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

org.apache.bval.cdi.BValInterceptor Maven / Gradle / Ivy

There is a newer version: 10.0.0-M3
Show newest version
/*
 * 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.cdi;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiPredicate;

import javax.annotation.Priority;
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.CDI;
import javax.inject.Inject;
import javax.interceptor.AroundConstruct;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InterceptorBinding;
import javax.interceptor.InvocationContext;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import javax.validation.executable.ExecutableType;
import javax.validation.executable.ExecutableValidator;
import javax.validation.executable.ValidateOnExecution;
import javax.validation.metadata.ConstructorDescriptor;
import javax.validation.metadata.MethodDescriptor;

import org.apache.bval.jsr.descriptor.DescriptorManager;
import org.apache.bval.jsr.metadata.Signature;
import org.apache.bval.jsr.util.ExecutableTypes;
import org.apache.bval.jsr.util.Methods;
import org.apache.bval.jsr.util.Proxies;
import org.apache.bval.util.ObjectUtils;
import org.apache.bval.util.Validate;
import org.apache.bval.util.reflection.Reflection;
import org.apache.bval.util.reflection.Reflection.Interfaces;

/**
 * Interceptor class for the {@link BValBinding} {@link InterceptorBinding}.
 */
@SuppressWarnings("serial")
@Interceptor
@BValBinding
@Priority(4800)
// TODO: maybe add it through ASM to be compliant with CDI 1.0 containers using simply this class as a template to
// generate another one for CDI 1.1 impl
public class BValInterceptor implements Serializable {
    private static Collection removeFrom(Collection coll,
        ExecutableType... executableTypes) {
        Validate.notNull(coll, "collection was null");
        if (!(coll.isEmpty() || ObjectUtils.isEmptyArray(executableTypes))) {
            final List toRemove = Arrays.asList(executableTypes);
            if (!Collections.disjoint(coll, toRemove)) {
                final Set result = EnumSet.copyOf(coll);
                result.removeAll(toRemove);
                return result;
            }
        }
        return coll;
    }

    private transient volatile Set classConfiguration;
    private transient volatile Map executableValidation;

    @Inject
    private Validator validator;

    @Inject
    private BValExtension globalConfiguration;

    private transient volatile ExecutableValidator executableValidator;
    private transient volatile ConcurrentMap, Class> classMapping;

    @SuppressWarnings({ "unchecked", "rawtypes" })
    @AroundConstruct // TODO: see previous one
    public Object construct(InvocationContext context) throws Exception {
        final Constructor ctor = context.getConstructor();
        if (!isConstructorValidated(ctor)) {
            return context.proceed();
        }
        final ConstructorDescriptor constraints = validator.getConstraintsForClass(ctor.getDeclaringClass())
            .getConstraintsForConstructor(ctor.getParameterTypes());

        if (!DescriptorManager.isConstrained(constraints)) {
            return context.proceed();
        }
        initExecutableValidator();

        if (constraints.hasConstrainedParameters()) {
            final Set> violations =
                executableValidator.validateConstructorParameters(ctor, context.getParameters());
            if (!violations.isEmpty()) {
                throw new ConstraintViolationException(violations);
            }
        }
        final Object result = context.proceed();

        if (constraints.hasConstrainedReturnValue()) {
            final Set> violations =
                executableValidator.validateConstructorReturnValue(ctor, context.getTarget());
            if (!violations.isEmpty()) {
                throw new ConstraintViolationException(violations);
            }
        }
        return result;
    }

    @AroundInvoke
    public Object invoke(final InvocationContext context) throws Exception {
        final Method method = context.getMethod();
        final Class targetClass = getTargetClass(context);

        if (!isExecutableValidated(targetClass, method, this::computeIsMethodValidated)) {
            return context.proceed();
        }

        final MethodDescriptor constraintsForMethod = validator.getConstraintsForClass(targetClass)
            .getConstraintsForMethod(method.getName(), method.getParameterTypes());

        if (!DescriptorManager.isConstrained(constraintsForMethod)) {
            return context.proceed();
        }
        initExecutableValidator();

        if (constraintsForMethod.hasConstrainedParameters()) {
            final Set> violations =
                executableValidator.validateParameters(context.getTarget(), method, context.getParameters());
            if (!violations.isEmpty()) {
                throw new ConstraintViolationException(violations);
            }
        }
        final Object result = context.proceed();

        if (constraintsForMethod.hasConstrainedReturnValue()) {
            final Set> violations =
                executableValidator.validateReturnValue(context.getTarget(), method, result);
            if (!violations.isEmpty()) {
                throw new ConstraintViolationException(violations);
            }
        }
        return result;
    }

    private Class getTargetClass(final InvocationContext context) {
        final Class key = context.getTarget().getClass();
        if (classMapping == null) {
            synchronized (this) {
                if (classMapping == null) {
                    classMapping = new ConcurrentHashMap<>();
                }
            }
        }
        Class mapped = classMapping.get(key);
        if (mapped == null) {
            mapped = Proxies.classFor(key);
            classMapping.putIfAbsent(key, mapped);
        }
        return mapped;
    }

