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

View File

@@ -0,0 +1,84 @@
/**
* 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 jakarta.persistence.PostLoad;
import jakarta.persistence.PostPersist;
import jakarta.persistence.PostRemove;
import jakarta.persistence.PostUpdate;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreRemove;
import jakarta.persistence.PreUpdate;
/**
* Interface for the entity interceptor lifecycle.
*/
// Exception squid:EmptyStatementUsageCheck - don't want to force users to
// implement all methods
@SuppressWarnings("squid:EmptyStatementUsageCheck")
public interface EntityInterceptor {
/**
* Callback for the {@link PrePersist} lifecycle event.
*
* @param entity the model entity
*/
default void prePersist(final Object entity) {
}
/**
* Callback for the {@link PostPersist} lifecycle event.
*
* @param entity the model entity
*/
default void postPersist(final Object entity) {
}
/**
* Callback for the {@link PostRemove} lifecycle event.
*
* @param entity the model entity
*/
default void postRemove(final Object entity) {
}
/**
* Callback for the {@link PreRemove} lifecycle event.
*
* @param entity the model entity
*/
default void preRemove(final Object entity) {
}
/**
* Callback for the {@link PostLoad} lifecycle event.
*
* @param entity the model entity
*/
default void postLoad(final Object entity) {
}
/**
* Callback for the {@link PreUpdate} lifecycle event.
*
* @param entity the model entity
*/
default void preUpdate(final Object entity) {
}
/**
* Callback for the {@link PostUpdate} lifecycle event.
*
* @param entity the model entity
*/
default void postUpdate(final Object entity) {
}
}

View File

@@ -0,0 +1,73 @@
/**
* 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.executor;
import java.util.ArrayList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* A Service which calls register runnable. This runnables will be executed after a successful spring transaction commit.
* The class is thread safe.
*/
@Slf4j
public class AfterTransactionCommitDefaultServiceExecutor implements AfterTransactionCommitExecutor {
private static class TransactionSynchronizationImpl implements TransactionSynchronization {
private final List<Runnable> afterCommitRunnables = new ArrayList<>();
@Override
// Exception squid:S1217 - Is aspectJ proxy
@SuppressWarnings({ "squid:S1217" })
public void afterCommit() {
log.debug("Transaction successfully committed, executing {} runnables", afterCommitRunnables.size());
for (final Runnable afterCommitRunnable : afterCommitRunnables) {
log.debug("Executing runnable {}", afterCommitRunnable);
try {
afterCommitRunnable.run();
} catch (final RuntimeException e) {
log.error("Failed to execute runnable {}", afterCommitRunnable, e);
}
}
}
@Override
@SuppressWarnings({ "squid:S1217" })
public void afterCompletion(final int status) {
log.debug("Transaction completed after commit with status {}", status == TransactionSynchronization.STATUS_COMMITTED ? "COMMITTED" : "ROLLEDBACK");
}
private void afterCommit(final Runnable runnable) {
afterCommitRunnables.add(runnable);
}
}
@Override
// Exception squid:S1217 - we want to run this synchronous
@SuppressWarnings("squid:S1217")
public void afterCommit(final Runnable runnable) {
log.debug("Submitting new runnable {} to run after transaction commit", runnable);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.getSynchronizations().stream().filter(TransactionSynchronizationImpl.class::isInstance)
.map(TransactionSynchronizationImpl.class::cast).findAny().orElseGet(() -> {
final TransactionSynchronizationImpl newTS = new TransactionSynchronizationImpl();
TransactionSynchronizationManager.registerSynchronization(newTS);
return newTS;
}).afterCommit(runnable);
} else {
log.info("Transaction synchronization is NOT ACTIVE/ INACTIVE. Executing right now runnable {}", runnable);
runnable.run();
}
}
}

View File

@@ -0,0 +1,26 @@
/**
* 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.executor;
/**
* A interface to register a runnable, which will be executed after a successful
* spring transaction.
*/
@FunctionalInterface
public interface AfterTransactionCommitExecutor {
/**
* Register a runnable which will be executed after a successful spring
* transaction.
*
* @param runnable the after commit runnable
*/
void afterCommit(Runnable runnable);
}

