introduce BookingItemEntityProcessor
This commit is contained in:
parent
5014160113
commit
fdbe46311a
@ -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.HsBookingItemInsertResource;
|
||||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource;
|
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.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.item.validators.HsBookingItemEntityValidatorRegistry;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
||||||
import net.hostsharing.hsadminng.mapper.KeyValueMap;
|
import net.hostsharing.hsadminng.mapper.KeyValueMap;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -48,7 +50,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
|||||||
|
|
||||||
final var entities = bookingItemRepo.findAllByProjectUuid(projectUuid);
|
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);
|
return ResponseEntity.ok(resources);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,16 +64,20 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
|||||||
context.define(currentSubject, assumedRoles);
|
context.define(currentSubject, assumedRoles);
|
||||||
|
|
||||||
final var entityToSave = mapper.map(body, HsBookingItemRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
final var entityToSave = mapper.map(body, HsBookingItemRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||||
|
final var mapped = new BookingItemEntitySaveProcessor(em, entityToSave)
|
||||||
final var saved = HsBookingItemEntityValidatorRegistry.validated(em, bookingItemRepo.save(entityToSave));
|
.preprocessEntity()
|
||||||
|
.validateEntity()
|
||||||
|
.prepareForSave()
|
||||||
|
.save()
|
||||||
|
.validateContext()
|
||||||
|
.mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER))
|
||||||
|
.revampProperties();
|
||||||
|
|
||||||
final var uri =
|
final var uri =
|
||||||
MvcUriComponentsBuilder.fromController(getClass())
|
MvcUriComponentsBuilder.fromController(getClass())
|
||||||
.path("/api/hs/booking/items/{id}")
|
.path("/api/hs/booking/items/{id}")
|
||||||
.buildAndExpand(saved.getUuid())
|
.buildAndExpand(mapped.getUuid())
|
||||||
.toUri();
|
.toUri();
|
||||||
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
|
||||||
|
|
||||||
return ResponseEntity.created(uri).body(mapped);
|
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
|
result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading
|
||||||
return result
|
return result
|
||||||
.map(bookingItemEntity -> ResponseEntity.ok(
|
.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());
|
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,18 +127,21 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
|||||||
new HsBookingItemEntityPatcher(current).apply(body);
|
new HsBookingItemEntityPatcher(current).apply(body);
|
||||||
|
|
||||||
final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(em, current));
|
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);
|
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());
|
resource.setValidFrom(entity.getValidity().lower());
|
||||||
if (entity.getValidity().hasUpperBound()) {
|
if (entity.getValidity().hasUpperBound()) {
|
||||||
resource.setValidTo(entity.getValidity().upper().minusDays(1));
|
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) -> {
|
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.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo()));
|
||||||
entity.putResources(KeyValueMap.from(resource.getResources()));
|
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) {
|
public static List<String> doValidate(final EntityManager em, final HsBookingItem bookingItem) {
|
||||||
|
final var bookingItemValidator = HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType());
|
||||||
return HsEntityValidator.doWithEntityManager(em, () ->
|
return HsEntityValidator.doWithEntityManager(em, () ->
|
||||||
HsEntityValidator.sequentiallyValidate(
|
HsEntityValidator.sequentiallyValidate(
|
||||||
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateEntity(bookingItem),
|
() -> bookingItemValidator.validateEntity(bookingItem),
|
||||||
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(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) {
|
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 -> {
|
stream(propertyValidators).forEach(p -> {
|
||||||
if (p.isWriteOnly()) {
|
if (p.isWriteOnly()) {
|
||||||
copy.remove(p.propertyName);
|
copy.remove(p.propertyName);
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package net.hostsharing.hsadminng.mapper;
|
package net.hostsharing.hsadminng.mapper;
|
||||||
|
|
||||||
import org.modelmapper.ModelMapper;
|
import org.modelmapper.ModelMapper;
|
||||||
|
import org.modelmapper.convention.MatchingStrategies;
|
||||||
|
import org.modelmapper.spi.MatchingStrategy;
|
||||||
import org.springframework.util.ReflectionUtils;
|
import org.springframework.util.ReflectionUtils;
|
||||||
|
|
||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
@ -8,11 +10,16 @@ import jakarta.persistence.ManyToOne;
|
|||||||
import jakarta.persistence.PersistenceContext;
|
import jakarta.persistence.PersistenceContext;
|
||||||
import jakarta.validation.ValidationException;
|
import jakarta.validation.ValidationException;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.stream.Collectors;
|
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 net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
|
||||||
|
import static org.modelmapper.convention.MatchingStrategies.STRICT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A nicer API for ModelMapper.
|
* A nicer API for ModelMapper.
|
||||||
@ -24,6 +31,8 @@ public class Mapper extends ModelMapper {
|
|||||||
|
|
||||||
public Mapper() {
|
public Mapper() {
|
||||||
getConfiguration().setAmbiguityIgnored(true);
|
getConfiguration().setAmbiguityIgnored(true);
|
||||||
|
// getConfiguration().setMatchingStrategy(STRICT);
|
||||||
|
// getConfiguration().setDeepCopyEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public <S, T> List<T> mapList(final List<S> source, final Class<T> targetClass) {
|
public <S, T> List<T> mapList(final List<S> source, final Class<T> targetClass) {
|
||||||
@ -46,7 +55,7 @@ public class Mapper extends ModelMapper {
|
|||||||
@Override
|
@Override
|
||||||
public <D> D map(final Object source, final Class<D> destinationType) {
|
public <D> D map(final Object source, final Class<D> destinationType) {
|
||||||
final var target = super.map(source, destinationType);
|
final var target = super.map(source, destinationType);
|
||||||
for (Field f : destinationType.getDeclaredFields()) {
|
for (Field f : getDeclaredFieldsIncludingSuperClasses(destinationType)) {
|
||||||
if (f.getAnnotation(ManyToOne.class) == null) {
|
if (f.getAnnotation(ManyToOne.class) == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -69,6 +78,17 @@ public class Mapper extends ModelMapper {
|
|||||||
return target;
|
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) {
|
private Object findEntityById(final Class<?> entityClass, final Object subEntityUuid) {
|
||||||
// using getReference would be more efficent, but results in very technical error messages
|
// using getReference would be more efficent, but results in very technical error messages
|
||||||
final var entity = em.find(entityClass, subEntityUuid);
|
final var entity = em.find(entityClass, subEntityUuid);
|
||||||
|
Loading…
Reference in New Issue
Block a user