Hibernate support (#2147)

* Hibernate support

---------

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2024-12-16 16:08:07 +02:00
committed by GitHub
parent af50e8c938
commit db3ac7f2dd
51 changed files with 1397 additions and 704 deletions

46
.github/workflows/verify-hibernate.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Verify (Hibernate)
on:
push:
branches:
- master
paths-ignore:
- '.3rd-party/**'
- 'site/**'
- '**.md'
pull_request:
paths-ignore:
- '.3rd-party/**'
- 'site/**'
- '**.md'
jobs:
verify-hibernate:
runs-on: ubuntu-latest
services:
rabbitmq:
image: rabbitmq:3-management-alpine
env:
RABBITMQ_DEFAULT_VHOST: /
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
ports:
- 15672:15672
- 5672:5672
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 17
cache: 'maven'
- name: Check file license headers
run: mvn license:check --batch-mode
- name: Run tests & javadoc
run: mvn verify javadoc:javadoc --batch-mode -Djpa.vendor=hibernate -Dlogging.level.org.hibernate.collection.spi.AbstractPersistentCollection=ERROR

View File

@@ -131,8 +131,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
}
@Override
public ResponseEntity<MgmtDistributionSet> getDistributionSet(
final Long distributionSetId) {
public ResponseEntity<MgmtDistributionSet> getDistributionSet(final Long distributionSetId) {
final DistributionSet foundDs = distributionSetManagement.getOrElseThrowException(distributionSetId);
final MgmtDistributionSet response = MgmtDistributionSetMapper.toResponse(foundDs);
@@ -142,8 +141,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
}
@Override
public ResponseEntity<List<MgmtDistributionSet>> createDistributionSets(
final List<MgmtDistributionSetRequestBodyPost> sets) {
public ResponseEntity<List<MgmtDistributionSet>> createDistributionSets(final List<MgmtDistributionSetRequestBodyPost> sets) {
log.debug("creating {} distribution sets", sets.size());
// set default Ds type if ds type is null
final String defaultDsKey = systemSecurityContext

View File

@@ -746,14 +746,4 @@ class MgmtDistributionSetTypeResourceTest extends AbstractManagementApiIntegrati
assertThat(testType.getMandatoryModuleTypes()).containsExactly(osType);
return testType;
}
private void createSoftwareModulesAlphabetical(final int amount) {
char character = 'a';
for (int index = 0; index < amount; index++) {
final String str = String.valueOf(character);
softwareModuleManagement.create(
entityFactory.softwareModule().create().name(str).description(str).vendor(str).version(str));
character++;
}
}
}

View File

@@ -0,0 +1,3 @@
# hawkBit JPA EclipseLink Vendor integration
Implementation of [EclipseLink](http://www.eclipse.org/eclipselink/) JPA vendor.

View File

@@ -0,0 +1,54 @@
<!--
Copyright (c) 2015 Bosch Software Innovations GmbH and others
This program and the accompanying materials are made
available under the terms of the Eclipse Public License 2.0
which is available at https://www.eclipse.org/legal/epl-2.0/
SPDX-License-Identifier: EPL-2.0
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.hawkbit</groupId>
<version>${revision}</version>
<artifactId>hawkbit-repository</artifactId>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>hawkbit-repository-jpa-api</artifactId>
<name>hawkBit :: Repository :: JPA API</name>
<properties>
<apt.source.dir>${project.build.directory}/generated-sources/apt/</apt.source.dir>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<exclusions>
<exclusion>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Static class generation -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -45,7 +45,7 @@ public class AfterTransactionCommitDefaultServiceExecutor implements AfterTransa
@Override
@SuppressWarnings({ "squid:S1217" })
public void afterCompletion(final int status) {
log.debug("Transaction completed after commit with status {}", status == STATUS_COMMITTED ? "COMMITTED" : "ROLLEDBACK");
log.debug("Transaction completed after commit with status {}", status == TransactionSynchronization.STATUS_COMMITTED ? "COMMITTED" : "ROLLEDBACK");
}
private void afterCommit(final Runnable runnable) {

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.model;
import java.io.Serializable;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PostPersist;
import jakarta.persistence.PostRemove;
import jakarta.persistence.PostUpdate;
import org.eclipse.hawkbit.repository.jpa.model.helper.AfterTransactionCommitExecutorHolder;
import org.eclipse.hawkbit.repository.model.BaseEntity;
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.security.core.context.SecurityContextHolder;
/**
* Core information of all entities.
*/
@MappedSuperclass
@EntityListeners({ AuditingEntityListener.class, EntityInterceptorListener.class })
public abstract class AbstractBaseEntity implements BaseEntity, Serializable {
/**
* Defined equals/hashcode strategy for the repository in general is that an entity is equal if it has the same {@link #getId()} and
* {@link #getOptLockRevision()} and class.
*/
@Override
// Exception squid:S864 - generated code
@SuppressWarnings({ "squid:S864" })
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (getId() == null ? 0 : getId().hashCode());
result = prime * result + getOptLockRevision();
result = prime * result + getClass().getName().hashCode();
return result;
}
/**
* Defined equals/hashcode strategy for the repository in general is that an entity is equal if it has the same {@link #getId()} and
* {@link #getOptLockRevision()} and class.
*/
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!getClass().isInstance(obj)) {
return false;
}
final BaseEntity other = (BaseEntity) obj;
final Long id = getId();
final Long otherId = other.getId();
if (id == null) {
if (otherId != null) {
return false;
}
} else if (!id.equals(otherId)) {
return false;
}
return getOptLockRevision() == other.getOptLockRevision();
}
@Override
public String toString() {
return getClass().getSimpleName() + " [id=" + getId() + "]";
}
@PostPersist
public void postInsert() {
if (this instanceof EventAwareEntity eventAwareEntity) {
doNotify(eventAwareEntity::fireCreateEvent);
}
}
@PostUpdate
public void postUpdate() {
if (this instanceof EventAwareEntity eventAwareEntity) {
doNotify(eventAwareEntity::fireUpdateEvent);
}
}
@PostRemove
public void postDelete() {
if (this instanceof EventAwareEntity eventAwareEntity) {
doNotify(eventAwareEntity::fireDeleteEvent);
}
}
protected static void doNotify(final Runnable runnable) {
// fire events onl AFTER transaction commit
AfterTransactionCommitExecutorHolder.getInstance().getAfterCommit().afterCommit(runnable);
}
protected boolean isController() {
return SecurityContextHolder.getContext().getAuthentication() != null
&& SecurityContextHolder.getContext().getAuthentication()
.getDetails() instanceof TenantAwareAuthenticationDetails tenantAwareDetails
&& tenantAwareDetails.isController();
}
}

View File

@@ -0,0 +1,3 @@
# hawkBit JPA EclipseLink Vendor integration
Implementation of [EclipseLink](http://www.eclipse.org/eclipselink/) JPA vendor.

View File

@@ -0,0 +1,49 @@
<!--
Copyright (c) 2015 Bosch Software Innovations GmbH and others
This program and the accompanying materials are made
available under the terms of the Eclipse Public License 2.0
which is available at https://www.eclipse.org/legal/epl-2.0/
SPDX-License-Identifier: EPL-2.0
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.hawkbit</groupId>
<version>${revision}</version>
<artifactId>hawkbit-repository</artifactId>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>hawkbit-repository-jpa-eclipselink</artifactId>
<name>hawkBit :: Repository :: JPA EclipseLink Vendor</name>
<properties>
<apt.source.dir>${project.build.directory}/generated-sources/apt/</apt.source.dir>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-jpa-api</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Static class generation -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.jpa</artifactId>
<version>${eclipselink.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -49,7 +49,7 @@ import org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect;
* 3.b.) the cause is not an {@link SQLException} and as a result cannot be
* mapped.
*/
public class HawkbitEclipseLinkJpaDialect extends EclipseLinkJpaDialect {
class HawkbitEclipseLinkJpaDialect extends EclipseLinkJpaDialect {
@Serial
private static final long serialVersionUID = 1L;
@@ -99,4 +99,4 @@ public class HawkbitEclipseLinkJpaDialect extends EclipseLinkJpaDialect {
return null;
}
}
}

View File

@@ -16,40 +16,35 @@ import java.util.stream.IntStream;
import jakarta.persistence.Query;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
@Slf4j
public class Jpa {
public enum JpaVendor {
ECLIPSELINK,
HIBERNATE // NOT SUPPORTED!
HIBERNATE
}
public static final JpaVendor JPA_VENDOR = JpaVendor.ECLIPSELINK;
static {
log.info("JPA vendor: {}", JPA_VENDOR);
}
public static char NATIVE_QUERY_PARAMETER_PREFIX = switch (JPA_VENDOR) {
case ECLIPSELINK -> '?';
case HIBERNATE -> ':';
};
public static final char NATIVE_QUERY_PARAMETER_PREFIX = '?';
public static <T> String formatNativeQueryInClause(final String name, final List<T> list) {
return switch (Jpa.JPA_VENDOR) {
case ECLIPSELINK -> formatEclipseLinkNativeQueryInClause(IntStream.range(0, list.size()).mapToObj(i -> name + "_" + i).toList());
case HIBERNATE -> ":" + name;
};
return formatEclipseLinkNativeQueryInClause(IntStream.range(0, list.size()).mapToObj(i -> name + "_" + i).toList());
}
public static <T> void setNativeQueryInParameter(final Query deleteQuery, final String name, final List<T> list) {
if (Jpa.JPA_VENDOR == Jpa.JpaVendor.ECLIPSELINK) {
for (int i = 0, len = list.size(); i < len; i++) {
deleteQuery.setParameter(name + "_" + i, list.get(i));
}
} else if (Jpa.JPA_VENDOR == Jpa.JpaVendor.HIBERNATE) {
deleteQuery.setParameter(name, list);
for (int i = 0, len = list.size(); i < len; i++) {
deleteQuery.setParameter(name + "_" + i, list.get(i));
}
}
private static String formatEclipseLinkNativeQueryInClause(final Collection<String> elements) {
return "?" + String.join(",?", elements);
}
}
}

View File

@@ -0,0 +1,86 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter;
import org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect;
import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.jta.JtaTransactionManager;
/**
* General EclipseLink configuration for hawkBit's Repository.
*/
@Configuration
public class JpaConfiguration extends JpaBaseConfiguration {
protected JpaConfiguration(
final DataSource dataSource, final JpaProperties properties,
final ObjectProvider<JtaTransactionManager> jtaTransactionManagerProvider) {
super(dataSource, properties, jtaTransactionManagerProvider);
}
/**
* {@link MultiTenantJpaTransactionManager} bean.
*
* @return a new {@link PlatformTransactionManager}
* @see org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration#transactionManager(ObjectProvider)
*/
@Override
@Bean
public PlatformTransactionManager transactionManager(final ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
return new MultiTenantJpaTransactionManager();
}
@Override
protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
return new EclipseLinkJpaVendorAdapter() {
private final HawkbitEclipseLinkJpaDialect jpaDialect = new HawkbitEclipseLinkJpaDialect();
@Override
public EclipseLinkJpaDialect getJpaDialect() {
return jpaDialect;
}
};
}
@Override
protected Map<String, Object> getVendorProperties() {
final Map<String, Object> properties = new HashMap<>(7);
// Turn off dynamic weaving to disable LTW lookup in static weaving mode
properties.put(PersistenceUnitProperties.WEAVING, "false");
// needed for reports
properties.put(PersistenceUnitProperties.ALLOW_NATIVE_SQL_QUERIES, "true");
// flyway
properties.put(PersistenceUnitProperties.DDL_GENERATION, "none");
// Embed into hawkBit logging
properties.put(PersistenceUnitProperties.LOGGING_LOGGER, "JavaLogger");
// Ensure that we flush only at the end of the transaction
properties.put(PersistenceUnitProperties.PERSISTENCE_CONTEXT_FLUSH_MODE, "COMMIT");
// Enable batch writing
properties.put(PersistenceUnitProperties.BATCH_WRITING, "JDBC");
// Batch size
properties.put(PersistenceUnitProperties.BATCH_WRITING_SIZE, "500");
return properties;
}
}

