introduce BookingItemEntityProcessor

This commit is contained in:
Michael Hoennig 2024-09-25 09:48:00 +02:00
parent 5014160113
commit fdbe46311a
5 changed files with 174 additions and 13 deletions

View File

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

View File

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

View File

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

View File

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

View File

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