Package org.apache.bval.jsr

Source Code of org.apache.bval.jsr.AnnotationConstraintBuilder$Pair

/*
* 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.groups.GroupsComputer;
import org.apache.bval.jsr.xml.AnnotationProxyBuilder;
import org.apache.bval.util.AccessStrategy;
import org.apache.commons.lang3.reflect.TypeUtils;

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 java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
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;

/**
* 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) {
        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' */
    private void buildFromAnnotation() {
        if (constraintValidation.getAnnotation() != null) {
            if (System.getSecurityManager() == null) {
                doBuildFromAnnotations();
            } else {
                AccessController.doPrivileged(new PrivilegedAction<Object>() {
                    public Object run() {
                        doBuildFromAnnotations();
                        return null;
                    }
                });
            }
        }
    }

    private void doBuildFromAnnotations() {
        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");
        } else {
            if (param >= 1 && annotatedElt >= 1 && constraintValidation.getValidationAppliesTo() == null) { // generic and cross param
                throw new ConstraintDefinitionException("cross parameter AND generic constraints should get validationAppliesTo attribute");
            } else 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)) {
            constraintValidation.setValidationAppliesTo(ConstraintTarget.class.cast(validationAppliesTo));
        } else {
            throw new ConstraintDefinitionException("validationAppliesTo type is " + ConstraintTarget.class.getName());
        }
    }

    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 (garr == null || garr.length == 0) {
            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");
        }

        Class<? extends Payload>[] payload_raw =
            (Class<? extends Payload>[]) method.invoke(constraintValidation.getAnnotation());
        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()) {
            int index = computeIndex(composite);

            // Search for the overrides to apply
            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);
            }

            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()) {
            OverridesAttribute.List annoOAL = method.getAnnotation(OverridesAttribute.List.class);
            if (annoOAL != null) {
                for (OverridesAttribute annoOA : annoOAL.value()) {
                    parseConstraintOverride(method.getName(), annoOA);
                }
            }
            OverridesAttribute annoOA = method.getAnnotation(OverridesAttribute.class);
            if (annoOA != null) {
                parseConstraintOverride(method.getName(), annoOA);
            }
        }
    }

    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
            Annotation originalAnnot = composite.getAnnotation();
            AnnotationProxyBuilder<Annotation> apb = new AnnotationProxyBuilder<Annotation>(originalAnnot);
            for (String key : values.keySet()) {
                apb.putValue(key, values.get(key));
            }
            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;
        }
    }
}
TOP

Related Classes of org.apache.bval.jsr.AnnotationConstraintBuilder$Pair

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.