All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.bval.jsr303.ClassValidator Maven / Gradle / Ivy

The newest version!
/*
 * 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.jsr303;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.ValidationException;
import javax.validation.groups.Default;
import javax.validation.metadata.BeanDescriptor;

import org.apache.bval.DynamicMetaBean;
import org.apache.bval.MetaBeanFinder;
import org.apache.bval.jsr303.groups.Group;
import org.apache.bval.jsr303.groups.Groups;
import org.apache.bval.jsr303.groups.GroupsComputer;
import org.apache.bval.jsr303.util.ClassHelper;
import org.apache.bval.jsr303.util.NodeImpl;
import org.apache.bval.jsr303.util.PathImpl;
import org.apache.bval.jsr303.util.PathNavigation;
import org.apache.bval.jsr303.util.ValidationContextTraversal;
import org.apache.bval.model.Features;
import org.apache.bval.model.FeaturesCapable;
import org.apache.bval.model.MetaBean;
import org.apache.bval.model.MetaProperty;
import org.apache.bval.util.AccessStrategy;
import org.apache.bval.util.ValidationHelper;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.reflect.TypeUtils;

// TODO: centralize treatMapsLikeBeans

/**
 * Objects of this class are able to validate bean instances (and the associated object graphs).
 * 

* Implementation is thread-safe. *

* API class * * @version $Rev: 1166451 $ $Date: 2011-09-07 17:32:26 -0500 (Wed, 07 Sep 2011) $ * * @author Roman Stumm * @author Carlos Vara */ public class ClassValidator implements CascadingPropertyValidator { private static final Object VALIDATE_PROPERTY = new Object() { public String toString() { return "VALIDATE_PROPERTY"; } }; /** * {@link ApacheFactoryContext} used */ protected final ApacheFactoryContext factoryContext; /** * {@link GroupsComputer} used */ protected final GroupsComputer groupsComputer = new GroupsComputer(); /** * Create a new ClassValidator instance. * * @param factoryContext */ public ClassValidator(ApacheFactoryContext factoryContext) { this.factoryContext = factoryContext; } /** * Create a new ClassValidator instance. * * @param factory * @deprecated provided for backward compatibility */ public ClassValidator(ApacheValidatorFactory factory) { this(factory.usingContext()); } /** * Get the metabean finder associated with this validator. * * @return a MetaBeanFinder * @see org.apache.bval.MetaBeanManagerFactory#getFinder() */ protected MetaBeanFinder getMetaBeanFinder() { return factoryContext.getMetaBeanFinder(); } // Validator implementation // -------------------------------------------------- /** * {@inheritDoc} Validates all constraints on object. * * @param object object to validate * @param groups group or list of groups targeted for validation (default to * {@link javax.validation.groups.Default}) * @return constraint violations or an empty Set if none * @throws IllegalArgumentException if object is null or if null is passed to the varargs groups * @throws ValidationException if a non recoverable error happens during the validation * process */ // @Override - not allowed in 1.5 for Interface methods @SuppressWarnings("unchecked") public Set> validate(T object, Class... groups) { if (object == null) throw new IllegalArgumentException("cannot validate null"); checkGroups(groups); try { Class objectClass = (Class) object.getClass(); MetaBean objectMetaBean = getMetaBeanFinder().findForClass(objectClass); final GroupValidationContext context = createContext(objectMetaBean, object, objectClass, groups); final ConstraintValidationListener result = context.getListener(); final Groups sequence = context.getGroups(); // 1. process groups for (Group current : sequence.getGroups()) { context.setCurrentGroup(current); validateBeanNet(context); } // 2. process sequences for (List eachSeq : sequence.getSequences()) { for (Group current : eachSeq) { context.setCurrentGroup(current); validateBeanNet(context); // if one of the group process in the sequence leads to one // or more validation failure, // the groups following in the sequence must not be // processed if (!result.isEmpty()) break; } if (!result.isEmpty()) break; } return result.getConstraintViolations(); } catch (RuntimeException ex) { throw unrecoverableValidationError(ex, object); } } /** * {@inheritDoc} Validates all constraints placed on the property of object named * propertyName. * * @param object object to validate * @param propertyName property to validate (ie field and getter constraints). Nested * properties may be referenced (e.g. prop[2].subpropA.subpropB) * @param groups group or list of groups targeted for validation (default to * {@link javax.validation.groups.Default}) * @return constraint violations or an empty Set if none * @throws IllegalArgumentException if object is null, if propertyName * null, empty or not a valid object property or if null is * passed to the varargs groups * @throws ValidationException if a non recoverable error happens during the validation * process */ // @Override - not allowed in 1.5 for Interface methods public Set> validateProperty(T object, String propertyName, Class... groups) { return validateProperty(object, propertyName, false, groups); } /** * {@inheritDoc} */ public Set> validateProperty(T object, String propertyName, boolean cascade, Class... groups) { if (object == null) throw new IllegalArgumentException("cannot validate null"); @SuppressWarnings("unchecked") Set> result = validateValueImpl((Class) object.getClass(), object, propertyName, VALIDATE_PROPERTY, cascade, groups); return result; } /** * {@inheritDoc} Validates all constraints placed on the property named propertyName of the class * beanType would the property value be value *

* ConstraintViolation objects return null for {@link ConstraintViolation#getRootBean()} and * {@link ConstraintViolation#getLeafBean()} * * @param beanType the bean type * @param propertyName property to validate * @param value property value to validate * @param groups group or list of groups targeted for validation (default to * {@link javax.validation.groups.Default}) * @return constraint violations or an empty Set if none * @throws IllegalArgumentException if beanType is null, if * propertyName null, empty or not a valid object * property or if null is passed to the varargs groups * @throws ValidationException if a non recoverable error happens during the validation * process */ // @Override - not allowed in 1.5 for Interface methods public Set> validateValue(Class beanType, String propertyName, Object value, Class... groups) { return validateValue(beanType, propertyName, value, false, groups); } /** * {@inheritDoc} */ public Set> validateValue(Class beanType, String propertyName, Object value, boolean cascade, Class... groups) { return validateValueImpl(checkBeanType(beanType), null, propertyName, value, cascade, groups); } /** * {@inheritDoc} Return the descriptor object describing bean constraints. The returned object (and associated * objects including ConstraintDescriptors) are immutable. * * @param clazz class or interface type evaluated * @return the bean descriptor for the specified class. * @throws IllegalArgumentException if clazz is null * @throws ValidationException if a non recoverable error happens during the metadata * discovery or if some constraints are invalid. */ // @Override - not allowed in 1.5 for Interface methods public BeanDescriptor getConstraintsForClass(Class clazz) { if (clazz == null) { throw new IllegalArgumentException("Class cannot be null"); } try { MetaBean metaBean = getMetaBeanFinder().findForClass(clazz); BeanDescriptorImpl edesc = metaBean.getFeature(Jsr303Features.Bean.BEAN_DESCRIPTOR); if (edesc == null) { edesc = createBeanDescriptor(metaBean); metaBean.putFeature(Jsr303Features.Bean.BEAN_DESCRIPTOR, edesc); } return edesc; } catch (RuntimeException ex) { throw new ValidationException("error retrieving constraints for " + clazz, ex); } } /** * {@inheritDoc} Return an instance of the specified type allowing access to provider-specific APIs. If the Bean * Validation provider implementation does not support the specified class, ValidationException is * thrown. * * @param type the class of the object to be returned. * @return an instance of the specified class * @throws ValidationException if the provider does not support the call. */ // @Override - not allowed in 1.5 for Interface methods public T unwrap(Class type) { // FIXME 2011-03-27 jw: // This code is unsecure. // It should allow only a fixed set of classes. // Can't fix this because don't know which classes this method should support. if (type.isAssignableFrom(getClass())) { @SuppressWarnings("unchecked") final T result = (T) this; return result; } else if (!(type.isInterface() || Modifier.isAbstract(type.getModifiers()))) { return newInstance(type); } else { try { Class cls = ClassUtils.getClass(type.getName() + "Impl"); if (type.isAssignableFrom(cls)) { @SuppressWarnings("unchecked") final Class implClass = (Class) cls; return newInstance(implClass); } } catch (ClassNotFoundException e) { } throw new ValidationException("Type " + type + " not supported"); } } private T newInstance(final Class cls) { return AccessController.doPrivileged(new PrivilegedAction() { public T run() { try { Constructor cons = cls.getConstructor(ApacheFactoryContext.class); if (!cons.isAccessible()) { cons.setAccessible(true); } return cons.newInstance(factoryContext); } catch (final Exception ex) { throw new ValidationException("Cannot instantiate : " + cls, ex); } } }); } // Helpers // ------------------------------------------------------------------- /** * Validates a bean and all its cascaded related beans for the currently defined group. *

* Special code is present to manage the {@link Default} group. * * @param validationContext The current context of this validation call. Must have its * {@link GroupValidationContext#getCurrentGroup()} field set. */ protected void validateBeanNet(GroupValidationContext context) { // If reached a cascaded bean which is null if (context.getBean() == null) { return; } // If reached a cascaded bean which has already been validated for the // current group if (!context.collectValidated()) { return; } // ### First, validate the bean // Default is a special case if (context.getCurrentGroup().isDefault()) { List defaultGroups = expandDefaultGroup(context); final ConstraintValidationListener result = (ConstraintValidationListener) context.getListener(); // If the rootBean defines a GroupSequence if (defaultGroups != null && defaultGroups.size() > 1) { int numViolations = result.violationsSize(); // Validate the bean for each group in the sequence Group currentGroup = context.getCurrentGroup(); for (Group each : defaultGroups) { context.setCurrentGroup(each); ValidationHelper.validateBean(context); // Spec 3.4.3 - Stop validation if errors already found if (result.violationsSize() > numViolations) { break; } } context.setCurrentGroup(currentGroup); } else { // For each class in the hierarchy of classes of rootBean, // validate the constraints defined in that class according // to the GroupSequence defined in the same class // Obtain the full class hierarchy List> classHierarchy = new ArrayList>(); ClassHelper.fillFullClassHierarchyAsList(classHierarchy, context.getMetaBean().getBeanClass()); Class initialOwner = context.getCurrentOwner(); // For each owner in the hierarchy for (Class owner : classHierarchy) { context.setCurrentOwner(owner); int numViolations = result.violationsSize(); // Obtain the group sequence of the owner, and use it for // the constraints that belong to it List ownerDefaultGroups = context.getMetaBean().getFeature("{GroupSequence:" + owner.getCanonicalName() + "}"); for (Group each : ownerDefaultGroups) { context.setCurrentGroup(each); ValidationHelper.validateBean(context); // Spec 3.4.3 - Stop validation if errors already found if (result.violationsSize() > numViolations) { break; } } } context.setCurrentOwner(initialOwner); context.setCurrentGroup(Group.DEFAULT); } } // if not the default group, proceed as normal else { ValidationHelper.validateBean(context); } // ### Then, the cascaded beans (@Valid) for (MetaProperty prop : context.getMetaBean().getProperties()) { validateCascadedBean(context, prop); } } /** * Checks if the the meta property prop defines a cascaded bean, and in case it does, validates it. * * @param context The current validation context. * @param prop The property to cascade from (in case it is possible). */ private void validateCascadedBean(GroupValidationContext context, MetaProperty prop) { AccessStrategy[] access = prop.getFeature(Features.Property.REF_CASCADE); if (access != null) { // different accesses to relation // save old values from context final Object bean = context.getBean(); final MetaBean mbean = context.getMetaBean(); // TODO implement Validation.groups support on related bean // Class[] groups = prop.getFeature(Jsr303Features.Property.REF_GROUPS); for (AccessStrategy each : access) { if (isCascadable(context, prop, each)) { // modify context state for relationship-target bean context.moveDown(prop, each); // Now, if the related bean is an instance of Map/Array/etc, ValidationHelper .validateContext(context, new Jsr303ValidationCallback(context), treatMapsLikeBeans); // restore old values in context context.moveUp(bean, mbean); } } } } /** * Before accessing a related bean (marked with {@link javax.validation.Valid}), the validator has to check if it is * reachable and cascadable. * * @param context The current validation context. * @param prop The property of the related bean. * @param access The access strategy used to get the related bean value. * @return true if the validator can access the related bean, false otherwise. */ private boolean isCascadable(GroupValidationContext context, MetaProperty prop, AccessStrategy access) { PathImpl beanPath = context.getPropertyPath(); NodeImpl node = new NodeImpl(prop.getName()); if (beanPath == null) { beanPath = PathImpl.create(null); } try { if (!context.getTraversableResolver().isReachable(context.getBean(), node, context.getRootMetaBean().getBeanClass(), beanPath, access.getElementType())) return false; } catch (RuntimeException e) { throw new ValidationException("Error in TraversableResolver.isReachable() for " + context.getBean(), e); } try { if (!context.getTraversableResolver().isCascadable(context.getBean(), node, context.getRootMetaBean().getBeanClass(), beanPath, access.getElementType())) return false; } catch (RuntimeException e) { throw new ValidationException("Error TraversableResolver.isCascadable() for " + context.getBean(), e); } return true; } /** * in case of a default group return the list of groups for a redefined default GroupSequence * * @return null when no in default group or default group sequence not redefined */ private List expandDefaultGroup(GroupValidationContext context) { if (context.getCurrentGroup().isDefault()) { // mention if metaBean redefines the default group List groupSeq = context.getMetaBean().getFeature(Jsr303Features.Bean.GROUP_SEQUENCE); if (groupSeq != null) { context.getGroups().assertDefaultGroupSequenceIsExpandable(groupSeq); } return groupSeq; } else { return null; } } /** * Generate an unrecoverable validation error * * @param ex * @param object * @return a {@link RuntimeException} of the appropriate type */ @SuppressWarnings("finally") protected static RuntimeException unrecoverableValidationError(RuntimeException ex, Object object) { if (ex instanceof UnknownPropertyException || ex instanceof IncompatiblePropertyValueException) { // Convert to IllegalArgumentException return new IllegalArgumentException(ex.getMessage(), ex); } else if (ex instanceof ValidationException) { return ex; // do not wrap specific ValidationExceptions (or // instances from subclasses) } else { String objectId = ""; try { if (object != null) { objectId = object.toString(); } else { objectId = ""; } } catch (Exception e) { objectId = ""; } finally { return new ValidationException("error during validation of " + objectId, ex); } } } private void validatePropertyInGroup(final GroupValidationContext context) { final Runnable helper; if (context.getMetaProperty() == null) { helper = new Runnable() { public void run() { ValidationHelper.validateBean(context); } }; } else { helper = new Runnable() { public void run() { ValidationHelper.validateProperty(context); } }; } Group currentGroup = context.getCurrentGroup(); List defaultGroups = expandDefaultGroup(context); if (defaultGroups != null) { for (Group each : defaultGroups) { context.setCurrentGroup(each); helper.run(); // continue validation, even if errors already found } context.setCurrentGroup(currentGroup); // restore } else { helper.run(); } } /** * Create a {@link GroupValidationContext}. * * @param * @param metaBean * @param object * @param objectClass * @param groups * @return {@link GroupValidationContext} instance */ protected GroupValidationContext createContext(MetaBean metaBean, T object, Class objectClass, Class... groups) { ConstraintValidationListener listener = new ConstraintValidationListener(object, objectClass); GroupValidationContextImpl context = new GroupValidationContextImpl(listener, this.factoryContext.getMessageInterpolator(), this.factoryContext.getTraversableResolver(), metaBean); context.setBean(object, metaBean); context.setGroups(groupsComputer.computeGroups(groups)); return context; } /** * Create a {@link BeanDescriptorImpl} * * @param metaBean * @return {@link BeanDescriptorImpl} instance */ protected BeanDescriptorImpl createBeanDescriptor(MetaBean metaBean) { return new BeanDescriptorImpl(factoryContext, metaBean); } private boolean treatMapsLikeBeans = false; /** * Behavior configuration - *

*

     * @return treatMapsLikeBeans - true (validate maps like beans, so that
     *                              you can use Maps to validate dynamic classes or
     *                              beans for which you have the MetaBean but no instances)
     *                            - false (default), validate maps like collections
     *                              (validating the values only)
     * 
*

* (is still configuration to better in BeanValidationContext?) */ public boolean isTreatMapsLikeBeans() { return treatMapsLikeBeans; } /** * Set whether maps are to be treated like beans. * *

     * @param treatMapsLikeBeans - true (validate maps like beans, so that
     *                             you can use Maps to validate dynamic classes or
     *                             beans for which you have the MetaBean but no instances)
     *                           - false (default), validate maps like collections
     *                             (validating the values only)
     * 
*/ public void setTreatMapsLikeBeans(boolean treatMapsLikeBeans) { this.treatMapsLikeBeans = treatMapsLikeBeans; } /** * Checks that beanType is valid according to spec Section 4.1.1 i. Throws an {@link IllegalArgumentException} if it * is not. * * @param beanType Bean type to check. */ private Class checkBeanType(Class beanType) { if (beanType == null) { throw new IllegalArgumentException("Bean type cannot be null."); } return beanType; } /** * Checks that the property name is valid according to spec Section 4.1.1 i. Throws an * {@link IllegalArgumentException} if it is not. * * @param propertyName Property name to check. */ private void checkPropertyName(String propertyName) { if (propertyName == null || propertyName.trim().length() == 0) { throw new IllegalArgumentException("Property path cannot be null or empty."); } } /** * Checks that the groups array is valid according to spec Section 4.1.1 i. Throws an * {@link IllegalArgumentException} if it is not. * * @param groups The groups to check. */ private void checkGroups(Class[] groups) { if (groups == null) { throw new IllegalArgumentException("Groups cannot be null."); } } /** * Dispatches a call from {@link #validate()} to {@link ClassValidator#validateBeanNet(GroupValidationContext)} with * the current context set. */ protected class Jsr303ValidationCallback implements ValidationHelper.ValidateCallback { private final GroupValidationContext context; public Jsr303ValidationCallback(GroupValidationContext context) { this.context = context; } public void validate() { validateBeanNet(context); } } /** * Create a {@link ValidationContextTraversal} instance for this {@link ClassValidator}. * * @param validationContext * @return {@link ValidationContextTraversal} */ protected ValidationContextTraversal createValidationContextTraversal(GroupValidationContext validationContext) { return new ValidationContextTraversal(validationContext); } /** * Implement {@link #validateProperty(Object, String, boolean, Class...)} and * {@link #validateValue(Class, String, Object, boolean, Class...)}. * * @param * @param beanType * @param object * @param propertyName * @param value * @param cascade * @param groups * @return {@link ConstraintViolation} {@link Set} */ private Set> validateValueImpl(Class beanType, T object, String propertyName, Object value, final boolean cascade, Class... groups) { assert (object == null) ^ (value == VALIDATE_PROPERTY); checkPropertyName(propertyName); checkGroups(groups); try { final MetaBean initialMetaBean = new DynamicMetaBean(getMetaBeanFinder()); initialMetaBean.setBeanClass(beanType); GroupValidationContext context = createContext(initialMetaBean, object, beanType, groups); ValidationContextTraversal contextTraversal = createValidationContextTraversal(context); PathNavigation.navigate(propertyName, contextTraversal); MetaProperty prop = context.getMetaProperty(); boolean fixed = false; if (value != VALIDATE_PROPERTY) { assert !context.getPropertyPath().isRootPath(); if (prop == null && value != null) { context.setMetaBean(getMetaBeanFinder().findForClass(value.getClass())); } if (!cascade) { //TCK doesn't care what type a property is if there are no constraints to validate: FeaturesCapable meta = prop == null ? context.getMetaBean() : prop; if (ArrayUtils.isEmpty(meta.getValidations())) { return Collections.> emptySet(); } } if (!TypeUtils.isAssignable(value == null ? null : value.getClass(), contextTraversal.getType())) { throw new IncompatiblePropertyValueException(String.format( "%3$s is not a valid value for property %2$s of type %1$s", beanType, propertyName, value)); } if (prop == null) { context.setBean(value); } else { context.setFixedValue(value); fixed = true; } } boolean doCascade = cascade && (prop == null || prop.getMetaBean() != null); Object bean = context.getBean(); ConstraintValidationListener result = context.getListener(); Groups sequence = context.getGroups(); // 1. process groups for (Group current : sequence.getGroups()) { context.setCurrentGroup(current); if (!doCascade || prop != null) { validatePropertyInGroup(context); } if (doCascade) { contextTraversal.moveDownIfNecessary(); if (context.getMetaBean() instanceof DynamicMetaBean) { context.setMetaBean(context.getMetaBean().resolveMetaBean( ObjectUtils.defaultIfNull(context.getBean(), contextTraversal.getRawType()))); } validateBeanNet(context); if (prop != null) { context.moveUp(bean, prop.getParentMetaBean()); context.setMetaProperty(prop); if (fixed) { context.setFixedValue(value); } } } } // 2. process sequences int groupViolations = result.getConstraintViolations().size(); outer: for (List eachSeq : sequence.getSequences()) { for (Group current : eachSeq) { context.setCurrentGroup(current); if (!doCascade || prop != null) { validatePropertyInGroup(context); } if (doCascade) { contextTraversal.moveDownIfNecessary(); if (context.getMetaBean() instanceof DynamicMetaBean) { context.setMetaBean(context.getMetaBean().resolveMetaBean( ObjectUtils.defaultIfNull(context.getBean(), contextTraversal.getRawType()))); } validateBeanNet(context); if (prop != null) { context.moveUp(bean, prop.getParentMetaBean()); context.setMetaProperty(prop); if (fixed) { context.setFixedValue(value); } } } /** * if one of the group process in the sequence leads to one or more validation failure, the groups * following in the sequence must not be processed */ if (result.getConstraintViolations().size() > groupViolations) break outer; } } return result.getConstraintViolations(); } catch (RuntimeException ex) { throw unrecoverableValidationError(ex, ObjectUtils.defaultIfNull(object, value)); } } }