| /* |
| * 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; |
| } |
| } |
| } |