View File

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

View File

@@ -0,0 +1,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.model;
import java.util.function.Consumer;
import jakarta.persistence.PostLoad;
import jakarta.persistence.PostPersist;
import jakarta.persistence.PostRemove;
import jakarta.persistence.PostUpdate;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreRemove;
import jakarta.persistence.PreUpdate;
import org.eclipse.hawkbit.repository.jpa.EntityInterceptor;
import org.eclipse.hawkbit.repository.jpa.model.helper.EntityInterceptorHolder;
/**
* Entity listener which calls the callback's of all registered entity interceptors.
*/
public class EntityInterceptorListener {
/**
* Callback for lifecyle event <i>pre persist</i>.
*
* @param entity the JPA entity which this listener is associated with
*/
@PrePersist
public void prePersist(final Object entity) {
notifyAll(interceptor -> interceptor.prePersist(entity));
}
/**
* Callback for lifecyle event <i>post persist</i>.
*
* @param entity the JPA entity which this listener is associated with
*/
@PostPersist
public void postPersist(final Object entity) {
notifyAll(interceptor -> interceptor.postPersist(entity));
}
/**
* Callback for lifecyle event <i>post remove</i>.
*
* @param entity the JPA entity which this listener is associated with
*/
@PostRemove
public void postRemove(final Object entity) {
notifyAll(interceptor -> interceptor.postRemove(entity));
}
/**
* Callback for lifecyle event <i>pre remove</i>.
*
* @param entity the JPA entity which this listener is associated with
*/
@PreRemove
public void preRemove(final Object entity) {
notifyAll(interceptor -> interceptor.preRemove(entity));
}
/**
* Callback for lifecyle event <i>post load</i>.
*
* @param entity the JPA entity which this listener is associated with
*/
@PostLoad
public void postLoad(final Object entity) {
notifyAll(interceptor -> interceptor.postLoad(entity));
}
/**
* Callback for lifecyle event <i>pre update</i>.
*
* @param entity the JPA entity which this listener is associated with
*/
@PreUpdate
public void preUpdate(final Object entity) {
notifyAll(interceptor -> interceptor.preUpdate(entity));
}
/**
* Callback for lifecyle event <i>post update</i>.
*
* @param entity the JPA entity which this listener is associated with
*/
@PostUpdate
public void postUpdate(final Object entity) {
notifyAll(interceptor -> interceptor.postUpdate(entity));
}
private static void notifyAll(final Consumer<? super EntityInterceptor> action) {
EntityInterceptorHolder.getInstance().getEntityInterceptors().forEach(action);
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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;
/**
* Interfaces which can be implemented by entities to be called when the entity should fire an event because the entity has been created,
* updated or deleted.
*/
public interface EventAwareEntity {
/**
* Fired for the Entity creation.
*/
void fireCreateEvent();
/**
* Fired for the Entity update.
*/
void fireUpdateEvent();
/**
* Fired for the Entity deletion.
*/
void fireDeleteEvent();
}

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
*/
package org.eclipse.hawkbit.repository.jpa.model.helper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.springframework.beans.factory.annotation.Autowired;
/**
* A singleton bean which holds the {@link AfterTransactionCommitExecutor} to provide it to in beans not instantiated by spring e.g. JPA
* entities which cannot be autowired.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AfterTransactionCommitExecutorHolder {
private static final AfterTransactionCommitExecutorHolder SINGLETON = new AfterTransactionCommitExecutorHolder();
@Autowired
private AfterTransactionCommitExecutor afterCommit;
/**
* @return the cache manager holder singleton instance
*/
public static AfterTransactionCommitExecutorHolder getInstance() {
return SINGLETON;
}
/**
* @return the afterCommit
*/
public AfterTransactionCommitExecutor getAfterCommit() {
return afterCommit;
}
/**
* @param afterCommit the afterCommit to set
*/
public void setAfterCommit(final AfterTransactionCommitExecutor afterCommit) {
this.afterCommit = afterCommit;
}
}

View File

@@ -0,0 +1,43 @@
/**
* 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.helper;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.hawkbit.repository.jpa.EntityInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
/**
* A singleton bean which holds the {@link EntityInterceptor} to have all
* interceptors in spring beans.
*/
public final class EntityInterceptorHolder {
private static final EntityInterceptorHolder SINGLETON = new EntityInterceptorHolder();
@Autowired(required = false)
private final List<EntityInterceptor> entityInterceptors = new ArrayList<>();
private EntityInterceptorHolder() {
}
/**
* @return the entity intreceptor holder singleton instance
*/
public static EntityInterceptorHolder getInstance() {
return SINGLETON;
}
public List<EntityInterceptor> getEntityInterceptors() {
return entityInterceptors;
}
}

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
*/
package org.eclipse.hawkbit.repository.jpa.model.helper;
import org.eclipse.hawkbit.security.SecurityTokenGenerator;
import org.springframework.beans.factory.annotation.Autowired;
/**
* A singleton bean which holds the {@link SecurityTokenGenerator} and make it
* accessible to beans which are not managed by spring, e.g. JPA entities.
*/
public final class SecurityTokenGeneratorHolder {
private static final SecurityTokenGeneratorHolder INSTANCE = new SecurityTokenGeneratorHolder();
@Autowired
private SecurityTokenGenerator securityTokenGenerator;
/**
* private constructor.
*/
private SecurityTokenGeneratorHolder() {
}
/**
* @return a singleton instance of the security token generator holder.
*/
public static SecurityTokenGeneratorHolder getInstance() {
return INSTANCE;
}
/**
* delegates to {@link SecurityTokenGenerator#generateToken()}.
*
* @return the result {@link SecurityTokenGenerator#generateToken()}
*/
public String generateToken() {
return securityTokenGenerator.generateToken();
}
}

View File

@@ -0,0 +1,42 @@
/**
* 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.helper;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.springframework.beans.factory.annotation.Autowired;
/**
* A singleton bean which holds {@link TenantAware} service and makes it
* accessible to beans which are not managed by spring, e.g. JPA entities.
*/
public final class TenantAwareHolder {
private static final TenantAwareHolder INSTANCE = new TenantAwareHolder();
@Autowired
private TenantAware tenantAware;
private TenantAwareHolder() {
}
/**
* @return the singleton {@link TenantAwareHolder} instance
*/
public static TenantAwareHolder getInstance() {
return INSTANCE;
}
/**
* @return the {@link TenantAware} service
*/
public TenantAware getTenantAware() {
return tenantAware;
}
}

View File

@@ -0,0 +1,48 @@
/**
* 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.utils;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import jakarta.persistence.AttributeConverter;
public class MapAttributeConverter<JAVA_TYPE extends Enum<JAVA_TYPE>, DB_TYPE> implements AttributeConverter<JAVA_TYPE, DB_TYPE> {
private final Map<JAVA_TYPE, DB_TYPE> javaToDbMap;
private final Map<DB_TYPE, JAVA_TYPE> dbToJavaMap;
private final DB_TYPE nullMapping;
protected MapAttributeConverter(final Map<JAVA_TYPE, DB_TYPE> javaToDbMap, final DB_TYPE nullMapping) {
this.javaToDbMap = javaToDbMap;
this.nullMapping = nullMapping;
this.dbToJavaMap = javaToDbMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
if (javaToDbMap.size() != dbToJavaMap.size()) {
throw new IllegalArgumentException("Duplicate values in javaToDbMap");
}
}
@Override
public DB_TYPE convertToDatabaseColumn(final JAVA_TYPE attribute) {
if (attribute == null) {
return nullMapping;
}
return javaToDbMap.get(attribute);
}
@Override
public JAVA_TYPE convertToEntityAttribute(final DB_TYPE dbData) {
if (Objects.equals(dbData, nullMapping)) {
return null;
}
return dbToJavaMap.get(dbData);
}
}