http://git-wip-us.apache.org/repos/asf/bval/blob/a43c0b0c/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..a6b185e 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 @@ -20,9 +20,10 @@ 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; /** * Description: object holding the property path as a list of nodes. @@ -88,8 +89,6 @@ public class PathImpl implements Path, Serializable { } - 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. @@ -127,6 +126,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 +143,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,16 +152,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) { + private PathImpl(Iterable<? extends Node> nodes) { + for (final Path.Node node : nodes) { nodeList.add(newNode(node)); } } @@ -176,7 +179,7 @@ public class PathImpl implements Path, Serializable { if (nodeList.size() != 1) { return false; } - Path.Node first = nodeList.get(0); + Path.Node first = nodeList.peekFirst(); return !first.isInIterable() && first.getName() == null; } @@ -186,13 +189,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)); } /** @@ -204,10 +204,9 @@ public class PathImpl implements Path, Serializable { public void addNode(Node node) { NodeImpl impl = node instanceof NodeImpl ? (NodeImpl) node : newNode(node); if (isRootPath()) { - nodeList.set(0, impl); - } else { - nodeList.add(impl); + nodeList.pop(); } + nodeList.add(impl); } /** @@ -252,7 +251,7 @@ public class PathImpl implements Path, Serializable { throw new IllegalStateException("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 +268,7 @@ public class PathImpl implements Path, Serializable { if (nodeList.isEmpty()) { return null; } - return (NodeImpl) nodeList.get(nodeList.size() - 1); + return nodeList.peekLast(); } /** @@ -346,9 +345,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 +353,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/a43c0b0c/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..09f19f4 --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/util/ToUnmodifiable.java @@ -0,0 +1,28 @@ +package org.apache.bval.jsr.util; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +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() { + return Collectors.collectingAndThen(Collectors.toSet(), 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/a43c0b0c/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/a43c0b0c/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/a43c0b0c/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/a43c0b0c/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/a43c0b0c/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/a43c0b0c/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..96a63a6 --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/ValueExtractors.java @@ -0,0 +1,167 @@ +/* + * 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.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 org.apache.bval.jsr.metadata.ContainerElementKey; +import org.apache.bval.jsr.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; + +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::addValueExtractor); + + split(defaultExtractors.getProperty(ValueExtractor.class.getName() + ".container")) + .flatMap(ValueExtractors::loadValueExtractors).forEach(super::addValueExtractor); + } + + @Override + public void addValueExtractor(ValueExtractor<?> extractor) { + throw new UnsupportedOperationException(); + } + }; + } + + 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 addValueExtractor(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/a43c0b0c/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..db3bc88 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.jsr.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/a43c0b0c/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..9fd78f4 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 @@ -34,6 +34,7 @@ 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,10 +46,17 @@ 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) { + if (annotationType.getName().startsWith("javax.validation.constraints.")) { // cache built-in constraints only to avoid mem leaks + return METHODS_CACHE.computeIfAbsent(annotationType, k -> Reflection.getDeclaredMethods(k)); + } + 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; /** @@ -61,21 +69,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,14 +77,12 @@ 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. */ @@ -102,8 +93,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 +114,8 @@ public final class AnnotationProxyBuilder<A extends Annotation> { * @param elementName * @param value */ - public void putValue(String elementName, Object value) { - elements.put(elementName, value); + public Object putValue(String elementName, Object value) { + return elements.put(elementName, value); } /** @@ -195,6 +185,14 @@ public final class AnnotationProxyBuilder<A extends Annotation> { } /** + * Configure the well-known "validationAppliesTo" element. + * @param constraintTarget + */ + public void setValidationAppliesTo(ConstraintTarget constraintTarget) { + ConstraintAnnotationAttributes.VALIDATION_APPLIES_TO.put(elements, constraintTarget); + } + + /** * Create the annotation represented by this builder. * * @return {@link Annotation} @@ -203,16 +201,21 @@ 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 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/a43c0b0c/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(); + } +}
