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

org.eclipse.persistence.internal.jpa.metadata.listeners.BeanValidationListener Maven / Gradle / Ivy

There is a newer version: 4.0.3-RC2
Show newest version
/*
 * Copyright (c) 2009, 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:
//     08/20/2014-2.5 Rick Curtis
//       - 441890: Cache Validator instances.
//     Marcel Valovy - 2.6 - skip validation of objects that are not constrained.
//     02/23/2016-2.6 Dalia Abo Sheasha
//       - 487889: Fix EclipseLink Bean Validation optimization
//     03/09/2016-2.6 Dalia Abo Sheasha
//       - 489298: Wrap EclipseLink's Bean Validation calls in doPrivileged blocks when security is enabled

package org.eclipse.persistence.internal.jpa.metadata.listeners;

import java.lang.annotation.ElementType;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

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.ClassDescriptor;
import org.eclipse.persistence.descriptors.DescriptorEvent;
import org.eclipse.persistence.descriptors.DescriptorEventAdapter;
import org.eclipse.persistence.descriptors.FetchGroupManager;
import org.eclipse.persistence.internal.localization.ExceptionLocalization;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.ForeignReferenceMapping;

/**
 * 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 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 aboutToUpdate(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 preUpdateWithChanges(DescriptorEvent event) {
        aboutToUpdate(event);
    }

    @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();
        Validator validator = getValidator(event);
        boolean isBeanConstrained = isBeanConstrained(source, validator);
        boolean noOptimization = "true".equalsIgnoreCase((String) event.getSession().getProperty(PersistenceUnitProperties.BEAN_VALIDATION_NO_OPTIMISATION));
        boolean shouldValidate = noOptimization || isBeanConstrained;
        if (shouldValidate) {
            Set> constraintViolations = validate(source, validationGroup, validator);
            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
                throw new ConstraintViolationException(
                        ExceptionLocalization.buildMessage("bean_validation_constraint_violated",
                                new Object[]{callbackEventName, source.getClass().getName()}),
                        (Set>) (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;
    }

    /**
     * Returns if a bean/entity is constrained by calling the bean validation provider's
     * #javax.validation.metadata.BeanDescriptor.isBeanConstrained method.
     */
    private boolean isBeanConstrained(final Object source, final Validator validator) {
        // If Java Security is enabled, surround this call with a doPrivileged block.
        if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()) {
            return AccessController.doPrivileged(new PrivilegedAction() {
                @Override
                public Boolean run() {
                    return validator.getConstraintsForClass(source.getClass()).isBeanConstrained();

                }
            });
        } else {
            return validator.getConstraintsForClass(source.getClass()).isBeanConstrained();
        }
    }

    private Set> validate(final Object source, final Class[] validationGroup, final Validator validator) {
        // If Java Security is enabled, surround this call with a doPrivileged block.
        if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()) {
            return AccessController.doPrivileged(new PrivilegedAction>>() {
                @Override
                public Set> run() {
                    return validator.validate(source, validationGroup);

                }
            });
        } else {
            return validator.validate(source, validationGroup);
        }
    }

    /**
     * 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
            // 
            //    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).
            // 
            return pathToTraversableObject.iterator().next().getName() == null;
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy