blob: ababfbdf8ef228b2d89ab96f6eba32cb02942c76 [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 java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.validation.Constraint;
import javax.validation.ConstraintDeclarationException;
import javax.validation.ConstraintDefinitionException;
import javax.validation.ConstraintTarget;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorFactory;
import javax.validation.OverridesAttribute;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import org.apache.bval.jsr.groups.GroupsComputer;
import org.apache.bval.jsr.xml.AnnotationProxyBuilder;
import org.apache.bval.util.AccessStrategy;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.reflect.TypeUtils;
import org.apache.commons.weaver.privilizer.Privileged;
/**
* Description: helper class that builds a {@link ConstraintValidation} or its
* composite constraint validations by parsing the jsr-annotations and
* providing information (e.g. for @OverridesAttributes) <br/>
*/
final class AnnotationConstraintBuilder<A extends Annotation> {
private static final Logger log = Logger.getLogger(AnnotationConstraintBuilder.class.getName());
private final ConstraintValidation<?> constraintValidation;
private List<ConstraintOverrides> overrides;
/**
* Create a new AnnotationConstraintBuilder instance.
*
* @param validatorClasses
* @param annotation
* @param owner
* @param access
*/
public AnnotationConstraintBuilder(ConstraintValidatorFactory factory,
Class<? extends ConstraintValidator<A, ?>>[] validatorClasses, A annotation, Class<?> owner,
AccessStrategy access, ConstraintTarget target) {
final boolean reportFromComposite =
annotation != null && annotation.annotationType().isAnnotationPresent(ReportAsSingleViolation.class);
constraintValidation =
new ConstraintValidation<A>(factory, validatorClasses, annotation, owner, access, reportFromComposite,
target);
buildFromAnnotation();
}
/** build attributes, payload, groups from 'annotation' */
@Privileged
private void buildFromAnnotation() {
if (constraintValidation.getAnnotation() == null) {
return;
}
final Class<? extends Annotation> annotationType = constraintValidation.getAnnotation().annotationType();
boolean foundPayload = false;
boolean foundGroups = false;
Method validationAppliesTo = null;
boolean foundMessage = false;
for (final Method method : AnnotationProxyBuilder.findMethods(annotationType)) {
// groups + payload must also appear in attributes (also
// checked by TCK-Tests)
if (method.getParameterTypes().length == 0) {
try {
final String name = method.getName();
if (ConstraintAnnotationAttributes.PAYLOAD.getAttributeName().equals(name)) {
buildPayload(method);
foundPayload = true;
} else if (ConstraintAnnotationAttributes.GROUPS.getAttributeName().equals(name)) {
buildGroups(method);
foundGroups = true;
} else if (ConstraintAnnotationAttributes.VALIDATION_APPLIES_TO.getAttributeName().equals(name)) {
buildValidationAppliesTo(method);
validationAppliesTo = method;
} else if (name.startsWith("valid")) {
throw new ConstraintDefinitionException("constraints parameters can't start with valid: " + name);
} else {
if (ConstraintAnnotationAttributes.MESSAGE.getAttributeName().equals(name)) {
foundMessage = true;
if (!TypeUtils.isAssignable(method.getReturnType(), ConstraintAnnotationAttributes.MESSAGE.getType())) {
throw new ConstraintDefinitionException("Return type for message() must be of type " + ConstraintAnnotationAttributes.MESSAGE.getType());
}
}
constraintValidation.getAttributes().put(name, method.invoke(constraintValidation.getAnnotation()));
}
} catch (final ConstraintDefinitionException cde) {
throw cde;
} catch (final Exception e) { // do nothing
log.log(Level.WARNING, String.format("Error processing annotation: %s ", constraintValidation.getAnnotation()), e);
}
}
}
if (!foundMessage) {
throw new ConstraintDefinitionException("Annotation " + annotationType.getName() + " has no message method");
}
if (!foundPayload) {
throw new ConstraintDefinitionException("Annotation " + annotationType.getName() + " has no payload method");
}
if (!foundGroups) {
throw new ConstraintDefinitionException("Annotation " + annotationType.getName() + " has no groups method");
}
if (validationAppliesTo != null && !ConstraintTarget.IMPLICIT.equals(validationAppliesTo.getDefaultValue())) {
throw new ConstraintDefinitionException("validationAppliesTo default value should be IMPLICIT");
}
// valid validationAppliesTo
final Constraint annotation = annotationType.getAnnotation(Constraint.class);
if (annotation == null) {
return;
}
final Pair validationTarget = computeValidationTarget(annotation.validatedBy());
for (final Annotation a : annotationType.getAnnotations()) {
final Class<? extends Annotation> aClass = a.annotationType();
if (aClass.getName().startsWith("java.lang.annotation.")) {
continue;
}
final Constraint inheritedConstraint = aClass.getAnnotation(Constraint.class);
if (inheritedConstraint != null && !aClass.getName().startsWith("javax.validation.constraints.")) {
final Pair validationTargetInherited = computeValidationTarget(inheritedConstraint.validatedBy());
if ((validationTarget.a > 0 && validationTargetInherited.b > 0 && validationTarget.b == 0)
|| (validationTarget.b > 0 && validationTargetInherited.a > 0 && validationTarget.a == 0)) {
throw new ConstraintDefinitionException("Parent and child constraint have different targets");
}
}
}
}
private Pair computeValidationTarget(final Class<?>[] validators) {
int param = 0;
int annotatedElt = 0;
for (final Class<?> validator : validators) {
final SupportedValidationTarget supportedAnnotationTypes = validator.getAnnotation(SupportedValidationTarget.class);
if (supportedAnnotationTypes != null) {
final List<ValidationTarget> values = Arrays.asList(supportedAnnotationTypes.value());
if (values.contains(ValidationTarget.PARAMETERS)) {
param++;
}
if (values.contains(ValidationTarget.ANNOTATED_ELEMENT)) {
annotatedElt++;
}
} else {
annotatedElt++;
}
}
if (annotatedElt == 0 && param >= 1 && constraintValidation.getValidationAppliesTo() != null) { // pure cross param
throw new ConstraintDefinitionException("pure cross parameter constraints shouldn't get validationAppliesTo attribute");
}
if (param >= 1 && annotatedElt >= 1 && constraintValidation.getValidationAppliesTo() == null) { // generic and cross param
throw new ConstraintDefinitionException("cross parameter AND generic constraints should get validationAppliesTo attribute");
}
if (param == 0 && constraintValidation.getValidationAppliesTo() != null) { // pure generic
throw new ConstraintDefinitionException("pure generic constraints shouldn't get validationAppliesTo attribute");
}
return new Pair(annotatedElt, param);
}
private void buildValidationAppliesTo(final Method method) throws InvocationTargetException, IllegalAccessException {
if (!TypeUtils.isAssignable(method.getReturnType(), ConstraintAnnotationAttributes.VALIDATION_APPLIES_TO.getType())) {
throw new ConstraintDefinitionException("Return type for validationAppliesTo() must be of type "
+ ConstraintAnnotationAttributes.VALIDATION_APPLIES_TO.getType());
}
final Object validationAppliesTo = method.invoke(constraintValidation.getAnnotation());
if (!ConstraintTarget.class.isInstance(validationAppliesTo)) {
throw new ConstraintDefinitionException("validationAppliesTo type is " + ConstraintTarget.class.getName());
}
constraintValidation.setValidationAppliesTo(ConstraintTarget.class.cast(validationAppliesTo));
}
private void buildGroups(final Method method) throws IllegalAccessException, InvocationTargetException {
if (!TypeUtils.isAssignable(method.getReturnType(), ConstraintAnnotationAttributes.GROUPS.getType())) {
throw new ConstraintDefinitionException("Return type for groups() must be of type "
+ ConstraintAnnotationAttributes.GROUPS.getType());
}
final Object raw = method.invoke(constraintValidation.getAnnotation());
Class<?>[] garr;
if (raw instanceof Class<?>) {
garr = new Class[] { (Class<?>) raw };
} else if (raw instanceof Class<?>[]) {
garr = (Class<?>[]) raw;
if (Object[].class.cast(method.getDefaultValue()).length > 0) {
throw new ConstraintDefinitionException("Default value for groups() must be an empty array");
}
} else {
garr = null;
}
if (ArrayUtils.isEmpty(garr)) {
garr = GroupsComputer.DEFAULT_GROUP;
}
constraintValidation.setGroups(garr);
}
@SuppressWarnings("unchecked")
private void buildPayload(final Method method) throws IllegalAccessException, InvocationTargetException {
if (!TypeUtils.isAssignable(method.getReturnType(), ConstraintAnnotationAttributes.PAYLOAD.getType())) {
throw new ConstraintDefinitionException("Return type for payload() must be of type "
+ ConstraintAnnotationAttributes.PAYLOAD.getType());
}
if (Object[].class.cast(method.getDefaultValue()).length > 0) {
throw new ConstraintDefinitionException("Default value for payload() must be an empty array");
}
final Class<? extends Payload>[] payload_raw =
(Class<? extends Payload>[]) method.invoke(constraintValidation.getAnnotation());
final Set<Class<? extends Payload>> payloadSet;
if (payload_raw == null) {
payloadSet = Collections.<Class<? extends Payload>> emptySet();
} else {
payloadSet = new HashSet<Class<? extends Payload>>(payload_raw.length);
Collections.addAll(payloadSet, payload_raw);
}
constraintValidation.setPayload(payloadSet);
}
/**
* Get the configured {@link ConstraintValidation}.
*
* @return {@link ConstraintValidation}
*/
public ConstraintValidation<?> getConstraintValidation() {
return constraintValidation;
}
/**
* initialize a child composite 'validation' with @OverridesAttribute from
* 'constraintValidation' and add to composites.
*/
public void addComposed(ConstraintValidation<?> composite) {
applyOverridesAttributes(composite);
if (constraintValidation.getValidationAppliesTo() != null) {
composite.setValidationAppliesTo(constraintValidation.getValidationAppliesTo());
}
constraintValidation.addComposed(composite); // add AFTER apply()
}
private void applyOverridesAttributes(ConstraintValidation<?> composite) {
if (null == overrides) {
buildOverridesAttributes();
}
if (!overrides.isEmpty()) {
final int index = computeIndex(composite);
// Search for the overrides to apply
final ConstraintOverrides generalOverride = findOverride(composite.getAnnotation().annotationType(), -1);
if (generalOverride != null) {
if (index > 0) {
throw new ConstraintDeclarationException("Wrong OverridesAttribute declaration for "
+ generalOverride.constraintType
+ ", it needs a defined index when there is a list of constraints");
}
generalOverride.applyOn(composite);
}
final ConstraintOverrides override = findOverride(composite.getAnnotation().annotationType(), index);
if (override != null) {
override.applyOn(composite);
}
}
}
/**
* Calculates the index of the composite constraint. The index represents
* the order in which it is added in reference to other constraints of the
* same type.
*
* @param composite
* The composite constraint (not yet added).
* @return An integer index always >= 0
*/
private int computeIndex(ConstraintValidation<?> composite) {
int idx = 0;
for (ConstraintValidation<?> each : constraintValidation.getComposingValidations()) {
if (each.getAnnotation().annotationType() == composite.getAnnotation().annotationType()) {
idx++;
}
}
return idx;
}
/** read overridesAttributes from constraintValidation.annotation */
private void buildOverridesAttributes() {
overrides = new LinkedList<ConstraintOverrides>();
for (Method method : constraintValidation.getAnnotation().annotationType().getDeclaredMethods()) {
final OverridesAttribute.List overridesAttributeList = method.getAnnotation(OverridesAttribute.List.class);
if (overridesAttributeList != null) {
for (OverridesAttribute overridesAttribute : overridesAttributeList.value()) {
parseConstraintOverride(method.getName(), overridesAttribute);
}
}
final OverridesAttribute overridesAttribute = method.getAnnotation(OverridesAttribute.class);
if (overridesAttribute != null) {
parseConstraintOverride(method.getName(), overridesAttribute);
}
}
}
private void parseConstraintOverride(String methodName, OverridesAttribute oa) {
ConstraintOverrides target = findOverride(oa.constraint(), oa.constraintIndex());
if (target == null) {
target = new ConstraintOverrides(oa.constraint(), oa.constraintIndex());
overrides.add(target);
}
target.values.put(oa.name(), constraintValidation.getAttributes().get(methodName));
}
private ConstraintOverrides findOverride(Class<? extends Annotation> constraint, int constraintIndex) {
for (ConstraintOverrides each : overrides) {
if (each.constraintType == constraint && each.constraintIndex == constraintIndex) {
return each;
}
}
return null;
}
/**
* Holds the values to override in a composed constraint during creation of
* a composed ConstraintValidation
*/
private static final class ConstraintOverrides {
final Class<? extends Annotation> constraintType;
final int constraintIndex;
/** key = attributeName, value = overridden value */
final Map<String, Object> values;
private ConstraintOverrides(Class<? extends Annotation> constraintType, int constraintIndex) {
this.constraintType = constraintType;
this.constraintIndex = constraintIndex;
values = new HashMap<String, Object>();
}
@SuppressWarnings("unchecked")
private void applyOn(ConstraintValidation<?> composite) {
// Update the attributes
composite.getAttributes().putAll(values);
// And the annotation
final Annotation originalAnnot = composite.getAnnotation();
final AnnotationProxyBuilder<Annotation> apb = new AnnotationProxyBuilder<Annotation>(originalAnnot);
for (String key : values.keySet()) {
apb.putValue(key, values.get(key));
}
final Annotation newAnnot = apb.createAnnotation();
((ConstraintValidation<Annotation>) composite).setAnnotation(newAnnot);
}
}
private static class Pair {
private int a;
private int b;
private Pair(int a, int b) {
this.a = a;
this.b = b;
}
}
}