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.EntityExistsException;
24 import javax.persistence.PersistenceException;
25 import javax.persistence.Query;
26 import javax.persistence.RollbackException;
28 import org.collectionspace.services.common.document.BadRequestException;
29 import org.collectionspace.services.common.document.DocumentException;
30 import org.collectionspace.services.common.document.DocumentFilter;
31 import org.collectionspace.services.common.document.DocumentHandler;
32 import org.collectionspace.services.common.document.DocumentNotFoundException;
33 import org.collectionspace.services.common.document.DocumentHandler.Action;
34 import org.collectionspace.services.common.document.DocumentWrapper;
35 import org.collectionspace.services.common.document.DocumentWrapperImpl;
36 import org.collectionspace.services.common.document.JaxbUtils;
37 import org.collectionspace.services.common.document.TransactionException;
38 import org.collectionspace.services.common.storage.StorageClient;
39 import org.collectionspace.services.common.storage.TransactionContext;
40 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthorityItemSpecifier;
41 import org.collectionspace.services.common.context.ServiceContextProperties;
42 import org.collectionspace.services.client.PoxPayloadIn;
43 import org.collectionspace.services.client.PoxPayloadOut;
44 import org.collectionspace.services.common.api.Tools;
45 import org.collectionspace.services.common.context.ServiceContext;
46 import org.collectionspace.services.lifecycle.TransitionDef;
47 import org.hibernate.exception.ConstraintViolationException;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * JpaStorageClient is used to perform CRUD operations on SQL storage using JPA.
53 * It uses @see DocumentHandler as IOHandler with the client.
54 * All the operations in this client are carried out under their own transactions.
55 * A call to any method would start and commit/rollback a transaction.
57 * Assumption: each persistent entityReceived has the following 3 attributes
58 <xs:element name="createdAt" type="xs:dateTime">
62 <orm:column name="created_at" nullable="false"/>
67 <xs:element name="updatedAt" type="xs:dateTime">
71 <orm:column name="updated_at" />
77 <xs:attribute name="csid" type="xs:string">
81 <orm:column name="csid" length="128" nullable="false"/>
87 * $LastChangedRevision: $ $LastChangedDate: $
89 public class JpaStorageClientImpl implements StorageClient {
92 private final Logger logger = LoggerFactory.getLogger(JpaStorageClientImpl.class);
95 * Instantiates a new jpa storage client.
97 public JpaStorageClientImpl() {
102 * @see org.collectionspace.services.common.storage.StorageClient#create(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
104 @SuppressWarnings({ "rawtypes", "unchecked" })
106 public String create(ServiceContext ctx,
107 DocumentHandler handler) throws BadRequestException,
109 String result = null;
111 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
113 handler.prepare(Action.CREATE);
114 Object entity = handler.getCommonPart();
115 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entity);
117 jpaConnectionContext.beginTransaction();
119 handler.handle(Action.CREATE, wrapDoc);
120 JaxbUtils.setValue(entity, "setCreatedAtItem", Date.class, new Date());
121 jpaConnectionContext.persist(entity);
122 } catch (EntityExistsException ee) { // FIXME: No, don't allow duplicates
124 // 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.
125 // 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.
127 entity = wrapDoc.getWrappedObject(); // the handler should have reset the wrapped transient object with the existing persisted entity we just found.
129 handler.complete(Action.CREATE, wrapDoc);
130 jpaConnectionContext.commitTransaction();
132 result = (String)JaxbUtils.getValue(entity, "getCsid");
133 } catch (BadRequestException bre) {
134 jpaConnectionContext.markForRollback();
136 } catch (DocumentException de) {
137 jpaConnectionContext.markForRollback();
139 } catch (RollbackException rbe) {
140 //jpaConnectionContext.markForRollback();
141 throw DocumentException.createDocumentException(rbe);
142 } catch (PersistenceException pe) {
143 if (pe.getCause() instanceof ConstraintViolationException) {
144 throw new DocumentException(DocumentException.DUPLICATE_RECORD_MSG, pe, DocumentException.DUPLICATE_RECORD_ERR);
146 throw new DocumentException(pe);
148 } catch (Exception e) {
149 jpaConnectionContext.markForRollback();
150 logger.debug("Caught exception ", e);
151 throw DocumentException.createDocumentException(e);
153 ctx.closeConnection();
160 * @see org.collectionspace.services.common.storage.StorageClient#get(org.collectionspace.services.common.context.ServiceContext, java.util.List, org.collectionspace.services.common.document.DocumentHandler)
162 @SuppressWarnings("rawtypes")
164 public void get(ServiceContext ctx, List<String> csidList, DocumentHandler handler)
165 throws DocumentNotFoundException, DocumentException {
166 throw new UnsupportedOperationException();
170 * @see org.collectionspace.services.common.storage.StorageClient#get(org.collectionspace.services.common.context.ServiceContext, java.lang.String, org.collectionspace.services.common.document.DocumentHandler)
172 @SuppressWarnings({ "unchecked", "rawtypes" })
174 public void get(ServiceContext ctx, String id, DocumentHandler handler)
175 throws DocumentNotFoundException, DocumentException {
177 JPATransactionContext jpaTransactionContext = (JPATransactionContext)ctx.openConnection();
179 handler.prepare(Action.GET);
181 o = JpaStorageUtils.getEntity(jpaTransactionContext, handler.getDocumentFilter(), getEntityName(ctx), id, ctx.getTenantId());
183 String msg = "Could not find entity with id=" + id;
184 throw new DocumentNotFoundException(msg);
186 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(o);
187 handler.handle(Action.GET, wrapDoc);
188 handler.complete(Action.GET, wrapDoc);
189 } catch (DocumentException de) {
191 } catch (Exception e) {
192 if (logger.isDebugEnabled()) {
193 logger.debug("Caught exception ", e);
195 throw new DocumentException(e);
197 ctx.closeConnection();
202 * @see org.collectionspace.services.common.storage.StorageClient#getAll(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
204 @SuppressWarnings("rawtypes")
206 public void getAll(ServiceContext ctx, DocumentHandler handler)
207 throws DocumentNotFoundException, DocumentException {
208 throw new UnsupportedOperationException("use getFiltered instead");
212 * @see org.collectionspace.services.common.storage.StorageClient#getFiltered(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
214 @SuppressWarnings({ "unchecked", "rawtypes" })
216 public void getFiltered(ServiceContext ctx, DocumentHandler handler)
217 throws DocumentNotFoundException, DocumentException {
219 DocumentFilter docFilter = handler.getDocumentFilter();
220 if (docFilter == null) {
221 docFilter = handler.createDocumentFilter();
224 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
226 handler.prepare(Action.GET_ALL);
227 StringBuilder queryStrBldr = new StringBuilder("SELECT a FROM ");
228 queryStrBldr.append(getEntityName(ctx));
229 queryStrBldr.append(" a");
231 String joinFetch = docFilter.getJoinFetchClause();
232 if (Tools.notBlank(joinFetch)) {
233 queryStrBldr.append(" " + joinFetch);
236 List<DocumentFilter.ParamBinding> params = docFilter.buildWhereForSearch(queryStrBldr);
237 String queryStr = queryStrBldr.toString(); //for debugging
238 Query q = jpaConnectionContext.createQuery(queryStr);
240 for (DocumentFilter.ParamBinding p : params) {
241 q.setParameter(p.getName(), p.getValue());
243 if (docFilter.getOffset() > 0) {
244 q.setFirstResult(docFilter.getOffset());
246 if (docFilter.getPageSize() > 0) {
247 q.setMaxResults(docFilter.getPageSize());
250 jpaConnectionContext.beginTransaction();
251 List list = q.getResultList();
252 long totalItems = getTotalItems(jpaConnectionContext, ctx, handler); // Find out how many items our query would find independent of the paging restrictions
253 docFilter.setTotalItemsResult(totalItems); // Save the items total in the doc filter for later reporting
254 DocumentWrapper<List> wrapDoc = new DocumentWrapperImpl<List>(list);
255 handler.handle(Action.GET_ALL, wrapDoc);
256 handler.complete(Action.GET_ALL, wrapDoc);
257 jpaConnectionContext.commitTransaction();
258 } catch (DocumentException de) {
260 } catch (Exception e) {
261 if (logger.isDebugEnabled()) {
262 logger.debug("Caught exception ", e);
264 throw new DocumentException(e);
266 ctx.closeConnection();
271 * Return the COUNT for a query to find the total number of matches independent of the paging restrictions.
273 @SuppressWarnings("rawtypes")
274 private long getTotalItems(JPATransactionContext jpaTransactionContext, ServiceContext ctx, DocumentHandler handler) {
277 DocumentFilter docFilter = handler.getDocumentFilter();
278 StringBuilder queryStrBldr = new StringBuilder("SELECT COUNT(*) FROM ");
279 queryStrBldr.append(getEntityName(ctx));
280 queryStrBldr.append(" a");
282 List<DocumentFilter.ParamBinding> params = docFilter.buildWhereForSearch(queryStrBldr);
283 String queryStr = queryStrBldr.toString();
284 Query q = jpaTransactionContext.createQuery(queryStr);
286 for (DocumentFilter.ParamBinding p : params) {
287 q.setParameter(p.getName(), p.getValue());
290 result = (long) q.getSingleResult();
296 * @see org.collectionspace.services.common.storage.StorageClient#update(org.collectionspace.services.common.context.ServiceContext, java.lang.String, org.collectionspace.services.common.document.DocumentHandler)
298 @SuppressWarnings({ "rawtypes", "unchecked" })
300 public void update(ServiceContext ctx, String id, DocumentHandler handler)
301 throws BadRequestException, DocumentNotFoundException,
304 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
306 jpaConnectionContext.beginTransaction();
308 handler.prepare(Action.UPDATE);
309 Object entityReceived = handler.getCommonPart();
310 Object entityFound = getEntity(ctx, id, entityReceived.getClass());
311 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entityFound);
312 handler.handle(Action.UPDATE, wrapDoc);
313 JaxbUtils.setValue(entityFound, "setUpdatedAtItem", Date.class, new Date());
314 handler.complete(Action.UPDATE, wrapDoc);
316 jpaConnectionContext.commitTransaction();
317 } catch (BadRequestException bre) {
318 jpaConnectionContext.markForRollback();
320 } catch (DocumentException de) {
321 jpaConnectionContext.markForRollback();
323 } catch (Exception e) {
324 jpaConnectionContext.markForRollback();
325 if (logger.isDebugEnabled()) {
326 logger.debug("Caught exception ", e);
328 throw new DocumentException(e);
330 ctx.closeConnection();
335 * delete removes entity and its child entities
336 * cost: a get before delete
337 * @see org.collectionspace.services.common.storage.StorageClient#delete(org.collectionspace.services.common.context.ServiceContext, java.lang.String)
340 public void delete(@SuppressWarnings("rawtypes") ServiceContext ctx, String id)
341 throws DocumentNotFoundException,
344 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
346 jpaConnectionContext.beginTransaction();
347 Object entityFound = getEntity(ctx, id);
348 if (entityFound == null) {
349 jpaConnectionContext.markForRollback();
350 String msg = "delete(ctx, id): could not find entity with id=" + id;
352 throw new DocumentNotFoundException(msg);
354 jpaConnectionContext.remove(entityFound);
355 jpaConnectionContext.commitTransaction();
356 } catch (DocumentException de) {
357 jpaConnectionContext.markForRollback();
359 } catch (Exception e) {
360 if (logger.isDebugEnabled()) {
361 logger.debug("delete(ctx, id): Caught exception ", e);
363 jpaConnectionContext.markForRollback();
364 throw new DocumentException(e);
366 ctx.closeConnection();
371 * deleteWhere uses the where clause to delete an entityReceived represented by the csidReceived
372 * it does not delete any child entities.
375 * @throws DocumentNotFoundException
376 * @throws DocumentException
378 public void deleteWhere(@SuppressWarnings("rawtypes") ServiceContext ctx, String id)
379 throws DocumentNotFoundException,
382 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
384 StringBuilder deleteStr = new StringBuilder("DELETE FROM ");
385 deleteStr.append(getEntityName(ctx));
386 deleteStr.append(" WHERE csid = :csid and tenantId = :tenantId");
387 //TODO: add tenant csidReceived
389 Query q = jpaConnectionContext.createQuery(deleteStr.toString());
390 q.setParameter("csid", id);
391 q.setParameter("tenantId", ctx.getTenantId());
394 jpaConnectionContext.beginTransaction();
395 rcount = q.executeUpdate();
397 jpaConnectionContext.markForRollback();
398 String msg = "deleteWhere(ctx, id) could not find entity with id=" + id;
400 throw new DocumentNotFoundException(msg);
402 jpaConnectionContext.commitTransaction();
403 } catch (DocumentException de) {
404 jpaConnectionContext.markForRollback();
406 } catch (Exception e) {
407 if (logger.isDebugEnabled()) {
408 logger.debug("deleteWhere(ctx, id) Caught exception ", e);
410 jpaConnectionContext.markForRollback();
411 throw new DocumentException(e);
413 ctx.closeConnection();
418 * delete removes entity and its child entities but calls back to given handler
419 * cost: a get before delete
420 * @see org.collectionspace.services.common.storage.StorageClient#delete(org.collectionspace.services.common.context.ServiceContext, java.lang.String)
422 @SuppressWarnings({ "rawtypes" })
424 public boolean delete(ServiceContext ctx, String id, DocumentHandler handler)
425 throws DocumentNotFoundException, DocumentException {
426 boolean result = false;
428 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
430 jpaConnectionContext.beginTransaction();
431 Object entityFound = getEntity(ctx, id);
432 if (entityFound == null) {
433 String msg = "delete(ctx, ix, handler) could not find entity with id=" + id;
435 throw new DocumentNotFoundException(msg);
437 result = delete(ctx, entityFound, handler);
438 jpaConnectionContext.commitTransaction();
439 } catch (DocumentException de) {
440 jpaConnectionContext.markForRollback();
442 } catch (Exception e) {
443 if (logger.isDebugEnabled()) {
444 logger.debug("delete(ctx, ix, handler): Caught exception ", e);
446 jpaConnectionContext.markForRollback();
447 throw new DocumentException(e);
449 ctx.closeConnection();
455 @SuppressWarnings({ "rawtypes", "unchecked" })
457 public boolean delete(ServiceContext ctx, Object entity, DocumentHandler handler)
458 throws DocumentNotFoundException, DocumentException {
459 boolean result = false;
461 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
463 jpaConnectionContext.beginTransaction();
464 handler.prepare(Action.DELETE);
465 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entity);
466 handler.handle(Action.DELETE, wrapDoc);
467 jpaConnectionContext.remove(entity);
468 handler.complete(Action.DELETE, wrapDoc);
469 jpaConnectionContext.commitTransaction();
471 } catch (DocumentException de) {
472 jpaConnectionContext.markForRollback();
474 } catch (Exception e) {
475 if (logger.isDebugEnabled()) {
476 logger.debug("delete(ctx, ix, handler): Caught exception ", e);
478 jpaConnectionContext.markForRollback();
479 throw new DocumentException(e);
481 ctx.closeConnection();
488 * Gets the entityReceived name.
492 * @return the entityReceived name
494 protected String getEntityName(@SuppressWarnings("rawtypes") ServiceContext ctx) {
495 Object o = ctx.getProperty(ServiceContextProperties.ENTITY_NAME);
497 throw new IllegalArgumentException(ServiceContextProperties.ENTITY_NAME
498 + "property is missing in context "
506 * getEntity returns persistent entity for given id. it assumes that
507 * service context has property ServiceContextProperties.ENTITY_CLASS set
508 * @param ctx service context
509 * @param csid received
511 * @throws DocumentNotFoundException
512 * @throws TransactionException
514 protected Object getEntity(@SuppressWarnings("rawtypes") ServiceContext ctx, String id)
515 throws DocumentNotFoundException, TransactionException {
516 Class<?> entityClazz = (Class<?>) ctx.getProperty(ServiceContextProperties.ENTITY_CLASS);
517 if (entityClazz == null) {
518 String msg = ServiceContextProperties.ENTITY_CLASS + " property is missing in the context";
520 throw new IllegalArgumentException(msg);
523 return getEntity(ctx, id, entityClazz);
527 * getEntity retrieves the persistent entity of given class for given id
528 * rolls back the transaction if not found
530 * @param id entity id
533 * @throws DocumentNotFoundException and rollsback the transaction if active
534 * @throws TransactionException
536 protected Object getEntity(@SuppressWarnings("rawtypes") ServiceContext ctx, String id, Class<?> entityClazz)
537 throws DocumentNotFoundException, TransactionException {
538 Object entityFound = null;
540 JPATransactionContext jpaTransactionContext = (JPATransactionContext)ctx.openConnection();
542 entityFound = JpaStorageUtils.getEntity(jpaTransactionContext, id, entityClazz); // FIXME: # Should be qualifying with the tenant ID
543 if (entityFound == null) {
544 String msg = "could not find entity of type=" + entityClazz.getName()
547 throw new DocumentNotFoundException(msg);
550 ctx.closeConnection();
556 @SuppressWarnings("rawtypes")
558 public void get(ServiceContext ctx, DocumentHandler handler)
559 throws DocumentNotFoundException, DocumentException {
560 throw new UnsupportedOperationException();
563 @SuppressWarnings("rawtypes")
565 public void doWorkflowTransition(ServiceContext ctx, String id,
566 DocumentHandler handler, TransitionDef transitionDef)
567 throws BadRequestException, DocumentNotFoundException,
569 // Do nothing. JPA services do not support workflow.
572 @SuppressWarnings("rawtypes")
574 public void deleteWithWhereClause(ServiceContext ctx, String whereClause,
575 DocumentHandler handler) throws DocumentNotFoundException,
577 throw new UnsupportedOperationException();
580 @SuppressWarnings("rawtypes")
582 public boolean synchronize(ServiceContext ctx, Object specifier,
583 DocumentHandler handler) throws DocumentNotFoundException,
584 TransactionException, DocumentException {
585 // TODO Auto-generated method stub
586 // Do nothing. Subclasses can override if they want/need to.
590 @SuppressWarnings("rawtypes")
592 public boolean synchronizeItem(ServiceContext ctx, AuthorityItemSpecifier itemSpecifier,
593 DocumentHandler handler) throws DocumentNotFoundException,
594 TransactionException, DocumentException {
595 // TODO Auto-generated method stub
596 // Do nothing. Subclasses can override if they want/need to.
601 public void releaseRepositorySession(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx, Object repoSession)
602 throws TransactionException {
603 // TODO Auto-generated method stub