http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/util/NodeContextBuilderImpl.java
----------------------------------------------------------------------
diff --git 
a/bval-jsr/src/main/java/org/apache/bval/jsr/util/NodeContextBuilderImpl.java 
b/bval-jsr/src/main/java/org/apache/bval/jsr/util/NodeContextBuilderImpl.java
index 80e5b8f..f32db9f 100644
--- 
a/bval-jsr/src/main/java/org/apache/bval/jsr/util/NodeContextBuilderImpl.java
+++ 
b/bval-jsr/src/main/java/org/apache/bval/jsr/util/NodeContextBuilderImpl.java
@@ -18,18 +18,19 @@
  */
 package org.apache.bval.jsr.util;
 
-import org.apache.bval.jsr.ConstraintValidatorContextImpl;
+import org.apache.bval.jsr.job.ConstraintValidatorContextImpl;
 
 import javax.validation.ConstraintValidatorContext;
+import 
javax.validation.ConstraintValidatorContext.ConstraintViolationBuilder.ContainerElementNodeBuilderCustomizableContext;
 import 
javax.validation.ConstraintValidatorContext.ConstraintViolationBuilder.NodeContextBuilder;
 
 /**
  * Description: Implementation of {@link NodeContextBuilder}.<br/>
  */
-final class NodeContextBuilderImpl implements 
ConstraintValidatorContext.ConstraintViolationBuilder.NodeContextBuilder {
-    private final ConstraintValidatorContextImpl parent;
-    private final String messageTemplate;
-    private final PathImpl propertyPath;
+public final class NodeContextBuilderImpl implements 
ConstraintValidatorContext.ConstraintViolationBuilder.NodeContextBuilder {
+    private final ConstraintValidatorContextImpl<?> context;
+    private final String template;
+    private final PathImpl path;
     // The name of the last "added" node, it will only be added if it has a 
non-null name
     // The actual incorporation in the path will take place when the 
definition of the current leaf node is complete
     private final NodeImpl node;
@@ -40,10 +41,10 @@ final class NodeContextBuilderImpl implements 
ConstraintValidatorContext.Constra
      * @param template
      * @param path
      */
-    NodeContextBuilderImpl(ConstraintValidatorContextImpl contextImpl, String 
template, PathImpl path, NodeImpl node) {
-        parent = contextImpl;
-        messageTemplate = template;
-        propertyPath = path;
+    NodeContextBuilderImpl(ConstraintValidatorContextImpl<?> contextImpl, 
String template, PathImpl path, NodeImpl node) {
+        this.context = contextImpl;
+        this.template = template;
+        this.path = path;
         this.node = node;
     }
 
@@ -53,8 +54,8 @@ final class NodeContextBuilderImpl implements 
ConstraintValidatorContext.Constra
     @Override
     public 
ConstraintValidatorContext.ConstraintViolationBuilder.NodeBuilderDefinedContext 
atKey(Object key) {
         node.setKey(key);
-        propertyPath.addNode(node);
-        return new NodeBuilderDefinedContextImpl(parent, messageTemplate, 
propertyPath);
+        path.addNode(node);
+        return new NodeBuilderDefinedContextImpl(context, template, path);
     }
 
     /**
@@ -63,8 +64,8 @@ final class NodeContextBuilderImpl implements 
ConstraintValidatorContext.Constra
     @Override
     public 
ConstraintValidatorContext.ConstraintViolationBuilder.NodeBuilderDefinedContext 
atIndex(Integer index) {
         node.setIndex(index);
-        propertyPath.addNode(node);
-        return new NodeBuilderDefinedContextImpl(parent, messageTemplate, 
propertyPath);
+        path.addNode(node);
+        return new NodeBuilderDefinedContextImpl(context, template, path);
     }
 
     /**
@@ -78,14 +79,14 @@ final class NodeContextBuilderImpl implements 
ConstraintValidatorContext.Constra
     @Override
     public 
ConstraintValidatorContext.ConstraintViolationBuilder.NodeBuilderCustomizableContext
 addPropertyNode(
         String name) {
-        propertyPath.addNode(node);
-        return new NodeBuilderCustomizableContextImpl(parent, messageTemplate, 
propertyPath, name);
+        path.addNode(node);
+        return new NodeBuilderCustomizableContextImpl(context, template, path, 
name);
     }
 
     @Override
     public 
ConstraintValidatorContext.ConstraintViolationBuilder.LeafNodeBuilderCustomizableContext
 addBeanNode() {
-        propertyPath.addNode(node);
-        return new LeafNodeBuilderCustomizableContextImpl(parent, 
messageTemplate, propertyPath);
+        path.addNode(node);
+        return new LeafNodeBuilderCustomizableContextImpl(context, template, 
path);
     }
 
     /**
@@ -93,9 +94,18 @@ final class NodeContextBuilderImpl implements 
ConstraintValidatorContext.Constra
      */
     @Override
     public ConstraintValidatorContext addConstraintViolation() {
-        propertyPath.addNode(node);
-        parent.addError(messageTemplate, propertyPath);
-        return parent;
+        path.addNode(node);
+        context.addError(template, path);
+        return context;
     }
 
-}
\ No newline at end of file
+    @Override
+    public ContainerElementNodeBuilderCustomizableContext 
addContainerElementNode(String name, Class<?> containerType,
+        Integer typeArgumentIndex) {
+        final NodeImpl node = new NodeImpl.ContainerElementNodeImpl(name, 
containerType, typeArgumentIndex);
+        path.addNode(node);
+        return new ContainerElementNodeBuilderCustomizableContextImpl(context, 
template, path, name, containerType,
+            typeArgumentIndex);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/util/NodeImpl.java
----------------------------------------------------------------------
diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/util/NodeImpl.java 
b/bval-jsr/src/main/java/org/apache/bval/jsr/util/NodeImpl.java
index 5a08f0e..96f9421 100644
--- a/bval-jsr/src/main/java/org/apache/bval/jsr/util/NodeImpl.java
+++ b/bval-jsr/src/main/java/org/apache/bval/jsr/util/NodeImpl.java
@@ -21,16 +21,25 @@ package org.apache.bval.jsr.util;
 import javax.validation.ElementKind;
 import javax.validation.Path;
 import javax.validation.Path.Node;
+
+import org.apache.bval.util.Exceptions;
+
 import java.io.Serializable;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
 
 public class NodeImpl implements Path.Node, Serializable {
 
     private static final long serialVersionUID = 1L;
     private static final String INDEX_OPEN = "[";
     private static final String INDEX_CLOSE = "]";
-    private List<Class<?>> parameterTypes;
+
+    private static <T extends Path.Node> Optional<T> optional(Class<T> type, 
Object o) {
+        return Optional.ofNullable(o).filter(type::isInstance).map(type::cast);
+    }
 
     /**
      * Append a Node to the specified StringBuilder.
@@ -63,7 +72,7 @@ public class NodeImpl implements Path.Node, Serializable {
      * @return NodeImpl
      */
     public static NodeImpl atIndex(Integer index) {
-        NodeImpl result = new NodeImpl();
+        final NodeImpl result = new NodeImpl();
         result.setIndex(index);
         return result;
     }
@@ -74,7 +83,7 @@ public class NodeImpl implements Path.Node, Serializable {
      * @return NodeImpl
      */
     public static NodeImpl atKey(Object key) {
-        NodeImpl result = new NodeImpl();
+        final NodeImpl result = new NodeImpl();
         result.setKey(key);
         return result;
     }
@@ -85,6 +94,9 @@ public class NodeImpl implements Path.Node, Serializable {
     private int parameterIndex;
     private Object key;
     private ElementKind kind;
+    private List<Class<?>> parameterTypes;
+    private Class<?> containerType;
+    private Integer typeArgumentIndex;
 
     /**
      * Create a new NodeImpl instance.
@@ -99,13 +111,18 @@ public class NodeImpl implements Path.Node, Serializable {
      * @param node
      */
     NodeImpl(Path.Node node) {
-        this.name = node.getName();
+        this(node.getName());
         this.inIterable = node.isInIterable();
         this.index = node.getIndex();
         this.key = node.getKey();
         this.kind = node.getKind();
     }
 
+    <T extends Path.Node> NodeImpl(Path.Node node, Class<T> nodeType, 
Consumer<T> handler) {
+        this(node);
+        
Optional.of(node).filter(nodeType::isInstance).map(nodeType::cast).ifPresent(handler);
+    }
+
     private NodeImpl() {
     }
 
@@ -191,10 +208,8 @@ public class NodeImpl implements Path.Node, Serializable {
 
     @Override
     public <T extends Node> T as(final Class<T> nodeType) {
-        if (nodeType.isInstance(this)) {
-            return nodeType.cast(this);
-        }
-        throw new ClassCastException("Type " + nodeType + " not supported");
+        Exceptions.raiseUnless(nodeType.isInstance(this), 
ClassCastException::new, "Type %s not supported", nodeType);
+        return nodeType.cast(this);
     }
 
     /**
@@ -213,29 +228,13 @@ public class NodeImpl implements Path.Node, Serializable {
         if (this == o) {
             return true;
         }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-
-        NodeImpl node = (NodeImpl) o;
-
-        if (inIterable != node.inIterable) {
-            return false;
-        }
-        if (index != null ? !index.equals(node.index) : node.index != null) {
-            return false;
-        }
-        if (key != null ? !key.equals(node.key) : node.key != null) {
-            return false;
-        }
-        if (name != null ? !name.equals(node.name) : node.name != null) {
-            return false;
-        }
-        if (kind != null ? !kind.equals(node.kind) : node.kind != null) {
+        if (o == null || !getClass().equals(o.getClass())) {
             return false;
         }
+        final NodeImpl node = (NodeImpl) o;
 
-        return true;
+        return inIterable == node.inIterable && Objects.equals(index, 
node.index) && Objects.equals(key, node.key)
+            && Objects.equals(name, node.name) && kind == node.kind;
     }
 
     /**
@@ -243,12 +242,7 @@ public class NodeImpl implements Path.Node, Serializable {
      */
     @Override
     public int hashCode() {
-        int result = name != null ? name.hashCode() : 0;
-        result = 31 * result + (inIterable ? 1 : 0);
-        result = 31 * result + (index != null ? index.hashCode() : 0);
-        result = 31 * result + (key != null ? key.hashCode() : 0);
-        result = 31 * result + (kind != null ? kind.hashCode() : 0);
-        return result;
+        return Objects.hash(name, Boolean.valueOf(inIterable), index, key, 
kind);
     }
 
     public int getParameterIndex() {
@@ -263,12 +257,24 @@ public class NodeImpl implements Path.Node, Serializable {
         this.parameterTypes = parameterTypes;
     }
 
+    public Class<?> getContainerClass() {
+        return containerType;
+    }
+
+    public Integer getTypeArgumentIndex() {
+        return typeArgumentIndex;
+    }
+
+    public void inContainer(Class<?> containerType, Integer typeArgumentIndex) 
{
+        this.containerType = containerType;
+        this.typeArgumentIndex = typeArgumentIndex;
+    }
+
+    @SuppressWarnings("serial")
     public static class ParameterNodeImpl extends NodeImpl implements 
Path.ParameterNode {
         public ParameterNodeImpl(final Node cast) {
             super(cast);
-            if (ParameterNodeImpl.class.isInstance(cast)) {
-                
setParameterIndex(ParameterNodeImpl.class.cast(cast).getParameterIndex());
-            }
+            optional(Path.ParameterNode.class, cast).ifPresent(n -> 
setParameterIndex(n.getParameterIndex()));
         }
 
         public ParameterNodeImpl(final String name, final int idx) {
@@ -282,12 +288,11 @@ public class NodeImpl implements Path.Node, Serializable {
         }
     }
 
+    @SuppressWarnings("serial")
     public static class ConstructorNodeImpl extends NodeImpl implements 
Path.ConstructorNode {
         public ConstructorNodeImpl(final Node cast) {
             super(cast);
-            if (NodeImpl.class.isInstance(cast)) {
-                setParameterTypes(NodeImpl.class.cast(cast).parameterTypes);
-            }
+            optional(Path.ConstructorNode.class, cast).ifPresent(n -> 
setParameterTypes(n.getParameterTypes()));
         }
 
         public ConstructorNodeImpl(final String simpleName, List<Class<?>> 
paramTypes) {
@@ -301,6 +306,7 @@ public class NodeImpl implements Path.Node, Serializable {
         }
     }
 
+    @SuppressWarnings("serial")
     public static class CrossParameterNodeImpl extends NodeImpl implements 
Path.CrossParameterNode {
         public CrossParameterNodeImpl() {
             super("<cross-parameter>");
@@ -316,12 +322,11 @@ public class NodeImpl implements Path.Node, Serializable {
         }
     }
 
+    @SuppressWarnings("serial")
     public static class MethodNodeImpl extends NodeImpl implements 
Path.MethodNode {
         public MethodNodeImpl(final Node cast) {
             super(cast);
-            if (MethodNodeImpl.class.isInstance(cast)) {
-                
setParameterTypes(MethodNodeImpl.class.cast(cast).getParameterTypes());
-            }
+            optional(Path.MethodNode.class, cast).ifPresent(n -> 
setParameterTypes(n.getParameterTypes()));
         }
 
         public MethodNodeImpl(final String name, final List<Class<?>> classes) 
{
@@ -335,6 +340,7 @@ public class NodeImpl implements Path.Node, Serializable {
         }
     }
 
+    @SuppressWarnings("serial")
     public static class ReturnValueNodeImpl extends NodeImpl implements 
Path.ReturnValueNode {
         public ReturnValueNodeImpl(final Node cast) {
             super(cast);
@@ -350,6 +356,7 @@ public class NodeImpl implements Path.Node, Serializable {
         }
     }
 
+    @SuppressWarnings("serial")
     public static class PropertyNodeImpl extends NodeImpl implements 
Path.PropertyNode {
         public PropertyNodeImpl(final String name) {
             super(name);
@@ -357,6 +364,8 @@ public class NodeImpl implements Path.Node, Serializable {
 
         public PropertyNodeImpl(final Node cast) {
             super(cast);
+            optional(Path.PropertyNode.class, cast)
+                .ifPresent(n -> inContainer(n.getContainerClass(), 
n.getTypeArgumentIndex()));
         }
 
         @Override
@@ -365,6 +374,7 @@ public class NodeImpl implements Path.Node, Serializable {
         }
     }
 
+    @SuppressWarnings("serial")
     public static class BeanNodeImpl extends NodeImpl implements Path.BeanNode 
{
         public BeanNodeImpl() {
             // no-op
@@ -372,6 +382,8 @@ public class NodeImpl implements Path.Node, Serializable {
 
         public BeanNodeImpl(final Node cast) {
             super(cast);
+            optional(Path.BeanNode.class, cast)
+                .ifPresent(n -> inContainer(n.getContainerClass(), 
n.getTypeArgumentIndex()));
         }
 
         @Override
@@ -379,4 +391,24 @@ public class NodeImpl implements Path.Node, Serializable {
             return ElementKind.BEAN;
         }
     }
+
+    @SuppressWarnings("serial")
+    public static class ContainerElementNodeImpl extends NodeImpl implements 
Path.ContainerElementNode {
+
+        public ContainerElementNodeImpl(String name, Class<?> containerType, 
Integer typeArgumentIndex) {
+            super(name);
+            inContainer(containerType, typeArgumentIndex);
+        }
+
+        public ContainerElementNodeImpl(final Node cast) {
+            super(cast);
+            optional(Path.ContainerElementNode.class, cast)
+                .ifPresent(n -> inContainer(n.getContainerClass(), 
n.getTypeArgumentIndex()));
+        }
+
+        @Override
+        public ElementKind getKind() {
+            return ElementKind.CONTAINER_ELEMENT;
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/util/PathImpl.java
----------------------------------------------------------------------
diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/util/PathImpl.java 
b/bval-jsr/src/main/java/org/apache/bval/jsr/util/PathImpl.java
index 59fba83..430d257 100644
--- a/bval-jsr/src/main/java/org/apache/bval/jsr/util/PathImpl.java
+++ b/bval-jsr/src/main/java/org/apache/bval/jsr/util/PathImpl.java
@@ -18,16 +18,19 @@
  */
 package org.apache.bval.jsr.util;
 
-import javax.validation.Path;
 import java.io.Serializable;
-import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.Objects;
+
+import javax.validation.Path;
+
+import org.apache.bval.util.Exceptions;
 
 /**
- * Description: object holding the property path as a list of nodes.
- * (Implementation partially based on reference implementation)
- * <br/>
+ * Description: object holding the property path as a list of nodes. 
(Implementation partially based on reference
+ * implementation) <br/>
  * This class is not synchronized.
  * 
  * @version $Rev: 1498347 $ $Date: 2013-07-01 12:06:18 +0200 (lun., 01 juil. 
2013) $
@@ -41,8 +44,8 @@ public class PathImpl implements Path, Serializable {
     /**
      * Builds non-root paths from expressions.
      */
-    private static class PathImplBuilder implements 
PathNavigation.Callback<PathImpl> {
-        PathImpl result = new PathImpl();
+    public static class Builder implements PathNavigation.Callback<PathImpl> {
+        private final PathImpl result = PathImpl.create();
 
         /**
          * {@inheritDoc}
@@ -72,9 +75,6 @@ public class PathImpl implements Path, Serializable {
          */
         @Override
         public PathImpl result() {
-            if (result.nodeList.isEmpty()) {
-                throw new IllegalStateException();
-            }
             return result;
         }
 
@@ -85,11 +85,8 @@ public class PathImpl implements Path, Serializable {
         public void handleGenericInIterable() {
             result.addNode(NodeImpl.atIndex(null));
         }
-
     }
 
-    private final List<NodeImpl> nodeList;
-
     /**
      * Returns a {@code Path} instance representing the path described by the 
given string. To create a root node the
      * empty string should be passed. Note: This signature is to maintain 
pluggability with the RI impl.
@@ -102,7 +99,7 @@ public class PathImpl implements Path, Serializable {
         if (propertyPath == null || propertyPath.isEmpty()) {
             return create();
         }
-        return PathNavigation.navigateAndReturn(propertyPath, new 
PathImplBuilder());
+        return PathNavigation.navigateAndReturn(propertyPath, new Builder());
     }
 
     /**
@@ -127,6 +124,10 @@ public class PathImpl implements Path, Serializable {
         return path == null ? null : new PathImpl(path);
     }
 
+    public static PathImpl of(Path path) {
+        return path instanceof PathImpl ? (PathImpl) path : copy(path);
+    }
+
     private static NodeImpl newNode(final Node cast) {
         if (PropertyNode.class.isInstance(cast)) {
             return new NodeImpl.PropertyNodeImpl(cast);
@@ -140,9 +141,6 @@ public class PathImpl implements Path, Serializable {
         if (ConstructorNode.class.isInstance(cast)) {
             return new NodeImpl.ConstructorNodeImpl(cast);
         }
-        if (ConstructorNode.class.isInstance(cast)) {
-            return new NodeImpl.ConstructorNodeImpl(cast);
-        }
         if (ReturnValueNode.class.isInstance(cast)) {
             return new NodeImpl.ReturnValueNodeImpl(cast);
         }
@@ -152,18 +150,19 @@ public class PathImpl implements Path, Serializable {
         if (CrossParameterNode.class.isInstance(cast)) {
             return new NodeImpl.CrossParameterNodeImpl(cast);
         }
+        if (ContainerElementNode.class.isInstance(cast)) {
+            return new NodeImpl.ContainerElementNodeImpl(cast);
+        }
         return new NodeImpl(cast);
     }
 
+    private final LinkedList<NodeImpl> nodeList = new LinkedList<>();
+
     private PathImpl() {
-        nodeList = new ArrayList<NodeImpl>();
     }
 
-    private PathImpl(Iterable<Node> path) {
-        this();
-        for (final Node node : path) {
-            nodeList.add(newNode(node));
-        }
+    private PathImpl(Iterable<? extends Node> nodes) {
+        nodes.forEach(n -> nodeList.add(newNode(n)));
     }
 
     /**
@@ -176,7 +175,7 @@ public class PathImpl implements Path, Serializable {
         if (nodeList.size() != 1) {
             return false;
         }
-        Path.Node first = nodeList.get(0);
+        final Path.Node first = nodeList.peekFirst();
         return !first.isInIterable() && first.getName() == null;
     }
 
@@ -186,13 +185,10 @@ public class PathImpl implements Path, Serializable {
      * @return PathImpl
      */
     public PathImpl getPathWithoutLeafNode() {
-        List<Node> nodes = new ArrayList<Node>(nodeList);
-        PathImpl path = null;
-        if (nodes.size() > 1) {
-            nodes.remove(nodes.size() - 1);
-            path = new PathImpl(nodes);
+        if (nodeList.size() < 2) {
+            return null;
         }
-        return path;
+        return new PathImpl(nodeList.subList(0, nodeList.size() - 1));
     }
 
     /**
@@ -202,12 +198,11 @@ public class PathImpl implements Path, Serializable {
      *            to add
      */
     public void addNode(Node node) {
-        NodeImpl impl = node instanceof NodeImpl ? (NodeImpl) node : 
newNode(node);
+        final NodeImpl impl = node instanceof NodeImpl ? (NodeImpl) node : 
newNode(node);
         if (isRootPath()) {
-            nodeList.set(0, impl);
-        } else {
-            nodeList.add(impl);
+            nodeList.pop();
         }
+        nodeList.add(impl);
     }
 
     /**
@@ -229,7 +224,6 @@ public class PathImpl implements Path, Serializable {
                 return;
             }
         }
-
         final NodeImpl node;
         if ("<cross-parameter>".equals(name)) {
             node = new NodeImpl.CrossParameterNodeImpl();
@@ -237,7 +231,6 @@ public class PathImpl implements Path, Serializable {
             node = new NodeImpl.PropertyNodeImpl(name);
         }
         addNode(node);
-
     }
 
     /**
@@ -248,11 +241,10 @@ public class PathImpl implements Path, Serializable {
      *             if no nodes are found
      */
     public NodeImpl removeLeafNode() {
-        if (isRootPath() || nodeList.isEmpty()) {
-            throw new IllegalStateException("No nodes in path!");
-        }
+        Exceptions.raiseIf(isRootPath() || nodeList.isEmpty(), 
IllegalStateException::new, "No nodes in path!");
+
         try {
-            return nodeList.remove(nodeList.size() - 1);
+            return nodeList.removeLast();
         } finally {
             if (nodeList.isEmpty()) {
                 nodeList.add(new NodeImpl((String) null));
@@ -269,7 +261,7 @@ public class PathImpl implements Path, Serializable {
         if (nodeList.isEmpty()) {
             return null;
         }
-        return (NodeImpl) nodeList.get(nodeList.size() - 1);
+        return nodeList.peekLast();
     }
 
     /**
@@ -292,14 +284,14 @@ public class PathImpl implements Path, Serializable {
         if (path instanceof PathImpl && ((PathImpl) path).isRootPath()) {
             return true;
         }
-        Iterator<Node> pathIter = path.iterator();
-        Iterator<Node> thisIter = iterator();
+        final Iterator<Node> pathIter = path.iterator();
+        final Iterator<Node> thisIter = iterator();
         while (pathIter.hasNext()) {
-            Node pathNode = pathIter.next();
+            final Node pathNode = pathIter.next();
             if (!thisIter.hasNext()) {
                 return false;
             }
-            Node thisNode = thisIter.next();
+            final Node thisNode = thisIter.next();
             if (pathNode.isInIterable()) {
                 if (!thisNode.isInIterable()) {
                     return false;
@@ -328,7 +320,7 @@ public class PathImpl implements Path, Serializable {
      */
     @Override
     public String toString() {
-        StringBuilder builder = new StringBuilder();
+        final StringBuilder builder = new StringBuilder();
         for (Path.Node node : this) {
             NodeImpl.appendNode(node, builder);
         }
@@ -346,9 +338,7 @@ public class PathImpl implements Path, Serializable {
         if (o == null || !getClass().equals(o.getClass())) {
             return false;
         }
-
-        PathImpl path = (PathImpl) o;
-        return nodeList == path.nodeList || nodeList != null && 
nodeList.equals(path.nodeList);
+        return Objects.equals(nodeList, ((PathImpl) o).nodeList);
     }
 
     /**
@@ -356,7 +346,6 @@ public class PathImpl implements Path, Serializable {
      */
     @Override
     public int hashCode() {
-        return nodeList == null ? 0 : nodeList.hashCode();
+        return Objects.hashCode(nodeList);
     }
-
 }

http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/util/PathNavigation.java
----------------------------------------------------------------------
diff --git 
a/bval-jsr/src/main/java/org/apache/bval/jsr/util/PathNavigation.java 
b/bval-jsr/src/main/java/org/apache/bval/jsr/util/PathNavigation.java
index 36fb919..7ba6bc3 100644
--- a/bval-jsr/src/main/java/org/apache/bval/jsr/util/PathNavigation.java
+++ b/bval-jsr/src/main/java/org/apache/bval/jsr/util/PathNavigation.java
@@ -18,12 +18,17 @@ package org.apache.bval.jsr.util;
 
 import javax.validation.ValidationException;
 
+import org.apache.bval.util.Exceptions;
 import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.commons.lang3.Validate;
 
 import java.io.IOException;
 import java.io.StringWriter;
 import java.io.Writer;
 import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.logging.Logger;
 
 /**
@@ -67,13 +72,13 @@ public class PathNavigation {
     /**
      * Callback "procedure" that always returns null.
      */
-    public static abstract class CallbackProcedure implements Callback<Object> 
{
+    public static abstract class CallbackProcedure implements Callback<Void> {
 
         /**
          * {@inheritDoc}
          */
         @Override
-        public final Object result() {
+        public final Void result() {
             complete();
             return null;
         }
@@ -85,6 +90,34 @@ public class PathNavigation {
         }
     }
 
+    public static class CompositeCallbackProcedure extends CallbackProcedure {
+        private final List<Callback<?>> delegates;
+
+        public CompositeCallbackProcedure(Callback<?>... delegates) {
+            this(new ArrayList<>(Arrays.asList(delegates)));
+        }
+
+        public CompositeCallbackProcedure(List<Callback<?>> delegates) {
+            super();
+            this.delegates = Validate.notNull(delegates);
+        }
+
+        @Override
+        public void handleProperty(String name) {
+            delegates.forEach(d -> d.handleProperty(name));
+        }
+
+        @Override
+        public void handleIndexOrKey(String value) {
+            delegates.forEach(d -> d.handleIndexOrKey(value));
+        }
+
+        @Override
+        public void handleGenericInIterable() {
+            delegates.forEach(Callback::handleGenericInIterable);
+        }
+    }
+
     private static class QuotedStringParser {
         String parseQuotedString(CharSequence path, PathPosition pos) throws 
Exception {
             final int len = path.length();
@@ -118,8 +151,7 @@ public class PathNavigation {
 
         @Override
         protected void handleNextChar(CharSequence path, PathPosition pos, 
Writer target) throws IOException {
-            final int 
-                codePoints = StringEscapeUtils.UNESCAPE_JAVA.translate(path, 
pos.getIndex(), target);
+            final int codePoints = 
StringEscapeUtils.UNESCAPE_JAVA.translate(path, pos.getIndex(), target);
             if (codePoints == 0) {
                 super.handleNextChar(path, pos, target);
             } else {
@@ -128,13 +160,12 @@ public class PathNavigation {
                 }
             }
         }
-
     }
 
     private static final Logger LOG = 
Logger.getLogger(PathNavigation.class.getName());
 
     private static final QuotedStringParser QUOTED_STRING_PARSER;
-    
+
     static {
         QuotedStringParser quotedStringParser;
         try {
@@ -164,10 +195,10 @@ public class PathNavigation {
     public static <T> T navigateAndReturn(CharSequence propertyPath, 
Callback<? extends T> callback) {
         try {
             parse(propertyPath == null ? "" : propertyPath, new 
PathPosition(callback));
-        } catch (ValidationException ex) {
+        } catch (ValidationException | IllegalArgumentException ex) {
             throw ex;
-        } catch (Exception ex) {
-            throw new ValidationException(String.format("invalid property: 
%s", propertyPath), ex);
+        } catch (Exception e) {
+            Exceptions.raise(ValidationException::new, e, "invalid property: 
%s", propertyPath);
         }
         return callback.result();
     }
@@ -190,23 +221,21 @@ public class PathNavigation {
             char c = path.charAt(here);
             switch (c) {
             case ']':
-                throw new IllegalStateException(String.format("Position %s: 
unexpected '%s'", here, c));
+                Exceptions.raise(IllegalStateException::new, "Position %s: 
unexpected '%s'", here, c);
             case '[':
                 handleIndex(path, pos.next());
                 break;
             case '.':
-                if (sep) {
-                    throw new IllegalStateException(
-                        String.format("Position %s: expected property, 
index/key, or end of expression", here));
-                }
+                Exceptions.raiseIf(sep, IllegalStateException::new,
+                    "Position %s: expected property, index/key, or end of 
expression", here);
+
                 sep = true;
                 pos.next();
                 // fall through:
             default:
-                if (!sep) {
-                    throw new IllegalStateException(String.format(
-                        "Position %s: expected property path separator, 
index/key, or end of expression", here));
-                }
+                Exceptions.raiseUnless(sep, IllegalStateException::new,
+                    "Position %s: expected property path separator, index/key, 
or end of expression", here);
+
                 pos.handleProperty(parseProperty(path, pos));
             }
             sep = false;
@@ -214,8 +243,8 @@ public class PathNavigation {
     }
 
     private static String parseProperty(CharSequence path, PathPosition pos) 
throws Exception {
-        int len = path.length();
-        int start = pos.getIndex();
+        final int len = path.length();
+        final int start = pos.getIndex();
         loop: while (pos.getIndex() < len) {
             switch (path.charAt(pos.getIndex())) {
             case '[':
@@ -225,27 +254,28 @@ public class PathNavigation {
             }
             pos.next();
         }
-        if (pos.getIndex() > start) {
-            return path.subSequence(start, pos.getIndex()).toString();
-        }
-        throw new IllegalStateException(String.format("Position %s: expected 
property", start));
+        Exceptions.raiseIf(pos.getIndex() == start, 
IllegalStateException::new, "Position %s: expected property",
+            start);
+
+        return path.subSequence(start, pos.getIndex()).toString();
     }
 
     /**
      * Handles an index/key. If the text contained between [] is surrounded by 
a pair of " or ', it will be treated as a
-     * string which may contain Java escape sequences. This function is only 
available if commons-lang3 is available on the classpath!
+     * string which may contain Java escape sequences. This function is only 
available if commons-lang3 is available on
+     * the classpath!
      * 
      * @param path
      * @param pos
      * @throws Exception
      */
     private static void handleIndex(CharSequence path, PathPosition pos) 
throws Exception {
-        int len = path.length();
-        int start = pos.getIndex();
+        final int len = path.length();
+        final int start = pos.getIndex();
         if (start < len) {
-            char first = path.charAt(pos.getIndex());
+            final char first = path.charAt(pos.getIndex());
             if (first == '"' || first == '\'') {
-                String s = QUOTED_STRING_PARSER.parseQuotedString(path, pos);
+                final String s = QUOTED_STRING_PARSER.parseQuotedString(path, 
pos);
                 if (s != null && path.charAt(pos.getIndex()) == ']') {
                     pos.handleIndexOrKey(s);
                     pos.next();
@@ -254,7 +284,7 @@ public class PathNavigation {
             }
             // no quoted string; match ] greedily
             while (pos.getIndex() < len) {
-                int here = pos.getIndex();
+                final int here = pos.getIndex();
                 try {
                     if (path.charAt(here) == ']') {
                         if (here == start) {
@@ -269,13 +299,13 @@ public class PathNavigation {
                 }
             }
         }
-        throw new IllegalStateException(String.format("Position %s: unparsable 
index", start));
+        Exceptions.raise(IllegalStateException::new, "Position %s: unparsable 
index", start);
     }
 
     /**
      * ParsePosition/Callback
      */
-    private static class PathPosition extends ParsePosition implements 
Callback<Object> {
+    private static class PathPosition extends ParsePosition implements 
Callback<Void> {
         final Callback<?> delegate;
 
         /**
@@ -336,7 +366,7 @@ public class PathNavigation {
          * {@inheritDoc}
          */
         @Override
-        public Object result() {
+        public Void result() {
             return null;
         }
 
@@ -344,9 +374,8 @@ public class PathNavigation {
          * {@inheritDoc}
          */
         /*
-         * Override equals to make findbugs happy;
-         * would simply ignore but doesn't seem to be possible at the inner 
class level
-         * without attaching the filter to the containing class.
+         * Override equals to make findbugs happy; would simply ignore but 
doesn't seem to be possible at the inner
+         * class level without attaching the filter to the containing class.
          */
         @Override
         public boolean equals(Object obj) {
@@ -364,5 +393,4 @@ public class PathNavigation {
             return super.hashCode();
         }
     }
-
 }

http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/util/Proxies.java
----------------------------------------------------------------------
diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/util/Proxies.java 
b/bval-jsr/src/main/java/org/apache/bval/jsr/util/Proxies.java
index 1fa033c..1d0c1ee 100644
--- a/bval-jsr/src/main/java/org/apache/bval/jsr/util/Proxies.java
+++ b/bval-jsr/src/main/java/org/apache/bval/jsr/util/Proxies.java
@@ -26,7 +26,7 @@ public final class Proxies {
     private static final Set<String> KNOWN_PROXY_CLASSNAMES;
 
     static {
-        final Set<String> s = new HashSet<String>();
+        final Set<String> s = new HashSet<>();
         s.add("org.jboss.weld.bean.proxy.ProxyObject");
         KNOWN_PROXY_CLASSNAMES = Collections.unmodifiableSet(s);
     }

http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/util/ToUnmodifiable.java
----------------------------------------------------------------------
diff --git 
a/bval-jsr/src/main/java/org/apache/bval/jsr/util/ToUnmodifiable.java 
b/bval-jsr/src/main/java/org/apache/bval/jsr/util/ToUnmodifiable.java
new file mode 100644
index 0000000..a470e0d
--- /dev/null
+++ b/bval-jsr/src/main/java/org/apache/bval/jsr/util/ToUnmodifiable.java
@@ -0,0 +1,34 @@
+package org.apache.bval.jsr.util;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+
+/**
+ * Utility {@link Collector} definitions.
+ */
+public class ToUnmodifiable {
+
+    public static <T> Collector<T, ?, Set<T>> set(Supplier<Set<T>> set) {
+        return Collectors.collectingAndThen(Collectors.toCollection(set), 
Collections::unmodifiableSet);
+    }
+    
+    public static <T> Collector<T, ?, Set<T>> set() {
+        return 
Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new), 
Collections::unmodifiableSet);
+    }
+
+    public static <T> Collector<T, ?, List<T>> list() {
+        return Collectors.collectingAndThen(Collectors.toList(), 
Collections::unmodifiableList);
+    }
+
+    public static <T, K, U> Collector<T, ?, Map<K, U>> map(Function<? super T, 
? extends K> keyMapper,
+        Function<? super T, ? extends U> valueMapper) {
+        return Collectors.collectingAndThen(Collectors.toMap(keyMapper, 
valueMapper), Collections::unmodifiableMap);
+    }
+}

http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/FxExtractor.java
----------------------------------------------------------------------
diff --git 
a/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/FxExtractor.java 
b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/FxExtractor.java
new file mode 100644
index 0000000..15601b2
--- /dev/null
+++ 
b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/FxExtractor.java
@@ -0,0 +1,96 @@
+/*
+ * 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.valueextraction;
+
+import java.util.Optional;
+import java.util.function.BooleanSupplier;
+
+import javax.validation.valueextraction.ExtractedValue;
+import javax.validation.valueextraction.UnwrapByDefault;
+import javax.validation.valueextraction.ValueExtractor;
+
+import org.apache.bval.util.reflection.Reflection;
+
+import javafx.beans.property.ReadOnlyListProperty;
+import javafx.beans.property.ReadOnlyMapProperty;
+import javafx.beans.property.ReadOnlySetProperty;
+import javafx.beans.value.ObservableValue;
+
+@SuppressWarnings("restriction")
+public abstract class FxExtractor {
+    public static class Activation implements BooleanSupplier {
+
+        @Override
+        public boolean getAsBoolean() {
+            try {
+                return Reflection.toClass("javafx.beans.Observable") != null;
+            } catch (ClassNotFoundException e) {
+                return false;
+            }
+        }
+    }
+
+    @UnwrapByDefault
+    public static class ForObservableValue implements 
ValueExtractor<ObservableValue<@ExtractedValue ?>> {
+
+        @Override
+        public void extractValues(ObservableValue<?> originalValue, 
ValueExtractor.ValueReceiver receiver) {
+            receiver.value(null, originalValue.getValue());
+        }
+    }
+
+    public static class ForListProperty implements 
ValueExtractor<ReadOnlyListProperty<@ExtractedValue ?>> {
+
+        @Override
+        public void extractValues(ReadOnlyListProperty<?> originalValue, 
ValueExtractor.ValueReceiver receiver) {
+            Optional.ofNullable(originalValue.getValue()).ifPresent(l -> {
+                for (int i = 0, sz = l.size(); i < sz; i++) {
+                    receiver.indexedValue("<list element>", i, l.get(i));
+                }
+            });
+        }
+    }
+
+    public static class ForSetProperty implements 
ValueExtractor<ReadOnlySetProperty<@ExtractedValue ?>> {
+
+        @Override
+        public void extractValues(ReadOnlySetProperty<?> originalValue, 
ValueExtractor.ValueReceiver receiver) {
+            Optional.ofNullable(originalValue.getValue())
+                .ifPresent(s -> s.forEach(e -> 
receiver.iterableValue("<iterable element>", e)));
+        }
+    }
+
+    public static class ForMapPropertyKey implements 
ValueExtractor<ReadOnlyMapProperty<@ExtractedValue ?, ?>> {
+
+        @Override
+        public void extractValues(ReadOnlyMapProperty<?, ?> originalValue, 
ValueExtractor.ValueReceiver receiver) {
+            Optional.ofNullable(originalValue.getValue())
+                .ifPresent(m -> m.keySet().forEach(k -> 
receiver.keyedValue("<map key>", k, k)));
+        }
+    }
+
+    public static class ForMapPropertyValue implements 
ValueExtractor<ReadOnlyMapProperty<?, @ExtractedValue ?>> {
+
+        @Override
+        public void extractValues(ReadOnlyMapProperty<?, ?> originalValue, 
ValueExtractor.ValueReceiver receiver) {
+            Optional.ofNullable(originalValue.getValue()).ifPresent(
+                m -> m.entrySet().forEach(e -> receiver.keyedValue("<map 
value>", e.getKey(), e.getValue())));
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/IterableElementExtractor.java
----------------------------------------------------------------------
diff --git 
a/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/IterableElementExtractor.java
 
b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/IterableElementExtractor.java
new file mode 100644
index 0000000..8fd2fc0
--- /dev/null
+++ 
b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/IterableElementExtractor.java
@@ -0,0 +1,30 @@
+/*
+ * 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.valueextraction;
+
+import javax.validation.valueextraction.ExtractedValue;
+import javax.validation.valueextraction.ValueExtractor;
+
+public class IterableElementExtractor implements 
ValueExtractor<Iterable<@ExtractedValue ?>> {
+
+    @Override
+    public void extractValues(Iterable<?> originalValue, 
ValueExtractor.ValueReceiver receiver) {
+        originalValue.forEach(v -> receiver.iterableValue("<iterable 
element>", v));
+    }
+}

http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/ListElementExtractor.java
----------------------------------------------------------------------
diff --git 
a/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/ListElementExtractor.java
 
b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/ListElementExtractor.java
new file mode 100644
index 0000000..bd84726
--- /dev/null
+++ 
b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/ListElementExtractor.java
@@ -0,0 +1,34 @@
+/*
+ * 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.valueextraction;
+
+import java.util.List;
+
+import javax.validation.valueextraction.ExtractedValue;
+import javax.validation.valueextraction.ValueExtractor;
+
+public class ListElementExtractor implements 
ValueExtractor<List<@ExtractedValue ?>> {
+
+    @Override
+    public void extractValues(List<?> originalValue, 
ValueExtractor.ValueReceiver receiver) {
+        for (int i = 0, sz = originalValue.size(); i < sz; i++) {
+            receiver.indexedValue("<list element>", i, originalValue.get(i));
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/MapExtractor.java
----------------------------------------------------------------------
diff --git 
a/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/MapExtractor.java 
b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/MapExtractor.java
new file mode 100644
index 0000000..a6848b8
--- /dev/null
+++ 
b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/MapExtractor.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.valueextraction;
+
+import java.util.Map;
+
+import javax.validation.valueextraction.ExtractedValue;
+import javax.validation.valueextraction.ValueExtractor;
+
+public abstract class MapExtractor {
+    public static class ForKey implements ValueExtractor<Map<@ExtractedValue 
?, ?>> {
+
+        @Override
+        public void extractValues(Map<?, ?> originalValue, 
ValueExtractor.ValueReceiver receiver) {
+            originalValue.keySet().forEach(k -> receiver.keyedValue("<map 
key>", k, k));
+        }
+    }
+
+    public static class ForValue implements ValueExtractor<Map<?, 
@ExtractedValue ?>> {
+
+        @Override
+        public void extractValues(Map<?, ?> originalValue, 
ValueExtractor.ValueReceiver receiver) {
+            originalValue.entrySet().forEach(e -> receiver.keyedValue("<map 
value>", e.getKey(), e.getValue()));
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/OptionalExtractor.java
----------------------------------------------------------------------
diff --git 
a/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/OptionalExtractor.java
 
b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/OptionalExtractor.java
new file mode 100644
index 0000000..5f073cc
--- /dev/null
+++ 
b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/OptionalExtractor.java
@@ -0,0 +1,65 @@
+/*
+ * 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.valueextraction;
+
+import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+
+import javax.validation.valueextraction.ExtractedValue;
+import javax.validation.valueextraction.UnwrapByDefault;
+import javax.validation.valueextraction.ValueExtractor;
+
+public abstract class OptionalExtractor {
+    public static class ForObject implements 
ValueExtractor<Optional<@ExtractedValue ?>> {
+
+        @Override
+        public void extractValues(Optional<?> originalValue, 
ValueExtractor.ValueReceiver receiver) {
+            receiver.value(null, originalValue.orElse(null));
+        }
+    }
+
+    @UnwrapByDefault
+    public static class ForInt implements ValueExtractor<@ExtractedValue(type 
= Integer.class) OptionalInt> {
+
+        @Override
+        public void extractValues(OptionalInt originalValue, 
ValueExtractor.ValueReceiver receiver) {
+            receiver.value(null, originalValue.isPresent() ? 
Integer.valueOf(originalValue.getAsInt()) : null);
+        }
+    }
+
+    @UnwrapByDefault
+    public static class ForLong implements ValueExtractor<@ExtractedValue(type 
= Long.class) OptionalLong> {
+
+        @Override
+        public void extractValues(OptionalLong originalValue, 
ValueExtractor.ValueReceiver receiver) {
+            receiver.value(null, originalValue.isPresent() ? 
Long.valueOf(originalValue.getAsLong()) : null);
+        }
+    }
+
+    @UnwrapByDefault
+    public static class ForDouble implements 
ValueExtractor<@ExtractedValue(type = Double.class) OptionalDouble> {
+
+        @Override
+        public void extractValues(OptionalDouble originalValue, 
ValueExtractor.ValueReceiver receiver) {
+            receiver.value(null, originalValue.isPresent() ? 
Double.valueOf(originalValue.getAsDouble()) : null);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/ValueExtractors.java
----------------------------------------------------------------------
diff --git 
a/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/ValueExtractors.java
 
b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/ValueExtractors.java
new file mode 100644
index 0000000..50feb6c
--- /dev/null
+++ 
b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/ValueExtractors.java
@@ -0,0 +1,181 @@
+/*
+ *  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.valueextraction;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.function.BooleanSupplier;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.validation.valueextraction.ValueExtractor;
+import javax.validation.valueextraction.ValueExtractorDeclarationException;
+import javax.validation.valueextraction.ValueExtractorDefinitionException;
+
+import org.apache.bval.jsr.metadata.ContainerElementKey;
+import org.apache.bval.util.Exceptions;
+import org.apache.bval.util.Lazy;
+import org.apache.bval.util.StringUtils;
+import org.apache.bval.util.Validate;
+import org.apache.bval.util.reflection.Reflection;
+import org.apache.bval.util.reflection.TypeUtils;
+
+public class ValueExtractors {
+    public static final ValueExtractors DEFAULT;
+
+    static {
+        DEFAULT = new ValueExtractors(null) {
+            {
+                final Properties defaultExtractors = new Properties();
+                try {
+                    
defaultExtractors.load(ValueExtractors.class.getResourceAsStream("DefaultExtractors.properties"));
+                } catch (IOException e) {
+                    throw new IllegalStateException(e);
+                }
+                
split(defaultExtractors.getProperty(ValueExtractor.class.getName())).map(cn -> {
+                    try {
+                        @SuppressWarnings("unchecked")
+                        final Class<? extends ValueExtractor<?>> result =
+                            (Class<? extends ValueExtractor<?>>) 
Reflection.toClass(cn)
+                                .asSubclass(ValueExtractor.class);
+                        return result;
+                    } catch (Exception e) {
+                        throw new IllegalStateException(e);
+                    }
+                }).map(ValueExtractors::newInstance).forEach(super::add);
+
+                
split(defaultExtractors.getProperty(ValueExtractor.class.getName() + 
".container"))
+                    
.flatMap(ValueExtractors::loadValueExtractors).forEach(super::add);
+            }
+
+            @Override
+            public void add(ValueExtractor<?> extractor) {
+                throw new UnsupportedOperationException();
+            }
+        };
+    }
+
+    public static Class<?> getExtractedType(ValueExtractor<?> extractor, Type 
target) {
+        final ContainerElementKey key = 
ContainerElementKey.forValueExtractor(extractor);
+        Type result = key.getAnnotatedType().getType();
+        if (result instanceof TypeVariable<?>) {
+            result = TypeUtils.getTypeArguments(target, 
key.getContainerClass()).get(result);
+        }
+        Exceptions.raiseUnless(result instanceof Class<?>, 
ValueExtractorDefinitionException::new,
+            "%s did not resolve to a %s relative to %s", key, 
Class.class.getName(), target);
+        return (Class<?>) result;
+    }
+
+    private static Stream<String> split(String s) {
+        return Stream.of(StringUtils.split(s, ','));
+    }
+
+    private static <T> T newInstance(Class<T> t) {
+        try {
+            return t.newInstance();
+        } catch (InstantiationException | IllegalAccessException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private static Stream<ValueExtractor<?>> loadValueExtractors(String 
containerClassName) {
+        try {
+            final Class<? extends BooleanSupplier> activation =
+                Reflection.toClass(containerClassName + 
"$Activation").asSubclass(BooleanSupplier.class);
+            if (!newInstance(activation).getAsBoolean()) {
+                return Stream.empty();
+            }
+        } catch (ClassNotFoundException e) {
+            // always active
+        }
+        final Class<?> containerClass;
+        try {
+            containerClass = Reflection.toClass(containerClassName);
+        } catch (ClassNotFoundException e) {
+            throw new IllegalStateException(e);
+        }
+        return 
Stream.of(containerClass.getClasses()).filter(ValueExtractor.class::isAssignableFrom).map(c
 -> {
+            @SuppressWarnings("unchecked")
+            final Class<? extends ValueExtractor<?>> result =
+                (Class<? extends ValueExtractor<?>>) 
c.asSubclass(ValueExtractor.class);
+            return result;
+        }).map(ValueExtractors::newInstance);
+    }
+
+    private final Lazy<Map<ContainerElementKey, ValueExtractor<?>>> 
valueExtractors = new Lazy<>(HashMap::new);
+    private final ValueExtractors parent;
+
+    public ValueExtractors() {
+        this(DEFAULT);
+    }
+
+    private ValueExtractors(ValueExtractors parent) {
+        this.parent = parent;
+    }
+
+    public ValueExtractors createChild() {
+        return new ValueExtractors(this);
+    }
+
+    public void add(ValueExtractor<?> extractor) {
+        Validate.notNull(extractor);
+        
valueExtractors.get().compute(ContainerElementKey.forValueExtractor(extractor), 
(k, v) -> {
+            Exceptions.raiseIf(v != null, 
ValueExtractorDeclarationException::new,
+                "Multiple context-level %ss specified for %s", 
ValueExtractor.class.getSimpleName(), k);
+            return extractor;
+        });
+    }
+
+    public Map<ContainerElementKey, ValueExtractor<?>> getValueExtractors() {
+        final Lazy<Map<ContainerElementKey, ValueExtractor<?>>> result = new 
Lazy<>(HashMap::new);
+        populate(result);
+        return result.optional().orElseGet(Collections::emptyMap);
+    }
+
+    public ValueExtractor<?> find(ContainerElementKey key) {
+        final Map<ContainerElementKey, ValueExtractor<?>> allValueExtractors = 
getValueExtractors();
+        if (allValueExtractors.containsKey(key)) {
+            return allValueExtractors.get(key);
+        }
+        // search for assignable ContainerElementKey:
+        Set<ContainerElementKey> assignableKeys = key.getAssignableKeys();
+        while (!assignableKeys.isEmpty()) {
+            final Optional<ValueExtractor<?>> found = 
assignableKeys.stream().filter(allValueExtractors::containsKey)
+                .<ValueExtractor<?>> map(allValueExtractors::get).findFirst();
+            if (found.isPresent()) {
+                return found.get();
+            }
+            assignableKeys = 
assignableKeys.stream().map(ContainerElementKey::getAssignableKeys)
+                .flatMap(Collection::stream).collect(Collectors.toSet());
+        }
+        return null;
+    }
+
+    protected void populate(Supplier<Map<ContainerElementKey, 
ValueExtractor<?>>> target) {
+        Optional.ofNullable(parent).ifPresent(p -> p.populate(target));
+        valueExtractors.optional().ifPresent(m -> target.get().putAll(m));
+    }
+}

http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/xml/AnnotationProxy.java
----------------------------------------------------------------------
diff --git 
a/bval-jsr/src/main/java/org/apache/bval/jsr/xml/AnnotationProxy.java 
b/bval-jsr/src/main/java/org/apache/bval/jsr/xml/AnnotationProxy.java
index 77aed70..96a2b46 100644
--- a/bval-jsr/src/main/java/org/apache/bval/jsr/xml/AnnotationProxy.java
+++ b/bval-jsr/src/main/java/org/apache/bval/jsr/xml/AnnotationProxy.java
@@ -16,20 +16,21 @@
  */
 package org.apache.bval.jsr.xml;
 
-import javax.validation.Valid;
 import java.io.Serializable;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.SortedSet;
-import java.util.TreeSet;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+import javax.validation.Valid;
+
+import org.apache.bval.util.Exceptions;
 
 /**
  * Description: <br/>
- * InvocationHandler implementation of <code>Annotation</code> that pretends it
- * is a "real" source code annotation.
+ * InvocationHandler implementation of <code>Annotation</code> that pretends 
it is a "real" source code annotation.
  * <p/>
  */
 class AnnotationProxy implements Annotation, InvocationHandler, Serializable {
@@ -38,7 +39,7 @@ class AnnotationProxy implements Annotation, 
InvocationHandler, Serializable {
     private static final long serialVersionUID = 1L;
 
     private final Class<? extends Annotation> annotationType;
-    private final Map<String, Object> values;
+    private final SortedMap<String, Object> values;
 
     /**
      * Create a new AnnotationProxy instance.
@@ -46,28 +47,23 @@ class AnnotationProxy implements Annotation, 
InvocationHandler, Serializable {
      * @param <A>
      * @param descriptor
      */
-    public <A extends Annotation> AnnotationProxy(AnnotationProxyBuilder<A> 
descriptor) {
+    <A extends Annotation> AnnotationProxy(AnnotationProxyBuilder<A> 
descriptor) {
         this.annotationType = descriptor.getType();
-        values = getAnnotationValues(descriptor);
-    }
-
-    private <A extends Annotation> Map<String, Object> 
getAnnotationValues(AnnotationProxyBuilder<A> descriptor) {
-        final Map<String, Object> result = new HashMap<String, Object>();
+        values = new TreeMap<>();
         int processedValuesFromDescriptor = 0;
         for (final Method m : descriptor.getMethods()) {
             if (descriptor.contains(m.getName())) {
-                result.put(m.getName(), descriptor.getValue(m.getName()));
+                values.put(m.getName(), descriptor.getValue(m.getName()));
                 processedValuesFromDescriptor++;
-            } else if (m.getDefaultValue() != null) {
-                result.put(m.getName(), m.getDefaultValue());
             } else {
-                throw new IllegalArgumentException("No value provided for " + 
m.getName());
+                Exceptions.raiseIf(m.getDefaultValue() == null, 
IllegalArgumentException::new,
+                    "No value provided for %s", m.getName());
+                values.put(m.getName(), m.getDefaultValue());
             }
         }
-        if (processedValuesFromDescriptor != descriptor.size() && 
!Valid.class.equals(annotationType)) {
-            throw new RuntimeException("Trying to instanciate " + 
annotationType + " with unknown paramters.");
-        }
-        return result;
+        Exceptions.raiseUnless(processedValuesFromDescriptor == 
descriptor.size() || Valid.class.equals(annotationType),
+            IllegalArgumentException::new, "Trying to instantiate %s with 
unknown parameters.",
+            annotationType.getName());
     }
 
     /**
@@ -94,22 +90,7 @@ class AnnotationProxy implements Annotation, 
InvocationHandler, Serializable {
      */
     @Override
     public String toString() {
-        StringBuilder result = new StringBuilder();
-        result.append('@').append(annotationType().getName()).append('(');
-        boolean comma = false;
-        for (String m : getMethodsSorted()) {
-            if (comma)
-                result.append(", ");
-            result.append(m).append('=').append(values.get(m));
-            comma = true;
-        }
-        result.append(")");
-        return result.toString();
-    }
-
-    private SortedSet<String> getMethodsSorted() {
-        SortedSet<String> result = new TreeSet<String>();
-        result.addAll(values.keySet());
-        return result;
+        return values.entrySet().stream().map(e -> String.format("%s=%s", 
e.getKey(), e.getValue()))
+            .collect(Collectors.joining(", ", String.format("@%s(", 
annotationType().getName()), ")"));
     }
 }

http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/xml/AnnotationProxyBuilder.java
----------------------------------------------------------------------
diff --git 
a/bval-jsr/src/main/java/org/apache/bval/jsr/xml/AnnotationProxyBuilder.java 
b/bval-jsr/src/main/java/org/apache/bval/jsr/xml/AnnotationProxyBuilder.java
index dedfabc..02383cb 100644
--- a/bval-jsr/src/main/java/org/apache/bval/jsr/xml/AnnotationProxyBuilder.java
+++ b/bval-jsr/src/main/java/org/apache/bval/jsr/xml/AnnotationProxyBuilder.java
@@ -30,10 +30,12 @@ import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 
 import javax.enterprise.util.AnnotationLiteral;
+import javax.validation.ConstraintTarget;
 import javax.validation.Payload;
 import javax.validation.Valid;
 import javax.validation.ValidationException;
@@ -45,11 +47,21 @@ import javax.validation.groups.ConvertGroup;
  */
 @Privilizing(@CallTo(Reflection.class))
 public final class AnnotationProxyBuilder<A extends Annotation> {
-    private static final ConcurrentMap<Class<?>, Method[]> METHODS_CACHE = new 
ConcurrentHashMap<Class<?>, Method[]>();
+    private static final ConcurrentMap<Class<?>, Method[]> METHODS_CACHE = new 
ConcurrentHashMap<>();
+
+    public static <A> Method[] findMethods(final Class<A> annotationType) {
+        // cache only built-in constraints to avoid memory leaks:
+        // TODO use configurable cache size property?
+        if 
(annotationType.getName().startsWith("javax.validation.constraints.")) {
+            return METHODS_CACHE.computeIfAbsent(annotationType, 
Reflection::getDeclaredMethods);
+        }
+        return Reflection.getDeclaredMethods(annotationType);
+    }
 
     private final Class<A> type;
-    private final Map<String, Object> elements = new HashMap<String, Object>();
+    private final Map<String, Object> elements = new HashMap<>();
     private final Method[] methods;
+    private boolean changed;
 
     /**
      * Create a new AnnotationProxyBuilder instance.
@@ -61,21 +73,6 @@ public final class AnnotationProxyBuilder<A extends 
Annotation> {
         this.methods = findMethods(annotationType);
     }
 
-    public static <A> Method[] findMethods(final Class<A> annotationType) {
-        if 
(annotationType.getName().startsWith("javax.validation.constraints.")) { // 
cache built-in constraints only to avoid mem leaks
-            Method[] mtd = METHODS_CACHE.get(annotationType);
-            if (mtd == null) {
-                final Method[] value = 
Reflection.getDeclaredMethods(annotationType);
-                mtd = METHODS_CACHE.putIfAbsent(annotationType, value);
-                if (mtd == null) {
-                    mtd = value;
-                }
-            }
-            return mtd;
-        }
-        return Reflection.getDeclaredMethods(annotationType);
-    }
-
     /**
      * Create a new AnnotationProxyBuilder instance.
      *
@@ -84,16 +81,15 @@ public final class AnnotationProxyBuilder<A extends 
Annotation> {
      */
     public AnnotationProxyBuilder(Class<A> annotationType, Map<String, Object> 
elements) {
         this(annotationType);
-        for (Map.Entry<String, Object> entry : elements.entrySet()) {
-            this.elements.put(entry.getKey(), entry.getValue());
-        }
+        elements.forEach(this.elements::put);
     }
 
     /**
      * Create a builder initially configured to create an annotation equivalent
-     * to <code>annot</code>.
+     * to {@code annot}.
      * 
-     * @param annot Annotation to be replicated.
+     * @param annot
+     *            Annotation to be replicated.
      */
     @SuppressWarnings("unchecked")
     public AnnotationProxyBuilder(A annot) {
@@ -102,8 +98,7 @@ public final class AnnotationProxyBuilder<A extends 
Annotation> {
         for (Method m : methods) {
             final boolean mustUnset = Reflection.setAccessible(m, true);
             try {
-                Object value = m.invoke(annot);
-                this.elements.put(m.getName(), value);
+                this.elements.put(m.getName(), m.invoke(annot));
             } catch (Exception e) {
                 throw new ValidationException("Cannot access annotation " + 
annot + " element: " + m.getName(), e);
             } finally {
@@ -124,8 +119,22 @@ public final class AnnotationProxyBuilder<A extends 
Annotation> {
      * @param elementName
      * @param value
      */
-    public void putValue(String elementName, Object value) {
-        elements.put(elementName, value);
+    @Deprecated
+    public Object putValue(String elementName, Object value) {
+        return elements.put(elementName, value);
+    }
+
+    /**
+     * Add an element to the configuration.
+     *
+     * @param elementName
+     * @param value
+     * @return whether any change occurred
+     */
+    public boolean setValue(String elementName, Object value) {
+        final boolean result = !Objects.equals(elements.put(elementName, 
value), value);
+        changed |= result;
+        return result;
     }
 
     /**
@@ -171,27 +180,42 @@ public final class AnnotationProxyBuilder<A extends 
Annotation> {
      * Configure the well-known JSR303 "message" element.
      *
      * @param message
+     * @return
      */
-    public void setMessage(String message) {
-        ConstraintAnnotationAttributes.MESSAGE.put(elements, message);
+    public boolean setMessage(String message) {
+        return 
setValue(ConstraintAnnotationAttributes.MESSAGE.getAttributeName(), message);
     }
 
     /**
      * Configure the well-known JSR303 "groups" element.
      *
      * @param groups
+     * @return
      */
-    public void setGroups(Class<?>[] groups) {
-        ConstraintAnnotationAttributes.GROUPS.put(elements, groups);
+    public boolean setGroups(Class<?>[] groups) {
+        return 
setValue(ConstraintAnnotationAttributes.GROUPS.getAttributeName(), groups);
     }
 
     /**
      * Configure the well-known JSR303 "payload" element.
      * 
      * @param payload
+     * @return
+     */
+    public boolean setPayload(Class<? extends Payload>[] payload) {
+        return 
setValue(ConstraintAnnotationAttributes.PAYLOAD.getAttributeName(), payload);
+    }
+
+    /**
+     * Configure the well-known "validationAppliesTo" element.
+     * 
+     * @param constraintTarget
      */
-    public void setPayload(Class<? extends Payload>[] payload) {
-        ConstraintAnnotationAttributes.PAYLOAD.put(elements, payload);
+    public boolean setValidationAppliesTo(ConstraintTarget constraintTarget) {
+        return 
setValue(ConstraintAnnotationAttributes.VALIDATION_APPLIES_TO.getAttributeName(),
 constraintTarget);
+    }
+    public boolean isChanged() {
+        return changed;
     }
 
     /**
@@ -203,16 +227,22 @@ public final class AnnotationProxyBuilder<A extends 
Annotation> {
         final ClassLoader classLoader = Reflection.getClassLoader(getType());
         @SuppressWarnings("unchecked")
         final Class<A> proxyClass = (Class<A>) 
Proxy.getProxyClass(classLoader, getType());
-        final InvocationHandler handler = new AnnotationProxy(this);
-        return doCreateAnnotation(proxyClass, handler);
+        return doCreateAnnotation(proxyClass, new AnnotationProxy(this));
     }
 
     @Privileged
     private A doCreateAnnotation(final Class<A> proxyClass, final 
InvocationHandler handler) {
         try {
-            Constructor<A> constructor = 
proxyClass.getConstructor(InvocationHandler.class);
-            Reflection.setAccessible(constructor, true); // java 8
-            return constructor.newInstance(handler);
+            final Constructor<A> constructor = 
proxyClass.getConstructor(InvocationHandler.class);
+            final boolean mustUnset = Reflection.setAccessible(constructor, 
true); // java
+                                                                               
    // 8
+            try {
+                return constructor.newInstance(handler);
+            } finally {
+                if (mustUnset) {
+                    Reflection.setAccessible(constructor, false);
+                }
+            }
         } catch (Exception e) {
             throw new ValidationException("Unable to create annotation for 
configured constraint", e);
         }

http://git-wip-us.apache.org/repos/asf/bval/blob/3f287a7a/bval-jsr/src/main/java/org/apache/bval/jsr/xml/SchemaManager.java
----------------------------------------------------------------------
diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/xml/SchemaManager.java 
b/bval-jsr/src/main/java/org/apache/bval/jsr/xml/SchemaManager.java
new file mode 100644
index 0000000..a502b7e
--- /dev/null
+++ b/bval-jsr/src/main/java/org/apache/bval/jsr/xml/SchemaManager.java
@@ -0,0 +1,286 @@
+/*
+ * 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.xml;
+
+import java.net.URL;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.xml.XMLConstants;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.UnmarshallerHandler;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.validation.ValidatorHandler;
+
+import org.apache.bval.util.Lazy;
+import org.apache.bval.util.reflection.Reflection;
+import org.w3c.dom.Document;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.AttributesImpl;
+import org.xml.sax.helpers.XMLFilterImpl;
+
+/**
+ * Unmarshals XML converging on latest schema version. Presumes backward 
compatiblity between schemae.
+ */
+public class SchemaManager {
+    public static class Builder {
+        private final SortedMap<Key, Lazy<Schema>> data = new TreeMap<>();
+
+        public Builder add(String version, String ns, String resource) {
+            data.put(new Key(version, ns), new Lazy<>(() -> 
SchemaManager.loadSchema(resource)));
+            return this;
+        }
+
+        public SchemaManager build() {
+            return new SchemaManager(new TreeMap<>(data));
+        }
+    }
+
+    private static class Key implements Comparable<Key> {
+        private static final Comparator<Key> CMP = 
Comparator.comparing(Key::getVersion).thenComparing(Key::getNs);
+
+        final String version;
+        final String ns;
+
+        Key(String version, String ns) {
+            super();
+            this.version = Objects.toString(version, "");
+            this.ns = Objects.toString(ns, "");
+        }
+
+        public String getVersion() {
+            return version;
+        }
+
+        public String getNs() {
+            return ns;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == this) {
+                return true;
+            }
+            return 
Optional.ofNullable(obj).filter(SchemaManager.Key.class::isInstance)
+                .map(SchemaManager.Key.class::cast)
+                .filter(k -> Objects.equals(this.version, k.version) && 
Objects.equals(this.ns, k.ns)).isPresent();
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(version, ns);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("%s:%s", version, ns);
+        }
+
+        @Override
+        public int compareTo(Key o) {
+            return CMP.compare(this, o);
+        }
+    }
+
+    private class DynamicValidatorHandler extends XMLFilterImpl {
+        ContentHandler ch;
+        SAXParseException e;
+
+        @Override
+        public void setContentHandler(ContentHandler handler) {
+            super.setContentHandler(handler);
+            this.ch = handler;
+        }
+
+        @Override
+        public void startElement(String uri, String localName, String qName, 
Attributes atts) throws SAXException {
+            if (getContentHandler() == ch) {
+                final Key schemaKey = new 
Key(Objects.toString(atts.getValue("version"), ""), uri);
+                if (data.containsKey(schemaKey)) {
+                    final Schema schema = data.get(schemaKey).get();
+                    final ValidatorHandler vh = schema.newValidatorHandler();
+                    vh.startDocument();
+                    vh.setContentHandler(ch);
+                    super.setContentHandler(vh);
+                }
+            }
+            try {
+                super.startElement(uri, localName, qName, atts);
+            } catch (SAXParseException e) {
+                this.e = e;
+            }
+        }
+
+        @Override
+        public void error(SAXParseException e) throws SAXException {
+            this.e = e;
+            super.error(e);
+        }
+
+        @Override
+        public void fatalError(SAXParseException e) throws SAXException {
+            this.e = e;
+            super.fatalError(e);
+        }
+
+        void validate() throws SAXParseException {
+            if (e != null) {
+                throw e;
+            }
+        }
+    }
+
+    //@formatter:off
+    private enum XmlAttributeType {
+        CDATA, ID, IDREF, IDREFS, NMTOKEN, NMTOKENS, ENTITY, ENTITIES, 
NOTATION;
+        //@formatter:on
+    }
+
+    private class SchemaRewriter extends XMLFilterImpl {
+        private boolean root = true;
+
+        @Override
+        public void startElement(String uri, String localName, String qName, 
Attributes atts) throws SAXException {
+            final Key schemaKey = new 
Key(Objects.toString(atts.getValue("version"), ""), uri);
+
+            if (!target.equals(schemaKey) && data.containsKey(schemaKey)) {
+                uri = target.ns;
+                if (root) {
+                    atts = rewrite(atts);
+                    root = false;
+                }
+            }
+            super.startElement(uri, localName, qName, atts);
+        }
+
+        private Attributes rewrite(Attributes atts) {
+            final AttributesImpl result;
+            if (atts instanceof AttributesImpl) {
+                result = (AttributesImpl) atts;
+            } else {
+                result = new AttributesImpl(atts);
+            }
+            set(result, "", VERSION_ATTRIBUTE, "", XmlAttributeType.CDATA, 
target.version);
+            return result;
+        }
+
+        private void set(AttributesImpl attrs, String uri, String localName, 
String qName, XmlAttributeType type,
+            String value) {
+            for (int i = 0, sz = attrs.getLength(); i < sz; i++) {
+                if (Objects.equals(qName, attrs.getQName(i))
+                    || Objects.equals(uri, attrs.getURI(i)) && 
Objects.equals(localName, attrs.getLocalName(i))) {
+                    attrs.setAttribute(i, uri, localName, qName, type.name(), 
value);
+                    return;
+                }
+            }
+            attrs.addAttribute(uri, localName, qName, type.name(), value);
+        }
+    }
+
+    public static final String VERSION_ATTRIBUTE = "version";
+
+    private static final Logger log = 
Logger.getLogger(SchemaManager.class.getName());
+    private static final SchemaFactory SCHEMA_FACTORY = 
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+    private static final SAXParserFactory SAX_PARSER_FACTORY;
+
+    static {
+        SAX_PARSER_FACTORY = SAXParserFactory.newInstance();
+        SAX_PARSER_FACTORY.setNamespaceAware(true);
+    }
+
+    static Schema loadSchema(String resource) {
+        final URL schemaUrl = 
Reflection.getClassLoader(XmlUtils.class).getResource(resource);
+        try {
+            return SCHEMA_FACTORY.newSchema(schemaUrl);
+        } catch (SAXException e) {
+            log.log(Level.WARNING, String.format("Unable to parse schema: %s", 
resource), e);
+            return null;
+        }
+    }
+
+    private static Class<?> getObjectFactory(Class<?> type) throws 
ClassNotFoundException {
+        final String className = String.format("%s.%s", 
type.getPackage().getName(), "ObjectFactory");
+        return Reflection.toClass(className, type.getClassLoader());
+    }
+
+    private final Key target;
+    private final SortedMap<Key, Lazy<Schema>> data;
+    private final String description;
+
+    private SchemaManager(SortedMap<Key, Lazy<Schema>> data) {
+        super();
+        this.data = Collections.unmodifiableSortedMap(data);
+        this.target = data.lastKey();
+        this.description = target.ns.substring(target.ns.lastIndexOf('/') + 1);
+    }
+
+    public Optional<Schema> getSchema(String ns, String version) {
+        return Optional.of(new Key(version, ns)).map(data::get).map(Lazy::get);
+    }
+
+    public Optional<Schema> getSchema(Document document) {
+        return Optional.ofNullable(document).map(Document::getDocumentElement)
+            .map(e -> getSchema(e.getAttribute(XMLConstants.XMLNS_ATTRIBUTE), 
e.getAttribute(VERSION_ATTRIBUTE))).get();
+    }
+
+    public <E extends Exception> Schema requireSchema(Document document, 
Function<String, E> exc) throws E {
+        return getSchema(document).orElseThrow(() -> 
Objects.requireNonNull(exc, "exc")
+            .apply(String.format("Unknown %s schema", 
Objects.toString(description, ""))));
+    }
+
+    public <T> T unmarshal(InputSource input, Class<T> type) throws Exception {
+        final XMLReader xmlReader = 
SAX_PARSER_FACTORY.newSAXParser().getXMLReader();
+
+        // validate specified schema:
+        final DynamicValidatorHandler schemaValidator = new 
DynamicValidatorHandler();
+        xmlReader.setContentHandler(schemaValidator);
+
+        // rewrite to latest schema, if required:
+        final SchemaRewriter schemaRewriter = new SchemaRewriter();
+        schemaValidator.setContentHandler(schemaRewriter);
+
+        JAXBContext jc = JAXBContext.newInstance(getObjectFactory(type));
+        // unmarshal:
+        final UnmarshallerHandler unmarshallerHandler = 
jc.createUnmarshaller().getUnmarshallerHandler();
+        schemaRewriter.setContentHandler(unmarshallerHandler);
+
+        xmlReader.parse(input);
+
+        schemaValidator.validate();
+
+        @SuppressWarnings("unchecked")
+        final JAXBElement<T> result = (JAXBElement<T>) 
unmarshallerHandler.getResult();
+        return result.getValue();
+    }
+}

Reply via email to