BookingItemEntitySaveProcessor.java

package net.hostsharing.hsadminng.hs.booking.item.validators;

import lombok.Getter;
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;

    @Getter
    private HsBookingItem entity;

    @Getter
    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.legacy: 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;
    }
}