Absract metatype impl (#2575)
* Add common "interface" for metadata supporting entities * Add common metadata implementation for distribution set and software module * Extract PermissionSupport + extend by TargetManagement * TargetManagement tags are now protected by Target permissions - as it should be --------- Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -54,14 +54,13 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
* Management service for {@link DistributionSet}s.
|
||||
*/
|
||||
public interface DistributionSetManagement<T extends DistributionSet>
|
||||
extends RepositoryManagement<T, DistributionSetManagement.Create, DistributionSetManagement.Update> {
|
||||
extends RepositoryManagement<T, DistributionSetManagement.Create, DistributionSetManagement.Update>, MetadataSupport<String> {
|
||||
|
||||
@Override
|
||||
default String permissionGroup() {
|
||||
return "DISTRIBUTION_SET";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find {@link DistributionSet} based on given ID including (lazy loaded) details, e.g. {@link DistributionSet#getModules()}. <br/>
|
||||
* For performance reasons it is recommended to use {@link #get(long)} if the details are not required.
|
||||
@@ -140,49 +139,6 @@ public interface DistributionSetManagement<T extends DistributionSet>
|
||||
@PreAuthorize(HAS_UPDATE_REPOSITORY)
|
||||
List<T> unassignTag(@NotEmpty Collection<Long> ids, long tagId);
|
||||
|
||||
/**
|
||||
* Creates a map of distribution set meta-data entries.
|
||||
*
|
||||
* @param id if the {@link DistributionSet} the meta-data has to be created for
|
||||
* @param metadata the meta-data entries to create or update
|
||||
* @throws EntityNotFoundException if given set does not exist
|
||||
* @throws EntityAlreadyExistsException in case one of the meta-data entry already exists for the specific key
|
||||
* @throws AssignmentQuotaExceededException if the maximum number of meta-data entries is exceeded for the addressed {@link DistributionSet}
|
||||
*/
|
||||
@PreAuthorize(HAS_UPDATE_REPOSITORY)
|
||||
void createMetadata(long id, @NotEmpty Map<String, String> metadata);
|
||||
|
||||
/**
|
||||
* Finds all meta-data by the given distribution set id.
|
||||
*
|
||||
* @param id the distribution set id to retrieve the meta-data from
|
||||
* @return a paged result of all meta-data entries for a given distribution set id
|
||||
* @throws EntityNotFoundException if distribution set with given ID does not exist
|
||||
*/
|
||||
@PreAuthorize(HAS_READ_REPOSITORY)
|
||||
Map<String, String> getMetadata(long id);
|
||||
|
||||
/**
|
||||
* Updates a distribution set meta-data values by adding them.
|
||||
*
|
||||
* @param id {@link DistributionSet} of the meta-data entry to be updated
|
||||
* @param key meta data-entry key to be updated
|
||||
* @param value meta data-entry to be new value
|
||||
* @throws EntityNotFoundException in case the meta-data entry does not exist and cannot be updated
|
||||
*/
|
||||
@PreAuthorize(HAS_UPDATE_REPOSITORY)
|
||||
void updateMetadata(long id, @NotNull String key, @NotNull String value);
|
||||
|
||||
/**
|
||||
* Deletes a distribution set meta-data entry.
|
||||
*
|
||||
* @param id where meta-data has to be deleted
|
||||
* @param key of the meta-data element
|
||||
* @throws EntityNotFoundException if given set does not exist
|
||||
*/
|
||||
@PreAuthorize(HAS_UPDATE_REPOSITORY)
|
||||
void deleteMetadata(long id, @NotEmpty String key);
|
||||
|
||||
/**
|
||||
* Locks a distribution set. From then on its functional properties could not be changed, and it could be assigned to targets
|
||||
*
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2025 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;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import org.eclipse.hawkbit.im.authentication.SpringEvalExpressions;
|
||||
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
/**
|
||||
* Represents interface for managing meta-data of entities. The keys are always Strings, while the values are generic
|
||||
*
|
||||
* @param <MV> the type of the meta-data value
|
||||
*/
|
||||
@SuppressWarnings("java:S119") // java:S119 - better self explainable
|
||||
public interface MetadataSupport<MV> {
|
||||
|
||||
/**
|
||||
* Creates or updates a meta-data value.
|
||||
*
|
||||
* @param id the entity id which meta-data has to be updated
|
||||
* @param key the key of the meta-data entry to be updated
|
||||
* @param value the meta-data value to be updated
|
||||
* @throws EntityNotFoundException in case the meta-data entry does not exist and cannot be updated
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY)
|
||||
void createMetadata(@NotNull Long id, @NotEmpty String key, @NotNull @Valid MV value);
|
||||
|
||||
/**
|
||||
* Creates a list of entity meta-data entries.
|
||||
*
|
||||
* @param id the entity id which meta-data has to be created
|
||||
* @param metadata the meta-data entries to create
|
||||
* @throws EntityAlreadyExistsException in case one of the meta-data entry already exists for the specific key
|
||||
* @throws EntityNotFoundException if entity with given ID does not exist
|
||||
* @throws AssignmentQuotaExceededException if the maximum number of meta-data entries is exceeded for the addressed entity
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY)
|
||||
void createMetadata(@NotNull Long id, @NotEmpty @Valid Map<String, ? extends MV> metadata);
|
||||
|
||||
/**
|
||||
* Finds all meta-data by the given entity id and key.
|
||||
*
|
||||
* @param id the entity id to retrieve the meta-data from
|
||||
* @param key the meta-data key to retrieve
|
||||
* @return a paged result of all meta-data entries for a given entity id
|
||||
* @throws EntityNotFoundException if entity with given ID does not exist ot the
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
|
||||
MV getMetadata(@NotNull Long id, @NotEmpty String key);
|
||||
|
||||
/**
|
||||
* Finds all meta-data by the given entity id.
|
||||
*
|
||||
* @param id the entity id to retrieve the meta-data from
|
||||
* @return a paged result of all meta-data entries for a given entity id
|
||||
* @throws EntityNotFoundException if entity with given ID does not exist
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
|
||||
Map<String, MV> getMetadata(@NotNull Long id);
|
||||
|
||||
/**
|
||||
* Deletes a entity meta-data entry.
|
||||
*
|
||||
* @param id where meta-data has to be deleted
|
||||
* @param key of the meta-data element
|
||||
* @throws EntityNotFoundException if entity with given ID does not exist or the key is not found
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY)
|
||||
void deleteMetadata(@NotNull Long id, @NotEmpty String key);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2025 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;
|
||||
|
||||
public interface PermissionSupport {
|
||||
|
||||
String permissionGroup();
|
||||
}
|
||||
@@ -36,7 +36,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
* @param <C> type of the create request
|
||||
* @param <U> type of the update request
|
||||
*/
|
||||
public interface RepositoryManagement<T extends BaseEntity, C, U extends Identifiable<Long>> {
|
||||
public interface RepositoryManagement<T extends BaseEntity, C, U extends Identifiable<Long>> extends PermissionSupport {
|
||||
|
||||
/**
|
||||
* Creates new {@link BaseEntity}.
|
||||
@@ -153,6 +153,7 @@ public interface RepositoryManagement<T extends BaseEntity, C, U extends Identif
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_DELETE_REPOSITORY)
|
||||
void delete(@NotEmpty Collection<Long> ids);
|
||||
|
||||
@Override
|
||||
default String permissionGroup() {
|
||||
return "REPOSITORY";
|
||||
}
|
||||
|
||||
@@ -42,66 +42,13 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
* Service for managing {@link SoftwareModule}s.
|
||||
*/
|
||||
public interface SoftwareModuleManagement<T extends SoftwareModule>
|
||||
extends RepositoryManagement<T, SoftwareModuleManagement.Create, SoftwareModuleManagement.Update> {
|
||||
extends RepositoryManagement<T, SoftwareModuleManagement.Create, SoftwareModuleManagement.Update>, MetadataSupport<MetadataValue> {
|
||||
|
||||
@Override
|
||||
default String permissionGroup() {
|
||||
return "SOFTWARE_MODULE";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates a distribution set meta-data value.
|
||||
*
|
||||
* @param id the software module id which meta-data has to be updated
|
||||
* @param key the key of the meta-data entry to be updated
|
||||
* @param value the meta-data value to be updated
|
||||
* @throws EntityNotFoundException in case the meta-data entry does not exist and cannot be updated
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY)
|
||||
void createMetadata(@NotNull Long id, @NotEmpty String key, @NotNull @Valid MetadataValue value);
|
||||
|
||||
/**
|
||||
* Creates a list of software module meta-data entries.
|
||||
*
|
||||
* @param metadata the meta-data entries to create
|
||||
* @throws EntityAlreadyExistsException in case one of the meta-data entry already exists for the specific key
|
||||
* @throws EntityNotFoundException if software module with given ID does not exist
|
||||
* @throws AssignmentQuotaExceededException if the maximum number of meta-data entries is exceeded for the addressed {@link SoftwareModule}
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY)
|
||||
void createMetadata(@NotNull Long id, @NotEmpty @Valid Map<String, ? extends MetadataValue> metadata);
|
||||
|
||||
/**
|
||||
* Finds all meta-data by the given software module id and key.
|
||||
*
|
||||
* @param id the software module id to retrieve the meta-data from
|
||||
* @param key the meta-data key to retrieve
|
||||
* @return a paged result of all meta-data entries for a given software module id
|
||||
* @throws EntityNotFoundException if software module with given ID does not exist ot the
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
|
||||
MetadataValue getMetadata(@NotNull Long id, @NotEmpty String key);
|
||||
|
||||
/**
|
||||
* Finds all meta-data by the given software module id.
|
||||
*
|
||||
* @param id the software module id to retrieve the meta-data from
|
||||
* @return a paged result of all meta-data entries for a given software module id
|
||||
* @throws EntityNotFoundException if software module with given ID does not exist
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
|
||||
Map<String, MetadataValue> getMetadata(@NotNull Long id);
|
||||
|
||||
/**
|
||||
* Deletes a software module meta-data entry.
|
||||
*
|
||||
* @param id where meta-data has to be deleted
|
||||
* @param key of the meta-data element
|
||||
* @throws EntityNotFoundException if software module with given ID does not exist or the key is not found
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY)
|
||||
void deleteMetadata(@NotNull Long id, @NotEmpty String key);
|
||||
|
||||
@PreAuthorize(SpringEvalExpressions.IS_SYSTEM_CODE)
|
||||
Map<Long, Map<String, String>> findMetaDataBySoftwareModuleIdsAndTargetVisible(Collection<Long> ids);
|
||||
|
||||
|
||||
@@ -15,12 +15,10 @@ import static org.eclipse.hawkbit.im.authentication.SpringEvalExpressions.HAS_AU
|
||||
import static org.eclipse.hawkbit.im.authentication.SpringEvalExpressions.HAS_AUTH_CREATE_TARGET;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpringEvalExpressions.HAS_AUTH_DELETE_TARGET;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpringEvalExpressions.HAS_AUTH_PREFIX;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpringEvalExpressions.HAS_READ_REPOSITORY;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpringEvalExpressions.HAS_AUTH_READ_TARGET;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ_AND_TARGET_READ;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpringEvalExpressions.HAS_AUTH_SUFFIX;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpringEvalExpressions.HAS_UPDATE_REPOSITORY;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -36,6 +34,7 @@ import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission;
|
||||
import org.eclipse.hawkbit.im.authentication.SpringEvalExpressions;
|
||||
import org.eclipse.hawkbit.repository.builder.TargetCreate;
|
||||
import org.eclipse.hawkbit.repository.builder.TargetUpdate;
|
||||
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
|
||||
@@ -60,7 +59,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
/**
|
||||
* Management service for {@link Target}s.
|
||||
*/
|
||||
public interface TargetManagement {
|
||||
public interface TargetManagement extends PermissionSupport {
|
||||
|
||||
String DETAILS_BASE = "base";
|
||||
String DETAILS_AUTO_CONFIRMATION_STATUS = "autoConfirmationStatus";
|
||||
@@ -72,6 +71,12 @@ public interface TargetManagement {
|
||||
HAS_AUTH_AND +
|
||||
HAS_AUTH_PREFIX + SpPermission.READ_TARGET + HAS_AUTH_SUFFIX +
|
||||
BRACKET_CLOSE;
|
||||
|
||||
@Override
|
||||
default String permissionGroup() {
|
||||
return "TARGET";
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts number of targets with the given distribution set assigned.
|
||||
*
|
||||
@@ -744,45 +749,56 @@ public interface TargetManagement {
|
||||
Page<Target> findByControllerAttributesRequested(@NotNull Pageable pageable);
|
||||
|
||||
/**
|
||||
* Creates a list of target meta-data entries.
|
||||
* Creates or updates a meta-data value.
|
||||
*
|
||||
* @param controllerId {@link Target} controller id the meta-data has to be created for
|
||||
* @param metadata the meta-data entries to create or update
|
||||
* @throws EntityNotFoundException if given target does not exist
|
||||
* @throws EntityAlreadyExistsException in case one of the metad-ata entry already exists for the specific key
|
||||
* @throws AssignmentQuotaExceededException if the maximum number of meta-data entries is exceeded for the addressed {@link Target}
|
||||
*/
|
||||
@PreAuthorize(HAS_UPDATE_REPOSITORY)
|
||||
void createMetadata(@NotEmpty String controllerId, @NotEmpty Map<String, String> metadata);
|
||||
|
||||
/**
|
||||
* Finds a single target meta-data by its id.
|
||||
*
|
||||
* @param controllerId of the {@link Target}
|
||||
* @return the found target meta-data
|
||||
* @throws EntityNotFoundException if target with given ID does not exist
|
||||
*/
|
||||
@PreAuthorize(HAS_READ_REPOSITORY)
|
||||
Map<String, String> getMetadata(@NotEmpty String controllerId);
|
||||
|
||||
/**
|
||||
* Updates a target meta-data value if corresponding entry exists.
|
||||
*
|
||||
* @param controllerId {@link Target} controller id of the meta-data entry to be updated
|
||||
* @param key meta data-entry key to be updated
|
||||
* @param value meta data-entry to be new value
|
||||
* @param controllerId the entity id which meta-data has to be updated
|
||||
* @param key the key of the meta-data entry to be updated
|
||||
* @param value the meta-data value to be updated
|
||||
* @throws EntityNotFoundException in case the meta-data entry does not exist and cannot be updated
|
||||
*/
|
||||
@PreAuthorize(HAS_UPDATE_REPOSITORY)
|
||||
void updateMetadata(@NotEmpty String controllerId, @NotNull String key, @NotNull String value);
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY)
|
||||
void createMetadata(@NotNull String controllerId, @NotEmpty String key, @NotNull @Valid String value);
|
||||
|
||||
/**
|
||||
* Deletes a target meta data entry.
|
||||
* Creates a list of entity meta-data entries.
|
||||
*
|
||||
* @param controllerId the entity id which meta-data has to be created
|
||||
* @param metadata the meta-data entries to create
|
||||
* @throws EntityAlreadyExistsException in case one of the meta-data entry already exists for the specific key
|
||||
* @throws EntityNotFoundException if entity with given ID does not exist
|
||||
* @throws AssignmentQuotaExceededException if the maximum number of meta-data entries is exceeded for the addressed entity
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY)
|
||||
void createMetadata(@NotNull String controllerId, @NotEmpty @Valid Map<String, String> metadata);
|
||||
|
||||
/**
|
||||
* Finds all meta-data by the given entity id and key.
|
||||
*
|
||||
* @param controllerId the entity id to retrieve the meta-data from
|
||||
* @param key the meta-data key to retrieve
|
||||
* @return a paged result of all meta-data entries for a given entity id
|
||||
* @throws EntityNotFoundException if entity with given ID does not exist ot the
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
|
||||
String getMetadata(@NotNull String controllerId, @NotEmpty String key);
|
||||
|
||||
/**
|
||||
* Finds all meta-data by the given entity id.
|
||||
*
|
||||
* @param controllerId the entity id to retrieve the meta-data from
|
||||
* @return a paged result of all meta-data entries for a given entity id
|
||||
* @throws EntityNotFoundException if entity with given ID does not exist
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
|
||||
Map<String, String> getMetadata(@NotNull String controllerId);
|
||||
|
||||
/**
|
||||
* Deletes a entity meta-data entry.
|
||||
*
|
||||
* @param controllerId where meta-data has to be deleted
|
||||
* @param key of the meta data element
|
||||
* @throws EntityNotFoundException if given target does not exist
|
||||
* @param key of the meta-data element
|
||||
* @throws EntityNotFoundException if entity with given ID does not exist or the key is not found
|
||||
*/
|
||||
@PreAuthorize(HAS_UPDATE_REPOSITORY)
|
||||
void deleteMetadata(@NotEmpty String controllerId, @NotEmpty String key);
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_UPDATE_REPOSITORY)
|
||||
void deleteMetadata(@NotNull String controllerId, @NotEmpty String key);
|
||||
}
|
||||
@@ -64,9 +64,9 @@ public class RepositoryConfiguration {
|
||||
public boolean hasPermission(final Authentication authentication, final Object targetDomainObject, final Object permission) {
|
||||
if (targetDomainObject instanceof MethodSecurityExpressionOperations root) {
|
||||
final String neededPermission =
|
||||
permission + "_" + (root.getThis() instanceof RepositoryManagement<?, ?, ?> repositoryManagement
|
||||
? repositoryManagement.permissionGroup()
|
||||
: "REPOSITORY"); // TODO - should not fall back here - all using parmissions should extend repository management interface
|
||||
permission + "_" + (root.getThis() instanceof PermissionSupport permissionSupport
|
||||
? permissionSupport.permissionGroup()
|
||||
: "REPOSITORY"); // TODO - should not fall back here - all using permissions should extend repository management interface
|
||||
final boolean hasPermission = roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities()).stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.anyMatch(authority -> authority.equals(neededPermission));
|
||||
|
||||
@@ -80,11 +80,12 @@ abstract class AbstractJpaRepositoryManagement<T extends AbstractJpaBaseEntity,
|
||||
|
||||
protected final R jpaRepository;
|
||||
protected final EntityManager entityManager;
|
||||
private final Constructor<T> entityConstructor;
|
||||
private final Supplier<T> jpaEntityCreator;
|
||||
|
||||
protected AbstractJpaRepositoryManagement(final R jpaRepository, final EntityManager entityManager) {
|
||||
this.jpaRepository = jpaRepository;
|
||||
this.entityManager = entityManager;
|
||||
final Constructor<T> entityConstructor;
|
||||
try {
|
||||
entityConstructor = jpaRepository.getDomainClass().getConstructor();
|
||||
// test if method works, if fine - it shall never fail when called later
|
||||
@@ -92,6 +93,13 @@ abstract class AbstractJpaRepositoryManagement<T extends AbstractJpaBaseEntity,
|
||||
} catch (final NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new IllegalStateException("JPA domain class " + jpaRepository.getDomainClass() + " shall have public no-args constructor", e);
|
||||
}
|
||||
jpaEntityCreator = () -> {
|
||||
try {
|
||||
return entityConstructor.newInstance();
|
||||
} catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new IllegalStateException("Must NEVER happen!", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -156,7 +164,7 @@ abstract class AbstractJpaRepositoryManagement<T extends AbstractJpaBaseEntity,
|
||||
}
|
||||
|
||||
protected T update(final Identifiable<Long> update, final T entity) {
|
||||
// update getId has not setter in target JPA entity but shall have getter and the value shall be the same
|
||||
// update getId has no setter in target JPA entity but shall have getter and the value shall be the same
|
||||
// otherwise the Utils will throw an exception that there is no counterpart setter for getId
|
||||
if (ObjectCopyUtil.copy(update, entity, false, this::attach)) {
|
||||
return jpaRepository.save(entity);
|
||||
@@ -289,12 +297,7 @@ abstract class AbstractJpaRepositoryManagement<T extends AbstractJpaBaseEntity,
|
||||
}
|
||||
|
||||
private T jpaEntity(final Object create) {
|
||||
final T jpaEntity;
|
||||
try {
|
||||
jpaEntity = entityConstructor.newInstance();
|
||||
} catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new IllegalStateException("Must NEVER happen!", e);
|
||||
}
|
||||
final T jpaEntity = jpaEntityCreator.get();
|
||||
ObjectCopyUtil.copy(create, jpaEntity, false, this::attach);
|
||||
return jpaEntity;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Copyright (c) 2025 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.management;
|
||||
|
||||
import static org.eclipse.hawkbit.repository.jpa.configuration.Constants.TX_RT_DELAY;
|
||||
import static org.eclipse.hawkbit.repository.jpa.configuration.Constants.TX_RT_MAX;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
import org.eclipse.hawkbit.repository.Identifiable;
|
||||
import org.eclipse.hawkbit.repository.MetadataSupport;
|
||||
import org.eclipse.hawkbit.repository.RsqlQueryField;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.AbstractJpaBaseEntity;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.WithMetadata;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.BaseEntityRepository;
|
||||
import org.eclipse.hawkbit.utils.ObjectCopyUtil;
|
||||
import org.springframework.dao.ConcurrencyFailureException;
|
||||
import org.springframework.retry.annotation.Backoff;
|
||||
import org.springframework.retry.annotation.Retryable;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@SuppressWarnings("java:S119") // java:S119 - better self explainable
|
||||
abstract class AbstractJpaRepositoryWithMetadataManagement<T extends AbstractJpaBaseEntity & WithMetadata<MV, MVI>, C, U extends Identifiable<Long>, R extends BaseEntityRepository<T>, A extends Enum<A> & RsqlQueryField, MV, MVI extends MV>
|
||||
extends AbstractJpaRepositoryManagement<T, C, U, R, A> implements MetadataSupport<MV> {
|
||||
|
||||
private final Supplier<MVI> metadataValueCreator;
|
||||
private final boolean useCopy;
|
||||
|
||||
// java:S3011 - intentionally to provide option to forbid constructors
|
||||
// java:S1141 - better visible this way
|
||||
@SuppressWarnings({"unchecked", "java:S3011", "java:S1141"})
|
||||
protected AbstractJpaRepositoryWithMetadataManagement(final R repository, final EntityManager entityManager) {
|
||||
super(repository, entityManager);
|
||||
try {
|
||||
final Class<MVI> metadataValueType = (Class<MVI>) ((ParameterizedType) jpaRepository.getDomainClass().getMethod("getMetadata")
|
||||
.getGenericReturnType())
|
||||
.getActualTypeArguments()[1];
|
||||
final Constructor<MVI> constructor;
|
||||
try {
|
||||
constructor = metadataValueType.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
constructor.newInstance();
|
||||
} catch (final NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new IllegalStateException("Metadata class " + jpaRepository.getDomainClass() + " shall have no-args constructor", e);
|
||||
}
|
||||
metadataValueCreator = () -> {
|
||||
try {
|
||||
return constructor.newInstance();
|
||||
} catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new IllegalStateException("Must NEVER happen!", e);
|
||||
}
|
||||
};
|
||||
useCopy = !String.class.equals(metadataValueType) && !metadataValueType.isPrimitive();
|
||||
} catch (final NoSuchMethodException | SecurityException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = TX_RT_MAX, backoff = @Backoff(delay = TX_RT_DELAY))
|
||||
public void createMetadata(final Long id, final String key, final MV value) {
|
||||
final T jpaEntity = getValid(id);
|
||||
final Map<String, MVI> metadataValueMap = jpaEntity.getMetadata();
|
||||
final MVI existingValue = metadataValueMap.get(key);
|
||||
if (existingValue == null) {
|
||||
assertMetadataQuota(metadataValueMap.size() + 1L);
|
||||
}
|
||||
if (setMetadataValue(key, value, existingValue, metadataValueMap)) {
|
||||
jpaRepository.save(jpaEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = TX_RT_MAX, backoff = @Backoff(delay = TX_RT_DELAY))
|
||||
public void createMetadata(final Long id, final Map<String, ? extends MV> metadata) {
|
||||
final T jpaEntity = getValid(id);
|
||||
final Map<String, MVI> metadataValueMap = jpaEntity.getMetadata();
|
||||
assertMetadataQuota(metadata.keySet().stream().filter(key -> !metadataValueMap.containsKey(key)).count() + metadataValueMap.size());
|
||||
final AtomicBoolean changed = new AtomicBoolean(false);
|
||||
metadata.forEach((key, value) -> {
|
||||
if (setMetadataValue(key, value, metadataValueMap.get(key), metadataValueMap)) {
|
||||
changed.set(true);
|
||||
}
|
||||
});
|
||||
if (changed.get()) {
|
||||
jpaRepository.save(jpaEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MV getMetadata(final Long id, final String key) {
|
||||
final T jpaEntity = getValid(id);
|
||||
final MV metadataValue = jpaEntity.getMetadata().get(key);
|
||||
if (metadataValue == null) {
|
||||
throw new EntityNotFoundException("Metadata", jpaRepository.getManagementClass().getSimpleName() + ":" + id + ":" + key);
|
||||
} else {
|
||||
return metadataValue;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, MV> getMetadata(final Long id) {
|
||||
return jpaRepository
|
||||
.findById(id)
|
||||
.map(T::getMetadata)
|
||||
.map(metadata -> metadata.entrySet().stream()
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
e -> (MV) e.getValue())))
|
||||
.orElseThrow(() -> new EntityNotFoundException(jpaRepository.getManagementClass(), id));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = TX_RT_MAX, backoff = @Backoff(delay = TX_RT_DELAY))
|
||||
public void deleteMetadata(final Long id, final String key) {
|
||||
final T jpaEntity = getValid(id);
|
||||
final Map<String, MVI> metadataValueMap = jpaEntity.getMetadata();
|
||||
if (!metadataValueMap.containsKey(key)) {
|
||||
throw new EntityNotFoundException("Metadata", jpaRepository.getManagementClass().getSimpleName() + ":" + id + ":" + key);
|
||||
}
|
||||
metadataValueMap.remove(key);
|
||||
jpaRepository.save(jpaEntity);
|
||||
}
|
||||
|
||||
protected abstract void assertMetadataQuota(final long requested);
|
||||
|
||||
private T getValid(final Long id) {
|
||||
final T jpaEntity = jpaRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException(jpaRepository.getManagementClass(), id));
|
||||
if (!jpaEntity.isValid()) {
|
||||
throw new InvalidDistributionSetException(jpaRepository.getManagementClass().getSimpleName() + " " + id + " is invalid");
|
||||
}
|
||||
return jpaEntity;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private boolean setMetadataValue(final String key, final MV newValue, final MVI existingValue, final Map<String, MVI> metadataValueMap) {
|
||||
if (useCopy) {
|
||||
final MVI jpaMetadataValue = existingValue == null ? metadataValueCreator.get() : existingValue;
|
||||
if (ObjectCopyUtil.copy(newValue, jpaMetadataValue, true, UnaryOperator.identity())) {
|
||||
metadataValueMap.put(key, jpaMetadataValue);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (Objects.equals(newValue, existingValue)) {
|
||||
return false;
|
||||
} else {
|
||||
metadataValueMap.put(key, (MVI)newValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ import org.springframework.util.ObjectUtils;
|
||||
@Service
|
||||
@ConditionalOnBooleanProperty(prefix = "hawkbit.jpa", name = { "enabled", "distribution-set-management" }, matchIfMissing = true)
|
||||
public class JpaDistributionSetManagement
|
||||
extends AbstractJpaRepositoryManagement<JpaDistributionSet, DistributionSetManagement.Create, DistributionSetManagement.Update, DistributionSetRepository, DistributionSetFields>
|
||||
extends AbstractJpaRepositoryWithMetadataManagement<JpaDistributionSet, DistributionSetManagement.Create, DistributionSetManagement.Update, DistributionSetRepository, DistributionSetFields, String, String>
|
||||
implements DistributionSetManagement<JpaDistributionSet> {
|
||||
|
||||
private final DistributionSetTagManagement<JpaDistributionSetTag> distributionSetTagManagement;
|
||||
@@ -253,63 +253,6 @@ public class JpaDistributionSetManagement
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = TX_RT_MAX, backoff = @Backoff(delay = TX_RT_DELAY))
|
||||
public void createMetadata(final long id, final Map<String, String> md) {
|
||||
final JpaDistributionSet distributionSet = getValid0(id);
|
||||
|
||||
// get the modifiable metadata map
|
||||
final Map<String, String> metadata = distributionSet.getMetadata();
|
||||
md.keySet().forEach(key -> {
|
||||
if (metadata.containsKey(key)) {
|
||||
throw new EntityAlreadyExistsException("Metadata entry with key '" + key + "' already exists");
|
||||
}
|
||||
});
|
||||
metadata.putAll(md);
|
||||
|
||||
assertMetaDataQuota(id, metadata.size());
|
||||
|
||||
jpaRepository.save(distributionSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getMetadata(final long id) {
|
||||
assertDistributionSetExists(id);
|
||||
return getMap(id, JpaDistributionSet_.metadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = TX_RT_MAX, backoff = @Backoff(delay = TX_RT_DELAY))
|
||||
public void updateMetadata(final long id, final String key, final String value) {
|
||||
final JpaDistributionSet distributionSet = getValid0(id);
|
||||
|
||||
// get the modifiable metadata map
|
||||
final Map<String, String> metadata = distributionSet.getMetadata();
|
||||
if (!metadata.containsKey(key)) {
|
||||
throw new EntityNotFoundException("DistributionSet metadata", id + ":" + key);
|
||||
}
|
||||
metadata.put(key, value);
|
||||
|
||||
jpaRepository.save(distributionSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = TX_RT_MAX, backoff = @Backoff(delay = TX_RT_DELAY))
|
||||
public void deleteMetadata(final long id, final String key) {
|
||||
final JpaDistributionSet distributionSet = getValid0(id);
|
||||
|
||||
// get the modifiable metadata map
|
||||
final Map<String, String> metadata = distributionSet.getMetadata();
|
||||
if (metadata.remove(key) == null) {
|
||||
throw new EntityNotFoundException("DistributionSet metadata", id + ":" + key);
|
||||
}
|
||||
|
||||
jpaRepository.save(distributionSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = TX_RT_MAX, backoff = @Backoff(delay = TX_RT_DELAY))
|
||||
@@ -450,6 +393,17 @@ public class JpaDistributionSetManagement
|
||||
return jpaRepository.countAutoAssignmentsForDistributionSet(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the meta-data quota for the software module with the given ID.
|
||||
*
|
||||
* @param requested Number of meta-data entries to be created.
|
||||
*/
|
||||
@Override
|
||||
protected void assertMetadataQuota(final long requested) {
|
||||
final int maxMetaData = quotaManagement.getMaxMetaDataEntriesPerDistributionSet();
|
||||
QuotaHelper.assertAssignmentQuota(requested, maxMetaData, String.class, DistributionSet.class);
|
||||
}
|
||||
|
||||
// check if it shall implicitly lock a distribution set
|
||||
boolean isImplicitLockApplicable(final DistributionSet distributionSet) {
|
||||
final JpaDistributionSet jpaDistributionSet = (JpaDistributionSet) distributionSet;
|
||||
@@ -584,11 +538,6 @@ public class JpaDistributionSetManagement
|
||||
return specifications;
|
||||
}
|
||||
|
||||
private void assertMetaDataQuota(final Long dsId, final int requested) {
|
||||
final int limit = quotaManagement.getMaxMetaDataEntriesPerDistributionSet();
|
||||
QuotaHelper.assertAssignmentQuota(dsId, requested, limit, "Metadata", DistributionSet.class.getSimpleName(), null);
|
||||
}
|
||||
|
||||
private void assertSoftwareModuleQuota(final Long id, final int requested) {
|
||||
QuotaHelper.assertAssignmentQuota(id, requested, quotaManagement.getMaxSoftwareModulesPerDistributionSet(),
|
||||
SoftwareModule.class, DistributionSet.class, softwareModuleRepository::countByAssignedToId);
|
||||
|
||||
@@ -20,8 +20,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
@@ -48,7 +46,6 @@ import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule.MetadataValue;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
|
||||
import org.eclipse.hawkbit.utils.ObjectCopyUtil;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.dao.ConcurrencyFailureException;
|
||||
import org.springframework.data.domain.Page;
|
||||
@@ -64,12 +61,9 @@ import org.springframework.util.ObjectUtils;
|
||||
@Service
|
||||
@ConditionalOnBooleanProperty(prefix = "hawkbit.jpa", name = { "enabled", "software-module-management" }, matchIfMissing = true)
|
||||
public class JpaSoftwareModuleManagement
|
||||
extends
|
||||
AbstractJpaRepositoryManagement<JpaSoftwareModule, SoftwareModuleManagement.Create, SoftwareModuleManagement.Update, SoftwareModuleRepository, SoftwareModuleFields>
|
||||
extends AbstractJpaRepositoryWithMetadataManagement<JpaSoftwareModule, SoftwareModuleManagement.Create, SoftwareModuleManagement.Update, SoftwareModuleRepository, SoftwareModuleFields, MetadataValue, JpaSoftwareModule.JpaMetadataValue>
|
||||
implements SoftwareModuleManagement<JpaSoftwareModule> {
|
||||
|
||||
protected static final String SOFTWARE_MODULE_METADATA = "SoftwareModuleMetadata";
|
||||
|
||||
private final DistributionSetRepository distributionSetRepository;
|
||||
private final SoftwareModuleTypeRepository softwareModuleTypeRepository;
|
||||
private final ArtifactManagement artifactManagement;
|
||||
@@ -164,88 +158,6 @@ public class JpaSoftwareModuleManagement
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = TX_RT_MAX, backoff = @Backoff(delay = TX_RT_DELAY))
|
||||
public void createMetadata(final Long id, final String key, final MetadataValue value) {
|
||||
final JpaSoftwareModule softwareModule = jpaRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, id));
|
||||
final Map<String, JpaMetadataValue> metadataValueMap = softwareModule.getMetadata();
|
||||
final JpaMetadataValue existingValue = metadataValueMap.get(key);
|
||||
if (existingValue == null) {
|
||||
assertMetadataQuota(metadataValueMap.size() + 1L);
|
||||
}
|
||||
final JpaMetadataValue jpaMetadataValue = existingValue == null ?
|
||||
new JpaMetadataValue() : existingValue;
|
||||
if (ObjectCopyUtil.copy(value, jpaMetadataValue, true, UnaryOperator.identity())) {
|
||||
metadataValueMap.put(key, jpaMetadataValue);
|
||||
jpaRepository.save(softwareModule);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = TX_RT_MAX, backoff = @Backoff(delay = TX_RT_DELAY))
|
||||
public void createMetadata(final Long id, final Map<String, ? extends MetadataValue> metadata) {
|
||||
final JpaSoftwareModule softwareModule = jpaRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, id));
|
||||
final Map<String, JpaMetadataValue> metadataValueMap = softwareModule.getMetadata();
|
||||
assertMetadataQuota(metadata.keySet().stream().filter(key -> !metadataValueMap.containsKey(key)).count() + metadataValueMap.size());
|
||||
final AtomicBoolean changed = new AtomicBoolean(false);
|
||||
metadata.forEach((key, value) -> {
|
||||
final JpaMetadataValue jpaMetadataValue = metadataValueMap.getOrDefault(key, new JpaMetadataValue());
|
||||
if (ObjectCopyUtil.copy(value, jpaMetadataValue, true, UnaryOperator.identity())) {
|
||||
metadataValueMap.put(key, jpaMetadataValue);
|
||||
changed.set(true);
|
||||
}
|
||||
});
|
||||
if (changed.get()) {
|
||||
jpaRepository.save(softwareModule);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MetadataValue getMetadata(final Long id, final String key) {
|
||||
final JpaSoftwareModule softwareModule = jpaRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, id));
|
||||
final MetadataValue metadataValue = softwareModule.getMetadata().get(key);
|
||||
if (metadataValue == null) {
|
||||
throw new EntityNotFoundException(SOFTWARE_MODULE_METADATA, id + ":" + key);
|
||||
} else {
|
||||
return metadataValue;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, MetadataValue> getMetadata(final Long id) {
|
||||
return jpaRepository
|
||||
.findById(id)
|
||||
.map(JpaSoftwareModule::getMetadata)
|
||||
.map(metadata -> metadata.entrySet().stream()
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
e -> (MetadataValue)e.getValue())))
|
||||
.orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, id));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = TX_RT_MAX, backoff = @Backoff(delay = TX_RT_DELAY))
|
||||
public void deleteMetadata(final Long id, final String key) {
|
||||
final JpaSoftwareModule softwareModule = jpaRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, id));
|
||||
final Map<String, JpaMetadataValue> metadataValueMap = softwareModule.getMetadata();
|
||||
if (!metadataValueMap.containsKey(key)) {
|
||||
throw new EntityNotFoundException(SOFTWARE_MODULE_METADATA, id + ":" + key);
|
||||
}
|
||||
metadataValueMap.remove(key);
|
||||
jpaRepository.save(softwareModule);
|
||||
}
|
||||
|
||||
// called only with 'system code' access, so no need to check access control
|
||||
@Override
|
||||
public Map<Long, Map<String, String>> findMetaDataBySoftwareModuleIdsAndTargetVisible(final Collection<Long> ids) {
|
||||
@@ -349,6 +261,17 @@ public class JpaSoftwareModuleManagement
|
||||
return jpaRepository.count(SoftwareModuleSpecification.byAssignedToDs(distributionSetId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the meta-data quota for the software module with the given ID.
|
||||
*
|
||||
* @param requested Number of meta-data entries to be created.
|
||||
*/
|
||||
@Override
|
||||
protected void assertMetadataQuota(final long requested) {
|
||||
final int maxMetaData = quotaManagement.getMaxMetaDataEntriesPerSoftwareModule();
|
||||
QuotaHelper.assertAssignmentQuota(requested, maxMetaData, SoftwareModule.MetadataValueCreate.class, SoftwareModule.class);
|
||||
}
|
||||
|
||||
private void deleteGridFsArtifacts(final JpaSoftwareModule swModule) {
|
||||
jpaRepository.getAccessController().ifPresent(accessController ->
|
||||
accessController.assertOperationAllowed(AccessController.Operation.DELETE, swModule));
|
||||
@@ -364,16 +287,6 @@ public class JpaSoftwareModuleManagement
|
||||
smFilterNameAndVersionEntries[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the meta-data quota for the software module with the given ID.
|
||||
*
|
||||
* @param requested Number of meta-data entries to be created.
|
||||
*/
|
||||
private void assertMetadataQuota(final long requested) {
|
||||
final int maxMetaData = quotaManagement.getMaxMetaDataEntriesPerSoftwareModule();
|
||||
QuotaHelper.assertAssignmentQuota(requested, maxMetaData, SoftwareModule.MetadataValueCreate.class, SoftwareModule.class);
|
||||
}
|
||||
|
||||
private void assertSoftwareModuleTypeExists(final Long typeId) {
|
||||
if (!softwareModuleTypeRepository.existsById(typeId)) {
|
||||
throw new EntityNotFoundException(SoftwareModuleType.class, typeId);
|
||||
|
||||
@@ -711,6 +711,23 @@ public class JpaTargetManagement implements TargetManagement {
|
||||
targetRepository, List.of(TargetSpecifications.hasRequestControllerAttributesTrue()), pageable);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
|
||||
backoff = @Backoff(delay = Constants.TX_RT_DELAY))
|
||||
public void createMetadata(final String controllerId, final String key, final String value) {
|
||||
final JpaTarget target = getByControllerIdAndThrowIfNotFound(controllerId);
|
||||
|
||||
// get the modifiable metadata map
|
||||
final Map<String, String> metadata = target.getMetadata();
|
||||
if (!metadata.containsKey(key)) {
|
||||
throw new EntityNotFoundException("Target metadata", controllerId + ":" + key);
|
||||
}
|
||||
metadata.put(key, value);
|
||||
|
||||
targetRepository.save(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
|
||||
@@ -737,20 +754,8 @@ public class JpaTargetManagement implements TargetManagement {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
|
||||
backoff = @Backoff(delay = Constants.TX_RT_DELAY))
|
||||
public void updateMetadata(final String controllerId, final String key, final String value) {
|
||||
final JpaTarget target = getByControllerIdAndThrowIfNotFound(controllerId);
|
||||
|
||||
// get the modifiable metadata map
|
||||
final Map<String, String> metadata = target.getMetadata();
|
||||
if (!metadata.containsKey(key)) {
|
||||
throw new EntityNotFoundException("Target metadata", controllerId + ":" + key);
|
||||
}
|
||||
metadata.put(key, value);
|
||||
|
||||
targetRepository.save(target);
|
||||
public String getMetadata(final String controllerId, final String key) {
|
||||
return getMap(controllerId, JpaTarget_.metadata).get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -70,7 +70,9 @@ import org.springframework.core.annotation.Order;
|
||||
attributeNodes = { @NamedAttributeNode("modules"), @NamedAttributeNode("tags"), @NamedAttributeNode("type") })
|
||||
// exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities
|
||||
@SuppressWarnings("squid:S2160")
|
||||
public class JpaDistributionSet extends AbstractJpaNamedVersionedEntity implements DistributionSet, EventAwareEntity {
|
||||
public class JpaDistributionSet
|
||||
extends AbstractJpaNamedVersionedEntity
|
||||
implements DistributionSet, WithMetadata<String, String>, EventAwareEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@@ -51,6 +51,7 @@ import org.eclipse.hawkbit.repository.exception.LockedException;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule.MetadataValue;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
|
||||
|
||||
/**
|
||||
@@ -70,7 +71,9 @@ import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
|
||||
@NamedEntityGraph(name = "SoftwareModule.artifacts", attributeNodes = { @NamedAttributeNode("artifacts") })
|
||||
// exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities
|
||||
@SuppressWarnings("squid:S2160")
|
||||
public class JpaSoftwareModule extends AbstractJpaNamedVersionedEntity implements SoftwareModule, EventAwareEntity {
|
||||
public class JpaSoftwareModule
|
||||
extends AbstractJpaNamedVersionedEntity
|
||||
implements SoftwareModule, WithMetadata<MetadataValue, JpaSoftwareModule.JpaMetadataValue>, EventAwareEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 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.model;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Interface for entities that support metadata.
|
||||
* @param <MV> metadata value type
|
||||
* @param <MVI> metadata value implementation type
|
||||
*/
|
||||
@SuppressWarnings("java:S119") // java:S119 - better self explainable
|
||||
public interface WithMetadata<MV, MVI extends MV> {
|
||||
|
||||
Map<String, MVI> getMetadata();
|
||||
|
||||
// return if the entity is valid for update metadata
|
||||
default boolean isValid() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -160,11 +160,11 @@ class DistributionSetAccessControllerTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
// verify distributionSetManagement#updateMetaData
|
||||
final String newValue = "newValue";
|
||||
distributionSetManagement.updateMetadata(permitted.getId(), mdPresetKey, newValue);
|
||||
assertThatThrownBy(() -> distributionSetManagement.updateMetadata(readOnlyId, mdPresetKey, newValue))
|
||||
distributionSetManagement.createMetadata(permitted.getId(), mdPresetKey, newValue);
|
||||
assertThatThrownBy(() -> distributionSetManagement.createMetadata(readOnlyId, mdPresetKey, newValue))
|
||||
.as("Distribution set not allowed to me modified.")
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
assertThatThrownBy(() -> distributionSetManagement.updateMetadata(hiddenId, mdPresetKey, newValue))
|
||||
assertThatThrownBy(() -> distributionSetManagement.createMetadata(hiddenId, mdPresetKey, newValue))
|
||||
.as("Distribution set should not be visible.")
|
||||
.isInstanceOf(EntityNotFoundException.class);
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ class DistributionSetManagementSecurityTest
|
||||
* Tests ManagementAPI PreAuthorized method with correct and insufficient permissions.
|
||||
*/
|
||||
@Test
|
||||
void createMetadataPermissionsCheck() {
|
||||
void createMetadataMapPermissionsCheck() {
|
||||
assertPermissions(
|
||||
() -> {
|
||||
distributionSetManagement.createMetadata(1L, Map.of("key", "value"));
|
||||
@@ -95,9 +95,9 @@ class DistributionSetManagementSecurityTest
|
||||
* Tests ManagementAPI PreAuthorized method with correct and insufficient permissions.
|
||||
*/
|
||||
@Test
|
||||
void updateMetadataPermissionsCheck() {
|
||||
void createMetadataPermissionsCheck() {
|
||||
assertPermissions(() -> {
|
||||
distributionSetManagement.updateMetadata(1L, "key", "value");
|
||||
distributionSetManagement.createMetadata(1L, "key", "value");
|
||||
return null;
|
||||
},
|
||||
List.of(SpPermission.UPDATE_REPOSITORY));
|
||||
|
||||
@@ -156,8 +156,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
|
||||
() -> distributionSetManagement.update(DistributionSetManagement.Update.builder().id(NOT_EXIST_IDL).build()),
|
||||
"DistributionSet");
|
||||
|
||||
verifyThrownExceptionBy(() -> distributionSetManagement.updateMetadata(NOT_EXIST_IDL, "xxx", "xxx"), "DistributionSet");
|
||||
verifyThrownExceptionBy(() -> distributionSetManagement.updateMetadata(set.getId(), NOT_EXIST_ID, "xxx"), "DistributionSet");
|
||||
verifyThrownExceptionBy(() -> distributionSetManagement.createMetadata(NOT_EXIST_IDL, "xxx", "xxx"), "DistributionSet");
|
||||
|
||||
verifyThrownExceptionBy(() -> distributionSetManagement.getOrElseThrowException(NOT_EXIST_IDL), "DistributionSet");
|
||||
|
||||
@@ -240,19 +239,6 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that metadata for a distribution set can be created.
|
||||
*/
|
||||
@Test
|
||||
void createMetadata() {
|
||||
final String knownKey = "dsMetaKnownKey";
|
||||
final String knownValue = "dsMetaKnownValue";
|
||||
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("testDs");
|
||||
|
||||
insertMetadata(knownKey, knownValue, ds); // and validate
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the enforcement of the metadata quota per distribution set.
|
||||
*/
|
||||
@@ -520,7 +506,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
@WithUser(allSpPermissions = true)
|
||||
void updateMetadata() {
|
||||
void createMetadata() {
|
||||
final String knownKey = "myKnownKey";
|
||||
final String knownValue = "myKnownValue";
|
||||
final String knownUpdateValue = "myNewUpdatedValue";
|
||||
@@ -539,7 +525,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
waitNextMillis();
|
||||
// update the DS metadata
|
||||
distributionSetManagement.updateMetadata(ds.getId(), knownKey, knownUpdateValue);
|
||||
distributionSetManagement.createMetadata(ds.getId(), knownKey, knownUpdateValue);
|
||||
// we are updating the sw metadata so also modifying the base software
|
||||
// module so opt lock revision must be three
|
||||
final DistributionSet reloadedDS = getOrThrow(distributionSetManagement.get(ds.getId()));
|
||||
@@ -944,7 +930,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
|
||||
// assert that an existing metadata can not be updated
|
||||
assertThatExceptionOfType(InvalidDistributionSetException.class)
|
||||
.as("Invalid distributionSet should throw an exception")
|
||||
.isThrownBy(() -> distributionSetManagement.updateMetadata(dsId, knownKey1, knownUpdateValue));
|
||||
.isThrownBy(() -> distributionSetManagement.createMetadata(dsId, knownKey1, knownUpdateValue));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -500,13 +500,13 @@ class TargetManagementSecurityTest extends AbstractJpaIntegrationTest {
|
||||
* Tests ManagementAPI PreAuthorized method with correct and insufficient permissions.
|
||||
*/
|
||||
@Test
|
||||
void createMetadataPermissionsCheck() {
|
||||
void createMetadataMapPermissionsCheck() {
|
||||
assertPermissions(
|
||||
() -> {
|
||||
targetManagement.createMetadata("controllerId", Map.of("key", "value"));
|
||||
return null;
|
||||
},
|
||||
List.of(SpPermission.UPDATE_REPOSITORY));
|
||||
List.of(SpPermission.UPDATE_TARGET));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -514,20 +514,20 @@ class TargetManagementSecurityTest extends AbstractJpaIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
void getMetadataPermissionsCheck() {
|
||||
assertPermissions(() -> targetManagement.getMetadata("controllerId"), List.of(SpPermission.READ_REPOSITORY));
|
||||
assertPermissions(() -> targetManagement.getMetadata("controllerId"), List.of(SpPermission.READ_TARGET));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests ManagementAPI PreAuthorized method with correct and insufficient permissions.
|
||||
*/
|
||||
@Test
|
||||
@WithUser(principal = "user", authorities = { SpPermission.UPDATE_REPOSITORY })
|
||||
void updateMetadataPermissionsCheck() {
|
||||
@WithUser(principal = "user", authorities = { SpPermission.UPDATE_TARGET })
|
||||
void createMetadataPermissionsCheck() {
|
||||
assertPermissions(() -> {
|
||||
targetManagement.updateMetadata("controllerId", "key", "value");
|
||||
targetManagement.createMetadata("controllerId", "key", "value");
|
||||
return null;
|
||||
},
|
||||
List.of(SpPermission.UPDATE_REPOSITORY));
|
||||
List.of(SpPermission.UPDATE_TARGET));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -538,6 +538,6 @@ class TargetManagementSecurityTest extends AbstractJpaIntegrationTest {
|
||||
assertPermissions(() -> {
|
||||
targetManagement.deleteMetadata("controllerId", "key");
|
||||
return null;
|
||||
}, List.of(SpPermission.UPDATE_REPOSITORY));
|
||||
}, List.of(SpPermission.UPDATE_TARGET));
|
||||
}
|
||||
}
|
||||
@@ -154,7 +154,7 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
|
||||
verifyThrownExceptionBy(() -> targetManagement.deleteMetadata(NOT_EXIST_ID, "xxx"), "Target");
|
||||
verifyThrownExceptionBy(() -> targetManagement.deleteMetadata(target.getControllerId(), NOT_EXIST_ID), "Target");
|
||||
verifyThrownExceptionBy(() -> targetManagement.getMetadata(NOT_EXIST_ID).get("xxx"), "Target");
|
||||
verifyThrownExceptionBy(() -> targetManagement.updateMetadata(NOT_EXIST_ID, "xxx", "xxx"), "Target");
|
||||
verifyThrownExceptionBy(() -> targetManagement.createMetadata(NOT_EXIST_ID, "xxx", "xxx"), "Target");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -773,17 +773,6 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
|
||||
assertThat(target.isRequestControllerAttributes()).isFalse();
|
||||
assertThat(targetManagement.findByControllerAttributesRequested(PAGE).getContent()).contains(updated);
|
||||
assertThat(targetManagement.isControllerAttributesRequested(knownControllerId)).isTrue();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that metadata for a target can be created.
|
||||
*/
|
||||
@Test
|
||||
void createMetadata() {
|
||||
insertMetadata(
|
||||
"targetMetaKnownKey", "targetMetaKnownValue",
|
||||
testdataFactory.createTarget("targetIdWithMetadata")); // and validate
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -847,7 +836,7 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
@WithUser(allSpPermissions = true)
|
||||
void updateMetadata() {
|
||||
void createMetadata() {
|
||||
final String knownKey = "myKnownKey";
|
||||
final String knownValue = "myKnownValue";
|
||||
final String knownUpdateValue = "myNewUpdatedValue";
|
||||
@@ -864,7 +853,7 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
|
||||
assertThat(changedLockRevisionTarget.getOptLockRevision()).isEqualTo(2);
|
||||
|
||||
// update the target metadata
|
||||
targetManagement.updateMetadata(target.getControllerId(), knownKey, knownUpdateValue);
|
||||
targetManagement.createMetadata(target.getControllerId(), knownKey, knownUpdateValue);
|
||||
// we are updating the target meta-data so also modifying the target so opt lock revision must be three
|
||||
final Target changedLockRevisionTarget2 = targetManagement.get(target.getId()).orElseThrow(NoSuchElementException::new);
|
||||
assertThat(changedLockRevisionTarget2.getOptLockRevision()).isEqualTo(3);
|
||||
|
||||
Reference in New Issue
Block a user