View Javadoc
1   /**
2    * Copyright 2005-2014 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 com.google.common.collect.Sets;
19  import org.eclipse.persistence.jpa.JpaEntityManager;
20  import org.kuali.rice.core.api.config.property.ConfigContext;
21  import org.kuali.rice.core.api.criteria.QueryByCriteria;
22  import org.kuali.rice.core.api.criteria.QueryResults;
23  import org.kuali.rice.core.api.exception.RiceRuntimeException;
24  import org.kuali.rice.krad.data.CompoundKey;
25  import org.kuali.rice.krad.data.DataObjectService;
26  import org.kuali.rice.krad.data.PersistenceOption;
27  import org.kuali.rice.krad.data.metadata.DataObjectMetadata;
28  import org.kuali.rice.krad.data.provider.PersistenceProvider;
29  import org.springframework.beans.BeansException;
30  import org.springframework.beans.factory.BeanFactory;
31  import org.springframework.beans.factory.BeanFactoryAware;
32  import org.springframework.beans.factory.BeanFactoryUtils;
33  import org.springframework.beans.factory.ListableBeanFactory;
34  import org.springframework.dao.DataAccessException;
35  import org.springframework.dao.support.ChainedPersistenceExceptionTranslator;
36  import org.springframework.dao.support.DataAccessUtils;
37  import org.springframework.dao.support.PersistenceExceptionTranslator;
38  import org.springframework.orm.jpa.EntityManagerFactoryUtils;
39  import org.springframework.transaction.annotation.Transactional;
40  
41  import javax.persistence.EntityManager;
42  import javax.persistence.NonUniqueResultException;
43  import javax.persistence.metamodel.ManagedType;
44  import java.util.Map;
45  import java.util.Set;
46  import java.util.concurrent.Callable;
47  
48  /**
49   * Java Persistence API (JPA) implementation of {@link PersistenceProvider}.
50   *
51   * <p>
52   * When creating a new instance of this provider, a reference to a "shared" entity manager (like that created by
53   * Spring's {@link org.springframework.orm.jpa.support.SharedEntityManagerBean} must be injected. Additionally, a
54   * reference to the {@link DataObjectService} must be injected as well.
55   * </p>
56   *
57   * <p>
58   * This class will perform persistence exception translation (converting JPA exceptions to
59   * {@link org.springframework.dao.DataAccessException}s. It will scan the
60   * {@link org.springframework.beans.factory.BeanFactory} in which it was created to find beans which implement
61   * {@link org.springframework.dao.support.PersistenceExceptionTranslator} and use those translators for translation.
62   * </p>
63   *
64   * @see org.springframework.orm.jpa.support.SharedEntityManagerBean
65   * @see org.springframework.dao.support.PersistenceExceptionTranslator
66   *
67   * @author Kuali Rice Team (rice.collab@kuali.org)
68   */
69  @Transactional
70  public class JpaPersistenceProvider implements PersistenceProvider, BeanFactoryAware {
71  
72  	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(JpaPersistenceProvider.class);
73  
74      /**
75       * Indicates if a JPA {@code EntityManager} flush should be automatically executed when calling
76       * {@link org.kuali.rice.krad.data.DataObjectService#save(Object, org.kuali.rice.krad.data.PersistenceOption...)}
77       * using a JPA provider.
78       *
79       * <p>This is recommended for testing only since the change is global and would affect all persistence units.</p>
80       */
81      public static final String AUTO_FLUSH = "rice.krad.data.jpa.autoFlush";
82  
83      private EntityManager sharedEntityManager;
84      private DataObjectService dataObjectService;
85  
86      private PersistenceExceptionTranslator persistenceExceptionTranslator;
87  
88      /**
89       * Initialization-on-demand holder idiom for thread-safe lazy loading of configuration.
90       */
91      private static final class LazyConfigHolder {
92          private static final boolean autoFlush = ConfigContext.getCurrentContextConfig().getBooleanProperty(AUTO_FLUSH, false);
93      }
94  
95      /**
96       * Gets the shared {@link EntityManager}.
97       *
98       * @return The shared {@link EntityManager}.
99       */
100     public EntityManager getSharedEntityManager() {
101         return sharedEntityManager;
102     }
103 
104     /**
105      * Setter for the shared {@link EntityManager}.
106      *
107      * @param sharedEntityManager The shared {@link EntityManager} to set.
108      */
109     public void setSharedEntityManager(EntityManager sharedEntityManager) {
110         this.sharedEntityManager = sharedEntityManager;
111     }
112 
113     /**
114      * Setter for the {@link DataObjectService}.
115      *
116      * @param dataObjectService The {@link DataObjectService} to set.
117      */
118     public void setDataObjectService(DataObjectService dataObjectService) {
119         this.dataObjectService = dataObjectService;
120     }
121 
122     /**
123      * Returns the {@link DataObjectService}.
124      *
125      * @return a {@link DataObjectService}
126      */
127     public DataObjectService getDataObjectService() {
128         return this.dataObjectService;
129     }
130 
131     /**
132      * {@inheritDoc}
133      */
134     @Override
135     public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
136         if (!(beanFactory instanceof ListableBeanFactory)) {
137             throw new IllegalArgumentException(
138                     "Cannot use PersistenceExceptionTranslator autodetection without ListableBeanFactory");
139         }
140         this.persistenceExceptionTranslator = detectPersistenceExceptionTranslators((ListableBeanFactory)beanFactory);
141     }
142 
143     /**
144      * Gets any {@link PersistenceExceptionTranslator}s from the {@link BeanFactory}.
145      *
146      * @param beanFactory The {@link BeanFactory} to use.
147      *
148      * @return A {@link PersistenceExceptionTranslator} from the {@link BeanFactory}.
149      */
150     protected PersistenceExceptionTranslator detectPersistenceExceptionTranslators(ListableBeanFactory beanFactory) {
151         // Find all translators, being careful not to activate FactoryBeans.
152         Map<String, PersistenceExceptionTranslator> pets = BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory,
153                 PersistenceExceptionTranslator.class, false, false);
154         ChainedPersistenceExceptionTranslator cpet = new ChainedPersistenceExceptionTranslator();
155         for (PersistenceExceptionTranslator pet : pets.values()) {
156             cpet.addDelegate(pet);
157         }
158         // always add one last persistence exception translator as a catch all
159         cpet.addDelegate(new DefaultPersistenceExceptionTranslator());
160         return cpet;
161     }
162 
163     /**
164      * {@inheritDoc}
165      */
166     @Override
167     public <T> T save(final T dataObject, final PersistenceOption... options) {
168         return doWithExceptionTranslation(new Callable<T>() {
169             @Override
170 			public T call() {
171                 verifyDataObjectWritable(dataObject);
172 
173         		Set<PersistenceOption> optionSet = Sets.newHashSet(options);
174 
175 		        T mergedDataObject = sharedEntityManager.merge(dataObject);
176 
177                 // We must flush if they pass us a flush option, have auto flush turned on, or are synching keys
178                 // after save. We are required to flush before synching because we may need to use generated values to
179                 // perform synchronization and those won't be there until after a flush
180                 //
181                 // note that the actual synchronization of keys is handled automatically by the framework after the
182                 // save has been completed
183                 if(optionSet.contains(PersistenceOption.FLUSH) || optionSet.contains(PersistenceOption.LINK_KEYS) ||
184                         LazyConfigHolder.autoFlush){
185 			        sharedEntityManager.flush();
186                 }
187 
188                 return mergedDataObject;
189             }
190         });
191     }
192 
193     /**
194      * {@inheritDoc}
195      */
196     @Override
197     public <T> T find(final Class<T> type, final Object id) {
198         return doWithExceptionTranslation(new Callable<T>() {
199             @Override
200 			public T call() {
201                 if (id instanceof CompoundKey) {
202 			        QueryResults<T> results = findMatching(type,
203 				        	QueryByCriteria.Builder.andAttributes(((CompoundKey) id).getKeys()).build());
204 			        if (results.getResults().size() > 1) {
205 				        throw new NonUniqueResultException("Error Compound Key: " + id + " on class " + type.getName()
206 					        	+ " returned more than one row.");
207 			        }
208                     if (!results.getResults().isEmpty()) {
209 				        return results.getResults().get(0);
210                     }
211 			        return null;
212                 } else {
213                     return sharedEntityManager.find(type, id);
214                 }
215             }
216         });
217     }
218 
219     /**
220      * {@inheritDoc}
221      */
222     @Override
223     public <T> QueryResults<T> findMatching(final Class<T> type, final QueryByCriteria queryByCriteria) {
224         return doWithExceptionTranslation(new Callable<QueryResults<T>>() {
225             @Override
226 			public QueryResults<T> call() {
227                 return new JpaCriteriaQuery(sharedEntityManager).lookup(type, queryByCriteria);
228             }
229         });
230     }
231 
232     /**
233      * {@inheritDoc}
234      */
235     @Override
236     public <T> QueryResults<T> findAll(final Class<T> type) {
237         return doWithExceptionTranslation(new Callable<QueryResults<T>>() {
238             @Override
239             public QueryResults<T> call() {
240                 return new JpaCriteriaQuery(getSharedEntityManager()).lookup(type, QueryByCriteria.Builder.create().build());
241             }
242         });
243     }
244 
245     /**
246      * {@inheritDoc}
247      */
248     @Override
249     public void delete(final Object dataObject) {
250         doWithExceptionTranslation(new Callable<Object>() {
251             @Override
252 			public Object call() {
253                 verifyDataObjectWritable(dataObject);
254                 sharedEntityManager.remove(sharedEntityManager.merge(dataObject));
255                 return null;
256             }
257         });
258     }
259 
260     /**
261      * {@inheritDoc}
262      */
263     @Override
264     public <T> void deleteMatching(final Class<T> type, final QueryByCriteria queryByCriteria) {
265         doWithExceptionTranslation(new Callable<Object>() {
266             @Override
267             public Object call() {
268                 new JpaCriteriaQuery(getSharedEntityManager()).deleteMatching(type, queryByCriteria);
269                 return null;
270             }
271         });
272     }
273 
274     /**
275      * {@inheritDoc}
276      */
277     @Override
278     public <T> void deleteAll(final Class<T> type) {
279         doWithExceptionTranslation(new Callable<Object>() {
280             @Override
281             public Object call() {
282                 new JpaCriteriaQuery(getSharedEntityManager()).deleteAll(type);
283                 return null;
284             }
285         });
286     }
287 
288     /**
289      * {@inheritDoc}
290      */
291     @Override
292     public <T> T copyInstance(final T dataObject) {
293         return doWithExceptionTranslation(new Callable<T>() {
294             @Override
295             public T call() {
296                 return (T) sharedEntityManager.unwrap(JpaEntityManager.class).getDatabaseSession().copy(dataObject);
297             }
298         });
299     }
300 
301     /**
302      * {@inheritDoc}
303      */
304     @Override
305     public boolean handles(final Class<?> type) {
306         return doWithExceptionTranslation(new Callable<Boolean>() {
307             @Override
308 			public Boolean call() {
309                 try {
310                     ManagedType<?> managedType = sharedEntityManager.getMetamodel().managedType(type);
311                     return Boolean.valueOf(managedType != null);
312                 } catch (IllegalArgumentException iae) {
313                     return Boolean.FALSE;
314 				} catch (IllegalStateException ex) {
315 					// This catches cases where the entity manager is not initialized or has already been destroyed
316 					LOG.warn("sharedEntityManager " + sharedEntityManager + " is not in a state to be used: "
317 							+ ex.getMessage());
318 					return Boolean.FALSE;
319                 }
320             }
321         }).booleanValue();
322     }
323 
324     /**
325      * {@inheritDoc}
326      */
327     @Override
328     public void flush(final Class<?> type) {
329         doWithExceptionTranslation(new Callable<Object>() {
330             @Override
331 			public Object call() {
332                 sharedEntityManager.flush();
333                 return null;
334             }
335         });
336     }
337 
338     /**
339      * Verifies that the data object can be written to.
340      *
341      * @param dataObject The data object to check.
342      */
343     protected void verifyDataObjectWritable(Object dataObject) {
344         DataObjectMetadata metaData = dataObjectService.getMetadataRepository().getMetadata(dataObject.getClass());
345         if (metaData == null) {
346             throw new IllegalArgumentException("Given data object class is not loaded into the MetadataRepository: " + dataObject.getClass());
347         }
348         if (metaData.isReadOnly()) {
349             throw new UnsupportedOperationException(dataObject.getClass() + " is read-only");
350         }
351     }
352 
353     /**
354      * Surrounds the transaction with a try/catch block that can use the {@link PersistenceExceptionTranslator} to
355      * translate the exception if necessary.
356      *
357      * @param callable The data operation to invoke.
358      * @param <T> The type of the data operation.
359      *
360      * @return The result from the data operation, if successful.
361      */
362     protected <T> T doWithExceptionTranslation(Callable<T> callable) {
363         try {
364             return callable.call();
365         }
366         catch (RuntimeException ex) {
367             throw DataAccessUtils.translateIfNecessary(ex, this.persistenceExceptionTranslator);
368         } catch (Exception ex) {
369             // this should really never happen based on the internal usage in this class
370             throw new RiceRuntimeException("Unexpected checked exception during data access.", ex);
371         }
372     }
373 
374     /**
375      * Defines a default {@link PersistenceExceptionTranslator} if no others exist.
376      */
377     private static final class DefaultPersistenceExceptionTranslator implements PersistenceExceptionTranslator {
378 
379         /**
380          * {@inheritDoc}
381          */
382         @Override
383         public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
384             return EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex);
385         }
386 
387     }
388 
389 }