Validator.java

package no.nav.data.common.validator;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import no.nav.data.common.exceptions.ValidationException;
import no.nav.data.common.storage.StorageService;
import no.nav.data.common.storage.domain.DomainObject;
import no.nav.data.common.storage.domain.TypeRegistration;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.hibernate.validator.internal.constraintvalidators.bv.EmailValidator;
import org.springframework.util.Assert;

import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;

import static no.nav.data.common.utils.StreamUtils.nullToEmptyList;
import static no.nav.data.common.utils.StreamUtils.safeStream;
import static no.nav.data.common.utils.StringUtils.isUUID;

@Slf4j
public class Validator<T extends Validated> {

    public static final Pattern NAV_IDENT_PATTERN = Pattern.compile("[A-Z][0-9]{6}");

    public static final String DOES_NOT_EXIST = "doesNotExist";
    public static final String ALREADY_EXISTS = "alreadyExist";
    public static final String ILLEGAL_ARGUMENT = "illegalArgument";
    private static final String ERROR_TYPE_MISSING = "fieldIsNullOrMissing";
    private static final String ERROR_TYPE_PATTERN = "fieldWrongFormat";
    private static final String ERROR_TYPE_ENUM = "fieldIsInvalidEnum";
    private static final String ERROR_TYPE_DATE = "fieldIsInvalidDate";
    private static final String ERROR_TYPE_UUID = "fieldIsInvalidUUID";
    public static final String ERROR_MESSAGE_MISSING = "null or missing";
    private static final String ERROR_MESSAGE_PATTERN = "%s is not valid for pattern '%s'";
    private static final String ERROR_MESSAGE_ENUM = "%s was invalid for type %s";
    private static final String ERROR_MESSAGE_DATE = "%s date is not a valid format";
    private static final String ERROR_MESSAGE_UUID = "%s uuid is not a valid format";

    private static final EmailValidator emailValidator = new EmailValidator();
    private static final String EMAIL_DOMAIN = "@nav.no";

    private final List<ValidationError> validationErrors = new ArrayList<>();
    private final String parentField;
    @Getter
    private final T item;
    private DomainObject domainItem;

    public Validator(T item) {
        this.parentField = "";
        this.item = item;
    }

    public Validator(T item, String parentField) {
        this.parentField = Strings.CI.appendIfMissing(parentField, ".");
        this.item = item;
    }

    public static <R extends RequestElement> Validator<R> validate(R item, StorageService storage) {
        Validator<R> validator = validate(item);
        UUID uuid = item.getIdAsUUID();
        String typeOfRequest = TypeRegistration.typeOfRequest(item);
        validator.domainItem = uuid != null && storage.exists(uuid, typeOfRequest) ? storage.get(uuid, typeOfRequest) : null;
        validator.validateRepositoryValues(item, validator.domainItem != null);
        return validator;
    }

    public static <R extends Validated> Validator<R> validate(R item) {
        item.format();
        RequestElement requestElement = item instanceof RequestElement re ? re : null;
        if (requestElement != null) {
            Assert.isTrue(requestElement.getUpdate() != null, "request not initialized");
        }
        Validator<R> validator = new Validator<>(item);
        item.validateFieldValues(validator);
        return validator;
    }

    @SuppressWarnings("unchecked")
    public <D extends DomainObject> D getDomainItem() {
        return (D) domainItem;
    }

    @SuppressWarnings("unchecked")
    public <D extends DomainObject> D getDomainItem(Class<D> type) {
        return (D) domainItem;
    }

    public void checkExists(String id, StorageService storage, Class<? extends DomainObject> aClass) {
        if (isUUID(id) && !storage.exists(UUID.fromString(id), aClass)) {
            String type = TypeRegistration.typeOf(aClass);
            addError(type, Validator.DOES_NOT_EXIST, type + " " + id + " does not exist");
        }
    }

    public boolean checkBlank(String fieldName, String fieldValue) {
        if (StringUtils.isBlank(fieldValue)) {
            validationErrors.add(new ValidationError(getFieldName(fieldName), ERROR_TYPE_MISSING, ERROR_MESSAGE_MISSING));
            return true;
        }
        return false;
    }

    public void checkNull(String fieldName, Object fieldValue) {
        if (fieldValue == null) {
            validationErrors.add(new ValidationError(getFieldName(fieldName), ERROR_TYPE_MISSING, ERROR_MESSAGE_MISSING));
        }
    }

    public void checkPatternRequired(String fieldName, String value, Pattern pattern) {
        if (checkBlank(fieldName, value)) {
            return;
        }
        checkPattern(fieldName, value, pattern);
    }

    public void checkPattern(String fieldName, String value, Pattern pattern) {
        if (StringUtils.isBlank(value)) {
            return;
        }
        if (!pattern.matcher(value).matches()) {
            validationErrors.add(new ValidationError(getFieldName(fieldName), ERROR_TYPE_PATTERN, String.format(ERROR_MESSAGE_PATTERN, value, pattern)));
        }
    }

    public <E extends Enum<E>> void checkRequiredEnum(String fieldName, String fieldValue, Class<E> type) {
        if (checkBlank(fieldName, fieldValue)) {
            return;
        }
        checkEnum(fieldName, fieldValue, type);
    }

