/******************************************************************************* | |
* Copyright (c) 2009, 2016 Sun Microsystems, Inc, IBM Corporation. All rights reserved. | |
* This program and the accompanying materials are made available under the | |
* terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 | |
* which accompanies this distribution. | |
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html | |
* and the Eclipse Distribution License is available at | |
* http://www.eclipse.org/org/documents/edl-v10.php. | |
* | |
* Contributors: | |
* 08/20/2014-2.5 Rick Curtis | |
* - 441890: Cache Validator instances. | |
* Marcel Valovy - 2.6 - skip validation of objects that are not constrained. | |
* 02/17/2016-2.6 Dalia Abo Sheasha | |
* - 487889: Fix EclipseLink Bean Validation optimization | |
******************************************************************************/ | |
package org.eclipse.persistence.internal.jpa.metadata.listeners; | |
import javax.validation.ConstraintViolation; | |
import javax.validation.ConstraintViolationException; | |
import javax.validation.Path; | |
import javax.validation.TraversableResolver; | |
import javax.validation.Validator; | |
import javax.validation.ValidatorFactory; | |
import javax.validation.groups.Default; | |
import org.eclipse.persistence.config.PersistenceUnitProperties; | |
import org.eclipse.persistence.descriptors.DescriptorEventAdapter; | |
import org.eclipse.persistence.descriptors.DescriptorEvent; | |
import org.eclipse.persistence.descriptors.ClassDescriptor; | |
import org.eclipse.persistence.descriptors.FetchGroupManager; | |
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl; | |
import org.eclipse.persistence.mappings.DatabaseMapping; | |
import org.eclipse.persistence.mappings.ForeignReferenceMapping; | |
import java.util.Map; | |
import java.util.Set; | |
import java.util.concurrent.ConcurrentHashMap; | |
import java.lang.annotation.ElementType; | |
/** | |
* Responsible for performing automatic bean validation on call back events. | |
* @author Mitesh Meswani | |
*/ | |
public class BeanValidationListener extends DescriptorEventAdapter { | |
private final ValidatorFactory validatorFactory; | |
private final Class[] groupPrePersit; | |
private final Class[] groupPreUpdate; | |
private final Class[] groupPreRemove; | |
private static final Class[] groupDefault = new Class[]{Default.class}; | |
private final Map<ClassDescriptor, Validator> validatorMap; | |
public BeanValidationListener(ValidatorFactory validatorFactory, Class[] groupPrePersit, Class[] groupPreUpdate, Class[] groupPreRemove) { | |
this.validatorFactory = validatorFactory; | |
//For prePersit and preUpdate, default the group to validation group Default if user has not specified one | |
this.groupPrePersit = groupPrePersit != null ? groupPrePersit : groupDefault; | |
this.groupPreUpdate = groupPreUpdate != null ? groupPreUpdate : groupDefault; | |
//No validation performed on preRemove if user has not explicitly specified a validation group | |
this.groupPreRemove = groupPreRemove; | |
validatorMap = new ConcurrentHashMap<>(); | |
} | |
@Override | |
public void prePersist (DescriptorEvent event) { | |
// since we are using prePersist to perform validation, invlid data may get inserted into database as shown by | |
// following example | |
// tx.begin() | |
// e = new MyEntity(...); | |
// em.perist(e); // prePersist validation happens here | |
// em.setXXX("invalid data"); | |
// tx.commit(); | |
// "invalid data" would get inserted into database. | |
// | |
// preInsert can be used to work around above issue. Howerver, the JPA spec does not itent it. | |
// This might be corrected in next iteration of spec | |
validateOnCallbackEvent(event, "prePersist", groupPrePersit); | |
} | |
@Override | |
public void preUpdate (DescriptorEvent event) { | |
Object source = event.getSource(); | |
UnitOfWorkImpl unitOfWork = (UnitOfWorkImpl )event.getSession(); | |
// preUpdate is also generated for deleted objects that were modified in this UOW. | |
// Do not perform preUpdate validation for such objects as preRemove would have already been called. | |
if(!unitOfWork.isObjectDeleted(source)) { | |
validateOnCallbackEvent(event, "preUpdate", groupPreUpdate); | |
} | |
} | |
@Override | |
public void preRemove (DescriptorEvent event) { | |
if(groupPreRemove != null) { //No validation performed on preRemove if user has not explicitly specified a validation group | |
validateOnCallbackEvent(event, "preRemove", groupPreRemove); | |
} | |
} | |
private void validateOnCallbackEvent(DescriptorEvent event, String callbackEventName, Class[] validationGroup) { | |
Object source = event.getSource(); | |
boolean noOptimization = "true".equalsIgnoreCase((String) event.getSession().getProperty(PersistenceUnitProperties.BEAN_VALIDATION_NO_OPTIMISATION)); | |
boolean isBeanConstrained = getValidator(event).getConstraintsForClass(source.getClass()).isBeanConstrained(); | |
boolean shouldValidate = noOptimization || isBeanConstrained; | |
if (shouldValidate) { | |
Set<ConstraintViolation<Object>> constraintViolations = getValidator(event).validate(source, validationGroup); | |
if (constraintViolations.size() > 0) { | |
// There were errors while call to validate above. | |
// Throw a ConstrainViolationException as required by the spec. | |
// The transaction would be rolled back automatically | |
// TODO need to I18N this. | |
throw new ConstraintViolationException( | |
"Bean Validation constraint(s) violated while executing Automatic Bean Validation on callback event:'" + | |
callbackEventName + "'. Please refer to embedded ConstraintViolations for details.", | |
(Set<ConstraintViolation<?>>) (Object) constraintViolations); /* Do not remove the explicit | |
cast. This issue is related to capture#a not being instance of capture#b. */ | |
} | |
} | |
} | |
private Validator getValidator(DescriptorEvent event) { | |
ClassDescriptor descriptor = event.getDescriptor(); | |
Validator res = validatorMap.get(descriptor); | |
if (res == null) { | |
TraversableResolver traversableResolver = new AutomaticLifeCycleValidationTraversableResolver(descriptor); | |
res = validatorFactory.usingContext().traversableResolver(traversableResolver).getValidator(); | |
Validator t = validatorMap.put(descriptor, res); | |
if (t != null) { | |
// Threading collision, use existing | |
res = t; | |
} | |
} | |
return res; | |
} | |
/** | |
* This traversable resolver ensures that validation is not cascaded to any associations and no lazily loaded | |
* attribute is loaded as a side effect of validation | |
*/ | |
private static class AutomaticLifeCycleValidationTraversableResolver implements TraversableResolver { | |
private final ClassDescriptor descriptor; | |
AutomaticLifeCycleValidationTraversableResolver(ClassDescriptor eventDescriptor) { | |
descriptor = eventDescriptor; | |
} | |
/** | |
* @return false for any lazily loaded property of root object being validated | |
*/ | |
public boolean isReachable(Object traversableObject, Path.Node traversableProperty, Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType) { | |
boolean reachable = true; | |
String attributeName = null; | |
if (isRootObjectPath(pathToTraversableObject)) { | |
attributeName = traversableProperty.getName(); //Refer to section 4.2 of Bean Validation spec for more details about Path.Node | |
DatabaseMapping mapping = getMappingForAttributeName(attributeName); | |
if(mapping != null) { | |
if(mapping.isForeignReferenceMapping()) { | |
// For lazy relationships check whether it is instantiated | |
if(mapping.isLazy()) { | |
Object attributeValue = mapping.getAttributeAccessor().getAttributeValueFromObject(traversableObject); | |
reachable = ((ForeignReferenceMapping)mapping).getIndirectionPolicy().objectIsInstantiatedOrChanged(attributeValue); | |
} | |
} else { | |
// For lazy non relationship attributes, check whether it is fetched | |
FetchGroupManager fetchGroupManager = descriptor.getFetchGroupManager(); | |
if (fetchGroupManager != null) { | |
reachable = fetchGroupManager.isAttributeFetched(traversableObject, attributeName); | |
} | |
} | |
} | |
} | |
return reachable; | |
} | |
/** | |
* Called only if isReachable returns true | |
* @return false for any associatons of root object being validated true otherwise | |
*/ | |
public boolean isCascadable(Object traversableObject, Path.Node traversableProperty, Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType) { | |
boolean cascadable = true; | |
if (isRootObjectPath(pathToTraversableObject)) { | |
String attributeName = traversableProperty.getName(); //Refer to section 4.2 of Bean Validation spec for more details about Path | |
DatabaseMapping mapping = getMappingForAttributeName(attributeName); | |
if(mapping != null && mapping.isForeignReferenceMapping()) { | |
cascadable = false; | |
} | |
} | |
return cascadable; | |
} | |
/** | |
* @return DatabaseMapping for given attribute name | |
*/ | |
private DatabaseMapping getMappingForAttributeName(String attributeName) { | |
return descriptor.getObjectBuilder().getMappingForAttributeName(attributeName); | |
} | |
/** | |
* @return true if given path corresponds to Root Object else false. | |
*/ | |
private boolean isRootObjectPath(Path pathToTraversableObject) { | |
// From Bean Validation spec section 3.5.2 | |
// <quote> | |
// pathToTraversableObject is the Path from the rootBeanType down to the traversableObject (it is composed of | |
// a single Node whose name is null if the root object is traversableObject). The path is described following the | |
// conventions described in Section 4.2 (getPropertyPath). | |
// </quote> | |
return pathToTraversableObject.iterator().next().getName() == null; | |
} | |
} | |
} |