org.apache.bval.jsr303.ClassValidator Maven / Gradle / Ivy
/*
* 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 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.SecureActions;
import org.apache.bval.model.Features;
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.lang.ClassUtils;
import javax.validation.ConstraintViolation;
import javax.validation.ValidationException;
import javax.validation.Validator;
import javax.validation.groups.Default;
import javax.validation.metadata.BeanDescriptor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
// TODO: centralize treatMapsLikeBeans
/**
* Objects of this class are able to validate bean instances (and the associated
* object graphs).
*
* Implementation is thread-safe.
*
* API class
*
* @author Roman Stumm
* @author Carlos Vara
*/
public class ClassValidator implements Validator {
/**
* {@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()
*/
public 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 =
factoryContext.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
@SuppressWarnings("unchecked")
public Set> validateProperty(T object,
String propertyName, Class... groups) {
if (object == null)
throw new IllegalArgumentException("cannot validate null");
checkPropertyName(propertyName);
checkGroups(groups);
try {
Class objectClass = (Class) object.getClass();
MetaBean objectMetaBean =
factoryContext.getMetaBeanFinder().findForClass(objectClass);
GroupValidationContext context =
createContext(objectMetaBean, object, objectClass, groups);
ConstraintValidationListener result = context.getListener();
NestedMetaProperty nestedProp =
getNestedProperty(objectMetaBean, object, propertyName);
context.setMetaProperty(nestedProp.getMetaProperty());
if (nestedProp.isNested()) {
context.setFixedValue(nestedProp.getValue());
}
if (context.getMetaProperty() == null)
throw new IllegalArgumentException("Unknown property "
+ object.getClass().getName() + "." + propertyName);
Groups sequence = context.getGroups();
// 1. process groups
for (Group current : sequence.getGroups()) {
context.setCurrentGroup(current);
validatePropertyInGroup(context);
}
// 2. process sequences
for (List eachSeq : sequence.getSequences()) {
for (Group current : eachSeq) {
context.setCurrentGroup(current);
validatePropertyInGroup(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 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) {
checkBeanType(beanType);
checkPropertyName(propertyName);
checkGroups(groups);
try {
MetaBean metaBean =
factoryContext.getMetaBeanFinder().findForClass(beanType);
GroupValidationContext context =
createContext(metaBean, null, beanType, groups);
ConstraintValidationListener result = context.getListener();
context.setMetaProperty(getNestedProperty(metaBean, null,
propertyName).getMetaProperty());
context.setFixedValue(value);
Groups sequence = context.getGroups();
// 1. process groups
for (Group current : sequence.getGroups()) {
context.setCurrentGroup(current);
validatePropertyInGroup(context);
}
// 2. process sequences
for (List eachSeq : sequence.getSequences()) {
for (Group current : eachSeq) {
context.setCurrentGroup(current);
validatePropertyInGroup(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, value);
}
}
/**
* {@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 =
factoryContext.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) {
if (type.isAssignableFrom(getClass())) {
@SuppressWarnings("unchecked")
final T result = (T) this;
return result;
} else if (!(type.isInterface() || Modifier.isAbstract(type
.getModifiers()))) {
return SecureActions.newInstance(type,
new Class[] { ApacheFactoryContext.class },
new Object[] { factoryContext });
} else {
try {
Class cls = ClassUtils.getClass(type.getName() + "Impl");
if (type.isAssignableFrom(cls)) {
@SuppressWarnings("unchecked")
final Class implClass =
(Class) cls;
return SecureActions.newInstance(implClass,
new Class[] { ApacheFactoryContext.class },
new Object[] { factoryContext });
}
} catch (ClassNotFoundException e) {
}
throw new ValidationException("Type " + type + " not supported");
}
}
// 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.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
*/
protected static RuntimeException unrecoverableValidationError(
RuntimeException ex, Object object) {
if (ex instanceof UnknownPropertyException) {
// 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(GroupValidationContext context) {
Group currentGroup = context.getCurrentGroup();
List defaultGroups = expandDefaultGroup(context);
if (defaultGroups != null) {
for (Group each : defaultGroups) {
context.setCurrentGroup(each);
ValidationHelper.validateProperty(context);
// continue validation, even if errors already found: if
// (!result.isEmpty())
}
context.setCurrentGroup(currentGroup); // restore
} else {
ValidationHelper.validateProperty(context);
}
}
/**
* find the MetaProperty for the given propertyName, which could contain a
* path, following the path on a given object to resolve types at runtime
* from the instance
*/
private NestedMetaProperty getNestedProperty(MetaBean metaBean, Object t,
String propertyName) {
NestedMetaProperty nested = new NestedMetaProperty(propertyName, t);
nested.setMetaBean(metaBean);
nested.parse();
return nested;
}
/**
* 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 -
*
*
* parameter: 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;
}
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 void checkBeanType(Class beanType) {
if (beanType == null) {
throw new IllegalArgumentException("Bean type cannot be null.");
}
}
/**
* 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);
}
}
}