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