View File

@@ -0,0 +1,91 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa;
import java.io.Serial;
import java.util.Objects;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transaction;
import org.eclipse.hawkbit.repository.jpa.model.EntityPropertyChangeListener;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.DescriptorEventManager;
import org.eclipse.persistence.sessions.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.jpa.EntityManagerHolder;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* {@link JpaTransactionManager} that sets the {@link TenantAware#getCurrentTenant()} in the eclipselink session. This has
* to be done in eclipselink after a {@link Transaction} has been started.
* <p/>
* The class also handles setting the {@link EntityPropertyChangeListener} to the {@link DescriptorEventManager} of the
*/
class MultiTenantJpaTransactionManager extends JpaTransactionManager {
@Serial
private static final long serialVersionUID = 1L;
@Autowired
private transient TenantAware tenantAware;
private static final Class<?> JPA_TARGET;
static {
try {
JPA_TARGET = Class.forName("org.eclipse.hawkbit.repository.jpa.model.JpaTarget");
} catch (final RuntimeException e) {
throw e;
} catch (final Exception e) {
throw new IllegalStateException(e);
}
}
private static final EntityPropertyChangeListener ENTITY_PROPERTY_CHANGE_LISTENER = new EntityPropertyChangeListener();
@Override
protected void doBegin(final Object transaction, final TransactionDefinition definition) {
super.doBegin(transaction, definition);
final EntityManager em = Objects.requireNonNull(
(EntityManagerHolder) TransactionSynchronizationManager.getResource(
Objects.requireNonNull(
getEntityManagerFactory(),
"No EntityManagerFactory provided by TransactionSynchronizationManager")),
"No EntityManagerHolder provided by TransactionSynchronizationManager")
.getEntityManager();
final ClassDescriptor classDescriptor = em.unwrap(Session.class).getClassDescriptor(JPA_TARGET);
if (classDescriptor != null) {
final DescriptorEventManager dem = classDescriptor.getEventManager();
if (dem != null && !dem.getEventListeners().contains(ENTITY_PROPERTY_CHANGE_LISTENER)) {
dem.addListener(ENTITY_PROPERTY_CHANGE_LISTENER);
}
}
final String currentTenant = tenantAware.getCurrentTenant();
if (currentTenant == null) {
cleanupTenant(em);
} else {
em.setProperty(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, currentTenant.toUpperCase());
}
}
private void cleanupTenant(final EntityManager em) {
if (em.isOpen() && em.getProperties().containsKey(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT)) {
em.setProperty(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, "");
}
}
}

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.model;
import java.io.Serial;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Version;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
/**
* Base hawkBit entity class containing the common attributes for EclipseLink.
*/
@NoArgsConstructor(access = AccessLevel.PROTECTED) // Default constructor needed for JPA entities.
@MappedSuperclass
// exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities
@SuppressWarnings("squid:S2160")
public abstract class AbstractJpaBaseEntity extends AbstractBaseEntity {
protected static final int USERNAME_FIELD_LENGTH = 64;
@Serial
private static final long serialVersionUID = 1L;
@Setter // should be used just for test purposes
@Getter
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Setter // should be used just for test purposes
@Getter
@Version
@Column(name = "optlock_revision")
private int optLockRevision;
// Audit fields. use property access to ensure that setters will be called and checked for modification
// (touch implementation depends on setLastModifiedAt(1).
@Column(name = "created_by", updatable = false, nullable = false, length = USERNAME_FIELD_LENGTH)
private String createdBy;
@Column(name = "created_at", updatable = false, nullable = false)
private long createdAt;
@Column(name = "last_modified_by", nullable = false, length = USERNAME_FIELD_LENGTH)
private String lastModifiedBy;
@Column(name = "last_modified_at", nullable = false)
private long lastModifiedAt;
@CreatedBy
public void setCreatedBy(final String createdBy) {
this.createdBy = createdBy;
}
@Access(AccessType.PROPERTY)
public String getCreatedBy() {
return createdBy;
}
@CreatedDate
public void setCreatedAt(final long createdAt) {
this.createdAt = createdAt;
}
@Access(AccessType.PROPERTY)
public long getCreatedAt() {
return createdAt;
}
@LastModifiedBy
public void setLastModifiedBy(final String lastModifiedBy) {
if (this.lastModifiedBy != null && isController()) {
// initialized and controller = doesn't update
return;
}
this.lastModifiedBy = lastModifiedBy;
}
@Access(AccessType.PROPERTY)
public String getLastModifiedBy() {
return lastModifiedBy == null ? createdBy : lastModifiedBy;
}
@LastModifiedDate
public void setLastModifiedAt(final long lastModifiedAt) {
if (this.lastModifiedAt != 0 && isController()) {
// initialized and controller = doesn't update
return;
}
this.lastModifiedAt = lastModifiedAt;
}
@Access(AccessType.PROPERTY)
public long getLastModifiedAt() {
return lastModifiedAt == 0 ? createdAt : lastModifiedAt;
}
}

View File

@@ -55,10 +55,7 @@ public abstract class AbstractJpaTenantAwareBaseEntity extends AbstractJpaBaseEn
*/
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + (tenant == null ? 0 : tenant.hashCode());
return result;
return 31 * super.hashCode() + (tenant == null ? 0 : tenant.hashCode());
}
/**
@@ -73,13 +70,12 @@ public abstract class AbstractJpaTenantAwareBaseEntity extends AbstractJpaBaseEn
if (!super.equals(obj)) {
return false;
}
final AbstractJpaTenantAwareBaseEntity other = (AbstractJpaTenantAwareBaseEntity) obj;
return Objects.equals(getTenant(), other.getTenant());
return Objects.equals(getTenant(), ((AbstractJpaTenantAwareBaseEntity) obj).getTenant());
}
@Override
public String toString() {
return "BaseEntity [id=" + super.getId() + "]";
return getClass().getSimpleName() + " [tenant=" + getTenant() + ", id=" + getId() + "]";
}
/**

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.model;
import java.util.List;
import org.eclipse.persistence.descriptors.DescriptorEvent;
import org.eclipse.persistence.descriptors.DescriptorEventAdapter;
import org.eclipse.persistence.queries.UpdateObjectQuery;
/**
* Listens to updates on <code></code>JpaTarget</code> entities, filtering out updates that only change the
* "lastTargetQuery" or "address" fields.
*/
public class EntityPropertyChangeListener extends DescriptorEventAdapter {
private static final List<String> TARGET_UPDATE_EVENT_IGNORE_FIELDS = List.of(
"lastTargetQuery", "address", // actual to be skipped
"optLockRevision", "lastModifiedAt", "lastModifiedBy" // system to be skipped
);
@Override
public void postUpdate(final DescriptorEvent event) {
final Object object = event.getObject();
if (((UpdateObjectQuery) event.getQuery()).getObjectChangeSet().getChangedAttributeNames().stream()
.anyMatch(field -> !TARGET_UPDATE_EVENT_IGNORE_FIELDS.contains(field))) {
AbstractJpaBaseEntity.doNotify(() -> ((EventAwareEntity) object).fireUpdateEvent());
}
}
}

View File

