blob: 042fa7e88ede4aec7b68b3e2b7f474d4c8eea62e [file] [log] [blame]
/*
* 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.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<ExecutableType> removeFrom(Collection<ExecutableType> coll,
ExecutableType... executableTypes) {
Validate.notNull(coll, "collection was null");
if (!(coll.isEmpty() || ObjectUtils.isEmptyArray(executableTypes))) {
final List<ExecutableType> toRemove = Arrays.asList(executableTypes);
if (!Collections.disjoint(coll, toRemove)) {
final Set<ExecutableType> result = EnumSet.copyOf(coll);
result.removeAll(toRemove);
return result;
}
}
return coll;
}
private transient volatile Set<ExecutableType> classConfiguration;
private transient volatile Map<Signature, Boolean> executableValidation;
@Inject
private Validator validator;
@Inject
private BValExtension globalConfiguration;
private transient volatile ExecutableValidator executableValidator;
@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<ConstraintViolation<?>> violations =
executableValidator.validateConstructorParameters(ctor, context.getParameters());
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
final Object result = context.proceed();
if (constraints.hasConstrainedReturnValue()) {
final Set<ConstraintViolation<?>> 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 = Proxies.classFor(context.getTarget().getClass());
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<ConstraintViolation<Object>> 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<ConstraintViolation<Object>> violations =
executableValidator.validateReturnValue(context.getTarget(), method, result);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
return result;
}
private <T> boolean isConstructorValidated(final Constructor<T> constructor)
{
return isExecutableValidated(constructor.getDeclaringClass(), constructor, this::computeIsConstructorValidated);
}
private <T, E extends Executable> boolean isExecutableValidated(final Class<T> targetClass, final E executable,
BiPredicate<? super Class<T>, ? 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 <T> boolean computeIsConstructorValidated(Class<T> targetClass, Constructor<T> ctor) {
final AnnotatedType<T> 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<ExecutableType> validatedExecutableTypes =
annotation == null ? classConfiguration : ExecutableTypes.interpret(annotation.type());
return validatedExecutableTypes.contains(ExecutableType.CONSTRUCTORS);
}
private <T> boolean computeIsMethodValidated(Class<T> 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<ExecutableType> declaredExecutableTypes;
if (declaringMethod.isAnnotationPresent(ValidateOnExecution.class)) {
final List<ExecutableType> 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();
}
}
}
}
}