blob: 8d67c2dd2e06ac58bc520b7ca1d814f214807c0e [file] [log] [blame]
/*
* 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 - marcel.valovy@oracle.com
* @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[])}.
* <p>After each {@link #validate(Object, Class[])} call, the reference is replaced.
*/
private Set<ConstraintViolation<Object>> constraintViolations = Collections.emptySet();
/**
* Computed value saying if the validation can proceed under current conditions, represented by:
* <blockquote><pre>
* - {@link #beanValidationMode}
* - {@link jakarta.validation.Validator} implementation present on classpath
* </pre></blockquote>
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<ConstraintViolationWrapper<Object>> getConstraintViolations() {
Set<ConstraintViolationWrapper<Object>> result = new HashSet<>(constraintViolations.size());
for (ConstraintViolation<Object> 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<? extends ConstraintViolation<?>> iterator = constraintViolations.iterator();
assert iterator.hasNext(); // this method is to be called only if constraints violations are not empty
ConstraintViolation<?> cv = iterator.next();
Collection<ConstraintViolationInfo> violatedConstraints = new LinkedList<>() {
@Override
public String toString() {
Iterator<ConstraintViolationInfo> 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 + "\".";
}
}
}