blob: 05ab691a989fdea2709fb047945f34e45e253a49 [file] [log] [blame]
/*
* 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 org.apache.bval.jsr.util.NodeImpl;
import org.apache.bval.jsr.util.PathImpl;
import org.apache.bval.model.Validation;
import org.apache.bval.model.ValidationContext;
import org.apache.bval.model.ValidationListener;
import org.apache.bval.util.AccessStrategy;
import org.apache.bval.util.ObjectUtils;
import org.apache.bval.util.StringUtils;
import org.apache.bval.util.reflection.Reflection;
import org.apache.bval.util.reflection.TypeUtils;
import javax.validation.ConstraintDefinitionException;
import javax.validation.ConstraintTarget;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorFactory;
import javax.validation.Payload;
import javax.validation.UnexpectedTypeException;
import javax.validation.ValidationException;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import javax.validation.metadata.ConstraintDescriptor;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Description: Adapter between Constraint (JSR303) and Validation (Core)<br/>
* this instance is immutable!<br/>
*/
public class ConstraintValidation<T extends Annotation> implements Validation, ConstraintDescriptor<T> {
private final AccessStrategy access;
private final boolean reportFromComposite;
private final Map<String, Object> attributes;
private T annotation; // for metadata request API
private volatile ConstraintValidator<T, ?> validator;
private Set<ConstraintValidation<?>> composedConstraints;
private boolean validated = false;
/**
* the owner is the type where the validation comes from. it is used to
* support implicit grouping.
*/
private final Class<?> owner;
private Set<Class<?>> groups;
private Set<Class<? extends Payload>> payload;
private Class<? extends ConstraintValidator<T, ?>>[] validatorClasses;
private ConstraintTarget validationAppliesTo = null;
public ConstraintValidation(Class<? extends ConstraintValidator<T, ?>>[] validatorClasses, T annotation,
Class<?> owner, AccessStrategy access, boolean reportFromComposite, ConstraintTarget target) {
this.attributes = new HashMap<String, Object>();
this.validatorClasses = validatorClasses != null ? validatorClasses.clone() : null;
this.annotation = annotation;
this.owner = owner;
this.access = access;
this.reportFromComposite = reportFromComposite;
this.validationAppliesTo = target;
}
/**
* Return a {@link Serializable} {@link ConstraintDescriptor} capturing a
* snapshot of current state.
*
* @return {@link ConstraintDescriptor}
*/
public ConstraintDescriptor<T> asSerializableDescriptor() {
return new ConstraintDescriptorImpl<T>(this);
}
void setGroups(final Set<Class<?>> groups) {
this.groups = groups;
ConstraintAnnotationAttributes.GROUPS.put(attributes, groups.toArray(new Class<?>[groups.size()]));
}
void setGroups(final Class<?>[] groups) {
this.groups = new HashSet<Class<?>>();
Collections.addAll(this.groups, groups);
ConstraintAnnotationAttributes.GROUPS.put(attributes, groups);
}
void setPayload(Set<Class<? extends Payload>> payload) {
this.payload = payload;
ConstraintAnnotationAttributes.PAYLOAD.put(attributes, payload.toArray(new Class[payload.size()]));
}
/**
* {@inheritDoc}
*/
@Override
public boolean isReportAsSingleViolation() {
return reportFromComposite;
}
/**
* Add a composing constraint.
*
* @param aConstraintValidation to add
*/
public void addComposed(ConstraintValidation<?> aConstraintValidation) {
if (composedConstraints == null) {
composedConstraints = new HashSet<ConstraintValidation<?>>();
}
composedConstraints.add(aConstraintValidation);
}
/**
* {@inheritDoc}
*/
@Override
public <L extends ValidationListener> void validate(ValidationContext<L> context) {
validateGroupContext((GroupValidationContext<?>) context);
}
/**
* Validate a {@link GroupValidationContext}.
*
* @param context root
*/
public void validateGroupContext(final GroupValidationContext<?> context) {
if (validator == null) {
synchronized (this) {
if (validator == null) {
try {
ConstraintValidator<T, ? super T> constraintValidator = getConstraintValidator(
context.getConstraintValidatorFactory(), annotation, validatorClasses, owner, access);
if (constraintValidator != null) {
constraintValidator.initialize(annotation);
}
validator = constraintValidator;
} catch (final RuntimeException re) {
if (ValidationException.class.isInstance(re)) {
throw re;
}
throw new ConstraintDefinitionException(re);
}
}
}
}
context.setConstraintValidation(this);
/**
* execute unless the given validation constraint has already been
* processed during this validation routine (as part of a previous group
* match)
*/
if (!isMemberOf(context.getCurrentGroup().getGroup())) {
return; // do not validate in the current group
}
if (context.getCurrentOwner() != null && !this.owner.equals(context.getCurrentOwner())) {
return;
}
if (validator != null && !context.collectValidated(validator))
return; // already done
if (context.getMetaProperty() != null && !isReachable(context)) {
return;
}
// process composed constraints
if (isReportAsSingleViolation()) {
final ConstraintValidationListener<?> listener = context.getListener();
listener.beginReportAsSingle();
boolean failed = listener.hasViolations();
try {
// stop validating when already failed and
// ReportAsSingleInvalidConstraint = true ?
for (Iterator<ConstraintValidation<?>> composed = getComposingValidations().iterator(); !failed
&& composed.hasNext();) {
composed.next().validate(context);
failed = listener.hasViolations();
}
} finally {
listener.endReportAsSingle();
// Restore current constraint validation
context.setConstraintValidation(this);
}
if (failed) {
// TODO RSt - how should the composed constraint error report look like?
addErrors(context, new ConstraintValidatorContextImpl(context, this)); // add defaultErrorMessage only
return;
}
} else {
for (ConstraintValidation<?> composed : getComposingValidations()) {
composed.validate(context);
}
// Restore current constraint validation
context.setConstraintValidation(this);
}
if (validator != null) {
@SuppressWarnings("unchecked")
final ConstraintValidator<T, Object> objectValidator = (ConstraintValidator<T, Object>) validator;
final ConstraintValidatorContextImpl jsrContext = new ConstraintValidatorContextImpl(context, this);
if (!objectValidator.isValid(context.getValidatedValue(), jsrContext)) {
addErrors(context, jsrContext);
}
}
}
private <A extends Annotation> ConstraintValidator<A, ? super T> getConstraintValidator(
ConstraintValidatorFactory factory, A annotation,
Class<? extends ConstraintValidator<A, ?>>[] constraintClasses, Class<?> owner, AccessStrategy access) {
if (ObjectUtils.isNotEmpty(constraintClasses)) {
final Type type = determineTargetedType(owner, access);
/**
* spec says in chapter 3.5.3.: The ConstraintValidator chosen to
* validate a declared type T is the one where the type supported by
* the ConstraintValidator is a supertype of T and where there is no
* other ConstraintValidator whose supported type is a supertype of
* T and not a supertype of the chosen ConstraintValidator supported
* type.
*/
final Map<Type, Collection<Class<? extends ConstraintValidator<A, ?>>>> validatorTypes =
getValidatorsTypes(constraintClasses);
reduceTarget(validatorTypes, access);
final List<Type> assignableTypes = new ArrayList<Type>(constraintClasses.length);
fillAssignableTypes(type, validatorTypes.keySet(), assignableTypes);
reduceAssignableTypes(assignableTypes);
checkOneType(assignableTypes, type, owner, annotation, access);
if ((type.equals(Object.class) || type.equals(Object[].class)) && validatorTypes.containsKey(Object.class)
&& validatorTypes.containsKey(Object[].class)) {
throw new ConstraintDefinitionException(
"Only a validator for Object or Object[] should be provided for cross-parameter validators");
}
final Collection<Class<? extends ConstraintValidator<A, ?>>> key =
validatorTypes.get(assignableTypes.get(0));
if (key.size() > 1) {
final String message = "Factory returned " + key.size() + " validators";
if (ParametersAccess.class.isInstance(access)) { // cross parameter
throw new ConstraintDefinitionException(message);
}
throw new UnexpectedTypeException(message);
}
@SuppressWarnings("unchecked")
final ConstraintValidator<A, ? super T> validator =
(ConstraintValidator<A, ? super T>) factory.getInstance(key.iterator().next());
if (validator == null) {
throw new ValidationException("Factory returned null validator for: " + key);
}
return validator;
// NOTE: validator initialization deferred until append phase
}
return null;
}
private <A extends Annotation> void reduceTarget(
final Map<Type, Collection<Class<? extends ConstraintValidator<A, ?>>>> validator,
final AccessStrategy access) {
for (final Map.Entry<Type, Collection<Class<? extends ConstraintValidator<A, ?>>>> entry : validator
.entrySet()) {
final Collection<Class<? extends ConstraintValidator<A, ?>>> validators = entry.getValue();
final Iterator<Class<? extends ConstraintValidator<A, ?>>> it = validators.iterator();
while (it.hasNext()) {
final Type v = it.next();
if (!Class.class.isInstance(v)) {
continue; // TODO: handle this case
}
final Class<?> clazz = Class.class.cast(v);
final SupportedValidationTarget target = clazz.getAnnotation(SupportedValidationTarget.class);
if (target != null) {
final Collection<ValidationTarget> targets = Arrays.asList(target.value());
final boolean isParameter =
ParameterAccess.class.isInstance(access) || ParametersAccess.class.isInstance(access);
if ((isParameter && !targets.contains(ValidationTarget.PARAMETERS))
|| (!isParameter && !targets.contains(ValidationTarget.ANNOTATED_ELEMENT))) {
it.remove();
}
}
}
if (validators.isEmpty()) {
validator.remove(entry.getKey());
}
}
}
private static void checkOneType(List<Type> types, Type targetType, Class<?> owner, Annotation anno,
AccessStrategy access) {
if (types.isEmpty()) {
final String message = "No validator could be found for type " + stringForType(targetType) + ". See: @"
+ anno.annotationType().getSimpleName() + " at " + stringForLocation(owner, access);
if (Object[].class.equals(targetType)) { // cross parameter
throw new ConstraintDefinitionException(message);
}
throw new UnexpectedTypeException(message);
}
if (types.size() > 1) {
throw new UnexpectedTypeException(
String.format("Ambiguous validators for type %s. See: @%s at %s. Validators are: %s",
stringForType(targetType), anno.annotationType().getSimpleName(), stringForLocation(owner, access),
StringUtils.join(types, ", ")));
}
}
private static String stringForType(Type clazz) {
if (clazz instanceof Class<?>) {
return ((Class<?>) clazz).isArray() ? ((Class<?>) clazz).getComponentType().getName() + "[]"
: ((Class<?>) clazz).getName();
}
return clazz.toString();
}
private static String stringForLocation(Class<?> owner, AccessStrategy access) {
return access == null ? owner.getName() : access.toString();
}
private static void fillAssignableTypes(Type type, Set<Type> validatorsTypes, List<Type> suitableTypes) {
for (final Type validatorType : validatorsTypes) {
if (TypeUtils.isAssignable(type, validatorType) && !suitableTypes.contains(validatorType)) {
suitableTypes.add(validatorType);
}
}
}
/**
* Tries to reduce all assignable classes down to a single class.
*
* @param assignableTypes The set of all classes which are assignable to the class of
* the value to be validated and which are handled by at least
* one of the validators for the specified constraint.
*/
private static void reduceAssignableTypes(List<Type> assignableTypes) {
if (assignableTypes.size() <= 1) {
return; // no need to reduce
}
boolean removed = false;
do {
final Type type = assignableTypes.get(0);
for (int i = 1; i < assignableTypes.size(); i++) {
final Type nextType = assignableTypes.get(i);
if (TypeUtils.isAssignable(nextType, type)) {
assignableTypes.remove(0);
i--;
removed = true;
} else if (TypeUtils.isAssignable(type, nextType)) {
assignableTypes.remove(i--);
removed = true;
}
}
} while (removed && assignableTypes.size() > 1);
}
private static <A extends Annotation> Map<Type, Collection<Class<? extends ConstraintValidator<A, ?>>>> getValidatorsTypes(
Class<? extends ConstraintValidator<A, ?>>[] constraintValidatorClasses) {
final Map<Type, Collection<Class<? extends ConstraintValidator<A, ?>>>> validatorsTypes =
new HashMap<Type, Collection<Class<? extends ConstraintValidator<A, ?>>>>();
for (Class<? extends ConstraintValidator<A, ?>> validatorType : constraintValidatorClasses) {
Type validatedType = TypeUtils.getTypeArguments(validatorType, ConstraintValidator.class)
.get(ConstraintValidator.class.getTypeParameters()[1]);
if (validatedType == null) {
throw new ValidationException(String.format("Could not detect validated type for %s", validatorType));
}
if (validatedType instanceof GenericArrayType) {
final Type componentType = TypeUtils.getArrayComponentType(validatedType);
if (componentType instanceof Class<?>) {
validatedType = Array.newInstance((Class<?>) componentType, 0).getClass();
}
}
if (!validatorsTypes.containsKey(validatedType)) {
validatorsTypes.put(validatedType, new ArrayList<Class<? extends ConstraintValidator<A, ?>>>());
}
validatorsTypes.get(validatedType).add(validatorType);
}
return validatorsTypes;
}
/**
* implements spec chapter 3.5.3. ConstraintValidator resolution algorithm.
*/
private static Type determineTargetedType(Class<?> owner, AccessStrategy access) {
// if the constraint declaration is hosted on a class or an interface,
// the targeted type is the class or the interface.
if (access == null) {
return owner;
}
final Type type = access.getJavaType();
if (type == null) {
return Object.class;
}
return type instanceof Class<?> ? Reflection.primitiveToWrapper((Class<?>) type) : type;
}
/**
* Initialize the validator (if not <code>null</code>) with the stored
* annotation.
*/
public void initialize() {
if (null != validator) {
try {
validator.initialize(annotation);
} catch (RuntimeException e) {
// Either a "legit" problem initializing the validator or a
// ClassCastException if the validator associated annotation is
// not a supertype of the validated annotation.
throw new ConstraintDefinitionException(
"Incorrect validator [" + validator.getClass().getCanonicalName() + "] for annotation "
+ annotation.annotationType().getCanonicalName(),
e);
}
}
}
private boolean isReachable(GroupValidationContext<?> context) {
final PathImpl path = context.getPropertyPath();
final NodeImpl node = path.getLeafNode();
PathImpl beanPath = path.getPathWithoutLeafNode();
if (beanPath == null) {
beanPath = PathImpl.create();
}
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);
}
return true;
}
private void addErrors(GroupValidationContext<?> context, ConstraintValidatorContextImpl jsrContext) {
for (ValidationListener.Error each : jsrContext.getErrorMessages()) {
context.getListener().addError(each, context);
}
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return "ConstraintValidation{" + validator + '}';
}
/**
* Get the message template used by this constraint.
*
* @return String
*/
@Override
public String getMessageTemplate() {
return ConstraintAnnotationAttributes.MESSAGE.get(attributes);
}
public ConstraintValidator<T, ?> getValidator() {
return validator;
}
protected boolean isMemberOf(Class<?> reqGroup) {
return groups.contains(reqGroup);
}
public Class<?> getOwner() {
return owner;
}
@Override
public T getAnnotation() {
return annotation;
}
public AccessStrategy getAccess() {
return access;
}
public void setAnnotation(T annotation) {
this.annotation = annotation;
}
// ///////////////////////// ConstraintDescriptor implementation
/**
* {@inheritDoc}
*/
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public Set<ConstraintDescriptor<?>> getComposingConstraints() {
return composedConstraints == null ? Collections.EMPTY_SET : composedConstraints;
}
/**
* Get the composing {@link ConstraintValidation} objects. This is
* effectively an implementation-specific analogue to
* {@link #getComposingConstraints()}.
*
* @return {@link Set} of {@link ConstraintValidation}
*/
Set<ConstraintValidation<?>> getComposingValidations() {
return composedConstraints == null ? Collections.<ConstraintValidation<?>> emptySet() : composedConstraints;
}
/**
* {@inheritDoc}
*/
@Override
public Set<Class<?>> getGroups() {
return groups;
}
/**
* {@inheritDoc}
*/
@Override
public Set<Class<? extends Payload>> getPayload() {
return payload;
}
@Override
public ConstraintTarget getValidationAppliesTo() {
return validationAppliesTo;
}
/**
* {@inheritDoc}
*/
@Override
public List<Class<? extends ConstraintValidator<T, ?>>> getConstraintValidatorClasses() {
return validatorClasses == null ? Collections.<Class<? extends ConstraintValidator<T, ?>>> emptyList()
: Arrays.asList(validatorClasses);
}
public void setValidationAppliesTo(final ConstraintTarget validationAppliesTo) {
this.validationAppliesTo = validationAppliesTo;
}
public boolean isValidated() {
return validated;
}
public void setValidated(final boolean validated) {
this.validated = validated;
}
}