org.kuali.rice.krad.data.jpa.JpaPersistenceProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.kuali.rice.krad.data.jpa.JpaPersistenceProvider.java

Source

/**
 * Copyright 2005-2015 The Kuali Foundation
 *
 * Licensed under the Educational Community License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.opensource.org/licenses/ecl2.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.kuali.rice.krad.data.jpa;

import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;

import javax.persistence.EntityManager;
import javax.persistence.NonUniqueResultException;
import javax.persistence.PersistenceException;
import javax.persistence.metamodel.ManagedType;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.ArrayUtils;
import org.eclipse.persistence.jpa.JpaEntityManager;
import org.eclipse.persistence.sessions.CopyGroup;
import org.kuali.rice.core.api.CoreConstants;
import org.kuali.rice.core.api.config.property.ConfigContext;
import org.kuali.rice.core.api.criteria.QueryByCriteria;
import org.kuali.rice.core.api.criteria.QueryResults;
import org.kuali.rice.core.api.exception.RiceRuntimeException;
import org.kuali.rice.core.api.mo.common.GloballyUnique;
import org.kuali.rice.core.api.mo.common.Versioned;
import org.kuali.rice.krad.data.CompoundKey;
import org.kuali.rice.krad.data.CopyOption;
import org.kuali.rice.krad.data.DataObjectService;
import org.kuali.rice.krad.data.DataObjectWrapper;
import org.kuali.rice.krad.data.KradDataServiceLocator;
import org.kuali.rice.krad.data.PersistenceOption;
import org.kuali.rice.krad.data.metadata.DataObjectCollection;
import org.kuali.rice.krad.data.metadata.DataObjectMetadata;
import org.kuali.rice.krad.data.metadata.DataObjectRelationship;
import org.kuali.rice.krad.data.provider.PersistenceProvider;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.ChainedPersistenceExceptionTranslator;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.orm.jpa.EntityManagerFactoryUtils;
import org.springframework.transaction.annotation.Transactional;

import com.google.common.collect.Sets;

/**
 * Java Persistence API (JPA) implementation of {@link PersistenceProvider}.
 *
 * <p>
 * When creating a new instance of this provider, a reference to a "shared" entity manager (like that created by
 * Spring's {@link org.springframework.orm.jpa.support.SharedEntityManagerBean} must be injected. Additionally, a
 * reference to the {@link DataObjectService} must be injected as well.
 * </p>
 *
 * <p>
 * This class will perform persistence exception translation (converting JPA exceptions to
 * {@link org.springframework.dao.DataAccessException}s. It will scan the
 * {@link org.springframework.beans.factory.BeanFactory} in which it was created to find beans which implement
 * {@link org.springframework.dao.support.PersistenceExceptionTranslator} and use those translators for translation.
 * </p>
 *
 * @see org.springframework.orm.jpa.support.SharedEntityManagerBean
 * @see org.springframework.dao.support.PersistenceExceptionTranslator
 *
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
public class JpaPersistenceProvider implements PersistenceProvider, BeanFactoryAware {

    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
            .getLogger(JpaPersistenceProvider.class);

    /**
     * Indicates if a JPA {@code EntityManager} flush should be automatically executed when calling
     * {@link org.kuali.rice.krad.data.DataObjectService#save(Object, org.kuali.rice.krad.data.PersistenceOption...)}
     * using a JPA provider.
     *
     * <p>This is recommended for testing only since the change is global and would affect all persistence units.</p>
     */
    public static final String AUTO_FLUSH = "rice.krad.data.jpa.autoFlush";

    private EntityManager sharedEntityManager;
    private DataObjectService dataObjectService;

    private PersistenceExceptionTranslator persistenceExceptionTranslator;

    private Set<Class<?>> managedTypesCache;

    /**
     * Initialization-on-demand holder idiom for thread-safe lazy loading of configuration.
     */
    private static final class LazyConfigHolder {
        private static final boolean autoFlush = ConfigContext.getCurrentContextConfig()
                .getBooleanProperty(AUTO_FLUSH, false);
    }

    /**
     * Gets the shared {@link EntityManager}.
     *
     * @return The shared {@link EntityManager}.
     */
    public EntityManager getSharedEntityManager() {
        return sharedEntityManager;
    }

    /**
     * Setter for the shared {@link EntityManager}.
     *
     * @param sharedEntityManager The shared {@link EntityManager} to set.
     */
    public void setSharedEntityManager(EntityManager sharedEntityManager) {
        this.sharedEntityManager = sharedEntityManager;
    }

    /**
     * Setter for the {@link DataObjectService}.
     *
     * @param dataObjectService The {@link DataObjectService} to set.
     */
    public void setDataObjectService(DataObjectService dataObjectService) {
        this.dataObjectService = dataObjectService;
    }

    /**
     * Returns the {@link DataObjectService}.
     *
     * @return a {@link DataObjectService}
     */
    public DataObjectService getDataObjectService() {
        return this.dataObjectService;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        if (!(beanFactory instanceof ListableBeanFactory)) {
            throw new IllegalArgumentException(
                    "Cannot use PersistenceExceptionTranslator autodetection without ListableBeanFactory");
        }
        this.persistenceExceptionTranslator = detectPersistenceExceptionTranslators(
                (ListableBeanFactory) beanFactory);
    }

    /**
     * Gets any {@link PersistenceExceptionTranslator}s from the {@link BeanFactory}.
     *
     * @param beanFactory The {@link BeanFactory} to use.
     *
     * @return A {@link PersistenceExceptionTranslator} from the {@link BeanFactory}.
     */
    protected PersistenceExceptionTranslator detectPersistenceExceptionTranslators(
            ListableBeanFactory beanFactory) {
        // Find all translators, being careful not to activate FactoryBeans.
        Map<String, PersistenceExceptionTranslator> pets = BeanFactoryUtils
                .beansOfTypeIncludingAncestors(beanFactory, PersistenceExceptionTranslator.class, false, false);
        ChainedPersistenceExceptionTranslator cpet = new ChainedPersistenceExceptionTranslator();
        for (PersistenceExceptionTranslator pet : pets.values()) {
            cpet.addDelegate(pet);
        }
        // always add one last persistence exception translator as a catch all
        cpet.addDelegate(new DefaultPersistenceExceptionTranslator());
        return cpet;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional
    public <T> T save(final T dataObject, final PersistenceOption... options) {
        return doWithExceptionTranslation(new Callable<T>() {
            @Override
            public T call() {
                verifyDataObjectWritable(dataObject);

                Set<PersistenceOption> optionSet = Sets.newHashSet(options);

                T mergedDataObject = sharedEntityManager.merge(dataObject);

                // We must flush if they pass us a flush option, have auto flush turned on, or are synching keys
                // after save. We are required to flush before synching because we may need to use generated values to
                // perform synchronization and those won't be there until after a flush
                //
                // note that the actual synchronization of keys is handled automatically by the framework after the
                // save has been completed
                if (optionSet.contains(PersistenceOption.FLUSH) || optionSet.contains(PersistenceOption.LINK_KEYS)
                        || LazyConfigHolder.autoFlush) {
                    sharedEntityManager.flush();
                }

                if (sharedEntityManager.getEntityManagerFactory().getCache() != null) {
                    try {
                        Object dataObjectKey = sharedEntityManager.getEntityManagerFactory()
                                .getPersistenceUnitUtil().getIdentifier(mergedDataObject);
                        if (dataObjectKey != null) {
                            sharedEntityManager.getEntityManagerFactory().getCache().evict(dataObject.getClass(),
                                    dataObjectKey);
                        }
                    } catch (PersistenceException ex) {
                        // JPA fails if it can't create the key field classes - we just need to catch and ignore here
                    }
                }

                return mergedDataObject;
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(readOnly = true)
    public <T> T find(final Class<T> type, final Object id) {
        return doWithExceptionTranslation(new Callable<T>() {
            @Override
            public T call() {
                if (id instanceof CompoundKey) {
                    QueryResults<T> results = findMatching(type,
                            QueryByCriteria.Builder.andAttributes(((CompoundKey) id).getKeys()).build());
                    if (results.getResults().size() > 1) {
                        throw new NonUniqueResultException("Error Compound Key: " + id + " on class "
                                + type.getName() + " returned more than one row.");
                    }
                    if (!results.getResults().isEmpty()) {
                        return results.getResults().get(0);
                    }
                    return null;
                } else {
                    return sharedEntityManager.find(type, id);
                }
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(readOnly = true)
    public <T> QueryResults<T> findMatching(final Class<T> type, final QueryByCriteria queryByCriteria) {
        return doWithExceptionTranslation(new Callable<QueryResults<T>>() {
            @Override
            public QueryResults<T> call() {
                return new JpaCriteriaQuery(sharedEntityManager).lookup(type, queryByCriteria);
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(readOnly = true)
    public <T> QueryResults<T> findAll(final Class<T> type) {
        return doWithExceptionTranslation(new Callable<QueryResults<T>>() {
            @Override
            public QueryResults<T> call() {
                return new JpaCriteriaQuery(getSharedEntityManager()).lookup(type,
                        QueryByCriteria.Builder.create().build());
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional
    public void delete(final Object dataObject) {
        doWithExceptionTranslation(new Callable<Object>() {
            @Override
            public Object call() {
                verifyDataObjectWritable(dataObject);
                // If the L2 cache is enabled, the item will still be served from the cache
                // So, we need to flush that as well for the given type and key
                if (sharedEntityManager.getEntityManagerFactory().getCache() != null) {
                    try {
                        Object dataObjectKey = sharedEntityManager.getEntityManagerFactory()
                                .getPersistenceUnitUtil().getIdentifier(dataObject);
                        if (dataObjectKey != null) {
                            sharedEntityManager.getEntityManagerFactory().getCache().evict(dataObject.getClass(),
                                    dataObjectKey);
                        }
                    } catch (PersistenceException ex) {
                        // JPA fails if it can't create the key field classes - we just need to catch and ignore here
                    }
                }
                Object mergedDataObject = sharedEntityManager.merge(dataObject);
                sharedEntityManager.remove(mergedDataObject);
                return null;
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional
    public <T> void deleteMatching(final Class<T> type, final QueryByCriteria queryByCriteria) {
        doWithExceptionTranslation(new Callable<Object>() {
            @Override
            public Object call() {
                new JpaCriteriaQuery(getSharedEntityManager()).deleteMatching(type, queryByCriteria);
                // If the L2 cache is enabled, items will still be served from the cache
                // So, we need to flush that as well for the given type
                if (sharedEntityManager.getEntityManagerFactory().getCache() != null) {
                    sharedEntityManager.getEntityManagerFactory().getCache().evict(type);
                }
                return null;
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional
    public <T> void deleteAll(final Class<T> type) {
        doWithExceptionTranslation(new Callable<Object>() {
            @Override
            public Object call() {
                new JpaCriteriaQuery(getSharedEntityManager()).deleteAll(type);
                // If the L2 cache is enabled, items will still be served from the cache
                // So, we need to flush that as well for the given type
                if (sharedEntityManager.getEntityManagerFactory().getCache() != null) {
                    sharedEntityManager.getEntityManagerFactory().getCache().evict(type);
                }
                return null;
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional
    public <T> T copyInstance(final T dataObject, CopyOption... options) {
        final CopyGroup copyGroup = new CopyGroup();
        if (ArrayUtils.contains(options, CopyOption.RESET_PK_FIELDS)) {
            copyGroup.setShouldResetPrimaryKey(true);
        }
        final boolean shouldResetVersionNumber = ArrayUtils.contains(options, CopyOption.RESET_VERSION_NUMBER);
        if (shouldResetVersionNumber) {
            copyGroup.setShouldResetVersion(true);
        }
        final boolean shouldResetObjectId = ArrayUtils.contains(options, CopyOption.RESET_OBJECT_ID);
        return doWithExceptionTranslation(new Callable<T>() {
            @SuppressWarnings("unchecked")
            @Override
            public T call() {
                T copiedObject = (T) sharedEntityManager.unwrap(JpaEntityManager.class).getDatabaseSession()
                        .copy(dataObject, copyGroup);
                if (shouldResetObjectId) {
                    clearObjectIdOnUpdatableObjects(copiedObject, new HashSet<Object>());
                }
                if (shouldResetVersionNumber) {
                    clearVersionNumberOnUpdatableObjects(copiedObject, new HashSet<Object>());
                }
                return copiedObject;
            }
        });
    }

    /**
     * For the given data object, recurse through all updatable references and clear the object ID on the basis that
     * this is a unique column in each object's table.
     * 
     * @param dataObject
     *            The data object on which to clear the object ID from itself and all updatable child objects.
     * @param visitedObjects
     *            A set of objects built by the recursion process which will be checked to ensure that the code does not
     *            get into an infinite loop.
     */
    protected void clearObjectIdOnUpdatableObjects(Object dataObject, Set<Object> visitedObjects) {
        if (dataObject == null) {
            return;
        }
        // avoid infinite loops
        if (visitedObjects.contains(dataObject)) {
            return;
        }
        visitedObjects.add(dataObject);
        if (dataObject instanceof GloballyUnique) {
            try {
                PropertyUtils.setSimpleProperty(dataObject, CoreConstants.CommonElements.OBJECT_ID, null);
            } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
                // there may not be a setter or some other issue. In any case, we don't want to blow the method.
                LOG.warn("Unable to clear the objectId from copyInstance on an object: " + dataObject, ex);
            }
        }
        DataObjectWrapper<Object> wrapper = KradDataServiceLocator.getDataObjectService().wrap(dataObject);
        if (wrapper.getMetadata() != null) {
            for (DataObjectRelationship rel : wrapper.getMetadata().getRelationships()) {
                if (rel.isSavedWithParent()) {
                    // recurse in
                    clearObjectIdOnUpdatableObjects(wrapper.getPropertyValue(rel.getName()), visitedObjects);
                }
            }
            for (DataObjectCollection rel : wrapper.getMetadata().getCollections()) {
                if (rel.isSavedWithParent()) {
                    Collection<?> collection = (Collection<?>) wrapper.getPropertyValue(rel.getName());
                    if (collection != null) {
                        for (Object element : collection) {
                            clearObjectIdOnUpdatableObjects(element, visitedObjects);
                        }
                    }
                }
            }

        }
    }

    /**
     * For the given data object, recurse through all updatable references and clear the object ID on the basis that
     * this is a unique column in each object's table.
     * 
     * @param dataObject
     *            The data object on which to clear the object ID from itself and all updatable child objects.
     * @param visitedObjects
     *            A set of objects built by the recursion process which will be checked to ensure that the code does not
     *            get into an infinite loop.
     */
    protected void clearVersionNumberOnUpdatableObjects(Object dataObject, Set<Object> visitedObjects) {
        if (dataObject == null) {
            return;
        }
        // avoid infinite loops
        if (visitedObjects.contains(dataObject)) {
            return;
        }
        visitedObjects.add(dataObject);
        if (dataObject instanceof Versioned) {
            try {
                PropertyUtils.setSimpleProperty(dataObject, CoreConstants.CommonElements.VERSION_NUMBER, null);
            } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
                // there may not be a setter or some other issue. In any case, we don't want to blow the method.
                LOG.warn("Unable to clear the objectId from copyInstance on an object: " + dataObject, ex);
            }
        }
        DataObjectWrapper<Object> wrapper = KradDataServiceLocator.getDataObjectService().wrap(dataObject);
        if (wrapper.getMetadata() != null) {
            for (DataObjectRelationship rel : wrapper.getMetadata().getRelationships()) {
                if (rel.isSavedWithParent()) {
                    // recurse in
                    clearVersionNumberOnUpdatableObjects(wrapper.getPropertyValue(rel.getName()), visitedObjects);
                }
            }
            for (DataObjectCollection rel : wrapper.getMetadata().getCollections()) {
                if (rel.isSavedWithParent()) {
                    Collection<?> collection = (Collection<?>) wrapper.getPropertyValue(rel.getName());
                    if (collection != null) {
                        for (Object element : collection) {
                            clearVersionNumberOnUpdatableObjects(element, visitedObjects);
                        }
                    }
                }
            }

        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean handles(final Class<?> type) {
        if (managedTypesCache == null) {
            managedTypesCache = new HashSet<Class<?>>();

            Set<ManagedType<?>> managedTypes = sharedEntityManager.getMetamodel().getManagedTypes();
            for (ManagedType managedType : managedTypes) {
                managedTypesCache.add(managedType.getJavaType());
            }
        }

        if (managedTypesCache.contains(type)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(readOnly = true)
    public void flush(final Class<?> type) {
        doWithExceptionTranslation(new Callable<Object>() {
            @Override
            public Object call() {
                sharedEntityManager.flush();
                // If the L2 cache is enabled, items will still be served from the cache
                // So, we need to flush that as well for the given type
                // if (sharedEntityManager.getEntityManagerFactory().getCache() != null) {
                // if (type != null) {
                // sharedEntityManager.getEntityManagerFactory().getCache().evict(type);
                // } else {
                // sharedEntityManager.getEntityManagerFactory().getCache().evictAll();
                // }
                // }
                return null;
            }
        });
    }

    /**
     * Verifies that the data object can be written to.
     *
     * @param dataObject The data object to check.
     */
    protected void verifyDataObjectWritable(Object dataObject) {
        DataObjectMetadata metaData = dataObjectService.getMetadataRepository().getMetadata(dataObject.getClass());
        if (metaData == null) {
            throw new IllegalArgumentException(
                    "Given data object class is not loaded into the MetadataRepository: " + dataObject.getClass());
        }
        if (metaData.isReadOnly()) {
            throw new UnsupportedOperationException(dataObject.getClass() + " is read-only");
        }
    }

    /**
     * Surrounds the transaction with a try/catch block that can use the {@link PersistenceExceptionTranslator} to
     * translate the exception if necessary.
     *
     * @param callable The data operation to invoke.
     * @param <T> The type of the data operation.
     *
     * @return The result from the data operation, if successful.
     */
    protected <T> T doWithExceptionTranslation(Callable<T> callable) {
        try {
            return callable.call();
        } catch (RuntimeException ex) {
            throw DataAccessUtils.translateIfNecessary(ex, this.persistenceExceptionTranslator);
        } catch (Exception ex) {
            // this should really never happen based on the internal usage in this class
            throw new RiceRuntimeException("Unexpected checked exception during data access.", ex);
        }
    }

    /**
     * Defines a default {@link PersistenceExceptionTranslator} if no others exist.
     */
    private static final class DefaultPersistenceExceptionTranslator implements PersistenceExceptionTranslator {

        /**
         * {@inheritDoc}
         */
        @Override
        public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
            return EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex);
        }

    }

}