View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.data.jpa;
17  
18  import java.lang.reflect.InvocationTargetException;
19  import java.util.Collection;
20  import java.util.HashSet;
21  import java.util.Map;
22  import java.util.Set;
23  import java.util.concurrent.Callable;
24  
25  import javax.persistence.EntityManager;
26  import javax.persistence.NonUniqueResultException;
27  import javax.persistence.PersistenceException;
28  import javax.persistence.metamodel.ManagedType;
29  
30  import org.apache.commons.beanutils.PropertyUtils;
31  import org.apache.commons.lang.ArrayUtils;
32  import org.eclipse.persistence.jpa.JpaEntityManager;
33  import org.eclipse.persistence.sessions.CopyGroup;
34  import org.kuali.rice.core.api.CoreConstants;
35  import org.kuali.rice.core.api.config.property.ConfigContext;
36  import org.kuali.rice.core.api.criteria.QueryByCriteria;
37  import org.kuali.rice.core.api.criteria.QueryResults;
38  import org.kuali.rice.core.api.exception.RiceRuntimeException;
39  import org.kuali.rice.core.api.mo.common.GloballyUnique;
40  import org.kuali.rice.core.api.mo.common.Versioned;
41  import org.kuali.rice.krad.data.CompoundKey;
42  import org.kuali.rice.krad.data.CopyOption;
43  import org.kuali.rice.krad.data.DataObjectService;
44  import org.kuali.rice.krad.data.DataObjectWrapper;
45  import org.kuali.rice.krad.data.KradDataServiceLocator;
46  import org.kuali.rice.krad.data.PersistenceOption;
47  import org.kuali.rice.krad.data.metadata.DataObjectCollection;
48  import org.kuali.rice.krad.data.metadata.DataObjectMetadata;
49  import org.kuali.rice.krad.data.metadata.DataObjectRelationship;
50  import org.kuali.rice.krad.data.provider.PersistenceProvider;
51  import org.springframework.beans.BeansException;
52  import org.springframework.beans.factory.BeanFactory;
53  import org.springframework.beans.factory.BeanFactoryAware;
54  import org.springframework.beans.factory.BeanFactoryUtils;
55  import org.springframework.beans.factory.ListableBeanFactory;
56  import org.springframework.dao.DataAccessException;
57  import org.springframework.dao.support.ChainedPersistenceExceptionTranslator;
58  import org.springframework.dao.support.DataAccessUtils;
59  import org.springframework.dao.support.PersistenceExceptionTranslator;
60  import org.springframework.orm.jpa.EntityManagerFactoryUtils;
61  import org.springframework.transaction.annotation.Transactional;
62  
63  import com.google.common.collect.Sets;
64  
65  /**
66   * Java Persistence API (JPA) implementation of {@link PersistenceProvider}.
67   *
68   * <p>
69   * When creating a new instance of this provider, a reference to a "shared" entity manager (like that created by
70   * Spring's {@link org.springframework.orm.jpa.support.SharedEntityManagerBean} must be injected. Additionally, a
71   * reference to the {@link DataObjectService} must be injected as well.
72   * </p>
73   *
74   * <p>
75   * This class will perform persistence exception translation (converting JPA exceptions to
76   * {@link org.springframework.dao.DataAccessException}s. It will scan the
77   * {@link org.springframework.beans.factory.BeanFactory} in which it was created to find beans which implement
78   * {@link org.springframework.dao.support.PersistenceExceptionTranslator} and use those translators for translation.
79   * </p>
80   *
81   * @see org.springframework.orm.jpa.support.SharedEntityManagerBean
82   * @see org.springframework.dao.support.PersistenceExceptionTranslator
83   *
84   * @author Kuali Rice Team (rice.collab@kuali.org)
85   */
86  public class JpaPersistenceProvider implements PersistenceProvider, BeanFactoryAware {
87  
88  	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(JpaPersistenceProvider.class);
89  
90      /**
91       * Indicates if a JPA {@code EntityManager} flush should be automatically executed when calling
92       * {@link org.kuali.rice.krad.data.DataObjectService#save(Object, org.kuali.rice.krad.data.PersistenceOption...)}
93       * using a JPA provider.
94       *
95       * <p>This is recommended for testing only since the change is global and would affect all persistence units.</p>
96       */
97      public static final String AUTO_FLUSH = "rice.krad.data.jpa.autoFlush";
98  
99      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 }