refactoring for implicit creation of dependend hosting-assets #108
@ -5,7 +5,9 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsA
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.validators.BookingItemEntitySaveProcessor;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
|
||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
||||
import net.hostsharing.hsadminng.mapper.KeyValueMap;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -48,7 +50,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
|
||||
final var entities = bookingItemRepo.findAllByProjectUuid(projectUuid);
|
||||
|
||||
final var resources = mapper.mapList(entities, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var resources = mapper.mapList(entities, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.ok(resources);
|
||||
}
|
||||
|
||||
@ -62,16 +64,20 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var entityToSave = mapper.map(body, HsBookingItemRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
|
||||
final var saved = HsBookingItemEntityValidatorRegistry.validated(em, bookingItemRepo.save(entityToSave));
|
||||
final var mapped = new BookingItemEntitySaveProcessor(em, entityToSave)
|
||||
.preprocessEntity()
|
||||
.validateEntity()
|
||||
.prepareForSave()
|
||||
.save()
|
||||
.validateContext()
|
||||
.mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER))
|
||||
.revampProperties();
|
||||
|
||||
final var uri =
|
||||
MvcUriComponentsBuilder.fromController(getClass())
|
||||
.path("/api/hs/booking/items/{id}")
|
||||
.buildAndExpand(saved.getUuid())
|
||||
.buildAndExpand(mapped.getUuid())
|
||||
.toUri();
|
||||
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
|
||||
return ResponseEntity.created(uri).body(mapped);
|
||||
}
|
||||
|
||||
@ -88,7 +94,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading
|
||||
return result
|
||||
.map(bookingItemEntity -> ResponseEntity.ok(
|
||||
mapper.map(bookingItemEntity, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER)))
|
||||
mapper.map(bookingItemEntity, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER)))
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@ -121,18 +127,21 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
new HsBookingItemEntityPatcher(current).apply(body);
|
||||
|
||||
final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(em, current));
|
||||
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var mapped = mapper.map(saved, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
|
||||
final BiConsumer<HsBookingItemRbacEntity, HsBookingItemResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
final BiConsumer<HsBookingItem, HsBookingItemResource> ITEM_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setValidFrom(entity.getValidity().lower());
|
||||
if (entity.getValidity().hasUpperBound()) {
|
||||
resource.setValidTo(entity.getValidity().upper().minusDays(1));
|
||||
}
|
||||
};
|
||||
|
||||
final BiConsumer<HsBookingItemRbacEntity, HsBookingItemResource> RBAC_ENTITY_TO_RESOURCE_POSTMAPPER = ITEM_TO_RESOURCE_POSTMAPPER::accept;
|
||||
|
||||
final BiConsumer<HsBookingItemInsertResource, HsBookingItemRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
entity.setProject(em.getReference(HsBookingProjectRealEntity.class, resource.getProjectUuid()));
|
||||
entity.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo()));
|
||||
entity.putResources(KeyValueMap.from(resource.getResources()));
|
||||
};
|
||||
|
@ -0,0 +1,131 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
||||
|
||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
||||
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
// TODO.refa: introduce common base class with HsHostingAssetEntitySaveProcessor
|
||||
/**
|
||||
* Wraps the steps of the pararation, validation, mapping and revamp around saving of a HsBookingItem into a readable API.
|
||||
*/
|
||||
public class BookingItemEntitySaveProcessor {
|
||||
|
||||
private final HsEntityValidator<HsBookingItem> validator;
|
||||
private String expectedStep = "preprocessEntity";
|
||||
private final EntityManager em;
|
||||
private HsBookingItem entity;
|
||||
private HsBookingItemResource resource;
|
||||
|
||||
public BookingItemEntitySaveProcessor(final EntityManager em, final HsBookingItem entity) {
|
||||
this.em = em;
|
||||
this.entity = entity;
|
||||
this.validator = HsBookingItemEntityValidatorRegistry.forType(entity.getType());
|
||||
}
|
||||
|
||||
/// initial step allowing to set default values before any validations
|
||||
public BookingItemEntitySaveProcessor preprocessEntity() {
|
||||
step("preprocessEntity", "validateEntity");
|
||||
validator.preprocessEntity(entity);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// validates the entity itself including its properties
|
||||
public BookingItemEntitySaveProcessor validateEntity() {
|
||||
step("validateEntity", "prepareForSave");
|
||||
MultiValidationException.throwIfNotEmpty(validator.validateEntity(entity));
|
||||
return this;
|
||||
}
|
||||
|
||||
// TODO.impl: remove once the migration of legacy data is done
|
||||
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
|
||||
public BookingItemEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
|
||||
step("validateEntity", "prepareForSave");
|
||||
final var ignoreRegExpPatterns = Arrays.stream(ignoreRegExp).map(Pattern::compile).toList();
|
||||
MultiValidationException.throwIfNotEmpty(
|
||||
validator.validateEntity(entity).stream()
|
||||
.filter(error -> ignoreRegExpPatterns.stream().noneMatch(p -> p.matcher(error).matches() ))
|
||||
.toList()
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// hashing passwords etc.
|
||||
public BookingItemEntitySaveProcessor prepareForSave() {
|
||||
step("prepareForSave", "save");
|
||||
validator.prepareProperties(em, entity);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the entity using the given `saveFunction`.
|
||||
*
|
||||
* <p>`validator.postPersist(em, entity)` is NOT called.
|
||||
* If any postprocessing is necessary, the saveFunction has to implement this.</p>
|
||||
* @param saveFunction
|
||||
* @return this
|
||||
*/
|
||||
public BookingItemEntitySaveProcessor saveUsing(final Function<HsBookingItem, HsBookingItem> saveFunction) {
|
||||
step("save", "validateContext");
|
||||
entity = saveFunction.apply(entity);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the using the `EntityManager`, but does NOT ever merge the entity.
|
||||
*
|
||||
* <p>`validator.postPersist(em, entity)` is called afterwards with the entity guaranteed to be flushed to the database.</p>
|
||||
* @return this
|
||||
*/
|
||||
public BookingItemEntitySaveProcessor save() {
|
||||
return saveUsing(e -> {
|
||||
if (!em.contains(entity)) {
|
||||
em.persist(entity);
|
||||
}
|
||||
em.flush(); // makes RbacEntity available as RealEntity if needed
|
||||
validator.postPersist(em, entity);
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
/// validates the entity within it's parent and child hierarchy (e.g. totals validators and other limits)
|
||||
public BookingItemEntitySaveProcessor validateContext() {
|
||||
step("validateContext", "mapUsing");
|
||||
return HsEntityValidator.doWithEntityManager(em, () -> {
|
||||
MultiValidationException.throwIfNotEmpty(validator.validateContext(entity));
|
||||
return this;
|
||||
});
|
||||
}
|
||||
|
||||
/// maps entity to JSON resource representation
|
||||
public BookingItemEntitySaveProcessor mapUsing(
|
||||
final Function<HsBookingItem, HsBookingItemResource> mapFunction) {
|
||||
step("mapUsing", "revampProperties");
|
||||
resource = mapFunction.apply(entity);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// removes write-only-properties and ads computed-properties
|
||||
@SuppressWarnings("unchecked")
|
||||
public HsBookingItemResource revampProperties() {
|
||||
step("revampProperties", null);
|
||||
final var revampedProps = validator.revampProperties(em, entity, (Map<String, Object>) resource.getResources());
|
||||
resource.setResources(revampedProps);
|
||||
return resource;
|
||||
}
|
||||
|
||||
// Makes sure that the steps are called in the correct order.
|
||||
// Could also be implemented using an interface per method, but that seems exaggerated.
|
||||
private void step(final String current, final String next) {
|
||||
if (!expectedStep.equals(current)) {
|
||||
throw new IllegalStateException("expected " + expectedStep + " but got " + current);
|
||||
}
|
||||
expectedStep = next;
|
||||
}
|
||||
}
|
@ -48,10 +48,11 @@ public class HsBookingItemEntityValidatorRegistry {
|
||||
}
|
||||
|
||||
public static List<String> doValidate(final EntityManager em, final HsBookingItem bookingItem) {
|
||||
final var bookingItemValidator = HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType());
|
||||
return HsEntityValidator.doWithEntityManager(em, () ->
|
||||
HsEntityValidator.sequentiallyValidate(
|
||||
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateEntity(bookingItem),
|
||||
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem))
|
||||
() -> bookingItemValidator.validateEntity(bookingItem),
|
||||
() -> bookingItemValidator.validateContext(bookingItem))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -130,7 +130,7 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
|
||||
}
|
||||
|
||||
public Map<String, Object> revampProperties(final EntityManager em, final E entity, final Map<String, Object> config) {
|
||||
final var copy = new HashMap<>(config);
|
||||
final var copy = config != null ? new HashMap<>(config) : new HashMap();
|
||||
stream(propertyValidators).forEach(p -> {
|
||||
if (p.isWriteOnly()) {
|
||||
copy.remove(p.propertyName);
|
||||
|
@ -1,6 +1,8 @@
|
||||
package net.hostsharing.hsadminng.mapper;
|
||||
|
||||
import org.modelmapper.ModelMapper;
|
||||
import org.modelmapper.convention.MatchingStrategies;
|
||||
import org.modelmapper.spi.MatchingStrategy;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
@ -8,11 +10,16 @@ import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
|
||||
import static org.modelmapper.convention.MatchingStrategies.STRICT;
|
||||
|
||||
/**
|
||||
* A nicer API for ModelMapper.
|
||||
@ -24,6 +31,8 @@ public class Mapper extends ModelMapper {
|
||||
|
||||
public Mapper() {
|
||||
getConfiguration().setAmbiguityIgnored(true);
|
||||
// getConfiguration().setMatchingStrategy(STRICT);
|
||||
// getConfiguration().setDeepCopyEnabled(true);
|
||||
}
|
||||
|
||||
public <S, T> List<T> mapList(final List<S> source, final Class<T> targetClass) {
|
||||
@ -46,7 +55,7 @@ public class Mapper extends ModelMapper {
|
||||
@Override
|
||||
public <D> D map(final Object source, final Class<D> destinationType) {
|
||||
final var target = super.map(source, destinationType);
|
||||
for (Field f : destinationType.getDeclaredFields()) {
|
||||
for (Field f : getDeclaredFieldsIncludingSuperClasses(destinationType)) {
|
||||
if (f.getAnnotation(ManyToOne.class) == null) {
|
||||
continue;
|
||||
}
|
||||
@ -69,6 +78,17 @@ public class Mapper extends ModelMapper {
|
||||
return target;
|
||||
}
|
||||
|
||||
private static <D> Field[] getDeclaredFieldsIncludingSuperClasses(final Class<D> destinationType) {
|
||||
if (destinationType == null) {
|
||||
return new Field[0];
|
||||
}
|
||||
|
||||
return Stream.concat(
|
||||
stream(destinationType.getDeclaredFields()),
|
||||
stream(getDeclaredFieldsIncludingSuperClasses(destinationType.getSuperclass())))
|
||||
.toArray(Field[]::new);
|
||||
}
|
||||
|
||||
private Object findEntityById(final Class<?> entityClass, final Object subEntityUuid) {
|
||||
// using getReference would be more efficent, but results in very technical error messages
|
||||
final var entity = em.find(entityClass, subEntityUuid);
|
||||
|
Loading…
Reference in New Issue
Block a user