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 * Unless required by applicable law or agreed to in writing, software
19 * distributed under the License is distributed on an "AS IS" BASIS,
20 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 * See the License for the specific language governing permissions and
22 * limitations under the License.
24 package org.collectionspace.services.nuxeo.client.java;
26 import java.util.Collection;
27 import java.util.List;
29 import javax.ws.rs.core.MultivaluedMap;
31 import org.apache.commons.lang.StringUtils;
32 import org.collectionspace.services.client.Profiler;
33 import org.collectionspace.services.client.CollectionSpaceClient;
34 import org.collectionspace.services.client.IClientQueryParams;
35 import org.collectionspace.services.client.IQueryManager;
36 import org.collectionspace.services.client.IRelationsManager;
37 import org.collectionspace.services.client.PoxPayloadIn;
38 import org.collectionspace.services.client.PoxPayloadOut;
39 import org.collectionspace.services.common.api.CommonAPI;
40 import org.collectionspace.services.common.api.GregorianCalendarDateTimeUtils;
41 import org.collectionspace.services.common.api.RefName;
42 import org.collectionspace.services.common.api.RefName.RefNameInterface;
43 import org.collectionspace.services.common.api.RefNameUtils;
44 import org.collectionspace.services.common.api.Tools;
45 import org.collectionspace.services.common.authorityref.AuthorityRefList;
46 import org.collectionspace.services.common.context.ServiceContext;
47 import org.collectionspace.services.common.document.AbstractMultipartDocumentHandlerImpl;
48 import org.collectionspace.services.common.document.DocumentException;
49 import org.collectionspace.services.common.document.DocumentFilter;
50 import org.collectionspace.services.common.document.DocumentNotFoundException;
51 import org.collectionspace.services.common.document.DocumentWrapper;
52 import org.collectionspace.services.common.document.DocumentWrapperImpl;
53 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
54 import org.collectionspace.services.common.query.QueryContext;
55 import org.collectionspace.services.common.repository.RepositoryClient;
56 import org.collectionspace.services.common.repository.RepositoryClientFactory;
57 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthRefConfigInfo;
58 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.Specifier;
59 import org.collectionspace.services.lifecycle.Lifecycle;
60 import org.collectionspace.services.lifecycle.State;
61 import org.collectionspace.services.lifecycle.StateList;
62 import org.collectionspace.services.lifecycle.TransitionDef;
63 import org.collectionspace.services.lifecycle.TransitionDefList;
64 import org.collectionspace.services.lifecycle.TransitionList;
65 import org.nuxeo.ecm.core.NXCore;
66 import org.nuxeo.ecm.core.api.ClientException;
67 import org.nuxeo.ecm.core.api.DocumentModel;
68 import org.nuxeo.ecm.core.api.DocumentModelList;
69 import org.nuxeo.ecm.core.api.model.PropertyException;
70 import org.nuxeo.ecm.core.lifecycle.LifeCycle;
71 import org.nuxeo.ecm.core.lifecycle.LifeCycleService;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
76 * DocumentModelHandler is a base abstract Nuxeo document handler
77 * using Nuxeo Java Remote APIs for CollectionSpace services
79 * $LastChangedRevision: $
82 public abstract class DocumentModelHandler<T, TL>
83 extends AbstractMultipartDocumentHandlerImpl<T, TL, DocumentModel, DocumentModelList> {
85 private final Logger logger = LoggerFactory.getLogger(DocumentModelHandler.class);
86 private CoreSessionInterface repositorySession;
88 protected String oldRefNameOnUpdate = null; // FIXME: REM - We should have setters and getters for these
89 protected String newRefNameOnUpdate = null; // FIXME: two fields.
93 * Returns the the life cycle definition of the related Nuxeo document type for this handler.
95 * @see org.collectionspace.services.common.document.DocumentHandler#getLifecycle()
98 public Lifecycle getLifecycle() {
99 Lifecycle result = null;
101 String docTypeName = null;
103 docTypeName = this.getServiceContext().getTenantQualifiedDoctype();
104 result = getLifecycle(docTypeName);
105 if (result == null) {
107 // Get the lifecycle of the generic type if one for the tenant qualified type doesn't exist
109 docTypeName = this.getServiceContext().getDocumentType();
110 result = getLifecycle(docTypeName);
112 } catch (Exception e) {
113 if (logger.isTraceEnabled() == true) {
114 logger.trace("Could not retrieve lifecycle definition for Nuxeo doctype: " + docTypeName);
122 * Returns the the life cycle definition of the related Nuxeo document type for this handler.
124 * @see org.collectionspace.services.common.document.DocumentHandler#getLifecycle(java.lang.String)
127 public Lifecycle getLifecycle(String docTypeName) {
128 return NuxeoUtils.getLifecycle(docTypeName);
132 * We're using the "name" field of Nuxeo's DocumentModel to store
135 public String getCsid(DocumentModel docModel) {
136 return NuxeoUtils.getCsid(docModel);
139 public String getUri(DocumentModel docModel) {
140 return getServiceContextPath()+getCsid(docModel);
143 public String getUri(Specifier specifier) {
144 return getServiceContextPath() + specifier.value;
148 public RepositoryClient<PoxPayloadIn, PoxPayloadOut> getRepositoryClient(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) {
149 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repositoryClient =
150 (RepositoryClient<PoxPayloadIn, PoxPayloadOut>) RepositoryClientFactory.getInstance().getClient(ctx.getRepositoryClientName());
151 return repositoryClient;
155 * getRepositorySession returns Nuxeo Repository Session
158 public CoreSessionInterface getRepositorySession() {
160 return repositorySession;
164 * setRepositorySession sets repository session
167 public void setRepositorySession(CoreSessionInterface repoSession) {
168 this.repositorySession = repoSession;
172 public void handleCreate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
173 // TODO for sub-docs - check to see if the current service context is a multipart input,
174 // OR a docfragment, and call a variant to fill the DocModel.
175 fillAllParts(wrapDoc, Action.CREATE);
176 handleCoreValues(wrapDoc, Action.CREATE);
179 // TODO for sub-docs - Add completeCreate in which we look for set-aside doc fragments
180 // and create the subitems. We will create service contexts with the doc fragments
185 public void handleUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
186 // TODO for sub-docs - check to see if the current service context is a multipart input,
187 // OR a docfragment, and call a variant to fill the DocModel.
188 fillAllParts(wrapDoc, Action.UPDATE);
189 handleCoreValues(wrapDoc, Action.UPDATE);
193 public void handleGet(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
194 extractAllParts(wrapDoc);
198 public void handleGetAll(DocumentWrapper<DocumentModelList> wrapDoc) throws Exception {
199 Profiler profiler = new Profiler(this, 2);
201 setCommonPartList(extractCommonPartList(wrapDoc));
206 public abstract void completeUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception;
209 public abstract void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc) throws Exception;
212 public abstract T extractCommonPart(DocumentWrapper<DocumentModel> wrapDoc) throws Exception;
215 public abstract void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception;
218 public abstract void fillCommonPart(T obj, DocumentWrapper<DocumentModel> wrapDoc) throws Exception;
221 public abstract TL extractCommonPartList(DocumentWrapper<DocumentModelList> wrapDoc) throws Exception;
224 public abstract T getCommonPart();
227 public abstract void setCommonPart(T obj);
230 public abstract TL getCommonPartList();
233 public abstract void setCommonPartList(TL obj);
236 public DocumentFilter createDocumentFilter() {
237 DocumentFilter filter = new NuxeoDocumentFilter(this.getServiceContext());
242 * Gets the authority refs.
244 * @param docWrapper the doc wrapper
245 * @param authRefFields the auth ref fields
246 * @return the authority refs
247 * @throws PropertyException the property exception
249 abstract public AuthorityRefList getAuthorityRefs(String csid,
250 List<AuthRefConfigInfo> authRefConfigInfoList) throws PropertyException, Exception;
253 * Subclasses should override this method if they need to customize their refname generation
255 protected RefName.RefNameInterface getRefName(ServiceContext ctx,
256 DocumentModel docModel) {
257 return getRefName(new DocumentWrapperImpl<DocumentModel>(docModel), ctx.getTenantName(), ctx.getServiceName());
261 * By default, we'll use the CSID as the short ID. Sub-classes can override this method if they want to use
262 * something else for a short ID.
265 * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#getRefName(org.collectionspace.services.common.document.DocumentWrapper, java.lang.String, java.lang.String)
268 protected RefName.RefNameInterface getRefName(DocumentWrapper<DocumentModel> docWrapper,
269 String tenantName, String serviceName) {
270 String csid = docWrapper.getWrappedObject().getName();
271 String refnameDisplayName = getRefnameDisplayName(docWrapper);
272 RefName.RefNameInterface refname = RefName.Authority.buildAuthority(tenantName, serviceName,
273 csid, null, refnameDisplayName);
277 private void handleCoreValues(DocumentWrapper<DocumentModel> docWrapper,
278 Action action) throws ClientException {
279 DocumentModel documentModel = docWrapper.getWrappedObject();
280 String now = GregorianCalendarDateTimeUtils.timestampUTC();
281 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
282 String userId = ctx.getUserId();
283 if (action == Action.CREATE) {
285 // Add the tenant ID value to the new entity
287 String tenantId = ctx.getTenantId();
288 documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
289 CollectionSpaceClient.COLLECTIONSPACE_CORE_TENANTID, tenantId);
291 // Add the uri value to the new entity
293 documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
294 CollectionSpaceClient.COLLECTIONSPACE_CORE_URI, getUri(documentModel));
296 // Add the CSID to the DublinCore title so we can see the CSID in the default
300 documentModel.setProperty(CommonAPI.NUXEO_DUBLINCORE_SCHEMANAME, CommonAPI.NUXEO_DUBLINCORE_TITLE,
301 documentModel.getName());
302 } catch (Exception x) {
303 if (logger.isWarnEnabled() == true) {
304 logger.warn("Could not set the Dublin Core 'title' field on document CSID:" +
305 documentModel.getName());
309 // Add createdAt timestamp and createdBy user
311 documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
312 CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_AT, now);
313 documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
314 CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_BY, userId);
317 if (action == Action.CREATE || action == Action.UPDATE) {
319 // Add/update the resource's refname
321 handleRefNameChanges(ctx, documentModel);
323 // Add updatedAt timestamp and updateBy user
325 if (ctx.shouldUpdateCoreValues() == true) { // Ensure that our caller wants us to record this update
326 documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
327 CollectionSpaceClient.COLLECTIONSPACE_CORE_UPDATED_AT, now);
328 documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
329 CollectionSpaceClient.COLLECTIONSPACE_CORE_UPDATED_BY, userId);
331 logger.debug(String.format("Document with CSID=%s updated %s by user %s", documentModel.getName(), now, userId));
336 protected boolean hasRefNameUpdate() {
337 boolean result = false;
340 // Check to see if the request contains a query parameter asking us to force a refname update
342 if (getServiceContext().shouldForceUpdateRefnameReferences() == true) {
346 if (Tools.notBlank(newRefNameOnUpdate) && Tools.notBlank(oldRefNameOnUpdate)) {
347 // CSPACE-6372: refNames are different if:
348 // - any part of the refName is different, using a case insensitive comparison, or
349 // - the display name portions are different, using a case sensitive comparison
350 if (newRefNameOnUpdate.equalsIgnoreCase(oldRefNameOnUpdate) == false) {
351 result = true; // refNames are different so updates are needed
354 String newDisplayNameOnUpdate = RefNameUtils.getDisplayName(newRefNameOnUpdate);
355 String oldDisplayNameOnUpdate = RefNameUtils.getDisplayName(oldRefNameOnUpdate);
357 if (StringUtils.equals(newDisplayNameOnUpdate, oldDisplayNameOnUpdate) == false) {
358 result = true; // display names are different so updates are needed
366 protected void handleRefNameChanges(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx, DocumentModel docModel) throws ClientException {
367 // First get the old refName
368 this.oldRefNameOnUpdate = (String)docModel.getProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
369 CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME);
370 // Next, get the new refName
371 RefNameInterface refName = getRefName(ctx, docModel); // Sub-classes may override the getRefName() method called here.
372 if (refName != null) {
373 this.newRefNameOnUpdate = refName.toString();
375 logger.error(String.format("The refName for document is missing. Document CSID=%s", docModel.getName())); // FIXME: REM - We should probably be throwing an exception here?
378 // Set the refName if it is an update or if the old refName was empty or null
380 if (hasRefNameUpdate() == true || this.oldRefNameOnUpdate == null) {
381 docModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
382 CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME, this.newRefNameOnUpdate);
387 * If we see the "rtSbj" query param then we need to perform a CMIS query. Currently, we have only one
388 * CMIS query, but we could add more. If we do, this method should look at the incoming request and corresponding
389 * query params to determine if we need to do a CMIS query
391 * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#isCMISQuery()
393 public boolean isCMISQuery() {
394 boolean result = false;
396 MultivaluedMap<String, String> queryParams = getServiceContext().getQueryParams();
398 // Look the query params to see if we need to make a CMSIS query.
400 String asSubjectCsid = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_SUBJECT);
401 String asOjectCsid = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_OBJECT);
402 String asEither = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_EITHER);
403 if (asSubjectCsid != null || asOjectCsid != null || asEither != null) {
411 public String getDocumentsToIndexQuery(String indexId, String csid) throws DocumentException, Exception {
412 String result = null;
414 ServiceContext<PoxPayloadIn,PoxPayloadOut> ctx = this.getServiceContext();
415 String selectClause = "SELECT ecm:uuid, ecm:primaryType FROM ";
416 String docFilterWhereClause = this.getDocumentFilter().getWhereClause();
418 // The where clause could be a combination of the document filter's where clause plus a CSID qualifier
420 String whereClause = (csid == null) ? null : String.format("ecm:name = '%s'", csid); // AND ecm:currentLifeCycleState <> 'deleted'"
421 if (whereClause != null && !whereClause.trim().isEmpty()) {
422 // Due to an apparent bug/issue in how Nuxeo translates the NXQL query string
423 // into SQL, we need to parenthesize our 'where' clause
424 if (docFilterWhereClause != null && !docFilterWhereClause.trim().isEmpty()) {
425 whereClause = whereClause + IQueryManager.SEARCH_QUALIFIER_AND + "(" + docFilterWhereClause + ")";
428 whereClause = docFilterWhereClause;
430 String orderByClause = "ecm:uuid";
433 QueryContext queryContext = new QueryContext(ctx, selectClause, whereClause, orderByClause);
434 result = NuxeoUtils.buildNXQLQuery(queryContext);
435 } catch (DocumentException de) {
437 } catch (Exception x) {
445 * Creates the CMIS query from the service context. Each document handler is
446 * responsible for returning (can override) a valid CMIS query using the information in the
447 * current service context -which includes things like the query parameters,
450 * This method implementation supports three mutually exclusive cases. We will build a query
451 * that can find a document(s) 'A' in a relationship with another document
452 * 'B' where document 'B' has a CSID equal to the query param passed in and:
453 * 1. Document 'B' is the subject of the relationship
454 * 2. Document 'B' is the object of the relationship
455 * 3. Document 'B' is either the object or the subject of the relationship
456 * @throws DocumentException
459 public String getCMISQuery(QueryContext queryContext) throws DocumentException {
460 String result = null;
462 if (isCMISQuery() == true) {
464 // Build up the query arguments
466 String theOnClause = "";
467 String theWhereClause = "";
468 MultivaluedMap<String, String> queryParams = getServiceContext().getQueryParams();
469 String asSubjectCsid = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_SUBJECT);
470 String asObjectCsid = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_OBJECT);
472 String matchObjDocTypes = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_MATCH_OBJ_DOCTYPES);
473 String selectDocType = (String)queryParams.getFirst(IQueryManager.SELECT_DOC_TYPE_FIELD);
475 String docType = NuxeoUtils.getTenantQualifiedDocType(this.getServiceContext()); // Fixed for https://issues.collectionspace.org/browse/DRYD-302
476 if (selectDocType != null && !selectDocType.isEmpty()) {
477 docType = selectDocType;
479 String selectFields = IQueryManager.CMIS_TARGET_CSID + ", "
480 + IQueryManager.CMIS_TARGET_TITLE + ", "
481 + IRelationsManager.CMIS_CSPACE_RELATIONS_TITLE + ", "
482 + IRelationsManager.CMIS_CSPACE_RELATIONS_OBJECT_ID + ", "
483 + IRelationsManager.CMIS_CSPACE_RELATIONS_SUBJECT_ID;
485 String targetTable = docType + " " + IQueryManager.CMIS_TARGET_PREFIX;
486 String relTable = IRelationsManager.DOC_TYPE + " " + IQueryManager.CMIS_RELATIONS_PREFIX;
488 String relSubjectCsidCol = IRelationsManager.CMIS_CSPACE_RELATIONS_SUBJECT_ID;
489 String relObjectCsidCol = IRelationsManager.CMIS_CSPACE_RELATIONS_OBJECT_ID;
491 String targetCsidCol = IQueryManager.CMIS_TARGET_CSID;
492 String tenantID = this.getServiceContext().getTenantId();
495 // Create the "ON" and "WHERE" query clauses based on the params passed into the HTTP request.
497 // First come, first serve -the first match determines the "ON" and "WHERE" query clauses.
499 if (asSubjectCsid != null && !asSubjectCsid.isEmpty()) {
500 // Since our query param is the "subject" value, join the tables where the CSID of the document is the other side (the "object") of the relationship.
501 theOnClause = relObjectCsidCol + " = " + targetCsidCol;
502 theWhereClause = relSubjectCsidCol + " = " + "'" + asSubjectCsid + "'";
503 } else if (asObjectCsid != null && !asObjectCsid.isEmpty()) {
504 // Since our query param is the "object" value, join the tables where the CSID of the document is the other side (the "subject") of the relationship.
505 theOnClause = relSubjectCsidCol + " = " + targetCsidCol;
506 theWhereClause = relObjectCsidCol + " = " + "'" + asObjectCsid + "'";
508 //Since the call to isCMISQuery() return true, we should never get here.
509 logger.error("Attempt to make CMIS query failed because the HTTP request was missing valid query parameters.");
512 // Now consider a constraint on the object doc types (for search by service group)
513 if (matchObjDocTypes != null && !matchObjDocTypes.isEmpty()) {
514 // Since our query param is the "subject" value, join the tables where the CSID of the document is the other side (the "object") of the relationship.
515 theWhereClause += " AND (" + IRelationsManager.CMIS_CSPACE_RELATIONS_OBJECT_TYPE
516 + " IN " + matchObjDocTypes + ")";
519 // Qualify the search for predicate types
520 theWhereClause = addWhereClauseForPredicates(theWhereClause, queryParams);
522 // Qualify the query with the current tenant ID.
523 theWhereClause += IQueryManager.SEARCH_QUALIFIER_AND + IQueryManager.CMIS_JOIN_TENANT_ID_FILTER + " = '" + tenantID + "'";
525 // This could later be in control of a queryParam, to omit if we want to see versions, or to
526 // only see old versions.
527 theWhereClause += IQueryManager.SEARCH_QUALIFIER_AND + IQueryManager.CMIS_JOIN_NUXEO_IS_VERSION_FILTER;
529 StringBuilder query = new StringBuilder();
530 // assemble the query from the string arguments
531 query.append("SELECT ");
532 query.append(selectFields);
533 query.append(" FROM " + targetTable + " JOIN " + relTable);
534 query.append(" ON " + theOnClause);
535 query.append(" WHERE " + theWhereClause);
538 NuxeoUtils.appendCMISOrderBy(query, queryContext);
539 } catch (Exception e) {
540 logger.error("Could not append ORDER BY clause to CMIS query", e);
544 // SELECT D.cmis:name, D.dc:title, R.dc:title, R.relations_common:subjectCsid
545 // FROM Dimension D JOIN Relation R
546 // ON R.relations_common:objectCsid = D.cmis:name
547 // WHERE R.relations_common:subjectCsid = '737527ec-a560-4776-99de'
548 // ORDER BY D.collectionspace_core:updatedAt DESC
550 result = query.toString();
551 if (logger.isDebugEnabled() == true && result != null) {
552 logger.debug("The CMIS query is: " + result);
559 private String addWhereClauseForPredicates(String theWhereClause, MultivaluedMap<String, String> queryParams) {
560 if (queryParams.containsKey(IQueryManager.SEARCH_RELATED_PREDICATE)) {
561 List<String> predicateList = queryParams.get(IQueryManager.SEARCH_RELATED_PREDICATE);
563 if (predicateList.size() == 1) {
564 String predicate = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_PREDICATE);
565 if (predicate != null && !predicate.trim().isEmpty()) {
566 theWhereClause += IQueryManager.SEARCH_QUALIFIER_AND + IRelationsManager.CMIS_CSPACE_RELATIONS_PREDICATE + " = '" + predicate + "'";
568 } else if (predicateList.size() > 1) {
569 StringBuffer partialClause = new StringBuffer();
570 for (String predicate : predicateList) {
571 if (!predicate.trim().isEmpty()) {
572 partialClause.append("'" + predicate + "', ");
575 String inValues = partialClause.toString().replaceAll(", $", ""); // remove the last ', ' squence
576 if (!inValues.trim().isEmpty()) {
577 theWhereClause += IQueryManager.SEARCH_QUALIFIER_AND + IRelationsManager.CMIS_CSPACE_RELATIONS_PREDICATE + " IN (" + inValues + ")";
582 return theWhereClause;