001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.data.jpa;
017
018import java.lang.reflect.InvocationTargetException;
019import java.util.Collection;
020import java.util.HashSet;
021import java.util.Map;
022import java.util.Set;
023import java.util.concurrent.Callable;
024
025import javax.persistence.EntityManager;
026import javax.persistence.NonUniqueResultException;
027import javax.persistence.PersistenceException;
028import javax.persistence.metamodel.ManagedType;
029
030import org.apache.commons.beanutils.PropertyUtils;
031import org.apache.commons.lang.ArrayUtils;
032import org.eclipse.persistence.jpa.JpaEntityManager;
033import org.eclipse.persistence.sessions.CopyGroup;
034import org.kuali.rice.core.api.CoreConstants;
035import org.kuali.rice.core.api.config.property.ConfigContext;
036import org.kuali.rice.core.api.criteria.QueryByCriteria;
037import org.kuali.rice.core.api.criteria.QueryResults;
038import org.kuali.rice.core.api.exception.RiceRuntimeException;
039import org.kuali.rice.core.api.mo.common.GloballyUnique;
040import org.kuali.rice.core.api.mo.common.Versioned;
041import org.kuali.rice.krad.data.CompoundKey;
042import org.kuali.rice.krad.data.CopyOption;
043import org.kuali.rice.krad.data.DataObjectService;
044import org.kuali.rice.krad.data.DataObjectWrapper;
045import org.kuali.rice.krad.data.KradDataServiceLocator;
046import org.kuali.rice.krad.data.PersistenceOption;
047import org.kuali.rice.krad.data.metadata.DataObjectCollection;
048import org.kuali.rice.krad.data.metadata.DataObjectMetadata;
049import org.kuali.rice.krad.data.metadata.DataObjectRelationship;
050import org.kuali.rice.krad.data.provider.PersistenceProvider;
051import org.springframework.beans.BeansException;
052import org.springframework.beans.factory.BeanFactory;
053import org.springframework.beans.factory.BeanFactoryAware;
054import org.springframework.beans.factory.BeanFactoryUtils;
055import org.springframework.beans.factory.ListableBeanFactory;
056import org.springframework.dao.DataAccessException;
057import org.springframework.dao.support.ChainedPersistenceExceptionTranslator;
058import org.springframework.dao.support.DataAccessUtils;
059import org.springframework.dao.support.PersistenceExceptionTranslator;
060import org.springframework.orm.jpa.EntityManagerFactoryUtils;
061import org.springframework.transaction.annotation.Transactional;
062
063import com.google.common.collect.Sets;
064
065/**
066 * Java Persistence API (JPA) implementation of {@link PersistenceProvider}.
067 *
068 * <p>
069 * When creating a new instance of this provider, a reference to a "shared" entity manager (like that created by
070 * Spring's {@link org.springframework.orm.jpa.support.SharedEntityManagerBean} must be injected. Additionally, a
071 * reference to the {@link DataObjectService} must be injected as well.
072 * </p>
073 *
074 * <p>
075 * This class will perform persistence exception translation (converting JPA exceptions to
076 * {@link org.springframework.dao.DataAccessException}s. It will scan the
077 * {@link org.springframework.beans.factory.BeanFactory} in which it was created to find beans which implement
078 * {@link org.springframework.dao.support.PersistenceExceptionTranslator} and use those translators for translation.
079 * </p>
080 *
081 * @see org.springframework.orm.jpa.support.SharedEntityManagerBean
082 * @see org.springframework.dao.support.PersistenceExceptionTranslator
083 *
084 * @author Kuali Rice Team (rice.collab@kuali.org)
085 */
086public class JpaPersistenceProvider implements PersistenceProvider, BeanFactoryAware {
087
088        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(JpaPersistenceProvider.class);
089
090    /**
091     * Indicates if a JPA {@code EntityManager} flush should be automatically executed when calling
092     * {@link org.kuali.rice.krad.data.DataObjectService#save(Object, org.kuali.rice.krad.data.PersistenceOption...)}
093     * using a JPA provider.
094     *
095     * <p>This is recommended for testing only since the change is global and would affect all persistence units.</p>
096     */
097    public static final String AUTO_FLUSH = "rice.krad.data.jpa.autoFlush";
098
099    private EntityManager sharedEntityManager;
100    private DataObjectService dataObjectService;
101
102    private PersistenceExceptionTranslator persistenceExceptionTranslator;
103
104    private Set<Class<?>> managedTypesCache;
105
106    /**
107     * Initialization-on-demand holder idiom for thread-safe lazy loading of configuration.
108     */
109    private static final class LazyConfigHolder {
110        private static final boolean autoFlush = ConfigContext.getCurrentContextConfig().getBooleanProperty(AUTO_FLUSH, false);
111    }
112
113    /**
114     * Gets the shared {@link EntityManager}.
115     *
116     * @return The shared {@link EntityManager}.
117     */
118    public EntityManager getSharedEntityManager() {
119        return sharedEntityManager;
120    }
121
122    /**
123     * Setter for the shared {@link EntityManager}.
124     *
125     * @param sharedEntityManager The shared {@link EntityManager} to set.
126     */
127    public void setSharedEntityManager(EntityManager sharedEntityManager) {
128        this.sharedEntityManager = sharedEntityManager;
129    }
130
131    /**
132     * Setter for the {@link DataObjectService}.
133     *
134     * @param dataObjectService The {@link DataObjectService} to set.
135     */
136    public void setDataObjectService(DataObjectService dataObjectService) {
137        this.dataObjectService = dataObjectService;
138    }
139
140    /**
141     * Returns the {@link DataObjectService}.
142     *
143     * @return a {@link DataObjectService}
144     */
145    public DataObjectService getDataObjectService() {
146        return this.dataObjectService;
147    }
148
149    /**
150     * {@inheritDoc}
151     */
152    @Override
153    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
154        if (!(beanFactory instanceof ListableBeanFactory)) {
155            throw new IllegalArgumentException(
156                    "Cannot use PersistenceExceptionTranslator autodetection without ListableBeanFactory");
157        }
158        this.persistenceExceptionTranslator = detectPersistenceExceptionTranslators((ListableBeanFactory)beanFactory);
159    }
160
161    /**
162     * Gets any {@link PersistenceExceptionTranslator}s from the {@link BeanFactory}.
163     *
164     * @param beanFactory The {@link BeanFactory} to use.
165     *
166     * @return A {@link PersistenceExceptionTranslator} from the {@link BeanFactory}.
167     */
168    protected PersistenceExceptionTranslator detectPersistenceExceptionTranslators(ListableBeanFactory beanFactory) {
169        // Find all translators, being careful not to activate FactoryBeans.
170        Map<String, PersistenceExceptionTranslator> pets = BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory,
171                PersistenceExceptionTranslator.class, false, false);
172        ChainedPersistenceExceptionTranslator cpet = new ChainedPersistenceExceptionTranslator();
173        for (PersistenceExceptionTranslator pet : pets.values()) {
174            cpet.addDelegate(pet);
175        }
176        // always add one last persistence exception translator as a catch all
177        cpet.addDelegate(new DefaultPersistenceExceptionTranslator());
178        return cpet;
179    }
180
181    /**
182     * {@inheritDoc}
183     */
184    @Override
185    @Transactional
186    public <T> T save(final T dataObject, final PersistenceOption... options) {
187        return doWithExceptionTranslation(new Callable<T>() {
188            @Override
189                        public T call() {
190                verifyDataObjectWritable(dataObject);
191
192                        Set<PersistenceOption> optionSet = Sets.newHashSet(options);
193
194                        T mergedDataObject = sharedEntityManager.merge(dataObject);
195
196                // We must flush if they pass us a flush option, have auto flush turned on, or are synching keys
197                // after save. We are required to flush before synching because we may need to use generated values to
198                // perform synchronization and those won't be there until after a flush
199                //
200                // note that the actual synchronization of keys is handled automatically by the framework after the
201                // save has been completed
202                if(optionSet.contains(PersistenceOption.FLUSH) || optionSet.contains(PersistenceOption.LINK_KEYS) ||
203                        LazyConfigHolder.autoFlush){
204                                        sharedEntityManager.flush();
205                }
206
207                                if (sharedEntityManager.getEntityManagerFactory().getCache() != null) {
208                                        try {
209                                                Object dataObjectKey = sharedEntityManager.getEntityManagerFactory().getPersistenceUnitUtil()
210                                                                .getIdentifier(mergedDataObject);
211                                                if (dataObjectKey != null) {
212                                                        sharedEntityManager.getEntityManagerFactory().getCache()
213                                                                        .evict(dataObject.getClass(), dataObjectKey);
214                                                }
215                                        } catch (PersistenceException ex) {
216                                                // JPA fails if it can't create the key field classes - we just need to catch and ignore here
217                                        }
218                                }
219
220                return mergedDataObject;
221            }
222        });
223    }
224
225    /**
226     * {@inheritDoc}
227     */
228    @Override
229    @Transactional(readOnly = true)
230    public <T> T find(final Class<T> type, final Object id) {
231        return doWithExceptionTranslation(new Callable<T>() {
232            @Override
233                        public T call() {
234                if (id instanceof CompoundKey) {
235                                QueryResults<T> results = findMatching(type,
236                                                QueryByCriteria.Builder.andAttributes(((CompoundKey) id).getKeys()).build());
237                                if (results.getResults().size() > 1) {
238                                        throw new NonUniqueResultException("Error Compound Key: " + id + " on class " + type.getName()
239                                                        + " returned more than one row.");
240                                }
241                    if (!results.getResults().isEmpty()) {
242                                        return results.getResults().get(0);
243                    }
244                                return null;
245                } else {
246                    return sharedEntityManager.find(type, id);
247                }
248            }
249        });
250    }
251
252    /**
253     * {@inheritDoc}
254     */
255    @Override
256    @Transactional(readOnly = true)
257    public <T> QueryResults<T> findMatching(final Class<T> type, final QueryByCriteria queryByCriteria) {
258        return doWithExceptionTranslation(new Callable<QueryResults<T>>() {
259            @Override
260                        public QueryResults<T> call() {
261                return new JpaCriteriaQuery(sharedEntityManager).lookup(type, queryByCriteria);
262            }
263        });
264    }
265
266    /**
267     * {@inheritDoc}
268     */
269    @Override
270    @Transactional(readOnly = true)
271    public <T> QueryResults<T> findAll(final Class<T> type) {
272        return doWithExceptionTranslation(new Callable<QueryResults<T>>() {
273            @Override
274            public QueryResults<T> call() {
275                return new JpaCriteriaQuery(getSharedEntityManager()).lookup(type, QueryByCriteria.Builder.create().build());
276            }
277        });
278    }
279
280    /**
281     * {@inheritDoc}
282     */
283    @Override
284    @Transactional
285    public void delete(final Object dataObject) {
286        doWithExceptionTranslation(new Callable<Object>() {
287            @Override
288                        public Object call() {
289                verifyDataObjectWritable(dataObject);
290                                // If the L2 cache is enabled, the item will still be served from the cache
291                                // So, we need to flush that as well for the given type and key
292                                if (sharedEntityManager.getEntityManagerFactory().getCache() != null) {
293                                        try {
294                                                Object dataObjectKey = sharedEntityManager.getEntityManagerFactory().getPersistenceUnitUtil()
295                                                                .getIdentifier(dataObject);
296                                                if (dataObjectKey != null) {
297                                                        sharedEntityManager.getEntityManagerFactory().getCache()
298                                                                        .evict(dataObject.getClass(), dataObjectKey);
299                                                }
300                                        } catch (PersistenceException ex) {
301                                                // JPA fails if it can't create the key field classes - we just need to catch and ignore here
302                                        }
303                                }
304                                Object mergedDataObject = sharedEntityManager.merge(dataObject);
305                                sharedEntityManager.remove(mergedDataObject);
306                return null;
307            }
308        });
309    }
310
311    /**
312     * {@inheritDoc}
313     */
314    @Override
315    @Transactional
316    public <T> void deleteMatching(final Class<T> type, final QueryByCriteria queryByCriteria) {
317        doWithExceptionTranslation(new Callable<Object>() {
318            @Override
319            public Object call() {
320                new JpaCriteriaQuery(getSharedEntityManager()).deleteMatching(type, queryByCriteria);
321                                // If the L2 cache is enabled, items will still be served from the cache
322                                // So, we need to flush that as well for the given type
323                                if (sharedEntityManager.getEntityManagerFactory().getCache() != null) {
324                                        sharedEntityManager.getEntityManagerFactory().getCache().evict(type);
325                                }
326                return null;
327            }
328        });
329    }
330
331    /**
332     * {@inheritDoc}
333     */
334    @Override
335    @Transactional
336    public <T> void deleteAll(final Class<T> type) {
337        doWithExceptionTranslation(new Callable<Object>() {
338            @Override
339            public Object call() {
340                new JpaCriteriaQuery(getSharedEntityManager()).deleteAll(type);
341                                // If the L2 cache is enabled, items will still be served from the cache
342                                // So, we need to flush that as well for the given type
343                                if (sharedEntityManager.getEntityManagerFactory().getCache() != null) {
344                                        sharedEntityManager.getEntityManagerFactory().getCache().evict(type);
345                                }
346                return null;
347            }
348        });
349    }
350
351    /**
352     * {@inheritDoc}
353     */
354    @Override
355    @Transactional
356        public <T> T copyInstance(final T dataObject, CopyOption... options) {
357                final CopyGroup copyGroup = new CopyGroup();
358                if (ArrayUtils.contains(options, CopyOption.RESET_PK_FIELDS)) {
359                        copyGroup.setShouldResetPrimaryKey(true);
360                }
361                final boolean shouldResetVersionNumber = ArrayUtils.contains(options, CopyOption.RESET_VERSION_NUMBER);
362                if (shouldResetVersionNumber) {
363                        copyGroup.setShouldResetVersion(true);
364                }
365                final boolean shouldResetObjectId = ArrayUtils.contains(options, CopyOption.RESET_OBJECT_ID);
366        return doWithExceptionTranslation(new Callable<T>() {
367                        @SuppressWarnings("unchecked")
368                        @Override
369            public T call() {
370                                T copiedObject = (T) sharedEntityManager.unwrap(JpaEntityManager.class).getDatabaseSession()
371                                                .copy(dataObject, copyGroup);
372                                if (shouldResetObjectId) {
373                                        clearObjectIdOnUpdatableObjects(copiedObject, new HashSet<Object>());
374                                }
375                                if (shouldResetVersionNumber) {
376                                    clearVersionNumberOnUpdatableObjects(copiedObject, new HashSet<Object>());
377                                }
378                                return copiedObject;
379            }
380        });
381    }
382
383        /**
384         * For the given data object, recurse through all updatable references and clear the object ID on the basis that
385         * this is a unique column in each object's table.
386         * 
387         * @param dataObject
388         *            The data object on which to clear the object ID from itself and all updatable child objects.
389         * @param visitedObjects
390         *            A set of objects built by the recursion process which will be checked to ensure that the code does not
391         *            get into an infinite loop.
392         */
393        protected void clearObjectIdOnUpdatableObjects(Object dataObject, Set<Object> visitedObjects) {
394                if (dataObject == null) {
395                        return;
396                }
397                // avoid infinite loops
398                if (visitedObjects.contains(dataObject)) {
399                        return;
400                }
401                visitedObjects.add(dataObject);
402                if (dataObject instanceof GloballyUnique) {
403                        try {
404                                PropertyUtils.setSimpleProperty(dataObject, CoreConstants.CommonElements.OBJECT_ID, null);
405                        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
406                                // there may not be a setter or some other issue. In any case, we don't want to blow the method.
407                                LOG.warn("Unable to clear the objectId from copyInstance on an object: " + dataObject, ex);
408                        }
409                }
410                DataObjectWrapper<Object> wrapper = KradDataServiceLocator.getDataObjectService().wrap(dataObject);
411                if (wrapper.getMetadata() != null) {
412                        for (DataObjectRelationship rel : wrapper.getMetadata().getRelationships()) {
413                                if (rel.isSavedWithParent()) {
414                                        // recurse in
415                                        clearObjectIdOnUpdatableObjects(wrapper.getPropertyValue(rel.getName()), visitedObjects);
416                                }
417                        }
418                        for (DataObjectCollection rel : wrapper.getMetadata().getCollections()) {
419                                if (rel.isSavedWithParent()) {
420                                        Collection<?> collection = (Collection<?>) wrapper.getPropertyValue(rel.getName());
421                                        if (collection != null) {
422                                                for (Object element : collection) {
423                                                        clearObjectIdOnUpdatableObjects(element, visitedObjects);
424                                                }
425                                        }
426                                }
427                        }
428
429                }
430        }
431
432        /**
433         * For the given data object, recurse through all updatable references and clear the object ID on the basis that
434         * this is a unique column in each object's table.
435         * 
436         * @param dataObject
437         *            The data object on which to clear the object ID from itself and all updatable child objects.
438         * @param visitedObjects
439         *            A set of objects built by the recursion process which will be checked to ensure that the code does not
440         *            get into an infinite loop.
441         */
442        protected void clearVersionNumberOnUpdatableObjects(Object dataObject, Set<Object> visitedObjects) {
443                if (dataObject == null) {
444                        return;
445                }
446                // avoid infinite loops
447                if (visitedObjects.contains(dataObject)) {
448                        return;
449                }
450                visitedObjects.add(dataObject);
451                if (dataObject instanceof Versioned) {
452                        try {
453                                PropertyUtils.setSimpleProperty(dataObject, CoreConstants.CommonElements.VERSION_NUMBER, null);
454                        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
455                                // there may not be a setter or some other issue. In any case, we don't want to blow the method.
456                                LOG.warn("Unable to clear the objectId from copyInstance on an object: " + dataObject, ex);
457                        }
458                }
459                DataObjectWrapper<Object> wrapper = KradDataServiceLocator.getDataObjectService().wrap(dataObject);
460                if (wrapper.getMetadata() != null) {
461                        for (DataObjectRelationship rel : wrapper.getMetadata().getRelationships()) {
462                                if (rel.isSavedWithParent()) {
463                                        // recurse in
464                                        clearVersionNumberOnUpdatableObjects(wrapper.getPropertyValue(rel.getName()), visitedObjects);
465                                }
466                        }
467                        for (DataObjectCollection rel : wrapper.getMetadata().getCollections()) {
468                                if (rel.isSavedWithParent()) {
469                                        Collection<?> collection = (Collection<?>) wrapper.getPropertyValue(rel.getName());
470                                        if (collection != null) {
471                                                for (Object element : collection) {
472                                                        clearVersionNumberOnUpdatableObjects(element, visitedObjects);
473                                                }
474                                        }
475                                }
476                        }
477
478                }
479        }
480
481    /**
482     * {@inheritDoc}
483     */
484    @Override
485    public boolean handles(final Class<?> type) {
486        if (managedTypesCache == null) {
487            managedTypesCache = new HashSet<Class<?>>();
488
489            Set<ManagedType<?>> managedTypes = sharedEntityManager.getMetamodel().getManagedTypes();
490            for (ManagedType managedType : managedTypes) {
491                managedTypesCache.add(managedType.getJavaType());
492            }
493        }
494
495        if (managedTypesCache.contains(type)) {
496            return true;
497        } else {
498            return false;
499        }
500    }
501
502    /**
503     * {@inheritDoc}
504     */
505    @Override
506    @Transactional(readOnly = true)
507    public void flush(final Class<?> type) {
508        doWithExceptionTranslation(new Callable<Object>() {
509            @Override
510                        public Object call() {
511                sharedEntityManager.flush();
512                                // If the L2 cache is enabled, items will still be served from the cache
513                                // So, we need to flush that as well for the given type
514                                // if (sharedEntityManager.getEntityManagerFactory().getCache() != null) {
515                                // if (type != null) {
516                                // sharedEntityManager.getEntityManagerFactory().getCache().evict(type);
517                                // } else {
518                                // sharedEntityManager.getEntityManagerFactory().getCache().evictAll();
519                                // }
520                                // }
521                return null;
522            }
523        });
524    }
525
526    /**
527     * Verifies that the data object can be written to.
528     *
529     * @param dataObject The data object to check.
530     */
531    protected void verifyDataObjectWritable(Object dataObject) {
532        DataObjectMetadata metaData = dataObjectService.getMetadataRepository().getMetadata(dataObject.getClass());
533        if (metaData == null) {
534            throw new IllegalArgumentException("Given data object class is not loaded into the MetadataRepository: " + dataObject.getClass());
535        }
536        if (metaData.isReadOnly()) {
537            throw new UnsupportedOperationException(dataObject.getClass() + " is read-only");
538        }
539    }
540
541    /**
542     * Surrounds the transaction with a try/catch block that can use the {@link PersistenceExceptionTranslator} to
543     * translate the exception if necessary.
544     *
545     * @param callable The data operation to invoke.
546     * @param <T> The type of the data operation.
547     *
548     * @return The result from the data operation, if successful.
549     */
550    protected <T> T doWithExceptionTranslation(Callable<T> callable) {
551        try {
552            return callable.call();
553        }
554        catch (RuntimeException ex) {
555            throw DataAccessUtils.translateIfNecessary(ex, this.persistenceExceptionTranslator);
556        } catch (Exception ex) {
557            // this should really never happen based on the internal usage in this class
558            throw new RiceRuntimeException("Unexpected checked exception during data access.", ex);
559        }
560    }
561
562    /**
563     * Defines a default {@link PersistenceExceptionTranslator} if no others exist.
564     */
565    private static final class DefaultPersistenceExceptionTranslator implements PersistenceExceptionTranslator {
566
567        /**
568         * {@inheritDoc}
569         */
570        @Override
571        public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
572            return EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex);
573        }
574
575    }
576
577}