blob: f2eaa262a18d5aa520579dc6f59eeca419832c9c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.bval.jsr.job;
import java.lang.reflect.Array;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintViolation;
import javax.validation.ElementKind;
import javax.validation.MessageInterpolator;
import javax.validation.Path;
import javax.validation.TraversableResolver;
import javax.validation.UnexpectedTypeException;
import javax.validation.ValidationException;
import javax.validation.groups.Default;
import javax.validation.metadata.CascadableDescriptor;
import javax.validation.metadata.ContainerDescriptor;
import javax.validation.metadata.ElementDescriptor.ConstraintFinder;
import javax.validation.metadata.PropertyDescriptor;
import org.apache.bval.jsr.ApacheFactoryContext;
import org.apache.bval.jsr.ConstraintViolationImpl;
import org.apache.bval.jsr.GraphContext;
import org.apache.bval.jsr.descriptor.BeanD;
import org.apache.bval.jsr.descriptor.ComposedD;
import org.apache.bval.jsr.descriptor.ConstraintD;
import org.apache.bval.jsr.descriptor.ContainerElementTypeD;
import org.apache.bval.jsr.descriptor.ElementD;
import org.apache.bval.jsr.descriptor.PropertyD;
import org.apache.bval.jsr.groups.Group;
import org.apache.bval.jsr.groups.Groups;
import org.apache.bval.jsr.metadata.ContainerElementKey;
import org.apache.bval.jsr.util.NodeImpl;
import org.apache.bval.jsr.util.PathImpl;
import org.apache.bval.jsr.util.Proxies;
import org.apache.bval.util.Exceptions;
import org.apache.bval.util.Lazy;
import org.apache.bval.util.ObjectUtils;
import org.apache.bval.util.Validate;
import org.apache.bval.util.reflection.TypeUtils;
public abstract class ValidationJob<T> {
public abstract class Frame<D extends ElementD<?, ?>> {
protected final Frame<?> parent;
protected final D descriptor;
protected final GraphContext context;
protected Frame(Frame<?> parent, D descriptor, GraphContext context) {
super();
this.parent = parent;
this.descriptor = Validate.notNull(descriptor, "descriptor");
this.context = Validate.notNull(context, "context");
}
final ValidationJob<T> getJob() {
return ValidationJob.this;
}
final void process(Class<?> group, Consumer<ConstraintViolation<T>> sink) {
Validate.notNull(sink, "sink");
each(expand(group), this::validateDescriptorConstraints, sink);
recurse(group, sink);
}
abstract void recurse(Class<?> group, Consumer<ConstraintViolation<T>> sink);
abstract Object getBean();
protected void validateDescriptorConstraints(Class<?> group, Consumer<ConstraintViolation<T>> sink) {
constraintsFrom(descriptor.findConstraints().unorderedAndMatchingGroups(group))
.forEach(c -> validate(c, sink));
}
@SuppressWarnings("unchecked")
private Stream<ConstraintD<?>> constraintsFrom(ConstraintFinder finder) {
// our ConstraintFinder implementation is a Stream supplier; reference without exposing it beyond its
// package:
if (finder instanceof Supplier<?>) {
return (Stream<ConstraintD<?>>) ((Supplier<?>) finder).get();
}
return finder.getConstraintDescriptors().stream().map(ConstraintD.class::cast);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private boolean validate(ConstraintD<?> constraint, Consumer<ConstraintViolation<T>> sink) {
if (!validatedPathsByConstraint
.computeIfAbsent(constraint, k -> new ConcurrentSkipListSet<>(PathImpl.PATH_COMPARATOR))
.add(context.getPath())) {
// seen, ignore:
return true;
}
final ConstraintValidatorContextImpl<T> constraintValidatorContext =
new ConstraintValidatorContextImpl<>(this, constraint);
final ConstraintValidator constraintValidator = getConstraintValidator(constraint);
final boolean valid;
if (constraintValidator == null) {
// null validator without exception implies composition:
valid = true;
} else {
try {
constraintValidator.initialize(constraint.getAnnotation());
valid = constraintValidator.isValid(context.getValue(), constraintValidatorContext);
} catch (ValidationException e) {
throw e;
} catch (Exception e) {
throw new ValidationException(e);
}
}
if (!valid) {
constraintValidatorContext.getRequiredViolations().forEach(sink);
}
if (valid || !constraint.isReportAsSingleViolation()) {
final boolean compositionValid = validateComposed(constraint, sink);
if (!compositionValid) {
if (valid && constraint.isReportAsSingleViolation()) {
constraintValidatorContext.getRequiredViolations().forEach(sink);
}
return false;
}
}
return valid;
}
private boolean validateComposed(ConstraintD<?> constraint, Consumer<ConstraintViolation<T>> sink) {
if (constraint.getComposingConstraints().isEmpty()) {
return true;
}
final Consumer<ConstraintViolation<T>> effectiveSink = constraint.isReportAsSingleViolation() ? cv -> {
} : sink;
// collect validation results to set of Boolean, ensuring all are evaluated:
final Set<Boolean> results = constraint.getComposingConstraints().stream().map(ConstraintD.class::cast)
.map(c -> validate(c, effectiveSink)).collect(Collectors.toSet());
return Collections.singleton(Boolean.TRUE).equals(results);
}
@SuppressWarnings({ "rawtypes" })
private ConstraintValidator getConstraintValidator(ConstraintD<?> constraint) {
final Class<? extends ConstraintValidator> constraintValidatorClass =
constraint.getConstraintValidatorClass();
if (constraintValidatorClass == null) {
Exceptions.raiseIf(constraint.getComposingConstraints().isEmpty(), UnexpectedTypeException::new,
"No %s type located for non-composed constraint %s", ConstraintValidator.class.getSimpleName(),
constraint);
return null;
}
ConstraintValidator constraintValidator = null;
Exception cause = null;
try {
constraintValidator =
validatorContext.getConstraintValidatorFactory().getInstance(constraintValidatorClass);
} catch (Exception e) {
cause = e;
}
Exceptions.raiseIf(constraintValidator == null, ValidationException::new, cause,
"Unable to get %s instance from %s", constraintValidatorClass.getName(),
validatorContext.getConstraintValidatorFactory());
return constraintValidator;
}
protected Stream<Class<?>> expand(Class<?> group) {
if (Default.class.equals(group)) {
final List<Class<?>> groupSequence = descriptor.getGroupSequence();
if (groupSequence != null) {
return groupSequence.stream();
}
}
return Stream.of(group);
}
}
public class BeanFrame<B> extends Frame<BeanD<B>> {
BeanFrame(GraphContext context) {
this(null, context);
}
BeanFrame(Frame<?> parent, GraphContext context) {
super(parent, getBeanDescriptor(context.getValue()), context);
}
@Override
void recurse(Class<?> group, Consumer<ConstraintViolation<T>> sink) {
// bean frame has to do some convoluted things to properly handle groups and recursion; skipping
// frame#process() on properties:
final List<Frame<?>> propertyFrames = propertyFrames();
each(expand(group), (g, s) -> propertyFrames.forEach(f -> f.validateDescriptorConstraints(g, s)), sink);
propertyFrames.forEach(f -> f.recurse(group, sink));
}
protected Frame<?> propertyFrame(PropertyD<?> d, GraphContext context) {
return new SproutFrame<>(this, d, context);
}
@Override
Object getBean() {
return context.getValue();
}
private List<Frame<?>> propertyFrames() {
final Stream<PropertyD<?>> properties = descriptor.getConstrainedProperties().stream()
.flatMap(d -> ComposedD.unwrap(d, PropertyD.class)).map(d -> (PropertyD<?>) d);
final TraversableResolver traversableResolver = validatorContext.getTraversableResolver();
final Stream<PropertyD<?>> reachableProperties =
properties.filter(d -> {
final PathImpl p = PathImpl.copy(context.getPath());
p.addProperty(d.getPropertyName());
return traversableResolver.isReachable(context.getValue(), p.removeLeafNode(), getRootBeanClass(),
p, d.getElementType());
});
return reachableProperties.flatMap(
d -> d.read(context).filter(context -> !context.isRecursive()).map(child -> propertyFrame(d, child)))
.collect(Collectors.toList());
}
}
public class SproutFrame<D extends ElementD<?, ?> & CascadableDescriptor & ContainerDescriptor> extends Frame<D> {
public SproutFrame(D descriptor, GraphContext context) {
this(null, descriptor, context);
}
public SproutFrame(Frame<?> parent, D descriptor, GraphContext context) {
super(parent, descriptor, context);
}
@Override
void recurse(Class<?> group, Consumer<ConstraintViolation<T>> sink) {
final Groups convertedGroups =
validatorContext.getGroupsComputer().computeCascadingGroups(descriptor.getGroupConversions(), group);
convertedGroups.getGroups().stream().map(Group::getGroup).forEach(g -> recurseSingleExpandedGroup(g, sink));
sequences: for (List<Group> seq : convertedGroups.getSequences()) {
final boolean proceed = each(seq.stream().map(Group::getGroup), this::recurseSingleExpandedGroup, sink);
if (!proceed) {
break sequences;
}
}
}
protected void recurseSingleExpandedGroup(Class<?> group, Consumer<ConstraintViolation<T>> sink) {
@SuppressWarnings({ "unchecked", "rawtypes" })
final Stream<ContainerElementTypeD> containerElements = descriptor.getConstrainedContainerElementTypes()
.stream().flatMap(d -> ComposedD.unwrap(d, (Class) ContainerElementTypeD.class));
containerElements.flatMap(d -> d.read(context).map(child -> new ContainerElementFrame(this, d, child)))
.forEach(f -> f.process(group, sink));
if (!descriptor.isCascaded()) {
return;
}
if (descriptor instanceof PropertyDescriptor) {
final TraversableResolver traversableResolver = validatorContext.getTraversableResolver();
final PathImpl pathToTraversableObject = PathImpl.copy(context.getPath());
final NodeImpl traversableProperty = pathToTraversableObject.removeLeafNode();
if (!traversableResolver.isCascadable(context.getValue(), traversableProperty, getRootBeanClass(),
pathToTraversableObject, ((PropertyD<?>) descriptor).getElementType())) {
return;
}
}
multiplex().filter(context -> context.getValue() != null && !context.isRecursive())
.map(context -> new BeanFrame<>(this, context)).forEach(b -> b.process(group, sink));
}
private Stream<GraphContext> multiplex() {
final Object value = context.getValue();
if (value == null) {
return Stream.empty();
}
if (value.getClass().isArray()) {
// inconsistent: use Object[] here but specific type for Iterable? RI compatibility
final Class<?> arrayType = value instanceof Object[] ? Object[].class : value.getClass();
return IntStream.range(0, Array.getLength(value)).mapToObj(i -> context
.child(NodeImpl.atIndex(i).inContainer(arrayType, null), Array.get(value, i)));
}
if (Map.class.isInstance(value)) {
return ((Map<?, ?>) value).entrySet().stream()
.map(e -> context.child(
setContainerInformation(NodeImpl.atKey(e.getKey()), MAP_VALUE, descriptor.getElementClass()),
e.getValue()));
}
if (List.class.isInstance(value)) {
final List<?> l = (List<?>) value;
return IntStream.range(0, l.size())
.mapToObj(i -> context.child(
setContainerInformation(NodeImpl.atIndex(i), ITERABLE_ELEMENT, descriptor.getElementClass()),
l.get(i)));
}
if (Iterable.class.isInstance(value)) {
final Stream.Builder<Object> b = Stream.builder();
((Iterable<?>) value).forEach(b);
return b.build()
.map(o -> context.child(
setContainerInformation(NodeImpl.atIndex(null), ITERABLE_ELEMENT, descriptor.getElementClass()),
o));
}
return Stream.of(context);
}
// RI apparently wants to use e.g. Set for Iterable containers, so use declared type + assigned type
// variable if present. not sure I agree, FWIW
private NodeImpl setContainerInformation(NodeImpl node, TypeVariable<?> originalTypeVariable,
Class<?> containerType) {
final TypeVariable<?> tv;
if (containerType.equals(originalTypeVariable.getGenericDeclaration())) {
tv = originalTypeVariable;
} else {
final Type assignedType =
TypeUtils.getTypeArguments(containerType, (Class<?>) originalTypeVariable.getGenericDeclaration())
.get(originalTypeVariable);
tv = assignedType instanceof TypeVariable<?> ? (TypeVariable<?>) assignedType : null;
}
final int i = tv == null ? -1 : ObjectUtils.indexOf(containerType.getTypeParameters(), tv);
return node.inContainer(containerType, i < 0 ? null : Integer.valueOf(i));
}
@Override
Object getBean() {
return Optional.ofNullable(parent).map(Frame::getBean).orElse(null);
}
}
private class ContainerElementFrame extends SproutFrame<ContainerElementTypeD> {
ContainerElementFrame(ValidationJob<T>.Frame<?> parent, ContainerElementTypeD descriptor,
GraphContext context) {
super(parent, descriptor, context);
}
@Override
protected void recurseSingleExpandedGroup(Class<?> group, Consumer<ConstraintViolation<T>> sink) {
final PathImpl path = context.getPath();
final NodeImpl leafNode = path.getLeafNode();
final NodeImpl newLeaf;
if (leafNode.getKind() == ElementKind.CONTAINER_ELEMENT) {
// recurse using elided path:
path.removeLeafNode();
newLeaf = new NodeImpl.PropertyNodeImpl(leafNode);
newLeaf.setName(null);
} else {
final ContainerElementKey key = descriptor.getKey();
newLeaf = new NodeImpl.PropertyNodeImpl((String) null).inContainer(key.getContainerClass(),
key.getTypeArgumentIndex());
}
path.addNode(newLeaf);
new SproutFrame<>(parent, descriptor, context.getParent().child(path, context.getValue())).recurse(group,
sink);
}
}
protected static final TypeVariable<?> MAP_VALUE = Map.class.getTypeParameters()[1];
protected static final TypeVariable<?> ITERABLE_ELEMENT = Iterable.class.getTypeParameters()[0];
protected final ApacheFactoryContext validatorContext;
private final Groups groups;
private final Lazy<Set<ConstraintViolation<T>>> results = new Lazy<>(LinkedHashSet::new);
private ConcurrentMap<ConstraintD<?>, Set<Path>> validatedPathsByConstraint;
ValidationJob(ApacheFactoryContext validatorContext, Class<?>[] groups) {
super();
this.validatorContext = Validate.notNull(validatorContext, "validatorContext");
this.groups = validatorContext.getGroupsComputer().computeGroups(groups);
}
public final Set<ConstraintViolation<T>> getResults() {
if (results.optional().isPresent()) {
return results.get();
}
final Frame<?> baseFrame = computeBaseFrame();
Validate.validState(baseFrame != null, "%s computed null baseFrame", getClass().getName());
final Consumer<ConstraintViolation<T>> sink = results.consumer(Set::add);
validatedPathsByConstraint = new ConcurrentHashMap<>();
try {
groups.getGroups().stream().map(Group::getGroup).forEach(g -> baseFrame.process(g, sink));
sequences: for (List<Group> seq : groups.getSequences()) {
final boolean proceed = each(seq.stream().map(Group::getGroup), baseFrame::process, sink);
if (!proceed) {
break sequences;
}
}
} finally {
validatedPathsByConstraint = null;
}
return results.optional().map(Collections::unmodifiableSet).orElse(Collections.emptySet());
}
private boolean each(Stream<Class<?>> groupSequence, BiConsumer<Class<?>, Consumer<ConstraintViolation<T>>> closure,
Consumer<ConstraintViolation<T>> sink) {
final Lazy<Set<ConstraintViolation<T>>> sequenceViolations = new Lazy<>(LinkedHashSet::new);
for (Class<?> g : (Iterable<Class<?>>) () -> groupSequence.iterator()) {
closure.accept(g, sequenceViolations.consumer(Set::add));
if (sequenceViolations.optional().isPresent()) {
sequenceViolations.get().forEach(sink);
return false;
}
}
return true;
}
@SuppressWarnings("unchecked")
private <O> BeanD<O> getBeanDescriptor(Object bean) {
final Class<? extends Object> t = Proxies.classFor(Validate.notNull(bean, "bean").getClass());
return (BeanD<O>) validatorContext.getDescriptorManager().getBeanDescriptor(t);
}
final ConstraintViolationImpl<T> createViolation(String messageTemplate, ConstraintValidatorContextImpl<T> context,
Path propertyPath) {
return createViolation(messageTemplate, interpolate(messageTemplate, context), context, propertyPath);
}
abstract ConstraintViolationImpl<T> createViolation(String messageTemplate, String message,
ConstraintValidatorContextImpl<T> context, Path propertyPath);
protected abstract Frame<?> computeBaseFrame();
protected abstract Class<T> getRootBeanClass();
private final String interpolate(String messageTemplate, MessageInterpolator.Context context) {
try {
return validatorContext.getMessageInterpolator().interpolate(messageTemplate, context);
} catch (ValidationException e) {
throw e;
} catch (Exception e) {
throw new ValidationException(e);
}
}
}