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;
24 import javax.xml.datatype.XMLGregorianCalendar;
26 import java.sql.BatchUpdateException;
28 import javax.persistence.EntityExistsException;
29 import javax.persistence.EntityManager;
30 import javax.persistence.EntityManagerFactory;
31 import javax.persistence.Query;
33 import org.collectionspace.services.common.document.BadRequestException;
34 import org.collectionspace.services.common.document.DocumentException;
35 import org.collectionspace.services.common.document.DocumentFilter;
36 import org.collectionspace.services.common.document.DocumentHandler;
37 import org.collectionspace.services.common.document.DocumentNotFoundException;
38 import org.collectionspace.services.common.document.DocumentHandler.Action;
39 import org.collectionspace.services.common.document.DocumentWrapper;
40 import org.collectionspace.services.common.document.DocumentWrapperImpl;
41 import org.collectionspace.services.common.document.JaxbUtils;
42 import org.collectionspace.services.common.document.TransactionException;
43 import org.collectionspace.services.common.storage.StorageClient;
44 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthorityItemSpecifier;
45 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.Specifier;
46 import org.collectionspace.services.common.context.ServiceContextProperties;
47 import org.collectionspace.services.common.api.GregorianCalendarDateTimeUtils;
48 import org.collectionspace.services.common.authorization_mgt.AuthorizationStore;
49 import org.collectionspace.services.common.context.ServiceContext;
50 import org.collectionspace.services.common.query.QueryContext;
51 import org.collectionspace.services.lifecycle.TransitionDef;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
56 * JpaStorageClient is used to perform CRUD operations on SQL storage using JPA.
57 * It uses @see DocumentHandler as IOHandler with the client.
58 * All the operations in this client are carried out under their own transactions.
59 * A call to any method would start and commit/rollback a transaction.
61 * Assumption: each persistent entityReceived has the following 3 attributes
62 <xs:element name="createdAt" type="xs:dateTime">
66 <orm:column name="created_at" nullable="false"/>
71 <xs:element name="updatedAt" type="xs:dateTime">
75 <orm:column name="updated_at" />
81 <xs:attribute name="csid" type="xs:string">
85 <orm:column name="csid" length="128" nullable="false"/>
91 * $LastChangedRevision: $ $LastChangedDate: $
93 public class JpaStorageClientImpl implements StorageClient {
96 private final Logger logger = LoggerFactory.getLogger(JpaStorageClientImpl.class);
99 * Instantiates a new jpa storage client.
101 public JpaStorageClientImpl() {
102 //intentionally empty
106 * @see org.collectionspace.services.common.storage.StorageClient#create(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
109 public String create(ServiceContext ctx,
110 DocumentHandler handler) throws BadRequestException,
113 throw new IllegalArgumentException(
114 "create: ctx is missing");
116 if (handler == null) {
117 throw new IllegalArgumentException(
118 "create: handler is missing");
121 boolean rollbackTransaction = false;
122 EntityManagerFactory emf = null;
123 EntityManager em = null;
125 handler.prepare(Action.CREATE);
126 Object entity = handler.getCommonPart();
127 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entity);
129 emf = JpaStorageUtils.getEntityManagerFactory();
130 em = emf.createEntityManager();
131 em.getTransaction().begin(); { //begin of transaction block
132 ctx.setProperty(AuthorizationStore.ENTITY_MANAGER_PROP_KEY, em);
134 handler.handle(Action.CREATE, wrapDoc);
135 JaxbUtils.setValue(entity, "setCreatedAtItem", Date.class, new Date());
137 } catch (EntityExistsException ee) {
139 // 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.
140 // 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.
142 entity = wrapDoc.getWrappedObject(); // the handler should have reset the wrapped transient object with the existing persisted entity we just found.
145 em.getTransaction().commit();
146 handler.complete(Action.CREATE, wrapDoc);
147 return (String) JaxbUtils.getValue(entity, "getCsid");
148 } catch (BadRequestException bre) {
149 rollbackTransaction = true;
151 } catch (DocumentException de) {
152 rollbackTransaction = true;
154 } catch (Exception e) {
155 rollbackTransaction = true;
156 if (logger.isDebugEnabled()) {
157 logger.debug("Caught exception ", e);
159 throw DocumentException.createDocumentException(e);
161 ctx.setProperty(AuthorizationStore.ENTITY_MANAGER_PROP_KEY, null);
163 if (rollbackTransaction == true) {
164 if (em.getTransaction().isActive() == true) {
165 em.getTransaction().rollback();
168 // Don't call this unless "em" is not null -hence the check above.
169 JpaStorageUtils.releaseEntityManagerFactory(emf);
176 * @see org.collectionspace.services.common.storage.StorageClient#get(org.collectionspace.services.common.context.ServiceContext, java.util.List, org.collectionspace.services.common.document.DocumentHandler)
179 public void get(ServiceContext ctx, List<String> csidList, DocumentHandler handler)
180 throws DocumentNotFoundException, DocumentException {
181 throw new UnsupportedOperationException();
185 * @see org.collectionspace.services.common.storage.StorageClient#get(org.collectionspace.services.common.context.ServiceContext, java.lang.String, org.collectionspace.services.common.document.DocumentHandler)
188 public void get(ServiceContext ctx, String id, DocumentHandler handler)
189 throws DocumentNotFoundException, DocumentException {
191 throw new IllegalArgumentException(
192 "get: ctx is missing");
194 if (handler == null) {
195 throw new IllegalArgumentException(
196 "get: handler is missing");
198 EntityManagerFactory emf = null;
199 EntityManager em = null;
201 handler.prepare(Action.GET);
203 o = JpaStorageUtils.getEntity(getEntityName(ctx), id,
206 if (em != null && em.getTransaction().isActive()) {
207 em.getTransaction().rollback();
209 String msg = "could not find entity with id=" + id;
210 throw new DocumentNotFoundException(msg);
212 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(o);
213 handler.handle(Action.GET, wrapDoc);
214 handler.complete(Action.GET, wrapDoc);
215 } catch (DocumentException de) {
217 } catch (Exception e) {
218 if (logger.isDebugEnabled()) {
219 logger.debug("Caught exception ", e);
221 throw new DocumentException(e);
224 JpaStorageUtils.releaseEntityManagerFactory(emf);
230 * @see org.collectionspace.services.common.storage.StorageClient#getAll(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
233 public void getAll(ServiceContext ctx, DocumentHandler handler)
234 throws DocumentNotFoundException, DocumentException {
235 throw new UnsupportedOperationException("use getFiltered instead");
239 * @see org.collectionspace.services.common.storage.StorageClient#getFiltered(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
242 public void getFiltered(ServiceContext ctx, DocumentHandler handler)
243 throws DocumentNotFoundException, DocumentException {
244 QueryContext queryContext = new QueryContext(ctx, handler);
246 DocumentFilter docFilter = handler.getDocumentFilter();
247 if (docFilter == null) {
248 docFilter = handler.createDocumentFilter();
250 EntityManagerFactory emf = null;
251 EntityManager em = null;
253 handler.prepare(Action.GET_ALL);
254 StringBuilder queryStrBldr = new StringBuilder("SELECT a FROM ");
255 queryStrBldr.append(getEntityName(ctx));
256 queryStrBldr.append(" a");
258 List<DocumentFilter.ParamBinding> params = docFilter.buildWhereForSearch(queryStrBldr);
259 emf = JpaStorageUtils.getEntityManagerFactory();
260 em = emf.createEntityManager();
261 String queryStr = queryStrBldr.toString(); //for debugging
262 Query q = em.createQuery(queryStr);
264 for (DocumentFilter.ParamBinding p : params) {
265 q.setParameter(p.getName(), p.getValue());
267 if (docFilter.getOffset() > 0) {
268 q.setFirstResult(docFilter.getOffset());
270 if (docFilter.getPageSize() > 0) {
271 q.setMaxResults(docFilter.getPageSize());
274 //FIXME is transaction required for get?
275 em.getTransaction().begin();
276 List list = q.getResultList();
277 long totalItems = getTotalItems(em, ctx, handler); // Find out how many items our query would find independent of the paging restrictions
278 em.getTransaction().commit();
280 docFilter.setTotalItemsResult(totalItems); // Save the items total in the doc filter for later reporting
282 DocumentWrapper<List> wrapDoc = new DocumentWrapperImpl<List>(list);
283 handler.handle(Action.GET_ALL, wrapDoc);
284 handler.complete(Action.GET_ALL, wrapDoc);
285 } catch (DocumentException de) {
287 } catch (Exception e) {
288 if (logger.isDebugEnabled()) {
289 logger.debug("Caught exception ", e);
291 throw new DocumentException(e);
294 JpaStorageUtils.releaseEntityManagerFactory(emf);
300 * Return the COUNT for a query to find the total number of matches independent of the paging restrictions.
302 private long getTotalItems(EntityManager em, ServiceContext ctx, DocumentHandler handler) {
305 DocumentFilter docFilter = handler.getDocumentFilter();
306 StringBuilder queryStrBldr = new StringBuilder("SELECT COUNT(*) FROM ");
307 queryStrBldr.append(getEntityName(ctx));
308 queryStrBldr.append(" a");
310 List<DocumentFilter.ParamBinding> params = docFilter.buildWhereForSearch(queryStrBldr);
311 String queryStr = queryStrBldr.toString();
312 Query q = em.createQuery(queryStr);
314 for (DocumentFilter.ParamBinding p : params) {
315 q.setParameter(p.getName(), p.getValue());
318 result = (long) q.getSingleResult();
324 * @see org.collectionspace.services.common.storage.StorageClient#update(org.collectionspace.services.common.context.ServiceContext, java.lang.String, org.collectionspace.services.common.document.DocumentHandler)
327 public void update(ServiceContext ctx, String id, DocumentHandler handler)
328 throws BadRequestException, DocumentNotFoundException,
331 throw new IllegalArgumentException(
332 "update: ctx is missing");
334 if (handler == null) {
335 throw new IllegalArgumentException(
336 "update: handler is missing");
338 EntityManagerFactory emf = null;
339 EntityManager em = null;
341 handler.prepare(Action.UPDATE);
342 Object entityReceived = handler.getCommonPart();
343 emf = JpaStorageUtils.getEntityManagerFactory();
344 em = emf.createEntityManager();
345 em.getTransaction().begin();
346 Object entityFound = getEntity(em, id, entityReceived.getClass());
347 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entityFound);
348 handler.handle(Action.UPDATE, wrapDoc);
349 JaxbUtils.setValue(entityFound, "setUpdatedAtItem", Date.class, new Date());
350 em.getTransaction().commit();
351 handler.complete(Action.UPDATE, wrapDoc);
352 } catch (BadRequestException bre) {
353 if (em != null && em.getTransaction().isActive()) {
354 em.getTransaction().rollback();
357 } catch (DocumentException de) {
358 if (em != null && em.getTransaction().isActive()) {
359 em.getTransaction().rollback();
362 } catch (Exception e) {
363 if (logger.isDebugEnabled()) {
364 logger.debug("Caught exception ", e);
366 throw new DocumentException(e);
369 JpaStorageUtils.releaseEntityManagerFactory(emf);
375 * delete removes entity and its child entities
376 * cost: a get before delete
377 * @see org.collectionspace.services.common.storage.StorageClient#delete(org.collectionspace.services.common.context.ServiceContext, java.lang.String)
380 public void delete(ServiceContext ctx, String id)
381 throws DocumentNotFoundException,
384 if (logger.isDebugEnabled()) {
385 logger.debug("delete(ctx, id): deleting entity with id=" + id);
389 throw new IllegalArgumentException(
390 "delete(ctx, id): ctx is missing");
392 EntityManagerFactory emf = null;
393 EntityManager em = null;
396 emf = JpaStorageUtils.getEntityManagerFactory();
397 em = emf.createEntityManager();
399 em.getTransaction().begin();
400 Object entityFound = getEntity(ctx, em, id);
401 if (entityFound == null) {
402 if (em != null && em.getTransaction().isActive()) {
403 em.getTransaction().rollback();
405 String msg = "delete(ctx, id): could not find entity with id=" + id;
407 throw new DocumentNotFoundException(msg);
409 em.remove(entityFound);
410 em.getTransaction().commit();
412 } catch (DocumentException de) {
413 if (em != null && em.getTransaction().isActive()) {
414 em.getTransaction().rollback();
417 } catch (Exception e) {
418 if (logger.isDebugEnabled()) {
419 logger.debug("delete(ctx, id): Caught exception ", e);
421 if (em != null && em.getTransaction().isActive()) {
422 em.getTransaction().rollback();
424 throw new DocumentException(e);
427 JpaStorageUtils.releaseEntityManagerFactory(emf);
433 * deleteWhere uses the where clause to delete an entityReceived represented by the csidReceived
434 * it does not delete any child entities.
437 * @throws DocumentNotFoundException
438 * @throws DocumentException
440 public void deleteWhere(ServiceContext ctx, String id)
441 throws DocumentNotFoundException,
445 throw new IllegalArgumentException(
446 "deleteWhere(ctx, id) : ctx is missing");
449 if (logger.isDebugEnabled()) {
450 logger.debug("deleteWhere(ctx, id): deleting entity with id=" + id);
452 EntityManagerFactory emf = null;
453 EntityManager em = null;
455 StringBuilder deleteStr = new StringBuilder("DELETE FROM ");
456 deleteStr.append(getEntityName(ctx));
457 deleteStr.append(" WHERE csid = :csid and tenantId = :tenantId");
458 //TODO: add tenant csidReceived
460 emf = JpaStorageUtils.getEntityManagerFactory();
461 em = emf.createEntityManager();
462 Query q = em.createQuery(deleteStr.toString());
463 q.setParameter("csid", id);
464 q.setParameter("tenantId", ctx.getTenantId());
467 em.getTransaction().begin();
468 rcount = q.executeUpdate();
470 if (em != null && em.getTransaction().isActive()) {
471 em.getTransaction().rollback();
473 String msg = "deleteWhere(ctx, id) could not find entity with id=" + id;
475 throw new DocumentNotFoundException(msg);
477 em.getTransaction().commit();
479 } catch (DocumentException de) {
480 if (em != null && em.getTransaction().isActive()) {
481 em.getTransaction().rollback();
484 } catch (Exception e) {
485 if (logger.isDebugEnabled()) {
486 logger.debug("deleteWhere(ctx, id) Caught exception ", e);
488 if (em != null && em.getTransaction().isActive()) {
489 em.getTransaction().rollback();
491 throw new DocumentException(e);
494 JpaStorageUtils.releaseEntityManagerFactory(emf);
500 * delete removes entity and its child entities but calls back to given handler
501 * cost: a get before delete
502 * @see org.collectionspace.services.common.storage.StorageClient#delete(org.collectionspace.services.common.context.ServiceContext, java.lang.String)
505 public boolean delete(ServiceContext ctx, String id, DocumentHandler handler)
506 throws DocumentNotFoundException, DocumentException {
507 boolean result = true;
510 throw new IllegalArgumentException(
511 "delete(ctx, ix, handler): ctx is missing");
513 if (handler == null) {
514 throw new IllegalArgumentException(
515 "delete(ctx, ix, handler): handler is missing");
518 EntityManagerFactory emf = null;
519 EntityManager em = null;
521 handler.prepare(Action.DELETE);
523 emf = JpaStorageUtils.getEntityManagerFactory();
524 em = emf.createEntityManager();
526 em.getTransaction().begin();
527 Object entityFound = getEntity(ctx, em, id);
528 if (entityFound == null) {
529 if (em != null && em.getTransaction().isActive()) {
530 em.getTransaction().rollback();
532 String msg = "delete(ctx, ix, handler) could not find entity with id=" + id;
534 throw new DocumentNotFoundException(msg);
536 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entityFound);
537 handler.handle(Action.DELETE, wrapDoc);
538 em.remove(entityFound);
539 em.getTransaction().commit();
540 handler.complete(Action.DELETE, wrapDoc);
541 } catch (DocumentException de) {
542 if (em != null && em.getTransaction().isActive()) {
543 em.getTransaction().rollback();
546 } catch (Exception e) {
547 if (logger.isDebugEnabled()) {
548 logger.debug("delete(ctx, ix, handler): Caught exception ", e);
550 if (em != null && em.getTransaction().isActive()) {
551 em.getTransaction().rollback();
553 throw new DocumentException(e);
556 JpaStorageUtils.releaseEntityManagerFactory(emf);
564 * Gets the entityReceived name.
568 * @return the entityReceived name
570 protected String getEntityName(ServiceContext ctx) {
571 Object o = ctx.getProperty(ServiceContextProperties.ENTITY_NAME);
573 throw new IllegalArgumentException(ServiceContextProperties.ENTITY_NAME
574 + "property is missing in context "
582 * getEntity returns persistent entity for given id. it assumes that
583 * service context has property ServiceContextProperties.ENTITY_CLASS set
584 * rolls back the transaction if not found
585 * @param ctx service context
586 * @param em entity manager
587 * @param csid received
589 * @throws DocumentNotFoundException and rollsback the transaction if active
591 protected Object getEntity(ServiceContext ctx, EntityManager em, String id)
592 throws DocumentNotFoundException {
593 Class entityClazz = (Class) ctx.getProperty(ServiceContextProperties.ENTITY_CLASS);
594 if (entityClazz == null) {
595 String msg = ServiceContextProperties.ENTITY_CLASS
596 + " property is missing in the context";
598 throw new IllegalArgumentException(msg);
600 return getEntity(em, id, entityClazz);
604 * getEntity retrieves the persistent entity of given class for given id
605 * rolls back the transaction if not found
607 * @param id entity id
610 * @throws DocumentNotFoundException and rollsback the transaction if active
612 protected Object getEntity(EntityManager em, String id, Class entityClazz)
613 throws DocumentNotFoundException {
614 Object entityFound = JpaStorageUtils.getEntity(em, id, entityClazz);
615 if (entityFound == null) {
616 if (em != null && em.getTransaction().isActive()) {
617 em.getTransaction().rollback();
619 String msg = "could not find entity of type=" + entityClazz.getName()
622 throw new DocumentNotFoundException(msg);
628 public void get(ServiceContext ctx, DocumentHandler handler)
629 throws DocumentNotFoundException, DocumentException {
630 throw new UnsupportedOperationException();
634 public void doWorkflowTransition(ServiceContext ctx, String id,
635 DocumentHandler handler, TransitionDef transitionDef)
636 throws BadRequestException, DocumentNotFoundException,
638 // Do nothing. JPA services do not support workflow.
642 public void deleteWithWhereClause(ServiceContext ctx, String whereClause,
643 DocumentHandler handler) throws DocumentNotFoundException,
645 throw new UnsupportedOperationException();
649 public boolean synchronize(ServiceContext ctx, Object specifier,
650 DocumentHandler handler) throws DocumentNotFoundException,
651 TransactionException, DocumentException {
652 // TODO Auto-generated method stub
653 // Do nothing. Subclasses can override if they want/need to.
658 public boolean synchronizeItem(ServiceContext ctx, AuthorityItemSpecifier itemSpecifier,
659 DocumentHandler handler) throws DocumentNotFoundException,
660 TransactionException, DocumentException {
661 // TODO Auto-generated method stub
662 // Do nothing. Subclasses can override if they want/need to.