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

org.eclipse.persistence.jaxb.JAXBBeanValidator Maven / Gradle / Ivy

There is a newer version: 5.0.0-B03
Show newest version
/*
 * Copyright (c) 2015, 2021 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 implementation
package org.eclipse.persistence.jaxb;

import java.security.CodeSource;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Path;
import jakarta.validation.Validation;
import jakarta.validation.ValidationException;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import jakarta.validation.groups.Default;

import org.eclipse.persistence.exceptions.BeanValidationException;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.jaxb.xmlmodel.XmlBindings;
import org.eclipse.persistence.logging.DefaultSessionLog;
import org.eclipse.persistence.logging.SessionLog;

/**
 * INTERNAL:
 *
 * JAXB Bean Validator. Serves three purposes:
 *  1. Determines if the validation callback should take place on the (un)marshal call.
 *  2. Processes the validation.
 *  3. Stores the constraintViolations from the last validation call.
 *
 * @author Marcel Valovy - [email protected]
 * @since 2.6
 */
class JAXBBeanValidator {

    /**
     * Represents the Default validation group. Storing it in constant saves resources.
     */
    static final Class[] DEFAULT_GROUP_ARRAY = new Class[] { Default.class };

    /**
     * Represents the difference between words 'marshalling' and 'unmarshalling';
     */
    private static final String PREFIX_UNMARSHALLING = "un";

    /**
     * Prevents endless invocation loops between unmarshaller - validator - unmarshaller.
     * Only used / needed in case {@link #noOptimisation} is {@code true}.
     */
    private static final ReentrantLock lock = new ReentrantLock();

    /**
     * Disable optimisations that skip bean validation processes on non-constrained objects.
     */
    private boolean noOptimisation = false;

    /**
     * Stores {@link #PREFIX_UNMARSHALLING} if this instance belongs to
     * {@link org.eclipse.persistence.jaxb.JAXBUnmarshaller}, otherwise stores empty String.
     */
    private final String prefix;

    /**
     * Reference to {@link org.eclipse.persistence.jaxb.JAXBContext}. Allows for callbacks.
     */
    private final JAXBContext context;

    /**
     * Stores the {@link jakarta.validation.Validator} implementation. Once found, the reference is preserved.
     */
    private Validator validator;

