HostingAssetEntityValidator.java

package net.hostsharing.hsadminng.hs.hosting.asset.validators;

import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import net.hostsharing.hsadminng.hs.validation.ValidatableProperty;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Arrays.stream;
import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;

public abstract class HostingAssetEntityValidator extends HsEntityValidator<HsHostingAsset> {

    static final ValidatableProperty<?, ?>[] NO_EXTRA_PROPERTIES = new ValidatableProperty<?, ?>[0];

    private final ReferenceValidator<HsBookingItem, HsBookingItemType> bookingItemReferenceValidation;
    private final ReferenceValidator<HsHostingAsset, HsHostingAssetType> parentAssetReferenceValidation;
    private final ReferenceValidator<HsHostingAsset, HsHostingAssetType> assignedToAssetReferenceValidation;
    private final HostingAssetEntityValidator.AlarmContact alarmContactValidation;

    HostingAssetEntityValidator(
            final HsHostingAssetType assetType,
            final AlarmContact alarmContactValidation, // hostmaster alert address is implicitly added where needed
            final ValidatableProperty<?, ?>... properties) {
        super(properties);
        this.bookingItemReferenceValidation = new ReferenceValidator<>(
                assetType.bookingItemPolicy(),
                assetType.bookingItemTypes(),
                HsHostingAsset::getBookingItem,
                HsBookingItem::getType);
        this.parentAssetReferenceValidation = new ReferenceValidator<>(
                assetType.parentAssetPolicy(),
                assetType.parentAssetTypes(),
                HsHostingAsset::getParentAsset,
                HsHostingAsset::getType);
        this.assignedToAssetReferenceValidation = new ReferenceValidator<>(
                assetType.assignedToAssetPolicy(),
                assetType.assignedToAssetTypes(),
                HsHostingAsset::getAssignedToAsset,
                HsHostingAsset::getType);
        this.alarmContactValidation = alarmContactValidation;
    }

    @Override
    public List<String> validateEntity(final HsHostingAsset assetEntity) {
        return sequentiallyValidate(
                () -> validateEntityReferencesAndProperties(assetEntity),
                () -> validateIdentifierPattern(assetEntity)
        );
    }

    @Override
    public List<String> validateContext(final HsHostingAsset assetEntity) {
        return sequentiallyValidate(
                () -> optionallyValidate(assetEntity.getBookingItem()),
                () -> optionallyValidate(assetEntity.getParentAsset()),
                () -> validateAgainstSubEntities(assetEntity)
        );
    }

    private List<String> validateEntityReferencesAndProperties(final HsHostingAsset assetEntity) {
        return Stream.of(
                        validateReferencedEntity(assetEntity, "bookingItem", bookingItemReferenceValidation::validate),
                        validateReferencedEntity(assetEntity, "parentAsset", parentAssetReferenceValidation::validate),
                        validateReferencedEntity(assetEntity, "assignedToAsset", assignedToAssetReferenceValidation::validate),
                        validateReferencedEntity(assetEntity, "alarmContact", alarmContactValidation::validate),
                        validateProperties(assetEntity))
                .filter(Objects::nonNull)
                .flatMap(List::stream)
                .filter(Objects::nonNull)
                .toList();
    }

    private List<String> validateReferencedEntity(
            final HsHostingAsset assetEntity,
            final String referenceFieldName,
            final BiFunction<HsHostingAsset, String, List<String>> validator) {
        return enrich(prefix(assetEntity.toShortString()), validator.apply(assetEntity, referenceFieldName));
    }

    private List<String> validateProperties(final HsHostingAsset assetEntity) {
        return enrich(prefix(assetEntity.toShortString(), "config"), super.validateProperties(assetEntity));
    }

    private static List<String> optionallyValidate(final HsHostingAsset assetEntity) {
        return assetEntity != null
                ? enrich(
                    prefix(assetEntity.toShortString(), "parentAsset"),
                    HostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validateContext(assetEntity))
                : emptyList();
    }

