Hibernate support (#2147)
* Hibernate support --------- Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -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>
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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.sql.SQLException;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect;
|
||||
|
||||
/**
|
||||
* {@link EclipseLinkJpaDialect} with additional exception translation
|
||||
* mechanisms based on {@link SQLStateSQLExceptionTranslator}.
|
||||
*
|
||||
* There are multiple variations of exceptions coming out of persistence
|
||||
* provider:
|
||||
*
|
||||
* <p>
|
||||
* 1) {@link PersistenceException}s that can be mapped by
|
||||
* {@link EclipseLinkJpaDialect} into corresponding {@link DataAccessException}.
|
||||
* <p>
|
||||
* 2) {@link PersistenceException}s that could not be mapped by
|
||||
* {@link EclipseLinkJpaDialect} directly but instead are wrapped into
|
||||
* {@link JpaSystemException}.
|
||||
* <p>
|
||||
* 2.a) here the wrapped exception's causes might be an {@link SQLException}
|
||||
* which might be mappable by {@link SQLStateSQLExceptionTranslator} or
|
||||
* <p>
|
||||
* 2.b.) the wrapped exception's causes due not contain an {@link SQLException}
|
||||
* and as a result cannot be mapped.
|
||||
* <p>
|
||||
* 3) A {@link RuntimeException} that is no {@link PersistenceException}.
|
||||
* <p>
|
||||
* 3.a) here a cause might be an {@link SQLException} which might be mappable by
|
||||
* {@link SQLStateSQLExceptionTranslator} or
|
||||
* <p>
|
||||
* 3.b.) the cause is not an {@link SQLException} and as a result cannot be
|
||||
* mapped.
|
||||
*/
|
||||
class HawkbitEclipseLinkJpaDialect extends EclipseLinkJpaDialect {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final SQLStateSQLExceptionTranslator SQLSTATE_EXCEPTION_TRANSLATOR = new SQLStateSQLExceptionTranslator();
|
||||
|
||||
@Override
|
||||
public DataAccessException translateExceptionIfPossible(@NonNull final RuntimeException ex) {
|
||||
final DataAccessException dataAccessException = super.translateExceptionIfPossible(ex);
|
||||
|
||||
if (dataAccessException == null) {
|
||||
return searchAndTranslateSqlException(ex);
|
||||
}
|
||||
return translateJpaSystemExceptionIfPossible(dataAccessException);
|
||||
}
|
||||
|
||||
private static DataAccessException translateJpaSystemExceptionIfPossible(
|
||||
final DataAccessException accessException) {
|
||||
if (!(accessException instanceof JpaSystemException)) {
|
||||
return accessException;
|
||||
}
|
||||
|
||||
final DataAccessException sqlException = searchAndTranslateSqlException(accessException);
|
||||
if (sqlException == null) {
|
||||
return accessException;
|
||||
}
|
||||
return sqlException;
|
||||
}
|
||||
|
||||
private static DataAccessException searchAndTranslateSqlException(final RuntimeException ex) {
|
||||
final SQLException sqlException = findSqlException(ex);
|
||||
if (sqlException == null) {
|
||||
return null;
|
||||
}
|
||||
return SQLSTATE_EXCEPTION_TRANSLATOR.translate("", null, sqlException);
|
||||
}
|
||||
|
||||
private static SQLException findSqlException(final RuntimeException jpaSystemException) {
|
||||
Throwable exception = jpaSystemException;
|
||||
do {
|
||||
final Throwable cause = exception.getCause();
|
||||
if (cause instanceof SQLException) {
|
||||
return (SQLException) cause;
|
||||
}
|
||||
exception = cause;
|
||||
} while (exception != null);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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.Collection;
|
||||
import java.util.List;
|
||||
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
|
||||
}
|
||||
|
||||
public static final JpaVendor JPA_VENDOR = JpaVendor.ECLIPSELINK;
|
||||
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 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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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.eclipse.persistence.annotations.Multitenant;
|
||||
import org.eclipse.persistence.annotations.MultitenantType;
|
||||
import org.eclipse.persistence.annotations.TenantDiscriminatorColumn;
|
||||
|
||||
/**
|
||||
* Holder of the base attributes common to all tenant aware entities.
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED) // Default constructor needed for JPA entities.
|
||||
@Setter
|
||||
@Getter
|
||||
@MappedSuperclass
|
||||
// Eclipse link MultiTenant support
|
||||
@TenantDiscriminatorColumn(name = "tenant", length = 40)
|
||||
@Multitenant(MultitenantType.SINGLE_TABLE)
|
||||
public abstract class AbstractJpaTenantAwareBaseEntity extends AbstractJpaBaseEntity implements TenantAwareBaseEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Column(name = "tenant", nullable = false, insertable = false, updatable = false, length = 40)
|
||||
@Size(min = 1, max = 40)
|
||||
@NotNull
|
||||
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,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
import jakarta.persistence.OptimisticLockException;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.dao.ConcurrencyFailureException;
|
||||
import org.springframework.dao.UncategorizedDataAccessException;
|
||||
|
||||
/**
|
||||
* Mapping tests for {@link HawkbitEclipseLinkJpaDialect}.
|
||||
*/
|
||||
@Feature("Unit Tests - Repository")
|
||||
@Story("Exception handling")
|
||||
class HawkBitEclipseLinkJpaDialectTest {
|
||||
|
||||
private final HawkbitEclipseLinkJpaDialect hawkBitEclipseLinkJpaDialectUnderTest = new HawkbitEclipseLinkJpaDialect();
|
||||
|
||||
@Test
|
||||
@Description("Use Case: PersistenceException that can be mapped by EclipseLinkJpaDialect into corresponding DataAccessException.")
|
||||
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.")
|
||||
void jpaSystemExceptionWithSqlDeadLockExceptionIsConcurrencyFailureException() {
|
||||
final PersistenceException persEception = mock(PersistenceException.class);
|
||||
when(persEception.getCause()).thenReturn(new SQLException("simulated transaction ER_LOCK_DEADLOCK", "40001"));
|
||||
|
||||
assertThat(hawkBitEclipseLinkJpaDialectUnderTest.translateExceptionIfPossible(persEception))
|
||||
.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 not an SQLException.")
|
||||
void jpaSystemExceptionWithNumberFormatExceptionIsNull() {
|
||||
final PersistenceException persEception = mock(PersistenceException.class);
|
||||
when(persEception.getCause()).thenReturn(new NumberFormatException());
|
||||
|
||||
assertThat(hawkBitEclipseLinkJpaDialectUnderTest.translateExceptionIfPossible(persEception))
|
||||
.isInstanceOf(UncategorizedDataAccessException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Use Case: RuntimeException that could not be mapped by EclipseLinkJpaDialect directly. Cause of "
|
||||
+ "RuntimeException is an SQLException.")
|
||||
void runtimeExceptionWithSqlDeadLockExceptionIsConcurrencyFailureException() {
|
||||
final RuntimeException persEception = mock(RuntimeException.class);
|
||||
when(persEception.getCause()).thenReturn(new SQLException("simulated transaction ER_LOCK_DEADLOCK", "40001"));
|
||||
|
||||
assertThat(hawkBitEclipseLinkJpaDialectUnderTest.translateExceptionIfPossible(persEception))
|
||||
.isInstanceOf(ConcurrencyFailureException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Use Case: RuntimeException that could not be mapped by EclipseLinkJpaDialect directly. Cause of "
|
||||
+ "RuntimeException is not an SQLException.")
|
||||
void runtimeExceptionWithNumberFormatExceptionIsNull() {
|
||||
final RuntimeException persEception = mock(RuntimeException.class);
|
||||
when(persEception.getCause()).thenReturn(new NumberFormatException());
|
||||
|
||||
assertThat(hawkBitEclipseLinkJpaDialectUnderTest.translateExceptionIfPossible(persEception)).isNull();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user