    /**
     * Stores constraint violations returned by last call to {@link jakarta.validation.Validator#validate(Object, Class[])}.
     * 

After each {@link #validate(Object, Class[])} call, the reference is replaced. */ private Set> constraintViolations = Collections.emptySet(); /** * Computed value saying if the validation can proceed under current conditions, represented by: *

     *     - {@link #beanValidationMode}
     *     - {@link jakarta.validation.Validator} implementation present on classpath
     * 
*

* Value is recomputed only on {@link #changeInternalState()} call. */ private boolean canValidate; /** * Represents a state where {@link #beanValidationMode} mode is set to * {@link org.eclipse.persistence.jaxb.BeanValidationMode#AUTO} and BV implementation could not be found. */ private boolean stopSearchingForValidator; /** * This field will usually be {@code null}. However, user may pass his own instance of * {@link jakarta.validation.ValidatorFactory} to * {@link #shouldValidate}() method, and it will be assigned to this field. *

* If not null, {@link #validator} field will be assigned only by calling method * {@link jakarta.validation.ValidatorFactory#getValidator()} the instance assigned to this field. */ private ValidatorFactory validatorFactory; /** * Setting initial value to "NONE" will not trigger internalStateChange() when validation is off and save resources. */ private BeanValidationMode beanValidationMode = BeanValidationMode.NONE; // Local logger instance. private final SessionLog log = new DefaultSessionLog(); /** * Private constructor. Only to be called by factory methods. * @param prefix differentiates between marshaller and unmarshaller during logging * @param context jaxb context reference */ private JAXBBeanValidator(String prefix, JAXBContext context) { this.prefix = prefix; this.context = context; } /** * Factory method. *

* The only difference between this method and {@link #getUnmarshallingBeanValidator} is not having * {@link #PREFIX_UNMARSHALLING} in String messages constructed for exceptions. * * @param context jaxb context reference * @return * a new instance of {@link JAXBBeanValidator}. */ static JAXBBeanValidator getMarshallingBeanValidator(JAXBContext context){ return new JAXBBeanValidator("", context); } /** * Factory method. *

* The only difference between this method and {@link #getMarshallingBeanValidator} is having * {@link #PREFIX_UNMARSHALLING} in String messages constructed for exceptions. * * @param context jaxb context reference * @return * a new instance of {@link JAXBBeanValidator}. */ static JAXBBeanValidator getUnmarshallingBeanValidator(JAXBContext context){ return new JAXBBeanValidator(PREFIX_UNMARSHALLING, context); } /** * PUBLIC: * * First, if validation has not been turned off before, check if passed value is constrained. * * Second, depending on Bean Validation Mode, either returns false or tries to initialize Validator: * - AUTO tries to initialize Validator: * returns true if succeeds, else false. * - CALLBACK tries to initialize Validator: * returns true if succeeds, else throws {@link BeanValidationException#providerNotFound}. * - NONE returns false; * * BeanValidationMode is fetched from (un)marshaller upon each call. * If change in mode is detected, the internal state of the JAXBBeanValidator will be switched. * * Third, analyses the value and determines whether validation may be skipped. * * @param beanValidationMode Bean validation mode - allowed values AUTO, CALLBACK, NONE. * @param value validated object. It is passed because validation on some objects may be skipped, * e.g. non-constrained objects (like XmlBindings). * @param preferredValidatorFactory Must be {@link ValidatorFactory} or null. Will use this factory as the * preferred provider; if null, will use javax defaults. * @param noOptimisation if true, bean validation optimisations that skip non-constrained objects will not be * performed * @return * true if should proceed with validation, else false. * @throws BeanValidationException * {@link BeanValidationException#illegalValidationMode} or {@link BeanValidationException#providerNotFound}. * @since 2.6 */ boolean shouldValidate (Object value, BeanValidationMode beanValidationMode, Object preferredValidatorFactory, boolean noOptimisation) throws BeanValidationException { if (isValidationEffectivelyOff(beanValidationMode)) return false; this.noOptimisation = noOptimisation; if (!isConstrainedObject(value)) return false; /* Mode or validator factory was changed externally (or it's the first time this method is called). */ if (this.beanValidationMode != beanValidationMode || this.validatorFactory != preferredValidatorFactory) { this.beanValidationMode = beanValidationMode; this.validatorFactory = (ValidatorFactory)preferredValidatorFactory; changeInternalState(); } /* Is Validation implementation ready to validate. */ return canValidate; } /** * Check if validation is effectively off, i.e. it was previously attempted to turn it on, but that failed. * @param beanValidationMode user passed beanValidationMode * @return true if validation is effectively off */ private boolean isValidationEffectivelyOff(BeanValidationMode beanValidationMode) { return ! ((beanValidationMode == BeanValidationMode.AUTO && canValidate) /* most common case */ || (beanValidationMode == BeanValidationMode.CALLBACK) /* beanValidationMode is AUTO but canValidate is yet to be resolved */ || (beanValidationMode != BeanValidationMode.NONE && beanValidationMode != this.beanValidationMode) ); } /** * Check if object contains any bean validation constraints or custom validation constraints. * @param value object * @return true if the object is not null and is constrained */ private boolean isConstrainedObject(Object value) { /* Json is allowed to pass a null root object. Avoid NPE & speed things up. */ if (value == null) return false; if (noOptimisation) { /* Stops the endless invocation loop which may occur when calling * Validation#buildDefaultValidatorFactory in a case when the user sets * custom validation configuration through "validation.xml" file and * the validation implementation tries to unmarshal the file with MOXy. */ if (lock.isHeldByCurrentThread()) return false; /* Do not validate XmlBindings. */ return !(value instanceof XmlBindings); } /* Ensure that the class contains BV annotations. If not, skip validation & speed things up. * note: This also effectively skips XmlBindings. */ return context.getBeanValidationHelper().isConstrained(value.getClass()); } /** * INTERNAL: * * Validates the value, as per BV spec. * Stores the result of validation in {@link #constraintViolations}. * * @param value Object to be validated. * @param groups Target groups as per BV spec. If null {@link #DEFAULT_GROUP_ARRAY} is used. */ void validate(Object value, Class... groups) throws BeanValidationException { Class[] grp = groups; if (grp == null || grp.length == 0) { grp = DEFAULT_GROUP_ARRAY; } constraintViolations = validator.validate(value, grp); if (!constraintViolations.isEmpty()) throw buildConstraintViolationException(); } /** * @return constraintViolations from the last {@link #validate} call. */ Set> getConstraintViolations() { Set> result = new HashSet<>(constraintViolations.size()); for (ConstraintViolation cv : constraintViolations) { result.add(new ConstraintViolationWrapper<>(cv)); } return result; } /** * INTERNAL: * * Puts variables to states which conform to the internal state machine. * * Internal states: * Mode/Field Value | NONE | AUTO | CALLBACK * --------------------------|-------------|--------------|-------------- * canValidate | false | true/false | true/false * stopSearchingForValidator | false | true/false | false * constraintViolations | EmptySet | n/a | n/a * * n/a ... value is not altered. * * @throws BeanValidationException illegalValidationMode or providerNotFound */ private void changeInternalState() throws BeanValidationException { stopSearchingForValidator = false; // Reset the switch. switch (beanValidationMode) { case NONE: canValidate = false; constraintViolations = Collections.emptySet(); // Clear the reference from previous (un)marshal calls. break; case CALLBACK: case AUTO: canValidate = initValidator(); break; default: throw BeanValidationException.illegalValidationMode(prefix, beanValidationMode.toString()); } } /** * PUBLIC: * * Initializes validator if not already initialized. * If mode is BeanValidationMode.AUTO, then after an unsuccessful try to * initialize a Validator, property {@code stopSearchingForValidator} will be set to true. * * NOTE: Property {@code stopSearchingForValidator} can be reset only by triggering * {@link #changeInternalState}. * * @return {@code true} if validator initialization succeeded, otherwise {@code false}. * @throws BeanValidationException * throws {@link org.eclipse.persistence.exceptions.BeanValidationException#PROVIDER_NOT_FOUND} */ private boolean initValidator() throws BeanValidationException { if (validator == null && !stopSearchingForValidator){ try { ValidatorFactory factory = getValidatorFactory(); validator = factory.getValidator(); printValidatorInfo(); } catch (ValidationException ve) { if (beanValidationMode == BeanValidationMode.CALLBACK){ /* The following line ensures that changeInternalState() will be the triggered on next (un)marshalling trials if mode is still CALLBACK. That will ensure searching for Validator implementation again. */ beanValidationMode = BeanValidationMode.AUTO; throw BeanValidationException.providerNotFound(prefix, ve); } else { // mode AUTO stopSearchingForValidator = true; // Will not try to initialize validator on next tries. } } } return validator != null; } /** * INTERNAL: * * @return Preferred ValidatorFactory if set, else {@link Validation#buildDefaultValidatorFactory()}. */ private ValidatorFactory getValidatorFactory() { if (validatorFactory != null) { return validatorFactory; } if (noOptimisation) { lock.lock(); try { return Validation.buildDefaultValidatorFactory(); } finally { lock.unlock(); } } return Validation.buildDefaultValidatorFactory(); } /** * INTERNAL: * * Builds ConstraintViolationException with constraintViolations, but no message. * Builds BeanValidationException with fully descriptive message, containing * the ConstraintViolationException. * * @return BeanValidationException, containing ConstraintViolationException. */ private BeanValidationException buildConstraintViolationException() { ConstraintViolationException cve = new ConstraintViolationException( /* Do not remove the cast. */ constraintViolations); return BeanValidationException.constraintViolation(createConstraintViolationExceptionArgs(), cve); } /** * INTERNAL: * Builds an Object array containing args for ConstraintViolationException constructor. * * @return [0] - prefix, * [1] - rootBean (on what object the validation failed), * [2] - linkedList of violatedConstraints, with overriden toString() for better formatting. */ private Object[] createConstraintViolationExceptionArgs() { Object[] args = new Object[3]; Iterator> iterator = constraintViolations.iterator(); assert iterator.hasNext(); // this method is to be called only if constraints violations are not empty ConstraintViolation cv = iterator.next(); Collection violatedConstraints = new LinkedList<>() { @Override public String toString() { Iterator it = iterator(); StringBuilder sb = new StringBuilder(); while (it.hasNext()) sb.append("\n-->").append(it.next().toString()); return sb.toString(); } }; args[0] = prefix; Object bean = cv.getRootBean(); // NOTE: // 1. Do not use bean.toString(), it could leak secure information. // 2. And use identityHashCode, for these reasons: // - prevents NPE which could be caused by a poorly implemented hashCode // - serves as a better mean of identification of the bean. args[1] = bean.getClass().toString().substring("class ".length()) + "@" + Integer.toHexString(System.identityHashCode(bean)); args[2] = violatedConstraints; for (;;) { violatedConstraints.add(new ConstraintViolationInfo(cv.getMessage(), cv.getPropertyPath())); if (iterator.hasNext()) cv = iterator.next(); else break; } return args; } /** * Logs the name of underlying validation impl jar used. Only logs once per context to avoid log cluttering. * To be called after successful assignment of validator. */ private void printValidatorInfo() { if (!context.getHasLoggedValidatorInfo().getAndSet(true)) { CodeSource validationImplJar = getValidatorCodeSource(); if (log.shouldLog(SessionLog.FINE)) { log.log(SessionLog.FINE, "EclipseLink is using " + validationImplJar + " as BeanValidation implementation."); } } } /** * INTERNAL: * Retrieves code source of validator. * * @return Validator code source. May be null. */ private CodeSource getValidatorCodeSource() { return PrivilegedAccessHelper.callDoPrivileged( () -> validator.getClass().getProtectionDomain().getCodeSource() ); } /** * INTERNAL: * * Value Object class that provides adequate toString() method which describes * on which field a Validation Constraint was violated and includes it's violationDescription. */ private static class ConstraintViolationInfo { /** * Description of constraint violation. */ private final String violationDescription; /** * Path to element on which the constraint violation occurred. */ private final Path propertyPath; /** * Private constructor. Only to be used from within {@link org.eclipse.persistence.jaxb.JAXBBeanValidator}. * * @param message description of constraint violation * @param propertyPath path to element on which the constraint violation occurred */ private ConstraintViolationInfo(String message, Path propertyPath){ this.violationDescription = message; this.propertyPath = propertyPath; } @Override public String toString() { return "Violated constraint on property " + propertyPath + ": \"" + violationDescription + "\"."; } } }