@@ -30,22 +30,21 @@ import org.springframework.dao.UncategorizedDataAccessException;
*/
@Feature("Unit Tests - Repository")
@Story("Exception handling")
public class HawkBitEclipseLinkJpaDialectTest {
class HawkBitEclipseLinkJpaDialectTest {
private final HawkbitEclipseLinkJpaDialect hawkBitEclipseLinkJpaDialectUnderTest = new HawkbitEclipseLinkJpaDialect();
@Test
@Description("Use Case: PersistenceException that can be mapped by EclipseLinkJpaDialect into corresponding DataAccessException.")
public void jpaOptimisticLockExceptionIsConcurrencyFailureException() {
assertThat(
hawkBitEclipseLinkJpaDialectUnderTest.translateExceptionIfPossible(mock(OptimisticLockException.class)))
void jpaOptimisticLockExceptionIsConcurrencyFailureException() {
assertThat(hawkBitEclipseLinkJpaDialectUnderTest.translateExceptionIfPossible(mock(OptimisticLockException.class)))
.isInstanceOf(ConcurrencyFailureException.class);
}
@Test
@Description("Use Case: PersistenceException that could not be mapped by EclipseLinkJpaDialect directly but "
+ "instead is wrapped into JpaSystemException. Cause of PersistenceException is an SQLException.")
public void jpaSystemExceptionWithSqlDeadLockExceptionIsConcurrencyFailureException() {
void jpaSystemExceptionWithSqlDeadLockExceptionIsConcurrencyFailureException() {
final PersistenceException persEception = mock(PersistenceException.class);
when(persEception.getCause()).thenReturn(new SQLException("simulated transaction ER_LOCK_DEADLOCK", "40001"));
@@ -56,7 +55,7 @@ public class HawkBitEclipseLinkJpaDialectTest {
@Test
@Description("Use Case: PersistenceException that could not be mapped by EclipseLinkJpaDialect directly but instead is wrapped"
+ " into JpaSystemException. Cause of PersistenceException is not an SQLException.")
public void jpaSystemExceptionWithNumberFormatExceptionIsNull() {
void jpaSystemExceptionWithNumberFormatExceptionIsNull() {
final PersistenceException persEception = mock(PersistenceException.class);
when(persEception.getCause()).thenReturn(new NumberFormatException());
@@ -67,7 +66,7 @@ public class HawkBitEclipseLinkJpaDialectTest {
@Test
@Description("Use Case: RuntimeException that could not be mapped by EclipseLinkJpaDialect directly. Cause of "
+ "RuntimeException is an SQLException.")
public void runtimeExceptionWithSqlDeadLockExceptionIsConcurrencyFailureException() {
void runtimeExceptionWithSqlDeadLockExceptionIsConcurrencyFailureException() {
final RuntimeException persEception = mock(RuntimeException.class);
when(persEception.getCause()).thenReturn(new SQLException("simulated transaction ER_LOCK_DEADLOCK", "40001"));
@@ -78,11 +77,10 @@ public class HawkBitEclipseLinkJpaDialectTest {
@Test
@Description("Use Case: RuntimeException that could not be mapped by EclipseLinkJpaDialect directly. Cause of "
+ "RuntimeException is not an SQLException.")
public void runtimeExceptionWithNumberFormatExceptionIsNull() {
void runtimeExceptionWithNumberFormatExceptionIsNull() {
final RuntimeException persEception = mock(RuntimeException.class);
when(persEception.getCause()).thenReturn(new NumberFormatException());
assertThat(hawkBitEclipseLinkJpaDialectUnderTest.translateExceptionIfPossible(persEception)).isNull();
}
}
}

View File

@@ -0,0 +1,25 @@
# hawkBit JPA Hibernate Vendor integration
Implementation of [Hibernate](https://hibernate.org/) JPA vendor.
To use this vendor you could exclude the org.eclipse.hawkbit:hawkbit.repository-jpa-eclipselink and include this module.
For instance if you use org.eclipse.hawkbit:hawkbit-repository-jpa via org.eclipse.hawkbit:hawkbit-starter you could do it like this:
```xml
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-starter</artifactId>
<version>${project.version}</version>
<exclusions>
<exclusion>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-jpa-eclipselink</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-jpa-hibernate</artifactId>
<version>${project.version}</version>
</dependency>
```

View File

@@ -0,0 +1,46 @@
<!--
Copyright (c) 2015 Bosch Software Innovations GmbH and others
This program and the accompanying materials are made
available under the terms of the Eclipse Public License 2.0
which is available at https://www.eclipse.org/legal/epl-2.0/
SPDX-License-Identifier: EPL-2.0
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.hawkbit</groupId>
<version>${revision}</version>
<artifactId>hawkbit-repository</artifactId>
</parent>
<artifactId>hawkbit-repository-jpa-hibernate</artifactId>
<name>hawkBit :: Repository :: JPA Hibernate Vendor</name>
<properties>
<apt.source.dir>${project.build.directory}/generated-sources/apt/</apt.source.dir>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-jpa-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
</dependency>
<!-- Static class generation -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2024 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa;
import java.util.List;
import jakarta.persistence.Query;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
@Slf4j
public class Jpa {
public enum JpaVendor {
ECLIPSELINK,
HIBERNATE
}
public static final JpaVendor JPA_VENDOR = JpaVendor.HIBERNATE;
static {
log.info("JPA vendor: {}", JPA_VENDOR);
}
public static final char NATIVE_QUERY_PARAMETER_PREFIX = ':';
public static <T> String formatNativeQueryInClause(final String name, final List<T> list) {
return ":" + name;
}
public static <T> void setNativeQueryInParameter(final Query deleteQuery, final String name, final List<T> list) {
deleteQuery.setParameter(name, list);
}
}

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.eclipse.hawkbit.repository.jpa.model.EntityPropertyChangeListener;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.spi.BootstrapContext;
import org.hibernate.cfg.MultiTenancySettings;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.integrator.spi.Integrator;
import org.hibernate.jpa.boot.spi.IntegratorProvider;
import org.hibernate.service.spi.SessionFactoryServiceRegistry;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.jta.JtaTransactionManager;
/**
* General Hibernate configuration for hawkBit's Repository.
*/
@Configuration
public class JpaConfiguration extends JpaBaseConfiguration {
private final TenantIdentifier tenantIdentifier;
protected JpaConfiguration(
final DataSource dataSource, final JpaProperties properties,
final ObjectProvider<JtaTransactionManager> jtaTransactionManagerProvider,
final TenantAware tenantAware) {
super(dataSource, properties, jtaTransactionManagerProvider);
tenantIdentifier = new TenantIdentifier(tenantAware);
}
@Bean
CurrentTenantIdentifierResolver<String> currentTenantIdentifierResolver() {
return tenantIdentifier;
}
@Override
protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
return new HibernateJpaVendorAdapter() {
private final HibernateJpaDialect jpaDialect = new HibernateJpaDialect();
@Override
public HibernateJpaDialect getJpaDialect() {
return jpaDialect;
}
};
}
@Override
protected Map<String, Object> getVendorProperties() {
final Map<String, Object> properties = new HashMap<>(4);
properties.put(MultiTenancySettings.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifier);
properties.put("hibernate.multiTenancy", "DISCRIMINATOR");
// LAZY_LOAD - Enable lazy loading of lazy fields when session is closed - N + 1 problem occur.
// So it would be good if in future hawkBit run without that
// Otherwise, if false, call for the lazy field will throw LazyInitializationException
properties.put("hibernate.enable_lazy_load_no_trans", "true");
properties.put("hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList(new Integrator() {
@Override
public void integrate(
final Metadata metadata, final BootstrapContext bootstrapContext,
final SessionFactoryImplementor sessionFactory) {
sessionFactory.getServiceRegistry()
.getService(EventListenerRegistry.class)
.appendListeners(EventType.POST_UPDATE, new EntityPropertyChangeListener());
}
@Override
public void disintegrate(final SessionFactoryImplementor sessionFactory, final SessionFactoryServiceRegistry serviceRegistry) {
// do nothing
}
}));
return properties;
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2024 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa;
import java.util.Optional;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
/**
* {@link CurrentTenantIdentifierResolver} and {@link HibernatePropertiesCustomizer} that resolves the
* {@link TenantAware#getCurrentTenant()} for hibernate.
*/
class TenantIdentifier implements CurrentTenantIdentifierResolver<String> {
private final TenantAware tenantAware;
TenantIdentifier(final TenantAware tenantAware) {
this.tenantAware = tenantAware;
}
@Override
public String resolveCurrentTenantIdentifier() {
// on bootstrapping hibernate requests tenant and want to be non-null
return Optional.ofNullable(tenantAware.getCurrentTenant()).map(String::toUpperCase).orElse("");
}
@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.model;
import java.io.Serial;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Version;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
/**
* Base hawkBit entity class containing the common attributes for Hibernate.
*/
@NoArgsConstructor(access = AccessLevel.PROTECTED) // Default constructor needed for JPA entities.
@MappedSuperclass
// exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities
@SuppressWarnings("squid:S2160")
public abstract class AbstractJpaBaseEntity extends AbstractBaseEntity {
protected static final int USERNAME_FIELD_LENGTH = 64;
@Serial
private static final long serialVersionUID = 1L;
@Setter // should be used just for test purposes
@Getter
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Setter // should be used just for test purposes
@Getter
@Version
@Column(name = "optlock_revision")
private int optLockRevision;
// Audit fields. use property access to ensure that setters will be called and checked for modification
// (touch implementation depends on setLastModifiedAt(1).
private String createdBy;
private long createdAt;
private String lastModifiedBy;
private long lastModifiedAt;
@CreatedBy
public void setCreatedBy(final String createdBy) {
this.createdBy = createdBy;
}
@Column(name = "created_by", updatable = false, nullable = false, length = USERNAME_FIELD_LENGTH)
@Access(AccessType.PROPERTY)
public String getCreatedBy() {
return createdBy;
}
@CreatedDate
public void setCreatedAt(final long createdAt) {
this.createdAt = createdAt;
}
@Column(name = "created_at", updatable = false, nullable = false)
@Access(AccessType.PROPERTY)
public long getCreatedAt() {
return createdAt;
}
@LastModifiedBy
public void setLastModifiedBy(final String lastModifiedBy) {
if (this.lastModifiedBy != null && isController()) {
// initialized and controller = doesn't update
return;
}
this.lastModifiedBy = lastModifiedBy;
}
@Column(name = "last_modified_by", nullable = false, length = USERNAME_FIELD_LENGTH)
@Access(AccessType.PROPERTY)
public String getLastModifiedBy() {
return lastModifiedBy == null ? createdBy : lastModifiedBy;
}
@LastModifiedDate
public void setLastModifiedAt(final long lastModifiedAt) {
if (this.lastModifiedAt != 0 && isController()) {
// initialized and controller = doesn't update
return;
}
this.lastModifiedAt = lastModifiedAt;
}
@Column(name = "last_modified_at", nullable = false)
@Access(AccessType.PROPERTY)
public long getLastModifiedAt() {
return lastModifiedAt == 0 ? createdAt : lastModifiedAt;
}
}

View File

@@ -0,0 +1,95 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.model;
import java.io.Serial;
import java.util.Objects;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.eclipse.hawkbit.repository.exception.TenantNotExistException;
import org.eclipse.hawkbit.repository.jpa.model.helper.TenantAwareHolder;
import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity;
import org.hibernate.annotations.TenantId;
/**
* Holder of the base attributes common to all tenant aware entities.
*/
@NoArgsConstructor(access = AccessLevel.PROTECTED) // Default constructor needed for JPA entities.
@Setter
@Getter
@MappedSuperclass
public abstract class AbstractJpaTenantAwareBaseEntity extends AbstractJpaBaseEntity implements TenantAwareBaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Column(name = "tenant", nullable = false, insertable = true, updatable = false, length = 40)
@Size(min = 1, max = 40)
@NotNull
@TenantId // Hibernate MultiTenant support
private String tenant;
/**
* Tenant aware entities extend the equals/hashcode strategy with the tenant name. That would allow for instance in a
* multi-schema based data separation setup to have the same primary key for different entities of different tenants.
*/
@Override
public int hashCode() {
return 31 * super.hashCode() + (tenant == null ? 0 : tenant.hashCode());
}
/**
* Tenant aware entities extend the equals/hashcode strategy with the tenant name. That would allow for instance in a
* multi-schema based data separation setup to have the same primary key for different entities of
* different tenants.
*/
@Override
// exception squid:S2259 - obj is checked for null in super
@SuppressWarnings("squid:S2259")
public boolean equals(final Object obj) {
if (!super.equals(obj)) {
return false;
}
return Objects.equals(getTenant(), ((AbstractJpaTenantAwareBaseEntity) obj).getTenant());
}
@Override
public String toString() {
return getClass().getSimpleName() + " [tenant=" + getTenant() + ", id=" + getId() + "]";
}
/**
* PrePersist listener method for all {@link TenantAwareBaseEntity} entities.
*
* // TODO - check if the tenant support should set tenant from context
* // TODO - should we check if tenant exists in the system? Note: seems it's not good to work with db in the listener
*/
@PrePersist
void prePersist() {
// before persisting the entity check the current ID of the tenant by using the TenantAware service
final String currentTenant = TenantAwareHolder.getInstance().getTenantAware().getCurrentTenant();
if (currentTenant == null) {
throw new TenantNotExistException(
String.format(
"Tenant %s does not exists, cannot create entity %s with id %d",
TenantAwareHolder.getInstance().getTenantAware().getCurrentTenant(), getClass(), getId()));
}
setTenant(currentTenant.toUpperCase());
}
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.model;
import java.util.List;
import org.hibernate.event.spi.PostUpdateEvent;
import org.hibernate.event.spi.PostUpdateEventListener;
import org.hibernate.persister.entity.EntityPersister;
/**
* Listens to updates on <code></code>JpaTarget</code> entities, filtering out updates that only change the
* "lastTargetQuery" or "address" fields.
*/
public class EntityPropertyChangeListener implements PostUpdateEventListener {
private static final List<String> TARGET_UPDATE_EVENT_IGNORE_FIELDS = List.of(
"lastTargetQuery", "address", // actual to be skipped
"optLockRevision", "lastModifiedAt", "lastModifiedBy" // system to be skipped
);
private static final Class<?> JPA_TARGET;
static {
try {
JPA_TARGET = Class.forName("org.eclipse.hawkbit.repository.jpa.model.JpaTarget");
} catch (final RuntimeException e) {
throw e;
} catch (final Exception e) {
throw new IllegalStateException(e);
}
}
@Override
public void onPostUpdate(final PostUpdateEvent event) {
if (!JPA_TARGET.isAssignableFrom(event.getEntity().getClass())) {
// only target entity updates goes through here
return;
}
boolean lastTargetQueryChanged = false;
boolean hasNonIgnoredChanges = false;
for (int i : event.getDirtyProperties()) {
final String attribute = event.getPersister().getAttributeMapping(i).getAttributeName();
if ("lastTargetQuery".equals(attribute)) {
lastTargetQueryChanged = true;
} else if (!TARGET_UPDATE_EVENT_IGNORE_FIELDS.contains(attribute)) {
hasNonIgnoredChanges = true;
break;
}
}
if (hasNonIgnoredChanges || !lastTargetQueryChanged) {
AbstractJpaBaseEntity.doNotify(() -> ((EventAwareEntity) event.getEntity()).fireUpdateEvent());
}
}
@Override
public boolean requiresPostCommitHandling(final EntityPersister persister) {
return false;
}
}

View File

@@ -23,32 +23,65 @@
<properties>
<apt.source.dir>${project.build.directory}/generated-sources/apt/</apt.source.dir>
<jpa.vendor>eclipselink</jpa.vendor>
</properties>
<profiles>
<profile>
<id>eclipselink</id>
<activation>
<property>
<!-- default, if not set - eclipse link -->
<name>!jpa.vendor</name>
</property>
</activation>
<dependencies>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-jpa-eclipselink</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</profile>
<profile>
<id>hibernate</id>
<activation>
<property>
<name>jpa.vendor</name>
<value>hibernate</value>
</property>
</activation>
<dependencies>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-jpa-hibernate</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</profile>
</profiles>
<dependencies>
<!-- Hawkbit -->
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-api</artifactId>
<artifactId>hawkbit-repository-jpa-api</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Static class generation -->
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-core</artifactId>
<version>${project.version}</version>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
@@ -70,13 +103,6 @@
<artifactId>commons-collections4</artifactId>
</dependency>
<!-- Static class generation -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
@@ -90,30 +116,4 @@
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<!-- Should be placed here below 'eclipselink-maven-plugin' as the plugin should be executed -->
<!-- after the code generation and the order of the plugins in the pom is relevant for maven -->
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${apt.source.dir}</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -9,14 +9,10 @@
*/
package org.eclipse.hawkbit.repository.jpa;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import javax.sql.DataSource;
import jakarta.persistence.EntityManager;
import jakarta.validation.Validation;
@@ -81,7 +77,6 @@ import org.eclipse.hawkbit.repository.jpa.builder.JpaSoftwareModuleMetadataBuild
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetBuilder;
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetFilterQueryBuilder;
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetTypeBuilder;
import org.eclipse.hawkbit.repository.jpa.configuration.MultiTenantJpaTransactionManager;
import org.eclipse.hawkbit.repository.jpa.event.JpaEventEntityManager;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitDefaultServiceExecutor;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
@@ -166,10 +161,7 @@ import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver;
import org.eclipse.hawkbit.utils.TenantConfigHelper;
import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.hibernate.validator.BaseHibernateValidatorConfiguration;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
@@ -177,9 +169,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@@ -191,14 +181,10 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.integration.support.locks.LockRegistry;
import org.springframework.lang.NonNull;
import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter;
import org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect;
import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.jta.JtaTransactionManager;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
/**
@@ -213,15 +199,9 @@ import org.springframework.validation.beanvalidation.MethodValidationPostProcess
@EnableRetry
@EntityScan("org.eclipse.hawkbit.repository.jpa.model")
@PropertySource("classpath:/hawkbit-jpa-defaults.properties")
@Import({ RepositoryDefaultConfiguration.class, DataSourceAutoConfiguration.class,
SystemManagementCacheKeyGenerator.class })
@Import({ JpaConfiguration.class, RepositoryDefaultConfiguration.class, DataSourceAutoConfiguration.class, SystemManagementCacheKeyGenerator.class })
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
protected RepositoryApplicationConfiguration(final DataSource dataSource, final JpaProperties properties,
final ObjectProvider<JtaTransactionManager> jtaTransactionManagerProvider) {
super(dataSource, properties, jtaTransactionManagerProvider);
}
public class RepositoryApplicationConfiguration {
/**
* Defines the validation processor bean.
@@ -233,58 +213,14 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
final MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
// ValidatorFactory shall NOT be closed because after closing the generated Validator
// methods shall not be called - we need the validator in future
processor.setValidator(Validation.byDefaultProvider().configure()
.addProperty(BaseHibernateValidatorConfiguration.ALLOW_PARALLEL_METHODS_DEFINE_PARAMETER_CONSTRAINTS,"true")
.buildValidatorFactory().getValidator());
processor.setValidator(Validation.byDefaultProvider()
.configure()
.addProperty(org.hibernate.validator.BaseHibernateValidatorConfiguration.ALLOW_PARALLEL_METHODS_DEFINE_PARAMETER_CONSTRAINTS, "true")
.buildValidatorFactory()
.getValidator());
return processor;
}
/**
* {@link MultiTenantJpaTransactionManager} bean.
*
* @return a new {@link PlatformTransactionManager}
* @see org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration#transactionManager(ObjectProvider)
*/
@Override
@Bean
public PlatformTransactionManager transactionManager(
final ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
return new MultiTenantJpaTransactionManager();
}
@Override
protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
return new EclipseLinkJpaVendorAdapter() {
private final HawkbitEclipseLinkJpaDialect jpaDialect = new HawkbitEclipseLinkJpaDialect();
@Override
public EclipseLinkJpaDialect getJpaDialect() {
return jpaDialect;
}
};
}
@Override
protected Map<String, Object> getVendorProperties() {
final Map<String, Object> properties = new HashMap<>(7);
// Turn off dynamic weaving to disable LTW lookup in static weaving mode
properties.put(PersistenceUnitProperties.WEAVING, "false");
// needed for reports
properties.put(PersistenceUnitProperties.ALLOW_NATIVE_SQL_QUERIES, "true");
// flyway
properties.put(PersistenceUnitProperties.DDL_GENERATION, "none");
// Embed into hawkBit logging
properties.put(PersistenceUnitProperties.LOGGING_LOGGER, "JavaLogger");
// Ensure that we flush only at the end of the transaction
properties.put(PersistenceUnitProperties.PERSISTENCE_CONTEXT_FLUSH_MODE, "COMMIT");
// Enable batch writing
properties.put(PersistenceUnitProperties.BATCH_WRITING, "JDBC");
// Batch size
properties.put(PersistenceUnitProperties.BATCH_WRITING_SIZE, "500");
return properties;
}
@Bean
public BeanPostProcessor entityManagerBeanPostProcessor(
@Autowired(required = false) final AccessController<JpaArtifact> artifactAccessController,

View File

@@ -1,53 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.configuration;
import java.io.Serial;
import java.util.Objects;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.transaction.Transaction;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.jpa.EntityManagerHolder;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* {@link JpaTransactionManager} that sets the {@link TenantAware#getCurrentTenant()} in the eclipselink session. This has
* to be done in eclipselink after a {@link Transaction} has been started.
*/
public class MultiTenantJpaTransactionManager extends JpaTransactionManager {
@Serial
private static final long serialVersionUID = 1L;
@Autowired
private transient TenantAware tenantAware;
@Override
protected void doBegin(final Object transaction, final TransactionDefinition definition) {
super.doBegin(transaction, definition);
final String currentTenant = tenantAware.getCurrentTenant();
if (currentTenant != null) {
final EntityManagerFactory emFactory = Objects.requireNonNull(getEntityManagerFactory());
final EntityManagerHolder emHolder = Objects.requireNonNull(
(EntityManagerHolder) TransactionSynchronizationManager.getResource(emFactory),
"No EntityManagerHolder provided by TransactionSynchronizationManager");
final EntityManager em = emHolder.getEntityManager();
em.setProperty(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, currentTenant.toUpperCase());
}
}
}

View File

@@ -23,7 +23,6 @@ import org.eclipse.hawkbit.artifact.repository.HashNotMatchException;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.repository.ArtifactEncryptionService;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.QuotaManagement;
@@ -36,7 +35,6 @@ import org.eclipse.hawkbit.repository.exception.InvalidMD5HashException;
import org.eclipse.hawkbit.repository.exception.InvalidSHA1HashException;
import org.eclipse.hawkbit.repository.exception.InvalidSHA256HashException;
import org.eclipse.hawkbit.repository.jpa.EncryptionAwareDbArtifact;
import org.eclipse.hawkbit.repository.jpa.Jpa;
import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper;
import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
@@ -53,17 +51,13 @@ import org.eclipse.hawkbit.repository.model.Artifact;
import org.eclipse.hawkbit.repository.model.ArtifactUpload;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.validation.annotation.Validated;
/**

View File

@@ -36,7 +36,6 @@ import jakarta.validation.constraints.NotEmpty;
import org.apache.commons.collections4.ListUtils;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.FilterParams;
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.TargetFields;
import org.eclipse.hawkbit.repository.TargetManagement;
@@ -90,7 +89,6 @@ import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.retry.annotation.Backoff;

View File

@@ -1,213 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.model;
import java.io.Serial;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PostPersist;
import jakarta.persistence.PostRemove;
import jakarta.persistence.PostUpdate;
import jakarta.persistence.Version;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.eclipse.hawkbit.repository.jpa.model.helper.AfterTransactionCommitExecutorHolder;
import org.eclipse.hawkbit.repository.model.BaseEntity;
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.security.core.context.SecurityContextHolder;
/**
* Base hawkBit entity class containing the common attributes.
*/
@NoArgsConstructor(access = AccessLevel.PROTECTED) // Default constructor needed for JPA entities.
@MappedSuperclass
@EntityListeners({ AuditingEntityListener.class, EntityInterceptorListener.class })
public abstract class AbstractJpaBaseEntity implements BaseEntity {
protected static final int USERNAME_FIELD_LENGTH = 64;
@Serial
private static final long serialVersionUID = 1L;
@Setter // should be used just for test purposes
@Getter
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Setter // should be used just for test purposes
@Getter
@Version
@Column(name = "optlock_revision")
private int optLockRevision;
// Audit fields. use property access to ensure that setters will be called and checked for modification
// (touch implementation depends on setLastModifiedAt(1).
@Column(name = "created_by", updatable = false, nullable = false, length = USERNAME_FIELD_LENGTH)
private String createdBy;
@Column(name = "created_at", updatable = false, nullable = false)
private long createdAt;
@Column(name = "last_modified_by", nullable = false, length = USERNAME_FIELD_LENGTH)
private String lastModifiedBy;
@Column(name = "last_modified_at", nullable = false)
private long lastModifiedAt;
@CreatedBy
public void setCreatedBy(final String createdBy) {
this.createdBy = createdBy;
}
// maybe needed to have correct createdBy value in the database
@Access(AccessType.PROPERTY)
public String getCreatedBy() {
return createdBy;
}
@CreatedDate
public void setCreatedAt(final long createdAt) {
this.createdAt = createdAt;
}
// property access to make entity manager to detect touch
@Access(AccessType.PROPERTY)
public long getCreatedAt() {
return createdAt;
}
@LastModifiedBy
public void setLastModifiedBy(final String lastModifiedBy) {
if (this.lastModifiedBy != null && isController()) {
// initialized and controller = doesn't update
return;
}
this.lastModifiedBy = lastModifiedBy;
}
// property access to make entity manager to detect touch
@Access(AccessType.PROPERTY)
public String getLastModifiedBy() {
return lastModifiedBy == null ? createdBy : lastModifiedBy;
}
@LastModifiedDate
public void setLastModifiedAt(final long lastModifiedAt) {
if (this.lastModifiedAt != 0 && isController()) {
// initialized and controller = doesn't update
return;
}
this.lastModifiedAt = lastModifiedAt;
}
// property access to make entity manager to detect touch
@Access(AccessType.PROPERTY)
public long getLastModifiedAt() {
return lastModifiedAt == 0 ? createdAt : lastModifiedAt;
}
/**
* Defined equals/hashcode strategy for the repository in general is that an entity is equal if it has the same {@link #getId()} and
* {@link #getOptLockRevision()} and class.
*/
@Override
// Exception squid:S864 - generated code
@SuppressWarnings({ "squid:S864" })
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (id == null ? 0 : id.hashCode());
result = prime * result + optLockRevision;
result = prime * result + this.getClass().getName().hashCode();
return result;
}
/**
* Defined equals/hashcode strategy for the repository in general is that an entity is equal if it has the same {@link #getId()} and
* {@link #getOptLockRevision()} and class.
*/
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(this.getClass().isInstance(obj))) {
return false;
}
final AbstractJpaBaseEntity other = (AbstractJpaBaseEntity) obj;
final Long id = getId();
final Long otherId = other.getId();
if (id == null) {
if (otherId != null) {
return false;
}
} else if (!id.equals(otherId)) {
return false;
}
return getOptLockRevision() == other.getOptLockRevision();
}
@Override
public String toString() {
return this.getClass().getSimpleName() + " [id=" + id + "]";
}
@PostPersist
public void postInsert() {
if (this instanceof EventAwareEntity eventAwareEntity) {
doNotify(eventAwareEntity::fireCreateEvent);
}
}
@PostUpdate
public void postUpdate() {
if (this instanceof EventAwareEntity eventAwareEntity) {
doNotify(eventAwareEntity::fireUpdateEvent);
}
}
@PostRemove
public void postDelete() {
if (this instanceof EventAwareEntity eventAwareEntity) {
doNotify(eventAwareEntity::fireDeleteEvent);
}
}
protected static void doNotify(final Runnable runnable) {
// fire events onl AFTER transaction commit
AfterTransactionCommitExecutorHolder.getInstance().getAfterCommit().afterCommit(runnable);
}
private boolean isController() {
return SecurityContextHolder.getContext().getAuthentication() != null
&& SecurityContextHolder.getContext().getAuthentication()
.getDetails() instanceof TenantAwareAuthenticationDetails tenantAwareDetails
&& tenantAwareDetails.isController();
}
}

View File

@@ -19,8 +19,6 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode;
@@ -37,7 +35,6 @@ import jakarta.persistence.NamedEntityGraph;
import jakarta.persistence.NamedEntityGraphs;
import jakarta.persistence.NamedSubgraph;
import jakarta.persistence.OneToMany;
import jakarta.persistence.PostUpdate;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
@@ -56,7 +53,6 @@ import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
import org.springframework.data.annotation.CreatedDate;
/**
* JPA implementation of {@link Action}.

View File

@@ -16,7 +16,6 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import jakarta.persistence.CascadeType;
@@ -27,7 +26,6 @@ import jakarta.persistence.Convert;
import jakarta.persistence.Converter;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.Index;
@@ -67,11 +65,6 @@ import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder;
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.persistence.descriptors.DescriptorEvent;
import org.eclipse.persistence.descriptors.DescriptorEventAdapter;
import org.eclipse.persistence.queries.UpdateObjectQuery;
import org.hibernate.event.spi.PreUpdateEvent;
import org.hibernate.event.spi.PreUpdateEventListener;
/**
* JPA implementation of {@link Target}.
@@ -88,7 +81,6 @@ import org.hibernate.event.spi.PreUpdateEventListener;
uniqueConstraints = @UniqueConstraint(columnNames = { "controller_id", "tenant" }, name = "uk_tenant_controller_id"))
// exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities
@SuppressWarnings("squid:S2160")
@EntityListeners({ JpaTarget.EntityPropertyChangeListener.class }) // add listener to the listeners declared into suppers
@Slf4j
public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAwareEntity {
@@ -330,40 +322,4 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
), null);
}
}
/**
* Listens to updates on {@link JpaTarget} entities, Filtering out updates that only change the "lastTargetQuery" or "address" fields.
*/
public static class EntityPropertyChangeListener extends DescriptorEventAdapter implements PreUpdateEventListener {
private static final List<String> TARGET_UPDATE_EVENT_IGNORE_FIELDS = List.of(
"lastTargetQuery", "address", // actual to be skipped
"optLockRevision", "lastModifiedAt", "lastModifiedBy" // system to be skipped
);
@Override
public void postUpdate(final DescriptorEvent event) {
final Object object = event.getObject();
if (((UpdateObjectQuery) event.getQuery()).getObjectChangeSet().getChangedAttributeNames().stream()
.anyMatch(field -> !TARGET_UPDATE_EVENT_IGNORE_FIELDS.contains(field))) {
doNotify(() -> ((EventAwareEntity) object).fireUpdateEvent());
}
}
@Override
public boolean onPreUpdate(final PreUpdateEvent event) {
final Object[] oldState = event.getOldState();
final Object[] newState = event.getState();
for (int i = 0; i < newState.length; i++) {
if (!Objects.equals(oldState[i], newState[i])) {
final String attribute = event.getPersister().getAttributeMapping(i).getAttributeName();
if (!TARGET_UPDATE_EVENT_IGNORE_FIELDS.contains(attribute)) {
doNotify(() -> ((EventAwareEntity) event.getEntity()).fireUpdateEvent());
break;
}
}
}
return false;
}
}
}

View File

@@ -34,7 +34,6 @@ import org.eclipse.hawkbit.repository.rsql.SuggestToken;
import org.eclipse.hawkbit.repository.rsql.SuggestionContext;
import org.eclipse.hawkbit.repository.rsql.SyntaxErrorContext;
import org.eclipse.hawkbit.repository.rsql.ValidationOracleContext;
import org.eclipse.persistence.exceptions.ConversionException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.util.CollectionUtils;
@@ -75,8 +74,12 @@ public class RsqlParserValidationOracle implements RsqlValidationOracle {
} catch (final RSQLParameterUnsupportedFieldException | IllegalArgumentException ex) {
errorContext.setErrorMessage(getCustomMessage(ex.getMessage(), null));
log.trace("Illegal argument on parsing :", ex);
} catch (@SuppressWarnings("squid:S1166") final ConversionException | JpaSystemException e) {
} catch (final JpaSystemException e) {
// noop
} catch (final RuntimeException e) {
if (!"org.eclipse.persistence.exceptions.ConversionException".equals(e.getClass().getName())) {
throw e;
}
}
return context;
}

View File

@@ -37,8 +37,7 @@ class ActionTest extends AbstractJpaIntegrationTest {
@Test
@Description("Ensures that timeforced moded switch from soft to forces after defined timeframe.")
void timeforcedHitNewHasCodeIsGenerated() {
void timeForcedHitNewHasCodeIsGenerated() {
// current time + 1 seconds
final long sleepTime = 1000;
final long timeForceTimeAt = System.currentTimeMillis() + sleepTime;
@@ -48,8 +47,7 @@ class ActionTest extends AbstractJpaIntegrationTest {
assertThat(timeforcedAction.isForcedOrTimeForced()).isFalse();
// wait until timeforce time is hit
Awaitility.await().atMost(Duration.ofSeconds(2)).pollInterval(Duration.ofMillis(100))
.until(timeforcedAction::isForcedOrTimeForced);
Awaitility.await().atMost(Duration.ofSeconds(2)).pollInterval(Duration.ofMillis(100)).until(timeforcedAction::isForcedOrTimeForced);
}
@Test
@@ -57,12 +55,11 @@ class ActionTest extends AbstractJpaIntegrationTest {
void testActionTypeConvert() {
final long id = createAction().getId();
for (final ActionType actionType : ActionType.values()) {
final JpaAction action = actionRepository
.findById(id).orElseThrow(() -> new IllegalStateException("Action not found"));
final JpaAction action = actionRepository.findById(id).orElseThrow(() -> new IllegalStateException("Action not found"));
action.setActionType(actionType);
actionRepository.save(action);
assertThat(actionRepository.findById(id).orElseThrow(() -> new IllegalStateException("Action not found"))
.getActionType()).isEqualTo(actionType);
assertThat(actionRepository.findById(id).orElseThrow(() -> new IllegalStateException("Action not found")).getActionType())
.isEqualTo(actionType);
}
}
@@ -71,12 +68,11 @@ class ActionTest extends AbstractJpaIntegrationTest {
void testStatusConvert() {
final long id = createAction().getId();
for (final Status status : Status.values()) {
final JpaAction action = actionRepository
.findById(id).orElseThrow(() -> new IllegalStateException("Action not found"));
final JpaAction action = actionRepository.findById(id).orElseThrow(() -> new IllegalStateException("Action not found"));
action.setStatus(status);
actionRepository.save(action);
assertThat(actionRepository.findById(id).orElseThrow(() -> new IllegalStateException("Action not found"))
.getStatus()).isEqualTo(status);
assertThat(actionRepository.findById(id).orElseThrow(() -> new IllegalStateException("Action not found")).getStatus())
.isEqualTo(status);
}
}
@@ -113,4 +109,4 @@ class ActionTest extends AbstractJpaIntegrationTest {
action.setActive(true);
return actionRepository.save(action);
}
}
}

View File

@@ -16,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
@@ -36,7 +35,6 @@ import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.event.remote.SoftwareModuleDeletedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.exception.FileSizeQuotaExceededException;
@@ -47,7 +45,6 @@ import org.eclipse.hawkbit.repository.exception.InvalidSHA256HashException;
import org.eclipse.hawkbit.repository.exception.StorageQuotaExceededException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.RandomGeneratedInputStream;
import org.eclipse.hawkbit.repository.jpa.model.JpaArtifact;
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule;
import org.eclipse.hawkbit.repository.model.Artifact;
import org.eclipse.hawkbit.repository.model.ArtifactUpload;
@@ -73,53 +70,46 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
final SoftwareModule module = testdataFactory.createSoftwareModuleOs();
assertThat(artifactManagement.get(NOT_EXIST_IDL)).isNotPresent();
assertThat(artifactManagement.getByFilenameAndSoftwareModule(NOT_EXIST_ID, module.getId()).isPresent())
.isFalse();
assertThat(artifactManagement.getByFilenameAndSoftwareModule(NOT_EXIST_ID, module.getId())).isEmpty();
assertThat(artifactManagement.findFirstBySHA1(NOT_EXIST_ID)).isNotPresent();
assertThat(artifactManagement.loadArtifactBinary(NOT_EXIST_ID, module.getId(), module.isEncrypted()))
.isNotPresent();
assertThat(artifactManagement.loadArtifactBinary(NOT_EXIST_ID, module.getId(), module.isEncrypted())).isEmpty();
}
@Test
@Description("Verifies that management queries react as specfied on calls for non existing entities "
+ " by means of throwing EntityNotFoundException.")
@ExpectEvents({ @Expect(type = SoftwareModuleDeletedEvent.class, count = 0) })
void entityQueriesReferringToNotExistingEntitiesThrowsException() throws URISyntaxException {
@Description("Verifies that management queries react as specfied on calls for non existing entities by means of " +
"throwing EntityNotFoundException.")
@ExpectEvents()
void entityQueriesReferringToNotExistingEntitiesThrowsException() {
final String artifactData = "test";
final int artifactSize = artifactData.length();
verifyThrownExceptionBy(
() -> artifactManagement.create(new ArtifactUpload(IOUtils.toInputStream(artifactData, "UTF-8"),
NOT_EXIST_IDL, "xxx", null, null, null, false, null, artifactSize)),
"SoftwareModule");
() -> artifactManagement.create(new ArtifactUpload(
IOUtils.toInputStream(artifactData, "UTF-8"),
NOT_EXIST_IDL, "xxx", null, null, null, false, null, artifactSize)), "SoftwareModule");
verifyThrownExceptionBy(
() -> artifactManagement.create(new ArtifactUpload(IOUtils.toInputStream(artifactData, "UTF-8"),
NOT_EXIST_IDL, "xxx", null, null, null, false, null, artifactSize)),
"SoftwareModule");
() -> artifactManagement.create(new ArtifactUpload(
IOUtils.toInputStream(artifactData, "UTF-8"),
NOT_EXIST_IDL, "xxx", null, null, null, false, null, artifactSize)), "SoftwareModule");
verifyThrownExceptionBy(() -> artifactManagement.delete(NOT_EXIST_IDL), "Artifact");
verifyThrownExceptionBy(() -> artifactManagement.findBySoftwareModule(PAGE, NOT_EXIST_IDL), "SoftwareModule");
assertThat(artifactManagement.getByFilename(NOT_EXIST_ID).isPresent()).isFalse();
assertThat(artifactManagement.getByFilename(NOT_EXIST_ID)).isEmpty();
verifyThrownExceptionBy(() -> artifactManagement.getByFilenameAndSoftwareModule("xxx", NOT_EXIST_IDL),
"SoftwareModule");
verifyThrownExceptionBy(() -> artifactManagement.getByFilenameAndSoftwareModule("xxx", NOT_EXIST_IDL), "SoftwareModule");
}
@Test
@Description("Test if a local artifact can be created by API including metadata.")
void createArtifact() throws IOException {
// check baseline
assertThat(softwareModuleRepository.findAll()).hasSize(0);
assertThat(artifactRepository.findAll()).hasSize(0);
assertThat(softwareModuleRepository.findAll()).isEmpty();
assertThat(artifactRepository.findAll()).isEmpty();
final JpaSoftwareModule sm = softwareModuleRepository
.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
final JpaSoftwareModule sm2 = softwareModuleRepository
.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
final JpaSoftwareModule sm = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
final JpaSoftwareModule sm2 = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 3", "version 3"));
final int artifactSize = 5 * 1024;
@@ -129,49 +119,43 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
final InputStream inputStream2 = new ByteArrayInputStream(randomBytes);
final InputStream inputStream3 = new ByteArrayInputStream(randomBytes);
final InputStream inputStream4 = new ByteArrayInputStream(randomBytes)) {
final Artifact artifact1 = createArtifactForSoftwareModule("file1", sm.getId(), artifactSize, inputStream1);
createArtifactForSoftwareModule("file11", sm.getId(), artifactSize, inputStream2);
createArtifactForSoftwareModule("file12", sm.getId(), artifactSize, inputStream3);
final Artifact artifact2 = createArtifactForSoftwareModule("file2", sm2.getId(), artifactSize,
inputStream4);
final Artifact artifact2 = createArtifactForSoftwareModule("file2", sm2.getId(), artifactSize, inputStream4);
assertThat(artifact1).isInstanceOf(Artifact.class);
assertThat(artifact1.getSoftwareModule().getId()).isEqualTo(sm.getId());
assertThat(artifact2.getSoftwareModule().getId()).isEqualTo(sm2.getId());
assertThat(((JpaArtifact) artifact1).getFilename()).isEqualTo("file1");
assertThat(((JpaArtifact) artifact1).getSha1Hash()).isNotNull();
assertThat(artifact1.getFilename()).isEqualTo("file1");
assertThat(artifact1.getSha1Hash()).isNotNull();
assertThat(artifact1).isNotEqualTo(artifact2);
assertThat(((JpaArtifact) artifact1).getSha1Hash()).isEqualTo(((JpaArtifact) artifact2).getSha1Hash());
assertThat(artifact1.getSha1Hash()).isEqualTo(artifact2.getSha1Hash());
assertThat(artifactManagement.getByFilename("file1").get().getSha1Hash())
.isEqualTo(HashGeneratorUtils.generateSHA1(randomBytes));
assertThat(artifactManagement.getByFilename("file1").get().getMd5Hash())
.isEqualTo(HashGeneratorUtils.generateMD5(randomBytes));
assertThat(artifactManagement.getByFilename("file1").get().getSha256Hash())
.isEqualTo(HashGeneratorUtils.generateSHA256(randomBytes));
assertThat(artifactManagement.getByFilename("file1").get().getSha1Hash()).isEqualTo(HashGeneratorUtils.generateSHA1(randomBytes));
assertThat(artifactManagement.getByFilename("file1").get().getMd5Hash()).isEqualTo(HashGeneratorUtils.generateMD5(randomBytes));
assertThat(artifactManagement.getByFilename("file1").get().getSha256Hash()).isEqualTo(
HashGeneratorUtils.generateSHA256(randomBytes));
assertThat(artifactRepository.findAll()).hasSize(4);
assertThat(softwareModuleRepository.findAll()).hasSize(3);
assertThat(softwareModuleManagement.get(sm.getId()).get().getArtifacts()).hasSize(3);
}
}
@Test
@Description("Verifies that artifact management does not create artifacts with illegal filename.")
void entityQueryWithIllegalFilenameThrowsException() throws URISyntaxException {
void entityQueryWithIllegalFilenameThrowsException() {
final String illegalFilename = "<img src=ernw onerror=alert(1)>.xml";
final String artifactData = "test";
final int artifactSize = artifactData.length();
final long smID = softwareModuleRepository.save(new JpaSoftwareModule(osType, "smIllegalFilenameTest", "1.0"))
.getId();
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(
() -> artifactManagement.create(new ArtifactUpload(IOUtils.toInputStream(artifactData, "UTF-8"), smID,
illegalFilename, false, artifactSize)));
assertThat(softwareModuleManagement.get(smID).get().getArtifacts()).hasSize(0);
final long smID = softwareModuleRepository.save(new JpaSoftwareModule(osType, "smIllegalFilenameTest", "1.0")).getId();
final ArtifactUpload artifactUpload = new ArtifactUpload(
IOUtils.toInputStream(artifactData, "UTF-8"), smID, illegalFilename, false, artifactSize);
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> artifactManagement.create(artifactUpload));
assertThat(softwareModuleManagement.get(smID).get().getArtifacts()).isEmpty();
}
@Test
@@ -206,9 +190,7 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
@Test
@Description("Verifies that the quota specifying the maximum artifact storage is enforced (across software modules).")
void createArtifactsUntilStorageQuotaIsExceeded() throws IOException {
// create as many small artifacts as possible w/o violating the storage
// quota
// create as many small artifacts as possible w/o violating the storage quota
final long maxBytes = quotaManagement.getMaxArtifactStorage();
final List<Long> artifactIds = new ArrayList<>();
@@ -221,99 +203,78 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
}
// upload one more artifact to trigger the quota exceeded error
final JpaSoftwareModule sm = softwareModuleRepository
.save(new JpaSoftwareModule(osType, "smd" + numArtifacts, "1.0"));
final long smId = softwareModuleRepository.save(new JpaSoftwareModule(osType, "smd" + numArtifacts, "1.0")).getId();
assertThatExceptionOfType(StorageQuotaExceededException.class)
.isThrownBy(() -> createArtifactForSoftwareModule("file" + numArtifacts, sm.getId(), artifactSize));
.isThrownBy(() -> createArtifactForSoftwareModule("file" + numArtifacts, smId, artifactSize));
// delete one of the artifacts
artifactManagement.delete(artifactIds.get(0));
// now we should be able to create an artifact again
createArtifactForSoftwareModule("fileXYZ", sm.getId(), artifactSize);
createArtifactForSoftwareModule("fileXYZ", smId, artifactSize);
}
@Test
@Description("Verifies that you cannot create artifacts which exceed the configured maximum size.")
void createArtifactFailsIfTooLarge() {
// create a software module
final JpaSoftwareModule sm1 = softwareModuleRepository.save(new JpaSoftwareModule(osType, "sm1", "1.0"));
final long smId = softwareModuleRepository.save(new JpaSoftwareModule(osType, "sm1", "1.0")).getId();
// create an artifact that exceeds the configured quota
final long maxSize = quotaManagement.getMaxArtifactSize();
final int artifactSize = Math.toIntExact(quotaManagement.getMaxArtifactSize()) + 8;
assertThatExceptionOfType(FileSizeQuotaExceededException.class)
.isThrownBy(() -> createArtifactForSoftwareModule("file", sm1.getId(), Math.toIntExact(maxSize) + 8));
.isThrownBy(() -> createArtifactForSoftwareModule("file", smId, artifactSize));
}
@Test
@Description("Tests hard delete directly on repository.")
void hardDeleteSoftwareModule() throws IOException {
final JpaSoftwareModule sm = softwareModuleRepository
.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
final JpaSoftwareModule sm = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
createArtifactForSoftwareModule("file1", sm.getId(), 5 * 1024);
assertThat(artifactRepository.findAll()).hasSize(1);
softwareModuleRepository.deleteAll();
assertThat(artifactRepository.findAll()).hasSize(0);
assertThat(artifactRepository.findAll()).isEmpty();
}
/**
* Test method for
* {@link org.eclipse.hawkbit.repository.ArtifactManagement#delete(long)} .
*
* @throws IOException
* Test method for {@link org.eclipse.hawkbit.repository.ArtifactManagement#delete(long)}.
*/
@Test
@Description("Tests the deletion of a local artifact including metadata.")
void deleteArtifact() throws IOException {
final JpaSoftwareModule sm = softwareModuleRepository
.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
final JpaSoftwareModule sm2 = softwareModuleRepository
.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
final JpaSoftwareModule sm = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
final JpaSoftwareModule sm2 = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
assertThat(artifactRepository.findAll()).isEmpty();
final int artifactSize = 5 * 1024;
try (final InputStream inputStream1 = new RandomGeneratedInputStream(artifactSize);
final InputStream inputStream2 = new RandomGeneratedInputStream(artifactSize)) {
final Artifact artifact1 = createArtifactForSoftwareModule("file1", sm.getId(), artifactSize, inputStream1);
final Artifact artifact2 = createArtifactForSoftwareModule("file2", sm2.getId(), artifactSize,
inputStream2);
final Artifact artifact2 = createArtifactForSoftwareModule("file2", sm2.getId(), artifactSize, inputStream2);
assertThat(artifactRepository.findAll()).hasSize(2);
assertThat(artifact1.getId()).isNotNull();
assertThat(artifact2.getId()).isNotNull();
assertThat(((JpaArtifact) artifact1).getSha1Hash()).isNotEqualTo(((JpaArtifact) artifact2).getSha1Hash());
assertThat(artifact1.getSha1Hash()).isNotEqualTo(artifact2.getSha1Hash());
assertThat(
binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact1.getSha1Hash()))
.isNotNull();
assertThat(
binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact2.getSha1Hash()))
.isNotNull();
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact1.getSha1Hash())).isNotNull();
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact2.getSha1Hash())).isNotNull();
artifactManagement.delete(artifact1.getId());
assertThat(artifactRepository.findAll()).hasSize(1);
assertThat(
binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact1.getSha1Hash()))
.isNull();
assertThat(
binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact2.getSha1Hash()))
.isNotNull();
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact1.getSha1Hash())).isNull();
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact2.getSha1Hash())).isNotNull();
artifactManagement.delete(artifact2.getId());
assertThat(
binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact2.getSha1Hash()))
.isNull();
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact2.getSha1Hash())).isNull();
assertThat(artifactRepository.findAll()).hasSize(0);
assertThat(artifactRepository.findAll()).isEmpty();
}
}
@@ -321,10 +282,8 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
@Description("Test the deletion of an artifact metadata where the binary is still linked to another metadata element. " +
"The expected result is that the metadata is deleted but the binary kept.")
void deleteDuplicateArtifacts() throws IOException {
final JpaSoftwareModule sm = softwareModuleRepository
.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
final JpaSoftwareModule sm2 = softwareModuleRepository
.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
final JpaSoftwareModule sm = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
final JpaSoftwareModule sm2 = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
final int artifactSize = 5 * 1024;
final byte[] randomBytes = randomBytes(artifactSize);
@@ -347,49 +306,44 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
artifactManagement.delete(artifact2.getId());
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact1.getSha1Hash())).isNull();
assertThat(artifactRepository.findAll()).hasSize(0);
assertThat(artifactRepository.findAll()).isEmpty();
}
}
@Test
@Description("Verifies that you cannot delete an artifact which exists with the same hash, in the same tenant and the SoftwareModule is not deleted .")
void deleteArtifactWithSameHashAndSoftwareModuleIsNotDeletedInSameTenants() throws IOException {
final JpaSoftwareModule sm = softwareModuleRepository
.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
final JpaSoftwareModule sm2 = softwareModuleRepository
.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
final JpaSoftwareModule sm = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
final JpaSoftwareModule sm2 = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
final int artifactSize = 5 * 1024;
final byte[] randomBytes = randomBytes(artifactSize);
try (final InputStream inputStream1 = new ByteArrayInputStream(randomBytes);
final InputStream inputStream2 = new ByteArrayInputStream(randomBytes)) {
final Artifact artifact1 = createArtifactForSoftwareModule("file1", sm.getId(), artifactSize, inputStream1);
final Artifact artifact2 = createArtifactForSoftwareModule("file2", sm2.getId(), artifactSize,
inputStream2);
final Artifact artifact2 = createArtifactForSoftwareModule("file2", sm2.getId(), artifactSize, inputStream2);
assertEqualFileContents(
artifactManagement.loadArtifactBinary(artifact2.getSha1Hash(), sm2.getId(), sm2.isEncrypted()),
randomBytes);
artifactManagement.loadArtifactBinary(artifact2.getSha1Hash(), sm2.getId(), sm2.isEncrypted()), randomBytes);
assertThat(artifactRepository.findAll()).hasSize(2);
assertThat(artifact1.getSha1Hash()).isEqualTo(artifact2.getSha1Hash());
assertThat(artifactRepository.countBySha1HashAndTenantAndSoftwareModuleDeletedIsFalse(
artifact1.getSha1Hash(), artifact1.getTenant())).isGreaterThan(1);
artifact1.getSha1Hash(), artifact1.getTenant()))
.isGreaterThan(1);
artifactRepository.deleteById(artifact1.getId());
assertThat(artifactRepository.findAll()).hasSize(1);
assertThat(artifactRepository.countBySha1HashAndTenantAndSoftwareModuleDeletedIsFalse(
artifact2.getSha1Hash(), artifact2.getTenant())).isLessThanOrEqualTo(1);
artifact2.getSha1Hash(), artifact2.getTenant()))
.isLessThanOrEqualTo(1);
artifactRepository.deleteById(artifact2.getId());
assertThat(artifactRepository.findAll()).hasSize(0);
assertThat(artifactRepository.findAll()).isEmpty();
}
}
@@ -427,7 +381,7 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
try (final InputStream inputStream = new RandomGeneratedInputStream(artifactSize)) {
final Artifact artifact = createArtifactForSoftwareModule(
"file1", testdataFactory.createSoftwareModuleOs().getId(), artifactSize, inputStream);
assertThat(artifactManagement.get(artifact.getId()).get()).isEqualTo(artifact);
assertThat(artifactManagement.get(artifact.getId())).contains(artifact);
}
}
@@ -440,8 +394,7 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
final SoftwareModule smOs = testdataFactory.createSoftwareModuleOs();
final Artifact artifact = createArtifactForSoftwareModule("file1", smOs.getId(), artifactSize, input);
assertEqualFileContents(
artifactManagement.loadArtifactBinary(artifact.getSha1Hash(), smOs.getId(), smOs.isEncrypted()),
randomBytes);
artifactManagement.loadArtifactBinary(artifact.getSha1Hash(), smOs.getId(), smOs.isEncrypted()), randomBytes);
}
}
@@ -492,24 +445,25 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
final DbArtifactHash artifactHashes = calcHashes(testData);
try (final InputStream inputStream = new ByteArrayInputStream(testData)) {
final ArtifactUpload artifactUploadWithInvalidSha1 = new ArtifactUpload(inputStream, sm.getId(),
"test-file", artifactHashes.getMd5(), "sha1", artifactHashes.getSha256(), false, null,
testData.length);
final ArtifactUpload artifactUploadWithInvalidSha1 = new ArtifactUpload(
inputStream, sm.getId(), "test-file",
artifactHashes.getMd5(), "sha1", artifactHashes.getSha256(), false, null, testData.length);
assertThatExceptionOfType(InvalidSHA1HashException.class)
.isThrownBy(() -> artifactManagement.create(artifactUploadWithInvalidSha1));
}
try (final InputStream inputStream = new ByteArrayInputStream(testData)) {
final ArtifactUpload artifactUploadWithInvalidMd5 = new ArtifactUpload(inputStream, sm.getId(), "test-file",
final ArtifactUpload artifactUploadWithInvalidMd5 = new ArtifactUpload(
inputStream, sm.getId(), "test-file",
"md5", artifactHashes.getSha1(), artifactHashes.getSha256(), false, null, testData.length);
assertThatExceptionOfType(InvalidMD5HashException.class)
.isThrownBy(() -> artifactManagement.create(artifactUploadWithInvalidMd5));
}
try (final InputStream inputStream = new ByteArrayInputStream(testData)) {
final ArtifactUpload artifactUploadWithInvalidSha256 = new ArtifactUpload(inputStream, sm.getId(),
"test-file", artifactHashes.getMd5(), artifactHashes.getSha1(), "sha256", false, null,
testData.length);
final ArtifactUpload artifactUploadWithInvalidSha256 = new ArtifactUpload(
inputStream, sm.getId(), "test-file",
artifactHashes.getMd5(), artifactHashes.getSha1(), "sha256", false, null, testData.length);
assertThatExceptionOfType(InvalidSHA256HashException.class)
.isThrownBy(() -> artifactManagement.create(artifactUploadWithInvalidSha256));
}
@@ -524,17 +478,16 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
final DbArtifactHash artifactHashes = calcHashes(testData);
try (final InputStream inputStream = new ByteArrayInputStream(testData)) {
final ArtifactUpload artifactUpload = new ArtifactUpload(inputStream, sm.getId(), "test-file",
artifactHashes.getMd5(), artifactHashes.getSha1(), artifactHashes.getSha256(), false, null,
testData.length);
final ArtifactUpload artifactUpload = new ArtifactUpload(
inputStream, sm.getId(), "test-file",
artifactHashes.getMd5(), artifactHashes.getSha1(), artifactHashes.getSha256(), false, null, testData.length);
final Artifact createdArtifact = artifactManagement.create(artifactUpload);
assertThat(createdArtifact.getSha1Hash()).isEqualTo(artifactHashes.getSha1());
assertThat(createdArtifact.getMd5Hash()).isEqualTo(artifactHashes.getMd5());
assertThat(createdArtifact.getSha256Hash()).isEqualTo(artifactHashes.getSha256());
}
final Optional<DbArtifact> dbArtifact = artifactManagement.loadArtifactBinary(artifactHashes.getSha1(),
sm.getId(), sm.isEncrypted());
final Optional<DbArtifact> dbArtifact = artifactManagement.loadArtifactBinary(artifactHashes.getSha1(), sm.getId(), sm.isEncrypted());
assertThat(dbArtifact).isPresent();
}
@@ -548,19 +501,19 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
final DbArtifactHash artifactHashes = calcHashes(testData);
try (final InputStream inputStream = new ByteArrayInputStream(testData)) {
final ArtifactUpload artifactUpload = new ArtifactUpload(inputStream, smOs.getId(), "test-file",
artifactHashes.getMd5(), artifactHashes.getSha1(), artifactHashes.getSha256(), false, null,
testData.length);
final ArtifactUpload artifactUpload = new ArtifactUpload(
inputStream, smOs.getId(), "test-file",
artifactHashes.getMd5(), artifactHashes.getSha1(), artifactHashes.getSha256(), false, null, testData.length);
final Artifact artifact = artifactManagement.create(artifactUpload);
assertThat(artifact).isNotNull();
}
final Optional<DbArtifact> dbArtifact = artifactManagement.loadArtifactBinary(artifactHashes.getSha1(),
smOs.getId(), smOs.isEncrypted());
final Optional<DbArtifact> dbArtifact = artifactManagement.loadArtifactBinary(
artifactHashes.getSha1(), smOs.getId(), smOs.isEncrypted());
assertThat(dbArtifact).isPresent();
try (final InputStream inputStream = new ByteArrayInputStream(testData)) {
final ArtifactUpload existingArtifactUpload = new ArtifactUpload(inputStream, smApp.getId(), "test-file",
false, testData.length);
final ArtifactUpload existingArtifactUpload = new ArtifactUpload(
inputStream, smApp.getId(), "test-file", false, testData.length);
final Artifact createdArtifact = artifactManagement.create(existingArtifactUpload);
assertThat(createdArtifact.getSha1Hash()).isEqualTo(artifactHashes.getSha1());
assertThat(createdArtifact.getMd5Hash()).isEqualTo(artifactHashes.getMd5());
@@ -594,8 +547,8 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
}
}
private Artifact createArtifactForSoftwareModule(final String filename, final long moduleId, final int artifactSize,
final InputStream inputStream) {
private Artifact createArtifactForSoftwareModule(
final String filename, final long moduleId, final int artifactSize, final InputStream inputStream) {
return artifactManagement.create(new ArtifactUpload(inputStream, moduleId, filename, false, artifactSize));
}
@@ -607,8 +560,8 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
return runAsTenant(tenant, () -> testdataFactory.createSoftwareModuleApp());
}
private Artifact createArtifactForTenant(final String tenant, final String artifactData, final long moduleId,
final String filename) throws Exception {
private Artifact createArtifactForTenant(final String tenant, final String artifactData, final long moduleId, final String filename)
throws Exception {
return runAsTenant(tenant, () -> testdataFactory.createArtifact(artifactData, moduleId, filename));
}
@@ -616,12 +569,11 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
assertThat(runAsTenant(tenant, () -> artifactRepository.findAll())).hasSize(count);
}
private void assertEqualFileContents(final Optional<DbArtifact> artifact, final byte[] randomBytes)
throws IOException {
private void assertEqualFileContents(final Optional<DbArtifact> artifact, final byte[] randomBytes) throws IOException {
try (final InputStream inputStream = artifact.get().getFileInputStream()) {
assertTrue(IOUtils.contentEquals(new ByteArrayInputStream(randomBytes), inputStream),
assertTrue(
IOUtils.contentEquals(new ByteArrayInputStream(randomBytes), inputStream),
"The stored binary matches the given binary");
}
}
}
}