    private static List<String> optionallyValidate(final HsBookingItem bookingItem) {
        return bookingItem != null
                ? enrich(
                prefix(bookingItem.toShortString(), "bookingItem"),
                HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem))
                : emptyList();
    }

    protected List<String> validateAgainstSubEntities(final HsHostingAsset assetEntity) {
        return enrich(
                prefix(assetEntity.toShortString(), "config"),
                stream(propertyValidators)
                        .filter(ValidatableProperty::isTotalsValidator)
                        .map(prop -> validateMaxTotalValue(assetEntity, prop))
                        .filter(Objects::nonNull)
                        .toList());
    }

    // TODO.test: check, if there are any hosting assets which need this validation at all
    private String validateMaxTotalValue(
            final HsHostingAsset hostingAsset,
            final ValidatableProperty<?, ?> propDef) {
        final var propName = propDef.propertyName();
        final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse("");
        final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList())
                .stream()
                .map(subItem -> propDef.getValue(subItem.getConfig()))
                .map(HsEntityValidator::toIntegerWithDefault0)
                .reduce(0, Integer::sum);
        final var maxValue = getIntegerValueWithDefault0(propDef, hostingAsset.getConfig());
        return totalValue > maxValue
                ? "%s' maximum total is %d%s, but actual total %s is %d%s".formatted(
                propName, maxValue, propUnit, propName, totalValue, propUnit)
                : null;
    }

    private List<String> validateIdentifierPattern(final HsHostingAsset assetEntity) {
        final var expectedIdentifierPattern = identifierPattern(assetEntity);
        if (assetEntity.getIdentifier() == null ||
                !expectedIdentifierPattern.matcher(assetEntity.getIdentifier()).matches()) {
            return List.of(
                    "'identifier' expected to match '" + expectedIdentifierPattern + "' but is '" + assetEntity.getIdentifier()
                            + "'");
        }
        return Collections.emptyList();
    }

    protected abstract Pattern identifierPattern(HsHostingAsset assetEntity);

    static class ReferenceValidator<S, T> {

        private final HsHostingAssetType.RelationPolicy policy;
        private final Set<T> referencedEntityTypes;
        private final Function<HsHostingAsset, S> referencedEntityGetter;
        private final Function<S, T> referencedEntityTypeGetter;

        public ReferenceValidator(
                final HsHostingAssetType.RelationPolicy policy,
                final Set<T> referencedEntityTypes,
                final Function<HsHostingAsset, S> referencedEntityGetter,
                final Function<S, T> referencedEntityTypeGetter) {
            this.policy = policy;
            this.referencedEntityTypes = referencedEntityTypes;
            this.referencedEntityGetter = referencedEntityGetter;
            this.referencedEntityTypeGetter = referencedEntityTypeGetter;
        }

        public ReferenceValidator(
                final HsHostingAssetType.RelationPolicy policy,
                final Function<HsHostingAsset, S> referencedEntityGetter) {
            this.policy = policy;
            this.referencedEntityTypes = Set.of();
            this.referencedEntityGetter = referencedEntityGetter;
            this.referencedEntityTypeGetter = e -> null;
        }

        @SuppressWarnings("PMD.CognitiveComplexity") // TODO.refa: cognitive complexity 21, split per policy branch
        List<String> validate(final HsHostingAsset assetEntity, final String referenceFieldName) {

            final var referencedEntity = referencedEntityGetter.apply(assetEntity);
            final var referencedEntityType = referencedEntity != null ? referencedEntityTypeGetter.apply(referencedEntity) : null;

            switch (policy) {
            case REQUIRED:
                if (!referencedEntityTypes.contains(referencedEntityType)) {
                    return List.of(referencedEntityType == null
                            ? referenceFieldName + "' must be of type " + toDisplay(referencedEntityTypes) + " but is null"
                            : referenceFieldName + "' must be of type " + toDisplay(referencedEntityTypes) + " but is of type " + referencedEntityType);
                }
                break;
            case TERMINATORY:
                if (assetEntity.getParentAsset() != null && assetEntity.getBookingItem() != null) {
                    return List.of(referenceFieldName + "' or parentItem must be null but is of type " + referencedEntityType);
                }
                if (assetEntity.getParentAsset() == null && !referencedEntityTypes.contains(referencedEntityType)) {
                    return List.of(referencedEntityType == null
                            ? referenceFieldName + "' must be of type " + toDisplay(referencedEntityTypes) + " but is null"
                            : referenceFieldName + "' must be of type " + toDisplay(referencedEntityTypes) + " but is of type " + referencedEntityType);
                }
                break;
            case OPTIONAL:
                if (referencedEntityType != null && !referencedEntityTypes.contains(referencedEntityType)) {
                    return List.of(referenceFieldName + "' must be null or of type " + toDisplay(referencedEntityTypes) + " but is of type "
                            + referencedEntityType);
                }
                break;
            case FORBIDDEN:
                if (referencedEntityType != null) {
                    return List.of(referenceFieldName + "' must be null but is of type " + referencedEntityType);
                }
                break;
            }
            return emptyList();
        }

        private String toDisplay(final Set<T> referencedEntityTypes) {
            return referencedEntityTypes.stream().sorted().map(Object::toString).collect(Collectors.joining(" or "));
        }
    }

    static class AlarmContact extends ReferenceValidator<HsOfficeContactRealEntity, Enum<?>> {

        AlarmContact(final HsHostingAssetType.RelationPolicy policy) {
            super(policy, HsHostingAsset::getAlarmContact);
        }

        // hostmaster alert address is implicitly added where neccessary
        static AlarmContact isOptional() {
            return new AlarmContact(HsHostingAssetType.RelationPolicy.OPTIONAL);
        }
    }

}