Hibernate support (#2147)
* Hibernate support --------- Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
46
.github/workflows/verify-hibernate.yml
vendored
Normal file
46
.github/workflows/verify-hibernate.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Verify (Hibernate)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '.3rd-party/**'
|
||||
- 'site/**'
|
||||
- '**.md'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.3rd-party/**'
|
||||
- 'site/**'
|
||||
- '**.md'
|
||||
|
||||
jobs:
|
||||
verify-hibernate:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
rabbitmq:
|
||||
image: rabbitmq:3-management-alpine
|
||||
env:
|
||||
RABBITMQ_DEFAULT_VHOST: /
|
||||
RABBITMQ_DEFAULT_USER: guest
|
||||
RABBITMQ_DEFAULT_PASS: guest
|
||||
ports:
|
||||
- 15672:15672
|
||||
- 5672:5672
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
cache: 'maven'
|
||||
|
||||
- name: Check file license headers
|
||||
run: mvn license:check --batch-mode
|
||||
|
||||
- name: Run tests & javadoc
|
||||
run: mvn verify javadoc:javadoc --batch-mode -Djpa.vendor=hibernate -Dlogging.level.org.hibernate.collection.spi.AbstractPersistentCollection=ERROR
|
||||
@@ -131,8 +131,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<MgmtDistributionSet> getDistributionSet(
|
||||
final Long distributionSetId) {
|
||||
public ResponseEntity<MgmtDistributionSet> getDistributionSet(final Long distributionSetId) {
|
||||
final DistributionSet foundDs = distributionSetManagement.getOrElseThrowException(distributionSetId);
|
||||
|
||||
final MgmtDistributionSet response = MgmtDistributionSetMapper.toResponse(foundDs);
|
||||
@@ -142,8 +141,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<List<MgmtDistributionSet>> createDistributionSets(
|
||||
final List<MgmtDistributionSetRequestBodyPost> sets) {
|
||||
public ResponseEntity<List<MgmtDistributionSet>> createDistributionSets(final List<MgmtDistributionSetRequestBodyPost> sets) {
|
||||
log.debug("creating {} distribution sets", sets.size());
|
||||
// set default Ds type if ds type is null
|
||||
final String defaultDsKey = systemSecurityContext
|
||||
|
||||
@@ -746,14 +746,4 @@ class MgmtDistributionSetTypeResourceTest extends AbstractManagementApiIntegrati
|
||||
assertThat(testType.getMandatoryModuleTypes()).containsExactly(osType);
|
||||
return testType;
|
||||
}
|
||||
|
||||
private void createSoftwareModulesAlphabetical(final int amount) {
|
||||
char character = 'a';
|
||||
for (int index = 0; index < amount; index++) {
|
||||
final String str = String.valueOf(character);
|
||||
softwareModuleManagement.create(
|
||||
entityFactory.softwareModule().create().name(str).description(str).vendor(str).version(str));
|
||||
character++;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
hawkbit-repository/hawkbit-repository-jpa-api/README.md
Normal file
3
hawkbit-repository/hawkbit-repository-jpa-api/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# hawkBit JPA EclipseLink Vendor integration
|
||||
|
||||
Implementation of [EclipseLink](http://www.eclipse.org/eclipselink/) JPA vendor.
|
||||
54
hawkbit-repository/hawkbit-repository-jpa-api/pom.xml
Normal file
54
hawkbit-repository/hawkbit-repository-jpa-api/pom.xml
Normal file
@@ -0,0 +1,54 @@
|
||||
<!--
|
||||
|
||||
Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
|
||||
This program and the accompanying materials are made
|
||||
available under the terms of the Eclipse Public License 2.0
|
||||
which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
|
||||
SPDX-License-Identifier: EPL-2.0
|
||||
|
||||
-->
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<version>${revision}</version>
|
||||
<artifactId>hawkbit-repository</artifactId>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-repository-jpa-api</artifactId>
|
||||
<name>hawkBit :: Repository :: JPA API</name>
|
||||
|
||||
<properties>
|
||||
<apt.source.dir>${project.build.directory}/generated-sources/apt/</apt.source.dir>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Static class generation -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-jpamodelgen</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -45,7 +45,7 @@ public class AfterTransactionCommitDefaultServiceExecutor implements AfterTransa
|
||||
@Override
|
||||
@SuppressWarnings({ "squid:S1217" })
|
||||
public void afterCompletion(final int status) {
|
||||
log.debug("Transaction completed after commit with status {}", status == STATUS_COMMITTED ? "COMMITTED" : "ROLLEDBACK");
|
||||
log.debug("Transaction completed after commit with status {}", status == TransactionSynchronization.STATUS_COMMITTED ? "COMMITTED" : "ROLLEDBACK");
|
||||
}
|
||||
|
||||
private void afterCommit(final Runnable runnable) {
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# hawkBit JPA EclipseLink Vendor integration
|
||||
|
||||
Implementation of [EclipseLink](http://www.eclipse.org/eclipselink/) JPA vendor.
|
||||
@@ -0,0 +1,49 @@
|
||||
<!--
|
||||
|
||||
Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
|
||||
This program and the accompanying materials are made
|
||||
available under the terms of the Eclipse Public License 2.0
|
||||
which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
|
||||
SPDX-License-Identifier: EPL-2.0
|
||||
|
||||
-->
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<version>${revision}</version>
|
||||
<artifactId>hawkbit-repository</artifactId>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-repository-jpa-eclipselink</artifactId>
|
||||
<name>hawkBit :: Repository :: JPA EclipseLink Vendor</name>
|
||||
|
||||
<properties>
|
||||
<apt.source.dir>${project.build.directory}/generated-sources/apt/</apt.source.dir>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-jpa-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Static class generation -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-jpamodelgen</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.persistence</groupId>
|
||||
<artifactId>org.eclipse.persistence.jpa</artifactId>
|
||||
<version>${eclipselink.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -49,7 +49,7 @@ import org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect;
|
||||
* 3.b.) the cause is not an {@link SQLException} and as a result cannot be
|
||||
* mapped.
|
||||
*/
|
||||
public class HawkbitEclipseLinkJpaDialect extends EclipseLinkJpaDialect {
|
||||
class HawkbitEclipseLinkJpaDialect extends EclipseLinkJpaDialect {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
@@ -99,4 +99,4 @@ public class HawkbitEclipseLinkJpaDialect extends EclipseLinkJpaDialect {
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,40 +16,35 @@ import java.util.stream.IntStream;
|
||||
import jakarta.persistence.Query;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
|
||||
@Slf4j
|
||||
public class Jpa {
|
||||
|
||||
public enum JpaVendor {
|
||||
ECLIPSELINK,
|
||||
HIBERNATE // NOT SUPPORTED!
|
||||
HIBERNATE
|
||||
}
|
||||
|
||||
public static final JpaVendor JPA_VENDOR = JpaVendor.ECLIPSELINK;
|
||||
static {
|
||||
log.info("JPA vendor: {}", JPA_VENDOR);
|
||||
}
|
||||
|
||||
public static char NATIVE_QUERY_PARAMETER_PREFIX = switch (JPA_VENDOR) {
|
||||
case ECLIPSELINK -> '?';
|
||||
case HIBERNATE -> ':';
|
||||
};
|
||||
public static final char NATIVE_QUERY_PARAMETER_PREFIX = '?';
|
||||
|
||||
public static <T> String formatNativeQueryInClause(final String name, final List<T> list) {
|
||||
return switch (Jpa.JPA_VENDOR) {
|
||||
case ECLIPSELINK -> formatEclipseLinkNativeQueryInClause(IntStream.range(0, list.size()).mapToObj(i -> name + "_" + i).toList());
|
||||
case HIBERNATE -> ":" + name;
|
||||
};
|
||||
return formatEclipseLinkNativeQueryInClause(IntStream.range(0, list.size()).mapToObj(i -> name + "_" + i).toList());
|
||||
}
|
||||
|
||||
public static <T> void setNativeQueryInParameter(final Query deleteQuery, final String name, final List<T> list) {
|
||||
if (Jpa.JPA_VENDOR == Jpa.JpaVendor.ECLIPSELINK) {
|
||||
for (int i = 0, len = list.size(); i < len; i++) {
|
||||
deleteQuery.setParameter(name + "_" + i, list.get(i));
|
||||
}
|
||||
} else if (Jpa.JPA_VENDOR == Jpa.JpaVendor.HIBERNATE) {
|
||||
deleteQuery.setParameter(name, list);
|
||||
for (int i = 0, len = list.size(); i < len; i++) {
|
||||
deleteQuery.setParameter(name + "_" + i, list.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatEclipseLinkNativeQueryInClause(final Collection<String> elements) {
|
||||
return "?" + String.join(",?", elements);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.jpa;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.eclipse.persistence.config.PersistenceUnitProperties;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
|
||||
import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter;
|
||||
import org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect;
|
||||
import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.jta.JtaTransactionManager;
|
||||
|
||||
/**
|
||||
* General EclipseLink configuration for hawkBit's Repository.
|
||||
*/
|
||||
@Configuration
|
||||
public class JpaConfiguration extends JpaBaseConfiguration {
|
||||
|
||||
protected JpaConfiguration(
|
||||
final DataSource dataSource, final JpaProperties properties,
|
||||
final ObjectProvider<JtaTransactionManager> jtaTransactionManagerProvider) {
|
||||
super(dataSource, properties, jtaTransactionManagerProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link MultiTenantJpaTransactionManager} bean.
|
||||
*
|
||||
* @return a new {@link PlatformTransactionManager}
|
||||
* @see org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration#transactionManager(ObjectProvider)
|
||||
*/
|
||||
@Override
|
||||
@Bean
|
||||
public PlatformTransactionManager transactionManager(final ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
|
||||
return new MultiTenantJpaTransactionManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
|
||||
return new EclipseLinkJpaVendorAdapter() {
|
||||
|
||||
private final HawkbitEclipseLinkJpaDialect jpaDialect = new HawkbitEclipseLinkJpaDialect();
|
||||
|
||||
@Override
|
||||
public EclipseLinkJpaDialect getJpaDialect() {
|
||||
return jpaDialect;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getVendorProperties() {
|
||||
final Map<String, Object> properties = new HashMap<>(7);
|
||||
// Turn off dynamic weaving to disable LTW lookup in static weaving mode
|
||||
properties.put(PersistenceUnitProperties.WEAVING, "false");
|
||||
// needed for reports
|
||||
properties.put(PersistenceUnitProperties.ALLOW_NATIVE_SQL_QUERIES, "true");
|
||||
// flyway
|
||||
properties.put(PersistenceUnitProperties.DDL_GENERATION, "none");
|
||||
// Embed into hawkBit logging
|
||||
properties.put(PersistenceUnitProperties.LOGGING_LOGGER, "JavaLogger");
|
||||
// Ensure that we flush only at the end of the transaction
|
||||
properties.put(PersistenceUnitProperties.PERSISTENCE_CONTEXT_FLUSH_MODE, "COMMIT");
|
||||
// Enable batch writing
|
||||
properties.put(PersistenceUnitProperties.BATCH_WRITING, "JDBC");
|
||||
// Batch size
|
||||
properties.put(PersistenceUnitProperties.BATCH_WRITING_SIZE, "500");
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.jpa;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.transaction.Transaction;
|
||||
|
||||
import org.eclipse.hawkbit.repository.jpa.model.EntityPropertyChangeListener;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.eclipse.persistence.config.PersistenceUnitProperties;
|
||||
import org.eclipse.persistence.descriptors.ClassDescriptor;
|
||||
import org.eclipse.persistence.descriptors.DescriptorEventManager;
|
||||
import org.eclipse.persistence.sessions.Session;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.orm.jpa.EntityManagerHolder;
|
||||
import org.springframework.orm.jpa.JpaTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
/**
|
||||
* {@link JpaTransactionManager} that sets the {@link TenantAware#getCurrentTenant()} in the eclipselink session. This has
|
||||
* to be done in eclipselink after a {@link Transaction} has been started.
|
||||
* <p/>
|
||||
* The class also handles setting the {@link EntityPropertyChangeListener} to the {@link DescriptorEventManager} of the
|
||||
*/
|
||||
class MultiTenantJpaTransactionManager extends JpaTransactionManager {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Autowired
|
||||
private transient TenantAware tenantAware;
|
||||
|
||||
private static final Class<?> JPA_TARGET;
|
||||
|
||||
static {
|
||||
try {
|
||||
JPA_TARGET = Class.forName("org.eclipse.hawkbit.repository.jpa.model.JpaTarget");
|
||||
} catch (final RuntimeException e) {
|
||||
throw e;
|
||||
} catch (final Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final EntityPropertyChangeListener ENTITY_PROPERTY_CHANGE_LISTENER = new EntityPropertyChangeListener();
|
||||
|
||||
@Override
|
||||
protected void doBegin(final Object transaction, final TransactionDefinition definition) {
|
||||
super.doBegin(transaction, definition);
|
||||
|
||||
final EntityManager em = Objects.requireNonNull(
|
||||
(EntityManagerHolder) TransactionSynchronizationManager.getResource(
|
||||
Objects.requireNonNull(
|
||||
getEntityManagerFactory(),
|
||||
"No EntityManagerFactory provided by TransactionSynchronizationManager")),
|
||||
"No EntityManagerHolder provided by TransactionSynchronizationManager")
|
||||
.getEntityManager();
|
||||
|
||||
final ClassDescriptor classDescriptor = em.unwrap(Session.class).getClassDescriptor(JPA_TARGET);
|
||||
if (classDescriptor != null) {
|
||||
final DescriptorEventManager dem = classDescriptor.getEventManager();
|
||||
if (dem != null && !dem.getEventListeners().contains(ENTITY_PROPERTY_CHANGE_LISTENER)) {
|
||||
dem.addListener(ENTITY_PROPERTY_CHANGE_LISTENER);
|
||||
}
|
||||
}
|
||||
|
||||
final String currentTenant = tenantAware.getCurrentTenant();
|
||||
if (currentTenant == null) {
|
||||
cleanupTenant(em);
|
||||
} else {
|
||||
em.setProperty(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, currentTenant.toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupTenant(final EntityManager em) {
|
||||
if (em.isOpen() && em.getProperties().containsKey(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT)) {
|
||||
em.setProperty(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -55,10 +55,7 @@ public abstract class AbstractJpaTenantAwareBaseEntity extends AbstractJpaBaseEn
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = super.hashCode();
|
||||
result = prime * result + (tenant == null ? 0 : tenant.hashCode());
|
||||
return result;
|
||||
return 31 * super.hashCode() + (tenant == null ? 0 : tenant.hashCode());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,13 +70,12 @@ public abstract class AbstractJpaTenantAwareBaseEntity extends AbstractJpaBaseEn
|
||||
if (!super.equals(obj)) {
|
||||
return false;
|
||||
}
|
||||
final AbstractJpaTenantAwareBaseEntity other = (AbstractJpaTenantAwareBaseEntity) obj;
|
||||
return Objects.equals(getTenant(), other.getTenant());
|
||||
return Objects.equals(getTenant(), ((AbstractJpaTenantAwareBaseEntity) obj).getTenant());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BaseEntity [id=" + super.getId() + "]";
|
||||
return getClass().getSimpleName() + " [tenant=" + getTenant() + ", id=" + getId() + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.jpa.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.persistence.descriptors.DescriptorEvent;
|
||||
import org.eclipse.persistence.descriptors.DescriptorEventAdapter;
|
||||
import org.eclipse.persistence.queries.UpdateObjectQuery;
|
||||
|
||||
/**
|
||||
* Listens to updates on <code></code>JpaTarget</code> entities, filtering out updates that only change the
|
||||
* "lastTargetQuery" or "address" fields.
|
||||
*/
|
||||
public class EntityPropertyChangeListener extends DescriptorEventAdapter {
|
||||
|
||||
private static final List<String> TARGET_UPDATE_EVENT_IGNORE_FIELDS = List.of(
|
||||
"lastTargetQuery", "address", // actual to be skipped
|
||||
"optLockRevision", "lastModifiedAt", "lastModifiedBy" // system to be skipped
|
||||
);
|
||||
|
||||
@Override
|
||||
public void postUpdate(final DescriptorEvent event) {
|
||||
final Object object = event.getObject();
|
||||
if (((UpdateObjectQuery) event.getQuery()).getObjectChangeSet().getChangedAttributeNames().stream()
|
||||
.anyMatch(field -> !TARGET_UPDATE_EVENT_IGNORE_FIELDS.contains(field))) {
|
||||
AbstractJpaBaseEntity.doNotify(() -> ((EventAwareEntity) object).fireUpdateEvent());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,22 +30,21 @@ import org.springframework.dao.UncategorizedDataAccessException;
|
||||
*/
|
||||
@Feature("Unit Tests - Repository")
|
||||
@Story("Exception handling")
|
||||
public class HawkBitEclipseLinkJpaDialectTest {
|
||||
class HawkBitEclipseLinkJpaDialectTest {
|
||||
|
||||
private final HawkbitEclipseLinkJpaDialect hawkBitEclipseLinkJpaDialectUnderTest = new HawkbitEclipseLinkJpaDialect();
|
||||
|
||||
@Test
|
||||
@Description("Use Case: PersistenceException that can be mapped by EclipseLinkJpaDialect into corresponding DataAccessException.")
|
||||
public void jpaOptimisticLockExceptionIsConcurrencyFailureException() {
|
||||
assertThat(
|
||||
hawkBitEclipseLinkJpaDialectUnderTest.translateExceptionIfPossible(mock(OptimisticLockException.class)))
|
||||
void jpaOptimisticLockExceptionIsConcurrencyFailureException() {
|
||||
assertThat(hawkBitEclipseLinkJpaDialectUnderTest.translateExceptionIfPossible(mock(OptimisticLockException.class)))
|
||||
.isInstanceOf(ConcurrencyFailureException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Use Case: PersistenceException that could not be mapped by EclipseLinkJpaDialect directly but "
|
||||
+ "instead is wrapped into JpaSystemException. Cause of PersistenceException is an SQLException.")
|
||||
public void jpaSystemExceptionWithSqlDeadLockExceptionIsConcurrencyFailureException() {
|
||||
void jpaSystemExceptionWithSqlDeadLockExceptionIsConcurrencyFailureException() {
|
||||
final PersistenceException persEception = mock(PersistenceException.class);
|
||||
when(persEception.getCause()).thenReturn(new SQLException("simulated transaction ER_LOCK_DEADLOCK", "40001"));
|
||||
|
||||
@@ -56,7 +55,7 @@ public class HawkBitEclipseLinkJpaDialectTest {
|
||||
@Test
|
||||
@Description("Use Case: PersistenceException that could not be mapped by EclipseLinkJpaDialect directly but instead is wrapped"
|
||||
+ " into JpaSystemException. Cause of PersistenceException is not an SQLException.")
|
||||
public void jpaSystemExceptionWithNumberFormatExceptionIsNull() {
|
||||
void jpaSystemExceptionWithNumberFormatExceptionIsNull() {
|
||||
final PersistenceException persEception = mock(PersistenceException.class);
|
||||
when(persEception.getCause()).thenReturn(new NumberFormatException());
|
||||
|
||||
@@ -67,7 +66,7 @@ public class HawkBitEclipseLinkJpaDialectTest {
|
||||
@Test
|
||||
@Description("Use Case: RuntimeException that could not be mapped by EclipseLinkJpaDialect directly. Cause of "
|
||||
+ "RuntimeException is an SQLException.")
|
||||
public void runtimeExceptionWithSqlDeadLockExceptionIsConcurrencyFailureException() {
|
||||
void runtimeExceptionWithSqlDeadLockExceptionIsConcurrencyFailureException() {
|
||||
final RuntimeException persEception = mock(RuntimeException.class);
|
||||
when(persEception.getCause()).thenReturn(new SQLException("simulated transaction ER_LOCK_DEADLOCK", "40001"));
|
||||
|
||||
@@ -78,11 +77,10 @@ public class HawkBitEclipseLinkJpaDialectTest {
|
||||
@Test
|
||||
@Description("Use Case: RuntimeException that could not be mapped by EclipseLinkJpaDialect directly. Cause of "
|
||||
+ "RuntimeException is not an SQLException.")
|
||||
public void runtimeExceptionWithNumberFormatExceptionIsNull() {
|
||||
void runtimeExceptionWithNumberFormatExceptionIsNull() {
|
||||
final RuntimeException persEception = mock(RuntimeException.class);
|
||||
when(persEception.getCause()).thenReturn(new NumberFormatException());
|
||||
|
||||
assertThat(hawkBitEclipseLinkJpaDialectUnderTest.translateExceptionIfPossible(persEception)).isNull();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# hawkBit JPA Hibernate Vendor integration
|
||||
|
||||
Implementation of [Hibernate](https://hibernate.org/) JPA vendor.
|
||||
|
||||
To use this vendor you could exclude the org.eclipse.hawkbit:hawkbit.repository-jpa-eclipselink and include this module.
|
||||
For instance if you use org.eclipse.hawkbit:hawkbit-repository-jpa via org.eclipse.hawkbit:hawkbit-starter you could do it like this:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-starter</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-jpa-eclipselink</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-jpa-hibernate</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
46
hawkbit-repository/hawkbit-repository-jpa-hibernate/pom.xml
Normal file
46
hawkbit-repository/hawkbit-repository-jpa-hibernate/pom.xml
Normal file
@@ -0,0 +1,46 @@
|
||||
<!--
|
||||
|
||||
Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
|
||||
This program and the accompanying materials are made
|
||||
available under the terms of the Eclipse Public License 2.0
|
||||
which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
|
||||
SPDX-License-Identifier: EPL-2.0
|
||||
|
||||
-->
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<version>${revision}</version>
|
||||
<artifactId>hawkbit-repository</artifactId>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-repository-jpa-hibernate</artifactId>
|
||||
<name>hawkBit :: Repository :: JPA Hibernate Vendor</name>
|
||||
|
||||
<properties>
|
||||
<apt.source.dir>${project.build.directory}/generated-sources/apt/</apt.source.dir>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-jpa-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Static class generation -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-jpamodelgen</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.jpa;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.persistence.Query;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
|
||||
@Slf4j
|
||||
public class Jpa {
|
||||
|
||||
public enum JpaVendor {
|
||||
ECLIPSELINK,
|
||||
HIBERNATE
|
||||
}
|
||||
|
||||
public static final JpaVendor JPA_VENDOR = JpaVendor.HIBERNATE;
|
||||
static {
|
||||
log.info("JPA vendor: {}", JPA_VENDOR);
|
||||
}
|
||||
|
||||
public static final char NATIVE_QUERY_PARAMETER_PREFIX = ':';
|
||||
|
||||
public static <T> String formatNativeQueryInClause(final String name, final List<T> list) {
|
||||
return ":" + name;
|
||||
}
|
||||
|
||||
public static <T> void setNativeQueryInParameter(final Query deleteQuery, final String name, final List<T> list) {
|
||||
deleteQuery.setParameter(name, list);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.jpa;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.eclipse.hawkbit.repository.jpa.model.EntityPropertyChangeListener;
|
||||
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.hibernate.boot.Metadata;
|
||||
import org.hibernate.boot.spi.BootstrapContext;
|
||||
import org.hibernate.cfg.MultiTenancySettings;
|
||||
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
|
||||
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||
import org.hibernate.event.service.spi.EventListenerRegistry;
|
||||
import org.hibernate.event.spi.EventType;
|
||||
import org.hibernate.integrator.spi.Integrator;
|
||||
import org.hibernate.jpa.boot.spi.IntegratorProvider;
|
||||
import org.hibernate.service.spi.SessionFactoryServiceRegistry;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter;
|
||||
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
|
||||
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
|
||||
import org.springframework.transaction.jta.JtaTransactionManager;
|
||||
|
||||
/**
|
||||
* General Hibernate configuration for hawkBit's Repository.
|
||||
*/
|
||||
@Configuration
|
||||
public class JpaConfiguration extends JpaBaseConfiguration {
|
||||
|
||||
private final TenantIdentifier tenantIdentifier;
|
||||
|
||||
protected JpaConfiguration(
|
||||
final DataSource dataSource, final JpaProperties properties,
|
||||
final ObjectProvider<JtaTransactionManager> jtaTransactionManagerProvider,
|
||||
final TenantAware tenantAware) {
|
||||
super(dataSource, properties, jtaTransactionManagerProvider);
|
||||
tenantIdentifier = new TenantIdentifier(tenantAware);
|
||||
}
|
||||
|
||||
@Bean
|
||||
CurrentTenantIdentifierResolver<String> currentTenantIdentifierResolver() {
|
||||
return tenantIdentifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
|
||||
return new HibernateJpaVendorAdapter() {
|
||||
|
||||
private final HibernateJpaDialect jpaDialect = new HibernateJpaDialect();
|
||||
|
||||
@Override
|
||||
public HibernateJpaDialect getJpaDialect() {
|
||||
return jpaDialect;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getVendorProperties() {
|
||||
final Map<String, Object> properties = new HashMap<>(4);
|
||||
|
||||
properties.put(MultiTenancySettings.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifier);
|
||||
properties.put("hibernate.multiTenancy", "DISCRIMINATOR");
|
||||
// LAZY_LOAD - Enable lazy loading of lazy fields when session is closed - N + 1 problem occur.
|
||||
// So it would be good if in future hawkBit run without that
|
||||
// Otherwise, if false, call for the lazy field will throw LazyInitializationException
|
||||
properties.put("hibernate.enable_lazy_load_no_trans", "true");
|
||||
properties.put("hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList(new Integrator() {
|
||||
|
||||
@Override
|
||||
public void integrate(
|
||||
final Metadata metadata, final BootstrapContext bootstrapContext,
|
||||
final SessionFactoryImplementor sessionFactory) {
|
||||
sessionFactory.getServiceRegistry()
|
||||
.getService(EventListenerRegistry.class)
|
||||
.appendListeners(EventType.POST_UPDATE, new EntityPropertyChangeListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disintegrate(final SessionFactoryImplementor sessionFactory, final SessionFactoryServiceRegistry serviceRegistry) {
|
||||
// do nothing
|
||||
}
|
||||
}));
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.jpa;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
|
||||
|
||||
/**
|
||||
* {@link CurrentTenantIdentifierResolver} and {@link HibernatePropertiesCustomizer} that resolves the
|
||||
* {@link TenantAware#getCurrentTenant()} for hibernate.
|
||||
*/
|
||||
class TenantIdentifier implements CurrentTenantIdentifierResolver<String> {
|
||||
|
||||
private final TenantAware tenantAware;
|
||||
|
||||
TenantIdentifier(final TenantAware tenantAware) {
|
||||
this.tenantAware = tenantAware;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveCurrentTenantIdentifier() {
|
||||
// on bootstrapping hibernate requests tenant and want to be non-null
|
||||
return Optional.ofNullable(tenantAware.getCurrentTenant()).map(String::toUpperCase).orElse("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateExistingCurrentSessions() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.jpa.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.hibernate.event.spi.PostUpdateEvent;
|
||||
import org.hibernate.event.spi.PostUpdateEventListener;
|
||||
import org.hibernate.persister.entity.EntityPersister;
|
||||
|
||||
/**
|
||||
* Listens to updates on <code></code>JpaTarget</code> entities, filtering out updates that only change the
|
||||
* "lastTargetQuery" or "address" fields.
|
||||
*/
|
||||
public class EntityPropertyChangeListener implements PostUpdateEventListener {
|
||||
|
||||
private static final List<String> TARGET_UPDATE_EVENT_IGNORE_FIELDS = List.of(
|
||||
"lastTargetQuery", "address", // actual to be skipped
|
||||
"optLockRevision", "lastModifiedAt", "lastModifiedBy" // system to be skipped
|
||||
);
|
||||
|
||||
private static final Class<?> JPA_TARGET;
|
||||
static {
|
||||
try {
|
||||
JPA_TARGET = Class.forName("org.eclipse.hawkbit.repository.jpa.model.JpaTarget");
|
||||
} catch (final RuntimeException e) {
|
||||
throw e;
|
||||
} catch (final Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostUpdate(final PostUpdateEvent event) {
|
||||
if (!JPA_TARGET.isAssignableFrom(event.getEntity().getClass())) {
|
||||
// only target entity updates goes through here
|
||||
return;
|
||||
}
|
||||
|
||||
boolean lastTargetQueryChanged = false;
|
||||
boolean hasNonIgnoredChanges = false;
|
||||
for (int i : event.getDirtyProperties()) {
|
||||
final String attribute = event.getPersister().getAttributeMapping(i).getAttributeName();
|
||||
if ("lastTargetQuery".equals(attribute)) {
|
||||
lastTargetQueryChanged = true;
|
||||
} else if (!TARGET_UPDATE_EVENT_IGNORE_FIELDS.contains(attribute)) {
|
||||
hasNonIgnoredChanges = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNonIgnoredChanges || !lastTargetQueryChanged) {
|
||||
AbstractJpaBaseEntity.doNotify(() -> ((EventAwareEntity) event.getEntity()).fireUpdateEvent());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresPostCommitHandling(final EntityPersister persister) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -23,32 +23,65 @@
|
||||
|
||||
<properties>
|
||||
<apt.source.dir>${project.build.directory}/generated-sources/apt/</apt.source.dir>
|
||||
<jpa.vendor>eclipselink</jpa.vendor>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>eclipselink</id>
|
||||
<activation>
|
||||
<property>
|
||||
<!-- default, if not set - eclipse link -->
|
||||
<name>!jpa.vendor</name>
|
||||
</property>
|
||||
</activation>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-jpa-eclipselink</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>hibernate</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>jpa.vendor</name>
|
||||
<value>hibernate</value>
|
||||
</property>
|
||||
</activation>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-jpa-hibernate</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<dependencies>
|
||||
<!-- Hawkbit -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-api</artifactId>
|
||||
<artifactId>hawkbit-repository-jpa-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Static class generation -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-jpamodelgen</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.persistence</groupId>
|
||||
<artifactId>org.eclipse.persistence.jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
@@ -70,13 +103,6 @@
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Static class generation -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-jpamodelgen</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
@@ -90,30 +116,4 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<!-- Should be placed here below 'eclipselink-maven-plugin' as the plugin should be executed -->
|
||||
<!-- after the code generation and the order of the plugins in the pom is relevant for maven -->
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>add-source</id>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>add-source</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sources>
|
||||
<source>${apt.source.dir}</source>
|
||||
</sources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -9,14 +9,10 @@
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.jpa;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.validation.Validation;
|
||||
|
||||
@@ -81,7 +77,6 @@ import org.eclipse.hawkbit.repository.jpa.builder.JpaSoftwareModuleMetadataBuild
|
||||
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetBuilder;
|
||||
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetFilterQueryBuilder;
|
||||
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetTypeBuilder;
|
||||
import org.eclipse.hawkbit.repository.jpa.configuration.MultiTenantJpaTransactionManager;
|
||||
import org.eclipse.hawkbit.repository.jpa.event.JpaEventEntityManager;
|
||||
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitDefaultServiceExecutor;
|
||||
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
|
||||
@@ -166,10 +161,7 @@ import org.eclipse.hawkbit.security.SystemSecurityContext;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver;
|
||||
import org.eclipse.hawkbit.utils.TenantConfigHelper;
|
||||
import org.eclipse.persistence.config.PersistenceUnitProperties;
|
||||
import org.hibernate.validator.BaseHibernateValidatorConfiguration;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
@@ -177,9 +169,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
|
||||
import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
@@ -191,14 +181,10 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.integration.support.locks.LockRegistry;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter;
|
||||
import org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect;
|
||||
import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter;
|
||||
import org.springframework.retry.annotation.EnableRetry;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
import org.springframework.transaction.jta.JtaTransactionManager;
|
||||
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
|
||||
|
||||
/**
|
||||
@@ -213,15 +199,9 @@ import org.springframework.validation.beanvalidation.MethodValidationPostProcess
|
||||
@EnableRetry
|
||||
@EntityScan("org.eclipse.hawkbit.repository.jpa.model")
|
||||
@PropertySource("classpath:/hawkbit-jpa-defaults.properties")
|
||||
@Import({ RepositoryDefaultConfiguration.class, DataSourceAutoConfiguration.class,
|
||||
SystemManagementCacheKeyGenerator.class })
|
||||
@Import({ JpaConfiguration.class, RepositoryDefaultConfiguration.class, DataSourceAutoConfiguration.class, SystemManagementCacheKeyGenerator.class })
|
||||
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
|
||||
public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
|
||||
|
||||
protected RepositoryApplicationConfiguration(final DataSource dataSource, final JpaProperties properties,
|
||||
final ObjectProvider<JtaTransactionManager> jtaTransactionManagerProvider) {
|
||||
super(dataSource, properties, jtaTransactionManagerProvider);
|
||||
}
|
||||
public class RepositoryApplicationConfiguration {
|
||||
|
||||
/**
|
||||
* Defines the validation processor bean.
|
||||
@@ -233,58 +213,14 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
|
||||
final MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
|
||||
// ValidatorFactory shall NOT be closed because after closing the generated Validator
|
||||
// methods shall not be called - we need the validator in future
|
||||
processor.setValidator(Validation.byDefaultProvider().configure()
|
||||
.addProperty(BaseHibernateValidatorConfiguration.ALLOW_PARALLEL_METHODS_DEFINE_PARAMETER_CONSTRAINTS,"true")
|
||||
.buildValidatorFactory().getValidator());
|
||||
processor.setValidator(Validation.byDefaultProvider()
|
||||
.configure()
|
||||
.addProperty(org.hibernate.validator.BaseHibernateValidatorConfiguration.ALLOW_PARALLEL_METHODS_DEFINE_PARAMETER_CONSTRAINTS, "true")
|
||||
.buildValidatorFactory()
|
||||
.getValidator());
|
||||
return processor;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link MultiTenantJpaTransactionManager} bean.
|
||||
*
|
||||
* @return a new {@link PlatformTransactionManager}
|
||||
* @see org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration#transactionManager(ObjectProvider)
|
||||
*/
|
||||
@Override
|
||||
@Bean
|
||||
public PlatformTransactionManager transactionManager(
|
||||
final ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
|
||||
return new MultiTenantJpaTransactionManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
|
||||
return new EclipseLinkJpaVendorAdapter() {
|
||||
|
||||
private final HawkbitEclipseLinkJpaDialect jpaDialect = new HawkbitEclipseLinkJpaDialect();
|
||||
|
||||
@Override
|
||||
public EclipseLinkJpaDialect getJpaDialect() {
|
||||
return jpaDialect;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getVendorProperties() {
|
||||
final Map<String, Object> properties = new HashMap<>(7);
|
||||
// Turn off dynamic weaving to disable LTW lookup in static weaving mode
|
||||
properties.put(PersistenceUnitProperties.WEAVING, "false");
|
||||
// needed for reports
|
||||
properties.put(PersistenceUnitProperties.ALLOW_NATIVE_SQL_QUERIES, "true");
|
||||
// flyway
|
||||
properties.put(PersistenceUnitProperties.DDL_GENERATION, "none");
|
||||
// Embed into hawkBit logging
|
||||
properties.put(PersistenceUnitProperties.LOGGING_LOGGER, "JavaLogger");
|
||||
// Ensure that we flush only at the end of the transaction
|
||||
properties.put(PersistenceUnitProperties.PERSISTENCE_CONTEXT_FLUSH_MODE, "COMMIT");
|
||||
// Enable batch writing
|
||||
properties.put(PersistenceUnitProperties.BATCH_WRITING, "JDBC");
|
||||
// Batch size
|
||||
properties.put(PersistenceUnitProperties.BATCH_WRITING_SIZE, "500");
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BeanPostProcessor entityManagerBeanPostProcessor(
|
||||
@Autowired(required = false) final AccessController<JpaArtifact> artifactAccessController,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import org.eclipse.hawkbit.artifact.repository.HashNotMatchException;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission;
|
||||
import org.eclipse.hawkbit.repository.ArtifactEncryptionService;
|
||||
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
||||
import org.eclipse.hawkbit.repository.QuotaManagement;
|
||||
@@ -36,7 +35,6 @@ import org.eclipse.hawkbit.repository.exception.InvalidMD5HashException;
|
||||
import org.eclipse.hawkbit.repository.exception.InvalidSHA1HashException;
|
||||
import org.eclipse.hawkbit.repository.exception.InvalidSHA256HashException;
|
||||
import org.eclipse.hawkbit.repository.jpa.EncryptionAwareDbArtifact;
|
||||
import org.eclipse.hawkbit.repository.jpa.Jpa;
|
||||
import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper;
|
||||
import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
|
||||
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
|
||||
@@ -53,17 +51,13 @@ import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.ArtifactUpload;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.ConcurrencyFailureException;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.retry.annotation.Backoff;
|
||||
import org.springframework.retry.annotation.Retryable;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,7 +36,6 @@ import jakarta.validation.constraints.NotEmpty;
|
||||
import org.apache.commons.collections4.ListUtils;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetManagement;
|
||||
import org.eclipse.hawkbit.repository.FilterParams;
|
||||
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
|
||||
import org.eclipse.hawkbit.repository.QuotaManagement;
|
||||
import org.eclipse.hawkbit.repository.TargetFields;
|
||||
import org.eclipse.hawkbit.repository.TargetManagement;
|
||||
@@ -90,7 +89,6 @@ import org.springframework.dao.ConcurrencyFailureException;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.orm.jpa.vendor.Database;
|
||||
import org.springframework.retry.annotation.Backoff;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.persistence.Access;
|
||||
import jakarta.persistence.AccessType;
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.ConstraintMode;
|
||||
@@ -37,7 +35,6 @@ import jakarta.persistence.NamedEntityGraph;
|
||||
import jakarta.persistence.NamedEntityGraphs;
|
||||
import jakarta.persistence.NamedSubgraph;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.PostUpdate;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
@@ -56,7 +53,6 @@ import org.eclipse.hawkbit.repository.model.Rollout;
|
||||
import org.eclipse.hawkbit.repository.model.RolloutGroup;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
|
||||
/**
|
||||
* JPA implementation of {@link Action}.
|
||||
|
||||
@@ -16,7 +16,6 @@ import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
@@ -27,7 +26,6 @@ import jakarta.persistence.Convert;
|
||||
import jakarta.persistence.Converter;
|
||||
import jakarta.persistence.ElementCollection;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.ForeignKey;
|
||||
import jakarta.persistence.Index;
|
||||
@@ -67,11 +65,6 @@ import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
|
||||
import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder;
|
||||
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
|
||||
import org.eclipse.hawkbit.security.SystemSecurityContext;
|
||||
import org.eclipse.persistence.descriptors.DescriptorEvent;
|
||||
import org.eclipse.persistence.descriptors.DescriptorEventAdapter;
|
||||
import org.eclipse.persistence.queries.UpdateObjectQuery;
|
||||
import org.hibernate.event.spi.PreUpdateEvent;
|
||||
import org.hibernate.event.spi.PreUpdateEventListener;
|
||||
|
||||
/**
|
||||
* JPA implementation of {@link Target}.
|
||||
@@ -88,7 +81,6 @@ import org.hibernate.event.spi.PreUpdateEventListener;
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = { "controller_id", "tenant" }, name = "uk_tenant_controller_id"))
|
||||
// exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities
|
||||
@SuppressWarnings("squid:S2160")
|
||||
@EntityListeners({ JpaTarget.EntityPropertyChangeListener.class }) // add listener to the listeners declared into suppers
|
||||
@Slf4j
|
||||
public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAwareEntity {
|
||||
|
||||
@@ -330,40 +322,4 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
|
||||
), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to updates on {@link JpaTarget} entities, Filtering out updates that only change the "lastTargetQuery" or "address" fields.
|
||||
*/
|
||||
public static class EntityPropertyChangeListener extends DescriptorEventAdapter implements PreUpdateEventListener {
|
||||
|
||||
private static final List<String> TARGET_UPDATE_EVENT_IGNORE_FIELDS = List.of(
|
||||
"lastTargetQuery", "address", // actual to be skipped
|
||||
"optLockRevision", "lastModifiedAt", "lastModifiedBy" // system to be skipped
|
||||
);
|
||||
|
||||
@Override
|
||||
public void postUpdate(final DescriptorEvent event) {
|
||||
final Object object = event.getObject();
|
||||
if (((UpdateObjectQuery) event.getQuery()).getObjectChangeSet().getChangedAttributeNames().stream()
|
||||
.anyMatch(field -> !TARGET_UPDATE_EVENT_IGNORE_FIELDS.contains(field))) {
|
||||
doNotify(() -> ((EventAwareEntity) object).fireUpdateEvent());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreUpdate(final PreUpdateEvent event) {
|
||||
final Object[] oldState = event.getOldState();
|
||||
final Object[] newState = event.getState();
|
||||
for (int i = 0; i < newState.length; i++) {
|
||||
if (!Objects.equals(oldState[i], newState[i])) {
|
||||
final String attribute = event.getPersister().getAttributeMapping(i).getAttributeName();
|
||||
if (!TARGET_UPDATE_EVENT_IGNORE_FIELDS.contains(attribute)) {
|
||||
doNotify(() -> ((EventAwareEntity) event.getEntity()).fireUpdateEvent());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,6 @@ import org.eclipse.hawkbit.repository.rsql.SuggestToken;
|
||||
import org.eclipse.hawkbit.repository.rsql.SuggestionContext;
|
||||
import org.eclipse.hawkbit.repository.rsql.SyntaxErrorContext;
|
||||
import org.eclipse.hawkbit.repository.rsql.ValidationOracleContext;
|
||||
import org.eclipse.persistence.exceptions.ConversionException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@@ -75,8 +74,12 @@ public class RsqlParserValidationOracle implements RsqlValidationOracle {
|
||||
} catch (final RSQLParameterUnsupportedFieldException | IllegalArgumentException ex) {
|
||||
errorContext.setErrorMessage(getCustomMessage(ex.getMessage(), null));
|
||||
log.trace("Illegal argument on parsing :", ex);
|
||||
} catch (@SuppressWarnings("squid:S1166") final ConversionException | JpaSystemException e) {
|
||||
} catch (final JpaSystemException e) {
|
||||
// noop
|
||||
} catch (final RuntimeException e) {
|
||||
if (!"org.eclipse.persistence.exceptions.ConversionException".equals(e.getClass().getName())) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -37,8 +37,7 @@ class ActionTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
@Test
|
||||
@Description("Ensures that timeforced moded switch from soft to forces after defined timeframe.")
|
||||
void timeforcedHitNewHasCodeIsGenerated() {
|
||||
|
||||
void timeForcedHitNewHasCodeIsGenerated() {
|
||||
// current time + 1 seconds
|
||||
final long sleepTime = 1000;
|
||||
final long timeForceTimeAt = System.currentTimeMillis() + sleepTime;
|
||||
@@ -48,8 +47,7 @@ class ActionTest extends AbstractJpaIntegrationTest {
|
||||
assertThat(timeforcedAction.isForcedOrTimeForced()).isFalse();
|
||||
|
||||
// wait until timeforce time is hit
|
||||
Awaitility.await().atMost(Duration.ofSeconds(2)).pollInterval(Duration.ofMillis(100))
|
||||
.until(timeforcedAction::isForcedOrTimeForced);
|
||||
Awaitility.await().atMost(Duration.ofSeconds(2)).pollInterval(Duration.ofMillis(100)).until(timeforcedAction::isForcedOrTimeForced);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -57,12 +55,11 @@ class ActionTest extends AbstractJpaIntegrationTest {
|
||||
void testActionTypeConvert() {
|
||||
final long id = createAction().getId();
|
||||
for (final ActionType actionType : ActionType.values()) {
|
||||
final JpaAction action = actionRepository
|
||||
.findById(id).orElseThrow(() -> new IllegalStateException("Action not found"));
|
||||
final JpaAction action = actionRepository.findById(id).orElseThrow(() -> new IllegalStateException("Action not found"));
|
||||
action.setActionType(actionType);
|
||||
actionRepository.save(action);
|
||||
assertThat(actionRepository.findById(id).orElseThrow(() -> new IllegalStateException("Action not found"))
|
||||
.getActionType()).isEqualTo(actionType);
|
||||
assertThat(actionRepository.findById(id).orElseThrow(() -> new IllegalStateException("Action not found")).getActionType())
|
||||
.isEqualTo(actionType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,12 +68,11 @@ class ActionTest extends AbstractJpaIntegrationTest {
|
||||
void testStatusConvert() {
|
||||
final long id = createAction().getId();
|
||||
for (final Status status : Status.values()) {
|
||||
final JpaAction action = actionRepository
|
||||
.findById(id).orElseThrow(() -> new IllegalStateException("Action not found"));
|
||||
final JpaAction action = actionRepository.findById(id).orElseThrow(() -> new IllegalStateException("Action not found"));
|
||||
action.setStatus(status);
|
||||
actionRepository.save(action);
|
||||
assertThat(actionRepository.findById(id).orElseThrow(() -> new IllegalStateException("Action not found"))
|
||||
.getStatus()).isEqualTo(status);
|
||||
assertThat(actionRepository.findById(id).orElseThrow(() -> new IllegalStateException("Action not found")).getStatus())
|
||||
.isEqualTo(status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,4 +109,4 @@ class ActionTest extends AbstractJpaIntegrationTest {
|
||||
action.setActive(true);
|
||||
return actionRepository.save(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
@@ -36,7 +35,6 @@ import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission;
|
||||
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
||||
import org.eclipse.hawkbit.repository.event.remote.SoftwareModuleDeletedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
|
||||
import org.eclipse.hawkbit.repository.exception.FileSizeQuotaExceededException;
|
||||
@@ -47,7 +45,6 @@ import org.eclipse.hawkbit.repository.exception.InvalidSHA256HashException;
|
||||
import org.eclipse.hawkbit.repository.exception.StorageQuotaExceededException;
|
||||
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
|
||||
import org.eclipse.hawkbit.repository.jpa.RandomGeneratedInputStream;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaArtifact;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.ArtifactUpload;
|
||||
@@ -73,53 +70,46 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
final SoftwareModule module = testdataFactory.createSoftwareModuleOs();
|
||||
|
||||
assertThat(artifactManagement.get(NOT_EXIST_IDL)).isNotPresent();
|
||||
assertThat(artifactManagement.getByFilenameAndSoftwareModule(NOT_EXIST_ID, module.getId()).isPresent())
|
||||
.isFalse();
|
||||
assertThat(artifactManagement.getByFilenameAndSoftwareModule(NOT_EXIST_ID, module.getId())).isEmpty();
|
||||
|
||||
assertThat(artifactManagement.findFirstBySHA1(NOT_EXIST_ID)).isNotPresent();
|
||||
assertThat(artifactManagement.loadArtifactBinary(NOT_EXIST_ID, module.getId(), module.isEncrypted()))
|
||||
.isNotPresent();
|
||||
assertThat(artifactManagement.loadArtifactBinary(NOT_EXIST_ID, module.getId(), module.isEncrypted())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that management queries react as specfied on calls for non existing entities "
|
||||
+ " by means of throwing EntityNotFoundException.")
|
||||
@ExpectEvents({ @Expect(type = SoftwareModuleDeletedEvent.class, count = 0) })
|
||||
void entityQueriesReferringToNotExistingEntitiesThrowsException() throws URISyntaxException {
|
||||
|
||||
@Description("Verifies that management queries react as specfied on calls for non existing entities by means of " +
|
||||
"throwing EntityNotFoundException.")
|
||||
@ExpectEvents()
|
||||
void entityQueriesReferringToNotExistingEntitiesThrowsException() {
|
||||
final String artifactData = "test";
|
||||
final int artifactSize = artifactData.length();
|
||||
verifyThrownExceptionBy(
|
||||
() -> artifactManagement.create(new ArtifactUpload(IOUtils.toInputStream(artifactData, "UTF-8"),
|
||||
NOT_EXIST_IDL, "xxx", null, null, null, false, null, artifactSize)),
|
||||
"SoftwareModule");
|
||||
() -> artifactManagement.create(new ArtifactUpload(
|
||||
IOUtils.toInputStream(artifactData, "UTF-8"),
|
||||
NOT_EXIST_IDL, "xxx", null, null, null, false, null, artifactSize)), "SoftwareModule");
|
||||
|
||||
verifyThrownExceptionBy(
|
||||
() -> artifactManagement.create(new ArtifactUpload(IOUtils.toInputStream(artifactData, "UTF-8"),
|
||||
NOT_EXIST_IDL, "xxx", null, null, null, false, null, artifactSize)),
|
||||
"SoftwareModule");
|
||||
() -> artifactManagement.create(new ArtifactUpload(
|
||||
IOUtils.toInputStream(artifactData, "UTF-8"),
|
||||
NOT_EXIST_IDL, "xxx", null, null, null, false, null, artifactSize)), "SoftwareModule");
|
||||
|
||||
verifyThrownExceptionBy(() -> artifactManagement.delete(NOT_EXIST_IDL), "Artifact");
|
||||
|
||||
verifyThrownExceptionBy(() -> artifactManagement.findBySoftwareModule(PAGE, NOT_EXIST_IDL), "SoftwareModule");
|
||||
assertThat(artifactManagement.getByFilename(NOT_EXIST_ID).isPresent()).isFalse();
|
||||
assertThat(artifactManagement.getByFilename(NOT_EXIST_ID)).isEmpty();
|
||||
|
||||
verifyThrownExceptionBy(() -> artifactManagement.getByFilenameAndSoftwareModule("xxx", NOT_EXIST_IDL),
|
||||
"SoftwareModule");
|
||||
verifyThrownExceptionBy(() -> artifactManagement.getByFilenameAndSoftwareModule("xxx", NOT_EXIST_IDL), "SoftwareModule");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test if a local artifact can be created by API including metadata.")
|
||||
void createArtifact() throws IOException {
|
||||
|
||||
// check baseline
|
||||
assertThat(softwareModuleRepository.findAll()).hasSize(0);
|
||||
assertThat(artifactRepository.findAll()).hasSize(0);
|
||||
assertThat(softwareModuleRepository.findAll()).isEmpty();
|
||||
assertThat(artifactRepository.findAll()).isEmpty();
|
||||
|
||||
final JpaSoftwareModule sm = softwareModuleRepository
|
||||
.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
|
||||
final JpaSoftwareModule sm2 = softwareModuleRepository
|
||||
.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
|
||||
final JpaSoftwareModule sm = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
|
||||
final JpaSoftwareModule sm2 = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
|
||||
softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 3", "version 3"));
|
||||
|
||||
final int artifactSize = 5 * 1024;
|
||||
@@ -129,49 +119,43 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
final InputStream inputStream2 = new ByteArrayInputStream(randomBytes);
|
||||
final InputStream inputStream3 = new ByteArrayInputStream(randomBytes);
|
||||
final InputStream inputStream4 = new ByteArrayInputStream(randomBytes)) {
|
||||
|
||||
final Artifact artifact1 = createArtifactForSoftwareModule("file1", sm.getId(), artifactSize, inputStream1);
|
||||
createArtifactForSoftwareModule("file11", sm.getId(), artifactSize, inputStream2);
|
||||
createArtifactForSoftwareModule("file12", sm.getId(), artifactSize, inputStream3);
|
||||
final Artifact artifact2 = createArtifactForSoftwareModule("file2", sm2.getId(), artifactSize,
|
||||
inputStream4);
|
||||
final Artifact artifact2 = createArtifactForSoftwareModule("file2", sm2.getId(), artifactSize, inputStream4);
|
||||
|
||||
assertThat(artifact1).isInstanceOf(Artifact.class);
|
||||
assertThat(artifact1.getSoftwareModule().getId()).isEqualTo(sm.getId());
|
||||
assertThat(artifact2.getSoftwareModule().getId()).isEqualTo(sm2.getId());
|
||||
assertThat(((JpaArtifact) artifact1).getFilename()).isEqualTo("file1");
|
||||
assertThat(((JpaArtifact) artifact1).getSha1Hash()).isNotNull();
|
||||
assertThat(artifact1.getFilename()).isEqualTo("file1");
|
||||
assertThat(artifact1.getSha1Hash()).isNotNull();
|
||||
assertThat(artifact1).isNotEqualTo(artifact2);
|
||||
assertThat(((JpaArtifact) artifact1).getSha1Hash()).isEqualTo(((JpaArtifact) artifact2).getSha1Hash());
|
||||
assertThat(artifact1.getSha1Hash()).isEqualTo(artifact2.getSha1Hash());
|
||||
|
||||
assertThat(artifactManagement.getByFilename("file1").get().getSha1Hash())
|
||||
.isEqualTo(HashGeneratorUtils.generateSHA1(randomBytes));
|
||||
assertThat(artifactManagement.getByFilename("file1").get().getMd5Hash())
|
||||
.isEqualTo(HashGeneratorUtils.generateMD5(randomBytes));
|
||||
assertThat(artifactManagement.getByFilename("file1").get().getSha256Hash())
|
||||
.isEqualTo(HashGeneratorUtils.generateSHA256(randomBytes));
|
||||
assertThat(artifactManagement.getByFilename("file1").get().getSha1Hash()).isEqualTo(HashGeneratorUtils.generateSHA1(randomBytes));
|
||||
assertThat(artifactManagement.getByFilename("file1").get().getMd5Hash()).isEqualTo(HashGeneratorUtils.generateMD5(randomBytes));
|
||||
assertThat(artifactManagement.getByFilename("file1").get().getSha256Hash()).isEqualTo(
|
||||
HashGeneratorUtils.generateSHA256(randomBytes));
|
||||
|
||||
assertThat(artifactRepository.findAll()).hasSize(4);
|
||||
assertThat(softwareModuleRepository.findAll()).hasSize(3);
|
||||
|
||||
assertThat(softwareModuleManagement.get(sm.getId()).get().getArtifacts()).hasSize(3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that artifact management does not create artifacts with illegal filename.")
|
||||
void entityQueryWithIllegalFilenameThrowsException() throws URISyntaxException {
|
||||
void entityQueryWithIllegalFilenameThrowsException() {
|
||||
final String illegalFilename = "<img src=ernw onerror=alert(1)>.xml";
|
||||
final String artifactData = "test";
|
||||
final int artifactSize = artifactData.length();
|
||||
|
||||
final long smID = softwareModuleRepository.save(new JpaSoftwareModule(osType, "smIllegalFilenameTest", "1.0"))
|
||||
.getId();
|
||||
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(
|
||||
() -> artifactManagement.create(new ArtifactUpload(IOUtils.toInputStream(artifactData, "UTF-8"), smID,
|
||||
illegalFilename, false, artifactSize)));
|
||||
assertThat(softwareModuleManagement.get(smID).get().getArtifacts()).hasSize(0);
|
||||
final long smID = softwareModuleRepository.save(new JpaSoftwareModule(osType, "smIllegalFilenameTest", "1.0")).getId();
|
||||
final ArtifactUpload artifactUpload = new ArtifactUpload(
|
||||
IOUtils.toInputStream(artifactData, "UTF-8"), smID, illegalFilename, false, artifactSize);
|
||||
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> artifactManagement.create(artifactUpload));
|
||||
assertThat(softwareModuleManagement.get(smID).get().getArtifacts()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -206,9 +190,7 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
@Test
|
||||
@Description("Verifies that the quota specifying the maximum artifact storage is enforced (across software modules).")
|
||||
void createArtifactsUntilStorageQuotaIsExceeded() throws IOException {
|
||||
|
||||
// create as many small artifacts as possible w/o violating the storage
|
||||
// quota
|
||||
// create as many small artifacts as possible w/o violating the storage quota
|
||||
final long maxBytes = quotaManagement.getMaxArtifactStorage();
|
||||
final List<Long> artifactIds = new ArrayList<>();
|
||||
|
||||
@@ -221,99 +203,78 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
}
|
||||
|
||||
// upload one more artifact to trigger the quota exceeded error
|
||||
final JpaSoftwareModule sm = softwareModuleRepository
|
||||
.save(new JpaSoftwareModule(osType, "smd" + numArtifacts, "1.0"));
|
||||
final long smId = softwareModuleRepository.save(new JpaSoftwareModule(osType, "smd" + numArtifacts, "1.0")).getId();
|
||||
assertThatExceptionOfType(StorageQuotaExceededException.class)
|
||||
.isThrownBy(() -> createArtifactForSoftwareModule("file" + numArtifacts, sm.getId(), artifactSize));
|
||||
.isThrownBy(() -> createArtifactForSoftwareModule("file" + numArtifacts, smId, artifactSize));
|
||||
|
||||
// delete one of the artifacts
|
||||
artifactManagement.delete(artifactIds.get(0));
|
||||
|
||||
// now we should be able to create an artifact again
|
||||
createArtifactForSoftwareModule("fileXYZ", sm.getId(), artifactSize);
|
||||
createArtifactForSoftwareModule("fileXYZ", smId, artifactSize);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that you cannot create artifacts which exceed the configured maximum size.")
|
||||
void createArtifactFailsIfTooLarge() {
|
||||
|
||||
// create a software module
|
||||
final JpaSoftwareModule sm1 = softwareModuleRepository.save(new JpaSoftwareModule(osType, "sm1", "1.0"));
|
||||
final long smId = softwareModuleRepository.save(new JpaSoftwareModule(osType, "sm1", "1.0")).getId();
|
||||
|
||||
// create an artifact that exceeds the configured quota
|
||||
final long maxSize = quotaManagement.getMaxArtifactSize();
|
||||
final int artifactSize = Math.toIntExact(quotaManagement.getMaxArtifactSize()) + 8;
|
||||
assertThatExceptionOfType(FileSizeQuotaExceededException.class)
|
||||
.isThrownBy(() -> createArtifactForSoftwareModule("file", sm1.getId(), Math.toIntExact(maxSize) + 8));
|
||||
.isThrownBy(() -> createArtifactForSoftwareModule("file", smId, artifactSize));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests hard delete directly on repository.")
|
||||
void hardDeleteSoftwareModule() throws IOException {
|
||||
|
||||
final JpaSoftwareModule sm = softwareModuleRepository
|
||||
.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
|
||||
final JpaSoftwareModule sm = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
|
||||
|
||||
createArtifactForSoftwareModule("file1", sm.getId(), 5 * 1024);
|
||||
assertThat(artifactRepository.findAll()).hasSize(1);
|
||||
|
||||
softwareModuleRepository.deleteAll();
|
||||
assertThat(artifactRepository.findAll()).hasSize(0);
|
||||
assertThat(artifactRepository.findAll()).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method for
|
||||
* {@link org.eclipse.hawkbit.repository.ArtifactManagement#delete(long)} .
|
||||
*
|
||||
* @throws IOException
|
||||
* Test method for {@link org.eclipse.hawkbit.repository.ArtifactManagement#delete(long)}.
|
||||
*/
|
||||
@Test
|
||||
@Description("Tests the deletion of a local artifact including metadata.")
|
||||
void deleteArtifact() throws IOException {
|
||||
final JpaSoftwareModule sm = softwareModuleRepository
|
||||
.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
|
||||
final JpaSoftwareModule sm2 = softwareModuleRepository
|
||||
.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
|
||||
final JpaSoftwareModule sm = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
|
||||
final JpaSoftwareModule sm2 = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
|
||||
|
||||
assertThat(artifactRepository.findAll()).isEmpty();
|
||||
|
||||
final int artifactSize = 5 * 1024;
|
||||
try (final InputStream inputStream1 = new RandomGeneratedInputStream(artifactSize);
|
||||
final InputStream inputStream2 = new RandomGeneratedInputStream(artifactSize)) {
|
||||
|
||||
final Artifact artifact1 = createArtifactForSoftwareModule("file1", sm.getId(), artifactSize, inputStream1);
|
||||
final Artifact artifact2 = createArtifactForSoftwareModule("file2", sm2.getId(), artifactSize,
|
||||
inputStream2);
|
||||
final Artifact artifact2 = createArtifactForSoftwareModule("file2", sm2.getId(), artifactSize, inputStream2);
|
||||
|
||||
assertThat(artifactRepository.findAll()).hasSize(2);
|
||||
|
||||
assertThat(artifact1.getId()).isNotNull();
|
||||
assertThat(artifact2.getId()).isNotNull();
|
||||
assertThat(((JpaArtifact) artifact1).getSha1Hash()).isNotEqualTo(((JpaArtifact) artifact2).getSha1Hash());
|
||||
assertThat(artifact1.getSha1Hash()).isNotEqualTo(artifact2.getSha1Hash());
|
||||
|
||||
assertThat(
|
||||
binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact1.getSha1Hash()))
|
||||
.isNotNull();
|
||||
assertThat(
|
||||
binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact2.getSha1Hash()))
|
||||
.isNotNull();
|
||||
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact1.getSha1Hash())).isNotNull();
|
||||
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact2.getSha1Hash())).isNotNull();
|
||||
|
||||
artifactManagement.delete(artifact1.getId());
|
||||
|
||||
assertThat(artifactRepository.findAll()).hasSize(1);
|
||||
|
||||
assertThat(
|
||||
binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact1.getSha1Hash()))
|
||||
.isNull();
|
||||
assertThat(
|
||||
binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact2.getSha1Hash()))
|
||||
.isNotNull();
|
||||
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact1.getSha1Hash())).isNull();
|
||||
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact2.getSha1Hash())).isNotNull();
|
||||
|
||||
artifactManagement.delete(artifact2.getId());
|
||||
assertThat(
|
||||
binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact2.getSha1Hash()))
|
||||
.isNull();
|
||||
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact2.getSha1Hash())).isNull();
|
||||
|
||||
assertThat(artifactRepository.findAll()).hasSize(0);
|
||||
assertThat(artifactRepository.findAll()).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,10 +282,8 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
@Description("Test the deletion of an artifact metadata where the binary is still linked to another metadata element. " +
|
||||
"The expected result is that the metadata is deleted but the binary kept.")
|
||||
void deleteDuplicateArtifacts() throws IOException {
|
||||
final JpaSoftwareModule sm = softwareModuleRepository
|
||||
.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
|
||||
final JpaSoftwareModule sm2 = softwareModuleRepository
|
||||
.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
|
||||
final JpaSoftwareModule sm = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
|
||||
final JpaSoftwareModule sm2 = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
|
||||
|
||||
final int artifactSize = 5 * 1024;
|
||||
final byte[] randomBytes = randomBytes(artifactSize);
|
||||
@@ -347,49 +306,44 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
artifactManagement.delete(artifact2.getId());
|
||||
assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifact1.getSha1Hash())).isNull();
|
||||
assertThat(artifactRepository.findAll()).hasSize(0);
|
||||
assertThat(artifactRepository.findAll()).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that you cannot delete an artifact which exists with the same hash, in the same tenant and the SoftwareModule is not deleted .")
|
||||
void deleteArtifactWithSameHashAndSoftwareModuleIsNotDeletedInSameTenants() throws IOException {
|
||||
|
||||
final JpaSoftwareModule sm = softwareModuleRepository
|
||||
.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
|
||||
final JpaSoftwareModule sm2 = softwareModuleRepository
|
||||
.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
|
||||
final JpaSoftwareModule sm = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 1", "version 1"));
|
||||
final JpaSoftwareModule sm2 = softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 2", "version 2"));
|
||||
|
||||
final int artifactSize = 5 * 1024;
|
||||
final byte[] randomBytes = randomBytes(artifactSize);
|
||||
|
||||
try (final InputStream inputStream1 = new ByteArrayInputStream(randomBytes);
|
||||
final InputStream inputStream2 = new ByteArrayInputStream(randomBytes)) {
|
||||
|
||||
final Artifact artifact1 = createArtifactForSoftwareModule("file1", sm.getId(), artifactSize, inputStream1);
|
||||
final Artifact artifact2 = createArtifactForSoftwareModule("file2", sm2.getId(), artifactSize,
|
||||
inputStream2);
|
||||
final Artifact artifact2 = createArtifactForSoftwareModule("file2", sm2.getId(), artifactSize, inputStream2);
|
||||
|
||||
assertEqualFileContents(
|
||||
artifactManagement.loadArtifactBinary(artifact2.getSha1Hash(), sm2.getId(), sm2.isEncrypted()),
|
||||
randomBytes);
|
||||
artifactManagement.loadArtifactBinary(artifact2.getSha1Hash(), sm2.getId(), sm2.isEncrypted()), randomBytes);
|
||||
|
||||
assertThat(artifactRepository.findAll()).hasSize(2);
|
||||
|
||||
assertThat(artifact1.getSha1Hash()).isEqualTo(artifact2.getSha1Hash());
|
||||
|
||||
assertThat(artifactRepository.countBySha1HashAndTenantAndSoftwareModuleDeletedIsFalse(
|
||||
artifact1.getSha1Hash(), artifact1.getTenant())).isGreaterThan(1);
|
||||
artifact1.getSha1Hash(), artifact1.getTenant()))
|
||||
.isGreaterThan(1);
|
||||
|
||||
artifactRepository.deleteById(artifact1.getId());
|
||||
assertThat(artifactRepository.findAll()).hasSize(1);
|
||||
|
||||
assertThat(artifactRepository.countBySha1HashAndTenantAndSoftwareModuleDeletedIsFalse(
|
||||
artifact2.getSha1Hash(), artifact2.getTenant())).isLessThanOrEqualTo(1);
|
||||
artifact2.getSha1Hash(), artifact2.getTenant()))
|
||||
.isLessThanOrEqualTo(1);
|
||||
|
||||
artifactRepository.deleteById(artifact2.getId());
|
||||
assertThat(artifactRepository.findAll()).hasSize(0);
|
||||
|
||||
assertThat(artifactRepository.findAll()).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,7 +381,7 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
try (final InputStream inputStream = new RandomGeneratedInputStream(artifactSize)) {
|
||||
final Artifact artifact = createArtifactForSoftwareModule(
|
||||
"file1", testdataFactory.createSoftwareModuleOs().getId(), artifactSize, inputStream);
|
||||
assertThat(artifactManagement.get(artifact.getId()).get()).isEqualTo(artifact);
|
||||
assertThat(artifactManagement.get(artifact.getId())).contains(artifact);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,8 +394,7 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
final SoftwareModule smOs = testdataFactory.createSoftwareModuleOs();
|
||||
final Artifact artifact = createArtifactForSoftwareModule("file1", smOs.getId(), artifactSize, input);
|
||||
assertEqualFileContents(
|
||||
artifactManagement.loadArtifactBinary(artifact.getSha1Hash(), smOs.getId(), smOs.isEncrypted()),
|
||||
randomBytes);
|
||||
artifactManagement.loadArtifactBinary(artifact.getSha1Hash(), smOs.getId(), smOs.isEncrypted()), randomBytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,24 +445,25 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
final DbArtifactHash artifactHashes = calcHashes(testData);
|
||||
|
||||
try (final InputStream inputStream = new ByteArrayInputStream(testData)) {
|
||||
final ArtifactUpload artifactUploadWithInvalidSha1 = new ArtifactUpload(inputStream, sm.getId(),
|
||||
"test-file", artifactHashes.getMd5(), "sha1", artifactHashes.getSha256(), false, null,
|
||||
testData.length);
|
||||
final ArtifactUpload artifactUploadWithInvalidSha1 = new ArtifactUpload(
|
||||
inputStream, sm.getId(), "test-file",
|
||||
artifactHashes.getMd5(), "sha1", artifactHashes.getSha256(), false, null, testData.length);
|
||||
assertThatExceptionOfType(InvalidSHA1HashException.class)
|
||||
.isThrownBy(() -> artifactManagement.create(artifactUploadWithInvalidSha1));
|
||||
}
|
||||
|
||||
try (final InputStream inputStream = new ByteArrayInputStream(testData)) {
|
||||
final ArtifactUpload artifactUploadWithInvalidMd5 = new ArtifactUpload(inputStream, sm.getId(), "test-file",
|
||||
final ArtifactUpload artifactUploadWithInvalidMd5 = new ArtifactUpload(
|
||||
inputStream, sm.getId(), "test-file",
|
||||
"md5", artifactHashes.getSha1(), artifactHashes.getSha256(), false, null, testData.length);
|
||||
assertThatExceptionOfType(InvalidMD5HashException.class)
|
||||
.isThrownBy(() -> artifactManagement.create(artifactUploadWithInvalidMd5));
|
||||
}
|
||||
|
||||
try (final InputStream inputStream = new ByteArrayInputStream(testData)) {
|
||||
final ArtifactUpload artifactUploadWithInvalidSha256 = new ArtifactUpload(inputStream, sm.getId(),
|
||||
"test-file", artifactHashes.getMd5(), artifactHashes.getSha1(), "sha256", false, null,
|
||||
testData.length);
|
||||
final ArtifactUpload artifactUploadWithInvalidSha256 = new ArtifactUpload(
|
||||
inputStream, sm.getId(), "test-file",
|
||||
artifactHashes.getMd5(), artifactHashes.getSha1(), "sha256", false, null, testData.length);
|
||||
assertThatExceptionOfType(InvalidSHA256HashException.class)
|
||||
.isThrownBy(() -> artifactManagement.create(artifactUploadWithInvalidSha256));
|
||||
}
|
||||
@@ -524,17 +478,16 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
final DbArtifactHash artifactHashes = calcHashes(testData);
|
||||
|
||||
try (final InputStream inputStream = new ByteArrayInputStream(testData)) {
|
||||
final ArtifactUpload artifactUpload = new ArtifactUpload(inputStream, sm.getId(), "test-file",
|
||||
artifactHashes.getMd5(), artifactHashes.getSha1(), artifactHashes.getSha256(), false, null,
|
||||
testData.length);
|
||||
final ArtifactUpload artifactUpload = new ArtifactUpload(
|
||||
inputStream, sm.getId(), "test-file",
|
||||
artifactHashes.getMd5(), artifactHashes.getSha1(), artifactHashes.getSha256(), false, null, testData.length);
|
||||
final Artifact createdArtifact = artifactManagement.create(artifactUpload);
|
||||
assertThat(createdArtifact.getSha1Hash()).isEqualTo(artifactHashes.getSha1());
|
||||
assertThat(createdArtifact.getMd5Hash()).isEqualTo(artifactHashes.getMd5());
|
||||
assertThat(createdArtifact.getSha256Hash()).isEqualTo(artifactHashes.getSha256());
|
||||
}
|
||||
|
||||
final Optional<DbArtifact> dbArtifact = artifactManagement.loadArtifactBinary(artifactHashes.getSha1(),
|
||||
sm.getId(), sm.isEncrypted());
|
||||
final Optional<DbArtifact> dbArtifact = artifactManagement.loadArtifactBinary(artifactHashes.getSha1(), sm.getId(), sm.isEncrypted());
|
||||
assertThat(dbArtifact).isPresent();
|
||||
}
|
||||
|
||||
@@ -548,19 +501,19 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
final DbArtifactHash artifactHashes = calcHashes(testData);
|
||||
|
||||
try (final InputStream inputStream = new ByteArrayInputStream(testData)) {
|
||||
final ArtifactUpload artifactUpload = new ArtifactUpload(inputStream, smOs.getId(), "test-file",
|
||||
artifactHashes.getMd5(), artifactHashes.getSha1(), artifactHashes.getSha256(), false, null,
|
||||
testData.length);
|
||||
final ArtifactUpload artifactUpload = new ArtifactUpload(
|
||||
inputStream, smOs.getId(), "test-file",
|
||||
artifactHashes.getMd5(), artifactHashes.getSha1(), artifactHashes.getSha256(), false, null, testData.length);
|
||||
final Artifact artifact = artifactManagement.create(artifactUpload);
|
||||
assertThat(artifact).isNotNull();
|
||||
}
|
||||
final Optional<DbArtifact> dbArtifact = artifactManagement.loadArtifactBinary(artifactHashes.getSha1(),
|
||||
smOs.getId(), smOs.isEncrypted());
|
||||
final Optional<DbArtifact> dbArtifact = artifactManagement.loadArtifactBinary(
|
||||
artifactHashes.getSha1(), smOs.getId(), smOs.isEncrypted());
|
||||
assertThat(dbArtifact).isPresent();
|
||||
|
||||
try (final InputStream inputStream = new ByteArrayInputStream(testData)) {
|
||||
final ArtifactUpload existingArtifactUpload = new ArtifactUpload(inputStream, smApp.getId(), "test-file",
|
||||
false, testData.length);
|
||||
final ArtifactUpload existingArtifactUpload = new ArtifactUpload(
|
||||
inputStream, smApp.getId(), "test-file", false, testData.length);
|
||||
final Artifact createdArtifact = artifactManagement.create(existingArtifactUpload);
|
||||
assertThat(createdArtifact.getSha1Hash()).isEqualTo(artifactHashes.getSha1());
|
||||
assertThat(createdArtifact.getMd5Hash()).isEqualTo(artifactHashes.getMd5());
|
||||
@@ -594,8 +547,8 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
private Artifact createArtifactForSoftwareModule(final String filename, final long moduleId, final int artifactSize,
|
||||
final InputStream inputStream) {
|
||||
private Artifact createArtifactForSoftwareModule(
|
||||
final String filename, final long moduleId, final int artifactSize, final InputStream inputStream) {
|
||||
return artifactManagement.create(new ArtifactUpload(inputStream, moduleId, filename, false, artifactSize));
|
||||
}
|
||||
|
||||
@@ -607,8 +560,8 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
return runAsTenant(tenant, () -> testdataFactory.createSoftwareModuleApp());
|
||||
}
|
||||
|
||||
private Artifact createArtifactForTenant(final String tenant, final String artifactData, final long moduleId,
|
||||
final String filename) throws Exception {
|
||||
private Artifact createArtifactForTenant(final String tenant, final String artifactData, final long moduleId, final String filename)
|
||||
throws Exception {
|
||||
return runAsTenant(tenant, () -> testdataFactory.createArtifact(artifactData, moduleId, filename));
|
||||
}
|
||||
|
||||
@@ -616,12 +569,11 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
assertThat(runAsTenant(tenant, () -> artifactRepository.findAll())).hasSize(count);
|
||||
}
|
||||
|
||||
private void assertEqualFileContents(final Optional<DbArtifact> artifact, final byte[] randomBytes)
|
||||
throws IOException {
|
||||
private void assertEqualFileContents(final Optional<DbArtifact> artifact, final byte[] randomBytes) throws IOException {
|
||||
try (final InputStream inputStream = artifact.get().getFileInputStream()) {
|
||||
assertTrue(IOUtils.contentEquals(new ByteArrayInputStream(randomBytes), inputStream),
|
||||
assertTrue(
|
||||
IOUtils.contentEquals(new ByteArrayInputStream(randomBytes), inputStream),
|
||||
"The stored binary matches the given binary");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -607,17 +607,15 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
|
||||
void lockDistributionSet() {
|
||||
final DistributionSet distributionSet = testdataFactory.createDistributionSet("ds-1");
|
||||
assertThat(
|
||||
distributionSetManagement.get(distributionSet.getId()).map(DistributionSet::isLocked)
|
||||
.orElse(true))
|
||||
distributionSetManagement.get(distributionSet.getId()).map(DistributionSet::isLocked).orElse(true))
|
||||
.isFalse();
|
||||
distributionSetManagement.lock(distributionSet.getId());
|
||||
assertThat(
|
||||
distributionSetManagement.get(distributionSet.getId()).map(DistributionSet::isLocked)
|
||||
.orElse(false))
|
||||
distributionSetManagement.get(distributionSet.getId()).map(DistributionSet::isLocked).orElse(false))
|
||||
.isTrue();
|
||||
// assert software modules are locked
|
||||
assertThat(distributionSet.getModules().size()).isNotEqualTo(0);
|
||||
distributionSetManagement.get(distributionSet.getId()).map(DistributionSet::getModules)
|
||||
distributionSetManagement.getWithDetails(distributionSet.getId()).map(DistributionSet::getModules)
|
||||
.orElseThrow().forEach(module -> assertThat(module.isLocked()).isTrue());
|
||||
}
|
||||
|
||||
@@ -668,7 +666,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
|
||||
.isFalse();
|
||||
// assert software modules are not unlocked
|
||||
assertThat(distributionSet.getModules().size()).isNotEqualTo(0);
|
||||
distributionSetManagement.get(distributionSet.getId()).map(DistributionSet::getModules)
|
||||
distributionSetManagement.getWithDetails(distributionSet.getId()).map(DistributionSet::getModules)
|
||||
.orElseThrow().forEach(module -> assertThat(module.isLocked()).isTrue());
|
||||
}
|
||||
|
||||
@@ -688,7 +686,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
|
||||
.as("Attempt to modify a locked DS software modules should throw an exception")
|
||||
.isThrownBy(() -> distributionSetManagement.assignSoftwareModules(
|
||||
distributionSet.getId(), List.of(testdataFactory.createSoftwareModule("sm-1").getId())));
|
||||
assertThat(distributionSetManagement.get(distributionSet.getId()).get().getModules().size())
|
||||
assertThat(distributionSetManagement.getWithDetails(distributionSet.getId()).get().getModules().size())
|
||||
.as("Software module shall not be added to a locked DS.")
|
||||
.isEqualTo(softwareModuleCount);
|
||||
|
||||
@@ -697,7 +695,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
|
||||
.as("Attempt to modify a locked DS software modules should throw an exception")
|
||||
.isThrownBy(() -> distributionSetManagement.unassignSoftwareModule(
|
||||
distributionSet.getId(), distributionSet.getModules().stream().findFirst().get().getId()));
|
||||
assertThat(distributionSetManagement.get(distributionSet.getId()).get().getModules().size())
|
||||
assertThat(distributionSetManagement.getWithDetails(distributionSet.getId()).get().getModules().size())
|
||||
.as("Software module shall not be removed from a locked DS.")
|
||||
.isEqualTo(softwareModuleCount);
|
||||
}
|
||||
@@ -722,8 +720,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
|
||||
.toList());
|
||||
// assert that implicit lock locks for every skip tag
|
||||
skipTags.forEach(skipTag -> {
|
||||
DistributionSet distributionSetWithSkipTag =
|
||||
testdataFactory.createDistributionSet("ds-skip-" + skipTag.getName());
|
||||
DistributionSet distributionSetWithSkipTag = testdataFactory.createDistributionSet("ds-skip-" + skipTag.getName());
|
||||
distributionSetManagement.assignTag(List.of(distributionSetWithSkipTag.getId()), skipTag.getId());
|
||||
distributionSetWithSkipTag = distributionSetManagement.get(distributionSetWithSkipTag.getId()).orElseThrow();
|
||||
// assert that implicit lock isn't applicable for skip tags
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.springframework.core.env.Environment;
|
||||
|
||||
@Feature("Component Tests - Repository")
|
||||
@Story("Tenant Configuration Management")
|
||||
public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest implements EnvironmentAware {
|
||||
class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest implements EnvironmentAware {
|
||||
|
||||
private Environment environment;
|
||||
|
||||
@@ -44,7 +44,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Tests that tenant specific configuration can be persisted and in case the tenant does not have specific configuration the default from environment is used instead.")
|
||||
public void storeTenantSpecificConfigurationAsString() {
|
||||
void storeTenantSpecificConfigurationAsString() {
|
||||
final String envPropertyDefault = environment
|
||||
.getProperty("hawkbit.server.ddi.security.authentication.gatewaytoken.key");
|
||||
assertThat(envPropertyDefault).isNotNull();
|
||||
@@ -73,7 +73,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Tests that the tenant specific configuration can be updated")
|
||||
public void updateTenantSpecificConfiguration() {
|
||||
void updateTenantSpecificConfiguration() {
|
||||
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY;
|
||||
final String value1 = "firstValue";
|
||||
final String value2 = "secondValue";
|
||||
@@ -89,7 +89,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Tests that the tenant specific configuration can be batch updated")
|
||||
public void batchUpdateTenantSpecificConfiguration() {
|
||||
void batchUpdateTenantSpecificConfiguration() {
|
||||
Map<String, Serializable> configuration = new HashMap<>() {{
|
||||
put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, "token_123");
|
||||
put(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, true);
|
||||
@@ -107,7 +107,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Tests that the configuration value can be converted from String to Integer automatically")
|
||||
public void storeAndUpdateTenantSpecificConfigurationAsBoolean() {
|
||||
void storeAndUpdateTenantSpecificConfigurationAsBoolean() {
|
||||
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED;
|
||||
final Boolean value1 = true;
|
||||
tenantConfigurationManagement.addOrUpdateConfiguration(configKey, value1);
|
||||
@@ -119,7 +119,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Tests that the get configuration throws exception in case the value cannot be automatically converted from String to Boolean")
|
||||
public void wrongTenantConfigurationValueTypeThrowsException() {
|
||||
void wrongTenantConfigurationValueTypeThrowsException() {
|
||||
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED;
|
||||
final String value1 = "thisIsNotABoolean";
|
||||
|
||||
@@ -131,7 +131,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Tests that the get configuration throws exception in case the value is the wrong type")
|
||||
public void batchWrongTenantConfigurationValueTypeThrowsException() {
|
||||
void batchWrongTenantConfigurationValueTypeThrowsException() {
|
||||
final Map<String, Serializable> configuration = new HashMap<>() {{
|
||||
put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, "token_123");
|
||||
put(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, true);
|
||||
@@ -155,7 +155,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Tests that a deletion of a tenant specific configuration deletes it from the database.")
|
||||
public void deleteConfigurationReturnNullConfiguration() {
|
||||
void deleteConfigurationReturnNullConfiguration() {
|
||||
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY;
|
||||
|
||||
// gateway token does not have default value so no configuration value should be available
|
||||
@@ -180,7 +180,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Test that an Exception is thrown, when an integer is stored but a string expected.")
|
||||
public void storesIntegerWhenStringIsExpected() {
|
||||
void storesIntegerWhenStringIsExpected() {
|
||||
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY;
|
||||
final Integer wrongDatType = 123;
|
||||
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongDatType))
|
||||
@@ -190,7 +190,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Test that an Exception is thrown, when an integer is stored but a boolean expected.")
|
||||
public void storesIntegerWhenBooleanIsExpected() {
|
||||
void storesIntegerWhenBooleanIsExpected() {
|
||||
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED;
|
||||
final Integer wrongDataType = 123;
|
||||
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongDataType))
|
||||
@@ -200,7 +200,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Test that an Exception is thrown, when an integer is stored as PollingTime.")
|
||||
public void storesIntegerWhenPollingIntervalIsExpected() {
|
||||
void storesIntegerWhenPollingIntervalIsExpected() {
|
||||
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
|
||||
final Integer wrongDataType = 123;
|
||||
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongDataType))
|
||||
@@ -210,7 +210,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Test that an Exception is thrown, when an invalid formatted string is stored as PollingTime.")
|
||||
public void storesWrongFormattedStringAsPollingInterval() {
|
||||
void storesWrongFormattedStringAsPollingInterval() {
|
||||
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
|
||||
final String wrongFormatted = "wrongFormatted";
|
||||
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongFormatted))
|
||||
@@ -220,7 +220,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Test that an Exception is thrown, when an invalid formatted string is stored as PollingTime.")
|
||||
public void storesTooSmallDurationAsPollingInterval() {
|
||||
void storesTooSmallDurationAsPollingInterval() {
|
||||
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
|
||||
|
||||
final String tooSmallDuration = DurationHelper
|
||||
@@ -232,7 +232,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Stores a correct formatted PollignTime and reads it again.")
|
||||
public void storesCorrectDurationAsPollingInterval() {
|
||||
void storesCorrectDurationAsPollingInterval() {
|
||||
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
|
||||
|
||||
final Duration duration = DurationHelper.getDurationByTimeValues(1, 2, 0);
|
||||
@@ -246,7 +246,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Request a config value in a wrong Value")
|
||||
public void requestConfigValueWithWrongType() {
|
||||
void requestConfigValueWithWrongType() {
|
||||
assertThatThrownBy(() -> tenantConfigurationManagement.getConfigurationValue(
|
||||
TenantConfigurationKey.POLLING_TIME_INTERVAL, Serializable.class))
|
||||
.isInstanceOf(TenantConfigurationValidatorException.class);
|
||||
@@ -254,7 +254,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Verifies that every TenenatConfiguraationKeyName exists only once")
|
||||
public void verifyThatAllKeysAreDifferent() {
|
||||
void verifyThatAllKeysAreDifferent() {
|
||||
final Map<String, Void> keyNames = new HashMap<>();
|
||||
tenantConfigurationProperties.getConfigurationKeys().forEach(key -> {
|
||||
if (keyNames.containsKey(key.getKeyName())) {
|
||||
@@ -266,14 +266,14 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
|
||||
|
||||
@Test
|
||||
@Description("Get TenantConfigurationKeyByName")
|
||||
public void getTenantConfigurationKeyByName() {
|
||||
void getTenantConfigurationKeyByName() {
|
||||
final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL;
|
||||
assertThat(tenantConfigurationProperties.fromKeyName(configKey).getKeyName()).isEqualTo(configKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tenant configuration which is not declared throws exception")
|
||||
public void storeTenantConfigurationWhichIsNotDeclaredThrowsException() {
|
||||
void storeTenantConfigurationWhichIsNotDeclaredThrowsException() {
|
||||
final String configKeyWhichDoesNotExists = "configKeyWhichDoesNotExists";
|
||||
assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKeyWhichDoesNotExists, "value"))
|
||||
.as("Expected InvalidTenantConfigurationKeyException for tenant configuration key which is not declared")
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.jpa.rsql;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
@@ -23,11 +25,9 @@ import cz.jirutka.rsql.parser.ast.Node;
|
||||
import cz.jirutka.rsql.parser.ast.RSQLOperators;
|
||||
import cz.jirutka.rsql.parser.ast.RSQLVisitor;
|
||||
import org.eclipse.hawkbit.repository.RsqlQueryField;
|
||||
import org.eclipse.hawkbit.repository.jpa.Jpa;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
|
||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
|
||||
import org.eclipse.persistence.config.PersistenceUnitProperties;
|
||||
import org.eclipse.persistence.jpa.JpaQuery;
|
||||
import org.eclipse.persistence.queries.DatabaseQuery;
|
||||
import org.springframework.orm.jpa.vendor.Database;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@@ -40,23 +40,33 @@ public class RSQLToSQL {
|
||||
this.entityManager = entityManager;
|
||||
}
|
||||
|
||||
public <T, A extends Enum<A> & RsqlQueryField> String toSQL(final Class<T> domainClass, final Class<A> fieldsClass, final String rsql,
|
||||
final boolean legacyRsqlVisitor) {
|
||||
return createDbQuery(domainClass, fieldsClass, rsql, legacyRsqlVisitor).getSQLString();
|
||||
}
|
||||
|
||||
public <T, A extends Enum<A> & RsqlQueryField> DatabaseQuery createDbQuery(final Class<T> domainClass, final Class<A> fieldsClass,
|
||||
final String rsql, final boolean legacyRsqlVisitor) {
|
||||
public <T, A extends Enum<A> & RsqlQueryField> String toSQL(
|
||||
final Class<T> domainClass, final Class<A> fieldsClass, final String rsql, final boolean legacyRsqlVisitor) {
|
||||
final CriteriaQuery<T> query = createQuery(domainClass, fieldsClass, rsql, legacyRsqlVisitor);
|
||||
final TypedQuery<?> typedQuery = entityManager.createQuery(query);
|
||||
// executes the query - otherwise the SQL string is not generated
|
||||
typedQuery.setParameter(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, "DEFAULT");
|
||||
typedQuery.getResultList();
|
||||
return typedQuery.unwrap(JpaQuery.class).getDatabaseQuery();
|
||||
if (Jpa.JPA_VENDOR.equals(Jpa.JpaVendor.ECLIPSELINK)) {
|
||||
typedQuery.setParameter("eclipselink.tenant-id", "DEFAULT");
|
||||
typedQuery.getResultList();
|
||||
try {
|
||||
final Class<?> jpaQueryClass = Class.forName("org.eclipse.persistence.jpa.JpaQuery");
|
||||
final Method getDatabaseQueryMethod = jpaQueryClass.getMethod("getDatabaseQuery");
|
||||
final Method getSQLString = getDatabaseQueryMethod.getReturnType().getMethod("getSQLString");
|
||||
return (String)getSQLString.invoke(getDatabaseQueryMethod.invoke(typedQuery.unwrap(jpaQueryClass)));
|
||||
} catch (final RuntimeException e) {
|
||||
throw e;
|
||||
} catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
|
||||
throw new UnsupportedOperationException("EclipseLink is not supported", e);
|
||||
} catch (final InvocationTargetException e) {
|
||||
throw e.getCause() instanceof RuntimeException ? (RuntimeException)e.getCause() : new RuntimeException(e.getCause());
|
||||
}
|
||||
} else { // hibernate
|
||||
throw new UnsupportedOperationException("Hibernate is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
private <T, A extends Enum<A> & RsqlQueryField> CriteriaQuery<T> createQuery(final Class<T> domainClass, final Class<A> fieldsClass,
|
||||
final String rsql, final boolean legacyRsqlVisitor) {
|
||||
private <T, A extends Enum<A> & RsqlQueryField> CriteriaQuery<T> createQuery(
|
||||
final Class<T> domainClass, final Class<A> fieldsClass, final String rsql, final boolean legacyRsqlVisitor) {
|
||||
final CriteriaQuery<T> query = entityManager.getCriteriaBuilder().createQuery(domainClass);
|
||||
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||
return query.where(
|
||||
@@ -78,16 +88,15 @@ public class RSQLToSQL {
|
||||
query.distinct(true);
|
||||
|
||||
final RSQLVisitor<List<Predicate>, String> jpqQueryRSQLVisitor =
|
||||
legacyRsqlVisitor ?
|
||||
new JpaQueryRsqlVisitor<>(
|
||||
legacyRsqlVisitor
|
||||
? new JpaQueryRsqlVisitor<>(
|
||||
root, cb, fieldsClass,
|
||||
virtualPropertyReplacer, DATABASE, query,
|
||||
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase())
|
||||
:
|
||||
new JpaQueryRsqlVisitorG2<>(
|
||||
fieldsClass, root, query, cb,
|
||||
DATABASE, virtualPropertyReplacer,
|
||||
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase());
|
||||
: new JpaQueryRsqlVisitorG2<>(
|
||||
fieldsClass, root, query, cb,
|
||||
DATABASE, virtualPropertyReplacer,
|
||||
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase());
|
||||
final List<Predicate> accept = rootNode.accept(jpqQueryRSQLVisitor);
|
||||
|
||||
if (CollectionUtils.isEmpty(accept)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<artifactId>hawkbit-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-repository</artifactId>
|
||||
<name>hawkBit :: Repository :: Parent</name>
|
||||
<packaging>pom</packaging>
|
||||
@@ -24,6 +25,9 @@
|
||||
<modules>
|
||||
<module>hawkbit-repository-api</module>
|
||||
<module>hawkbit-repository-core</module>
|
||||
<module>hawkbit-repository-jpa-api</module>
|
||||
<module>hawkbit-repository-jpa-eclipselink</module>
|
||||
<module>hawkbit-repository-jpa-hibernate</module>
|
||||
<module>hawkbit-repository-jpa</module>
|
||||
<module>hawkbit-repository-jpa-flyway</module>
|
||||
|
||||
|
||||
10
pom.xml
10
pom.xml
@@ -671,16 +671,6 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.persistence</groupId>
|
||||
<artifactId>org.eclipse.persistence.core</artifactId>
|
||||
<version>${eclipselink.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.persistence</groupId>
|
||||
<artifactId>org.eclipse.persistence.jpa</artifactId>
|
||||
<version>${eclipselink.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.plugin</groupId>
|
||||
<artifactId>spring-plugin-core</artifactId>
|
||||
|
||||
Reference in New Issue
Block a user