package io.dropwizard.jersey.validation;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.dropwizard.util.Lists;
import io.dropwizard.util.Strings;
import io.dropwizard.validation.ValidationMethod;
import io.dropwizard.validation.selfvalidating.SelfValidating;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.glassfish.jersey.server.model.Invocable;
import org.glassfish.jersey.server.model.Parameter;
import javax.validation.ConstraintViolation;
import javax.validation.ElementKind;
import javax.validation.Path;
import javax.validation.metadata.ConstraintDescriptor;
import java.lang.reflect.Field;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
public class ConstraintMessage {
private static final Cache<Pair<Path, ? extends ConstraintDescriptor<?>>, String> PREFIX_CACHE =
Caffeine.newBuilder()
.expireAfterWrite(Duration.ofHours(1))
.build();
private ConstraintMessage() {
}
public static String getMessage(ConstraintViolation<?> v, Invocable invocable) {
final Pair<Path, ? extends ConstraintDescriptor<?>> of =
Pair.of(v.getPropertyPath(), v.getConstraintDescriptor());
final String cachePrefix = PREFIX_CACHE.getIfPresent(of);
if (cachePrefix == null) {
final String prefix = calculatePrefix(v, invocable);
PREFIX_CACHE.put(of, prefix);
return prefix + v.getMessage();
}
return cachePrefix + v.getMessage();
}
private static String calculatePrefix(ConstraintViolation<?> v, Invocable invocable) {
final Optional<String> returnValueName = getMethodReturnValueName(v);
if (returnValueName.isPresent()) {
final String name = isValidationMethod(v) ?
StringUtils.substringBeforeLast(returnValueName.get(), ".") : returnValueName.get();
return name + " ";
}
if (isValidationMethod(v) || isSelfValidating(v)) {
return "";
}
final Optional<String> entity = isRequestEntity(v, invocable);
if (entity.isPresent()) {
final String prefix = Strings.isNullOrEmpty(entity.get()) ? "The request body" : entity.get();
return prefix + " " ;
}
final Optional<String> memberName = getMemberName(v, invocable);
return memberName.map(s -> s + " ").orElseGet(() -> v.getPropertyPath() + " ");
}
public static Optional<String> isRequestEntity(ConstraintViolation<?> violation, Invocable invocable) {
final Collection<Path.Node> propertyPath = Lists.of(violation.getPropertyPath());
final Path.Node parent = propertyPath.stream()
.skip(1L)
.findFirst()
.orElse(null);
if (parent == null) {
return Optional.empty();
}
final List<Parameter> parameters = invocable.getParameters();
if (parent.getKind() == ElementKind.PARAMETER) {
final Parameter param = parameters.get(parent.as(Path.ParameterNode.class).getParameterIndex());
if (param.getSource().equals(Parameter.Source.UNKNOWN)) {
final String path = propertyPath.stream()
.skip(2L)
.map(Path.Node::toString)
.collect(Collectors.joining("."));
return Optional.of(path);
}
}
return Optional.empty();
}
private static Optional<String> getMemberName(ConstraintViolation<?> violation, Invocable invocable) {
final List<Path.Node> propertyPath = Lists.of(violation.getPropertyPath());
final int size = propertyPath.size();
if (size < 2) {
return Optional.empty();
}
final Path.Node parent = propertyPath.get(size - 2);
final Path.Node member = propertyPath.get(size - 1);
switch (parent.getKind()) {
case PARAMETER:
final List<Parameter> parameters = invocable.getParameters();
final Parameter param = parameters.get(parent.as(Path.ParameterNode.class).getParameterIndex());
if (param.getSource().equals(Parameter.Source.BEAN_PARAM)) {
final Field field = FieldUtils.getField(param.getRawType(), member.getName(), true);
return JerseyParameterNameProvider.getParameterNameFromAnnotations(field.getDeclaredAnnotations());
}
break;
case METHOD:
return Optional.of(member.getName());
default:
break;
}
return Optional.empty();
}
private static Optional<String> getMethodReturnValueName(ConstraintViolation<?> violation) {
int returnValueNames = -1;
final StringBuilder result = new StringBuilder("server response");
for (Path.Node node : violation.getPropertyPath()) {
if (node.getKind().equals(ElementKind.RETURN_VALUE)) {
returnValueNames = 0;
} else if (returnValueNames >= 0) {
result.append(returnValueNames++ == 0 ? " " : ".").append(node);
}
}
return returnValueNames >= 0 ? Optional.of(result.toString()) : Optional.empty();
}
private static boolean isValidationMethod(ConstraintViolation<?> v) {
return v.getConstraintDescriptor().getAnnotation() instanceof ValidationMethod;
}
private static boolean isSelfValidating(ConstraintViolation<?> v) {
return v.getConstraintDescriptor().getAnnotation() instanceof SelfValidating;
}
public static <T extends ConstraintViolation<?>> int determineStatus(Set<T> violations, Invocable invocable) {
if (violations.size() > 0) {
final ConstraintViolation<?> violation = violations.iterator().next();
for (Path.Node node : violation.getPropertyPath()) {
switch (node.getKind()) {
case RETURN_VALUE:
return 500;
case PARAMETER:
final int index = node.as(Path.ParameterNode.class).getParameterIndex();
final Parameter parameter = invocable.getParameters().get(index);
return parameter.getSource().equals(Parameter.Source.UNKNOWN) ? 422 : 400;
default:
continue;
}
}
}
return 422;
}
}