blob: 045f7f366da1c0150f715c043e7309335761dda6 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2015 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.
******************************************************************************/
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.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;
import static org.eclipse.persistence.internal.jpa.metadata.beanvalidation.BeanValidationHelper.BEAN_VALIDATION_HELPER;
/**
* 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 shouldValidate = BEAN_VALIDATION_HELPER.isConstrained(source.getClass());
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;
}
}
}