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.api.Tools;
42 import org.collectionspace.services.common.context.ServiceContext;
43 import org.collectionspace.services.lifecycle.TransitionDef;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * JpaStorageClient is used to perform CRUD operations on SQL storage using JPA.
50 * It uses @see DocumentHandler as IOHandler with the client.
51 * All the operations in this client are carried out under their own transactions.
52 * A call to any method would start and commit/rollback a transaction.
54 * Assumption: each persistent entityReceived has the following 3 attributes
55 <xs:element name="createdAt" type="xs:dateTime">
59 <orm:column name="created_at" nullable="false"/>
64 <xs:element name="updatedAt" type="xs:dateTime">
68 <orm:column name="updated_at" />
74 <xs:attribute name="csid" type="xs:string">
78 <orm:column name="csid" length="128" nullable="false"/>
84 * $LastChangedRevision: $ $LastChangedDate: $
86 public class JpaStorageClientImpl implements StorageClient {
89 private final Logger logger = LoggerFactory.getLogger(JpaStorageClientImpl.class);
92 * Instantiates a new jpa storage client.
94 public JpaStorageClientImpl() {
99 * @see org.collectionspace.services.common.storage.StorageClient#create(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
101 @SuppressWarnings({ "rawtypes", "unchecked" })
103 public String create(ServiceContext ctx,
104 DocumentHandler handler) throws BadRequestException,
106 String result = null;
108 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
110 handler.prepare(Action.CREATE);
111 Object entity = handler.getCommonPart();
112 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entity);
114 jpaConnectionContext.beginTransaction();
116 handler.handle(Action.CREATE, wrapDoc);
117 JaxbUtils.setValue(entity, "setCreatedAtItem", Date.class, new Date());
118 jpaConnectionContext.persist(entity);
119 } catch (EntityExistsException ee) {
121 // 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.
122 // 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.
124 entity = wrapDoc.getWrappedObject(); // the handler should have reset the wrapped transient object with the existing persisted entity we just found.
126 handler.complete(Action.CREATE, wrapDoc);
127 jpaConnectionContext.commitTransaction();
129 result = (String)JaxbUtils.getValue(entity, "getCsid");
130 } catch (BadRequestException bre) {
131 jpaConnectionContext.markForRollback();
133 } catch (DocumentException de) {
134 jpaConnectionContext.markForRollback();
136 } catch (RollbackException rbe) {
137 //jpaConnectionContext.markForRollback();
138 throw DocumentException.createDocumentException(rbe);
139 } catch (Exception e) {
140 jpaConnectionContext.markForRollback();
141 logger.debug("Caught exception ", e);
142 throw DocumentException.createDocumentException(e);
144 ctx.closeConnection();
151 * @see org.collectionspace.services.common.storage.StorageClient#get(org.collectionspace.services.common.context.ServiceContext, java.util.List, org.collectionspace.services.common.document.DocumentHandler)
153 @SuppressWarnings("rawtypes")
155 public void get(ServiceContext ctx, List<String> csidList, DocumentHandler handler)
156 throws DocumentNotFoundException, DocumentException {
157 throw new UnsupportedOperationException();
161 * @see org.collectionspace.services.common.storage.StorageClient#get(org.collectionspace.services.common.context.ServiceContext, java.lang.String, org.collectionspace.services.common.document.DocumentHandler)
163 @SuppressWarnings({ "unchecked", "rawtypes" })
165 public void get(ServiceContext ctx, String id, DocumentHandler handler)
166 throws DocumentNotFoundException, DocumentException {
168 JPATransactionContext jpaTransactionContext = (JPATransactionContext)ctx.openConnection();
170 handler.prepare(Action.GET);
172 o = JpaStorageUtils.getEntity(jpaTransactionContext, handler.getDocumentFilter(), getEntityName(ctx), id, ctx.getTenantId());
174 String msg = "Could not find entity with id=" + id;
175 throw new DocumentNotFoundException(msg);
177 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(o);
178 handler.handle(Action.GET, wrapDoc);
179 handler.complete(Action.GET, wrapDoc);
180 } catch (DocumentException de) {
182 } catch (Exception e) {
183 if (logger.isDebugEnabled()) {
184 logger.debug("Caught exception ", e);
186 throw new DocumentException(e);
188 ctx.closeConnection();
193 * @see org.collectionspace.services.common.storage.StorageClient#getAll(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
195 @SuppressWarnings("rawtypes")
197 public void getAll(ServiceContext ctx, DocumentHandler handler)
198 throws DocumentNotFoundException, DocumentException {
199 throw new UnsupportedOperationException("use getFiltered instead");
203 * @see org.collectionspace.services.common.storage.StorageClient#getFiltered(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.document.DocumentHandler)
205 @SuppressWarnings({ "unchecked", "rawtypes" })
207 public void getFiltered(ServiceContext ctx, DocumentHandler handler)
208 throws DocumentNotFoundException, DocumentException {
210 DocumentFilter docFilter = handler.getDocumentFilter();
211 if (docFilter == null) {
212 docFilter = handler.createDocumentFilter();
215 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
217 handler.prepare(Action.GET_ALL);
218 StringBuilder queryStrBldr = new StringBuilder("SELECT a FROM ");
219 queryStrBldr.append(getEntityName(ctx));
220 queryStrBldr.append(" a");
222 String joinFetch = docFilter.getJoinFetchClause();
223 if (Tools.notBlank(joinFetch)) {
224 queryStrBldr.append(" " + joinFetch);
227 List<DocumentFilter.ParamBinding> params = docFilter.buildWhereForSearch(queryStrBldr);
228 String queryStr = queryStrBldr.toString(); //for debugging
229 Query q = jpaConnectionContext.createQuery(queryStr);
231 for (DocumentFilter.ParamBinding p : params) {
232 q.setParameter(p.getName(), p.getValue());
234 if (docFilter.getOffset() > 0) {
235 q.setFirstResult(docFilter.getOffset());
237 if (docFilter.getPageSize() > 0) {
238 q.setMaxResults(docFilter.getPageSize());
241 jpaConnectionContext.beginTransaction();
242 List list = q.getResultList();
243 long totalItems = getTotalItems(jpaConnectionContext, ctx, handler); // Find out how many items our query would find independent of the paging restrictions
244 docFilter.setTotalItemsResult(totalItems); // Save the items total in the doc filter for later reporting
245 DocumentWrapper<List> wrapDoc = new DocumentWrapperImpl<List>(list);
246 handler.handle(Action.GET_ALL, wrapDoc);
247 handler.complete(Action.GET_ALL, wrapDoc);
248 jpaConnectionContext.commitTransaction();
249 } catch (DocumentException de) {
251 } catch (Exception e) {
252 if (logger.isDebugEnabled()) {
253 logger.debug("Caught exception ", e);
255 throw new DocumentException(e);
257 ctx.closeConnection();
262 * Return the COUNT for a query to find the total number of matches independent of the paging restrictions.
264 @SuppressWarnings("rawtypes")
265 private long getTotalItems(JPATransactionContext jpaTransactionContext, ServiceContext ctx, DocumentHandler handler) {
268 DocumentFilter docFilter = handler.getDocumentFilter();
269 StringBuilder queryStrBldr = new StringBuilder("SELECT COUNT(*) FROM ");
270 queryStrBldr.append(getEntityName(ctx));
271 queryStrBldr.append(" a");
273 List<DocumentFilter.ParamBinding> params = docFilter.buildWhereForSearch(queryStrBldr);
274 String queryStr = queryStrBldr.toString();
275 Query q = jpaTransactionContext.createQuery(queryStr);
277 for (DocumentFilter.ParamBinding p : params) {
278 q.setParameter(p.getName(), p.getValue());
281 result = (long) q.getSingleResult();
287 * @see org.collectionspace.services.common.storage.StorageClient#update(org.collectionspace.services.common.context.ServiceContext, java.lang.String, org.collectionspace.services.common.document.DocumentHandler)
289 @SuppressWarnings({ "rawtypes", "unchecked" })
291 public void update(ServiceContext ctx, String id, DocumentHandler handler)
292 throws BadRequestException, DocumentNotFoundException,
295 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
297 jpaConnectionContext.beginTransaction();
299 handler.prepare(Action.UPDATE);
300 Object entityReceived = handler.getCommonPart();
301 Object entityFound = getEntity(ctx, id, entityReceived.getClass());
302 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entityFound);
303 handler.handle(Action.UPDATE, wrapDoc);
304 JaxbUtils.setValue(entityFound, "setUpdatedAtItem", Date.class, new Date());
305 handler.complete(Action.UPDATE, wrapDoc);
307 jpaConnectionContext.commitTransaction();
308 } catch (BadRequestException bre) {
309 jpaConnectionContext.markForRollback();
311 } catch (DocumentException de) {
312 jpaConnectionContext.markForRollback();
314 } catch (Exception e) {
315 jpaConnectionContext.markForRollback();
316 if (logger.isDebugEnabled()) {
317 logger.debug("Caught exception ", e);
319 throw new DocumentException(e);
321 ctx.closeConnection();
326 * delete removes entity and its child entities
327 * cost: a get before delete
328 * @see org.collectionspace.services.common.storage.StorageClient#delete(org.collectionspace.services.common.context.ServiceContext, java.lang.String)
331 public void delete(@SuppressWarnings("rawtypes") ServiceContext ctx, String id)
332 throws DocumentNotFoundException,
335 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
337 jpaConnectionContext.beginTransaction();
338 Object entityFound = getEntity(ctx, id);
339 if (entityFound == null) {
340 jpaConnectionContext.markForRollback();
341 String msg = "delete(ctx, id): could not find entity with id=" + id;
343 throw new DocumentNotFoundException(msg);
345 jpaConnectionContext.remove(entityFound);
346 jpaConnectionContext.commitTransaction();
347 } catch (DocumentException de) {
348 jpaConnectionContext.markForRollback();
350 } catch (Exception e) {
351 if (logger.isDebugEnabled()) {
352 logger.debug("delete(ctx, id): Caught exception ", e);
354 jpaConnectionContext.markForRollback();
355 throw new DocumentException(e);
357 ctx.closeConnection();
362 * deleteWhere uses the where clause to delete an entityReceived represented by the csidReceived
363 * it does not delete any child entities.
366 * @throws DocumentNotFoundException
367 * @throws DocumentException
369 public void deleteWhere(@SuppressWarnings("rawtypes") ServiceContext ctx, String id)
370 throws DocumentNotFoundException,
373 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
375 StringBuilder deleteStr = new StringBuilder("DELETE FROM ");
376 deleteStr.append(getEntityName(ctx));
377 deleteStr.append(" WHERE csid = :csid and tenantId = :tenantId");
378 //TODO: add tenant csidReceived
380 Query q = jpaConnectionContext.createQuery(deleteStr.toString());
381 q.setParameter("csid", id);
382 q.setParameter("tenantId", ctx.getTenantId());
385 jpaConnectionContext.beginTransaction();
386 rcount = q.executeUpdate();
388 jpaConnectionContext.markForRollback();
389 String msg = "deleteWhere(ctx, id) could not find entity with id=" + id;
391 throw new DocumentNotFoundException(msg);
393 jpaConnectionContext.commitTransaction();
394 } catch (DocumentException de) {
395 jpaConnectionContext.markForRollback();
397 } catch (Exception e) {
398 if (logger.isDebugEnabled()) {
399 logger.debug("deleteWhere(ctx, id) Caught exception ", e);
401 jpaConnectionContext.markForRollback();
402 throw new DocumentException(e);
404 ctx.closeConnection();
409 * delete removes entity and its child entities but calls back to given handler
410 * cost: a get before delete
411 * @see org.collectionspace.services.common.storage.StorageClient#delete(org.collectionspace.services.common.context.ServiceContext, java.lang.String)
413 @SuppressWarnings({ "rawtypes", "unchecked" })
415 public boolean delete(ServiceContext ctx, String id, DocumentHandler handler)
416 throws DocumentNotFoundException, DocumentException {
417 boolean result = true;
419 JPATransactionContext jpaConnectionContext = (JPATransactionContext)ctx.openConnection();
421 jpaConnectionContext.beginTransaction();
422 handler.prepare(Action.DELETE);
423 Object entityFound = getEntity(ctx, id);
424 if (entityFound == null) {
425 jpaConnectionContext.markForRollback();
426 String msg = "delete(ctx, ix, handler) could not find entity with id=" + id;
428 throw new DocumentNotFoundException(msg);
430 DocumentWrapper<Object> wrapDoc = new DocumentWrapperImpl<Object>(entityFound);
431 handler.handle(Action.DELETE, wrapDoc);
432 jpaConnectionContext.remove(entityFound);
433 handler.complete(Action.DELETE, wrapDoc);
434 jpaConnectionContext.commitTransaction();
435 } catch (DocumentException de) {
436 jpaConnectionContext.markForRollback();
438 } catch (Exception e) {
439 if (logger.isDebugEnabled()) {
440 logger.debug("delete(ctx, ix, handler): Caught exception ", e);
442 jpaConnectionContext.markForRollback();
443 throw new DocumentException(e);
445 ctx.closeConnection();
452 * Gets the entityReceived name.
456 * @return the entityReceived name
458 protected String getEntityName(@SuppressWarnings("rawtypes") ServiceContext ctx) {
459 Object o = ctx.getProperty(ServiceContextProperties.ENTITY_NAME);
461 throw new IllegalArgumentException(ServiceContextProperties.ENTITY_NAME
462 + "property is missing in context "
470 * getEntity returns persistent entity for given id. it assumes that
471 * service context has property ServiceContextProperties.ENTITY_CLASS set
472 * @param ctx service context
473 * @param csid received
475 * @throws DocumentNotFoundException
476 * @throws TransactionException
478 protected Object getEntity(@SuppressWarnings("rawtypes") ServiceContext ctx, String id)
479 throws DocumentNotFoundException, TransactionException {
480 Class<?> entityClazz = (Class<?>) ctx.getProperty(ServiceContextProperties.ENTITY_CLASS);
481 if (entityClazz == null) {
482 String msg = ServiceContextProperties.ENTITY_CLASS + " property is missing in the context";
484 throw new IllegalArgumentException(msg);
487 return getEntity(ctx, id, entityClazz);
491 * getEntity retrieves the persistent entity of given class for given id
492 * rolls back the transaction if not found
494 * @param id entity id
497 * @throws DocumentNotFoundException and rollsback the transaction if active
498 * @throws TransactionException
500 protected Object getEntity(@SuppressWarnings("rawtypes") ServiceContext ctx, String id, Class<?> entityClazz)
501 throws DocumentNotFoundException, TransactionException {
502 Object entityFound = null;
504 JPATransactionContext jpaTransactionConnection = (JPATransactionContext)ctx.openConnection();
506 entityFound = JpaStorageUtils.getEntity(jpaTransactionConnection.getEntityManager(), id, entityClazz);
507 if (entityFound == null) {
508 String msg = "could not find entity of type=" + entityClazz.getName()
511 throw new DocumentNotFoundException(msg);
514 ctx.closeConnection();
520 @SuppressWarnings("rawtypes")
522 public void get(ServiceContext ctx, DocumentHandler handler)
523 throws DocumentNotFoundException, DocumentException {
524 throw new UnsupportedOperationException();
527 @SuppressWarnings("rawtypes")
529 public void doWorkflowTransition(ServiceContext ctx, String id,
530 DocumentHandler handler, TransitionDef transitionDef)
531 throws BadRequestException, DocumentNotFoundException,
533 // Do nothing. JPA services do not support workflow.
536 @SuppressWarnings("rawtypes")
538 public void deleteWithWhereClause(ServiceContext ctx, String whereClause,
539 DocumentHandler handler) throws DocumentNotFoundException,
541 throw new UnsupportedOperationException();
544 @SuppressWarnings("rawtypes")
546 public boolean synchronize(ServiceContext ctx, Object specifier,
547 DocumentHandler handler) throws DocumentNotFoundException,
548 TransactionException, DocumentException {
549 // TODO Auto-generated method stub
550 // Do nothing. Subclasses can override if they want/need to.
554 @SuppressWarnings("rawtypes")
556 public boolean synchronizeItem(ServiceContext ctx, AuthorityItemSpecifier itemSpecifier,
557 DocumentHandler handler) throws DocumentNotFoundException,
558 TransactionException, DocumentException {
559 // TODO Auto-generated method stub
560 // Do nothing. Subclasses can override if they want/need to.