| /* |
| * 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; |
| } |
| } |
| } |