initial BVal 2 implementation of method inheritance rules
Project: http://git-wip-us.apache.org/repos/asf/bval/repo Commit: http://git-wip-us.apache.org/repos/asf/bval/commit/faec747d Tree: http://git-wip-us.apache.org/repos/asf/bval/tree/faec747d Diff: http://git-wip-us.apache.org/repos/asf/bval/diff/faec747d Branch: refs/heads/bv2 Commit: faec747db9653ac9cce3c253ffa09a9d89d0adf7 Parents: f2acb64 Author: Matt Benson <[email protected]> Authored: Tue Feb 27 17:44:59 2018 -0600 Committer: Matt Benson <[email protected]> Committed: Tue Feb 27 17:44:59 2018 -0600 ---------------------------------------------------------------------- .../bval/jsr/descriptor/DescriptorManager.java | 7 +- .../bval/jsr/descriptor/MetadataReader.java | 25 +- .../bval/jsr/metadata/CompositeBuilder.java | 134 +++++++-- .../bval/jsr/metadata/HierarchyBuilder.java | 291 +++++++++++++++---- .../org/apache/bval/jsr/metadata/Liskov.java | 197 +++++++++++++ .../java/org/apache/bval/jsr/metadata/Meta.java | 25 +- .../java/org/apache/bval/jsr/util/Methods.java | 10 + 7 files changed, 595 insertions(+), 94 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/bval/blob/faec747d/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/DescriptorManager.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/DescriptorManager.java b/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/DescriptorManager.java index 01a1b5c..12c9a55 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/DescriptorManager.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/DescriptorManager.java @@ -68,9 +68,10 @@ public class DescriptorManager { private MetadataBuilder.ForBean builder(Class<?> beanClass) { final MetadataBuilder.ForBean primaryBuilder = - new HierarchyBuilder(reflectionBuilder::forBean).forBean(beanClass); + new HierarchyBuilder(validatorFactory, reflectionBuilder::forBean).forBean(beanClass); - final MetadataBuilder.ForBean customBuilder = new HierarchyBuilder(this::customBuilder).forBean(beanClass); + final MetadataBuilder.ForBean customBuilder = + new HierarchyBuilder(validatorFactory, this::customBuilder).forBean(beanClass); return customBuilder.isEmpty() ? primaryBuilder : DualBuilder.forBean(primaryBuilder, customBuilder); } @@ -86,6 +87,6 @@ public class DescriptorManager { return customBuilders.get(0); } return customBuilders.stream() - .collect(CompositeBuilder.with(AnnotationBehaviorMergeStrategy.consensus()).compose()); + .collect(CompositeBuilder.with(validatorFactory, AnnotationBehaviorMergeStrategy.consensus()).compose()); } } http://git-wip-us.apache.org/repos/asf/bval/blob/faec747d/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/MetadataReader.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/MetadataReader.java b/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/MetadataReader.java index 77cf9fc..2e9a31f 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/MetadataReader.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/MetadataReader.java @@ -39,9 +39,12 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; +import javax.validation.ConstraintDeclarationException; import javax.validation.GroupDefinitionException; import javax.validation.GroupSequence; import javax.validation.ParameterNameProvider; +import javax.validation.Valid; +import javax.validation.groups.ConvertGroup; import javax.validation.groups.Default; import javax.validation.metadata.PropertyDescriptor; import javax.validation.metadata.Scope; @@ -100,10 +103,8 @@ class MetadataReader { }); beanBuilder.getGetters(meta).forEach((g, builder) -> { - final Method getter = Reflection.find(meta.getHost(), t -> { - return Stream.of(Reflection.getDeclaredMethods(t)).filter(Methods::isGetter) - .filter(m -> g.equals(Methods.propertyName(m))).findFirst().orElse(null); - }); + final Method getter = Methods.getter(meta.getHost(), g); + Exceptions.raiseIf(getter == null, IllegalStateException::new, "Getter method for property %s not found", g); @@ -199,7 +200,21 @@ class MetadataReader { } Set<GroupConversion> getGroupConversions() { - return builder.getGroupConversions(meta); + final Set<GroupConversion> groupConversions = builder.getGroupConversions(meta); + Exceptions.raiseUnless(groupConversions.isEmpty() || isCascaded(), ConstraintDeclarationException::new, + "@%s declared without @%s on %s", ConvertGroup.class.getSimpleName(), Valid.class.getSimpleName(), + meta.describeHost()); + + Exceptions.raiseIf( + groupConversions.stream().map(GroupConversion::getFrom).distinct().count() < groupConversions.size(), + ConstraintDeclarationException::new, "%s has duplicate 'from' group conversions", meta.describeHost()); + + groupConversions.stream().map(GroupConversion::getFrom) + .forEach(f -> Exceptions.raiseIf(f.isAnnotationPresent(GroupSequence.class), + ConstraintDeclarationException::new, + "Invalid group conversion declared on %s from group sequence %s", meta.describeHost(), f)); + + return groupConversions; } Set<ContainerElementTypeD> getContainerElementTypes(CascadableContainerD<?, ?> parent) { http://git-wip-us.apache.org/repos/asf/bval/blob/faec747d/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/CompositeBuilder.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/CompositeBuilder.java b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/CompositeBuilder.java index 3aed1d0..56dd1f8 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/CompositeBuilder.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/CompositeBuilder.java @@ -30,17 +30,24 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; +import javax.validation.ElementKind; +import javax.validation.ParameterNameProvider; import javax.validation.metadata.Scope; +import org.apache.bval.jsr.ApacheValidatorFactory; import org.apache.bval.jsr.groups.GroupConversion; +import org.apache.bval.jsr.util.Methods; import org.apache.bval.jsr.util.ToUnmodifiable; +import org.apache.bval.util.Exceptions; import org.apache.bval.util.Validate; +import org.apache.bval.util.reflection.Reflection; public class CompositeBuilder { @@ -60,14 +67,17 @@ public class CompositeBuilder { } <K, D> Map<K, D> merge(Function<DELEGATE, Map<K, D>> toMap, Function<List<D>, D> merge) { + return merge(toMap, (k, l) -> merge.apply(l)); + } + + <K, D> Map<K, D> merge(Function<DELEGATE, Map<K, D>> toMap, BiFunction<K, List<D>, D> merge) { final List<Map<K, D>> maps = delegates.stream().map(toMap).collect(Collectors.toList()); final Function<? super K, ? extends D> valueMapper = k -> { final List<D> mappedDelegates = maps.stream().map(m -> m.get(k)).filter(Objects::nonNull).collect(Collectors.toList()); - return mappedDelegates.size() == 1 ? mappedDelegates.get(0) : merge.apply(mappedDelegates); + return mappedDelegates.size() == 1 ? mappedDelegates.get(0) : merge.apply(k, mappedDelegates); }; - return maps.stream().map(Map::keySet).flatMap(Collection::stream).distinct() .collect(Collectors.toMap(Function.identity(), valueMapper)); } @@ -88,22 +98,34 @@ public class CompositeBuilder { @Override public Map<String, MetadataBuilder.ForContainer<Field>> getFields(Meta<Class<?>> meta) { - return merge(b -> b.getFields(meta), CompositeBuilder.ForContainer::new); + return merge(b -> b.getFields(meta), (f, l) -> { + final Field fld = Reflection.find(meta.getHost(), t -> Reflection.getDeclaredField(t, f)); + Exceptions.raiseIf(fld == null, IllegalStateException::new, "Could not find field %s of %s", f, + meta.getHost()); + return forContainer(l, new Meta.ForField(fld), ElementKind.PROPERTY); + }); } @Override public Map<String, MetadataBuilder.ForContainer<Method>> getGetters(Meta<Class<?>> meta) { - return merge(b -> b.getGetters(meta), CompositeBuilder.ForContainer::new); + return merge(b -> b.getGetters(meta), (g, l) -> { + final Method getter = Methods.getter(meta.getHost(), g); + Exceptions.raiseIf(getter == null, IllegalStateException::new, + "Could not find getter for property %s of %s", g, meta.getHost()); + return forContainer(l, new Meta.ForMethod(getter), ElementKind.PROPERTY); + }); } @Override public Map<Signature, MetadataBuilder.ForExecutable<Constructor<?>>> getConstructors(Meta<Class<?>> meta) { - return merge(b -> b.getConstructors(meta), CompositeBuilder.ForExecutable::new); + return merge(b -> b.getConstructors(meta), + d -> new CompositeBuilder.ForExecutable<>(d, ParameterNameProvider::getParameterNames)); } @Override public Map<Signature, MetadataBuilder.ForExecutable<Method>> getMethods(Meta<Class<?>> meta) { - return merge(b -> b.getMethods(meta), CompositeBuilder.ForExecutable::new); + return merge(b -> b.getMethods(meta), + d -> new CompositeBuilder.ForExecutable<>(d, ParameterNameProvider::getParameterNames)); } } @@ -138,7 +160,7 @@ public class CompositeBuilder { } } - private class ForContainer<DELEGATE extends MetadataBuilder.ForContainer<E>, E extends AnnotatedElement> + class ForContainer<DELEGATE extends MetadataBuilder.ForContainer<E>, E extends AnnotatedElement> extends CompositeBuilder.ForElement<DELEGATE, E> implements MetadataBuilder.ForContainer<E> { ForContainer(List<DELEGATE> delegates) { @@ -146,34 +168,68 @@ public class CompositeBuilder { } @Override - public final boolean isCascade(Meta<E> meta) { + public boolean isCascade(Meta<E> meta) { return delegates.stream().anyMatch(d -> d.isCascade(meta)); } @Override - public final Set<GroupConversion> getGroupConversions(Meta<E> meta) { + public Set<GroupConversion> getGroupConversions(Meta<E> meta) { return delegates.stream().map(d -> d.getGroupConversions(meta)).flatMap(Collection::stream) .collect(ToUnmodifiable.set()); } @Override - public final Map<ContainerElementKey, MetadataBuilder.ForContainer<AnnotatedType>> getContainerElementTypes( + public Map<ContainerElementKey, MetadataBuilder.ForContainer<AnnotatedType>> getContainerElementTypes( Meta<E> meta) { - return merge(b -> b.getContainerElementTypes(meta), CompositeBuilder.ForContainer::new); + return merge(b -> b.getContainerElementTypes(meta), + (k, l) -> forContainer(l, new Meta.ForContainerElement(meta, k), ElementKind.CONTAINER_ELEMENT)); } } - private class ForExecutable<DELEGATE extends MetadataBuilder.ForExecutable<E>, E extends Executable> + class ForReturnValue<E extends Executable> implements MetadataBuilder.ForContainer<E> { + private final MetadataBuilder.ForContainer<E> delegate; + + ForReturnValue(MetadataBuilder.ForContainer<E> delegate) { + this.delegate = delegate; + } + + @Override + public Annotation[] getDeclaredConstraints(Meta<E> meta) { + return delegate.getDeclaredConstraints(meta); + } + + @Override + public boolean isCascade(Meta<E> meta) { + return delegate.isCascade(meta); + } + + @Override + public Set<GroupConversion> getGroupConversions(Meta<E> meta) { + return delegate.getGroupConversions(meta); + } + + @Override + public Map<ContainerElementKey, MetadataBuilder.ForContainer<AnnotatedType>> getContainerElementTypes( + Meta<E> meta) { + return delegate.getContainerElementTypes(meta); + } + } + + class ForExecutable<DELEGATE extends MetadataBuilder.ForExecutable<E>, E extends Executable> extends Delegator<DELEGATE> implements MetadataBuilder.ForExecutable<E> { - ForExecutable(List<DELEGATE> delegates) { + private final BiFunction<ParameterNameProvider, E, List<String>> getParameterNames; + + ForExecutable(List<DELEGATE> delegates, BiFunction<ParameterNameProvider, E, List<String>> getParameterNames) { super(delegates); + this.getParameterNames = Validate.notNull(getParameterNames, "getParameterNames"); } @Override - public MetadataBuilder.ForContainer<E> getReturnValue(Meta<E> meta) { - return new CompositeBuilder.ForContainer<>( - delegates.stream().map(d -> d.getReturnValue(meta)).collect(Collectors.toList())); + public ForReturnValue<E> getReturnValue(Meta<E> meta) { + return new ForReturnValue<>(CompositeBuilder.this.forContainer( + delegates.stream().map(d -> d.getReturnValue(meta)).collect(Collectors.toList()), meta, + ElementKind.RETURN_VALUE)); } @Override @@ -185,35 +241,53 @@ public class CompositeBuilder { Validate.validState(parameterCounts.size() == 1, "Mismatched parameter counts: %s", parameterCounts); final int parameterCount = parameterCounts.iterator().next().intValue(); - return IntStream.range(0, parameterCount) - .mapToObj(n -> new CompositeBuilder.ForContainer<>( - parameterLists.stream().map(l -> l.get(n)).collect(Collectors.toList()))) - .collect(ToUnmodifiable.list()); + final List<Meta<Parameter>> metaParams = getMetaParameters(meta, getParameterNames); + return IntStream.range(0, parameterCount).mapToObj(n -> { + return forContainer(parameterLists.stream().map(l -> l.get(n)).collect(Collectors.toList()), + metaParams.get(n), ElementKind.PARAMETER); + }).collect(ToUnmodifiable.list()); } @Override public MetadataBuilder.ForElement<E> getCrossParameter(Meta<E> meta) { - return new CompositeBuilder.ForElement<MetadataBuilder.ForElement<E>, E>( - delegates.stream().map(d -> d.getCrossParameter(meta)).collect(Collectors.toList())); + return forCrossParameter( + delegates.stream().map(d -> d.getCrossParameter(meta)).collect(Collectors.toList()), meta); } } - public static CompositeBuilder with(AnnotationBehaviorMergeStrategy annotationBehaviorStrategy) { - return new CompositeBuilder(annotationBehaviorStrategy); + public static CompositeBuilder with(ApacheValidatorFactory validatorFactory, + AnnotationBehaviorMergeStrategy annotationBehaviorStrategy) { + return new CompositeBuilder(validatorFactory, annotationBehaviorStrategy); } private final AnnotationBehaviorMergeStrategy annotationBehaviorStrategy; + protected final ApacheValidatorFactory validatorFactory; - CompositeBuilder(AnnotationBehaviorMergeStrategy annotationBehaviorMergeStrategy) { + CompositeBuilder(ApacheValidatorFactory validatorFactory, + AnnotationBehaviorMergeStrategy annotationBehaviorMergeStrategy) { super(); this.annotationBehaviorStrategy = Validate.notNull(annotationBehaviorMergeStrategy, "annotationBehaviorMergeStrategy"); + this.validatorFactory = Validate.notNull(validatorFactory, "validatorFactory"); } public Collector<MetadataBuilder.ForBean, ?, MetadataBuilder.ForBean> compose() { return Collectors.collectingAndThen(Collectors.toList(), CompositeBuilder.ForBean::new); } + protected final <E extends Executable> List<Meta<Parameter>> getMetaParameters(Meta<E> meta, + BiFunction<ParameterNameProvider, E, List<String>> getParameterNames) { + final Parameter[] parameters = meta.getHost().getParameters(); + final List<String> parameterNames = + getParameterNames.apply(validatorFactory.getParameterNameProvider(), meta.getHost()); + + Exceptions.raiseUnless(parameterNames.size() == parameters.length, IllegalStateException::new, + "%s returned wrong number of parameter names", validatorFactory.getParameterNameProvider()); + + return IntStream.range(0, parameters.length) + .mapToObj(n -> new Meta.ForParameter(parameters[n], parameterNames.get(n))).collect(Collectors.toList()); + } + protected <E extends AnnotatedElement> Map<Scope, Annotation[]> getConstraintsByScope( CompositeBuilder.ForElement<? extends MetadataBuilder.ForElement<E>, E> composite, Meta<E> meta) { return Collections.singletonMap(Scope.LOCAL_ELEMENT, composite.getDeclaredConstraints(meta)); @@ -226,4 +300,14 @@ public class CompositeBuilder { "group sequence returned from multiple composite class metadata builders"); return groupSequence.isEmpty() ? null : groupSequence.get(0); } + + protected <DELEGATE extends MetadataBuilder.ForContainer<E>, E extends AnnotatedElement> MetadataBuilder.ForContainer<E> forContainer( + List<DELEGATE> delegates, Meta<E> meta, ElementKind elementKind) { + return new CompositeBuilder.ForContainer<>(delegates); + } + + protected <DELEGATE extends MetadataBuilder.ForElement<E>, E extends Executable> MetadataBuilder.ForElement<E> forCrossParameter( + List<DELEGATE> delegates, Meta<E> meta) { + return new CompositeBuilder.ForElement<>(delegates); + } } http://git-wip-us.apache.org/repos/asf/bval/blob/faec747d/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/HierarchyBuilder.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/HierarchyBuilder.java b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/HierarchyBuilder.java index 9cac057..a900501 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/HierarchyBuilder.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/HierarchyBuilder.java @@ -18,21 +18,35 @@ package org.apache.bval.jsr.metadata; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.AnnotatedType; import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import javax.validation.ElementKind; +import javax.validation.ParameterNameProvider; import javax.validation.metadata.Scope; +import org.apache.bval.jsr.ApacheValidatorFactory; +import org.apache.bval.jsr.groups.GroupConversion; +import org.apache.bval.jsr.util.Methods; +import org.apache.bval.util.Exceptions; import org.apache.bval.util.Validate; import org.apache.bval.util.reflection.Reflection; import org.apache.bval.util.reflection.Reflection.Interfaces; @@ -41,78 +55,217 @@ import org.apache.commons.weaver.privilizer.Privilizing.CallTo; @Privilizing(@CallTo(Reflection.class)) public class HierarchyBuilder extends CompositeBuilder { - private static abstract class HierarchyDelegate<T> { + static abstract class HierarchyDelegate<E extends AnnotatedElement, T> { final T delegate; + final Meta<E> hierarchyElement; - HierarchyDelegate(T delegate) { + HierarchyDelegate(T delegate, Meta<E> hierarchyElement) { super(); this.delegate = Validate.notNull(delegate, "delegate"); + this.hierarchyElement = Validate.notNull(hierarchyElement, "hierarchyElement"); } - static class ForBean extends HierarchyDelegate<MetadataBuilder.ForBean> implements MetadataBuilder.ForBean { - final Meta<Class<?>> hierarchyType; + Meta<E> getHierarchyElement() { + return hierarchyElement; + } + } - ForBean(MetadataBuilder.ForBean delegate, Class<?> hierarchyType) { - super(delegate); - this.hierarchyType = new Meta.ForClass(hierarchyType); - } + static abstract class ElementDelegate<E extends AnnotatedElement, T extends MetadataBuilder.ForElement<E>> + extends HierarchyDelegate<E, T> implements MetadataBuilder.ForElement<E> { - @Override - public MetadataBuilder.ForClass getClass(Meta<Class<?>> meta) { - return new HierarchyDelegate.ForClass(delegate.getClass(hierarchyType), hierarchyType); - } + ElementDelegate(T delegate, Meta<E> hierarchyElement) { + super(delegate, hierarchyElement); + } + + Annotation[] getDeclaredConstraints() { + return delegate.getDeclaredConstraints(hierarchyElement); + } - @Override - public Map<String, MetadataBuilder.ForContainer<Field>> getFields(Meta<Class<?>> meta) { - return delegate.getFields(hierarchyType); - } + @Override + public final Annotation[] getDeclaredConstraints(Meta<E> meta) { + return getDeclaredConstraints(); + } + } - @Override - public Map<String, MetadataBuilder.ForContainer<Method>> getGetters(Meta<Class<?>> meta) { - return delegate.getGetters(hierarchyType); - } + private class BeanDelegate extends HierarchyDelegate<Class<?>, MetadataBuilder.ForBean> + implements MetadataBuilder.ForBean { - @SuppressWarnings("unlikely-arg-type") - @Override - public Map<Signature, MetadataBuilder.ForExecutable<Constructor<?>>> getConstructors(Meta<Class<?>> meta) { - // suppress hierarchical ctors: - return hierarchyType.equals(meta.getHost()) ? delegate.getConstructors(hierarchyType) - : Collections.emptyMap(); - } + BeanDelegate(MetadataBuilder.ForBean delegate, Class<?> hierarchyType) { + super(delegate, new Meta.ForClass(hierarchyType)); + } - @Override - public Map<Signature, MetadataBuilder.ForExecutable<Method>> getMethods(Meta<Class<?>> meta) { - final Map<Signature, MetadataBuilder.ForExecutable<Method>> m = delegate.getMethods(hierarchyType); + @Override + public MetadataBuilder.ForClass getClass(Meta<Class<?>> meta) { + return new ClassDelegate(delegate.getClass(hierarchyElement), hierarchyElement); + } - return m; - } + @Override + public Map<String, MetadataBuilder.ForContainer<Field>> getFields(Meta<Class<?>> meta) { + return delegate.getFields(hierarchyElement); + } + + @Override + public Map<Signature, MetadataBuilder.ForExecutable<Constructor<?>>> getConstructors(Meta<Class<?>> meta) { + // ignore hierarchical ctors: + return hierarchyElement.equals(meta) ? delegate.getConstructors(hierarchyElement) : Collections.emptyMap(); } - static class ForClass extends HierarchyDelegate<MetadataBuilder.ForClass> implements MetadataBuilder.ForClass { + @Override + public Map<String, MetadataBuilder.ForContainer<Method>> getGetters(Meta<Class<?>> meta) { + final Map<String, MetadataBuilder.ForContainer<Method>> getters = delegate.getGetters(hierarchyElement); + final Map<String, MetadataBuilder.ForContainer<Method>> result = new LinkedHashMap<>(); - final Meta<Class<?>> hierarchyType; + getters.forEach((k, v) -> { + final Method getter = Methods.getter(hierarchyElement.getHost(), k); - ForClass(MetadataBuilder.ForClass delegate, Meta<Class<?>> hierarchyType) { - super(delegate); - this.hierarchyType = hierarchyType; - } + Exceptions.raiseIf(getter == null, IllegalStateException::new, + "delegate builder specified unknown getter"); - @Override - public Annotation[] getDeclaredConstraints(Meta<Class<?>> meta) { - return delegate.getDeclaredConstraints(hierarchyType); + result.put(k, new ContainerDelegate<Method>(v, new Meta.ForMethod(getter))); + }); + return result; + } + + @Override + public Map<Signature, MetadataBuilder.ForExecutable<Method>> getMethods(Meta<Class<?>> meta) { + final Map<Signature, MetadataBuilder.ForExecutable<Method>> methods = delegate.getMethods(hierarchyElement); + if (methods.isEmpty()) { + return methods; } + final Map<Signature, MetadataBuilder.ForExecutable<Method>> result = new LinkedHashMap<>(); + methods + .forEach((k, v) -> result.put(k, + new ExecutableDelegate<>(v, new Meta.ForMethod( + Reflection.getDeclaredMethod(hierarchyElement.getHost(), k.getName(), k.getParameterTypes())), + ParameterNameProvider::getParameterNames))); + return result; + } + } + + private class ClassDelegate extends ElementDelegate<Class<?>, MetadataBuilder.ForClass> + implements MetadataBuilder.ForClass { + + ClassDelegate(MetadataBuilder.ForClass delegate, Meta<Class<?>> hierarchyType) { + super(delegate, hierarchyType); + } + + @Override + public List<Class<?>> getGroupSequence(Meta<Class<?>> meta) { + return delegate.getGroupSequence(hierarchyElement); + } + } + + class ContainerDelegate<E extends AnnotatedElement> extends ElementDelegate<E, MetadataBuilder.ForContainer<E>> + implements MetadataBuilder.ForContainer<E> { + + ContainerDelegate(MetadataBuilder.ForContainer<E> delegate, Meta<E> hierarchyElement) { + super(delegate, hierarchyElement); + } + + boolean isCascade() { + return delegate.isCascade(hierarchyElement); + } + + @Override + public final boolean isCascade(Meta<E> meta) { + return isCascade(); + } + + Set<GroupConversion> getGroupConversions() { + return delegate.getGroupConversions(hierarchyElement); + } + + @Override + public final Set<GroupConversion> getGroupConversions(Meta<E> meta) { + return getGroupConversions(); + } + + @Override + public Map<ContainerElementKey, MetadataBuilder.ForContainer<AnnotatedType>> getContainerElementTypes( + Meta<E> meta) { + final Map<ContainerElementKey, MetadataBuilder.ForContainer<AnnotatedType>> containerElementTypes = + delegate.getContainerElementTypes(hierarchyElement); + + final Map<ContainerElementKey, MetadataBuilder.ForContainer<AnnotatedType>> result = new LinkedHashMap<>(); - @Override - public List<Class<?>> getGroupSequence(Meta<Class<?>> meta) { - return delegate.getGroupSequence(hierarchyType); + containerElementTypes.forEach((k, v) -> { + result.put(k, new ContainerDelegate<>(v, new Meta.ForContainerElement(hierarchyElement, k))); + }); + return result; + } + } + + private class ExecutableDelegate<E extends Executable> + extends HierarchyDelegate<E, MetadataBuilder.ForExecutable<E>> implements MetadataBuilder.ForExecutable<E> { + + final BiFunction<ParameterNameProvider, E, List<String>> getParameterNames; + + ExecutableDelegate(MetadataBuilder.ForExecutable<E> delegate, Meta<E> hierarchyElement, + BiFunction<ParameterNameProvider, E, List<String>> getParameterNames) { + super(delegate, hierarchyElement); + this.getParameterNames = Validate.notNull(getParameterNames, "getParameterNames"); + } + + @Override + public MetadataBuilder.ForContainer<E> getReturnValue(Meta<E> meta) { + return new ContainerDelegate<>(delegate.getReturnValue(hierarchyElement), hierarchyElement); + } + + @Override + public MetadataBuilder.ForElement<E> getCrossParameter(Meta<E> meta) { + return new CrossParameterDelegate<>(delegate.getCrossParameter(hierarchyElement), hierarchyElement); + } + + @Override + public List<MetadataBuilder.ForContainer<Parameter>> getParameters(Meta<E> meta) { + final List<MetadataBuilder.ForContainer<Parameter>> parameterDelegates = + delegate.getParameters(hierarchyElement); + + if (parameterDelegates.isEmpty()) { + return parameterDelegates; } + final List<Meta<Parameter>> metaParameters = getMetaParameters(hierarchyElement, getParameterNames); + + Exceptions.raiseUnless(metaParameters.size() == parameterDelegates.size(), IllegalStateException::new, + "Got wrong number of parameter delegates for %s", meta.getHost()); + + return IntStream.range(0, parameterDelegates.size()) + .mapToObj(n -> new ContainerDelegate<>(parameterDelegates.get(n), metaParameters.get(n))) + .collect(Collectors.toList()); + } + } + + private class CrossParameterDelegate<E extends Executable> + extends ElementDelegate<E, MetadataBuilder.ForElement<E>> implements MetadataBuilder.ForElement<E> { + + CrossParameterDelegate(MetadataBuilder.ForElement<E> delegate, Meta<E> hierarchyElement) { + super(delegate, hierarchyElement); + } + } + + private class ForCrossParameter<E extends Executable> + extends CompositeBuilder.ForElement<CrossParameterDelegate<E>, E> { + + ForCrossParameter(List<CrossParameterDelegate<E>> delegates) { + super(delegates); + Liskov.validateCrossParameterHierarchy(delegates); + } + } + + private class ForContainer<E extends AnnotatedElement> + extends CompositeBuilder.ForContainer<ContainerDelegate<E>, E> { + + ForContainer(List<ContainerDelegate<E>> delegates, ElementKind elementKind) { + super(delegates); + Liskov.validateContainerHierarchy(delegates, Validate.notNull(elementKind, "elementKind")); } } private final Function<Class<?>, MetadataBuilder.ForBean> getBeanBuilder; - public HierarchyBuilder(Function<Class<?>, MetadataBuilder.ForBean> getBeanBuilder) { - super(AnnotationBehaviorMergeStrategy.first()); + public HierarchyBuilder(ApacheValidatorFactory validatorFactory, + Function<Class<?>, MetadataBuilder.ForBean> getBeanBuilder) { + super(validatorFactory, AnnotationBehaviorMergeStrategy.first()); this.getBeanBuilder = Validate.notNull(getBeanBuilder, "getBeanBuilder function was null"); } @@ -126,16 +279,13 @@ public class HierarchyBuilder extends CompositeBuilder { */ delegates.add(Optional.of(beanClass).map(getBeanBuilder).orElseGet(() -> EmptyBuilder.instance().forBean())); - // iterate the hierarchy, skipping the first (i.e. beanClass handled - // above) + // iterate the hierarchy, skipping the first (i.e. beanClass handled above) final Iterator<Class<?>> hierarchy = Reflection.hierarchy(beanClass, Interfaces.INCLUDE).iterator(); hierarchy.next(); - // skip Object.class; skip null/empty hierarchy builders, mapping others - // to HierarchyDelegate - hierarchy - .forEachRemaining(t -> Optional.of(t).filter(Predicate.isEqual(Object.class).negate()).map(getBeanBuilder) - .filter(b -> !b.isEmpty()).map(b -> new HierarchyDelegate.ForBean(b, t)).ifPresent(delegates::add)); + // skip Object.class; skip null/empty hierarchy builders, mapping others to BeanDelegate + hierarchy.forEachRemaining(t -> Optional.of(t).filter(Predicate.isEqual(Object.class).negate()) + .map(getBeanBuilder).filter(b -> !b.isEmpty()).map(b -> new BeanDelegate(b, t)).ifPresent(delegates::add)); // if we have nothing but empty builders (which should only happen for // absent custom metadata), return empty: @@ -166,4 +316,39 @@ public class HierarchyBuilder extends CompositeBuilder { protected List<Class<?>> getGroupSequence(CompositeBuilder.ForClass composite, Meta<Class<?>> meta) { return composite.delegates.get(0).getGroupSequence(meta); } + + @Override + protected <DELEGATE extends MetadataBuilder.ForContainer<E>, E extends AnnotatedElement> MetadataBuilder.ForContainer<E> forContainer( + List<DELEGATE> delegates, Meta<E> meta, ElementKind elementKind) { + + if (delegates.isEmpty()) { + return super.forContainer(delegates, meta, elementKind); + } + final List<ContainerDelegate<E>> hierarchyDelegates = delegates.stream() + .<ContainerDelegate<E>> map( + d -> d instanceof ContainerDelegate<?> ? (ContainerDelegate<E>) d : new ContainerDelegate<>(d, meta)) + .collect(Collectors.toList()); + + @SuppressWarnings("unchecked") + final CompositeBuilder.ForContainer<DELEGATE, E> result = + (CompositeBuilder.ForContainer<DELEGATE, E>) new HierarchyBuilder.ForContainer<E>(hierarchyDelegates, + elementKind); + + return result; + } + + @Override + protected <DELEGATE extends MetadataBuilder.ForElement<E>, E extends Executable> MetadataBuilder.ForElement<E> forCrossParameter( + List<DELEGATE> delegates, Meta<E> meta) { + + if (delegates.isEmpty()) { + return super.forCrossParameter(delegates, meta); + } + final List<CrossParameterDelegate<E>> hierarchyDelegates = + delegates.stream() + .<CrossParameterDelegate<E>> map(d -> d instanceof CrossParameterDelegate<?> + ? (CrossParameterDelegate<E>) d : new CrossParameterDelegate<>(d, meta)) + .collect(Collectors.toList()); + return new HierarchyBuilder.ForCrossParameter<>(hierarchyDelegates); + } } http://git-wip-us.apache.org/repos/asf/bval/blob/faec747d/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/Liskov.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/Liskov.java b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/Liskov.java new file mode 100644 index 0000000..74bd8da --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/Liskov.java @@ -0,0 +1,197 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.bval.jsr.metadata; + +import java.lang.annotation.ElementType; +import java.util.Collection; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.validation.ConstraintDeclarationException; +import javax.validation.ElementKind; +import javax.validation.Valid; + +import org.apache.bval.jsr.metadata.HierarchyBuilder.ContainerDelegate; +import org.apache.bval.jsr.metadata.HierarchyBuilder.HierarchyDelegate; +import org.apache.bval.jsr.metadata.HierarchyBuilder.ElementDelegate; +import org.apache.bval.util.Exceptions; +import org.apache.commons.lang3.Validate; + +class Liskov { + //@formatter:off + private enum ValidationElement { + constraints, cascades, groupConversions; + } + + private enum StrengtheningIssue implements Predicate<Map<Meta<?>, Set<ValidationElement>>> { + overriddenHierarchy("overridden %s in inheritance hierarchy: %s") { + + @Override + public boolean test(Map<Meta<?>, Set<ValidationElement>> detectedValidationElements) { + boolean lowestFound = false; + + for (Set<ValidationElement> validated : detectedValidationElements.values()) { + if (lowestFound) { + return false; + } + lowestFound = !validated.isEmpty(); + } + return true; + } + }, + unrelatedInheritance("declared %s in unrelated inheritance hierarchies: %s") { + + @Override + public boolean test(Map<Meta<?>, Set<ValidationElement>> detectedValidationElements) { + final Set<Class<?>> interfaces = detectedValidationElements.keySet().stream().map(Meta::getDeclaringClass) + .filter(Class::isInterface).collect(Collectors.toSet()); + if (interfaces.isEmpty()) { + return true; + } + final boolean allRelated = + detectedValidationElements.keySet().stream().map(Meta::getDeclaringClass).allMatch(ifc -> interfaces + .stream().filter(Predicate.isEqual(ifc).negate()).allMatch(ifc2 -> related(ifc, ifc2))); + + return allRelated; + } + }; + //@formatter:on + + final String format; + + private StrengtheningIssue(String format) { + this.format = "Illegal strengthening: " + format; + } + + Supplier<String> messageFor(Map<Meta<?>, Set<ValidationElement>> detectedValidationElements) { + return () -> { + final Set<ValidationElement> validationElements = detectedValidationElements.values().stream() + .flatMap(Collection::stream).collect(Collectors.toSet()); + + final String describeHierarchy = detectedValidationElements.keySet().stream().map(Meta::describeHost) + .collect(Collectors.joining(", ", "[", "]")); + + return String.format(format, validationElements, describeHierarchy); + }; + } + + void check(Map<Meta<?>, Set<ValidationElement>> detectedValidationElements) { + Exceptions.raiseUnless(test(detectedValidationElements), ConstraintDeclarationException::new, + messageFor(detectedValidationElements)); + } + } + + static void validateContainerHierarchy(List<? extends ContainerDelegate<?>> delegates, ElementKind elementKind) { + if (Validate.notNull(delegates, "delegates").isEmpty()) { + return; + } + if (elementKind == ElementKind.CONTAINER_ELEMENT) { + elementKind = getContainer(delegates.get(0).getHierarchyElement()); + } + switch (Validate.notNull(elementKind, "elementKind")) { + case PROPERTY: + break; + case RETURN_VALUE: + noRedeclarationOfReturnValueCascading(delegates); + break; + case PARAMETER: + noStrengtheningOfPreconditions(delegates, detectConstraints(), detectCascading(), detectGroupConversion()); + break; + default: + Exceptions.raise(IllegalArgumentException::new, "Cannot validate %s.%s as %s", + ElementKind.class.getSimpleName(), elementKind, ContainerDelegate.class.getSimpleName()); + } + } + + static void validateCrossParameterHierarchy(List<? extends ElementDelegate<?, ?>> delegates) { + if (Validate.notNull(delegates, "delegates").isEmpty()) { + return; + } + noStrengtheningOfPreconditions(delegates, detectConstraints()); + } + + private static ElementKind getContainer(Meta<?> meta) { + Meta<?> m = meta; + while (m.getElementType() == ElementType.TYPE_USE) { + m = ((Meta.ForContainerElement) m).getParent(); + } + switch (m.getElementType()) { + case METHOD: + return ElementKind.RETURN_VALUE; + case PARAMETER: + return ElementKind.PARAMETER; + default: + return ElementKind.PROPERTY; + } + } + + private static void noRedeclarationOfReturnValueCascading(List<? extends ContainerDelegate<?>> delegates) { + final Set<Meta<?>> markedForCascade = delegates.stream().filter(ContainerDelegate::isCascade) + .map(HierarchyDelegate::getHierarchyElement).collect(Collectors.toCollection(LinkedHashSet::new)); + + Exceptions.raiseIf(markedForCascade.size() > 1, ConstraintDeclarationException::new, + "Multiple return values marked @%s in same hierarchy: %s", Valid.class.getSimpleName(), markedForCascade); + } + + @SafeVarargs + private static <D extends ElementDelegate<?, ?>> void noStrengtheningOfPreconditions(List<? extends D> delegates, + Function<? super D, ValidationElement>... detectors) { + + final Map<Meta<?>, Set<ValidationElement>> detectedValidationElements = new LinkedHashMap<>(); + delegates.forEach(d -> { + detectedValidationElements.put(d.getHierarchyElement(), + Stream.of(detectors).map(dt -> dt.apply(d)).filter(Objects::nonNull) + .collect(Collectors.toCollection(() -> EnumSet.noneOf(ValidationElement.class)))); + }); + if (detectedValidationElements.values().stream().allMatch(Collection::isEmpty)) { + // nothing declared + return; + } + for (StrengtheningIssue s : StrengtheningIssue.values()) { + s.check(detectedValidationElements); + } + } + + private static boolean related(Class<?> c1, Class<?> c2) { + return c1.isAssignableFrom(c2) || c2.isAssignableFrom(c1); + } + + private static Function<ElementDelegate<?, ?>, ValidationElement> detectConstraints() { + return d -> d.getDeclaredConstraints().length > 0 ? ValidationElement.constraints : null; + } + + private static Function<ContainerDelegate<?>, ValidationElement> detectCascading() { + return d -> d.isCascade() ? ValidationElement.cascades : null; + } + + private static Function<ContainerDelegate<?>, ValidationElement> detectGroupConversion() { + return d -> d.getGroupConversions().isEmpty() ? null : ValidationElement.groupConversions; + } + + private Liskov() { + } +} http://git-wip-us.apache.org/repos/asf/bval/blob/faec747d/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/Meta.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/Meta.java b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/Meta.java index dbcaeeb..ece48b0 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/Meta.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/metadata/Meta.java @@ -196,8 +196,8 @@ public abstract class Meta<E extends AnnotatedElement> { } @Override - public String toString() { - return String.format("%s(%s of %s)", getStringPrefix(), getName(), getHost()); + public String describeHost() { + return String.format("%s of %s", getName(), getHost()); } } @@ -233,6 +233,11 @@ public abstract class Meta<E extends AnnotatedElement> { public String getName() { return name; } + + @Override + public String describeHost() { + return String.format("%s of %s", getName(), getHost().getDeclaringExecutable()); + } } public static class ForContainerElement extends Meta<AnnotatedType> { @@ -271,8 +276,12 @@ public abstract class Meta<E extends AnnotatedElement> { } @Override - public String toString() { - return String.format("%s(%s of %s)", getStringPrefix(), key, getHost()); + public String describeHost() { + return String.format("%s of %s", key, parent); + } + + public Meta<?> getParent() { + return parent; } } @@ -311,12 +320,12 @@ public abstract class Meta<E extends AnnotatedElement> { } @Override - public String toString() { - return String.format("%s(%s)", getStringPrefix(), host); + public final String toString() { + return String.format("%s.%s(%s)", Meta.class.getSimpleName(), getClass().getSimpleName(), describeHost()); } - protected String getStringPrefix() { - return Meta.class.getSimpleName() + '.' + getClass().getSimpleName(); + public String describeHost() { + return host.toString(); } @Override http://git-wip-us.apache.org/repos/asf/bval/blob/faec747d/bval-jsr/src/main/java/org/apache/bval/jsr/util/Methods.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/util/Methods.java b/bval-jsr/src/main/java/org/apache/bval/jsr/util/Methods.java index 9f98311..4b5ce45 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/util/Methods.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/util/Methods.java @@ -18,9 +18,14 @@ package org.apache.bval.jsr.util; import java.beans.Introspector; import java.lang.reflect.Method; +import java.util.stream.Stream; import org.apache.bval.util.Validate; +import org.apache.bval.util.reflection.Reflection; +import org.apache.commons.weaver.privilizer.Privilizing; +import org.apache.commons.weaver.privilizer.Privilizing.CallTo; +@Privilizing(@CallTo(Reflection.class)) public final class Methods { public static boolean isGetter(Method m) { if (m.getParameterCount() > 0) { @@ -40,6 +45,11 @@ public final class Methods { return Introspector.decapitalize(suffix); } + public static Method getter(Class<?> clazz, String property) { + return Reflection.find(clazz, t -> Stream.of(Reflection.getDeclaredMethods(t)).filter(Methods::isGetter) + .filter(m -> property.equals(Methods.propertyName(m))).findFirst().orElse(null)); + } + private Methods() { } }
