All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cz.jirutka.validator.collection.CommonEachValidator Maven / Gradle / Ivy

The newest version!
/*
 * The MIT License
 *
 * Copyright 2013-2016 Jakub Jirutka .
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package cz.jirutka.validator.collection;

import cz.jirutka.validator.collection.constraints.EachConstraint;
import cz.jirutka.validator.collection.internal.ConstraintDescriptorFactory;
import cz.jirutka.validator.collection.internal.MessageInterpolatorContext;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.reflect.TypeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.MessageInterpolator.Context;
import javax.validation.Validation;
import javax.validation.ValidatorFactory;
import javax.validation.metadata.ConstraintDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.TypeVariable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static cz.jirutka.validator.collection.internal.AnnotationUtils.*;
import static cz.jirutka.validator.collection.internal.ConstraintValidatorContextUtils.addConstraintViolationInIterable;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableMap;
import static org.apache.commons.lang3.StringUtils.isEmpty;

/**
 * Common validator for collection constraints that validates each element of
 * the given collection.
 */
@SuppressWarnings("unchecked")
public class CommonEachValidator implements ConstraintValidator> {

    private static final Logger LOG = LoggerFactory.getLogger(CommonEachValidator.class);

    private static final ConstraintDescriptorFactory DESCRIPTOR_FACTORY = ConstraintDescriptorFactory.newInstance();

    // injected by container, or set default during initialization
    private @Inject ValidatorFactory factory;

    // after initialization it's read-only
    private List descriptors;

    // after initialization it's read-only
    private Map>> validators;

    // modifiable after initialization; must be thread-safe!
    private Map validatorInstances;

    // after initialization it's read-only
    private boolean earlyInterpolation;


    public void initialize(Annotation eachAnnotation) {

        Class eachAType = eachAnnotation.annotationType();
        LOG.trace("Initializing CommonEachValidator for {}", eachAType);

        if (factory == null) {
            LOG.debug("No ValidatorFactory injected, building default one");
            factory = Validation.buildDefaultValidatorFactory();
        }
        validatorInstances = new ConcurrentHashMap<>(2);

        if (eachAType.isAnnotationPresent(EachConstraint.class)) {
            Class constraintClass = eachAType.getAnnotation(EachConstraint.class).validateAs();

            Annotation constraint = createConstraintAndCopyAttributes(constraintClass, eachAnnotation);
            ConstraintDescriptor descriptor = createConstraintDescriptor(constraint);

            descriptors = unmodifiableList(asList(descriptor));

        // legacy and deprecated, will be removed in next major version!
        } else if (isWrapperAnnotation(eachAType)) {
            Annotation[] constraints = unwrapConstraints(eachAnnotation);
            Validate.notEmpty(constraints, "%s annotation does not contain any constraint", eachAType);

            List list = new ArrayList<>(constraints.length);
            for (Annotation constraint : constraints) {
                list.add( createConstraintDescriptor(constraint) );
            }
            descriptors = unmodifiableList(list);
            earlyInterpolation = true;

            LOG.info("You're using legacy @EachX annotation style, this will be removed soon! " +
                     "Please update your @EachX annotations.");

        } else {
            throw new IllegalArgumentException(String.format(
                    "%s is not annotated with @EachConstraint and doesn't declare 'value' of type Annotation[] either.",
                    eachAType.getName()));
        }
        // constraints are always of the same type, so just pick first
        ConstraintDescriptor descriptor = descriptors.get(0);

        validators = categorizeValidatorsByType(descriptor.getConstraintValidatorClasses());
        Validate.notEmpty(validators,
                "No validator found for constraint: %s", descriptor.getAnnotation().annotationType());
    }

    public boolean isValid(Collection collection, ConstraintValidatorContext context) {
        if (collection == null || collection.isEmpty()) {
            return true;  //nothing to validate here
        }
        context.disableDefaultConstraintViolation();  //do not add wrapper's message

        int index = 0;
        for (Iterator it = collection.iterator(); it.hasNext(); index++) {
            Object element = it.next();

            ConstraintValidator validator = element != null
                    ? getValidatorInstance(element.getClass())
                    : getAnyValidatorInstance();

            for (ConstraintDescriptor descriptor : descriptors) {
                validator.initialize(descriptor.getAnnotation());

                if (! validator.isValid(element, context)) {
                    LOG.debug("Element [{}] = '{}' is invalid according to: {}",
                            index, element, validator.getClass().getName());

                    // early interpolation hack is needed only for legacy annotations
                    // and will go away with them
                    String message = earlyInterpolation
                            ? createInterpolatedMessage(descriptor, element)
                            : readAttribute(descriptor.getAnnotation(), "message", String.class);

                    addConstraintViolationInIterable(context, message, index);
                    return false;
                }
            }
        }
        return true;
    }

