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();
+    }
+}

Reply via email to