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.common.context.ServiceContext;
42 import org.collectionspace.services.lifecycle.TransitionDef;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * JpaStorageClient is used to perform CRUD operations on SQL storage using JPA.
49 * It uses @see DocumentHandler as IOHandler with the client.
50 * All the operations in this client are carried out under their own transactions.
51 * A call to any method would start and commit/rollback a transaction.
53 * Assumption: each persistent entityReceived has the following 3 attributes
54 <xs:element name="createdAt" type="xs:dateTime">
58 <orm:column name="created_at" nullable="false"/>
63 <xs:element name="updatedAt" type="xs:dateTime">
67 <orm:column name="updated_at" />
73 <xs:attribute name="csid" type="xs:string">
77 <orm:column name="csid" length="128" nullable="false"/>
83 * $LastChangedRevision: $ $LastChangedDate: $
85 public class JpaStorageClientImpl implements StorageClient {
88 private final Logger logger = LoggerFactory.getLogger(JpaStorageClientImpl.class);
91 * Instantiates a new jpa storage client.
93 public JpaStorageClientImpl() {
98 * @see org.collectionspace.services.common.storage.StorageClient#create(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
100 @SuppressWarnings({ "rawtypes", "unchecked" })
102 public String create(ServiceContext ctx,
103 DocumentHandler handler) throws BadRequestException,
105 String result = null;
107 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
109 handler.prepare(Action.CREATE);
110 Object entity = handler.getCommonPart();
111 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entity);
113 jpaConnectionContext.beginTransaction();
115 handler.handle(Action.CREATE, wrapDoc);
116 JaxbUtils.setValue(entity, "setCreatedAtItem", Date.class, new Date());
117 jpaConnectionContext.persist(entity);
118 } catch (EntityExistsException ee) {
120 // 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.
121 // 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.
123 entity = wrapDoc.getWrappedObject(); // the handler should have reset the wrapped transient object with the existing persisted entity we just found.
125 handler.complete(Action.CREATE, wrapDoc);
126 jpaConnectionContext.commitTransaction();
128 result = (String)JaxbUtils.getValue(entity, "getCsid");
129 } catch (BadRequestException bre) {
130 jpaConnectionContext.markForRollback();
132 } catch (DocumentException de) {
133 jpaConnectionContext.markForRollback();
135 } catch (RollbackException rbe) {
136 //jpaConnectionContext.markForRollback();
137 throw DocumentException.createDocumentException(rbe);
138 } catch (Exception e) {
139 jpaConnectionContext.markForRollback();
140 logger.debug("Caught exception ", e);
141 throw DocumentException.createDocumentException(e);
143 ctx.closeConnection();
150 * @see org.collectionspace.services.common.storage.StorageClient#get(org.collectionspace.services.common.context.ServiceContext, java.util.List, org.collectionspace.services.common.document.DocumentHandler)
152 @SuppressWarnings("rawtypes")
154 public void get(ServiceContext ctx, List<String> csidList, DocumentHandler handler)
155 throws DocumentNotFoundException, DocumentException {
156 throw new UnsupportedOperationException();
160 * @see org.collectionspace.services.common.storage.StorageClient#get(org.collectionspace.services.common.context.ServiceContext, java.lang.String, org.collectionspace.services.common.document.DocumentHandler)
162 @SuppressWarnings({ "unchecked", "rawtypes" })
164 public void get(ServiceContext ctx, String id, DocumentHandler handler)
165 throws DocumentNotFoundException, DocumentException {
167 JPATransactionContext jpaTransactionContext = (JPATransactionContext)ctx.openConnection();
169 handler.prepare(Action.GET);
171 o = JpaStorageUtils.getEntity(jpaTransactionContext, getEntityName(ctx), id, ctx.getTenantId());
173 String msg = "Could not find entity with id=" + id;
174 throw new DocumentNotFoundException(msg);
176 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(o);
177 handler.handle(Action.GET, wrapDoc);
178 handler.complete(Action.GET, wrapDoc);
179 } catch (DocumentException de) {
181 } catch (Exception e) {
182 if (logger.isDebugEnabled()) {
183 logger.debug("Caught exception ", e);
185 throw new DocumentException(e);
187 ctx.closeConnection();
192 * @see org.collectionspace.services.common.storage.StorageClient#getAll(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
194 @SuppressWarnings("rawtypes")
196 public void getAll(ServiceContext ctx, DocumentHandler handler)
197 throws DocumentNotFoundException, DocumentException {
198 throw new UnsupportedOperationException("use getFiltered instead");
202 * @see org.collectionspace.services.common.storage.StorageClient#getFiltered(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
204 @SuppressWarnings({ "unchecked", "rawtypes" })
206 public void getFiltered(ServiceContext ctx, DocumentHandler handler)
207 throws DocumentNotFoundException, DocumentException {
209 DocumentFilter docFilter = handler.getDocumentFilter();
210 if (docFilter == null) {
211 docFilter = handler.createDocumentFilter();
214 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
216 handler.prepare(Action.GET_ALL);
217 StringBuilder queryStrBldr = new StringBuilder("SELECT a FROM ");
218 queryStrBldr.append(getEntityName(ctx));
219 queryStrBldr.append(" a");
221 List<DocumentFilter.ParamBinding> params = docFilter.buildWhereForSearch(queryStrBldr);
222 String queryStr = queryStrBldr.toString(); //for debugging
223 Query q = jpaConnectionContext.createQuery(queryStr);
225 for (DocumentFilter.ParamBinding p : params) {
226 q.setParameter(p.getName(), p.getValue());
228 if (docFilter.getOffset() > 0) {
229 q.setFirstResult(docFilter.getOffset());
231 if (docFilter.getPageSize() > 0) {
232 q.setMaxResults(docFilter.getPageSize());
235 jpaConnectionContext.beginTransaction();
236 List list = q.getResultList();
237 long totalItems = getTotalItems(jpaConnectionContext, ctx, handler); // Find out how many items our query would find independent of the paging restrictions
238 docFilter.setTotalItemsResult(totalItems); // Save the items total in the doc filter for later reporting
239 DocumentWrapper<List> wrapDoc = new DocumentWrapperImpl<List>(list);
240 handler.handle(Action.GET_ALL, wrapDoc);
241 handler.complete(Action.GET_ALL, wrapDoc);
242 jpaConnectionContext.commitTransaction();
243 } catch (DocumentException de) {
245 } catch (Exception e) {
246 if (logger.isDebugEnabled()) {
247 logger.debug("Caught exception ", e);
249 throw new DocumentException(e);
251 ctx.closeConnection();
256 * Return the COUNT for a query to find the total number of matches independent of the paging restrictions.
258 @SuppressWarnings("rawtypes")
259 private long getTotalItems(JPATransactionContext jpaTransactionContext, ServiceContext ctx, DocumentHandler handler) {
262 DocumentFilter docFilter = handler.getDocumentFilter();
263 StringBuilder queryStrBldr = new StringBuilder("SELECT COUNT(*) FROM ");
264 queryStrBldr.append(getEntityName(ctx));
265 queryStrBldr.append(" a");
267 List<DocumentFilter.ParamBinding> params = docFilter.buildWhereForSearch(queryStrBldr);
268 String queryStr = queryStrBldr.toString();
269 Query q = jpaTransactionContext.createQuery(queryStr);
271 for (DocumentFilter.ParamBinding p : params) {
272 q.setParameter(p.getName(), p.getValue());
275 result = (long) q.getSingleResult();
281 * @see org.collectionspace.services.common.storage.StorageClient#update(org.collectionspace.services.common.context.ServiceContext, java.lang.String, org.collectionspace.services.common.document.DocumentHandler)
283 @SuppressWarnings({ "rawtypes", "unchecked" })
285 public void update(ServiceContext ctx, String id, DocumentHandler handler)
286 throws BadRequestException, DocumentNotFoundException,
289 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
291 jpaConnectionContext.beginTransaction();
293 handler.prepare(Action.UPDATE);
294 Object entityReceived = handler.getCommonPart();
295 Object entityFound = getEntity(ctx, id, entityReceived.getClass());
296 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entityFound);
297 handler.handle(Action.UPDATE, wrapDoc);
298 JaxbUtils.setValue(entityFound, "setUpdatedAtItem", Date.class, new Date());
299 handler.complete(Action.UPDATE, wrapDoc);
301 jpaConnectionContext.commitTransaction();
302 } catch (BadRequestException bre) {
303 jpaConnectionContext.markForRollback();
305 } catch (DocumentException de) {
306 jpaConnectionContext.markForRollback();
308 } catch (Exception e) {
309 jpaConnectionContext.markForRollback();
310 if (logger.isDebugEnabled()) {
311 logger.debug("Caught exception ", e);
313 throw new DocumentException(e);
315 ctx.closeConnection();
320 * delete removes entity and its child entities
321 * cost: a get before delete
322 * @see org.collectionspace.services.common.storage.StorageClient#delete(org.collectionspace.services.common.context.ServiceContext, java.lang.String)
325 public void delete(@SuppressWarnings("rawtypes") ServiceContext ctx, String id)
326 throws DocumentNotFoundException,
329 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
331 jpaConnectionContext.beginTransaction();
332 Object entityFound = getEntity(ctx, id);
333 if (entityFound == null) {
334 jpaConnectionContext.markForRollback();
335 String msg = "delete(ctx, id): could not find entity with id=" + id;
337 throw new DocumentNotFoundException(msg);
339 jpaConnectionContext.remove(entityFound);
340 jpaConnectionContext.commitTransaction();
341 } catch (DocumentException de) {
342 jpaConnectionContext.markForRollback();
344 } catch (Exception e) {
345 if (logger.isDebugEnabled()) {
346 logger.debug("delete(ctx, id): Caught exception ", e);
348 jpaConnectionContext.markForRollback();
349 throw new DocumentException(e);
351 ctx.closeConnection();
356 * deleteWhere uses the where clause to delete an entityReceived represented by the csidReceived
357 * it does not delete any child entities.
360 * @throws DocumentNotFoundException
361 * @throws DocumentException
363 public void deleteWhere(@SuppressWarnings("rawtypes") ServiceContext ctx, String id)
364 throws DocumentNotFoundException,
367 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
369 StringBuilder deleteStr = new StringBuilder("DELETE FROM ");
370 deleteStr.append(getEntityName(ctx));
371 deleteStr.append(" WHERE csid = :csid and tenantId = :tenantId");
372 //TODO: add tenant csidReceived
374 Query q = jpaConnectionContext.createQuery(deleteStr.toString());
375 q.setParameter("csid", id);
376 q.setParameter("tenantId", ctx.getTenantId());
379 jpaConnectionContext.beginTransaction();
380 rcount = q.executeUpdate();
382 jpaConnectionContext.markForRollback();
383 String msg = "deleteWhere(ctx, id) could not find entity with id=" + id;
385 throw new DocumentNotFoundException(msg);
387 jpaConnectionContext.commitTransaction();
388 } catch (DocumentException de) {
389 jpaConnectionContext.markForRollback();
391 } catch (Exception e) {
392 if (logger.isDebugEnabled()) {
393 logger.debug("deleteWhere(ctx, id) Caught exception ", e);
395 jpaConnectionContext.markForRollback();
396 throw new DocumentException(e);
398 ctx.closeConnection();
403 * delete removes entity and its child entities but calls back to given handler
404 * cost: a get before delete
405 * @see org.collectionspace.services.common.storage.StorageClient#delete(org.collectionspace.services.common.context.ServiceContext, java.lang.String)
407 @SuppressWarnings({ "rawtypes", "unchecked" })
409 public boolean delete(ServiceContext ctx, String id, DocumentHandler handler)
410 throws DocumentNotFoundException, DocumentException {
411 boolean result = true;
413 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
415 jpaConnectionContext.beginTransaction();
416 handler.prepare(Action.DELETE);
417 Object entityFound = getEntity(ctx, id);
418 if (entityFound == null) {
419 jpaConnectionContext.markForRollback();
420 String msg = "delete(ctx, ix, handler) could not find entity with id=" + id;
422 throw new DocumentNotFoundException(msg);
424 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entityFound);
425 handler.handle(Action.DELETE, wrapDoc);
426 jpaConnectionContext.remove(entityFound);
427 handler.complete(Action.DELETE, wrapDoc);
428 jpaConnectionContext.commitTransaction();
429 } catch (DocumentException de) {
430 jpaConnectionContext.markForRollback();
432 } catch (Exception e) {
433 if (logger.isDebugEnabled()) {
434 logger.debug("delete(ctx, ix, handler): Caught exception ", e);
436 jpaConnectionContext.markForRollback();
437 throw new DocumentException(e);
439 ctx.closeConnection();
446 * Gets the entityReceived name.
450 * @return the entityReceived name
452 protected String getEntityName(@SuppressWarnings("rawtypes") ServiceContext ctx) {
453 Object o = ctx.getProperty(ServiceContextProperties.ENTITY_NAME);
455 throw new IllegalArgumentException(ServiceContextProperties.ENTITY_NAME
456 + "property is missing in context "
464 * getEntity returns persistent entity for given id. it assumes that
465 * service context has property ServiceContextProperties.ENTITY_CLASS set
466 * @param ctx service context
467 * @param csid received
469 * @throws DocumentNotFoundException
470 * @throws TransactionException
472 protected Object getEntity(@SuppressWarnings("rawtypes") ServiceContext ctx, String id)
473 throws DocumentNotFoundException, TransactionException {
474 Class<?> entityClazz = (Class<?>) ctx.getProperty(ServiceContextProperties.ENTITY_CLASS);
475 if (entityClazz == null) {
476 String msg = ServiceContextProperties.ENTITY_CLASS + " property is missing in the context";
478 throw new IllegalArgumentException(msg);
481 return getEntity(ctx, id, entityClazz);
485 * getEntity retrieves the persistent entity of given class for given id
486 * rolls back the transaction if not found
488 * @param id entity id
491 * @throws DocumentNotFoundException and rollsback the transaction if active
492 * @throws TransactionException
494 protected Object getEntity(@SuppressWarnings("rawtypes") ServiceContext ctx, String id, Class<?> entityClazz)
495 throws DocumentNotFoundException, TransactionException {
496 Object entityFound = null;
498 JPATransactionContext jpaTransactionConnection = (JPATransactionContext)ctx.openConnection();
500 entityFound = JpaStorageUtils.getEntity(jpaTransactionConnection.getEntityManager(), id, entityClazz);
501 if (entityFound == null) {
502 String msg = "could not find entity of type=" + entityClazz.getName()
505 throw new DocumentNotFoundException(msg);
508 ctx.closeConnection();
514 @SuppressWarnings("rawtypes")
516 public void get(ServiceContext ctx, DocumentHandler handler)
517 throws DocumentNotFoundException, DocumentException {
518 throw new UnsupportedOperationException();
521 @SuppressWarnings("rawtypes")
523 public void doWorkflowTransition(ServiceContext ctx, String id,
524 DocumentHandler handler, TransitionDef transitionDef)
525 throws BadRequestException, DocumentNotFoundException,
527 // Do nothing. JPA services do not support workflow.
530 @SuppressWarnings("rawtypes")
532 public void deleteWithWhereClause(ServiceContext ctx, String whereClause,
533 DocumentHandler handler) throws DocumentNotFoundException,
535 throw new UnsupportedOperationException();
538 @SuppressWarnings("rawtypes")
540 public boolean synchronize(ServiceContext ctx, Object specifier,
541 DocumentHandler handler) throws DocumentNotFoundException,
542 TransactionException, DocumentException {
543 // TODO Auto-generated method stub
544 // Do nothing. Subclasses can override if they want/need to.
548 @SuppressWarnings("rawtypes")
550 public boolean synchronizeItem(ServiceContext ctx, AuthorityItemSpecifier itemSpecifier,
551 DocumentHandler handler) throws DocumentNotFoundException,
552 TransactionException, DocumentException {
553 // TODO Auto-generated method stub
554 // Do nothing. Subclasses can override if they want/need to.