Hibernate support (#2147)

* Hibernate support

---------

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

View File

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

View File

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

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

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

View File

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

View File

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

View File

@@ -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());
}
}

View File

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

View File

@@ -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();
}
}