    public <E extends Enum<E>> void checkEnum(String fieldName, String fieldValue, Class<E> type) {
        if (StringUtils.isBlank(fieldValue)) {
            return;
        }
        try {
            Enum.valueOf(type, fieldValue);
        } catch (IllegalArgumentException e) {
            validationErrors.add(new ValidationError(getFieldName(fieldName), ERROR_TYPE_ENUM, String.format(ERROR_MESSAGE_ENUM, fieldValue, type.getSimpleName())));
        }
    }

    public void checkDate(String fieldName, String fieldValue) {
        if (StringUtils.isBlank(fieldValue)) {
            return;
        }
        try {
            LocalDate.parse(fieldValue);
        } catch (DateTimeParseException e) {
            validationErrors.add(new ValidationError(getFieldName(fieldName), ERROR_TYPE_DATE, String.format(ERROR_MESSAGE_DATE, fieldValue)));
        }
    }

    public void checkUUID(String fieldName, String fieldValue) {
        if (StringUtils.isBlank(fieldValue)) {
            return;
        }
        try {
            //noinspection ResultOfMethodCallIgnored
            UUID.fromString(fieldValue);
        } catch (Exception e) {
            validationErrors.add(new ValidationError(getFieldName(fieldName), ERROR_TYPE_UUID, String.format(ERROR_MESSAGE_UUID, fieldValue)));
        }
    }

    public void checkEmail(String fieldName, String fieldValue) {
        if (!emailValidator.isValid(fieldValue, null)) {
            validationErrors.add(new ValidationError(getFieldName(fieldName), "invalidEmail", "%s is an invalid email".formatted(fieldValue)));
        } else if (!Strings.CI.endsWith(fieldValue, EMAIL_DOMAIN)) {
            validationErrors.add(new ValidationError(getFieldName(fieldName), "invalidEmail", "%s is not an @nav.no email".formatted(fieldValue)));
        }
    }


    public void addError(String fieldName, String errorType, String errorMessage) {
        validationErrors.add(new ValidationError(getFieldName(fieldName), errorType, errorMessage));
    }

    public void checkId(RequestElement request) {
        boolean nullId = request.getId() == null;
        boolean update = request.isUpdate();
        if (update && nullId) {
            validationErrors
                    .add(new ValidationError(getFieldName("id"), "missingIdForUpdate", "Request is missing ID for update"));
        } else if (!update && !nullId) {
            validationErrors.add(new ValidationError(getFieldName("id"), "idForCreate", "Request has ID for create"));
        }
    }

    private String getFieldName(String fieldName) {
        return parentField + fieldName;
    }

    public void validateType(String fieldName, Collection<? extends Validated> fieldValues) {
        AtomicInteger i = new AtomicInteger(0);
        safeStream(fieldValues).forEach(fieldValue -> validateType(String.format("%s[%d]", fieldName, i.getAndIncrement()), fieldValue));
    }

    public void validateType(String fieldName, Validated fieldValue) {
        Validator<Validated> validator = new Validator<>(fieldValue, parentField + fieldName);
        fieldValue.format();
        fieldValue.validateFieldValues(validator);
        validationErrors.addAll(validator.getErrors());
    }

    void validateRepositoryValues(RequestElement request, boolean existInRepository) {
        if (creatingExistingElement(request.isUpdate(), existInRepository)) {
            validationErrors.add(new ValidationError(getFieldName("id"), "creatingExisting",
                    String.format("The %s %s already exists and therefore cannot be created", request.getRequestType(), request.getId())));
        }

        if (updatingNonExistingElement(request.isUpdate(), existInRepository)) {
            validationErrors.add(new ValidationError(getFieldName("id"), "updatingNonExisting",
                    String.format("The %s %s does not exist and therefore cannot be updated", request.getRequestType(), request.getId())));
        }
    }

    private boolean creatingExistingElement(boolean isUpdate, boolean existInRepository) {
        return !isUpdate && existInRepository;
    }

    private boolean updatingNonExistingElement(boolean isUpdate, boolean existInRepository) {
        return isUpdate && !existInRepository;
    }

    public void ifErrorsThrowValidationException() {
        if (!validationErrors.isEmpty()) {
            log.warn("The request was not accepted. The following errors occurred during validation:{}", validationErrors);
            throw new ValidationException(validationErrors, "The request was not accepted. The following errors occurred during validation:");
        }
    }

    public final <R> Validator<T> addValidations(Function<? super T, Collection<R>> extractor, BiConsumer<Validator<T>, R> consumer) {
        Collection<R> subItems = extractor.apply(item);
        nullToEmptyList(subItems).forEach(it -> consumer.accept(this, it));
        return this;
    }

    public final <R> Validator<T> addValidation(Function<? super T, R> extractor, BiConsumer<Validator<T>, R> consumer) {
        R subItem = extractor.apply(item);
        consumer.accept(this, subItem);
        return this;
    }

    public final Validator<T> addValidations(Consumer<Validator<T>> consumer) {
        consumer.accept(this);
        return this;
    }

    public List<ValidationError> getErrors() {
        return validationErrors;
    }
}