    private  boolean isConstructorValidated(final Constructor constructor) {
        return isExecutableValidated(constructor.getDeclaringClass(), constructor, this::computeIsConstructorValidated);
    }

    private  boolean isExecutableValidated(final Class targetClass, final E executable,
        BiPredicate, ? super E> compute) {
        initClassConfig(targetClass);

        if (executableValidation == null) {
            synchronized (this) {
                if (executableValidation == null) {
                    executableValidation = new ConcurrentHashMap<>();
                }
            }
        }
        return executableValidation.computeIfAbsent(Signature.of(executable),
            s -> compute.test(targetClass, executable));
    }

    private void initClassConfig(Class targetClass) {
        if (classConfiguration == null) {
            synchronized (this) {
                if (classConfiguration == null) {
                    final AnnotatedType annotatedType = CDI.current().getBeanManager()
                        .createAnnotatedType(targetClass);

                    if (annotatedType.isAnnotationPresent(ValidateOnExecution.class)) {
                        // implicit does not apply at the class level:
                        classConfiguration = ExecutableTypes.interpret(
                            removeFrom(Arrays.asList(annotatedType.getAnnotation(ValidateOnExecution.class).type()),
                                ExecutableType.IMPLICIT));
                    } else {
                        classConfiguration = globalConfiguration.getGlobalExecutableTypes();
                    }
                }
            }
        }
    }

    private  boolean computeIsConstructorValidated(Class targetClass, Constructor ctor) {
        final AnnotatedType annotatedType =
            CDI.current().getBeanManager().createAnnotatedType(ctor.getDeclaringClass());

        final ValidateOnExecution annotation =
            annotatedType.getConstructors().stream().filter(ac -> ctor.equals(ac.getJavaMember())).findFirst()
                .map(ac -> ac.getAnnotation(ValidateOnExecution.class))
                .orElseGet(() -> ctor.getAnnotation(ValidateOnExecution.class));

        final Set validatedExecutableTypes =
            annotation == null ? classConfiguration : ExecutableTypes.interpret(annotation.type());

        return validatedExecutableTypes.contains(ExecutableType.CONSTRUCTORS);
    }

    private  boolean computeIsMethodValidated(Class targetClass, Method method) {
        final Signature signature = Signature.of(method);

        AnnotatedMethod declaringMethod = null;

        for (final Class c : Reflection.hierarchy(targetClass, Interfaces.INCLUDE)) {
            final AnnotatedType annotatedType = CDI.current().getBeanManager().createAnnotatedType(c);

            final AnnotatedMethod annotatedMethod = annotatedType.getMethods().stream()
                .filter(am -> Signature.of(am.getJavaMember()).equals(signature)).findFirst().orElse(null);

            if (annotatedMethod != null) {
                declaringMethod = annotatedMethod;
            }
        }
        if (declaringMethod == null) {
            return false;
        }
        final Collection declaredExecutableTypes;

        if (declaringMethod.isAnnotationPresent(ValidateOnExecution.class)) {
            final List validatedTypesOnMethod =
                Arrays.asList(declaringMethod.getAnnotation(ValidateOnExecution.class).type());

            // implicit directly on method -> early return:
            if (validatedTypesOnMethod.contains(ExecutableType.IMPLICIT)) {
                return true;
            }
            declaredExecutableTypes = validatedTypesOnMethod;
        } else {
            final AnnotatedType declaringType = declaringMethod.getDeclaringType();
            if (declaringType.isAnnotationPresent(ValidateOnExecution.class)) {
                // IMPLICIT is meaningless at class level:
                declaredExecutableTypes =
                    removeFrom(Arrays.asList(declaringType.getAnnotation(ValidateOnExecution.class).type()),
                        ExecutableType.IMPLICIT);
            } else {
                final Package pkg = declaringType.getJavaClass().getPackage();
                if (pkg != null && pkg.isAnnotationPresent(ValidateOnExecution.class)) {
                    // presumably IMPLICIT is likewise meaningless at package level:
                    declaredExecutableTypes = removeFrom(
                        Arrays.asList(pkg.getAnnotation(ValidateOnExecution.class).type()), ExecutableType.IMPLICIT);
                } else {
                    declaredExecutableTypes = null;
                }
            }
        }
        final ExecutableType methodType =
            Methods.isGetter(method) ? ExecutableType.GETTER_METHODS : ExecutableType.NON_GETTER_METHODS;

        return Optional.ofNullable(declaredExecutableTypes).map(ExecutableTypes::interpret)
            .orElse(globalConfiguration.getGlobalExecutableTypes()).contains(methodType);
    }

    private void initExecutableValidator() {
        if (executableValidator == null) {
            synchronized (this) {
                if (executableValidator == null) {
                    executableValidator = validator.forExecutables();
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy