Switch (mostly) to JPA generic events (#2104)

instead of EclipseLink specific

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2024-11-26 12:59:45 +02:00
committed by GitHub
parent 46c83feb6b
commit d435c0a424
18 changed files with 219 additions and 481 deletions

View File

@@ -321,8 +321,7 @@ public interface ControllerManagement {
* @throws InvalidTargetAttributeException if attributes violate constraints * @throws InvalidTargetAttributeException if attributes violate constraints
*/ */
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
Target updateControllerAttributes(@NotEmpty String controllerId, @NotNull Map<String, String> attributes, Target updateControllerAttributes(@NotEmpty String controllerId, @NotNull Map<String, String> attributes, UpdateMode mode);
UpdateMode mode);
/** /**
* Finds {@link Target} based on given controller ID returns found Target * Finds {@link Target} based on given controller ID returns found Target

View File

@@ -53,7 +53,9 @@ public interface Target extends NamedEntity {
String getControllerId(); String getControllerId();
/** /**
* @return the securityToken * @return the securityToken if the current security context contains the necessary permission
* {@link org.eclipse.hawkbit.im.authentication.SpPermission#READ_TARGET_SEC_TOKEN}
* or the current context is executed as system code, otherwise {@code null}.
*/ */
String getSecurityToken(); String getSecurityToken();
@@ -70,8 +72,7 @@ public interface Target extends NamedEntity {
URI getAddress(); URI getAddress();
/** /**
* @return time in {@link TimeUnit#MILLISECONDS} GMT when the {@link Target} * @return time in {@link TimeUnit#MILLISECONDS} GMT when the {@link Target} polled the server the last time or <code>null</code> if target
* polled the server the last time or <code>null</code> if target
* has never queried yet. * has never queried yet.
*/ */
Long getLastTargetQuery(); Long getLastTargetQuery();
@@ -113,4 +114,4 @@ public interface Target extends NamedEntity {
* {@link #getControllerAttributes()}. * {@link #getControllerAttributes()}.
*/ */
boolean isRequestControllerAttributes(); boolean isRequestControllerAttributes();
} }

View File

@@ -73,9 +73,8 @@ public class ExceptionMappingAspectHandler implements Ordered {
@SuppressWarnings({ "squid:S00112", "squid:S1162" }) @SuppressWarnings({ "squid:S00112", "squid:S1162" })
public void catchAndWrapJpaExceptionsService(final Exception ex) throws Throwable { public void catchAndWrapJpaExceptionsService(final Exception ex) throws Throwable {
// Workarround for EclipseLink merge where it does not throw // Workaround for EclipseLink merge where it does not throw ConstraintViolationException directly in case of
// ConstraintViolationException directly in case of existing entity // existing entity update
// update
if (ex instanceof TransactionSystemException) { if (ex instanceof TransactionSystemException) {
throw replaceWithCauseIfConstraintViolationException((TransactionSystemException) ex); throw replaceWithCauseIfConstraintViolationException((TransactionSystemException) ex);
} }

View File

@@ -408,13 +408,8 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
@Transactional @Transactional
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, @Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
backoff = @Backoff(delay = Constants.TX_RT_DELAY)) backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public Target updateControllerAttributes(final String controllerId, final Map<String, String> data, public Target updateControllerAttributes(final String controllerId, final Map<String, String> data, final UpdateMode mode) {
final UpdateMode mode) { // Constraints on attribute keys & values are not validated by EclipseLink. Hence, they are validated here.
/*
* Constraints on attribute keys & values are not validated by
* EclipseLink. Hence, they are validated here.
*/
if (data.entrySet().stream().anyMatch(e -> !isAttributeEntryValid(e))) { if (data.entrySet().stream().anyMatch(e -> !isAttributeEntryValid(e))) {
throw new InvalidTargetAttributeException(); throw new InvalidTargetAttributeException();
} }
@@ -889,7 +884,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
// because the currently assigned DS can be unequal to the currently // because the currently assigned DS can be unequal to the currently
// installed DS (the downloadOnly DS) // installed DS (the downloadOnly DS)
if (isDownloadOnly(action)) { if (isDownloadOnly(action)) {
target.setAssignedDistributionSet(action.getDistributionSet()); target.setAssignedDistributionSet((JpaDistributionSet) action.getDistributionSet());
} }
// check if the assigned set is equal to the installed set (not // check if the assigned set is equal to the installed set (not

View File

@@ -1009,7 +1009,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
private void setAssignmentOnTargets(final List<JpaAction> actions) { private void setAssignmentOnTargets(final List<JpaAction> actions) {
final List<JpaTarget> assignedDsTargets = actions.stream().map(savedAction -> { final List<JpaTarget> assignedDsTargets = actions.stream().map(savedAction -> {
final JpaTarget mergedTarget = (JpaTarget) entityManager.merge(savedAction.getTarget()); final JpaTarget mergedTarget = (JpaTarget) entityManager.merge(savedAction.getTarget());
mergedTarget.setAssignedDistributionSet(savedAction.getDistributionSet()); mergedTarget.setAssignedDistributionSet((JpaDistributionSet) savedAction.getDistributionSet());
mergedTarget.setUpdateStatus(TargetUpdateStatus.PENDING); mergedTarget.setUpdateStatus(TargetUpdateStatus.PENDING);
return mergedTarget; return mergedTarget;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
@@ -1022,8 +1022,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
actionStatus.setAction(action); actionStatus.setAction(action);
actionStatus.setOccurredAt(action.getCreatedAt()); actionStatus.setOccurredAt(action.getCreatedAt());
actionStatus.setStatus(Status.RUNNING); actionStatus.setStatus(Status.RUNNING);
actionStatus.addMessage(RepositoryConstants.SERVER_MESSAGE_PREFIX actionStatus.addMessage(RepositoryConstants.SERVER_MESSAGE_PREFIX + "Distribution Set is already assigned. Skipping this action.");
+ "Distribution Set is already assigned. Skipping this action.");
actionStatusRepository.save(actionStatus); actionStatusRepository.save(actionStatus);
} }

View File

@@ -19,11 +19,15 @@ import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass; import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PostPersist;
import jakarta.persistence.PostRemove;
import jakarta.persistence.PostUpdate;
import jakarta.persistence.Version; import jakarta.persistence.Version;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.eclipse.hawkbit.repository.jpa.model.helper.AfterTransactionCommitExecutorHolder;
import org.eclipse.hawkbit.repository.model.BaseEntity; import org.eclipse.hawkbit.repository.model.BaseEntity;
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails; import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedBy;
@@ -39,7 +43,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
@NoArgsConstructor(access = AccessLevel.PROTECTED) // Default constructor needed for JPA entities. @NoArgsConstructor(access = AccessLevel.PROTECTED) // Default constructor needed for JPA entities.
@MappedSuperclass @MappedSuperclass
@Access(AccessType.FIELD) @Access(AccessType.FIELD)
@EntityListeners({ AuditingEntityListener.class, EntityPropertyChangeListener.class, EntityInterceptorListener.class }) @EntityListeners({ AuditingEntityListener.class, EntityInterceptorListener.class })
public abstract class AbstractJpaBaseEntity implements BaseEntity { public abstract class AbstractJpaBaseEntity implements BaseEntity {
protected static final int USERNAME_FIELD_LENGTH = 64; protected static final int USERNAME_FIELD_LENGTH = 64;
@@ -199,6 +203,32 @@ public abstract class AbstractJpaBaseEntity implements BaseEntity {
return this.getClass().getSimpleName() + " [id=" + id + "]"; 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() { private boolean isController() {
return SecurityContextHolder.getContext().getAuthentication() != null return SecurityContextHolder.getContext().getAuthentication() != null
&& SecurityContextHolder.getContext().getAuthentication() && SecurityContextHolder.getContext().getAuthentication()

View File

@@ -16,13 +16,19 @@ import jakarta.persistence.MappedSuperclass;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.eclipse.hawkbit.repository.model.NamedEntity; import org.eclipse.hawkbit.repository.model.NamedEntity;
import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity; import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity;
/** /**
* {@link TenantAwareBaseEntity} extension for all entities that are named in * {@link TenantAwareBaseEntity} extension for all entities that are named in addition to their technical ID.
* addition to their technical ID.
*/ */
@NoArgsConstructor(access = AccessLevel.PROTECTED) // Default constructor needed for JPA entities
@Setter
@Getter
@MappedSuperclass @MappedSuperclass
// exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities // exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities
@SuppressWarnings("squid:S2160") @SuppressWarnings("squid:S2160")
@@ -40,39 +46,8 @@ public abstract class AbstractJpaNamedEntity extends AbstractJpaTenantAwareBaseE
@Size(max = DESCRIPTION_MAX_SIZE) @Size(max = DESCRIPTION_MAX_SIZE)
private String description; private String description;
/** protected AbstractJpaNamedEntity(final String name, final String description) {
* Default constructor.
*/
protected AbstractJpaNamedEntity() {
// Default constructor needed for JPA entities
}
/**
* Parameterized constructor.
*
* @param name of the {@link NamedEntity}
* @param description of the {@link NamedEntity}
*/
AbstractJpaNamedEntity(final String name, final String description) {
this.name = name; this.name = name;
this.description = description; this.description = description;
} }
}
@Override
public String getDescription() {
return description;
}
@Override
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public void setDescription(final String description) {
this.description = description;
}
}

View File

@@ -17,6 +17,10 @@ import jakarta.persistence.PrePersist;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size; 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.exception.TenantNotExistException;
import org.eclipse.hawkbit.repository.jpa.model.helper.TenantAwareHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.TenantAwareHolder;
import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity; import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity;
@@ -28,6 +32,7 @@ import org.eclipse.persistence.annotations.TenantDiscriminatorColumn;
/** /**
* Holder of the base attributes common to all tenant aware entities. * Holder of the base attributes common to all tenant aware entities.
*/ */
@NoArgsConstructor(access = AccessLevel.PROTECTED) // Default constructor needed for JPA entities.
@MappedSuperclass @MappedSuperclass
@TenantDiscriminatorColumn(name = "tenant", length = 40) @TenantDiscriminatorColumn(name = "tenant", length = 40)
@Multitenant(MultitenantType.SINGLE_TABLE) @Multitenant(MultitenantType.SINGLE_TABLE)
@@ -36,34 +41,16 @@ public abstract class AbstractJpaTenantAwareBaseEntity extends AbstractJpaBaseEn
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Setter
@Getter
@Column(name = "tenant", nullable = false, insertable = false, updatable = false, length = 40) @Column(name = "tenant", nullable = false, insertable = false, updatable = false, length = 40)
@Size(min = 1, max = 40) @Size(min = 1, max = 40)
@NotNull @NotNull
private String tenant; private String tenant;
/** /**
* Default constructor needed for JPA entities. * 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.
protected AbstractJpaTenantAwareBaseEntity() {
// Default constructor needed for JPA entities.
}
@Override
public String getTenant() {
return tenant;
}
public void setTenant(final String tenant) {
this.tenant = 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.
*
* @see org.eclipse.hawkbit.repository.model.BaseEntity#hashCode()
*/ */
@Override @Override
public int hashCode() { public int hashCode() {
@@ -74,12 +61,9 @@ public abstract class AbstractJpaTenantAwareBaseEntity extends AbstractJpaBaseEn
} }
/** /**
* Tenant aware entities extend the equals/hashcode strategy with the tenant * Tenant aware entities extend the equals/hashcode strategy with the tenant name. That would allow for instance in a
* name. That would allow for instance in a multi-schema based data * multi-schema based data separation setup to have the same primary key for different entities of
* separation setup to have the same primary key for different entities of
* different tenants. * different tenants.
*
* @see org.eclipse.hawkbit.repository.model.BaseEntity#equals(java.lang.Object)
*/ */
@Override @Override
// exception squid:S2259 - obj is checked for null in super // exception squid:S2259 - obj is checked for null in super
@@ -105,21 +89,18 @@ public abstract class AbstractJpaTenantAwareBaseEntity extends AbstractJpaBaseEn
} }
/** /**
* PrePersist listener method for all {@link TenantAwareBaseEntity} * PrePersist listener method for all {@link TenantAwareBaseEntity} entities.
* entities.
*/ */
@PrePersist @PrePersist
void prePersist() { void prePersist() {
// before persisting the entity check the current ID of the tenant by // before persisting the entity check the current ID of the tenant by using the TenantAware service
// using the TenantAware
// service
final String currentTenant = SystemManagementHolder.getInstance().currentTenant(); final String currentTenant = SystemManagementHolder.getInstance().currentTenant();
if (currentTenant == null) { if (currentTenant == null) {
throw new TenantNotExistException("Tenant " throw new TenantNotExistException(
+ TenantAwareHolder.getInstance().getTenantAware().getCurrentTenant() String.format(
+ " does not exists, cannot create entity " + this.getClass() + " with id " + super.getId()); "Tenant %s does not exists, cannot create entity %s with id %d",
TenantAwareHolder.getInstance().getTenantAware().getCurrentTenant(), getClass(), getId()));
} }
setTenant(currentTenant.toUpperCase()); setTenant(currentTenant.toUpperCase());
} }
}
}

View File

@@ -16,13 +16,19 @@ import jakarta.persistence.MappedSuperclass;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity; import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity;
import org.eclipse.hawkbit.repository.model.Type; import org.eclipse.hawkbit.repository.model.Type;
/** /**
* {@link TenantAwareBaseEntity} extension for all entities that are named in * {@link TenantAwareBaseEntity} extension for all entities that are named in addition to their technical ID.
* addition to their technical ID.
*/ */
@NoArgsConstructor(access = AccessLevel.PROTECTED) // Default constructor needed for JPA entities
@Setter
@Getter
@MappedSuperclass @MappedSuperclass
// exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities // exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities
@SuppressWarnings("squid:S2160") @SuppressWarnings("squid:S2160")
@@ -40,40 +46,9 @@ public abstract class AbstractJpaTypeEntity extends AbstractJpaNamedEntity imple
@Size(max = COLOUR_MAX_SIZE) @Size(max = COLOUR_MAX_SIZE)
private String colour; private String colour;
/** protected AbstractJpaTypeEntity(final String name, final String description, final String key, final String colour) {
* Default constructor.
*/
protected AbstractJpaTypeEntity() {
// Default constructor needed for JPA entities
}
/**
* Parameterized constructor.
*
* @param key of the {@link Type}
* @param colour of the {@link Type}
*/
AbstractJpaTypeEntity(final String name, final String description, final String key, final String colour) {
super(name, description); super(name, description);
this.key = key; this.key = key;
this.colour = colour; this.colour = colour;
} }
}
@Override
public String getKey() {
return key;
}
@Override
public String getColour() {
return colour;
}
public void setColour(final String colour) {
this.colour = colour;
}
public void setKey(final String key) {
this.key = key;
}
}

View File

@@ -23,8 +23,7 @@ import org.eclipse.hawkbit.repository.jpa.EntityInterceptor;
import org.eclipse.hawkbit.repository.jpa.model.helper.EntityInterceptorHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.EntityInterceptorHolder;
/** /**
* Entity listener which calls the callback's of all registered entity * Entity listener which calls the callback's of all registered entity interceptors.
* interceptors.
*/ */
public class EntityInterceptorListener { public class EntityInterceptorListener {

View File

@@ -1,59 +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 org.eclipse.hawkbit.repository.jpa.model.helper.AfterTransactionCommitExecutorHolder;
import org.eclipse.persistence.descriptors.DescriptorEvent;
import org.eclipse.persistence.descriptors.DescriptorEventAdapter;
import org.eclipse.persistence.queries.UpdateObjectQuery;
/**
* Listens to change in property values of an entity and calls the corresponding
* {@link EventAwareEntity}.
*/
public class EntityPropertyChangeListener extends DescriptorEventAdapter {
@Override
public void postDelete(final DescriptorEvent event) {
final Object object = event.getObject();
if (isEventAwareEntity(object)) {
doNotify(() -> ((EventAwareEntity) object).fireDeleteEvent());
}
}
@Override
public void postInsert(final DescriptorEvent event) {
final Object object = event.getObject();
if (isEventAwareEntity(object)) {
doNotify(() -> ((EventAwareEntity) object).fireCreateEvent());
}
}
@Override
public void postUpdate(final DescriptorEvent event) {
final Object object = event.getObject();
if (isEventAwareEntity(object) && isFireUpdate((EventAwareEntity) object, (UpdateObjectQuery) event.getQuery())) {
doNotify(() -> ((EventAwareEntity) object).fireUpdateEvent());
}
}
private static boolean isEventAwareEntity(final Object object) {
return object instanceof EventAwareEntity;
}
private static void doNotify(final Runnable runnable) {
AfterTransactionCommitExecutorHolder.getInstance().getAfterCommit().afterCommit(runnable);
}
private static boolean isFireUpdate(final EventAwareEntity entity, final UpdateObjectQuery query) {
return entity.getUpdateIgnoreFields().isEmpty() || query.getObjectChangeSet().getChangedAttributeNames()
.stream().anyMatch(field -> !entity.getUpdateIgnoreFields().contains(field));
}
}

View File

@@ -9,12 +9,9 @@
*/ */
package org.eclipse.hawkbit.repository.jpa.model; package org.eclipse.hawkbit.repository.jpa.model;
import java.util.Collections;
import java.util.List;
/** /**
* Interfaces which can be implemented by entities to be called when the entity * Interfaces which can be implemented by entities to be called when the entity should fire an event because the entity has been created,
* should fire an event because the entity has been created, updated or deleted. * updated or deleted.
*/ */
public interface EventAwareEntity { public interface EventAwareEntity {
@@ -32,11 +29,4 @@ public interface EventAwareEntity {
* Fired for the Entity deletion. * Fired for the Entity deletion.
*/ */
void fireDeleteEvent(); void fireDeleteEvent();
}
/**
* @return list of entity fields that if the only changed fields prevents {@link #fireUpdateEvent()} call.
*/
default List<String> getUpdateIgnoreFields() {
return Collections.emptyList();
}
}

View File

@@ -55,11 +55,12 @@ import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
@Getter @Getter
@ToString(callSuper = true) @ToString(callSuper = true)
@Entity @Entity
@Table(name = "sp_base_software_module", uniqueConstraints = @UniqueConstraint(columnNames = { "module_type", "name", @Table(name = "sp_base_software_module",
"version", "tenant" }, name = "uk_base_sw_mod"), indexes = { uniqueConstraints = @UniqueConstraint(columnNames = { "module_type", "name", "version", "tenant" }, name = "uk_base_sw_mod"),
@Index(name = "sp_idx_base_sw_module_01", columnList = "tenant,deleted,name,version"), indexes = {
@Index(name = "sp_idx_base_sw_module_02", columnList = "tenant,deleted,module_type"), @Index(name = "sp_idx_base_sw_module_01", columnList = "tenant,deleted,name,version"),
@Index(name = "sp_idx_base_sw_module_prim", columnList = "tenant,id") }) @Index(name = "sp_idx_base_sw_module_02", columnList = "tenant,deleted,module_type"),
@Index(name = "sp_idx_base_sw_module_prim", columnList = "tenant,id") })
@NamedEntityGraph(name = "SoftwareModule.artifacts", attributeNodes = { @NamedAttributeNode("artifacts") }) @NamedEntityGraph(name = "SoftwareModule.artifacts", attributeNodes = { @NamedAttributeNode("artifacts") })
// exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities // exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities
@SuppressWarnings("squid:S2160") @SuppressWarnings("squid:S2160")

View File

@@ -27,6 +27,7 @@ import jakarta.persistence.Convert;
import jakarta.persistence.Converter; import jakarta.persistence.Converter;
import jakarta.persistence.ElementCollection; import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType; import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey; import jakarta.persistence.ForeignKey;
import jakarta.persistence.Index; import jakarta.persistence.Index;
@@ -44,6 +45,10 @@ import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent;
@@ -52,8 +57,6 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder;
import org.eclipse.hawkbit.repository.jpa.utils.MapAttributeConverter; import org.eclipse.hawkbit.repository.jpa.utils.MapAttributeConverter;
import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.PollStatus; import org.eclipse.hawkbit.repository.model.PollStatus;
import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetMetadata; import org.eclipse.hawkbit.repository.model.TargetMetadata;
@@ -64,78 +67,97 @@ import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder; import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder;
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder; import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.persistence.descriptors.DescriptorEvent;
import org.eclipse.persistence.descriptors.DescriptorEventAdapter;
import org.eclipse.persistence.queries.UpdateObjectQuery;
/** /**
* JPA implementation of {@link Target}. * JPA implementation of {@link Target}.
*/ */
@NoArgsConstructor(access = AccessLevel.PUBLIC) // empty constructor for JPA.
@Entity @Entity
@Table(name = "sp_target", indexes = { @Table(name = "sp_target",
@Index(name = "sp_idx_target_01", columnList = "tenant,name,assigned_distribution_set"), indexes = {
@Index(name = "sp_idx_target_03", columnList = "tenant,controller_id,assigned_distribution_set"), @Index(name = "sp_idx_target_01", columnList = "tenant,name,assigned_distribution_set"),
@Index(name = "sp_idx_target_04", columnList = "tenant,created_at"), @Index(name = "sp_idx_target_03", columnList = "tenant,controller_id,assigned_distribution_set"),
@Index(name = "sp_idx_target_05", columnList = "tenant,last_modified_at"), @Index(name = "sp_idx_target_04", columnList = "tenant,created_at"),
@Index(name = "sp_idx_target_prim", columnList = "tenant,id") }, uniqueConstraints = @UniqueConstraint(columnNames = { @Index(name = "sp_idx_target_05", columnList = "tenant,last_modified_at"),
"controller_id", "tenant" }, name = "uk_tenant_controller_id")) @Index(name = "sp_idx_target_prim", columnList = "tenant,id") },
uniqueConstraints = @UniqueConstraint(columnNames = { "controller_id", "tenant" }, name = "uk_tenant_controller_id"))
// exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities // exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities
@SuppressWarnings("squid:S2160") @SuppressWarnings("squid:S2160")
@EntityListeners({ JpaTarget.EntityPropertyChangeListener.class }) // add listener to the listeners declared into suppers
@Slf4j @Slf4j
public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAwareEntity { public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAwareEntity {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final List<String> TARGET_UPDATE_EVENT_IGNORE_FIELDS = Arrays.asList("lastTargetQuery", "address",
"optLockRevision", "lastModifiedAt", "lastModifiedBy");
@Column(name = "controller_id", length = Target.CONTROLLER_ID_MAX_SIZE, updatable = false, nullable = false) @Column(name = "controller_id", length = Target.CONTROLLER_ID_MAX_SIZE, updatable = false, nullable = false)
@Size(min = 1, max = Target.CONTROLLER_ID_MAX_SIZE) @Size(min = 1, max = Target.CONTROLLER_ID_MAX_SIZE)
@NotNull @NotNull
@Pattern(regexp = "[\\S]*", message = "has whitespaces which are not allowed") @Pattern(regexp = "[\\S]*", message = "has whitespaces which are not allowed")
private String controllerId; private String controllerId;
@OneToMany(mappedBy = "target", fetch = FetchType.LAZY, cascade = { CascadeType.REMOVE }, targetEntity = JpaAction.class) /** the security token of the target which allows if enabled to authenticate with this security token */
private List<JpaAction> actions; @Setter
/**
* the security token of the target which allows if enabled to authenticate
* with this security token.
*/
@Column(name = "sec_token", nullable = false, length = Target.SECURITY_TOKEN_MAX_SIZE) @Column(name = "sec_token", nullable = false, length = Target.SECURITY_TOKEN_MAX_SIZE)
@Size(min = 1, max = Target.SECURITY_TOKEN_MAX_SIZE) @Size(min = 1, max = Target.SECURITY_TOKEN_MAX_SIZE)
@NotNull @NotNull
private String securityToken; private String securityToken;
@OneToMany(mappedBy = "target", fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE }) @Setter
private List<RolloutTargetGroup> rolloutTargetGroup;
@Column(name = "address", length = Target.ADDRESS_MAX_SIZE) @Column(name = "address", length = Target.ADDRESS_MAX_SIZE)
@Size(max = Target.ADDRESS_MAX_SIZE) @Size(max = Target.ADDRESS_MAX_SIZE)
private String address; private String address;
@Setter
@Getter
@Column(name = "last_target_query") @Column(name = "last_target_query")
private Long lastTargetQuery; private Long lastTargetQuery;
@Setter
@Getter
@Column(name = "install_date") @Column(name = "install_date")
private Long installationDate; private Long installationDate;
@Setter
@Getter
@Column(name = "update_status", nullable = false) @Column(name = "update_status", nullable = false)
@Convert(converter = TargetUpdateStatusConverter.class) @Convert(converter = TargetUpdateStatusConverter.class)
@NotNull @NotNull
private TargetUpdateStatus updateStatus = TargetUpdateStatus.UNKNOWN; private TargetUpdateStatus updateStatus = TargetUpdateStatus.UNKNOWN;
@Setter
@Getter
@ManyToOne(optional = true, fetch = FetchType.LAZY) @ManyToOne(optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "installed_distribution_set", nullable = true, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_inst_ds")) @JoinColumn(name = "installed_distribution_set", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_inst_ds"))
private JpaDistributionSet installedDistributionSet; private JpaDistributionSet installedDistributionSet;
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "assigned_distribution_set", nullable = true, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_assign_ds")) @Setter
@Getter
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "assigned_distribution_set", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_assign_ds"))
private JpaDistributionSet assignedDistributionSet; private JpaDistributionSet assignedDistributionSet;
// set default request controller attributes to true, because we want to request them the first time
@Setter
@Getter
@Column(name = "request_controller_attributes", nullable = false) @Column(name = "request_controller_attributes", nullable = false)
// set default request controller attributes to true, because we want to request them the first time
private boolean requestControllerAttributes = true; private boolean requestControllerAttributes = true;
@Setter
@Getter
@OneToOne(fetch = FetchType.LAZY, mappedBy = "target", orphanRemoval = true) @OneToOne(fetch = FetchType.LAZY, mappedBy = "target", orphanRemoval = true)
@PrimaryKeyJoinColumn @PrimaryKeyJoinColumn
private JpaAutoConfirmationStatus autoConfirmationStatus; private JpaAutoConfirmationStatus autoConfirmationStatus;
@Setter
@Getter
@ManyToOne(fetch = FetchType.LAZY, targetEntity = JpaTargetType.class) @ManyToOne(fetch = FetchType.LAZY, targetEntity = JpaTargetType.class)
@JoinColumn(name = "target_type", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_relation_target_type")) @JoinColumn(name = "target_type", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_relation_target_type"))
private TargetType targetType; private TargetType targetType;
@ManyToMany(targetEntity = JpaTargetTag.class) @ManyToMany(targetEntity = JpaTargetTag.class)
@JoinTable( @JoinTable(
name = "sp_target_target_tag", name = "sp_target_target_tag",
@@ -149,10 +171,9 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_targ_targtag_tag")) foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_targ_targtag_tag"))
}) })
private Set<TargetTag> tags; private Set<TargetTag> tags;
/**
* Supplied / committed by the controller. Read-only via management API.
*/
// no cascade option on an ElementCollection, the target objects are always persisted, merged, removed with their parent. // no cascade option on an ElementCollection, the target objects are always persisted, merged, removed with their parent.
@Getter
@ElementCollection @ElementCollection
@Column(name = "attribute_value", length = Target.CONTROLLER_ATTRIBUTE_VALUE_SIZE) @Column(name = "attribute_value", length = Target.CONTROLLER_ATTRIBUTE_VALUE_SIZE)
@MapKeyColumn(name = "attribute_key", nullable = false, length = Target.CONTROLLER_ATTRIBUTE_KEY_SIZE) @MapKeyColumn(name = "attribute_key", nullable = false, length = Target.CONTROLLER_ATTRIBUTE_KEY_SIZE)
@@ -161,24 +182,20 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
joinColumns = { @JoinColumn(name = "target_id", nullable = false, insertable = false, updatable = false) }, joinColumns = { @JoinColumn(name = "target_id", nullable = false, insertable = false, updatable = false) },
foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_targ_attrib_target")) foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_targ_attrib_target"))
private Map<String, String> controllerAttributes; private Map<String, String> controllerAttributes;
@OneToMany(mappedBy = "target", fetch = FetchType.LAZY, cascade = { CascadeType.REMOVE }, targetEntity = JpaTargetMetadata.class) @OneToMany(mappedBy = "target", fetch = FetchType.LAZY, cascade = { CascadeType.REMOVE }, targetEntity = JpaTargetMetadata.class)
private List<TargetMetadata> metadata; private List<TargetMetadata> metadata;
/** @OneToMany(mappedBy = "target", fetch = FetchType.LAZY, cascade = { CascadeType.REMOVE }, targetEntity = JpaAction.class)
* Constructor. private List<JpaAction> actions;
*
* @param controllerId controller ID of the {@link Target} @OneToMany(mappedBy = "target", fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE })
*/ private List<RolloutTargetGroup> rolloutTargetGroup;
public JpaTarget(final String controllerId) { public JpaTarget(final String controllerId) {
this(controllerId, SecurityTokenGeneratorHolder.getInstance().generateToken()); this(controllerId, SecurityTokenGeneratorHolder.getInstance().generateToken());
} }
/**
* Constructor.
*
* @param controllerId controller ID of the {@link Target}
* @param securityToken for target authentication if enabled
*/
public JpaTarget(final String controllerId, final String securityToken) { public JpaTarget(final String controllerId, final String securityToken) {
this.controllerId = controllerId; this.controllerId = controllerId;
// truncate controller ID to max name length (if needed) // truncate controller ID to max name length (if needed)
@@ -186,63 +203,20 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
this.securityToken = securityToken; this.securityToken = securityToken;
} }
/**
* Constructor
*/
public JpaTarget() {
// empty constructor for JPA.
}
/**
* @return assigned distribution set
*/
public DistributionSet getAssignedDistributionSet() {
return assignedDistributionSet;
}
/**
* @param assignedDistributionSet Distribution set
*/
public void setAssignedDistributionSet(final DistributionSet assignedDistributionSet) {
this.assignedDistributionSet = (JpaDistributionSet) assignedDistributionSet;
}
@Override @Override
public String getControllerId() { public String getControllerId() {
return controllerId; return controllerId;
} }
/**
* @param controllerId Controller ID
*/
public void setControllerId(final String controllerId) {
this.controllerId = controllerId;
}
/**
* @return the securityToken if the current security context contains the
* necessary permission {@link SpPermission#READ_TARGET_SEC_TOKEN}
* or the current context is executed as system code, otherwise
* {@code null}.
*/
@Override @Override
public String getSecurityToken() { public String getSecurityToken() {
final SystemSecurityContext systemSecurityContext = final SystemSecurityContext systemSecurityContext = SystemSecurityContextHolder.getInstance().getSystemSecurityContext();
SystemSecurityContextHolder.getInstance().getSystemSecurityContext(); if (systemSecurityContext.isCurrentThreadSystemCode() || systemSecurityContext.hasPermission(SpPermission.READ_TARGET_SEC_TOKEN)) {
if (systemSecurityContext.isCurrentThreadSystemCode() ||
systemSecurityContext.hasPermission(SpPermission.READ_TARGET_SEC_TOKEN)) {
return securityToken; return securityToken;
} }
return null; return null;
} }
/**
* @param securityToken token value
*/
public void setSecurityToken(final String securityToken) {
this.securityToken = securityToken;
}
/** /**
* @return the ipAddress * @return the ipAddress
*/ */
@@ -259,31 +233,9 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
} }
} }
@Override
public Long getLastTargetQuery() {
return lastTargetQuery;
}
@Override
public Long getInstallationDate() {
return installationDate;
}
@Override
public TargetUpdateStatus getUpdateStatus() {
return updateStatus;
}
@Override
public TargetType getTargetType() {
return targetType;
}
/** /**
* @return the poll time which holds the last poll time of the target, the * @return the poll time which holds the last poll time of the target, the next poll time and the overdue time. In case the
* next poll time and the overdue time. In case the * {@link #lastTargetQuery} is not set e.g. the target never polled before this method returns {@code null}
* {@link #lastTargetQuery} is not set e.g. the target never polled
* before this method returns {@code null}
*/ */
@Override @Override
public PollStatus getPollStatus() { public PollStatus getPollStatus() {
@@ -292,89 +244,10 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
return null; return null;
} }
return TenantConfigurationManagementHolder.getInstance().getTenantConfigurationManagement() return TenantConfigurationManagementHolder.getInstance().getTenantConfigurationManagement()
.pollStatusResolver().apply(this); .pollStatusResolver()
.apply(this);
} }
@Override
public AutoConfirmationStatus getAutoConfirmationStatus() {
return autoConfirmationStatus;
}
public void setAutoConfirmationStatus(final JpaAutoConfirmationStatus autoConfirmationStatus) {
this.autoConfirmationStatus = autoConfirmationStatus;
}
@Override
public boolean isRequestControllerAttributes() {
return requestControllerAttributes;
}
/**
* @param requestControllerAttributes Attributes
*/
public void setRequestControllerAttributes(final boolean requestControllerAttributes) {
this.requestControllerAttributes = requestControllerAttributes;
}
/**
* @param type Target type
*/
public void setTargetType(final TargetType type) {
this.targetType = type;
}
/**
* @param updateStatus Status
*/
public void setUpdateStatus(final TargetUpdateStatus updateStatus) {
this.updateStatus = updateStatus;
}
/**
* @param installationDate installation date
*/
public void setInstallationDate(final Long installationDate) {
this.installationDate = installationDate;
}
/**
* @param lastTargetQuery last query ID
*/
public void setLastTargetQuery(final Long lastTargetQuery) {
this.lastTargetQuery = lastTargetQuery;
}
/**
* @param address Address
*/
public void setAddress(final String address) {
this.address = address;
}
/**
* @return tags
*/
public Set<TargetTag> getTags() {
if (tags == null) {
return Collections.emptySet();
}
return Collections.unmodifiableSet(tags);
}
/**
* @return rollouts target group
*/
public List<RolloutTargetGroup> getRolloutTargetGroup() {
if (rolloutTargetGroup == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(rolloutTargetGroup);
}
/**
* @param tag to be added
*/
public void addTag(final TargetTag tag) { public void addTag(final TargetTag tag) {
if (tags == null) { if (tags == null) {
tags = new HashSet<>(); tags = new HashSet<>();
@@ -382,29 +255,20 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
tags.add(tag); tags.add(tag);
} }
/**
* @param tag the tag to be removed from the target
*/
public void removeTag(final TargetTag tag) { public void removeTag(final TargetTag tag) {
if (tags != null) { if (tags != null) {
tags.remove(tag); tags.remove(tag);
} }
} }
/** public Set<TargetTag> getTags() {
* @return list of action return tags == null ? Collections.emptySet() : Collections.unmodifiableSet(tags);
*/ }
public List<Action> getActions() {
if (actions == null) { public List<TargetMetadata> getMetadata() {
return Collections.emptyList(); return metadata == null ? Collections.emptyList() : Collections.unmodifiableList(metadata);
}
return Collections.unmodifiableList(actions);
} }
/**
* @param action Action
*/
public void addAction(final Action action) { public void addAction(final Action action) {
if (actions == null) { if (actions == null) {
actions = new ArrayList<>(); actions = new ArrayList<>();
@@ -412,42 +276,8 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
actions.add((JpaAction) action); actions.add((JpaAction) action);
} }
/** public List<Action> getActions() {
* @return Distribution set return actions == null ? Collections.emptyList() : Collections.unmodifiableList(actions);
*/
public JpaDistributionSet getInstalledDistributionSet() {
return installedDistributionSet;
}
/**
* @param installedDistributionSet Distribution set
*/
public void setInstalledDistributionSet(final JpaDistributionSet installedDistributionSet) {
this.installedDistributionSet = installedDistributionSet;
}
/**
* @return controller attributes
*/
public Map<String, String> getControllerAttributes() {
return controllerAttributes;
}
/**
* @return target metadata
*/
public List<TargetMetadata> getMetadata() {
if (metadata == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(metadata);
}
@Override
public String toString() {
return "JpaTarget [controllerId=" + controllerId + ", revision=" + getOptLockRevision() + ", id=" + getId()
+ "]";
} }
@Override @Override
@@ -469,9 +299,20 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
getClass(), EventPublisherHolder.getInstance().getApplicationId())); getClass(), EventPublisherHolder.getInstance().getApplicationId()));
} }
public List<RolloutTargetGroup> getRolloutTargetGroup() {
return rolloutTargetGroup == null ? Collections.emptyList() : Collections.unmodifiableList(rolloutTargetGroup);
}
@Override @Override
public List<String> getUpdateIgnoreFields() { public String toString() {
return TARGET_UPDATE_EVENT_IGNORE_FIELDS; return "JpaTarget [controllerId=" + controllerId + ", revision=" + getOptLockRevision() + ", id=" + getId() + "]";
}
// if "lastTargetQuery", "address" only are changed - the remove events are skipped
@Override
public void postUpdate() {
// do nothing - processed by EntityPropertyChangeListener
// since we want to skip the event if only "lastTargetQuery" or "address" are changed
} }
@Converter @Converter
@@ -487,4 +328,24 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
), null); ), 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 {
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());
}
}
}
}

