From db3ac7f2ddb1e323bbd83185adb00b027f1e9ab3 Mon Sep 17 00:00:00 2001 From: Avgustin Marinov Date: Mon, 16 Dec 2024 16:08:07 +0200 Subject: [PATCH] Hibernate support (#2147) * Hibernate support --------- Signed-off-by: Avgustin Marinov --- .github/workflows/verify-hibernate.yml | 46 ++++ .../resource/MgmtDistributionSetResource.java | 6 +- .../MgmtDistributionSetTypeResourceTest.java | 10 - .../hawkbit-repository-jpa-api/README.md | 3 + .../hawkbit-repository-jpa-api/pom.xml | 54 ++++ .../repository/jpa/EntityInterceptor.java | 0 ...ansactionCommitDefaultServiceExecutor.java | 2 +- .../AfterTransactionCommitExecutor.java | 0 .../jpa/model/AbstractBaseEntity.java | 114 +++++++++ .../jpa/model/EntityInterceptorListener.java | 0 .../jpa/model/EventAwareEntity.java | 0 .../AfterTransactionCommitExecutorHolder.java | 0 .../model/helper/EntityInterceptorHolder.java | 0 .../helper/SecurityTokenGeneratorHolder.java | 0 .../jpa/model/helper/TenantAwareHolder.java | 0 .../jpa/utils/MapAttributeConverter.java | 0 .../README.md | 3 + .../pom.xml | 49 ++++ .../jpa/HawkbitEclipseLinkJpaDialect.java | 4 +- .../eclipse/hawkbit/repository/jpa/Jpa.java | 27 +- .../repository/jpa/JpaConfiguration.java | 86 +++++++ .../jpa/MultiTenantJpaTransactionManager.java | 91 +++++++ .../jpa/model/AbstractJpaBaseEntity.java | 119 +++++++++ .../AbstractJpaTenantAwareBaseEntity.java | 10 +- .../model/EntityPropertyChangeListener.java | 37 +++ .../jpa/HawkBitEclipseLinkJpaDialectTest.java | 18 +- .../README.md | 25 ++ .../hawkbit-repository-jpa-hibernate/pom.xml | 46 ++++ .../eclipse/hawkbit/repository/jpa/Jpa.java | 42 ++++ .../repository/jpa/JpaConfiguration.java | 103 ++++++++ .../repository/jpa/TenantIdentifier.java | 40 +++ .../jpa/model/AbstractJpaBaseEntity.java | 119 +++++++++ .../AbstractJpaTenantAwareBaseEntity.java | 95 +++++++ .../model/EntityPropertyChangeListener.java | 68 +++++ .../hawkbit-repository-jpa/pom.xml | 90 +++---- .../RepositoryApplicationConfiguration.java | 78 +----- .../MultiTenantJpaTransactionManager.java | 53 ---- .../jpa/management/JpaArtifactManagement.java | 6 - .../jpa/management/JpaTargetManagement.java | 2 - .../jpa/model/AbstractJpaBaseEntity.java | 213 ---------------- .../repository/jpa/model/JpaAction.java | 4 - .../repository/jpa/model/JpaTarget.java | 44 ---- .../jpa/rsql/RsqlParserValidationOracle.java | 7 +- .../repository/jpa/management/ActionTest.java | 22 +- .../management/ArtifactManagementTest.java | 234 +++++++----------- .../DistributionSetManagementTest.java | 17 +- .../TenantConfigurationManagementTest.java | 36 +-- .../repository/jpa/rsql/RSQLToSQL.java | 53 ++-- .../src/test/resources/jpa-test.properties | 11 + hawkbit-repository/pom.xml | 4 + pom.xml | 10 - 51 files changed, 1397 insertions(+), 704 deletions(-) create mode 100644 .github/workflows/verify-hibernate.yml create mode 100644 hawkbit-repository/hawkbit-repository-jpa-api/README.md create mode 100644 hawkbit-repository/hawkbit-repository-jpa-api/pom.xml rename hawkbit-repository/{hawkbit-repository-jpa => hawkbit-repository-jpa-api}/src/main/java/org/eclipse/hawkbit/repository/jpa/EntityInterceptor.java (100%) rename hawkbit-repository/{hawkbit-repository-jpa => hawkbit-repository-jpa-api}/src/main/java/org/eclipse/hawkbit/repository/jpa/executor/AfterTransactionCommitDefaultServiceExecutor.java (96%) rename hawkbit-repository/{hawkbit-repository-jpa => hawkbit-repository-jpa-api}/src/main/java/org/eclipse/hawkbit/repository/jpa/executor/AfterTransactionCommitExecutor.java (100%) create mode 100644 hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractBaseEntity.java rename hawkbit-repository/{hawkbit-repository-jpa => hawkbit-repository-jpa-api}/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EntityInterceptorListener.java (100%) rename hawkbit-repository/{hawkbit-repository-jpa => hawkbit-repository-jpa-api}/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EventAwareEntity.java (100%) rename hawkbit-repository/{hawkbit-repository-jpa => hawkbit-repository-jpa-api}/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/AfterTransactionCommitExecutorHolder.java (100%) rename hawkbit-repository/{hawkbit-repository-jpa => hawkbit-repository-jpa-api}/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/EntityInterceptorHolder.java (100%) rename hawkbit-repository/{hawkbit-repository-jpa => hawkbit-repository-jpa-api}/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/SecurityTokenGeneratorHolder.java (100%) rename hawkbit-repository/{hawkbit-repository-jpa => hawkbit-repository-jpa-api}/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/TenantAwareHolder.java (100%) rename hawkbit-repository/{hawkbit-repository-jpa => hawkbit-repository-jpa-api}/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/MapAttributeConverter.java (100%) create mode 100644 hawkbit-repository/hawkbit-repository-jpa-eclipselink/README.md create mode 100644 hawkbit-repository/hawkbit-repository-jpa-eclipselink/pom.xml rename hawkbit-repository/{hawkbit-repository-jpa => hawkbit-repository-jpa-eclipselink}/src/main/java/org/eclipse/hawkbit/repository/jpa/HawkbitEclipseLinkJpaDialect.java (97%) rename hawkbit-repository/{hawkbit-repository-jpa => hawkbit-repository-jpa-eclipselink}/src/main/java/org/eclipse/hawkbit/repository/jpa/Jpa.java (59%) create mode 100644 hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaConfiguration.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/MultiTenantJpaTransactionManager.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaBaseEntity.java rename hawkbit-repository/{hawkbit-repository-jpa => hawkbit-repository-jpa-eclipselink}/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaTenantAwareBaseEntity.java (90%) create mode 100644 hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EntityPropertyChangeListener.java rename hawkbit-repository/{hawkbit-repository-jpa => hawkbit-repository-jpa-eclipselink}/src/test/java/org/eclipse/hawkbit/repository/jpa/HawkBitEclipseLinkJpaDialectTest.java (85%) create mode 100644 hawkbit-repository/hawkbit-repository-jpa-hibernate/README.md create mode 100644 hawkbit-repository/hawkbit-repository-jpa-hibernate/pom.xml create mode 100644 hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/Jpa.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaConfiguration.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/TenantIdentifier.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaBaseEntity.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaTenantAwareBaseEntity.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EntityPropertyChangeListener.java delete mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/configuration/MultiTenantJpaTransactionManager.java delete mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaBaseEntity.java diff --git a/.github/workflows/verify-hibernate.yml b/.github/workflows/verify-hibernate.yml new file mode 100644 index 0000000000..ec0e913f05 --- /dev/null +++ b/.github/workflows/verify-hibernate.yml @@ -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 \ No newline at end of file diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java index 9506948205..7ab72fb117 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java @@ -131,8 +131,7 @@ public ResponseEntity> getDistributionSets( } @Override - public ResponseEntity getDistributionSet( - final Long distributionSetId) { + public ResponseEntity getDistributionSet(final Long distributionSetId) { final DistributionSet foundDs = distributionSetManagement.getOrElseThrowException(distributionSetId); final MgmtDistributionSet response = MgmtDistributionSetMapper.toResponse(foundDs); @@ -142,8 +141,7 @@ public ResponseEntity getDistributionSet( } @Override - public ResponseEntity> createDistributionSets( - final List sets) { + public ResponseEntity> createDistributionSets(final List sets) { log.debug("creating {} distribution sets", sets.size()); // set default Ds type if ds type is null final String defaultDsKey = systemSecurityContext diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResourceTest.java index 607a804dbd..c5328b3cde 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResourceTest.java @@ -746,14 +746,4 @@ private DistributionSetType generateTestType() { 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++; - } - } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-api/README.md b/hawkbit-repository/hawkbit-repository-jpa-api/README.md new file mode 100644 index 0000000000..dd3c4c1798 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-api/README.md @@ -0,0 +1,3 @@ +# hawkBit JPA EclipseLink Vendor integration + +Implementation of [EclipseLink](http://www.eclipse.org/eclipselink/) JPA vendor. \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-api/pom.xml b/hawkbit-repository/hawkbit-repository-jpa-api/pom.xml new file mode 100644 index 0000000000..78c884cd9c --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-api/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + org.eclipse.hawkbit + ${revision} + hawkbit-repository + ../pom.xml + + + hawkbit-repository-jpa-api + hawkBit :: Repository :: JPA API + + + ${project.build.directory}/generated-sources/apt/ + + + + + org.eclipse.hawkbit + hawkbit-repository-core + ${project.version} + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.hibernate.orm + hibernate-core + + + + + + + org.hibernate.orm + hibernate-jpamodelgen + true + + + diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/EntityInterceptor.java b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/EntityInterceptor.java similarity index 100% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/EntityInterceptor.java rename to hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/EntityInterceptor.java diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/executor/AfterTransactionCommitDefaultServiceExecutor.java b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/executor/AfterTransactionCommitDefaultServiceExecutor.java similarity index 96% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/executor/AfterTransactionCommitDefaultServiceExecutor.java rename to hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/executor/AfterTransactionCommitDefaultServiceExecutor.java index 25d28b8fe8..69918b727a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/executor/AfterTransactionCommitDefaultServiceExecutor.java +++ b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/executor/AfterTransactionCommitDefaultServiceExecutor.java @@ -45,7 +45,7 @@ public void afterCommit() { @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) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/executor/AfterTransactionCommitExecutor.java b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/executor/AfterTransactionCommitExecutor.java similarity index 100% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/executor/AfterTransactionCommitExecutor.java rename to hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/executor/AfterTransactionCommitExecutor.java diff --git a/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractBaseEntity.java b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractBaseEntity.java new file mode 100644 index 0000000000..820c24f042 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractBaseEntity.java @@ -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(); + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EntityInterceptorListener.java b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EntityInterceptorListener.java similarity index 100% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EntityInterceptorListener.java rename to hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EntityInterceptorListener.java diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EventAwareEntity.java b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EventAwareEntity.java similarity index 100% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EventAwareEntity.java rename to hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EventAwareEntity.java diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/AfterTransactionCommitExecutorHolder.java b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/AfterTransactionCommitExecutorHolder.java similarity index 100% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/AfterTransactionCommitExecutorHolder.java rename to hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/AfterTransactionCommitExecutorHolder.java diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/EntityInterceptorHolder.java b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/EntityInterceptorHolder.java similarity index 100% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/EntityInterceptorHolder.java rename to hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/EntityInterceptorHolder.java diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/SecurityTokenGeneratorHolder.java b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/SecurityTokenGeneratorHolder.java similarity index 100% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/SecurityTokenGeneratorHolder.java rename to hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/SecurityTokenGeneratorHolder.java diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/TenantAwareHolder.java b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/TenantAwareHolder.java similarity index 100% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/TenantAwareHolder.java rename to hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/model/helper/TenantAwareHolder.java diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/MapAttributeConverter.java b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/MapAttributeConverter.java similarity index 100% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/MapAttributeConverter.java rename to hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/MapAttributeConverter.java diff --git a/hawkbit-repository/hawkbit-repository-jpa-eclipselink/README.md b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/README.md new file mode 100644 index 0000000000..dd3c4c1798 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/README.md @@ -0,0 +1,3 @@ +# hawkBit JPA EclipseLink Vendor integration + +Implementation of [EclipseLink](http://www.eclipse.org/eclipselink/) JPA vendor. \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-eclipselink/pom.xml b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/pom.xml new file mode 100644 index 0000000000..06c16c85a1 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + org.eclipse.hawkbit + ${revision} + hawkbit-repository + ../pom.xml + + + hawkbit-repository-jpa-eclipselink + hawkBit :: Repository :: JPA EclipseLink Vendor + + + ${project.build.directory}/generated-sources/apt/ + + + + + org.eclipse.hawkbit + hawkbit-repository-jpa-api + ${project.version} + + + + + org.hibernate.orm + hibernate-jpamodelgen + true + + + + org.eclipse.persistence + org.eclipse.persistence.jpa + ${eclipselink.version} + + + diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/HawkbitEclipseLinkJpaDialect.java b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/HawkbitEclipseLinkJpaDialect.java similarity index 97% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/HawkbitEclipseLinkJpaDialect.java rename to hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/HawkbitEclipseLinkJpaDialect.java index 1e0385bb09..0562a17816 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/HawkbitEclipseLinkJpaDialect.java +++ b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/HawkbitEclipseLinkJpaDialect.java @@ -49,7 +49,7 @@ * 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 @@ private static SQLException findSqlException(final RuntimeException jpaSystemExc return null; } -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/Jpa.java b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/Jpa.java similarity index 59% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/Jpa.java rename to hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/Jpa.java index ab9c9f0fe3..ad743e032a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/Jpa.java +++ b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/Jpa.java @@ -16,40 +16,35 @@ 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 String formatNativeQueryInClause(final String name, final List 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 void setNativeQueryInParameter(final Query deleteQuery, final String name, final List 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 elements) { return "?" + String.join(",?", elements); } -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaConfiguration.java new file mode 100644 index 0000000000..ecb91ff2f3 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaConfiguration.java @@ -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 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) { + 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 getVendorProperties() { + final Map 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; + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/MultiTenantJpaTransactionManager.java b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/MultiTenantJpaTransactionManager.java new file mode 100644 index 0000000000..0370b58ef6 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/MultiTenantJpaTransactionManager.java @@ -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. + *

+ * 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, ""); + } + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaBaseEntity.java b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaBaseEntity.java new file mode 100644 index 0000000000..0771489b04 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaBaseEntity.java @@ -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; + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaTenantAwareBaseEntity.java b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaTenantAwareBaseEntity.java similarity index 90% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaTenantAwareBaseEntity.java rename to hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaTenantAwareBaseEntity.java index 2bee50b42f..e12ba9bb98 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaTenantAwareBaseEntity.java +++ b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaTenantAwareBaseEntity.java @@ -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 boolean equals(final Object obj) { 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() + "]"; } /** diff --git a/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EntityPropertyChangeListener.java b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EntityPropertyChangeListener.java new file mode 100644 index 0000000000..bfc62575ac --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EntityPropertyChangeListener.java @@ -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 JpaTarget entities, filtering out updates that only change the + * "lastTargetQuery" or "address" fields. + */ +public class EntityPropertyChangeListener extends DescriptorEventAdapter { + + private static final List 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()); + } + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/HawkBitEclipseLinkJpaDialectTest.java b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/test/java/org/eclipse/hawkbit/repository/jpa/HawkBitEclipseLinkJpaDialectTest.java similarity index 85% rename from hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/HawkBitEclipseLinkJpaDialectTest.java rename to hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/test/java/org/eclipse/hawkbit/repository/jpa/HawkBitEclipseLinkJpaDialectTest.java index c3cce1d754..524cfa20e7 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/HawkBitEclipseLinkJpaDialectTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/test/java/org/eclipse/hawkbit/repository/jpa/HawkBitEclipseLinkJpaDialectTest.java @@ -30,22 +30,21 @@ */ @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 void jpaSystemExceptionWithSqlDeadLockExceptionIsConcurrencyFailureExcept @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 void jpaSystemExceptionWithNumberFormatExceptionIsNull() { @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 void runtimeExceptionWithSqlDeadLockExceptionIsConcurrencyFailureExceptio @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(); } - -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-hibernate/README.md b/hawkbit-repository/hawkbit-repository-jpa-hibernate/README.md new file mode 100644 index 0000000000..91e3a66a40 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-hibernate/README.md @@ -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 + + org.eclipse.hawkbit + hawkbit-starter + ${project.version} + + + org.eclipse.hawkbit + hawkbit-repository-jpa-eclipselink + + + + + org.eclipse.hawkbit + hawkbit-repository-jpa-hibernate + ${project.version} + +``` \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-hibernate/pom.xml b/hawkbit-repository/hawkbit-repository-jpa-hibernate/pom.xml new file mode 100644 index 0000000000..6f0a57552c --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-hibernate/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + org.eclipse.hawkbit + ${revision} + hawkbit-repository + + + hawkbit-repository-jpa-hibernate + hawkBit :: Repository :: JPA Hibernate Vendor + + + ${project.build.directory}/generated-sources/apt/ + + + + + org.eclipse.hawkbit + hawkbit-repository-jpa-api + ${project.version} + + + org.hibernate.orm + hibernate-core + + + + + org.hibernate.orm + hibernate-jpamodelgen + true + + + diff --git a/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/Jpa.java b/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/Jpa.java new file mode 100644 index 0000000000..34213c1b03 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/Jpa.java @@ -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 String formatNativeQueryInClause(final String name, final List list) { + return ":" + name; + } + + public static void setNativeQueryInParameter(final Query deleteQuery, final String name, final List list) { + deleteQuery.setParameter(name, list); + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaConfiguration.java new file mode 100644 index 0000000000..45a0f5029d --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaConfiguration.java @@ -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 jtaTransactionManagerProvider, + final TenantAware tenantAware) { + super(dataSource, properties, jtaTransactionManagerProvider); + tenantIdentifier = new TenantIdentifier(tenantAware); + } + + @Bean + CurrentTenantIdentifierResolver 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 getVendorProperties() { + final Map 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; + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/TenantIdentifier.java b/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/TenantIdentifier.java new file mode 100644 index 0000000000..bcb09c7a79 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/TenantIdentifier.java @@ -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 { + + 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; + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaBaseEntity.java b/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaBaseEntity.java new file mode 100644 index 0000000000..d358430678 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaBaseEntity.java @@ -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; + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaTenantAwareBaseEntity.java b/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaTenantAwareBaseEntity.java new file mode 100644 index 0000000000..8f0bbd0e23 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaTenantAwareBaseEntity.java @@ -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()); + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EntityPropertyChangeListener.java b/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EntityPropertyChangeListener.java new file mode 100644 index 0000000000..3daf5cba77 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/model/EntityPropertyChangeListener.java @@ -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 JpaTarget entities, filtering out updates that only change the + * "lastTargetQuery" or "address" fields. + */ +public class EntityPropertyChangeListener implements PostUpdateEventListener { + + private static final List 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; + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/pom.xml b/hawkbit-repository/hawkbit-repository-jpa/pom.xml index 80aa20d55f..efc32dabc3 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/pom.xml +++ b/hawkbit-repository/hawkbit-repository-jpa/pom.xml @@ -23,32 +23,65 @@ ${project.build.directory}/generated-sources/apt/ + eclipselink + + + eclipselink + + + + !jpa.vendor + + + + + + org.eclipse.hawkbit + hawkbit-repository-jpa-eclipselink + ${project.version} + + + + + hibernate + + + jpa.vendor + hibernate + + + + + + org.eclipse.hawkbit + hawkbit-repository-jpa-hibernate + ${project.version} + + + + + org.eclipse.hawkbit - hawkbit-repository-api + hawkbit-repository-jpa-api ${project.version} + + - org.eclipse.hawkbit - hawkbit-repository-core - ${project.version} + org.hibernate.orm + hibernate-jpamodelgen + true + org.springframework spring-core - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.eclipse.persistence - org.eclipse.persistence.jpa - org.springframework.security spring-security-core @@ -70,13 +103,6 @@ commons-collections4 - - - org.hibernate.orm - hibernate-jpamodelgen - true - - org.eclipse.hawkbit @@ -90,30 +116,4 @@ test - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - add-source - generate-sources - - add-source - - - - ${apt.source.dir} - - - - - - - - diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java index daa3531e9b..1cb672e23a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java @@ -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.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.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.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.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 @@ @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 jtaTransactionManagerProvider) { - super(dataSource, properties, jtaTransactionManagerProvider); - } +public class RepositoryApplicationConfiguration { /** * Defines the validation processor bean. @@ -233,58 +213,14 @@ public MethodValidationPostProcessor methodValidationPostProcessor() { 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) { - 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 getVendorProperties() { - final Map 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 artifactAccessController, diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/configuration/MultiTenantJpaTransactionManager.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/configuration/MultiTenantJpaTransactionManager.java deleted file mode 100644 index 3096586674..0000000000 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/configuration/MultiTenantJpaTransactionManager.java +++ /dev/null @@ -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()); - } - } -} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaArtifactManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaArtifactManagement.java index 9645a85d26..c4c7f3653f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaArtifactManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaArtifactManagement.java @@ -23,7 +23,6 @@ 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.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.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; /** diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java index facb408063..a3847b5b8c 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java @@ -36,7 +36,6 @@ 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.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; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaBaseEntity.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaBaseEntity.java deleted file mode 100644 index 07033f753a..0000000000 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/AbstractJpaBaseEntity.java +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java index 5acadac92f..e1e75585d3 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java @@ -19,8 +19,6 @@ 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.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.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}. diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java index f662392735..eb155fdd48 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java @@ -16,7 +16,6 @@ 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.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.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 @@ 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 TargetUpdateStatusConverter() { ), 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 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; - } - } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java index 52966cd804..1265e16715 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java @@ -34,7 +34,6 @@ 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 ValidationOracleContext suggest(final String rsqlQuery, final int cursorP } 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; } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ActionTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ActionTest.java index fcf334673d..9505a6e5bf 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ActionTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ActionTest.java @@ -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 @@ void timeforcedHitNewHasCodeIsGenerated() { 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 @@ void timeforcedHitNewHasCodeIsGenerated() { 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 @@ void testActionTypeConvert() { 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 @@ private Action createAction() { action.setActive(true); return actionRepository.save(action); } -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ArtifactManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ArtifactManagementTest.java index 77f70c47aa..60e33cd8d7 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ArtifactManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ArtifactManagementTest.java @@ -16,7 +16,6 @@ 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.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.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 @@ void nonExistingEntityAccessReturnsNotPresent() { 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 @@ void createArtifact() throws IOException { 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 = ".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 @@ void createArtifactsUntilQuotaIsExceeded() throws IOException { @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 artifactIds = new ArrayList<>(); @@ -221,99 +203,78 @@ void createArtifactsUntilStorageQuotaIsExceeded() throws IOException { } // 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 @@ void deleteArtifact() throws IOException { @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 @@ void deleteDuplicateArtifacts() throws IOException { 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 @@ void findArtifact() throws IOException { 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 @@ void loadStreamOfArtifact() throws IOException { 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 @@ void createArtifactWithNoneMatchingHashes() throws IOException, NoSuchAlgorithmE 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 @@ void createArtifactWithMatchingHashes() throws IOException, NoSuchAlgorithmExcep 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 = artifactManagement.loadArtifactBinary(artifactHashes.getSha1(), - sm.getId(), sm.isEncrypted()); + final Optional dbArtifact = artifactManagement.loadArtifactBinary(artifactHashes.getSha1(), sm.getId(), sm.isEncrypted()); assertThat(dbArtifact).isPresent(); } @@ -548,19 +501,19 @@ void createExistingArtifactReturnsFullHashList() throws IOException, NoSuchAlgor 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 = artifactManagement.loadArtifactBinary(artifactHashes.getSha1(), - smOs.getId(), smOs.isEncrypted()); + final Optional 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 @@ private Artifact createArtifactForSoftwareModule(final String filename, final lo } } - 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 @@ private SoftwareModule createSoftwareModuleForTenant(final String tenant) throws 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 @@ private void verifyTenantArtifactCountIs(final String tenant, final int count) t assertThat(runAsTenant(tenant, () -> artifactRepository.findAll())).hasSize(count); } - private void assertEqualFileContents(final Optional artifact, final byte[] randomBytes) - throws IOException { + private void assertEqualFileContents(final Optional 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"); } } - -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java index cadaf9be63..d9f15f9677 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java @@ -607,17 +607,15 @@ void findDistributionSetsWithoutLazy() { 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 @@ void unlockDistributionSet() { .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 @@ void lockDistributionSetApplied() { .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 @@ void lockDistributionSetApplied() { .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 @@ void isImplicitLockApplicableForDistributionSet() { .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 diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TenantConfigurationManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TenantConfigurationManagementTest.java index 15973e020a..1694d0762b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TenantConfigurationManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TenantConfigurationManagementTest.java @@ -33,7 +33,7 @@ @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 void setEnvironment(final Environment environment) { @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 void storeTenantSpecificConfigurationAsString() { @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 void updateTenantSpecificConfiguration() { @Test @Description("Tests that the tenant specific configuration can be batch updated") - public void batchUpdateTenantSpecificConfiguration() { + void batchUpdateTenantSpecificConfiguration() { Map configuration = new HashMap<>() {{ put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, "token_123"); put(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, true); @@ -107,7 +107,7 @@ public void batchUpdateTenantSpecificConfiguration() { @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 void storeAndUpdateTenantSpecificConfigurationAsBoolean() { @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 void wrongTenantConfigurationValueTypeThrowsException() { @Test @Description("Tests that the get configuration throws exception in case the value is the wrong type") - public void batchWrongTenantConfigurationValueTypeThrowsException() { + void batchWrongTenantConfigurationValueTypeThrowsException() { final Map configuration = new HashMap<>() {{ put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, "token_123"); put(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, true); @@ -155,7 +155,7 @@ public void batchWrongTenantConfigurationValueTypeThrowsException() { @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 void deleteConfigurationReturnNullConfiguration() { @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 void storesIntegerWhenStringIsExpected() { @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 void storesIntegerWhenBooleanIsExpected() { @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 void storesIntegerWhenPollingIntervalIsExpected() { @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 void storesWrongFormattedStringAsPollingInterval() { @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 void storesTooSmallDurationAsPollingInterval() { @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 void storesCorrectDurationAsPollingInterval() { @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 void requestConfigValueWithWrongType() { @Test @Description("Verifies that every TenenatConfiguraationKeyName exists only once") - public void verifyThatAllKeysAreDifferent() { + void verifyThatAllKeysAreDifferent() { final Map keyNames = new HashMap<>(); tenantConfigurationProperties.getConfigurationKeys().forEach(key -> { if (keyNames.containsKey(key.getKeyName())) { @@ -266,14 +266,14 @@ public void verifyThatAllKeysAreDifferent() { @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") diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLToSQL.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLToSQL.java index 895ab02d73..da4d5758d0 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLToSQL.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLToSQL.java @@ -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.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 RSQLToSQL(final EntityManager entityManager) { this.entityManager = entityManager; } - public & RsqlQueryField> String toSQL(final Class domainClass, final Class fieldsClass, final String rsql, - final boolean legacyRsqlVisitor) { - return createDbQuery(domainClass, fieldsClass, rsql, legacyRsqlVisitor).getSQLString(); - } - - public & RsqlQueryField> DatabaseQuery createDbQuery(final Class domainClass, final Class fieldsClass, - final String rsql, final boolean legacyRsqlVisitor) { + public & RsqlQueryField> String toSQL( + final Class domainClass, final Class fieldsClass, final String rsql, final boolean legacyRsqlVisitor) { final CriteriaQuery 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 & RsqlQueryField> CriteriaQuery createQuery(final Class domainClass, final Class fieldsClass, - final String rsql, final boolean legacyRsqlVisitor) { + private & RsqlQueryField> CriteriaQuery createQuery( + final Class domainClass, final Class fieldsClass, final String rsql, final boolean legacyRsqlVisitor) { final CriteriaQuery query = entityManager.getCriteriaBuilder().createQuery(domainClass); final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); return query.where( @@ -78,16 +88,15 @@ private & RsqlQueryField> Predicate toPredicate( query.distinct(true); final RSQLVisitor, 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 accept = rootNode.accept(jpqQueryRSQLVisitor); if (CollectionUtils.isEmpty(accept)) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties b/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties index 8c0d0f6092..ab9117e809 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties @@ -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 diff --git a/hawkbit-repository/pom.xml b/hawkbit-repository/pom.xml index 76e2492643..c8ad17eeb3 100644 --- a/hawkbit-repository/pom.xml +++ b/hawkbit-repository/pom.xml @@ -17,6 +17,7 @@ hawkbit-parent ${revision} + hawkbit-repository hawkBit :: Repository :: Parent pom @@ -24,6 +25,9 @@ hawkbit-repository-api hawkbit-repository-core + hawkbit-repository-jpa-api + hawkbit-repository-jpa-eclipselink + hawkbit-repository-jpa-hibernate hawkbit-repository-jpa hawkbit-repository-jpa-flyway diff --git a/pom.xml b/pom.xml index 2eeec8f7b2..5ac40120fa 100644 --- a/pom.xml +++ b/pom.xml @@ -671,16 +671,6 @@ - - org.eclipse.persistence - org.eclipse.persistence.core - ${eclipselink.version} - - - org.eclipse.persistence - org.eclipse.persistence.jpa - ${eclipselink.version} - org.springframework.plugin spring-plugin-core