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.Query;
25 import javax.persistence.RollbackException;
27 import org.collectionspace.services.common.document.BadRequestException;
28 import org.collectionspace.services.common.document.DocumentException;
29 import org.collectionspace.services.common.document.DocumentFilter;
30 import org.collectionspace.services.common.document.DocumentHandler;
31 import org.collectionspace.services.common.document.DocumentNotFoundException;
32 import org.collectionspace.services.common.document.DocumentHandler.Action;
33 import org.collectionspace.services.common.document.DocumentWrapper;
34 import org.collectionspace.services.common.document.DocumentWrapperImpl;
35 import org.collectionspace.services.common.document.JaxbUtils;
36 import org.collectionspace.services.common.document.TransactionException;
37 import org.collectionspace.services.common.storage.StorageClient;
38 import org.collectionspace.services.common.storage.TransactionContext;
39 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthorityItemSpecifier;
40 import org.collectionspace.services.common.context.ServiceContextProperties;
41 import org.collectionspace.services.client.PoxPayloadIn;
42 import org.collectionspace.services.client.PoxPayloadOut;
43 import org.collectionspace.services.common.api.Tools;
44 import org.collectionspace.services.common.context.ServiceContext;
45 import org.collectionspace.services.lifecycle.TransitionDef;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
51 * JpaStorageClient is used to perform CRUD operations on SQL storage using JPA.
52 * It uses @see DocumentHandler as IOHandler with the client.
53 * All the operations in this client are carried out under their own transactions.
54 * A call to any method would start and commit/rollback a transaction.
56 * Assumption: each persistent entityReceived has the following 3 attributes
57 <xs:element name="createdAt" type="xs:dateTime">
61 <orm:column name="created_at" nullable="false"/>
66 <xs:element name="updatedAt" type="xs:dateTime">
70 <orm:column name="updated_at" />
76 <xs:attribute name="csid" type="xs:string">
80 <orm:column name="csid" length="128" nullable="false"/>
86 * $LastChangedRevision: $ $LastChangedDate: $
88 public class JpaStorageClientImpl implements StorageClient {
91 private final Logger logger = LoggerFactory.getLogger(JpaStorageClientImpl.class);
94 * Instantiates a new jpa storage client.
96 public JpaStorageClientImpl() {
101 * @see org.collectionspace.services.common.storage.StorageClient#create(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
103 @SuppressWarnings({ "rawtypes", "unchecked" })
105 public String create(ServiceContext ctx,
106 DocumentHandler handler) throws BadRequestException,
108 String result = null;
110 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
112 handler.prepare(Action.CREATE);
113 Object entity = handler.getCommonPart();
114 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entity);
116 jpaConnectionContext.beginTransaction();
118 handler.handle(Action.CREATE, wrapDoc);
119 JaxbUtils.setValue(entity, "setCreatedAtItem", Date.class, new Date());
120 jpaConnectionContext.persist(entity);
121 } catch (EntityExistsException ee) { // FIXME: No, don't allow duplicates
123 // 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.
124 // 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.
126 entity = wrapDoc.getWrappedObject(); // the handler should have reset the wrapped transient object with the existing persisted entity we just found.
128 handler.complete(Action.CREATE, wrapDoc);
129 jpaConnectionContext.commitTransaction();
131 result = (String)JaxbUtils.getValue(entity, "getCsid");
132 } catch (BadRequestException bre) {
133 jpaConnectionContext.markForRollback();
135 } catch (DocumentException de) {
136 jpaConnectionContext.markForRollback();
138 } catch (RollbackException rbe) {
139 //jpaConnectionContext.markForRollback();
140 throw DocumentException.createDocumentException(rbe);
141 } catch (Exception e) {
142 jpaConnectionContext.markForRollback();
143 logger.debug("Caught exception ", e);
144 throw DocumentException.createDocumentException(e);
146 ctx.closeConnection();
153 * @see org.collectionspace.services.common.storage.StorageClient#get(org.collectionspace.services.common.context.ServiceContext, java.util.List, org.collectionspace.services.common.document.DocumentHandler)
155 @SuppressWarnings("rawtypes")
157 public void get(ServiceContext ctx, List<String> csidList, DocumentHandler handler)
158 throws DocumentNotFoundException, DocumentException {
159 throw new UnsupportedOperationException();
163 * @see org.collectionspace.services.common.storage.StorageClient#get(org.collectionspace.services.common.context.ServiceContext, java.lang.String, org.collectionspace.services.common.document.DocumentHandler)
165 @SuppressWarnings({ "unchecked", "rawtypes" })
167 public void get(ServiceContext ctx, String id, DocumentHandler handler)
168 throws DocumentNotFoundException, DocumentException {
170 JPATransactionContext jpaTransactionContext = (JPATransactionContext)ctx.openConnection();
172 handler.prepare(Action.GET);
174 o = JpaStorageUtils.getEntity(jpaTransactionContext, handler.getDocumentFilter(), getEntityName(ctx), id, ctx.getTenantId());
176 String msg = "Could not find entity with id=" + id;
177 throw new DocumentNotFoundException(msg);
179 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(o);
180 handler.handle(Action.GET, wrapDoc);
181 handler.complete(Action.GET, wrapDoc);
182 } catch (DocumentException de) {
184 } catch (Exception e) {
185 if (logger.isDebugEnabled()) {
186 logger.debug("Caught exception ", e);
188 throw new DocumentException(e);
190 ctx.closeConnection();
195 * @see org.collectionspace.services.common.storage.StorageClient#getAll(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
197 @SuppressWarnings("rawtypes")
199 public void getAll(ServiceContext ctx, DocumentHandler handler)
200 throws DocumentNotFoundException, DocumentException {
201 throw new UnsupportedOperationException("use getFiltered instead");
205 * @see org.collectionspace.services.common.storage.StorageClient#getFiltered(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
207 @SuppressWarnings({ "unchecked", "rawtypes" })
209 public void getFiltered(ServiceContext ctx, DocumentHandler handler)
210 throws DocumentNotFoundException, DocumentException {
212 DocumentFilter docFilter = handler.getDocumentFilter();
213 if (docFilter == null) {
214 docFilter = handler.createDocumentFilter();
217 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
219 handler.prepare(Action.GET_ALL);
220 StringBuilder queryStrBldr = new StringBuilder("SELECT a FROM ");
221 queryStrBldr.append(getEntityName(ctx));
222 queryStrBldr.append(" a");
224 String joinFetch = docFilter.getJoinFetchClause();
225 if (Tools.notBlank(joinFetch)) {
226 queryStrBldr.append(" " + joinFetch);
229 List<DocumentFilter.ParamBinding> params = docFilter.buildWhereForSearch(queryStrBldr);
230 String queryStr = queryStrBldr.toString(); //for debugging
231 Query q = jpaConnectionContext.createQuery(queryStr);
233 for (DocumentFilter.ParamBinding p : params) {
234 q.setParameter(p.getName(), p.getValue());
236 if (docFilter.getOffset() > 0) {
237 q.setFirstResult(docFilter.getOffset());
239 if (docFilter.getPageSize() > 0) {
240 q.setMaxResults(docFilter.getPageSize());
243 jpaConnectionContext.beginTransaction();
244 List list = q.getResultList();
245 long totalItems = getTotalItems(jpaConnectionContext, ctx, handler); // Find out how many items our query would find independent of the paging restrictions
246 docFilter.setTotalItemsResult(totalItems); // Save the items total in the doc filter for later reporting
247 DocumentWrapper<List> wrapDoc = new DocumentWrapperImpl<List>(list);
248 handler.handle(Action.GET_ALL, wrapDoc);
249 handler.complete(Action.GET_ALL, wrapDoc);
250 jpaConnectionContext.commitTransaction();
251 } catch (DocumentException de) {
253 } catch (Exception e) {
254 if (logger.isDebugEnabled()) {
255 logger.debug("Caught exception ", e);
257 throw new DocumentException(e);
259 ctx.closeConnection();
264 * Return the COUNT for a query to find the total number of matches independent of the paging restrictions.
266 @SuppressWarnings("rawtypes")
267 private long getTotalItems(JPATransactionContext jpaTransactionContext, ServiceContext ctx, DocumentHandler handler) {
270 DocumentFilter docFilter = handler.getDocumentFilter();
271 StringBuilder queryStrBldr = new StringBuilder("SELECT COUNT(*) FROM ");
272 queryStrBldr.append(getEntityName(ctx));
273 queryStrBldr.append(" a");
275 List<DocumentFilter.ParamBinding> params = docFilter.buildWhereForSearch(queryStrBldr);
276 String queryStr = queryStrBldr.toString();
277 Query q = jpaTransactionContext.createQuery(queryStr);
279 for (DocumentFilter.ParamBinding p : params) {
280 q.setParameter(p.getName(), p.getValue());
283 result = (long) q.getSingleResult();
289 * @see org.collectionspace.services.common.storage.StorageClient#update(org.collectionspace.services.common.context.ServiceContext, java.lang.String, org.collectionspace.services.common.document.DocumentHandler)
291 @SuppressWarnings({ "rawtypes", "unchecked" })
293 public void update(ServiceContext ctx, String id, DocumentHandler handler)
294 throws BadRequestException, DocumentNotFoundException,
297 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
299 jpaConnectionContext.beginTransaction();
301 handler.prepare(Action.UPDATE);
302 Object entityReceived = handler.getCommonPart();
303 Object entityFound = getEntity(ctx, id, entityReceived.getClass());
304 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entityFound);
305 handler.handle(Action.UPDATE, wrapDoc);
306 JaxbUtils.setValue(entityFound, "setUpdatedAtItem", Date.class, new Date());
307 handler.complete(Action.UPDATE, wrapDoc);
309 jpaConnectionContext.commitTransaction();
310 } catch (BadRequestException bre) {
311 jpaConnectionContext.markForRollback();
313 } catch (DocumentException de) {
314 jpaConnectionContext.markForRollback();
316 } catch (Exception e) {
317 jpaConnectionContext.markForRollback();
318 if (logger.isDebugEnabled()) {
319 logger.debug("Caught exception ", e);
321 throw new DocumentException(e);
323 ctx.closeConnection();
328 * delete removes entity and its child entities
329 * cost: a get before delete
330 * @see org.collectionspace.services.common.storage.StorageClient#delete(org.collectionspace.services.common.context.ServiceContext, java.lang.String)
333 public void delete(@SuppressWarnings("rawtypes") ServiceContext ctx, String id)
334 throws DocumentNotFoundException,
337 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
339 jpaConnectionContext.beginTransaction();
340 Object entityFound = getEntity(ctx, id);
341 if (entityFound == null) {
342 jpaConnectionContext.markForRollback();
343 String msg = "delete(ctx, id): could not find entity with id=" + id;
345 throw new DocumentNotFoundException(msg);
347 jpaConnectionContext.remove(entityFound);
348 jpaConnectionContext.commitTransaction();
349 } catch (DocumentException de) {
350 jpaConnectionContext.markForRollback();
352 } catch (Exception e) {
353 if (logger.isDebugEnabled()) {
354 logger.debug("delete(ctx, id): Caught exception ", e);
356 jpaConnectionContext.markForRollback();
357 throw new DocumentException(e);
359 ctx.closeConnection();
364 * deleteWhere uses the where clause to delete an entityReceived represented by the csidReceived
365 * it does not delete any child entities.
368 * @throws DocumentNotFoundException
369 * @throws DocumentException
371 public void deleteWhere(@SuppressWarnings("rawtypes") ServiceContext ctx, String id)
372 throws DocumentNotFoundException,
375 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
377 StringBuilder deleteStr = new StringBuilder("DELETE FROM ");
378 deleteStr.append(getEntityName(ctx));
379 deleteStr.append(" WHERE csid = :csid and tenantId = :tenantId");
380 //TODO: add tenant csidReceived
382 Query q = jpaConnectionContext.createQuery(deleteStr.toString());
383 q.setParameter("csid", id);
384 q.setParameter("tenantId", ctx.getTenantId());
387 jpaConnectionContext.beginTransaction();
388 rcount = q.executeUpdate();
390 jpaConnectionContext.markForRollback();
391 String msg = "deleteWhere(ctx, id) could not find entity with id=" + id;
393 throw new DocumentNotFoundException(msg);
395 jpaConnectionContext.commitTransaction();
396 } catch (DocumentException de) {
397 jpaConnectionContext.markForRollback();
399 } catch (Exception e) {
400 if (logger.isDebugEnabled()) {
401 logger.debug("deleteWhere(ctx, id) Caught exception ", e);
403 jpaConnectionContext.markForRollback();
404 throw new DocumentException(e);
406 ctx.closeConnection();
411 * delete removes entity and its child entities but calls back to given handler
412 * cost: a get before delete
413 * @see org.collectionspace.services.common.storage.StorageClient#delete(org.collectionspace.services.common.context.ServiceContext, java.lang.String)
415 @SuppressWarnings({ "rawtypes" })
417 public boolean delete(ServiceContext ctx, String id, DocumentHandler handler)
418 throws DocumentNotFoundException, DocumentException {
419 boolean result = false;
421 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
423 jpaConnectionContext.beginTransaction();
424 Object entityFound = getEntity(ctx, id);
425 if (entityFound == null) {
426 String msg = "delete(ctx, ix, handler) could not find entity with id=" + id;
428 throw new DocumentNotFoundException(msg);
430 result = delete(ctx, entityFound, handler);
431 jpaConnectionContext.commitTransaction();
432 } catch (DocumentException de) {
433 jpaConnectionContext.markForRollback();
435 } catch (Exception e) {
436 if (logger.isDebugEnabled()) {
437 logger.debug("delete(ctx, ix, handler): Caught exception ", e);
439 jpaConnectionContext.markForRollback();
440 throw new DocumentException(e);
442 ctx.closeConnection();
448 @SuppressWarnings({ "rawtypes", "unchecked" })
450 public boolean delete(ServiceContext ctx, Object entity, DocumentHandler handler)
451 throws DocumentNotFoundException, DocumentException {
452 boolean result = false;
454 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
456 jpaConnectionContext.beginTransaction();
457 handler.prepare(Action.DELETE);
458 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entity);
459 handler.handle(Action.DELETE, wrapDoc);
460 jpaConnectionContext.remove(entity);
461 handler.complete(Action.DELETE, wrapDoc);
462 jpaConnectionContext.commitTransaction();
464 } catch (DocumentException de) {
465 jpaConnectionContext.markForRollback();
467 } catch (Exception e) {
468 if (logger.isDebugEnabled()) {
469 logger.debug("delete(ctx, ix, handler): Caught exception ", e);
471 jpaConnectionContext.markForRollback();
472 throw new DocumentException(e);
474 ctx.closeConnection();
481 * Gets the entityReceived name.
485 * @return the entityReceived name
487 protected String getEntityName(@SuppressWarnings("rawtypes") ServiceContext ctx) {
488 Object o = ctx.getProperty(ServiceContextProperties.ENTITY_NAME);
490 throw new IllegalArgumentException(ServiceContextProperties.ENTITY_NAME
491 + "property is missing in context "
499 * getEntity returns persistent entity for given id. it assumes that
500 * service context has property ServiceContextProperties.ENTITY_CLASS set
501 * @param ctx service context
502 * @param csid received
504 * @throws DocumentNotFoundException
505 * @throws TransactionException
507 protected Object getEntity(@SuppressWarnings("rawtypes") ServiceContext ctx, String id)
508 throws DocumentNotFoundException, TransactionException {
509 Class<?> entityClazz = (Class<?>) ctx.getProperty(ServiceContextProperties.ENTITY_CLASS);
510 if (entityClazz == null) {
511 String msg = ServiceContextProperties.ENTITY_CLASS + " property is missing in the context";
513 throw new IllegalArgumentException(msg);
516 return getEntity(ctx, id, entityClazz);
520 * getEntity retrieves the persistent entity of given class for given id
521 * rolls back the transaction if not found
523 * @param id entity id
526 * @throws DocumentNotFoundException and rollsback the transaction if active
527 * @throws TransactionException
529 protected Object getEntity(@SuppressWarnings("rawtypes") ServiceContext ctx, String id, Class<?> entityClazz)
530 throws DocumentNotFoundException, TransactionException {
531 Object entityFound = null;
533 JPATransactionContext jpaTransactionContext = (JPATransactionContext)ctx.openConnection();
535 entityFound = JpaStorageUtils.getEntity(jpaTransactionContext, id, entityClazz); // FIXME: # Should be qualifying with the tenant ID
536 if (entityFound == null) {
537 String msg = "could not find entity of type=" + entityClazz.getName()
540 throw new DocumentNotFoundException(msg);
543 ctx.closeConnection();
549 @SuppressWarnings("rawtypes")
551 public void get(ServiceContext ctx, DocumentHandler handler)
552 throws DocumentNotFoundException, DocumentException {
553 throw new UnsupportedOperationException();
556 @SuppressWarnings("rawtypes")
558 public void doWorkflowTransition(ServiceContext ctx, String id,
559 DocumentHandler handler, TransitionDef transitionDef)
560 throws BadRequestException, DocumentNotFoundException,
562 // Do nothing. JPA services do not support workflow.
565 @SuppressWarnings("rawtypes")
567 public void deleteWithWhereClause(ServiceContext ctx, String whereClause,
568 DocumentHandler handler) throws DocumentNotFoundException,
570 throw new UnsupportedOperationException();
573 @SuppressWarnings("rawtypes")
575 public boolean synchronize(ServiceContext ctx, Object specifier,
576 DocumentHandler handler) throws DocumentNotFoundException,
577 TransactionException, DocumentException {
578 // TODO Auto-generated method stub
579 // Do nothing. Subclasses can override if they want/need to.
583 @SuppressWarnings("rawtypes")
585 public boolean synchronizeItem(ServiceContext ctx, AuthorityItemSpecifier itemSpecifier,
586 DocumentHandler handler) throws DocumentNotFoundException,
587 TransactionException, DocumentException {
588 // TODO Auto-generated method stub
589 // Do nothing. Subclasses can override if they want/need to.
594 public void releaseRepositorySession(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx, Object repoSession)
595 throws TransactionException {
596 // TODO Auto-generated method stub