View File

@@ -9,15 +9,16 @@
*/ */
package org.eclipse.hawkbit.repository.jpa.model.helper; package org.eclipse.hawkbit.repository.jpa.model.helper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.model.EntityPropertyChangeListener;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
/** /**
* A singleton bean which holds the {@link AfterTransactionCommitExecutor} to * A singleton bean which holds the {@link AfterTransactionCommitExecutor} to provide it to in beans not instantiated by spring e.g. JPA
* have to the cache manager in beans not instantiated by spring e.g. JPA * entities which cannot be autowired.
* entities or {@link EntityPropertyChangeListener} which cannot be autowired.
*/ */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AfterTransactionCommitExecutorHolder { public final class AfterTransactionCommitExecutorHolder {
private static final AfterTransactionCommitExecutorHolder SINGLETON = new AfterTransactionCommitExecutorHolder(); private static final AfterTransactionCommitExecutorHolder SINGLETON = new AfterTransactionCommitExecutorHolder();
@@ -25,10 +26,6 @@ public final class AfterTransactionCommitExecutorHolder {
@Autowired @Autowired
private AfterTransactionCommitExecutor afterCommit; private AfterTransactionCommitExecutor afterCommit;
private AfterTransactionCommitExecutorHolder() {
}
/** /**
* @return the cache manager holder singleton instance * @return the cache manager holder singleton instance
*/ */
@@ -49,5 +46,4 @@ public final class AfterTransactionCommitExecutorHolder {
public void setAfterCommit(final AfterTransactionCommitExecutor afterCommit) { public void setAfterCommit(final AfterTransactionCommitExecutor afterCommit) {
this.afterCommit = afterCommit; this.afterCommit = afterCommit;
} }
}
}

View File

@@ -18,6 +18,7 @@ import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction; import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction_; import org.eclipse.hawkbit.repository.jpa.model.JpaAction_;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository; import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository;
import org.eclipse.hawkbit.repository.jpa.repository.TargetRepository; import org.eclipse.hawkbit.repository.jpa.repository.TargetRepository;
@@ -68,7 +69,7 @@ public final class DeploymentHelper {
target.setAssignedDistributionSet(target.getInstalledDistributionSet()); target.setAssignedDistributionSet(target.getInstalledDistributionSet());
target.setUpdateStatus(TargetUpdateStatus.IN_SYNC); target.setUpdateStatus(TargetUpdateStatus.IN_SYNC);
} else { } else {
target.setAssignedDistributionSet(nextActiveActions.get(0).getDistributionSet()); target.setAssignedDistributionSet((JpaDistributionSet) nextActiveActions.get(0).getDistributionSet());
} }
targetRepository.save(target); targetRepository.save(target);

View File

@@ -622,22 +622,25 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
@Test @Test
@Description("Register a controller which does not exist") @Description("Register a controller which does not exist")
@WithUser(principal = "controller", authorities = { CONTROLLER_ROLE }) @WithUser(principal = "controller", authorities = { CONTROLLER_ROLE })
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @ExpectEvents({
@Expect(type = TargetCreatedEvent.class, count = 1),
@Expect(type = TargetPollEvent.class, count = 2) }) @Expect(type = TargetPollEvent.class, count = 2) })
void findOrRegisterTargetIfItDoesNotExist() { void findOrRegisterTargetIfItDoesNotExist() {
final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST); final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST);
assertThat(target).as("target should not be null").isNotNull(); assertThat(target).as("target should not be null").isNotNull();
final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST); final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST);
assertThat(target.getId()).as("Target should be the equals").isEqualTo(sameTarget.getId()); assertThat(target.getId()).as("Target should ben equals").isEqualTo(sameTarget.getId());
assertThat(targetRepository.count()).as("Only 1 target should be registered").isEqualTo(1L); assertThat(targetRepository.count()).as("Only 1 target should be registered").isEqualTo(1L);
} }
@Test @Test
@Description("Register a controller with name which does not exist and update its name") @Description("Register a controller with name which does not exist and update its name")
@WithUser(principal = "controller", authorities = { CONTROLLER_ROLE }) @WithUser(principal = "controller", authorities = { CONTROLLER_ROLE })
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @ExpectEvents({
@Expect(type = TargetPollEvent.class, count = 2), @Expect(type = TargetUpdatedEvent.class, count = 1) }) @Expect(type = TargetCreatedEvent.class, count = 1),
@Expect(type = TargetPollEvent.class, count = 2),
@Expect(type = TargetUpdatedEvent.class, count = 1) })
void findOrRegisterTargetIfItDoesNotExistWithName() { void findOrRegisterTargetIfItDoesNotExistWithName() {
final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, "TestName", null); final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, "TestName", null);
final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST,
@@ -1085,10 +1088,10 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
@Test @Test
@Description("Ensures that target attributes can be updated using different update modes.") @Description("Ensures that target attributes can be updated using different update modes.")
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @ExpectEvents({
@Expect(type = TargetCreatedEvent.class, count = 1),
@Expect(type = TargetUpdatedEvent.class, count = 4) }) @Expect(type = TargetUpdatedEvent.class, count = 4) })
void updateTargetAttributesWithDifferentUpdateModes() { void updateTargetAttributesWithDifferentUpdateModes() {
final String controllerId = "testCtrl"; final String controllerId = "testCtrl";
testdataFactory.createTarget(controllerId); testdataFactory.createTarget(controllerId);
@@ -1103,7 +1106,6 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
// update mode REMOVE // update mode REMOVE
updateTargetAttributesWithUpdateModeRemove(controllerId); updateTargetAttributesWithUpdateModeRemove(controllerId);
} }
@Test @Test
@@ -1710,7 +1712,6 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
@Step @Step
private void updateTargetAttributesWithUpdateModeRemove(final String controllerId) { private void updateTargetAttributesWithUpdateModeRemove(final String controllerId) {
final int previousSize = targetManagement.getControllerAttributes(controllerId).size(); final int previousSize = targetManagement.getControllerAttributes(controllerId).size();
// update the attributes using update mode REMOVE // update the attributes using update mode REMOVE
@@ -1723,14 +1724,12 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
final Map<String, String> updatedAttributes = targetManagement.getControllerAttributes(controllerId); final Map<String, String> updatedAttributes = targetManagement.getControllerAttributes(controllerId);
assertThat(updatedAttributes.size()).isEqualTo(previousSize - 2); assertThat(updatedAttributes.size()).isEqualTo(previousSize - 2);
assertThat(updatedAttributes).doesNotContainKeys("k1", "k3"); assertThat(updatedAttributes).doesNotContainKeys("k1", "k3");
} }
@Step @Step
private void updateTargetAttributesWithUpdateModeMerge(final String controllerId) { private void updateTargetAttributesWithUpdateModeMerge(final String controllerId) {
// get the current attributes // get the current attributes
final HashMap<String, String> attributes = new HashMap<>( final HashMap<String, String> attributes = new HashMap<>(targetManagement.getControllerAttributes(controllerId));
targetManagement.getControllerAttributes(controllerId));
// update the attributes using update mode MERGE // update the attributes using update mode MERGE
final Map<String, String> mergeAttributes = new HashMap<>(); final Map<String, String> mergeAttributes = new HashMap<>();
@@ -1748,10 +1747,8 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
@Step @Step
private void updateTargetAttributesWithUpdateModeReplace(final String controllerId) { private void updateTargetAttributesWithUpdateModeReplace(final String controllerId) {
// get the current attributes // get the current attributes
final HashMap<String, String> attributes = new HashMap<>( final HashMap<String, String> attributes = new HashMap<>(targetManagement.getControllerAttributes(controllerId));
targetManagement.getControllerAttributes(controllerId));
// update the attributes using update mode REPLACE // update the attributes using update mode REPLACE
final Map<String, String> replacementAttributes = new HashMap<>(); final Map<String, String> replacementAttributes = new HashMap<>();
@@ -1770,7 +1767,6 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
@Step @Step
private void updateTargetAttributesWithoutUpdateMode(final String controllerId) { private void updateTargetAttributesWithoutUpdateMode(final String controllerId) {
// set the initial attributes // set the initial attributes
final Map<String, String> attributes = new HashMap<>(); final Map<String, String> attributes = new HashMap<>();
attributes.put("k0", "v0"); attributes.put("k0", "v0");

View File

@@ -58,21 +58,20 @@ public class EventVerifier extends AbstractTestExecutionListener {
* executor in the ApplicationEventMultiCaster, so the order of the events * executor in the ApplicationEventMultiCaster, so the order of the events
* keep the same. * keep the same.
* *
* @param publisher the {@link ApplicationEventPublisher} to publish the marker * @param publisher the {@link ApplicationEventPublisher} to publish the marker event to
* event to
*/ */
public static void publishResetMarkerEvent(final ApplicationEventPublisher publisher) { public static void publishResetMarkerEvent(final ApplicationEventPublisher publisher) {
publisher.publishEvent(new ResetCounterMarkerEvent()); publisher.publishEvent(new ResetCounterMarkerEvent());
} }
@Override @Override
public void beforeTestMethod(final TestContext testContext) throws Exception { public void beforeTestMethod(final TestContext testContext) {
final Optional<Expect[]> expectedEvents = getExpectationsFrom(testContext.getTestMethod()); final Optional<Expect[]> expectedEvents = getExpectationsFrom(testContext.getTestMethod());
expectedEvents.ifPresent(events -> beforeTest(testContext)); expectedEvents.ifPresent(events -> beforeTest(testContext));
} }
@Override @Override
public void afterTestMethod(final TestContext testContext) throws Exception { public void afterTestMethod(final TestContext testContext) {
final Optional<Expect[]> expectedEvents = getExpectationsFrom(testContext.getTestMethod()); final Optional<Expect[]> expectedEvents = getExpectationsFrom(testContext.getTestMethod());
try { try {
expectedEvents.ifPresent(events -> afterTest(events)); expectedEvents.ifPresent(events -> afterTest(events));