blob: fa1bc568420f32b377e0baa18a6791e1e8d74b8f [file] [log] [blame]
/*
* Copyright (c) 2015, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Marcel Valovy - 2.6 - initial API and implementation
// Dmitry Kornilov - 2.6.1 - removed static maps
package org.eclipse.persistence.jaxb;
import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import jakarta.validation.Constraint;
import jakarta.validation.Valid;
import jakarta.validation.constraints.AssertFalse;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import org.eclipse.persistence.internal.cache.AdvancedProcessor;
import org.eclipse.persistence.internal.cache.ComputableTask;
import org.eclipse.persistence.logging.AbstractSessionLog;
import org.eclipse.persistence.logging.SessionLog;
/**
* INTERNAL:
* Asynchronously starts validation.xml file processing when created. Holds a map of classes with BV annotations.
*
* @author Marcel Valovy, Dmitry Kornilov
* @since 2.6
*/
final public class BeanValidationHelper {
private Future<Map<Class<?>, Boolean>> future;
/**
* Advanced memoizer.
*/
private final AdvancedProcessor memoizer = new AdvancedProcessor();
/**
* Computable task for memoizer.
*/
private final ConstraintsDetectorService<Class<?>, Boolean> cds = new ConstraintsDetectorService<>();
/**
* Set of all default BeanValidation field or method annotations and known custom field or method constraints.
*/
private final Set<Class<? extends Annotation>> knownConstraints = new HashSet<>();
/**
* Map of all classes that have undergone check for bean validation constraints.
* Maps the key with boolean value telling whether the class contains an annotation from {@link #knownConstraints}.
*/
private Map<Class<?>, Boolean> constraintsOnClasses = null;
{
knownConstraints.add(Valid.class);
knownConstraints.add(Max.class);
knownConstraints.add(Min.class);
knownConstraints.add(DecimalMax.class);
knownConstraints.add(DecimalMin.class);
knownConstraints.add(Digits.class);
knownConstraints.add(NotNull.class);
knownConstraints.add(Pattern.class);
knownConstraints.add(Size.class);
knownConstraints.add(AssertTrue.class);
knownConstraints.add(AssertFalse.class);
knownConstraints.add(jakarta.validation.constraints.Future.class);
knownConstraints.add(Past.class);
knownConstraints.add(Max.List.class);
knownConstraints.add(Min.List.class);
knownConstraints.add(DecimalMax.List.class);
knownConstraints.add(DecimalMin.List.class);
knownConstraints.add(Digits.List.class);
knownConstraints.add(NotNull.List.class);
knownConstraints.add(Pattern.List.class);
knownConstraints.add(Size.List.class);
knownConstraints.add(AssertTrue.List.class);
knownConstraints.add(AssertFalse.List.class);
knownConstraints.add(jakarta.validation.constraints.Future.List.class);
knownConstraints.add(Past.List.class);
}
/**
* Creates a new instance. Starts asynchronous parsing of validation.xml if it exists.
*/
public BeanValidationHelper() {
// Try to run validation.xml parsing asynchronously if validation.xml exists
if (ValidationXMLReader.isValidationXmlPresent()) {
parseValidationXmlAsync();
} else {
// validation.xml doesn't exist -> create an empty map
constraintsOnClasses = new HashMap<>();
}
}
/**
* Tells whether any of the class's {@link java.lang.reflect.AccessibleObject}s are constrained by Bean
* Validation annotations or custom constraints.
*
* @param clazz checked class
* @return true or false
*/
boolean isConstrained(Class<?> clazz) {
return memoizer.compute(cds, clazz);
}
@SuppressWarnings("unchecked")
public class ConstraintsDetectorService<A, V> implements ComputableTask<A, V> {
@Override
public V compute(A arg) throws InterruptedException {
Boolean b = isConstrained0((Class<?>) arg);
return (V) b;
}
private Boolean isConstrained0(Class<?> clazz) {
Boolean constrained = getConstraintsMap().get(clazz);
if (constrained == null) {
constrained = detectConstraints(clazz);
constraintsOnClasses.put(clazz, constrained);
}
return constrained;
}
/**
* INTERNAL:
* Determines whether this class is subject to validation according to rules in BV spec.
* Will accept upon first detected annotation - faster.
* Uses reflection, recursion and DP.
*/
private Boolean detectConstraints(Class<?> clazz) {
if (detectAncestorConstraints(clazz)) return true;
for (Field f : ReflectionUtils.getDeclaredFields(clazz)) {
if ((f.getModifiers() & Modifier.STATIC) != 0) continue; // section 4.1 of BV spec
if (detectFirstClassConstraints(f)) return true;
}
for (Method m : ReflectionUtils.getDeclaredMethods(clazz)) {
if ((m.getModifiers() & Modifier.STATIC) != 0) continue; // section 4.1 of BV spec
if (detectFirstClassConstraints(m) || detectParameterConstraints(m)) return true;
}
// length 0 if an interface, a primitive type, an array class, or void
for (Constructor<?> c : ReflectionUtils.getDeclaredConstructors(clazz)) {
if (clazz.isEnum()) continue; // cannot construct enum instances during runtime
if (detectFirstClassConstraints(c) || detectParameterConstraints(c)) return true;
}
return false;
}
/**
* Recursively detects constraints on ancestors. Uses strong form of dynamic programming.
*
* @param clazz class whose ancestors are to be scanned
* @return true if any of the ancestors are constrained
*/
private boolean detectAncestorConstraints(Class<?> clazz) {
/* If this Class represents either the Object class, an interface, a primitive type, or void, then null is
returned. If this object represents an array class then the Class object representing the Object class
is returned. */
Class<?> superClass = clazz.getSuperclass();
if (superClass == null) return false;
return memoizer.compute(cds, superClass);
}
private boolean detectFirstClassConstraints(AccessibleObject accessibleObject) {
for (Annotation a : accessibleObject.getDeclaredAnnotations()) {
final Class<? extends Annotation> annType = a.annotationType();
if (knownConstraints.contains(annType)){
return true;
}
// detect custom annotations
for (Annotation annOnAnnType : annType.getAnnotations()) {
final Class<? extends Annotation> annTypeOnAnnType = annOnAnnType.annotationType();
if (Constraint.class == annTypeOnAnnType) {
knownConstraints.add(annType);
return true;
}
}
}
return false;
}
private boolean detectParameterConstraints(Executable c) {
for (Annotation[] aa : c.getParameterAnnotations())
for (Annotation a : aa) {
final Class<? extends Annotation> annType = a.annotationType();
if (knownConstraints.contains(annType)) {
return true;
}
// detect custom annotations
for (Annotation annOnAnnType : annType.getAnnotations()) {
final Class<? extends Annotation> annTypeOnAnnType = annOnAnnType.annotationType();
if (Constraint.class == annTypeOnAnnType) {
knownConstraints.add(annType);
return true;
}
}
}
return false;
}
}
/**
* Lazy getter for constraintsOnClasses property. Waits until the map is returned by async XML reader.
*/
public Map<Class<?>, Boolean> getConstraintsMap() {
if (constraintsOnClasses == null) {
if (future == null) {
// This happens when submission of async task is failed. Run validation.xml parsing synchronously.
constraintsOnClasses = parseValidationXml();
} else {
// Async task was successfully submitted. get a result from future
try {
constraintsOnClasses = future.get();
} catch (InterruptedException | ExecutionException e) {
// For some reason the async parsing attempt failed. Call it synchronously.
AbstractSessionLog.getLog().log(SessionLog.WARNING, SessionLog.MOXY, "Error parsing validation.xml the async way", new Object[0], false);
AbstractSessionLog.getLog().logThrowable(SessionLog.WARNING, SessionLog.MOXY, e);
constraintsOnClasses = parseValidationXml();
}
}
}
return constraintsOnClasses;
}
/**
* Tries to run validation.xml parsing asynchronously.
*/
private void parseValidationXmlAsync() {
Executor executor = null;
try {
executor = createExecutor();
future = executor.executorService.submit(new ValidationXMLReader());
} catch (Throwable e) {
// In the rare cases submitting a task throws OutOfMemoryError. In this case we call validation.xml
// parsing lazily when requested
AbstractSessionLog.getLog().log(SessionLog.WARNING, SessionLog.MOXY, "Error creating/submitting async validation.xml parsing task.", new Object[0], false);
AbstractSessionLog.getLog().logThrowable(SessionLog.WARNING, SessionLog.MOXY, e);
future = null;
} finally {
// Shutdown is needed only for JDK executor
if (executor != null && executor.shutdownNeeded) {
executor.executorService.shutdown();
}
}
}
/**
* Runs validation.xml parsing synchronously.
*/
private Map<Class<?>, Boolean> parseValidationXml() {
final ValidationXMLReader reader = new ValidationXMLReader();
Map<Class<?>, Boolean> result;
try {
result = reader.call();
} catch (Exception e) {
AbstractSessionLog.getLog().log(SessionLog.WARNING, SessionLog.MOXY, "Error parsing validation.xml synchronously", new Object[0], false);
AbstractSessionLog.getLog().logThrowable(SessionLog.WARNING, SessionLog.MOXY, e);
result = new HashMap<>();
}
return result;
}
/**
* Creates an executor service. Tries to get a managed executor service. If failed creates a JDK one.
* Sets shutdownNeeded property to true in case JDK executor is created.
*/
private Executor createExecutor() {
try {
InitialContext jndiCtx = new InitialContext();
// type: jakarta.enterprise.concurrent.ManagedExecutorService
// jndi-name: concurrent/ThreadPool
return new Executor((ExecutorService) jndiCtx.lookup("java:comp/env/concurrent/ThreadPool"), false);
} catch (NamingException ignored) {
// aka continue to proceed with retrieving jdk executor
}
return new Executor(Executors.newFixedThreadPool(1), true);
}
/**
* This class holds a managed or JDK executor instance with a flag indicating do we need to shut it down or not.
*/
private static class Executor {
ExecutorService executorService;
boolean shutdownNeeded;
Executor(ExecutorService executorService, boolean shutdownNeeded) {
this.executorService = executorService;
this.shutdownNeeded = shutdownNeeded;
}
}
}