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}