http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateProperty.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateProperty.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateProperty.java new file mode 100644 index 0000000..a8fbdbc --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateProperty.java @@ -0,0 +1,522 @@ +/* + * 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.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import javax.validation.ConstraintViolation; +import javax.validation.Path; +import javax.validation.metadata.BeanDescriptor; +import javax.validation.metadata.CascadableDescriptor; +import javax.validation.metadata.ContainerDescriptor; +import javax.validation.metadata.ContainerElementTypeDescriptor; +import javax.validation.metadata.ElementDescriptor; +import javax.validation.metadata.PropertyDescriptor; +import javax.validation.valueextraction.ValueExtractor; +import javax.validation.valueextraction.ValueExtractor.ValueReceiver; + +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.CascadableContainerD; +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.metadata.ContainerElementKey; +import org.apache.bval.jsr.util.PathImpl; +import org.apache.bval.jsr.util.PathNavigation; +import org.apache.bval.util.Exceptions; +import org.apache.bval.util.ObjectWrapper; +import org.apache.bval.util.StringUtils; +import org.apache.bval.util.Validate; +import org.apache.bval.util.reflection.TypeUtils; + +public final class ValidateProperty<T> extends ValidationJob<T> { + + interface Strategy<T> { + default PathNavigation.Callback<?> callback(PathImpl.Builder pathBuilder, FindDescriptor findDescriptor) { + return new PathNavigation.CompositeCallbackProcedure(Arrays.asList(pathBuilder, findDescriptor)); + } + + default T getRootBean() { + return null; + } + + ValidateProperty<T>.Frame<?> frame(ValidateProperty<T> job, PathImpl path); + } + + static class ForBeanProperty<T> implements Strategy<T> { + final ApacheFactoryContext validatorContext; + final T rootBean; + final GraphContext rootContext; + final ObjectWrapper<GraphContext> leafContext; + final ObjectWrapper<Object> value; + + ForBeanProperty(ApacheFactoryContext validatorContext, T bean) { + super(); + this.validatorContext = validatorContext; + this.rootBean = bean; + this.rootContext = new GraphContext(validatorContext, PathImpl.create(), bean); + this.leafContext = new ObjectWrapper<>(rootContext); + this.value = new ObjectWrapper<>(bean); + } + + @Override + public PathNavigation.Callback<?> callback(PathImpl.Builder pathBuilder, FindDescriptor findDescriptor) { + return new WalkGraph(validatorContext, pathBuilder, findDescriptor, value, + (p, v) -> leafContext.accept(p.isRootPath() ? rootContext : rootContext.child(p, v))); + } + + @Override + public T getRootBean() { + return rootBean; + } + + public GraphContext baseContext(PathImpl path, ApacheFactoryContext validatorContext) { + return new GraphContext(validatorContext, PathImpl.create(), rootBean).child(path, value.get()); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public ValidationJob<T>.Frame<?> frame(ValidateProperty<T> job, PathImpl path) { + if (job.descriptor instanceof BeanDescriptor) { + return job.new LeafFrame(leafContext.get()); + } + return job.new PropertyFrame(job.new BeanFrame(leafContext.get()), job.descriptor, + leafContext.get().child(path, value.get())); + } + } + + static class ForPropertyValue<T> implements Strategy<T> { + final Object value; + + ForPropertyValue(Object value) { + super(); + this.value = value; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public ValidationJob<T>.Frame<?> frame(ValidateProperty<T> job, PathImpl path) { + final GraphContext context = new GraphContext(job.validatorContext, path, value); + if (job.descriptor instanceof BeanDescriptor) { + return job.new LeafFrame(context); + } + return job.new PropertyFrame(null, job.descriptor, context); + } + } + + private interface Step { + Type type(); + + ElementD<?, ?> element(); + } + + private static class DescriptorWrapper implements Step { + final ElementD<?, ?> wrapped; + + DescriptorWrapper(ElementDescriptor wrapped) { + super(); + this.wrapped = (ElementD<?, ?>) wrapped; + } + + @Override + public Type type() { + return wrapped.getGenericType(); + } + + @Override + public ElementD<?, ?> element() { + return wrapped; + } + } + + private static class TypeWrapper implements Step { + final ApacheFactoryContext validatorContext; + final Type type; + + TypeWrapper(ApacheFactoryContext validatorContext, Type type) { + super(); + this.validatorContext = validatorContext; + this.type = type; + } + + @Override + public Type type() { + return type; + } + + @Override + public ElementD<?, ?> element() { + final Class<?> beanClass = TypeUtils.getRawType(type, null); + return beanClass == null ? null + : (BeanD) validatorContext.getDescriptorManager().getBeanDescriptor(beanClass); + } + } + + private static class FindDescriptor implements PathNavigation.Callback<ElementD<?, ?>> { + private final ApacheFactoryContext validatorContext; + Step current; + + FindDescriptor(ApacheFactoryContext validatorContext, Class<?> beanClass) { + this.validatorContext = validatorContext; + this.current = new DescriptorWrapper(validatorContext.getDescriptorManager().getBeanDescriptor(beanClass)); + } + + @Override + public void handleProperty(String name) { + final ElementDescriptor element = current.element(); + final BeanD bean; + if (element instanceof BeanD) { + bean = (BeanD) element; + } else { + bean = (BeanD) validatorContext.getDescriptorManager().getBeanDescriptor(element.getElementClass()); + } + final PropertyDescriptor property = bean.getProperty(name); + Exceptions.raiseIf(property == null, IllegalArgumentException::new, "Unknown property %s of %s", name, + bean.getElementClass()); + current = new DescriptorWrapper(property); + } + + @Override + public void handleIndexOrKey(String value) { + handleGenericInIterable(); + } + + @Override + public void handleGenericInIterable() { + final ElementDescriptor desc = current.element(); + if (desc instanceof CascadableContainerD<?, ?>) { + final Step containerElement = handleContainerElement((CascadableContainerD<?, ?>) desc); + if (containerElement != null) { + current = containerElement; + return; + } + } + current = handleElementByType(current.type()); + } + + private Step handleContainerElement(CascadableContainerD<?, ?> desc) { + final Set<ContainerElementTypeDescriptor> containerElements = desc.getConstrainedContainerElementTypes(); + if (containerElements.isEmpty()) { + return null; + } + final ContainerElementTypeDescriptor element; + if (containerElements.size() == 1) { + element = containerElements.iterator().next(); + } else { + final Predicate<ContainerElementKey> wellKnown = + k -> k.represents(MAP_VALUE) || k.represents(ITERABLE_ELEMENT); + + final Optional<ContainerElementTypeD> found = + containerElements.stream().map(ContainerElementTypeD.class::cast) + .filter(d -> d.getKey().getAssignableKeys().stream().anyMatch(wellKnown)).findFirst(); + + if (!found.isPresent()) { + return null; + } + element = found.get(); + } + return new DescriptorWrapper(element); + } + + private Step handleElementByType(Type type) { + Type elementType; + + if (TypeUtils.isArrayType(type)) { + elementType = TypeUtils.getArrayComponentType(type); + } else if (TypeUtils.isAssignable(type, Map.class)) { + elementType = + Optional.ofNullable(TypeUtils.getTypeArguments(type, Map.class).get(MAP_VALUE)).orElse(MAP_VALUE); + } else if (TypeUtils.isAssignable(type, Iterable.class)) { + elementType = + Optional.ofNullable(TypeUtils.getTypeArguments(type, Iterable.class).get(ITERABLE_ELEMENT)) + .orElse(ITERABLE_ELEMENT); + } else { + elementType = null; + } + Exceptions.raiseIf(elementType == null, IllegalArgumentException::new, + "Unable to resolve element type of %s", type); + + return new TypeWrapper(validatorContext, elementType); + } + + @Override + public ElementD<?, ?> result() { + return current.element(); + } + } + + private static class WalkGraph extends PathNavigation.CallbackProcedure { + final ApacheFactoryContext validatorContext; + final PathImpl.Builder pathBuilder; + final FindDescriptor findDescriptor; + final ObjectWrapper<Object> value; + final BiConsumer<PathImpl, Object> recordLeaf; + + WalkGraph(ApacheFactoryContext validatorContext, PathImpl.Builder pathBuilder, FindDescriptor findDescriptor, + ObjectWrapper<Object> value, BiConsumer<PathImpl, Object> recordLeaf) { + this.validatorContext = validatorContext; + this.pathBuilder = pathBuilder; + this.findDescriptor = findDescriptor; + this.value = value; + this.recordLeaf = recordLeaf; + } + + @Override + public void handleProperty(String name) { + final PathImpl p = PathImpl.copy(pathBuilder.result()); + pathBuilder.handleProperty(name); + if (value.optional().isPresent()) { + recordLeaf.accept(p, value.get()); + + findDescriptor.handleProperty(name); + + final PropertyD<?> propertyD = + ComposedD.unwrap(findDescriptor.current.element(), PropertyD.class).findFirst().get(); + try { + value.accept(propertyD.getValue(value.get())); + } catch (Exception e) { + Exceptions.raise(IllegalStateException::new, e, "Unable to get value of property %s", + propertyD.getPropertyName()); + } + } + } + + @Override + public void handleIndexOrKey(final String indexOrKey) { + pathBuilder.handleIndexOrKey(indexOrKey); + findDescriptor.handleIndexOrKey(indexOrKey); + if (value.optional().isPresent()) { + ElementDescriptor element = findDescriptor.current.element(); + if (element instanceof ContainerElementTypeD) { + value.accept(handleContainer(value.get(), ((ContainerElementTypeD) element).getKey(), indexOrKey)); + } else { + value.accept(handleBasic(value.get(), indexOrKey)); + + if (element == null && value.optional().isPresent()) { + // no generic info available at some previous index level; fall back to runtime type of value + // and repair structure of findDescriptor: + findDescriptor.current = new TypeWrapper(validatorContext, value.get().getClass()); + element = findDescriptor.current.element(); + } + if (element instanceof BeanDescriptor) { + recordLeaf.accept(PathImpl.copy(pathBuilder.result()), value.get()); + } + } + } + } + + @SuppressWarnings("unchecked") + private Object handleContainer(Object o, ContainerElementKey key, String indexOrKey) { + @SuppressWarnings("rawtypes") + final ValueExtractor valueExtractor = validatorContext.getValueExtractors().find(key); + + final ObjectWrapper<Object> result = new ObjectWrapper<>(); + valueExtractor.extractValues(o, new ValueReceiver() { + + @Override + public void indexedValue(String nodeName, int index, Object object) { + if (Integer.toString(index).equals(indexOrKey)) { + result.accept(object); + } + } + + @Override + public void iterableValue(String nodeName, Object object) { + // ? + result.accept(object); + } + + @Override + public void keyedValue(String nodeName, Object key, Object object) { + if (String.valueOf(key).equals(indexOrKey)) { + result.accept(object); + } + } + + @Override + public void value(String nodeName, Object object) { + // ? + result.accept(object); + } + }); + return result.get(); + } + + private Object handleBasic(Object o, String indexOrKey) { + if (Map.class.isInstance(o)) { + for (Map.Entry<?, ?> e : ((Map<?, ?>) o).entrySet()) { + if (String.valueOf(e.getKey()).equals(indexOrKey)) { + return e.getValue(); + } + } + } else { + try { + final int index = Integer.parseInt(indexOrKey); + Exceptions.raiseIf(index < 0, IllegalArgumentException::new, "Invalid index %d", index); + if (o != null && TypeUtils.isArrayType(o.getClass())) { + if (Array.getLength(o) > index) { + return Array.get(o, index); + } + } else if (List.class.isInstance(o)) { + final List<?> l = (List<?>) o; + if (l.size() > index) { + return l.get(index); + } + } else if (Iterable.class.isInstance(o)) { + int i = -1; + for (Object e : (Iterable<?>) o) { + if (++i == index) { + return e; + } + } + } + } catch (NumberFormatException e) { + } + } + return null; + } + + @Override + public void handleGenericInIterable() { + throw new UnsupportedOperationException("Cannot resolve generic inIterable against actual object graph"); + } + } + + class LeafFrame extends BeanFrame { + + LeafFrame(GraphContext context) { + super(context); + } + + @Override + protected ValidationJob<T>.Frame<?> propertyFrame(PropertyD<?> d, GraphContext context) { + return new PropertyFrame<>(this, d, context); + } + } + + class PropertyFrame<D extends ElementD<?, ?> & CascadableDescriptor & ContainerDescriptor> extends SproutFrame<D> { + + PropertyFrame(ValidationJob<T>.Frame<?> parent, D descriptor, GraphContext context) { + super(parent, descriptor, context); + } + + @Override + void recurse(Class<?> group, Consumer<ConstraintViolation<T>> sink) { + if (cascade) { + super.recurse(group, sink); + } + } + } + + private static final TypeVariable<?> MAP_VALUE = Map.class.getTypeParameters()[1]; + private static final TypeVariable<?> ITERABLE_ELEMENT = Iterable.class.getTypeParameters()[0]; + + private final Strategy<T> strategy; + private final Class<T> rootBeanClass; + private final PathImpl propertyPath; + private final T rootBean; + private ElementD<?, ?> descriptor; + private boolean cascade; + + private ValidateProperty(Strategy<T> strategy, ApacheFactoryContext validatorContext, Class<T> rootBeanClass, + String property, Class<?>[] groups) { + super(validatorContext, groups); + + Exceptions.raiseIf(StringUtils.isBlank(property), IllegalArgumentException::new, + "property cannot be null/empty/blank"); + + this.strategy = strategy; + this.rootBeanClass = Validate.notNull(rootBeanClass, IllegalArgumentException::new, "rootBeanClass"); + + final PathImpl.Builder pathBuilder = new PathImpl.Builder(); + final FindDescriptor findDescriptor = new FindDescriptor(validatorContext, rootBeanClass); + + PathNavigation.navigate(property, strategy.callback(pathBuilder, findDescriptor)); + + this.propertyPath = pathBuilder.result(); + this.descriptor = findDescriptor.result(); + this.rootBean = strategy.getRootBean(); + } + + ValidateProperty(ApacheFactoryContext validatorContext, Class<T> rootBeanClass, String property, Object value, + Class<?>[] groups) { + this(new ForPropertyValue<>(value), validatorContext, rootBeanClass, property, groups); + if (descriptor == null) { + // should only occur when the root class is raw + descriptor = (ElementD<?, ?>) validatorContext.getDescriptorManager() + .getBeanDescriptor(value == null ? Object.class : value.getClass()); + } else { + final Class<?> propertyType = descriptor.getElementClass(); + Exceptions.raiseUnless(TypeUtils.isInstance(value, propertyType), IllegalArgumentException::new, + "%s is not an instance of %s", value, propertyType); + } + } + + @SuppressWarnings("unchecked") + ValidateProperty(ApacheFactoryContext validatorContext, T bean, String property, Class<?>[] groups) + throws Exception { + this(new ForBeanProperty<>(validatorContext, bean), validatorContext, + (Class<T>) Validate.notNull(bean, IllegalArgumentException::new, "bean").getClass(), property, groups); + + Exceptions.raiseIf(descriptor == null, IllegalArgumentException::new, + "Could not resolve property name/path: %s", property); + } + + public ValidateProperty<T> cascade(boolean cascade) { + this.cascade = cascade; + return this; + } + + @Override + protected Frame<?> computeBaseFrame() { + // TODO assign bean as its own property and figure out what to do + + return strategy.frame(this, propertyPath); + } + + @Override + protected Class<T> getRootBeanClass() { + return rootBeanClass; + } + + @Override + ConstraintViolationImpl<T> createViolation(String messageTemplate, ConstraintValidatorContextImpl<T> context, + Path propertyPath) { + final String message = validatorContext.getMessageInterpolator().interpolate(messageTemplate, context); + + return new ConstraintViolationImpl<>(messageTemplate, message, rootBean, context.getFrame().getBean(), + propertyPath, context.getFrame().context.getValue(), context.getConstraintDescriptor(), rootBeanClass, + context.getConstraintDescriptor().unwrap(ConstraintD.class).getDeclaredOn(), null, null); + } +}
http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateReturnValue.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateReturnValue.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateReturnValue.java new file mode 100644 index 0000000..e71e7ae --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateReturnValue.java @@ -0,0 +1,126 @@ +package org.apache.bval.jsr.job; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Type; + +import javax.validation.Path; + +import org.apache.bval.jsr.ApacheFactoryContext; +import org.apache.bval.jsr.ConstraintViolationImpl; +import org.apache.bval.jsr.GraphContext; +import org.apache.bval.jsr.descriptor.ConstraintD; +import org.apache.bval.jsr.descriptor.ExecutableD; +import org.apache.bval.jsr.descriptor.ReturnValueD; +import org.apache.bval.jsr.metadata.Metas; +import org.apache.bval.jsr.util.NodeImpl; +import org.apache.bval.jsr.util.PathImpl; +import org.apache.bval.util.Exceptions; +import org.apache.bval.util.Validate; +import org.apache.bval.util.reflection.TypeUtils; + +public abstract class ValidateReturnValue<E extends Executable, T> extends ValidationJob<T> { + public static class ForMethod<T> extends ValidateReturnValue<Method, T> { + private final T object; + + ForMethod(ApacheFactoryContext validatorContext, T object, Method method, Object returnValue, + Class<?>[] groups) { + super(validatorContext, + new Metas.ForMethod(Validate.notNull(method, IllegalArgumentException::new, "method")), returnValue, + groups); + this.object = Validate.notNull(object, IllegalArgumentException::new, "object"); + } + + @Override + protected T getRootBean() { + return object; + } + + @SuppressWarnings("unchecked") + @Override + protected Class<T> getRootBeanClass() { + return (Class<T>) object.getClass(); + } + + @SuppressWarnings("unchecked") + @Override + protected ExecutableD<Method, ?, ?> describe() { + return (ExecutableD<Method, ?, ?>) validatorContext.getDescriptorManager() + .getBeanDescriptor(object.getClass()) + .getConstraintsForMethod(executable.getName(), executable.getParameterTypes()); + } + } + + public static class ForConstructor<T> extends ValidateReturnValue<Constructor<?>, T> { + + ForConstructor(ApacheFactoryContext validatorContext, Constructor<? extends T> ctor, Object returnValue, + Class<?>[] groups) { + super(validatorContext, + new Metas.ForConstructor(Validate.notNull(ctor, IllegalArgumentException::new, "ctor")), returnValue, + groups); + } + + @Override + protected T getRootBean() { + return null; + } + + @SuppressWarnings("unchecked") + @Override + protected Class<T> getRootBeanClass() { + return (Class<T>) executable.getDeclaringClass(); + } + + @SuppressWarnings("unchecked") + @Override + protected ExecutableD<Constructor<T>, ?, ?> describe() { + return (ExecutableD<Constructor<T>, ?, ?>) validatorContext.getDescriptorManager() + .getBeanDescriptor(executable.getDeclaringClass()) + .getConstraintsForConstructor(executable.getParameterTypes()); + } + } + + protected final E executable; + private final Object returnValue; + + ValidateReturnValue(ApacheFactoryContext validatorContext, Metas<E> meta, Object returnValue, Class<?>[] groups) { + super(validatorContext, groups); + + final Type type = Validate.notNull(meta, "meta").getType(); + Exceptions.raiseUnless(TypeUtils.isInstance(returnValue, type), IllegalArgumentException::new, + "%s is not an instance of %s", returnValue, type); + + this.executable = meta.getHost(); + this.returnValue = returnValue; + } + + @Override + protected Frame<?> computeBaseFrame() { + final PathImpl path = PathImpl.create(); + path.addNode(new NodeImpl.ReturnValueNodeImpl()); + + return new SproutFrame<ReturnValueD<?, ?>>((ReturnValueD<?, ?>) describe().getReturnValueDescriptor(), + new GraphContext(validatorContext, path, returnValue)) { + @Override + Object getBean() { + return getRootBean(); + } + }; + } + + @Override + ConstraintViolationImpl<T> createViolation(String messageTemplate, ConstraintValidatorContextImpl<T> context, + Path propertyPath) { + + final String message = validatorContext.getMessageInterpolator().interpolate(messageTemplate, context); + + return new ConstraintViolationImpl<>(messageTemplate, message, getRootBean(), context.getFrame().getBean(), + propertyPath, context.getFrame().context.getValue(), context.getConstraintDescriptor(), getRootBeanClass(), + context.getConstraintDescriptor().unwrap(ConstraintD.class).getDeclaredOn(), returnValue, null); + } + + protected abstract ExecutableD<?, ?, ?> describe(); + + protected abstract T getRootBean(); +} http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java new file mode 100644 index 0000000..9221184 --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java @@ -0,0 +1,380 @@ +/* + * 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.util.Collections; +import java.util.Comparator; +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.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.CascadableContainerD; +import org.apache.bval.jsr.descriptor.ComposedD; +import org.apache.bval.jsr.descriptor.ConstraintD; +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.util.NodeImpl; +import org.apache.bval.jsr.util.PathImpl; +import org.apache.bval.util.Exceptions; +import org.apache.bval.util.Lazy; +import org.apache.bval.util.Validate; + +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<>(COMPARE_TO_STRING)) + .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 { + constraintValidator.initialize(constraint.getAnnotation()); + valid = constraintValidator.isValid(context.getValue(), constraintValidatorContext); + } + 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 extends Frame<BeanD> { + + 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 -> traversableResolver.isReachable(context.getValue(), + new NodeImpl.PropertyNodeImpl(d.getPropertyName()), getRootBeanClass(), context.getPath(), + 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) { + @SuppressWarnings({ "unchecked", "rawtypes" }) + final Stream<CascadableContainerD<?, ?>> containerElements = + descriptor.getConstrainedContainerElementTypes().stream() + .flatMap(d -> ComposedD.unwrap(d, (Class) CascadableContainerD.class)); + + containerElements.flatMap(d -> d.read(context).map(child -> new SproutFrame<>(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).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 (Map.class.isInstance(value)) { + return ((Map<?, ?>) value).entrySet().stream() + .map(e -> context.child(NodeImpl.atKey(e.getKey()), e.getValue())); + } + if (value.getClass().isArray()) { + return IntStream.range(0, Array.getLength(value)) + .mapToObj(i -> context.child(NodeImpl.atIndex(i), Array.get(value, i))); + } + if (List.class.isInstance(value)) { + final List<?> l = (List<?>) value; + return IntStream.range(0, l.size()).mapToObj(i -> context.child(NodeImpl.atIndex(i), 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(NodeImpl.atIndex(null), o)); + } + return Stream.of(context); + } + + @Override + Object getBean() { + return Optional.ofNullable(parent).map(Frame::getBean).orElse(null); + } + } + + private static final Comparator<Path> COMPARE_TO_STRING = Comparator.comparing(Object::toString); + + 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; + } + + private BeanD getBeanDescriptor(Object bean) { + return (BeanD) validatorContext.getFactory().getDescriptorManager() + .getBeanDescriptor(Validate.notNull(bean, "bean").getClass()); + } + + abstract ConstraintViolationImpl<T> createViolation(String messageTemplate, + ConstraintValidatorContextImpl<T> context, Path propertyPath); + + protected abstract Frame<?> computeBaseFrame(); + + protected abstract Class<T> getRootBeanClass(); +} http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJobFactory.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJobFactory.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJobFactory.java new file mode 100644 index 0000000..2a23192 --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJobFactory.java @@ -0,0 +1,112 @@ +/* + * 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.Constructor; +import java.lang.reflect.Method; + +import javax.validation.ValidationException; +import javax.validation.Validator; +import javax.validation.executable.ExecutableValidator; + +import org.apache.bval.jsr.ApacheFactoryContext; +import org.apache.bval.util.Validate; + +/** + * Creates {@link ValidationJob} instances. + */ +public class ValidationJobFactory { + + private final ApacheFactoryContext validatorContext; + + /** + * Create a new {@link ValidationJobFactory}. + * + * @param validatorContext + */ + public ValidationJobFactory(ApacheFactoryContext validatorContext) { + super(); + this.validatorContext = Validate.notNull(validatorContext, "validatorContext"); + } + + /** + * @see Validator#validate(Object, Class...) + */ + public <T> ValidateBean<T> validateBean(T bean, Class<?>... groups) { + return new ValidateBean<>(validatorContext, bean, groups); + } + + /** + * @see Validator#validateProperty(Object, String, Class...) + */ + public <T> ValidateProperty<T> validateProperty(T bean, String property, Class<?>... groups) { + try { + @SuppressWarnings({ "unchecked", "rawtypes" }) + final ValidateProperty<T> result = new ValidateProperty(validatorContext, bean, property, groups); + return result; + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new ValidationException(e); + } + } + + /** + * @see Validator#validateValue(Class, String, Object, Class...) + */ + public <T> ValidateProperty<T> validateValue(Class<T> rootBeanClass, String property, Object value, + Class<?>... groups) { + @SuppressWarnings({ "unchecked", "rawtypes" }) + final ValidateProperty<T> result = + new ValidateProperty(validatorContext, rootBeanClass, property, value, groups); + return result; + } + + /** + * @see ExecutableValidator#validateParameters(Object, Method, Object[], Class...) + */ + public <T> ValidateParameters.ForMethod<T> validateParameters(T object, Method method, Object[] parameterValues, + Class<?>... groups) { + return new ValidateParameters.ForMethod<T>(validatorContext, object, method, parameterValues, groups); + } + + /** + * @see ExecutableValidator#validateReturnValue(Object, Method, Object, Class...) + */ + public <T> ValidateReturnValue.ForMethod<T> validateReturnValue(T object, Method method, Object returnValue, + Class<?>... groups) { + return new ValidateReturnValue.ForMethod<>(validatorContext, object, method, returnValue, groups); + } + + /** + * @see ExecutableValidator#validateConstructorParameters(Constructor, Object[], Class...) + */ + public <T> ValidateParameters.ForConstructor<T> validateConstructorParameters(Constructor<? extends T> constructor, + Object[] parameterValues, Class<?>... groups) { + return new ValidateParameters.ForConstructor<T>(validatorContext, constructor, parameterValues, groups); + } + + /** + * @see ExecutableValidator#validateConstructorReturnValue(Constructor, Object, Class...) + */ + public <T> ValidateReturnValue.ForConstructor<T> validateConstructorReturnValue( + Constructor<? extends T> constructor, T createdObject, Class<?>... groups) { + return new ValidateReturnValue.ForConstructor<T>(validatorContext, constructor, createdObject, groups); + } +} http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/AnnotationBehavior.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/AnnotationBehavior.java b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/AnnotationBehavior.java new file mode 100644 index 0000000..56ed4f0 --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/AnnotationBehavior.java @@ -0,0 +1,35 @@ +/* + * 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.metadata; + +import org.apache.bval.jsr.metadata.MetadataBuilder; + +/** + * Models the behavior of a {@link MetadataBuilder} with regard to bean validation annotations. + * + * @see DualBuilder + */ +public enum AnnotationBehavior implements AnnotationBehaviorMergeStrategy { + //@formatter:off + INCLUDE, EXCLUDE, ABSTAIN; + //@formatter:on + + @Override + public AnnotationBehavior apply(Iterable<? extends HasAnnotationBehavior> t) { + return this; + } +} http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/AnnotationBehaviorMergeStrategy.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/AnnotationBehaviorMergeStrategy.java b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/AnnotationBehaviorMergeStrategy.java new file mode 100644 index 0000000..bfd16c5 --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/AnnotationBehaviorMergeStrategy.java @@ -0,0 +1,54 @@ +/* + * 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.metadata; + +import java.util.EnumSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.bval.util.Validate; + +@FunctionalInterface +public interface AnnotationBehaviorMergeStrategy + extends Function<Iterable<? extends HasAnnotationBehavior>, AnnotationBehavior> { + + public static AnnotationBehaviorMergeStrategy first() { + return coll -> { + final Iterator<? extends HasAnnotationBehavior> iterator = coll.iterator(); + return iterator.hasNext() ? iterator.next().getAnnotationBehavior() : AnnotationBehavior.ABSTAIN; + }; + } + + public static AnnotationBehaviorMergeStrategy consensus() { + return coll -> { + final Stream.Builder<HasAnnotationBehavior> b = Stream.builder(); + coll.forEach(b); + final Set<AnnotationBehavior> annotationBehaviors = + b.build().map(HasAnnotationBehavior::getAnnotationBehavior).filter(Objects::nonNull) + .filter(Predicate.isEqual(AnnotationBehavior.ABSTAIN).negate()) + .collect(Collectors.toCollection(() -> EnumSet.noneOf(AnnotationBehavior.class))); + Validate.validState(annotationBehaviors.size() <= 1, + "Conflicting annotation inclusion behaviors found among %s", coll); + return annotationBehaviors.isEmpty() ? AnnotationBehavior.ABSTAIN : annotationBehaviors.iterator().next(); + }; + } +} http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/AnnotationDeclaredValidatorMappingProvider.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/AnnotationDeclaredValidatorMappingProvider.java b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/AnnotationDeclaredValidatorMappingProvider.java new file mode 100644 index 0000000..b2126ac --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/AnnotationDeclaredValidatorMappingProvider.java @@ -0,0 +1,42 @@ +/* + * 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.metadata; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.List; + +import javax.validation.Constraint; +import javax.validation.ConstraintValidator; + +import org.apache.bval.util.Validate; + +public class AnnotationDeclaredValidatorMappingProvider extends ValidatorMappingProvider { + public static final AnnotationDeclaredValidatorMappingProvider INSTANCE = + new AnnotationDeclaredValidatorMappingProvider(); + + @Override + protected <A extends Annotation> ValidatorMapping<A> doGetValidatorMapping(Class<A> constraintType) { + Validate.notNull(constraintType); + Validate.isTrue(constraintType.isAnnotationPresent(Constraint.class), + "%s does not represent a validation constraint", constraintType); + @SuppressWarnings({ "unchecked", "rawtypes" }) + final List<Class<? extends ConstraintValidator<A, ?>>> validatorTypes = + (List) Arrays.asList(constraintType.getAnnotation(Constraint.class).validatedBy()); + return new ValidatorMapping<>("@Constraint.validatedBy()", validatorTypes); + } +} http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/ClassLoadingValidatorMappingProvider.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/ClassLoadingValidatorMappingProvider.java b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/ClassLoadingValidatorMappingProvider.java new file mode 100644 index 0000000..e636a8a --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/ClassLoadingValidatorMappingProvider.java @@ -0,0 +1,48 @@ +/* + * 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.metadata; + +import java.util.Objects; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.apache.bval.util.reflection.Reflection; +import org.apache.commons.weaver.privilizer.Privilizing; +import org.apache.commons.weaver.privilizer.Privilizing.CallTo; + +@Privilizing(@CallTo(Reflection.class)) +public abstract class ClassLoadingValidatorMappingProvider extends ValidatorMappingProvider { + + protected final <T> Stream<Class<? extends T>> load(Stream<String> classNames, Class<T> assignableTo, + Consumer<? super ClassNotFoundException> handleException) { + return classNames.map(className -> { + try { + return Reflection.toClass(className, getClassLoader()); + } catch (ClassNotFoundException e) { + handleException.accept(e); + return (Class<?>) null; + } + }).filter(Objects::nonNull).map(c -> (Class<? extends T>) c.asSubclass(assignableTo)); + } + + protected ClassLoader getClassLoader() { + final ClassLoader classloader = Thread.currentThread().getContextClassLoader(); + return classloader == null ? getClass().getClassLoader() : classloader; + } +} http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/CompositeBuilder.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/CompositeBuilder.java b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/CompositeBuilder.java new file mode 100644 index 0000000..52a7407 --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/CompositeBuilder.java @@ -0,0 +1,227 @@ +/* + * 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.metadata; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import javax.validation.metadata.Scope; + +import org.apache.bval.jsr.descriptor.GroupConversion; +import org.apache.bval.jsr.util.ToUnmodifiable; +import org.apache.bval.util.Validate; + +public class CompositeBuilder { + + class Delegator<DELEGATE extends HasAnnotationBehavior> implements HasAnnotationBehavior { + + protected final List<DELEGATE> delegates; + + Delegator(List<DELEGATE> delegates) { + this.delegates = Validate.notNull(delegates, "delegates"); + Validate.isTrue(!delegates.isEmpty(), "no delegates specified"); + Validate.isTrue(delegates.stream().noneMatch(Objects::isNull), "One or more supplied delegates was null"); + } + + @Override + public AnnotationBehavior getAnnotationBehavior() { + return annotationBehaviorStrategy.apply(delegates); + } + + <K, D> Map<K, D> merge(Function<DELEGATE, Map<K, D>> toMap, Function<List<D>, D> merge) { + final List<Map<K, D>> maps = delegates.stream().map(toMap).collect(Collectors.toList()); + + final Function<? super K, ? extends D> valueMapper = k -> { + final List<D> mappedDelegates = + maps.stream().map(m -> m.get(k)).filter(Objects::nonNull).collect(Collectors.toList()); + return mappedDelegates.size() == 1 ? mappedDelegates.get(0) : merge.apply(mappedDelegates); + }; + + return maps.stream().map(Map::keySet).flatMap(Collection::stream).distinct() + .collect(Collectors.toMap(Function.identity(), valueMapper)); + } + } + + private class ForBean extends CompositeBuilder.Delegator<MetadataBuilder.ForBean> + implements MetadataBuilder.ForBean { + + ForBean(List<MetadataBuilder.ForBean> delegates) { + super(delegates); + } + + @Override + public MetadataBuilder.ForClass getClass(Metas<Class<?>> meta) { + return new CompositeBuilder.ForClass( + delegates.stream().map(d -> d.getClass(meta)).collect(Collectors.toList())); + } + + @Override + public Map<String, MetadataBuilder.ForContainer<Field>> getFields(Metas<Class<?>> meta) { + return merge(b -> b.getFields(meta), CompositeBuilder.ForContainer::new); + } + + @Override + public Map<String, MetadataBuilder.ForContainer<Method>> getGetters(Metas<Class<?>> meta) { + return merge(b -> b.getGetters(meta), CompositeBuilder.ForContainer::new); + } + + @Override + public Map<Signature, MetadataBuilder.ForExecutable<Constructor<?>>> getConstructors(Metas<Class<?>> meta) { + return merge(b -> b.getConstructors(meta), CompositeBuilder.ForExecutable::new); + } + + @Override + public Map<Signature, MetadataBuilder.ForExecutable<Method>> getMethods(Metas<Class<?>> meta) { + return merge(b -> b.getMethods(meta), CompositeBuilder.ForExecutable::new); + } + } + + class ForElement<DELEGATE extends MetadataBuilder.ForElement<E>, E extends AnnotatedElement> + extends Delegator<DELEGATE> implements MetadataBuilder.ForElement<E> { + + ForElement(List<DELEGATE> delegates) { + super(delegates); + } + + @Override + public Map<Scope, Annotation[]> getConstraintsByScope(Metas<E> meta) { + return CompositeBuilder.this.getConstraintsByScope(this, meta); + } + + @Override + public final Annotation[] getDeclaredConstraints(Metas<E> meta) { + return delegates.stream().map(d -> d.getDeclaredConstraints(meta)).flatMap(Stream::of) + .toArray(Annotation[]::new); + } + } + + class ForClass extends ForElement<MetadataBuilder.ForClass, Class<?>> implements MetadataBuilder.ForClass { + + ForClass(List<MetadataBuilder.ForClass> delegates) { + super(delegates); + } + + @Override + public List<Class<?>> getGroupSequence(Metas<Class<?>> meta) { + return CompositeBuilder.this.getGroupSequence(this, meta); + } + } + + private class ForContainer<DELEGATE extends MetadataBuilder.ForContainer<E>, E extends AnnotatedElement> + extends CompositeBuilder.ForElement<DELEGATE, E> implements MetadataBuilder.ForContainer<E> { + + ForContainer(List<DELEGATE> delegates) { + super(delegates); + } + + @Override + public final boolean isCascade(Metas<E> meta) { + return delegates.stream().anyMatch(d -> d.isCascade(meta)); + } + + @Override + public final Set<GroupConversion> getGroupConversions(Metas<E> meta) { + return delegates.stream().map(d -> d.getGroupConversions(meta)).flatMap(Collection::stream) + .collect(ToUnmodifiable.set()); + } + + @Override + public final Map<ContainerElementKey, MetadataBuilder.ForContainer<AnnotatedType>> getContainerElementTypes( + Metas<E> meta) { + return merge(b -> b.getContainerElementTypes(meta), CompositeBuilder.ForContainer::new); + } + } + + private class ForExecutable<DELEGATE extends MetadataBuilder.ForExecutable<E>, E extends Executable> + extends Delegator<DELEGATE> implements MetadataBuilder.ForExecutable<E> { + + ForExecutable(List<DELEGATE> delegates) { + super(delegates); + } + + @Override + public MetadataBuilder.ForContainer<E> getReturnValue(Metas<E> meta) { + return new CompositeBuilder.ForContainer<>( + delegates.stream().map(d -> d.getReturnValue(meta)).collect(Collectors.toList())); + } + + @Override + public List<MetadataBuilder.ForContainer<Parameter>> getParameters(Metas<E> meta) { + final List<List<MetadataBuilder.ForContainer<Parameter>>> parameterLists = + delegates.stream().map(d -> d.getParameters(meta)).collect(Collectors.toList()); + + final Set<Integer> parameterCounts = parameterLists.stream().map(List::size).collect(Collectors.toSet()); + Validate.validState(parameterCounts.size() == 1, "Mismatched parameter counts: %s", parameterCounts); + + return IntStream.range(0, parameterCounts.iterator().next().intValue()) + .mapToObj(n -> new CompositeBuilder.ForContainer<>(parameterLists.get(n))) + .collect(ToUnmodifiable.list()); + } + + @Override + public MetadataBuilder.ForElement<E> getCrossParameter(Metas<E> meta) { + return new CompositeBuilder.ForElement<MetadataBuilder.ForElement<E>, E>( + delegates.stream().map(d -> d.getCrossParameter(meta)).collect(Collectors.toList())); + } + } + + public static CompositeBuilder with(AnnotationBehaviorMergeStrategy annotationBehaviorStrategy) { + return new CompositeBuilder(annotationBehaviorStrategy); + } + + private final AnnotationBehaviorMergeStrategy annotationBehaviorStrategy; + + CompositeBuilder(AnnotationBehaviorMergeStrategy annotationBehaviorMergeStrategy) { + super(); + this.annotationBehaviorStrategy = + Validate.notNull(annotationBehaviorMergeStrategy, "annotationBehaviorMergeStrategy"); + } + + public Collector<MetadataBuilder.ForBean, ?, MetadataBuilder.ForBean> compose() { + return Collectors.collectingAndThen(Collectors.toList(), CompositeBuilder.ForBean::new); + } + + protected <E extends AnnotatedElement> Map<Scope, Annotation[]> getConstraintsByScope( + CompositeBuilder.ForElement<? extends MetadataBuilder.ForElement<E>, E> composite, Metas<E> meta) { + return Collections.singletonMap(Scope.LOCAL_ELEMENT, composite.getDeclaredConstraints(meta)); + } + + protected List<Class<?>> getGroupSequence(CompositeBuilder.ForClass composite, Metas<Class<?>> meta) { + final List<List<Class<?>>> groupSequence = + composite.delegates.stream().map(d -> d.getGroupSequence(meta)).collect(Collectors.toList()); + Validate.validState(groupSequence.size() <= 1, + "group sequence returned from multiple composite class metadata builders"); + return groupSequence.isEmpty() ? null : groupSequence.get(0); + } +} http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/CompositeValidatorMappingProvider.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/CompositeValidatorMappingProvider.java b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/CompositeValidatorMappingProvider.java new file mode 100644 index 0000000..9808f89 --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/CompositeValidatorMappingProvider.java @@ -0,0 +1,42 @@ +/* + * 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.metadata; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.apache.bval.util.Validate; + +public class CompositeValidatorMappingProvider extends ValidatorMappingProvider { + + private final List<ValidatorMappingProvider> delegates; + + public CompositeValidatorMappingProvider(List<ValidatorMappingProvider> delegates) { + super(); + this.delegates = Validate.notNull(delegates, "delegates"); + Validate.isTrue(!delegates.isEmpty(), "no delegates specified"); + Validate.isTrue(delegates.stream().noneMatch(Objects::isNull), "One or more supplied delegates was null"); + } + + @Override + protected <A extends Annotation> ValidatorMapping<A> doGetValidatorMapping(Class<A> constraintType) { + return ValidatorMapping.merge(delegates.stream().map(d -> d.doGetValidatorMapping(constraintType)) + .filter(Objects::nonNull).collect(Collectors.toList()), AnnotationBehaviorMergeStrategy.consensus()); + } +} http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/ContainerElementKey.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/ContainerElementKey.java b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/ContainerElementKey.java new file mode 100644 index 0000000..322a4ef --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/ContainerElementKey.java @@ -0,0 +1,175 @@ +/* + * 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.metadata; + +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.TypeVariable; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import javax.validation.valueextraction.ExtractedValue; +import javax.validation.valueextraction.ValueExtractor; +import javax.validation.valueextraction.ValueExtractorDefinitionException; + +import org.apache.bval.util.Exceptions; +import org.apache.bval.util.Lazy; +import org.apache.bval.util.LazyInt; +import org.apache.bval.util.Validate; +import org.apache.bval.util.reflection.TypeUtils; + +public class ContainerElementKey implements Comparable<ContainerElementKey> { + private static Logger log = Logger.getLogger(ContainerElementKey.class.getName()); + + public static ContainerElementKey forValueExtractor(ValueExtractor<?> extractor) { + @SuppressWarnings("rawtypes") + final Class<? extends ValueExtractor> extractorType = extractor.getClass(); + final Lazy<Set<ContainerElementKey>> result = new Lazy<>(HashSet::new); + + Stream.of(extractorType.getAnnotatedInterfaces()).filter(AnnotatedParameterizedType.class::isInstance) + .map(AnnotatedParameterizedType.class::cast) + .filter(apt -> ValueExtractor.class.equals(((ParameterizedType) apt.getType()).getRawType())) + .forEach(decl -> { + final AnnotatedType containerType = decl.getAnnotatedActualTypeArguments()[0]; + + if (containerType.isAnnotationPresent(ExtractedValue.class)) { + Exceptions.raiseIf(void.class.equals(containerType.getAnnotation(ExtractedValue.class).type()), + ValueExtractorDefinitionException::new, "%s does not specify %s type for %s", extractorType, + ExtractedValue.class.getSimpleName(), containerType); + result.get().add(new ContainerElementKey(containerType, null)); + } + Optional.of(containerType).filter(AnnotatedParameterizedType.class::isInstance) + .map(AnnotatedParameterizedType.class::cast) + .map(AnnotatedParameterizedType::getAnnotatedActualTypeArguments).ifPresent(args -> { + IntStream.range(0, args.length).forEach(n -> { + if (args[n].isAnnotationPresent(ExtractedValue.class)) { + if (void.class.equals(args[n].getAnnotation(ExtractedValue.class).type())) { + log.warning(String.format("Ignoring non-default %s type specified for %s by %s", + ExtractedValue.class.getSimpleName(), containerType, extractorType)); + } + result.get().add(new ContainerElementKey(containerType, Integer.valueOf(n))); + } + }); + }); + }); + return result.optional().filter(s -> s.size() == 1) + .orElseThrow(() -> new ValueExtractorDefinitionException(extractorType.getName())).iterator().next(); + } + + private static Integer validTypeArgumentIndex(Integer typeArgumentIndex, Class<?> containerClass) { + if (typeArgumentIndex != null) { + final int i = typeArgumentIndex.intValue(); + Validate.isTrue(i >= 0 && i < containerClass.getTypeParameters().length, + "type argument index %d is invalid for container type %s", typeArgumentIndex, containerClass); + } + return typeArgumentIndex; + } + + private final Integer typeArgumentIndex; + private final Class<?> containerClass; + private final LazyInt hashCode = new LazyInt(() -> Objects.hash(getContainerClass(), getTypeArgumentIndex())); + private final Lazy<String> toString = new Lazy<>(() -> String.format("%s: %s<[%d]>", + ContainerElementKey.class.getSimpleName(), getContainerClass().getName(), getTypeArgumentIndex())); + private final AnnotatedType annotatedType; + + public ContainerElementKey(AnnotatedType containerType, Integer typeArgumentIndex) { + super(); + Validate.notNull(containerType, "containerType"); + this.containerClass = TypeUtils.getRawType(containerType.getType(), null); + this.typeArgumentIndex = validTypeArgumentIndex(typeArgumentIndex, containerClass); + this.annotatedType = typeArgumentIndex == null ? containerType : ((AnnotatedParameterizedType) containerType) + .getAnnotatedActualTypeArguments()[typeArgumentIndex.intValue()]; + } + + public Class<?> getContainerClass() { + return containerClass; + } + + public Integer getTypeArgumentIndex() { + return typeArgumentIndex; + } + + public AnnotatedType getAnnotatedType() { + return annotatedType; + } + + @Override + public boolean equals(Object obj) { + return obj == this || Optional.ofNullable(obj).filter(ContainerElementKey.class::isInstance) + .map(ContainerElementKey.class::cast) + .filter( + cek -> Objects.equals(containerClass, cek.containerClass) && typeArgumentIndex == cek.typeArgumentIndex) + .isPresent(); + } + + @Override + public int hashCode() { + return hashCode.getAsInt(); + } + + @Override + public String toString() { + return toString.get(); + } + + @Override + public int compareTo(ContainerElementKey o) { + return Comparator.comparing(ContainerElementKey::containerClassName) + .thenComparing(Comparator.comparing(ContainerElementKey::getTypeArgumentIndex)).compare(this, o); + } + + public Set<ContainerElementKey> getAssignableKeys() { + final Lazy<Set<ContainerElementKey>> result = new Lazy<>(LinkedHashSet::new); + + if (typeArgumentIndex != null) { + final TypeVariable<?> var = containerClass.getTypeParameters()[typeArgumentIndex.intValue()]; + + Stream + .concat(Stream.of(containerClass.getAnnotatedSuperclass()), + Stream.of(containerClass.getAnnotatedInterfaces())) + .filter(AnnotatedParameterizedType.class::isInstance).map(AnnotatedParameterizedType.class::cast) + .forEach(t -> { + final AnnotatedType[] args = t.getAnnotatedActualTypeArguments(); + + for (int i = 0; i < args.length; i++) { + if (args[i].getType().equals(var)) { + result.get().add(new ContainerElementKey(t, Integer.valueOf(i))); + } + } + }); + } + return result.optional().map(Collections::unmodifiableSet).orElseGet(Collections::emptySet); + } + + public boolean represents(TypeVariable<?> var) { + return Optional.ofNullable(typeArgumentIndex) + .map(index -> getContainerClass().getTypeParameters()[index.intValue()]).filter(var::equals).isPresent(); + } + + private String containerClassName() { + return getContainerClass().getName(); + } +}