View File

@@ -607,17 +607,15 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
void lockDistributionSet() {
final DistributionSet distributionSet = testdataFactory.createDistributionSet("ds-1");
assertThat(
distributionSetManagement.get(distributionSet.getId()).map(DistributionSet::isLocked)
.orElse(true))
distributionSetManagement.get(distributionSet.getId()).map(DistributionSet::isLocked).orElse(true))
.isFalse();
distributionSetManagement.lock(distributionSet.getId());
assertThat(
distributionSetManagement.get(distributionSet.getId()).map(DistributionSet::isLocked)
.orElse(false))
distributionSetManagement.get(distributionSet.getId()).map(DistributionSet::isLocked).orElse(false))
.isTrue();
// assert software modules are locked
assertThat(distributionSet.getModules().size()).isNotEqualTo(0);
distributionSetManagement.get(distributionSet.getId()).map(DistributionSet::getModules)
distributionSetManagement.getWithDetails(distributionSet.getId()).map(DistributionSet::getModules)
.orElseThrow().forEach(module -> assertThat(module.isLocked()).isTrue());
}
@@ -668,7 +666,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
.isFalse();
// assert software modules are not unlocked
assertThat(distributionSet.getModules().size()).isNotEqualTo(0);
distributionSetManagement.get(distributionSet.getId()).map(DistributionSet::getModules)
distributionSetManagement.getWithDetails(distributionSet.getId()).map(DistributionSet::getModules)
.orElseThrow().forEach(module -> assertThat(module.isLocked()).isTrue());
}
@@ -688,7 +686,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
.as("Attempt to modify a locked DS software modules should throw an exception")
.isThrownBy(() -> distributionSetManagement.assignSoftwareModules(
distributionSet.getId(), List.of(testdataFactory.createSoftwareModule("sm-1").getId())));
assertThat(distributionSetManagement.get(distributionSet.getId()).get().getModules().size())
assertThat(distributionSetManagement.getWithDetails(distributionSet.getId()).get().getModules().size())
.as("Software module shall not be added to a locked DS.")
.isEqualTo(softwareModuleCount);
@@ -697,7 +695,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
.as("Attempt to modify a locked DS software modules should throw an exception")
.isThrownBy(() -> distributionSetManagement.unassignSoftwareModule(
distributionSet.getId(), distributionSet.getModules().stream().findFirst().get().getId()));
assertThat(distributionSetManagement.get(distributionSet.getId()).get().getModules().size())
assertThat(distributionSetManagement.getWithDetails(distributionSet.getId()).get().getModules().size())
.as("Software module shall not be removed from a locked DS.")
.isEqualTo(softwareModuleCount);
}
@@ -722,8 +720,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
.toList());
// assert that implicit lock locks for every skip tag
skipTags.forEach(skipTag -> {
DistributionSet distributionSetWithSkipTag =
testdataFactory.createDistributionSet("ds-skip-" + skipTag.getName());
DistributionSet distributionSetWithSkipTag = testdataFactory.createDistributionSet("ds-skip-" + skipTag.getName());
distributionSetManagement.assignTag(List.of(distributionSetWithSkipTag.getId()), skipTag.getId());
distributionSetWithSkipTag = distributionSetManagement.get(distributionSetWithSkipTag.getId()).orElseThrow();
// assert that implicit lock isn't applicable for skip tags

View File

@@ -33,7 +33,7 @@ import org.springframework.core.env.Environment;
@Feature("Component Tests - Repository")
@Story("Tenant Configuration Management")
public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest implements EnvironmentAware {
class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest implements EnvironmentAware {
private Environment environment;
@@ -44,7 +44,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Tests that tenant specific configuration can be persisted and in case the tenant does not have specific configuration the default from environment is used instead.")
public void storeTenantSpecificConfigurationAsString() {
void storeTenantSpecificConfigurationAsString() {
final String envPropertyDefault = environment
.getProperty("hawkbit.server.ddi.security.authentication.gatewaytoken.key");
assertThat(envPropertyDefault).isNotNull();
@@ -73,7 +73,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Tests that the tenant specific configuration can be updated")
public void updateTenantSpecificConfiguration() {
void updateTenantSpecificConfiguration() {
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY;
final String value1 = "firstValue";
final String value2 = "secondValue";
@@ -89,7 +89,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Tests that the tenant specific configuration can be batch updated")
public void batchUpdateTenantSpecificConfiguration() {
void batchUpdateTenantSpecificConfiguration() {
Map<String, Serializable> configuration = new HashMap<>() {{
put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, "token_123");
put(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, true);
@@ -107,7 +107,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Tests that the configuration value can be converted from String to Integer automatically")
public void storeAndUpdateTenantSpecificConfigurationAsBoolean() {
void storeAndUpdateTenantSpecificConfigurationAsBoolean() {
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED;
final Boolean value1 = true;
tenantConfigurationManagement.addOrUpdateConfiguration(configKey, value1);
@@ -119,7 +119,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Tests that the get configuration throws exception in case the value cannot be automatically converted from String to Boolean")
public void wrongTenantConfigurationValueTypeThrowsException() {
void wrongTenantConfigurationValueTypeThrowsException() {
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED;
final String value1 = "thisIsNotABoolean";
@@ -131,7 +131,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Tests that the get configuration throws exception in case the value is the wrong type")
public void batchWrongTenantConfigurationValueTypeThrowsException() {
void batchWrongTenantConfigurationValueTypeThrowsException() {
final Map<String, Serializable> configuration = new HashMap<>() {{
put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, "token_123");
put(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, true);
@@ -155,7 +155,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Tests that a deletion of a tenant specific configuration deletes it from the database.")
public void deleteConfigurationReturnNullConfiguration() {
void deleteConfigurationReturnNullConfiguration() {
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY;
// gateway token does not have default value so no configuration value should be available
@@ -180,7 +180,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Test that an Exception is thrown, when an integer is stored but a string expected.")
public void storesIntegerWhenStringIsExpected() {
void storesIntegerWhenStringIsExpected() {
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY;
final Integer wrongDatType = 123;
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongDatType))
@@ -190,7 +190,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Test that an Exception is thrown, when an integer is stored but a boolean expected.")
public void storesIntegerWhenBooleanIsExpected() {
void storesIntegerWhenBooleanIsExpected() {
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED;
final Integer wrongDataType = 123;
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongDataType))
@@ -200,7 +200,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Test that an Exception is thrown, when an integer is stored as PollingTime.")
public void storesIntegerWhenPollingIntervalIsExpected() {
void storesIntegerWhenPollingIntervalIsExpected() {
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
final Integer wrongDataType = 123;
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongDataType))
@@ -210,7 +210,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Test that an Exception is thrown, when an invalid formatted string is stored as PollingTime.")
public void storesWrongFormattedStringAsPollingInterval() {
void storesWrongFormattedStringAsPollingInterval() {
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
final String wrongFormatted = "wrongFormatted";
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongFormatted))
@@ -220,7 +220,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Test that an Exception is thrown, when an invalid formatted string is stored as PollingTime.")
public void storesTooSmallDurationAsPollingInterval() {
void storesTooSmallDurationAsPollingInterval() {
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
final String tooSmallDuration = DurationHelper
@@ -232,7 +232,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Stores a correct formatted PollignTime and reads it again.")
public void storesCorrectDurationAsPollingInterval() {
void storesCorrectDurationAsPollingInterval() {
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
final Duration duration = DurationHelper.getDurationByTimeValues(1, 2, 0);
@@ -246,7 +246,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Request a config value in a wrong Value")
public void requestConfigValueWithWrongType() {
void requestConfigValueWithWrongType() {
assertThatThrownBy(() -> tenantConfigurationManagement.getConfigurationValue(
TenantConfigurationKey.POLLING_TIME_INTERVAL, Serializable.class))
.isInstanceOf(TenantConfigurationValidatorException.class);
@@ -254,7 +254,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Verifies that every TenenatConfiguraationKeyName exists only once")
public void verifyThatAllKeysAreDifferent() {
void verifyThatAllKeysAreDifferent() {
final Map<String, Void> keyNames = new HashMap<>();
tenantConfigurationProperties.getConfigurationKeys().forEach(key -> {
if (keyNames.containsKey(key.getKeyName())) {
@@ -266,14 +266,14 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Get TenantConfigurationKeyByName")
public void getTenantConfigurationKeyByName() {
void getTenantConfigurationKeyByName() {
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
assertThat(tenantConfigurationProperties.fromKeyName(configKey).getKeyName()).isEqualTo(configKey);
}
@Test
@Description("Tenant configuration which is not declared throws exception")
public void storeTenantConfigurationWhichIsNotDeclaredThrowsException() {
void storeTenantConfigurationWhichIsNotDeclaredThrowsException() {
final String configKeyWhichDoesNotExists = "configKeyWhichDoesNotExists";
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKeyWhichDoesNotExists, "value"))
.as("Expected InvalidTenantConfigurationKeyException for tenant configuration key which is not declared")

View File

@@ -9,6 +9,8 @@
*/
package org.eclipse.hawkbit.repository.jpa.rsql;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import jakarta.persistence.EntityManager;
@@ -23,11 +25,9 @@ import cz.jirutka.rsql.parser.ast.Node;
import cz.jirutka.rsql.parser.ast.RSQLOperators;
import cz.jirutka.rsql.parser.ast.RSQLVisitor;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.jpa.Jpa;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.eclipse.persistence.jpa.JpaQuery;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.util.CollectionUtils;
@@ -40,23 +40,33 @@ public class RSQLToSQL {
this.entityManager = entityManager;
}
public <T, A extends Enum<A> & RsqlQueryField> String toSQL(final Class<T> domainClass, final Class<A> fieldsClass, final String rsql,
final boolean legacyRsqlVisitor) {
return createDbQuery(domainClass, fieldsClass, rsql, legacyRsqlVisitor).getSQLString();
}
public <T, A extends Enum<A> & RsqlQueryField> DatabaseQuery createDbQuery(final Class<T> domainClass, final Class<A> fieldsClass,
final String rsql, final boolean legacyRsqlVisitor) {
public <T, A extends Enum<A> & RsqlQueryField> String toSQL(
final Class<T> domainClass, final Class<A> fieldsClass, final String rsql, final boolean legacyRsqlVisitor) {
final CriteriaQuery<T> query = createQuery(domainClass, fieldsClass, rsql, legacyRsqlVisitor);
final TypedQuery<?> typedQuery = entityManager.createQuery(query);
// executes the query - otherwise the SQL string is not generated
typedQuery.setParameter(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, "DEFAULT");
typedQuery.getResultList();
return typedQuery.unwrap(JpaQuery.class).getDatabaseQuery();
if (Jpa.JPA_VENDOR.equals(Jpa.JpaVendor.ECLIPSELINK)) {
typedQuery.setParameter("eclipselink.tenant-id", "DEFAULT");
typedQuery.getResultList();
try {
final Class<?> jpaQueryClass = Class.forName("org.eclipse.persistence.jpa.JpaQuery");
final Method getDatabaseQueryMethod = jpaQueryClass.getMethod("getDatabaseQuery");
final Method getSQLString = getDatabaseQueryMethod.getReturnType().getMethod("getSQLString");
return (String)getSQLString.invoke(getDatabaseQueryMethod.invoke(typedQuery.unwrap(jpaQueryClass)));
} catch (final RuntimeException e) {
throw e;
} catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
throw new UnsupportedOperationException("EclipseLink is not supported", e);
} catch (final InvocationTargetException e) {
throw e.getCause() instanceof RuntimeException ? (RuntimeException)e.getCause() : new RuntimeException(e.getCause());
}
} else { // hibernate
throw new UnsupportedOperationException("Hibernate is not supported");
}
}
private <T, A extends Enum<A> & RsqlQueryField> CriteriaQuery<T> createQuery(final Class<T> domainClass, final Class<A> fieldsClass,
final String rsql, final boolean legacyRsqlVisitor) {
private <T, A extends Enum<A> & RsqlQueryField> CriteriaQuery<T> createQuery(
final Class<T> domainClass, final Class<A> fieldsClass, final String rsql, final boolean legacyRsqlVisitor) {
final CriteriaQuery<T> query = entityManager.getCriteriaBuilder().createQuery(domainClass);
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
return query.where(
@@ -78,16 +88,15 @@ public class RSQLToSQL {
query.distinct(true);
final RSQLVisitor<List<Predicate>, String> jpqQueryRSQLVisitor =
legacyRsqlVisitor ?
new JpaQueryRsqlVisitor<>(
legacyRsqlVisitor
? new JpaQueryRsqlVisitor<>(
root, cb, fieldsClass,
virtualPropertyReplacer, DATABASE, query,
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase())
:
new JpaQueryRsqlVisitorG2<>(
fieldsClass, root, query, cb,
DATABASE, virtualPropertyReplacer,
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase());
: new JpaQueryRsqlVisitorG2<>(
fieldsClass, root, query, cb,
DATABASE, virtualPropertyReplacer,
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase());
final List<Predicate> accept = rootNode.accept(jpqQueryRSQLVisitor);
if (CollectionUtils.isEmpty(accept)) {

View File

@@ -15,7 +15,18 @@ logging.level.org.eclipse.persistence=ERROR
spring.jpa.properties.eclipselink.logging.level=FINE
spring.jpa.properties.eclipselink.logging.level.sql=FINE
spring.jpa.properties.eclipselink.logging.parameters=true
#hibernate.generate_statistics=true
#logging.level.org.hibernate.SQL=TRACE
#logging.level.org.hibernate.stat=TRACE
# Debug utility functions - END
# Switch to mysql
#spring.jpa.database=MYSQL
#spring.datasource.url=jdbc:mariadb://localhost:3306/hawkbit_test
#spring.datasource.driverClassName=org.mariadb.jdbc.Driver
#spring.datasource.username=root
#spring.datasource.password=
# enable / disable case sensitiveness of the DB when playing around
#hawkbit.rsql.caseInsensitiveDB=true

View File

@@ -17,6 +17,7 @@
<artifactId>hawkbit-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>hawkbit-repository</artifactId>
<name>hawkBit :: Repository :: Parent</name>
<packaging>pom</packaging>
@@ -24,6 +25,9 @@
<modules>
<module>hawkbit-repository-api</module>
<module>hawkbit-repository-core</module>
<module>hawkbit-repository-jpa-api</module>
<module>hawkbit-repository-jpa-eclipselink</module>
<module>hawkbit-repository-jpa-hibernate</module>
<module>hawkbit-repository-jpa</module>
<module>hawkbit-repository-jpa-flyway</module>

10
pom.xml
View File

@@ -671,16 +671,6 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.core</artifactId>
<version>${eclipselink.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.jpa</artifactId>
<version>${eclipselink.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.plugin</groupId>
<artifactId>spring-plugin-core</artifactId>