BV2: validation job model
Project: http://git-wip-us.apache.org/repos/asf/bval/repo Commit: http://git-wip-us.apache.org/repos/asf/bval/commit/a921963f Tree: http://git-wip-us.apache.org/repos/asf/bval/tree/a921963f Diff: http://git-wip-us.apache.org/repos/asf/bval/diff/a921963f Branch: refs/heads/bv2 Commit: a921963f7e6d4766288c583278641cfc1add3620 Parents: 40ac09f Author: Matt Benson <[email protected]> Authored: Wed Feb 21 14:51:34 2018 -0600 Committer: Matt Benson <[email protected]> Committed: Wed Feb 21 14:59:58 2018 -0600 ---------------------------------------------------------------------- .../java/org/apache/bval/jsr/GraphContext.java | 95 ++++ .../jsr/job/ConstraintValidatorContextImpl.java | 195 +++++++ .../org/apache/bval/jsr/job/ValidateBean.java | 60 +++ .../apache/bval/jsr/job/ValidateParameters.java | 188 +++++++ .../apache/bval/jsr/job/ValidateProperty.java | 522 +++++++++++++++++++ .../bval/jsr/job/ValidateReturnValue.java | 126 +++++ .../org/apache/bval/jsr/job/ValidationJob.java | 380 ++++++++++++++ .../bval/jsr/job/ValidationJobFactory.java | 112 ++++ 8 files changed, 1678 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/bval/blob/a921963f/bval-jsr/src/main/java/org/apache/bval/jsr/GraphContext.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/GraphContext.java b/bval-jsr/src/main/java/org/apache/bval/jsr/GraphContext.java new file mode 100644 index 0000000..26350d6 --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/GraphContext.java @@ -0,0 +1,95 @@ +/* + * 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; + +import javax.validation.Path; + +import org.apache.bval.jsr.util.NodeImpl; +import org.apache.bval.jsr.util.PathImpl; +import org.apache.bval.util.Validate; + +public class GraphContext { + + private final ApacheFactoryContext validatorContext; + private final PathImpl path; + private final Object value; + private final GraphContext parent; + + public GraphContext(ApacheFactoryContext validatorContext, PathImpl path, Object value) { + this(validatorContext, path, value, null); + } + + private GraphContext(ApacheFactoryContext validatorContext, PathImpl path, Object value, GraphContext parent) { + super(); + this.validatorContext = Validate.notNull(validatorContext, "validatorContext"); + this.path = Validate.notNull(path, "path"); + this.value = value; + this.parent = parent; + } + + public ApacheFactoryContext getValidatorContext() { + return validatorContext; + } + + public PathImpl getPath() { + return PathImpl.copy(path); + } + + public Object getValue() { + return value; + } + + public GraphContext child(NodeImpl node, Object value) { + Validate.notNull(node, "node"); + final PathImpl p = PathImpl.copy(path); + p.addNode(node); + return new GraphContext(validatorContext, p, value, this); + } + + public GraphContext child(Path p, Object value) { + Validate.notNull(p, "Path"); + final PathImpl impl = PathImpl.copy(p); + Validate.isTrue(impl.isSubPathOf(path), "%s is not a subpath of %s", p, path); + return new GraphContext(validatorContext, impl, value, this); + } + + public boolean isRoot() { + return parent == null; + } + + public boolean isRecursive() { + GraphContext c = parent; + while (c != null) { + if (c.value == value) { + return true; + } + c = c.parent; + } + return false; + } + + public GraphContext getParent() { + return parent; + } + + @Override + public String toString() { + return String.format("%s: %s at '%s'", getClass().getSimpleName(), value, path); + } +} http://git-wip-us.apache.org/repos/asf/bval/blob/a921963f/bval-jsr/src/main/java/org/apache/bval/jsr/job/ConstraintValidatorContextImpl.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ConstraintValidatorContextImpl.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ConstraintValidatorContextImpl.java new file mode 100644 index 0000000..c3cc3a7 --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ConstraintValidatorContextImpl.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.bval.jsr.job; + +import java.util.HashSet; +import java.util.Set; + +import javax.validation.ClockProvider; +import javax.validation.ConstraintValidatorContext; +import javax.validation.ConstraintViolation; +import javax.validation.MessageInterpolator; +import javax.validation.Path; +import javax.validation.ValidationException; +import javax.validation.metadata.ConstraintDescriptor; +import javax.validation.metadata.CrossParameterDescriptor; + +import org.apache.bval.jsr.descriptor.ComposedD; +import org.apache.bval.jsr.descriptor.ConstraintD; +import org.apache.bval.jsr.descriptor.CrossParameterD; +import org.apache.bval.jsr.util.ContainerElementNodeBuilderCustomizableContextImpl; +import org.apache.bval.jsr.util.LeafNodeBuilderCustomizableContextImpl; +import org.apache.bval.jsr.util.NodeBuilderCustomizableContextImpl; +import org.apache.bval.jsr.util.NodeBuilderDefinedContextImpl; +import org.apache.bval.jsr.util.NodeImpl; +import org.apache.bval.jsr.util.PathImpl; +import org.apache.bval.util.Exceptions; +import org.apache.bval.util.Lazy; +import org.apache.bval.util.Validate; + +public class ConstraintValidatorContextImpl<T> implements ConstraintValidatorContext, MessageInterpolator.Context { + private class ConstraintViolationBuilderImpl implements ConstraintValidatorContext.ConstraintViolationBuilder { + private final String template; + private final PathImpl path; + + ConstraintViolationBuilderImpl(String template, PathImpl path) { + this.template = template; + this.path = path; + } + + /** + * {@inheritDoc} + */ + @Override + public NodeBuilderDefinedContext addNode(String name) { + PathImpl p; + if (path.isRootPath()) { + p = PathImpl.create(); + p.getLeafNode().setName(name); + } else { + p = PathImpl.copy(path); + p.addNode(new NodeImpl(name)); + } + return new NodeBuilderDefinedContextImpl(ConstraintValidatorContextImpl.this, template, p); + } + + @Override + public NodeBuilderCustomizableContext addPropertyNode(String name) { + return new NodeBuilderCustomizableContextImpl(ConstraintValidatorContextImpl.this, template, path, name); + } + + @Override + public LeafNodeBuilderCustomizableContext addBeanNode() { + return new LeafNodeBuilderCustomizableContextImpl(ConstraintValidatorContextImpl.this, template, path); + } + + @Override + public NodeBuilderDefinedContext addParameterNode(int index) { + Exceptions.raiseUnless(frame.descriptor instanceof CrossParameterDescriptor, ValidationException::new, + "Cannot add parameter node for %s", frame.descriptor.getClass().getName()); + + final CrossParameterD<?, ?> crossParameter = + ComposedD.unwrap(frame.descriptor, CrossParameterD.class).findFirst().get(); + + final String parameterName = crossParameter.getParent().getParameterDescriptors().get(index).getName(); + + final NodeImpl node = new NodeImpl.ParameterNodeImpl(parameterName, index); + if (!path.isRootPath()) { + path.removeLeafNode(); + } + path.addNode(node); + return new NodeBuilderDefinedContextImpl(ConstraintValidatorContextImpl.this, template, path); + } + + /** + * {@inheritDoc} + */ + @Override + public ConstraintValidatorContext addConstraintViolation() { + addError(template, path); + return ConstraintValidatorContextImpl.this; + } + + @Override + public ContainerElementNodeBuilderCustomizableContext addContainerElementNode(String name, + Class<?> containerType, Integer typeArgumentIndex) { + return new ContainerElementNodeBuilderCustomizableContextImpl(ConstraintValidatorContextImpl.this, template, + path, name, containerType, typeArgumentIndex); + } + } + + private final ValidationJob<T>.Frame<?> frame; + private final ConstraintD<?> constraint; + private final Lazy<Set<ConstraintViolation<T>>> violations = new Lazy<>(HashSet::new); + private boolean defaultConstraintViolationDisabled; + + /** + * Temporary for code migration + */ + // TODO delete + @Deprecated + protected ConstraintValidatorContextImpl() { + this.frame = null; + this.constraint = null; + } + + ConstraintValidatorContextImpl(ValidationJob<T>.Frame<?> frame, ConstraintD<?> constraint) { + super(); + this.frame = Validate.notNull(frame, "frame"); + this.constraint = Validate.notNull(constraint, "constraint"); + } + + @Override + public void disableDefaultConstraintViolation() { + this.defaultConstraintViolationDisabled = true; + } + + @Override + public String getDefaultConstraintMessageTemplate() { + return constraint.getMessageTemplate(); + } + + @Override + public ConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate) { + return new ConstraintViolationBuilderImpl(messageTemplate, frame.context.getPath()); + } + + @Override + public ClockProvider getClockProvider() { + return frame.getJob().validatorContext.getClockProvider(); + } + + @Override + public <U> U unwrap(Class<U> type) { + try { + return type.cast(this); + } catch (ClassCastException e) { + throw new ValidationException(e); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void addError(String messageTemplate, Path propertyPath) { + violations.get().add(((ValidationJob) frame.getJob()).createViolation(messageTemplate, this, propertyPath)); + } + + ValidationJob<T>.Frame<?> getFrame() { + return frame; + } + + Set<ConstraintViolation<T>> getRequiredViolations() { + if (!violations.optional().isPresent()) { + Exceptions.raiseIf(defaultConstraintViolationDisabled, ValidationException::new, + "Expected custom constraint violation(s)"); + + addError(getDefaultConstraintMessageTemplate(), frame.context.getPath()); + } + return violations.get(); + } + + @Override + public ConstraintDescriptor<?> getConstraintDescriptor() { + return constraint; + } + + @Override + public Object getValidatedValue() { + return frame.context.getValue(); + } +} http://git-wip-us.apache.org/repos/asf/bval/blob/a921963f/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateBean.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateBean.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateBean.java new file mode 100644 index 0000000..dc3fab5 --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateBean.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.bval.jsr.job; + +import javax.validation.Path; + +import org.apache.bval.jsr.ApacheFactoryContext; +import org.apache.bval.jsr.ConstraintViolationImpl; +import org.apache.bval.jsr.GraphContext; +import org.apache.bval.jsr.descriptor.BeanD; +import org.apache.bval.jsr.descriptor.ConstraintD; +import org.apache.bval.jsr.util.PathImpl; +import org.apache.bval.util.Validate; + +public final class ValidateBean<T> extends ValidationJob<T> { + + private final T bean; + + ValidateBean(ApacheFactoryContext validatorContext, T bean, Class<?>[] groups) { + super(validatorContext, groups); + this.bean = Validate.notNull(bean, IllegalArgumentException::new, "bean"); + } + + @Override + protected Frame<BeanD> computeBaseFrame() { + return new BeanFrame(new GraphContext(validatorContext, PathImpl.create(), bean)); + } + + @SuppressWarnings("unchecked") + @Override + protected Class<T> getRootBeanClass() { + return (Class<T>) bean.getClass(); + } + + @Override + ConstraintViolationImpl<T> createViolation(String messageTemplate, ConstraintValidatorContextImpl<T> context, + Path propertyPath) { + final String message = validatorContext.getMessageInterpolator().interpolate(messageTemplate, context); + + return new ConstraintViolationImpl<>(messageTemplate, message, bean, context.getFrame().getBean(), propertyPath, + context.getFrame().context.getValue(), context.getConstraintDescriptor(), getRootBeanClass(), + context.getConstraintDescriptor().unwrap(ConstraintD.class).getDeclaredOn(), null, null); + } +} http://git-wip-us.apache.org/repos/asf/bval/blob/a921963f/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateParameters.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateParameters.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateParameters.java new file mode 100644 index 0000000..c0d866b --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateParameters.java @@ -0,0 +1,188 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.bval.jsr.job; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.IntStream; + +import javax.validation.ConstraintViolation; +import javax.validation.ParameterNameProvider; +import javax.validation.Path; +import javax.validation.metadata.ExecutableDescriptor; + +import org.apache.bval.jsr.ApacheFactoryContext; +import org.apache.bval.jsr.ConstraintViolationImpl; +import org.apache.bval.jsr.GraphContext; +import org.apache.bval.jsr.descriptor.ConstraintD; +import org.apache.bval.jsr.descriptor.CrossParameterD; +import org.apache.bval.jsr.descriptor.ParameterD; +import org.apache.bval.jsr.util.NodeImpl; +import org.apache.bval.jsr.util.PathImpl; +import org.apache.bval.util.Exceptions; +import org.apache.bval.util.Lazy; +import org.apache.bval.util.Validate; +import org.apache.bval.util.reflection.TypeUtils; + +public abstract class ValidateParameters<E extends Executable, T> extends ValidationJob<T> { + + public static class ForMethod<T> extends ValidateParameters<Method, T> { + + private final T object; + + ForMethod(ApacheFactoryContext validatorContext, T object, Method executable, Object[] parameterValues, + Class<?>[] groups) { + super(validatorContext, object, executable, parameterValues, groups); + this.object = Validate.notNull(object, IllegalArgumentException::new, "object"); + } + + @Override + protected ExecutableDescriptor describe() { + return validatorContext.getDescriptorManager().getBeanDescriptor(object.getClass()) + .getConstraintsForMethod(executable.getName(), executable.getParameterTypes()); + } + + @SuppressWarnings("unchecked") + @Override + protected Class<T> getRootBeanClass() { + return (Class<T>) object.getClass(); + } + + @Override + protected List<String> getParameterNames(ParameterNameProvider parameterNameProvider) { + return parameterNameProvider.getParameterNames(executable); + } + + @Override + protected T getRootBean() { + return object; + } + } + + public static class ForConstructor<T> extends ValidateParameters<Constructor<? extends T>, T> { + + ForConstructor(ApacheFactoryContext validatorContext, Constructor<? extends T> executable, + Object[] parameterValues, Class<?>[] groups) { + super(validatorContext, null, executable, parameterValues, groups); + } + + @Override + protected ExecutableDescriptor describe() { + return validatorContext.getDescriptorManager().getBeanDescriptor(executable.getDeclaringClass()) + .getConstraintsForConstructor(executable.getParameterTypes()); + } + + @SuppressWarnings("unchecked") + @Override + protected Class<T> getRootBeanClass() { + return (Class<T>) executable.getDeclaringClass(); + } + + @Override + protected List<String> getParameterNames(ParameterNameProvider parameterNameProvider) { + return parameterNameProvider.getParameterNames(executable); + } + + @Override + protected T getRootBean() { + return null; + } + } + + class ParametersFrame extends Frame<CrossParameterD<?, ?>> { + private final ExecutableDescriptor executableDescriptor; + + protected ParametersFrame(ExecutableDescriptor executableDescriptor, GraphContext context) { + super(null, (CrossParameterD<?, ?>) Validate.notNull(executableDescriptor, "executableDescriptor") + .getCrossParameterDescriptor(), context); + this.executableDescriptor = executableDescriptor; + } + + @Override + void recurse(Class<?> group, Consumer<ConstraintViolation<T>> sink) { + executableDescriptor.getParameterDescriptors().stream() + .map(pd -> new SproutFrame<ParameterD<?>>(this, (ParameterD<?>) pd, parameter(pd.getIndex()))) + .forEach(f -> f.process(group, sink)); + } + + @Override + Object getBean() { + return object; + } + } + + private static final String PARAMETERS_DO_NOT_MATCH = "Parameters do not match"; + + protected final T object; + protected final E executable; + protected final Lazy<List<String>> parameterNames = + new Lazy<>(() -> getParameterNames(validatorContext.getParameterNameProvider())); + + private final Object[] parameterValues; + + ValidateParameters(ApacheFactoryContext validatorContext, T object, E executable, Object[] parameterValues, + Class<?>[] groups) { + super(validatorContext, groups); + this.object = object; + this.executable = Validate.notNull(executable, IllegalArgumentException::new, "executable"); + this.parameterValues = + Validate.notNull(parameterValues, IllegalArgumentException::new, "parameterValues").clone(); + + final Type[] genericParameterTypes = executable.getGenericParameterTypes(); + Exceptions.raiseUnless(parameterValues.length == genericParameterTypes.length, IllegalArgumentException::new, + PARAMETERS_DO_NOT_MATCH); + IntStream.range(0, genericParameterTypes.length) + .forEach(n -> Exceptions.raiseUnless(TypeUtils.isInstance(parameterValues[n], genericParameterTypes[n]), + IllegalArgumentException::new, PARAMETERS_DO_NOT_MATCH)); + } + + @Override + protected Frame<?> computeBaseFrame() { + final PathImpl cp = PathImpl.create(); + cp.addNode(new NodeImpl.CrossParameterNodeImpl()); + return new ParametersFrame(describe(), new GraphContext(validatorContext, cp, parameterValues)); + } + + protected abstract ExecutableDescriptor describe(); + + protected abstract List<String> getParameterNames(ParameterNameProvider parameterNameProvider); + + protected abstract T getRootBean(); + + @Override + ConstraintViolationImpl<T> createViolation(String messageTemplate, ConstraintValidatorContextImpl<T> context, + Path propertyPath) { + + final String message = validatorContext.getMessageInterpolator().interpolate(messageTemplate, context); + + return new ConstraintViolationImpl<T>(messageTemplate, message, getRootBean(), context.getFrame().getBean(), + propertyPath, context.getFrame().context.getValue(), context.getConstraintDescriptor(), getRootBeanClass(), + context.getConstraintDescriptor().unwrap(ConstraintD.class).getDeclaredOn(), null, parameterValues); + } + + private GraphContext parameter(int i) { + final PathImpl path = PathImpl.create(); + path.addNode(new NodeImpl.ParameterNodeImpl(parameterNames.get().get(i), i)); + return new GraphContext(validatorContext, path, parameterValues[i]); + } +} http://git-wip-us.apache.org/repos/asf/bval/blob/a921963f/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateProperty.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateProperty.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateProperty.java new file mode 100644 index 0000000..a8fbdbc --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateProperty.java @@ -0,0 +1,522 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.bval.jsr.job; + +import java.lang.reflect.Array; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import javax.validation.ConstraintViolation; +import javax.validation.Path; +import javax.validation.metadata.BeanDescriptor; +import javax.validation.metadata.CascadableDescriptor; +import javax.validation.metadata.ContainerDescriptor; +import javax.validation.metadata.ContainerElementTypeDescriptor; +import javax.validation.metadata.ElementDescriptor; +import javax.validation.metadata.PropertyDescriptor; +import javax.validation.valueextraction.ValueExtractor; +import javax.validation.valueextraction.ValueExtractor.ValueReceiver; + +import org.apache.bval.jsr.ApacheFactoryContext; +import org.apache.bval.jsr.ConstraintViolationImpl; +import org.apache.bval.jsr.GraphContext; +import org.apache.bval.jsr.descriptor.BeanD; +import org.apache.bval.jsr.descriptor.CascadableContainerD; +import org.apache.bval.jsr.descriptor.ComposedD; +import org.apache.bval.jsr.descriptor.ConstraintD; +import org.apache.bval.jsr.descriptor.ContainerElementTypeD; +import org.apache.bval.jsr.descriptor.ElementD; +import org.apache.bval.jsr.descriptor.PropertyD; +import org.apache.bval.jsr.metadata.ContainerElementKey; +import org.apache.bval.jsr.util.PathImpl; +import org.apache.bval.jsr.util.PathNavigation; +import org.apache.bval.util.Exceptions; +import org.apache.bval.util.ObjectWrapper; +import org.apache.bval.util.StringUtils; +import org.apache.bval.util.Validate; +import org.apache.bval.util.reflection.TypeUtils; + +public final class ValidateProperty<T> extends ValidationJob<T> { + + interface Strategy<T> { + default PathNavigation.Callback<?> callback(PathImpl.Builder pathBuilder, FindDescriptor findDescriptor) { + return new PathNavigation.CompositeCallbackProcedure(Arrays.asList(pathBuilder, findDescriptor)); + } + + default T getRootBean() { + return null; + } + + ValidateProperty<T>.Frame<?> frame(ValidateProperty<T> job, PathImpl path); + } + + static class ForBeanProperty<T> implements Strategy<T> { + final ApacheFactoryContext validatorContext; + final T rootBean; + final GraphContext rootContext; + final ObjectWrapper<GraphContext> leafContext; + final ObjectWrapper<Object> value; + + ForBeanProperty(ApacheFactoryContext validatorContext, T bean) { + super(); + this.validatorContext = validatorContext; + this.rootBean = bean; + this.rootContext = new GraphContext(validatorContext, PathImpl.create(), bean); + this.leafContext = new ObjectWrapper<>(rootContext); + this.value = new ObjectWrapper<>(bean); + } + + @Override + public PathNavigation.Callback<?> callback(PathImpl.Builder pathBuilder, FindDescriptor findDescriptor) { + return new WalkGraph(validatorContext, pathBuilder, findDescriptor, value, + (p, v) -> leafContext.accept(p.isRootPath() ? rootContext : rootContext.child(p, v))); + } + + @Override + public T getRootBean() { + return rootBean; + } + + public GraphContext baseContext(PathImpl path, ApacheFactoryContext validatorContext) { + return new GraphContext(validatorContext, PathImpl.create(), rootBean).child(path, value.get()); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public ValidationJob<T>.Frame<?> frame(ValidateProperty<T> job, PathImpl path) { + if (job.descriptor instanceof BeanDescriptor) { + return job.new LeafFrame(leafContext.get()); + } + return job.new PropertyFrame(job.new BeanFrame(leafContext.get()), job.descriptor, + leafContext.get().child(path, value.get())); + } + } + + static class ForPropertyValue<T> implements Strategy<T> { + final Object value; + + ForPropertyValue(Object value) { + super(); + this.value = value; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public ValidationJob<T>.Frame<?> frame(ValidateProperty<T> job, PathImpl path) { + final GraphContext context = new GraphContext(job.validatorContext, path, value); + if (job.descriptor instanceof BeanDescriptor) { + return job.new LeafFrame(context); + } + return job.new PropertyFrame(null, job.descriptor, context); + } + } + + private interface Step { + Type type(); + + ElementD<?, ?> element(); + } + + private static class DescriptorWrapper implements Step { + final ElementD<?, ?> wrapped; + + DescriptorWrapper(ElementDescriptor wrapped) { + super(); + this.wrapped = (ElementD<?, ?>) wrapped; + } + + @Override + public Type type() { + return wrapped.getGenericType(); + } + + @Override + public ElementD<?, ?> element() { + return wrapped; + } + } + + private static class TypeWrapper implements Step { + final ApacheFactoryContext validatorContext; + final Type type; + + TypeWrapper(ApacheFactoryContext validatorContext, Type type) { + super(); + this.validatorContext = validatorContext; + this.type = type; + } + + @Override + public Type type() { + return type; + } + + @Override + public ElementD<?, ?> element() { + final Class<?> beanClass = TypeUtils.getRawType(type, null); + return beanClass == null ? null + : (BeanD) validatorContext.getDescriptorManager().getBeanDescriptor(beanClass); + } + } + + private static class FindDescriptor implements PathNavigation.Callback<ElementD<?, ?>> { + private final ApacheFactoryContext validatorContext; + Step current; + + FindDescriptor(ApacheFactoryContext validatorContext, Class<?> beanClass) { + this.validatorContext = validatorContext; + this.current = new DescriptorWrapper(validatorContext.getDescriptorManager().getBeanDescriptor(beanClass)); + } + + @Override + public void handleProperty(String name) { + final ElementDescriptor element = current.element(); + final BeanD bean; + if (element instanceof BeanD) { + bean = (BeanD) element; + } else { + bean = (BeanD) validatorContext.getDescriptorManager().getBeanDescriptor(element.getElementClass()); + } + final PropertyDescriptor property = bean.getProperty(name); + Exceptions.raiseIf(property == null, IllegalArgumentException::new, "Unknown property %s of %s", name, + bean.getElementClass()); + current = new DescriptorWrapper(property); + } + + @Override + public void handleIndexOrKey(String value) { + handleGenericInIterable(); + } + + @Override + public void handleGenericInIterable() { + final ElementDescriptor desc = current.element(); + if (desc instanceof CascadableContainerD<?, ?>) { + final Step containerElement = handleContainerElement((CascadableContainerD<?, ?>) desc); + if (containerElement != null) { + current = containerElement; + return; + } + } + current = handleElementByType(current.type()); + } + + private Step handleContainerElement(CascadableContainerD<?, ?> desc) { + final Set<ContainerElementTypeDescriptor> containerElements = desc.getConstrainedContainerElementTypes(); + if (containerElements.isEmpty()) { + return null; + } + final ContainerElementTypeDescriptor element; + if (containerElements.size() == 1) { + element = containerElements.iterator().next(); + } else { + final Predicate<ContainerElementKey> wellKnown = + k -> k.represents(MAP_VALUE) || k.represents(ITERABLE_ELEMENT); + + final Optional<ContainerElementTypeD> found = + containerElements.stream().map(ContainerElementTypeD.class::cast) + .filter(d -> d.getKey().getAssignableKeys().stream().anyMatch(wellKnown)).findFirst(); + + if (!found.isPresent()) { + return null; + } + element = found.get(); + } + return new DescriptorWrapper(element); + } + + private Step handleElementByType(Type type) { + Type elementType; + + if (TypeUtils.isArrayType(type)) { + elementType = TypeUtils.getArrayComponentType(type); + } else if (TypeUtils.isAssignable(type, Map.class)) { + elementType = + Optional.ofNullable(TypeUtils.getTypeArguments(type, Map.class).get(MAP_VALUE)).orElse(MAP_VALUE); + } else if (TypeUtils.isAssignable(type, Iterable.class)) { + elementType = + Optional.ofNullable(TypeUtils.getTypeArguments(type, Iterable.class).get(ITERABLE_ELEMENT)) + .orElse(ITERABLE_ELEMENT); + } else { + elementType = null; + } + Exceptions.raiseIf(elementType == null, IllegalArgumentException::new, + "Unable to resolve element type of %s", type); + + return new TypeWrapper(validatorContext, elementType); + } + + @Override + public ElementD<?, ?> result() { + return current.element(); + } + } + + private static class WalkGraph extends PathNavigation.CallbackProcedure { + final ApacheFactoryContext validatorContext; + final PathImpl.Builder pathBuilder; + final FindDescriptor findDescriptor; + final ObjectWrapper<Object> value; + final BiConsumer<PathImpl, Object> recordLeaf; + + WalkGraph(ApacheFactoryContext validatorContext, PathImpl.Builder pathBuilder, FindDescriptor findDescriptor, + ObjectWrapper<Object> value, BiConsumer<PathImpl, Object> recordLeaf) { + this.validatorContext = validatorContext; + this.pathBuilder = pathBuilder; + this.findDescriptor = findDescriptor; + this.value = value; + this.recordLeaf = recordLeaf; + } + + @Override + public void handleProperty(String name) { + final PathImpl p = PathImpl.copy(pathBuilder.result()); + pathBuilder.handleProperty(name); + if (value.optional().isPresent()) { + recordLeaf.accept(p, value.get()); + + findDescriptor.handleProperty(name); + + final PropertyD<?> propertyD = + ComposedD.unwrap(findDescriptor.current.element(), PropertyD.class).findFirst().get(); + try { + value.accept(propertyD.getValue(value.get())); + } catch (Exception e) { + Exceptions.raise(IllegalStateException::new, e, "Unable to get value of property %s", + propertyD.getPropertyName()); + } + } + } + + @Override + public void handleIndexOrKey(final String indexOrKey) { + pathBuilder.handleIndexOrKey(indexOrKey); + findDescriptor.handleIndexOrKey(indexOrKey); + if (value.optional().isPresent()) { + ElementDescriptor element = findDescriptor.current.element(); + if (element instanceof ContainerElementTypeD) { + value.accept(handleContainer(value.get(), ((ContainerElementTypeD) element).getKey(), indexOrKey)); + } else { + value.accept(handleBasic(value.get(), indexOrKey)); + + if (element == null && value.optional().isPresent()) { + // no generic info available at some previous index level; fall back to runtime type of value + // and repair structure of findDescriptor: + findDescriptor.current = new TypeWrapper(validatorContext, value.get().getClass()); + element = findDescriptor.current.element(); + } + if (element instanceof BeanDescriptor) { + recordLeaf.accept(PathImpl.copy(pathBuilder.result()), value.get()); + } + } + } + } + + @SuppressWarnings("unchecked") + private Object handleContainer(Object o, ContainerElementKey key, String indexOrKey) { + @SuppressWarnings("rawtypes") + final ValueExtractor valueExtractor = validatorContext.getValueExtractors().find(key); + + final ObjectWrapper<Object> result = new ObjectWrapper<>(); + valueExtractor.extractValues(o, new ValueReceiver() { + + @Override + public void indexedValue(String nodeName, int index, Object object) { + if (Integer.toString(index).equals(indexOrKey)) { + result.accept(object); + } + } + + @Override + public void iterableValue(String nodeName, Object object) { + // ? + result.accept(object); + } + + @Override + public void keyedValue(String nodeName, Object key, Object object) { + if (String.valueOf(key).equals(indexOrKey)) { + result.accept(object); + } + } + + @Override + public void value(String nodeName, Object object) { + // ? + result.accept(object); + } + }); + return result.get(); + } + + private Object handleBasic(Object o, String indexOrKey) { + if (Map.class.isInstance(o)) { + for (Map.Entry<?, ?> e : ((Map<?, ?>) o).entrySet()) { + if (String.valueOf(e.getKey()).equals(indexOrKey)) { + return e.getValue(); + } + } + } else { + try { + final int index = Integer.parseInt(indexOrKey); + Exceptions.raiseIf(index < 0, IllegalArgumentException::new, "Invalid index %d", index); + if (o != null && TypeUtils.isArrayType(o.getClass())) { + if (Array.getLength(o) > index) { + return Array.get(o, index); + } + } else if (List.class.isInstance(o)) { + final List<?> l = (List<?>) o; + if (l.size() > index) { + return l.get(index); + } + } else if (Iterable.class.isInstance(o)) { + int i = -1; + for (Object e : (Iterable<?>) o) { + if (++i == index) { + return e; + } + } + } + } catch (NumberFormatException e) { + } + } + return null; + } + + @Override + public void handleGenericInIterable() { + throw new UnsupportedOperationException("Cannot resolve generic inIterable against actual object graph"); + } + } + + class LeafFrame extends BeanFrame { + + LeafFrame(GraphContext context) { + super(context); + } + + @Override + protected ValidationJob<T>.Frame<?> propertyFrame(PropertyD<?> d, GraphContext context) { + return new PropertyFrame<>(this, d, context); + } + } + + class PropertyFrame<D extends ElementD<?, ?> & CascadableDescriptor & ContainerDescriptor> extends SproutFrame<D> { + + PropertyFrame(ValidationJob<T>.Frame<?> parent, D descriptor, GraphContext context) { + super(parent, descriptor, context); + } + + @Override + void recurse(Class<?> group, Consumer<ConstraintViolation<T>> sink) { + if (cascade) { + super.recurse(group, sink); + } + } + } + + private static final TypeVariable<?> MAP_VALUE = Map.class.getTypeParameters()[1]; + private static final TypeVariable<?> ITERABLE_ELEMENT = Iterable.class.getTypeParameters()[0]; + + private final Strategy<T> strategy; + private final Class<T> rootBeanClass; + private final PathImpl propertyPath; + private final T rootBean; + private ElementD<?, ?> descriptor; + private boolean cascade; + + private ValidateProperty(Strategy<T> strategy, ApacheFactoryContext validatorContext, Class<T> rootBeanClass, + String property, Class<?>[] groups) { + super(validatorContext, groups); + + Exceptions.raiseIf(StringUtils.isBlank(property), IllegalArgumentException::new, + "property cannot be null/empty/blank"); + + this.strategy = strategy; + this.rootBeanClass = Validate.notNull(rootBeanClass, IllegalArgumentException::new, "rootBeanClass"); + + final PathImpl.Builder pathBuilder = new PathImpl.Builder(); + final FindDescriptor findDescriptor = new FindDescriptor(validatorContext, rootBeanClass); + + PathNavigation.navigate(property, strategy.callback(pathBuilder, findDescriptor)); + + this.propertyPath = pathBuilder.result(); + this.descriptor = findDescriptor.result(); + this.rootBean = strategy.getRootBean(); + } + + ValidateProperty(ApacheFactoryContext validatorContext, Class<T> rootBeanClass, String property, Object value, + Class<?>[] groups) { + this(new ForPropertyValue<>(value), validatorContext, rootBeanClass, property, groups); + if (descriptor == null) { + // should only occur when the root class is raw + descriptor = (ElementD<?, ?>) validatorContext.getDescriptorManager() + .getBeanDescriptor(value == null ? Object.class : value.getClass()); + } else { + final Class<?> propertyType = descriptor.getElementClass(); + Exceptions.raiseUnless(TypeUtils.isInstance(value, propertyType), IllegalArgumentException::new, + "%s is not an instance of %s", value, propertyType); + } + } + + @SuppressWarnings("unchecked") + ValidateProperty(ApacheFactoryContext validatorContext, T bean, String property, Class<?>[] groups) + throws Exception { + this(new ForBeanProperty<>(validatorContext, bean), validatorContext, + (Class<T>) Validate.notNull(bean, IllegalArgumentException::new, "bean").getClass(), property, groups); + + Exceptions.raiseIf(descriptor == null, IllegalArgumentException::new, + "Could not resolve property name/path: %s", property); + } + + public ValidateProperty<T> cascade(boolean cascade) { + this.cascade = cascade; + return this; + } + + @Override + protected Frame<?> computeBaseFrame() { + // TODO assign bean as its own property and figure out what to do + + return strategy.frame(this, propertyPath); + } + + @Override + protected Class<T> getRootBeanClass() { + return rootBeanClass; + } + + @Override + ConstraintViolationImpl<T> createViolation(String messageTemplate, ConstraintValidatorContextImpl<T> context, + Path propertyPath) { + final String message = validatorContext.getMessageInterpolator().interpolate(messageTemplate, context); + + return new ConstraintViolationImpl<>(messageTemplate, message, rootBean, context.getFrame().getBean(), + propertyPath, context.getFrame().context.getValue(), context.getConstraintDescriptor(), rootBeanClass, + context.getConstraintDescriptor().unwrap(ConstraintD.class).getDeclaredOn(), null, null); + } +} http://git-wip-us.apache.org/repos/asf/bval/blob/a921963f/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateReturnValue.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateReturnValue.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateReturnValue.java new file mode 100644 index 0000000..e71e7ae --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateReturnValue.java @@ -0,0 +1,126 @@ +package org.apache.bval.jsr.job; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Type; + +import javax.validation.Path; + +import org.apache.bval.jsr.ApacheFactoryContext; +import org.apache.bval.jsr.ConstraintViolationImpl; +import org.apache.bval.jsr.GraphContext; +import org.apache.bval.jsr.descriptor.ConstraintD; +import org.apache.bval.jsr.descriptor.ExecutableD; +import org.apache.bval.jsr.descriptor.ReturnValueD; +import org.apache.bval.jsr.metadata.Metas; +import org.apache.bval.jsr.util.NodeImpl; +import org.apache.bval.jsr.util.PathImpl; +import org.apache.bval.util.Exceptions; +import org.apache.bval.util.Validate; +import org.apache.bval.util.reflection.TypeUtils; + +public abstract class ValidateReturnValue<E extends Executable, T> extends ValidationJob<T> { + public static class ForMethod<T> extends ValidateReturnValue<Method, T> { + private final T object; + + ForMethod(ApacheFactoryContext validatorContext, T object, Method method, Object returnValue, + Class<?>[] groups) { + super(validatorContext, + new Metas.ForMethod(Validate.notNull(method, IllegalArgumentException::new, "method")), returnValue, + groups); + this.object = Validate.notNull(object, IllegalArgumentException::new, "object"); + } + + @Override + protected T getRootBean() { + return object; + } + + @SuppressWarnings("unchecked") + @Override + protected Class<T> getRootBeanClass() { + return (Class<T>) object.getClass(); + } + + @SuppressWarnings("unchecked") + @Override + protected ExecutableD<Method, ?, ?> describe() { + return (ExecutableD<Method, ?, ?>) validatorContext.getDescriptorManager() + .getBeanDescriptor(object.getClass()) + .getConstraintsForMethod(executable.getName(), executable.getParameterTypes()); + } + } + + public static class ForConstructor<T> extends ValidateReturnValue<Constructor<?>, T> { + + ForConstructor(ApacheFactoryContext validatorContext, Constructor<? extends T> ctor, Object returnValue, + Class<?>[] groups) { + super(validatorContext, + new Metas.ForConstructor(Validate.notNull(ctor, IllegalArgumentException::new, "ctor")), returnValue, + groups); + } + + @Override + protected T getRootBean() { + return null; + } + + @SuppressWarnings("unchecked") + @Override + protected Class<T> getRootBeanClass() { + return (Class<T>) executable.getDeclaringClass(); + } + + @SuppressWarnings("unchecked") + @Override + protected ExecutableD<Constructor<T>, ?, ?> describe() { + return (ExecutableD<Constructor<T>, ?, ?>) validatorContext.getDescriptorManager() + .getBeanDescriptor(executable.getDeclaringClass()) + .getConstraintsForConstructor(executable.getParameterTypes()); + } + } + + protected final E executable; + private final Object returnValue; + + ValidateReturnValue(ApacheFactoryContext validatorContext, Metas<E> meta, Object returnValue, Class<?>[] groups) { + super(validatorContext, groups); + + final Type type = Validate.notNull(meta, "meta").getType(); + Exceptions.raiseUnless(TypeUtils.isInstance(returnValue, type), IllegalArgumentException::new, + "%s is not an instance of %s", returnValue, type); + + this.executable = meta.getHost(); + this.returnValue = returnValue; + } + + @Override + protected Frame<?> computeBaseFrame() { + final PathImpl path = PathImpl.create(); + path.addNode(new NodeImpl.ReturnValueNodeImpl()); + + return new SproutFrame<ReturnValueD<?, ?>>((ReturnValueD<?, ?>) describe().getReturnValueDescriptor(), + new GraphContext(validatorContext, path, returnValue)) { + @Override + Object getBean() { + return getRootBean(); + } + }; + } + + @Override + ConstraintViolationImpl<T> createViolation(String messageTemplate, ConstraintValidatorContextImpl<T> context, + Path propertyPath) { + + final String message = validatorContext.getMessageInterpolator().interpolate(messageTemplate, context); + + return new ConstraintViolationImpl<>(messageTemplate, message, getRootBean(), context.getFrame().getBean(), + propertyPath, context.getFrame().context.getValue(), context.getConstraintDescriptor(), getRootBeanClass(), + context.getConstraintDescriptor().unwrap(ConstraintD.class).getDeclaredOn(), returnValue, null); + } + + protected abstract ExecutableD<?, ?, ?> describe(); + + protected abstract T getRootBean(); +} http://git-wip-us.apache.org/repos/asf/bval/blob/a921963f/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java new file mode 100644 index 0000000..9221184 --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java @@ -0,0 +1,380 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.bval.jsr.job; + +import java.lang.reflect.Array; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintViolation; +import javax.validation.Path; +import javax.validation.TraversableResolver; +import javax.validation.UnexpectedTypeException; +import javax.validation.ValidationException; +import javax.validation.groups.Default; +import javax.validation.metadata.CascadableDescriptor; +import javax.validation.metadata.ContainerDescriptor; +import javax.validation.metadata.ElementDescriptor.ConstraintFinder; +import javax.validation.metadata.PropertyDescriptor; + +import org.apache.bval.jsr.ApacheFactoryContext; +import org.apache.bval.jsr.ConstraintViolationImpl; +import org.apache.bval.jsr.GraphContext; +import org.apache.bval.jsr.descriptor.BeanD; +import org.apache.bval.jsr.descriptor.CascadableContainerD; +import org.apache.bval.jsr.descriptor.ComposedD; +import org.apache.bval.jsr.descriptor.ConstraintD; +import org.apache.bval.jsr.descriptor.ElementD; +import org.apache.bval.jsr.descriptor.PropertyD; +import org.apache.bval.jsr.groups.Group; +import org.apache.bval.jsr.groups.Groups; +import org.apache.bval.jsr.util.NodeImpl; +import org.apache.bval.jsr.util.PathImpl; +import org.apache.bval.util.Exceptions; +import org.apache.bval.util.Lazy; +import org.apache.bval.util.Validate; + +public abstract class ValidationJob<T> { + + public abstract class Frame<D extends ElementD<?, ?>> { + protected final Frame<?> parent; + protected final D descriptor; + protected final GraphContext context; + + protected Frame(Frame<?> parent, D descriptor, GraphContext context) { + super(); + this.parent = parent; + this.descriptor = Validate.notNull(descriptor, "descriptor"); + this.context = Validate.notNull(context, "context"); + } + + final ValidationJob<T> getJob() { + return ValidationJob.this; + } + + final void process(Class<?> group, Consumer<ConstraintViolation<T>> sink) { + Validate.notNull(sink, "sink"); + + each(expand(group), this::validateDescriptorConstraints, sink); + recurse(group, sink); + } + + abstract void recurse(Class<?> group, Consumer<ConstraintViolation<T>> sink); + + abstract Object getBean(); + + protected void validateDescriptorConstraints(Class<?> group, Consumer<ConstraintViolation<T>> sink) { + constraintsFrom(descriptor.findConstraints().unorderedAndMatchingGroups(group)) + .forEach(c -> validate(c, sink)); + } + + @SuppressWarnings("unchecked") + private Stream<ConstraintD<?>> constraintsFrom(ConstraintFinder finder) { + // our ConstraintFinder implementation is a Stream supplier; reference without exposing it beyond its + // package: + if (finder instanceof Supplier<?>) { + return (Stream<ConstraintD<?>>) ((Supplier<?>) finder).get(); + } + return finder.getConstraintDescriptors().stream().map(ConstraintD.class::cast); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private boolean validate(ConstraintD<?> constraint, Consumer<ConstraintViolation<T>> sink) { + if (!validatedPathsByConstraint + .computeIfAbsent(constraint, k -> new ConcurrentSkipListSet<>(COMPARE_TO_STRING)) + .add(context.getPath())) { + // seen, ignore: + return true; + } + final ConstraintValidatorContextImpl<T> constraintValidatorContext = + new ConstraintValidatorContextImpl<>(this, constraint); + + final ConstraintValidator constraintValidator = getConstraintValidator(constraint); + + final boolean valid; + if (constraintValidator == null) { + // null validator without exception implies composition: + valid = true; + } else { + constraintValidator.initialize(constraint.getAnnotation()); + valid = constraintValidator.isValid(context.getValue(), constraintValidatorContext); + } + if (!valid) { + constraintValidatorContext.getRequiredViolations().forEach(sink); + } + if (valid || !constraint.isReportAsSingleViolation()) { + final boolean compositionValid = validateComposed(constraint, sink); + + if (!compositionValid) { + if (valid && constraint.isReportAsSingleViolation()) { + constraintValidatorContext.getRequiredViolations().forEach(sink); + } + return false; + } + } + return valid; + } + + private boolean validateComposed(ConstraintD<?> constraint, Consumer<ConstraintViolation<T>> sink) { + if (constraint.getComposingConstraints().isEmpty()) { + return true; + } + final Consumer<ConstraintViolation<T>> effectiveSink = constraint.isReportAsSingleViolation() ? cv -> { + } : sink; + + // collect validation results to set of Boolean, ensuring all are evaluated: + final Set<Boolean> results = constraint.getComposingConstraints().stream().map(ConstraintD.class::cast) + .map(c -> validate(c, effectiveSink)).collect(Collectors.toSet()); + + return Collections.singleton(Boolean.TRUE).equals(results); + } + + @SuppressWarnings({ "rawtypes" }) + private ConstraintValidator getConstraintValidator(ConstraintD<?> constraint) { + final Class<? extends ConstraintValidator> constraintValidatorClass = + constraint.getConstraintValidatorClass(); + + if (constraintValidatorClass == null) { + Exceptions.raiseIf(constraint.getComposingConstraints().isEmpty(), UnexpectedTypeException::new, + "No %s type located for non-composed constraint %s", ConstraintValidator.class.getSimpleName(), + constraint); + return null; + } + ConstraintValidator constraintValidator = null; + Exception cause = null; + try { + constraintValidator = + validatorContext.getConstraintValidatorFactory().getInstance(constraintValidatorClass); + } catch (Exception e) { + cause = e; + } + Exceptions.raiseIf(constraintValidator == null, ValidationException::new, cause, + "Unable to get %s instance from %s", constraintValidatorClass.getName(), + validatorContext.getConstraintValidatorFactory()); + + return constraintValidator; + } + + protected Stream<Class<?>> expand(Class<?> group) { + if (Default.class.equals(group)) { + final List<Class<?>> groupSequence = descriptor.getGroupSequence(); + if (groupSequence != null) { + return groupSequence.stream(); + } + } + return Stream.of(group); + } + } + + public class BeanFrame extends Frame<BeanD> { + + BeanFrame(GraphContext context) { + this(null, context); + } + + BeanFrame(Frame<?> parent, GraphContext context) { + super(parent, getBeanDescriptor(context.getValue()), context); + } + + @Override + void recurse(Class<?> group, Consumer<ConstraintViolation<T>> sink) { + // bean frame has to do some convoluted things to properly handle groups and recursion; skipping + // frame#process() on properties: + final List<Frame<?>> propertyFrames = propertyFrames(); + + each(expand(group), (g, s) -> propertyFrames.forEach(f -> f.validateDescriptorConstraints(g, s)), sink); + propertyFrames.forEach(f -> f.recurse(group, sink)); + } + + protected Frame<?> propertyFrame(PropertyD<?> d, GraphContext context) { + return new SproutFrame<>(this, d, context); + } + + @Override + Object getBean() { + return context.getValue(); + } + + private List<Frame<?>> propertyFrames() { + final Stream<PropertyD<?>> properties = descriptor.getConstrainedProperties().stream() + .flatMap(d -> ComposedD.unwrap(d, PropertyD.class)).map(d -> (PropertyD<?>) d); + + final TraversableResolver traversableResolver = validatorContext.getTraversableResolver(); + + final Stream<PropertyD<?>> reachableProperties = + properties.filter(d -> traversableResolver.isReachable(context.getValue(), + new NodeImpl.PropertyNodeImpl(d.getPropertyName()), getRootBeanClass(), context.getPath(), + d.getElementType())); + + return reachableProperties.flatMap( + d -> d.read(context).filter(context -> !context.isRecursive()).map(child -> propertyFrame(d, child))) + .collect(Collectors.toList()); + } + } + + public class SproutFrame<D extends ElementD<?, ?> & CascadableDescriptor & ContainerDescriptor> extends Frame<D> { + + public SproutFrame(D descriptor, GraphContext context) { + this(null, descriptor, context); + } + + public SproutFrame(Frame<?> parent, D descriptor, GraphContext context) { + super(parent, descriptor, context); + } + + @Override + void recurse(Class<?> group, Consumer<ConstraintViolation<T>> sink) { + @SuppressWarnings({ "unchecked", "rawtypes" }) + final Stream<CascadableContainerD<?, ?>> containerElements = + descriptor.getConstrainedContainerElementTypes().stream() + .flatMap(d -> ComposedD.unwrap(d, (Class) CascadableContainerD.class)); + + containerElements.flatMap(d -> d.read(context).map(child -> new SproutFrame<>(this, d, child))) + .forEach(f -> f.process(group, sink)); + + if (!descriptor.isCascaded()) { + return; + } + if (descriptor instanceof PropertyDescriptor) { + final TraversableResolver traversableResolver = validatorContext.getTraversableResolver(); + + final PathImpl pathToTraversableObject = PathImpl.copy(context.getPath()); + final NodeImpl traversableProperty = pathToTraversableObject.removeLeafNode(); + + if (!traversableResolver.isCascadable(context.getValue(), traversableProperty, getRootBeanClass(), + pathToTraversableObject, ((PropertyD<?>) descriptor).getElementType())) { + return; + } + } + multiplex().filter(context -> context.getValue() != null).map(context -> new BeanFrame(this, context)) + .forEach(b -> b.process(group, sink)); + } + + private Stream<GraphContext> multiplex() { + final Object value = context.getValue(); + if (value == null) { + return Stream.empty(); + } + if (Map.class.isInstance(value)) { + return ((Map<?, ?>) value).entrySet().stream() + .map(e -> context.child(NodeImpl.atKey(e.getKey()), e.getValue())); + } + if (value.getClass().isArray()) { + return IntStream.range(0, Array.getLength(value)) + .mapToObj(i -> context.child(NodeImpl.atIndex(i), Array.get(value, i))); + } + if (List.class.isInstance(value)) { + final List<?> l = (List<?>) value; + return IntStream.range(0, l.size()).mapToObj(i -> context.child(NodeImpl.atIndex(i), l.get(i))); + } + if (Iterable.class.isInstance(value)) { + final Stream.Builder<Object> b = Stream.builder(); + ((Iterable<?>) value).forEach(b); + return b.build().map(o -> context.child(NodeImpl.atIndex(null), o)); + } + return Stream.of(context); + } + + @Override + Object getBean() { + return Optional.ofNullable(parent).map(Frame::getBean).orElse(null); + } + } + + private static final Comparator<Path> COMPARE_TO_STRING = Comparator.comparing(Object::toString); + + protected final ApacheFactoryContext validatorContext; + + private final Groups groups; + private final Lazy<Set<ConstraintViolation<T>>> results = new Lazy<>(LinkedHashSet::new); + + private ConcurrentMap<ConstraintD<?>, Set<Path>> validatedPathsByConstraint; + + ValidationJob(ApacheFactoryContext validatorContext, Class<?>[] groups) { + super(); + this.validatorContext = Validate.notNull(validatorContext, "validatorContext"); + this.groups = validatorContext.getGroupsComputer().computeGroups(groups); + } + + public final Set<ConstraintViolation<T>> getResults() { + if (results.optional().isPresent()) { + return results.get(); + } + final Frame<?> baseFrame = computeBaseFrame(); + Validate.validState(baseFrame != null, "%s computed null baseFrame", getClass().getName()); + + final Consumer<ConstraintViolation<T>> sink = results.consumer(Set::add); + + validatedPathsByConstraint = new ConcurrentHashMap<>(); + + try { + groups.getGroups().stream().map(Group::getGroup).forEach(g -> baseFrame.process(g, sink)); + + sequences: for (List<Group> seq : groups.getSequences()) { + final boolean proceed = each(seq.stream().map(Group::getGroup), baseFrame::process, sink); + if (!proceed) { + break sequences; + } + } + } finally { + validatedPathsByConstraint = null; + } + return results.optional().map(Collections::unmodifiableSet).orElse(Collections.emptySet()); + } + + private boolean each(Stream<Class<?>> groupSequence, BiConsumer<Class<?>, Consumer<ConstraintViolation<T>>> closure, + Consumer<ConstraintViolation<T>> sink) { + final Lazy<Set<ConstraintViolation<T>>> sequenceViolations = new Lazy<>(LinkedHashSet::new); + for (Class<?> g : (Iterable<Class<?>>) () -> groupSequence.iterator()) { + closure.accept(g, sequenceViolations.consumer(Set::add)); + if (sequenceViolations.optional().isPresent()) { + sequenceViolations.get().forEach(sink); + return false; + } + } + return true; + } + + private BeanD getBeanDescriptor(Object bean) { + return (BeanD) validatorContext.getFactory().getDescriptorManager() + .getBeanDescriptor(Validate.notNull(bean, "bean").getClass()); + } + + abstract ConstraintViolationImpl<T> createViolation(String messageTemplate, + ConstraintValidatorContextImpl<T> context, Path propertyPath); + + protected abstract Frame<?> computeBaseFrame(); + + protected abstract Class<T> getRootBeanClass(); +} http://git-wip-us.apache.org/repos/asf/bval/blob/a921963f/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJobFactory.java ---------------------------------------------------------------------- diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJobFactory.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJobFactory.java new file mode 100644 index 0000000..2a23192 --- /dev/null +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJobFactory.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.bval.jsr.job; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import javax.validation.ValidationException; +import javax.validation.Validator; +import javax.validation.executable.ExecutableValidator; + +import org.apache.bval.jsr.ApacheFactoryContext; +import org.apache.bval.util.Validate; + +/** + * Creates {@link ValidationJob} instances. + */ +public class ValidationJobFactory { + + private final ApacheFactoryContext validatorContext; + + /** + * Create a new {@link ValidationJobFactory}. + * + * @param validatorContext + */ + public ValidationJobFactory(ApacheFactoryContext validatorContext) { + super(); + this.validatorContext = Validate.notNull(validatorContext, "validatorContext"); + } + + /** + * @see Validator#validate(Object, Class...) + */ + public <T> ValidateBean<T> validateBean(T bean, Class<?>... groups) { + return new ValidateBean<>(validatorContext, bean, groups); + } + + /** + * @see Validator#validateProperty(Object, String, Class...) + */ + public <T> ValidateProperty<T> validateProperty(T bean, String property, Class<?>... groups) { + try { + @SuppressWarnings({ "unchecked", "rawtypes" }) + final ValidateProperty<T> result = new ValidateProperty(validatorContext, bean, property, groups); + return result; + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new ValidationException(e); + } + } + + /** + * @see Validator#validateValue(Class, String, Object, Class...) + */ + public <T> ValidateProperty<T> validateValue(Class<T> rootBeanClass, String property, Object value, + Class<?>... groups) { + @SuppressWarnings({ "unchecked", "rawtypes" }) + final ValidateProperty<T> result = + new ValidateProperty(validatorContext, rootBeanClass, property, value, groups); + return result; + } + + /** + * @see ExecutableValidator#validateParameters(Object, Method, Object[], Class...) + */ + public <T> ValidateParameters.ForMethod<T> validateParameters(T object, Method method, Object[] parameterValues, + Class<?>... groups) { + return new ValidateParameters.ForMethod<T>(validatorContext, object, method, parameterValues, groups); + } + + /** + * @see ExecutableValidator#validateReturnValue(Object, Method, Object, Class...) + */ + public <T> ValidateReturnValue.ForMethod<T> validateReturnValue(T object, Method method, Object returnValue, + Class<?>... groups) { + return new ValidateReturnValue.ForMethod<>(validatorContext, object, method, returnValue, groups); + } + + /** + * @see ExecutableValidator#validateConstructorParameters(Constructor, Object[], Class...) + */ + public <T> ValidateParameters.ForConstructor<T> validateConstructorParameters(Constructor<? extends T> constructor, + Object[] parameterValues, Class<?>... groups) { + return new ValidateParameters.ForConstructor<T>(validatorContext, constructor, parameterValues, groups); + } + + /** + * @see ExecutableValidator#validateConstructorReturnValue(Constructor, Object, Class...) + */ + public <T> ValidateReturnValue.ForConstructor<T> validateConstructorReturnValue( + Constructor<? extends T> constructor, T createdObject, Class<?>... groups) { + return new ValidateReturnValue.ForConstructor<T>(validatorContext, constructor, createdObject, groups); + } +}
