2 * This document is a part of the source code and related artifacts
3 * for CollectionSpace, an open source collections management system
4 * for museums and related institutions:
6 * http://www.collectionspace.org
7 * http://wiki.collectionspace.org
9 * Copyright 2009 University of California at Berkeley
11 * Licensed under the Educational Community License (ECL), Version 2.0.
12 * You may not use this file except in compliance with this License.
14 * You may obtain a copy of the ECL 2.0 License at
16 * https://source.collectionspace.org/collection-space/LICENSE.txt
18 package org.collectionspace.services.common.storage.jpa;
20 import java.util.Date;
21 import java.util.List;
23 import javax.persistence.RollbackException;
25 import java.sql.BatchUpdateException;
27 import javax.persistence.EntityExistsException;
28 import javax.persistence.EntityManager;
29 import javax.persistence.EntityManagerFactory;
30 import javax.persistence.Query;
32 import org.collectionspace.services.common.document.BadRequestException;
33 import org.collectionspace.services.common.document.DocumentException;
34 import org.collectionspace.services.common.document.DocumentFilter;
35 import org.collectionspace.services.common.document.DocumentHandler;
36 import org.collectionspace.services.common.document.DocumentNotFoundException;
37 import org.collectionspace.services.common.document.DocumentHandler.Action;
38 import org.collectionspace.services.common.document.DocumentWrapper;
39 import org.collectionspace.services.common.document.DocumentWrapperImpl;
40 import org.collectionspace.services.common.document.JaxbUtils;
41 import org.collectionspace.services.common.document.TransactionException;
42 import org.collectionspace.services.common.storage.StorageClient;
43 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthorityItemSpecifier;
44 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.Specifier;
45 import org.collectionspace.services.common.context.ServiceContextProperties;
46 import org.collectionspace.services.common.authorization_mgt.AuthorizationStore;
47 import org.collectionspace.services.common.context.ServiceContext;
48 import org.collectionspace.services.common.query.QueryContext;
49 import org.collectionspace.services.lifecycle.TransitionDef;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * JpaStorageClient is used to perform CRUD operations on SQL storage using JPA.
55 * It uses @see DocumentHandler as IOHandler with the client.
56 * All the operations in this client are carried out under their own transactions.
57 * A call to any method would start and commit/rollback a transaction.
59 * Assumption: each persistent entityReceived has the following 3 attributes
60 <xs:element name="createdAt" type="xs:dateTime">
64 <orm:column name="created_at" nullable="false"/>
69 <xs:element name="updatedAt" type="xs:dateTime">
73 <orm:column name="updated_at" />
79 <xs:attribute name="csid" type="xs:string">
83 <orm:column name="csid" length="128" nullable="false"/>
89 * $LastChangedRevision: $ $LastChangedDate: $
91 public class JpaStorageClientImpl implements StorageClient {
94 private final Logger logger = LoggerFactory.getLogger(JpaStorageClientImpl.class);
97 * Instantiates a new jpa storage client.
99 public JpaStorageClientImpl() {
100 //intentionally empty
104 * @see org.collectionspace.services.common.storage.StorageClient#create(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
107 public String create(ServiceContext ctx,
108 DocumentHandler handler) throws BadRequestException,
111 throw new IllegalArgumentException(
112 "create: ctx is missing");
114 if (handler == null) {
115 throw new IllegalArgumentException(
116 "create: handler is missing");
119 boolean rollbackTransaction = false;
120 EntityManagerFactory emf = null;
121 EntityManager em = null;
123 handler.prepare(Action.CREATE);
124 Object entity = handler.getCommonPart();
125 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entity);
127 emf = JpaStorageUtils.getEntityManagerFactory();
128 em = emf.createEntityManager();
129 em.getTransaction().begin(); { //begin of transaction block
130 ctx.setProperty(AuthorizationStore.ENTITY_MANAGER_PROP_KEY, em);
132 handler.handle(Action.CREATE, wrapDoc);
133 JaxbUtils.setValue(entity, "setCreatedAtItem", Date.class, new Date());
135 } catch (EntityExistsException ee) {
137 // We found an existing matching entity in the store, so we don't need to create one. Just update the transient 'entity' instance with the existing persisted entity we found.
138 // An entity's document handler class will throw this exception only if attempting to create (but not actually creating) duplicate is ok -e.g., Permission records.
140 entity = wrapDoc.getWrappedObject(); // the handler should have reset the wrapped transient object with the existing persisted entity we just found.
143 em.getTransaction().commit();
144 handler.complete(Action.CREATE, wrapDoc);
145 return (String) JaxbUtils.getValue(entity, "getCsid");
146 } catch (BadRequestException bre) {
147 rollbackTransaction = true;
149 } catch (DocumentException de) {
150 rollbackTransaction = true;
152 } catch (Exception e) {
153 rollbackTransaction = true;
154 if (logger.isDebugEnabled()) {
155 logger.debug("Caught exception ", e);
157 throw DocumentException.createDocumentException(e);
159 ctx.setProperty(AuthorizationStore.ENTITY_MANAGER_PROP_KEY, null);
161 if (rollbackTransaction == true) {
162 if (em.getTransaction().isActive() == true) {
163 em.getTransaction().rollback();
166 // Don't call this unless "em" is not null -hence the check above.
167 JpaStorageUtils.releaseEntityManagerFactory(emf);
174 * @see org.collectionspace.services.common.storage.StorageClient#get(org.collectionspace.services.common.context.ServiceContext, java.util.List, org.collectionspace.services.common.document.DocumentHandler)
177 public void get(ServiceContext ctx, List<String> csidList, DocumentHandler handler)
178 throws DocumentNotFoundException, DocumentException {
179 throw new UnsupportedOperationException();
183 * @see org.collectionspace.services.common.storage.StorageClient#get(org.collectionspace.services.common.context.ServiceContext, java.lang.String, org.collectionspace.services.common.document.DocumentHandler)
186 public void get(ServiceContext ctx, String id, DocumentHandler handler)
187 throws DocumentNotFoundException, DocumentException {
189 throw new IllegalArgumentException(
190 "get: ctx is missing");
192 if (handler == null) {
193 throw new IllegalArgumentException(
194 "get: handler is missing");
196 EntityManagerFactory emf = null;
197 EntityManager em = null;
199 handler.prepare(Action.GET);
201 o = JpaStorageUtils.getEntity(getEntityName(ctx), id,
204 if (em != null && em.getTransaction().isActive()) {
205 em.getTransaction().rollback();
207 String msg = "could not find entity with id=" + id;
208 throw new DocumentNotFoundException(msg);
210 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(o);
211 handler.handle(Action.GET, wrapDoc);
212 handler.complete(Action.GET, wrapDoc);
213 } catch (DocumentException de) {
215 } catch (Exception e) {
216 if (logger.isDebugEnabled()) {
217 logger.debug("Caught exception ", e);
219 throw new DocumentException(e);
222 JpaStorageUtils.releaseEntityManagerFactory(emf);
228 * @see org.collectionspace.services.common.storage.StorageClient#getAll(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
231 public void getAll(ServiceContext ctx, DocumentHandler handler)
232 throws DocumentNotFoundException, DocumentException {
233 throw new UnsupportedOperationException("use getFiltered instead");
237 * @see org.collectionspace.services.common.storage.StorageClient#getFiltered(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
240 public void getFiltered(ServiceContext ctx, DocumentHandler handler)
241 throws DocumentNotFoundException, DocumentException {
242 QueryContext queryContext = new QueryContext(ctx, handler);
244 DocumentFilter docFilter = handler.getDocumentFilter();
245 if (docFilter == null) {
246 docFilter = handler.createDocumentFilter();
248 EntityManagerFactory emf = null;
249 EntityManager em = null;
251 handler.prepare(Action.GET_ALL);
252 StringBuilder queryStrBldr = new StringBuilder("SELECT a FROM ");
253 queryStrBldr.append(getEntityName(ctx));
254 queryStrBldr.append(" a");
256 List<DocumentFilter.ParamBinding> params = docFilter.buildWhereForSearch(queryStrBldr);
257 emf = JpaStorageUtils.getEntityManagerFactory();
258 em = emf.createEntityManager();
259 String queryStr = queryStrBldr.toString(); //for debugging
260 Query q = em.createQuery(queryStr);
262 for (DocumentFilter.ParamBinding p : params) {
263 q.setParameter(p.getName(), p.getValue());
265 if (docFilter.getOffset() > 0) {
266 q.setFirstResult(docFilter.getOffset());
268 if (docFilter.getPageSize() > 0) {
269 q.setMaxResults(docFilter.getPageSize());
272 //FIXME is transaction required for get?
273 em.getTransaction().begin();
274 List list = q.getResultList();
275 long totalItems = getTotalItems(em, ctx, handler); // Find out how many items our query would find independent of the paging restrictions
276 em.getTransaction().commit();
278 docFilter.setTotalItemsResult(totalItems); // Save the items total in the doc filter for later reporting
280 DocumentWrapper<List> wrapDoc = new DocumentWrapperImpl<List>(list);
281 handler.handle(Action.GET_ALL, wrapDoc);
282 handler.complete(Action.GET_ALL, wrapDoc);
283 } catch (DocumentException de) {
285 } catch (Exception e) {
286 if (logger.isDebugEnabled()) {
287 logger.debug("Caught exception ", e);
289 throw new DocumentException(e);
292 JpaStorageUtils.releaseEntityManagerFactory(emf);
298 * Return the COUNT for a query to find the total number of matches independent of the paging restrictions.
300 private long getTotalItems(EntityManager em, ServiceContext ctx, DocumentHandler handler) {
303 DocumentFilter docFilter = handler.getDocumentFilter();
304 StringBuilder queryStrBldr = new StringBuilder("SELECT COUNT(*) FROM ");
305 queryStrBldr.append(getEntityName(ctx));
306 queryStrBldr.append(" a");
308 List<DocumentFilter.ParamBinding> params = docFilter.buildWhereForSearch(queryStrBldr);
309 String queryStr = queryStrBldr.toString();
310 Query q = em.createQuery(queryStr);
312 for (DocumentFilter.ParamBinding p : params) {
313 q.setParameter(p.getName(), p.getValue());
316 result = (long) q.getSingleResult();
322 * @see org.collectionspace.services.common.storage.StorageClient#update(org.collectionspace.services.common.context.ServiceContext, java.lang.String, org.collectionspace.services.common.document.DocumentHandler)
325 public void update(ServiceContext ctx, String id, DocumentHandler handler)
326 throws BadRequestException, DocumentNotFoundException,
329 throw new IllegalArgumentException(
330 "update: ctx is missing");
332 if (handler == null) {
333 throw new IllegalArgumentException(
334 "update: handler is missing");
336 EntityManagerFactory emf = null;
337 EntityManager em = null;
339 handler.prepare(Action.UPDATE);
340 Object entityReceived = handler.getCommonPart();
341 emf = JpaStorageUtils.getEntityManagerFactory();
342 em = emf.createEntityManager();
343 em.getTransaction().begin();
344 Object entityFound = getEntity(em, id, entityReceived.getClass());
345 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entityFound);
346 handler.handle(Action.UPDATE, wrapDoc);
347 JaxbUtils.setValue(entityFound, "setUpdatedAtItem", Date.class, new Date());
348 em.getTransaction().commit();
349 handler.complete(Action.UPDATE, wrapDoc);
350 } catch (BadRequestException bre) {
351 if (em != null && em.getTransaction().isActive()) {
352 em.getTransaction().rollback();
355 } catch (DocumentException de) {
356 if (em != null && em.getTransaction().isActive()) {
357 em.getTransaction().rollback();
360 } catch (Exception e) {
361 if (logger.isDebugEnabled()) {
362 logger.debug("Caught exception ", e);
364 throw new DocumentException(e);
367 JpaStorageUtils.releaseEntityManagerFactory(emf);
373 * delete removes entity and its child entities
374 * cost: a get before delete
375 * @see org.collectionspace.services.common.storage.StorageClient#delete(org.collectionspace.services.common.context.ServiceContext, java.lang.String)
378 public void delete(ServiceContext ctx, String id)
379 throws DocumentNotFoundException,
382 if (logger.isDebugEnabled()) {
383 logger.debug("delete(ctx, id): deleting entity with id=" + id);
387 throw new IllegalArgumentException(
388 "delete(ctx, id): ctx is missing");
390 EntityManagerFactory emf = null;
391 EntityManager em = null;
394 emf = JpaStorageUtils.getEntityManagerFactory();
395 em = emf.createEntityManager();
397 em.getTransaction().begin();
398 Object entityFound = getEntity(ctx, em, id);
399 if (entityFound == null) {
400 if (em != null && em.getTransaction().isActive()) {
401 em.getTransaction().rollback();
403 String msg = "delete(ctx, id): could not find entity with id=" + id;
405 throw new DocumentNotFoundException(msg);
407 em.remove(entityFound);
408 em.getTransaction().commit();
410 } catch (DocumentException de) {
411 if (em != null && em.getTransaction().isActive()) {
412 em.getTransaction().rollback();
415 } catch (Exception e) {
416 if (logger.isDebugEnabled()) {
417 logger.debug("delete(ctx, id): Caught exception ", e);
419 if (em != null && em.getTransaction().isActive()) {
420 em.getTransaction().rollback();
422 throw new DocumentException(e);
425 JpaStorageUtils.releaseEntityManagerFactory(emf);
431 * deleteWhere uses the where clause to delete an entityReceived represented by the csidReceived
432 * it does not delete any child entities.
435 * @throws DocumentNotFoundException
436 * @throws DocumentException
438 public void deleteWhere(ServiceContext ctx, String id)
439 throws DocumentNotFoundException,
443 throw new IllegalArgumentException(
444 "deleteWhere(ctx, id) : ctx is missing");
447 if (logger.isDebugEnabled()) {
448 logger.debug("deleteWhere(ctx, id): deleting entity with id=" + id);
450 EntityManagerFactory emf = null;
451 EntityManager em = null;
453 StringBuilder deleteStr = new StringBuilder("DELETE FROM ");
454 deleteStr.append(getEntityName(ctx));
455 deleteStr.append(" WHERE csid = :csid and tenantId = :tenantId");
456 //TODO: add tenant csidReceived
458 emf = JpaStorageUtils.getEntityManagerFactory();
459 em = emf.createEntityManager();
460 Query q = em.createQuery(deleteStr.toString());
461 q.setParameter("csid", id);
462 q.setParameter("tenantId", ctx.getTenantId());
465 em.getTransaction().begin();
466 rcount = q.executeUpdate();
468 if (em != null && em.getTransaction().isActive()) {
469 em.getTransaction().rollback();
471 String msg = "deleteWhere(ctx, id) could not find entity with id=" + id;
473 throw new DocumentNotFoundException(msg);
475 em.getTransaction().commit();
477 } catch (DocumentException de) {
478 if (em != null && em.getTransaction().isActive()) {
479 em.getTransaction().rollback();
482 } catch (Exception e) {
483 if (logger.isDebugEnabled()) {
484 logger.debug("deleteWhere(ctx, id) Caught exception ", e);
486 if (em != null && em.getTransaction().isActive()) {
487 em.getTransaction().rollback();
489 throw new DocumentException(e);
492 JpaStorageUtils.releaseEntityManagerFactory(emf);
498 * delete removes entity and its child entities but calls back to given handler
499 * cost: a get before delete
500 * @see org.collectionspace.services.common.storage.StorageClient#delete(org.collectionspace.services.common.context.ServiceContext, java.lang.String)
503 public boolean delete(ServiceContext ctx, String id, DocumentHandler handler)
504 throws DocumentNotFoundException, DocumentException {
505 boolean result = true;
508 throw new IllegalArgumentException(
509 "delete(ctx, ix, handler): ctx is missing");
511 if (handler == null) {
512 throw new IllegalArgumentException(
513 "delete(ctx, ix, handler): handler is missing");
516 EntityManagerFactory emf = null;
517 EntityManager em = null;
519 handler.prepare(Action.DELETE);
521 emf = JpaStorageUtils.getEntityManagerFactory();
522 em = emf.createEntityManager();
524 em.getTransaction().begin();
525 Object entityFound = getEntity(ctx, em, id);
526 if (entityFound == null) {
527 if (em != null && em.getTransaction().isActive()) {
528 em.getTransaction().rollback();
530 String msg = "delete(ctx, ix, handler) could not find entity with id=" + id;
532 throw new DocumentNotFoundException(msg);
534 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entityFound);
535 handler.handle(Action.DELETE, wrapDoc);
536 em.remove(entityFound);
537 em.getTransaction().commit();
538 handler.complete(Action.DELETE, wrapDoc);
539 } catch (DocumentException de) {
540 if (em != null && em.getTransaction().isActive()) {
541 em.getTransaction().rollback();
544 } catch (Exception e) {
545 if (logger.isDebugEnabled()) {
546 logger.debug("delete(ctx, ix, handler): Caught exception ", e);
548 if (em != null && em.getTransaction().isActive()) {
549 em.getTransaction().rollback();
551 throw new DocumentException(e);
554 JpaStorageUtils.releaseEntityManagerFactory(emf);
562 * Gets the entityReceived name.
566 * @return the entityReceived name
568 protected String getEntityName(ServiceContext ctx) {
569 Object o = ctx.getProperty(ServiceContextProperties.ENTITY_NAME);
571 throw new IllegalArgumentException(ServiceContextProperties.ENTITY_NAME
572 + "property is missing in context "
580 * getEntity returns persistent entity for given id. it assumes that
581 * service context has property ServiceContextProperties.ENTITY_CLASS set
582 * rolls back the transaction if not found
583 * @param ctx service context
584 * @param em entity manager
585 * @param csid received
587 * @throws DocumentNotFoundException and rollsback the transaction if active
589 protected Object getEntity(ServiceContext ctx, EntityManager em, String id)
590 throws DocumentNotFoundException {
591 Class entityClazz = (Class) ctx.getProperty(ServiceContextProperties.ENTITY_CLASS);
592 if (entityClazz == null) {
593 String msg = ServiceContextProperties.ENTITY_CLASS
594 + " property is missing in the context";
596 throw new IllegalArgumentException(msg);
598 return getEntity(em, id, entityClazz);
602 * getEntity retrieves the persistent entity of given class for given id
603 * rolls back the transaction if not found
605 * @param id entity id
608 * @throws DocumentNotFoundException and rollsback the transaction if active
610 protected Object getEntity(EntityManager em, String id, Class entityClazz)
611 throws DocumentNotFoundException {
612 Object entityFound = JpaStorageUtils.getEntity(em, id, entityClazz);
613 if (entityFound == null) {
614 if (em != null && em.getTransaction().isActive()) {
615 em.getTransaction().rollback();
617 String msg = "could not find entity of type=" + entityClazz.getName()
620 throw new DocumentNotFoundException(msg);
626 public void get(ServiceContext ctx, DocumentHandler handler)
627 throws DocumentNotFoundException, DocumentException {
628 throw new UnsupportedOperationException();
632 public void doWorkflowTransition(ServiceContext ctx, String id,
633 DocumentHandler handler, TransitionDef transitionDef)
634 throws BadRequestException, DocumentNotFoundException,
636 // Do nothing. JPA services do not support workflow.
640 public void deleteWithWhereClause(ServiceContext ctx, String whereClause,
641 DocumentHandler handler) throws DocumentNotFoundException,
643 throw new UnsupportedOperationException();
647 public boolean synchronize(ServiceContext ctx, Object specifier,
648 DocumentHandler handler) throws DocumentNotFoundException,
649 TransactionException, DocumentException {
650 // TODO Auto-generated method stub
651 // Do nothing. Subclasses can override if they want/need to.
656 public boolean synchronizeItem(ServiceContext ctx, AuthorityItemSpecifier itemSpecifier,
657 DocumentHandler handler) throws DocumentNotFoundException,
658 TransactionException, DocumentException {
659 // TODO Auto-generated method stub
660 // Do nothing. Subclasses can override if they want/need to.