    public void setValidatorFactory(ValidatorFactory factory) {
        this.factory = factory;
    }


    /**
     * Whether the given annotation type contains the {@code value} attribute
     * of the type that extends {@code Annotation[]}.
     */
    protected boolean isWrapperAnnotation(Class annotationType) {
        return hasAttribute(annotationType, "value")
                && Annotation[].class.isAssignableFrom(getAttributeType(annotationType, "value"));
    }

    protected Annotation[] unwrapConstraints(Annotation wrapper) {
        return readAttribute(wrapper, "value", Annotation[].class);
    }

    protected ConstraintDescriptor createConstraintDescriptor(Annotation constraint) {
        return DESCRIPTOR_FACTORY.buildConstraintDescriptor(constraint);
    }

    protected >
            Map> categorizeValidatorsByType(List> validatorClasses) {

        Map> validators = new LinkedHashMap<>(10);

        for (Class validator : validatorClasses) {
            Class type = determineTargetType(validator);
            if (type.isArray()) continue;

            LOG.trace("Found validator {} for type {}", validator.getName(), type.getName());
            validators.put(type, validator);
        }
        return unmodifiableMap(validators);
    }

    protected Class determineTargetType(Class> validatorClass) {
        TypeVariable typeVar = ConstraintValidator.class.getTypeParameters()[1];
        return TypeUtils.getRawType(typeVar, validatorClass);
    }

    /**
     * Returns initialized validator instance for the specified object type.
     * Instances are cached.
     *
     * @param type Type of the object to be validated.
     */
    protected ConstraintValidator getValidatorInstance(Class type) {
        ConstraintValidator validator = validatorInstances.get(type);

        if (validator == null) {
            validator = findAndInitializeValidator(type);
            validatorInstances.put(type, validator);
        }
        return validator;
    }

    /**
     * Returns initialized validator instance for any object type. This is used
     * when the object to be validated is null so we can't determine
     * it's type. Instances are cached.
     */
    protected ConstraintValidator getAnyValidatorInstance() {

        if (validatorInstances.isEmpty()) {
            Class type = validators.keySet().iterator().next();
            return findAndInitializeValidator(type);

        } else {
            return validatorInstances.values().iterator().next();
        }
    }

    protected ConstraintValidator findAndInitializeValidator(Class type) {
        LOG.trace("Looking for validator for type: {}", type.getName());

        for (Class clazz : validators.keySet()) {
            if (clazz.isAssignableFrom(type)) {

                Class validatorClass = validators.get(clazz);

                LOG.trace("Initializing validator: {}", validatorClass.getName());
                return factory.getConstraintValidatorFactory().getInstance(validatorClass);
            }
        }
        throw new IllegalArgumentException("No validator found for type: " + type.getName());
    }

    /**
     * Reads and interpolates an error message for the given constraint and
     * value.
     *
     * @param descriptor Descriptor of the constraint that the value violated.
     * @param value The validated value.
     * @return An interpolated message.
     */
    protected String createInterpolatedMessage(ConstraintDescriptor descriptor, Object value) {
        Context context = new MessageInterpolatorContext(descriptor, value);

        Annotation constraint = descriptor.getAnnotation();
        String template = readAttribute(constraint, "message", String.class);

        return factory.getMessageInterpolator().interpolate(template, context);
    }

    /**
     * Instantiates constraint of the specified type and copies values of all
     * the common attributes from the given source constraint (of any type)
     * to it.
     *
     * 

If the source constraint's {@code message} is empty, then it will * not copy it (so the default {@code message} of the target * constraint will be preserved).

* * @param constraintType Type of the constraint to create. * @param source Any annotation to copy attribute values from. * @return An instance of the specified constraint. */ protected T createConstraintAndCopyAttributes(Class constraintType, Annotation source) { Map attributes = readAllAttributes(source); // if message is not set, keep message from original constraint instead if (isEmpty((String) attributes.get("message"))) { attributes.remove("message"); } return createAnnotation(constraintType, attributes); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy