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

io.micronaut.validation.validator.constraints.DefaultConstraintValidators Maven / Gradle / Ivy

/*
 * Copyright 2017-2020 original authors
 *
 * Licensed 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
 *
 * https://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 io.micronaut.validation.validator.constraints;

import io.micronaut.context.BeanContext;
import io.micronaut.context.Qualifier;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.reflect.ReflectionUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap;
import io.micronaut.inject.qualifiers.Qualifiers;
import io.micronaut.inject.qualifiers.TypeArgumentQualifier;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * A factory bean that contains implementation for many of the default validations.
 * This approach is preferred as it generates less classes and smaller byte code than defining a
 * validator class for each case.
 *
 * @author graemerocher
 * @since 1.2
 */
@Singleton
@Introspected
public class DefaultConstraintValidators implements ConstraintValidatorRegistry {

    private final Map> validatorCache = new ConcurrentLinkedHashMap.
            Builder>().initialCapacity(10).maximumWeightedCapacity(40).build();

    @Nullable
    private final BeanContext beanContext;
    private final Map> internalValidators;

    /**
     * Default constructor.
     */
    public DefaultConstraintValidators() {
        this(null);
    }

    /**
     * Constructor used for DI.
     *
     * @param beanContext The bean context
     */
    @Inject
    public DefaultConstraintValidators(@Nullable BeanContext beanContext) {
        this.beanContext = beanContext;
        List, ConstraintValidator>> constraintValidators = InternalConstraintValidators.getConstraintValidators();
        Map> validatorMap = CollectionUtils.newHashMap(constraintValidators.size());
        for (Map.Entry, ConstraintValidator> entry : constraintValidators) {
            Argument definition = entry.getKey();
            ConstraintValidator constraintValidator = entry.getValue();
            final Argument[] typeParameters = definition.getTypeParameters();
            if (ArrayUtils.isEmpty(typeParameters)) {
                continue;
            }

            final int len = typeParameters.length;
            if (len == 2) {
                final Class targetType = ReflectionUtils.getWrapperType(typeParameters[1].getType());
                final ValidatorKey key = new ValidatorKey(typeParameters[0].getType(), targetType);
                validatorMap.put(key, constraintValidator);
            } else if (len == 1) {
                Class type = typeParameters[0].getType();
                if (constraintValidator instanceof SizeValidator) {
                    final ValidatorKey key = new ValidatorKey(Size.class, type);
                    validatorMap.put(key, constraintValidator);
                } else if (constraintValidator instanceof DigitsValidator) {
                    final ValidatorKey key = new ValidatorKey(Digits.class, type);
                    validatorMap.put(key, constraintValidator);
                } else if (constraintValidator instanceof DecimalMaxValidator) {
                    final ValidatorKey key = new ValidatorKey(DecimalMax.class, type);
                    validatorMap.put(key, constraintValidator);
                } else if (constraintValidator instanceof DecimalMinValidator) {
                    final ValidatorKey key = new ValidatorKey(DecimalMin.class, type);
                    validatorMap.put(key, constraintValidator);
                }
            }
        }
        validatorMap.put(
                new ValidatorKey(Pattern.class, CharSequence.class),
                new PatternValidator()
        );
        validatorMap.put(
                new ValidatorKey(Email.class, CharSequence.class),
                new EmailValidator()
        );
        this.internalValidators = validatorMap;
    }

    @SuppressWarnings("unchecked")
    @NonNull
    @Override
    public  Optional> findConstraintValidator(@NonNull Class constraintType, @NonNull Class targetType) {
        ArgumentUtils.requireNonNull("constraintType", constraintType);
        ArgumentUtils.requireNonNull("targetType", targetType);
        final ValidatorKey key = new ValidatorKey(constraintType, targetType);
        targetType = (Class) ReflectionUtils.getWrapperType(targetType);
        ConstraintValidator constraintValidator = internalValidators.get(key);
        if (constraintValidator != null) {
            return Optional.of((ConstraintValidator) constraintValidator);
        } else {
            constraintValidator = validatorCache.get(key);
            if (constraintValidator != null) {
                return Optional.of((ConstraintValidator) constraintValidator);
            } else {
                Optional> local = findInternalConstraintValidator(constraintType, targetType);
                if (local.isPresent()) {
                    validatorCache.put(key, local.get());
                    return local;
                } else if (beanContext != null) {
                    Argument> argument = (Argument) Argument.of(ConstraintValidator.class);
                    final Qualifier> qualifier = Qualifiers.byTypeArguments(
                            constraintType,
                            targetType
                    );
                    Optional> bean = beanContext.findBean(argument, qualifier);
                    if (bean.isEmpty()) {
                        validatorCache.put(key, ConstraintValidator.VALID);
                    } else {
                        ConstraintValidator found = bean.get();
                        validatorCache.put(key, found);
                        return Optional.of(found);
                    }
                } else {
                    // last chance lookup
                    final ConstraintValidator cv = findLocalConstraintValidator(constraintType, targetType)
                            .orElse((ConstraintValidator) ConstraintValidator.VALID);
                    validatorCache.put(key, cv);
                    if (cv != ConstraintValidator.VALID) {
                        return Optional.of(cv);
                    }
                }
            }
        }
        return Optional.empty();
    }

    private  Optional> findInternalConstraintValidator(Class constraintType,
                                                                                                          Class validationType) {
        final Class[] finalTypeArguments = {constraintType, validationType};
        return internalValidators.entrySet().stream()
                .filter(entry -> {
                            final ValidatorKey k = entry.getKey();
                            return TypeArgumentQualifier.areTypesCompatible(finalTypeArguments, Arrays.asList(k.constraintType, k.targetType));
                        }
                ).map(e -> (ConstraintValidator) e.getValue())
                .findFirst();
    }

    /**
     * Last chance resolve for constraint validator.
     *
     * @param constraintType The constraint type
     * @param targetType     The target type
     * @param             The annotation type
     * @param             The target type
     * @return The validator if present
     */
    protected  Optional> findLocalConstraintValidator(@NonNull Class constraintType,
                                                                                                         @NonNull Class targetType) {
        return Optional.empty();
    }

    /**
     * Key for caching validators.
     *
     * @param constraintType The constraint type
     * @param targetType     The target type
     */
    public record ValidatorKey(@NonNull Class constraintType,
                               @NonNull Class targetType) {
    }

}