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.IQueryManager;
35 import org.collectionspace.services.client.IRelationsManager;
36 import org.collectionspace.services.client.PoxPayloadIn;
37 import org.collectionspace.services.client.PoxPayloadOut;
38 import org.collectionspace.services.common.api.GregorianCalendarDateTimeUtils;
39 import org.collectionspace.services.common.api.RefName;
40 import org.collectionspace.services.common.api.RefName.RefNameInterface;
41 import org.collectionspace.services.common.api.RefNameUtils;
42 import org.collectionspace.services.common.api.Tools;
43 import org.collectionspace.services.common.authorityref.AuthorityRefList;
44 import org.collectionspace.services.common.context.ServiceContext;
45 import org.collectionspace.services.common.document.AbstractMultipartDocumentHandlerImpl;
46 import org.collectionspace.services.common.document.DocumentException;
47 import org.collectionspace.services.common.document.DocumentFilter;
48 import org.collectionspace.services.common.document.DocumentWrapper;
49 import org.collectionspace.services.common.document.DocumentWrapperImpl;
50 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
51 import org.collectionspace.services.common.query.QueryContext;
52 import org.collectionspace.services.common.repository.RepositoryClient;
53 import org.collectionspace.services.common.repository.RepositoryClientFactory;
54 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthRefConfigInfo;
55 import org.collectionspace.services.lifecycle.Lifecycle;
56 import org.collectionspace.services.lifecycle.State;
57 import org.collectionspace.services.lifecycle.StateList;
58 import org.collectionspace.services.lifecycle.TransitionDef;
59 import org.collectionspace.services.lifecycle.TransitionDefList;
60 import org.collectionspace.services.lifecycle.TransitionList;
61 import org.nuxeo.ecm.core.NXCore;
62 import org.nuxeo.ecm.core.api.ClientException;
63 import org.nuxeo.ecm.core.api.DocumentModel;
64 import org.nuxeo.ecm.core.api.DocumentModelList;
65 import org.nuxeo.ecm.core.api.model.PropertyException;
66 import org.nuxeo.ecm.core.api.repository.RepositoryInstance;
67 import org.nuxeo.ecm.core.lifecycle.LifeCycleService;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
72 * DocumentModelHandler is a base abstract Nuxeo document handler
73 * using Nuxeo Java Remote APIs for CollectionSpace services
75 * $LastChangedRevision: $
78 public abstract class DocumentModelHandler<T, TL>
79 extends AbstractMultipartDocumentHandlerImpl<T, TL, DocumentModel, DocumentModelList> {
81 private final Logger logger = LoggerFactory.getLogger(DocumentModelHandler.class);
82 private RepositoryInstanceInterface repositorySession;
84 protected String oldRefNameOnUpdate = null; // FIXME: REM - We should have setters and getters for these
85 protected String newRefNameOnUpdate = null; // FIXME: two fields.
88 * Map Nuxeo's life cycle object to our JAX-B based life cycle object
90 private Lifecycle createCollectionSpaceLifecycle(org.nuxeo.ecm.core.lifecycle.LifeCycle nuxeoLifecyle) {
91 Lifecycle result = null;
93 if (nuxeoLifecyle != null) {
95 // Copy the life cycle's name
96 result = new Lifecycle();
97 result.setName(nuxeoLifecyle.getName());
99 // We currently support only one initial state, so take the first one from Nuxeo
100 Collection<String> initialStateNames = nuxeoLifecyle.getInitialStateNames();
101 result.setDefaultInitial(initialStateNames.iterator().next());
103 // Next, we copy the state and corresponding transition lists
104 StateList stateList = new StateList();
105 List<State> states = stateList.getState();
106 Collection<org.nuxeo.ecm.core.lifecycle.LifeCycleState> nuxeoStates = nuxeoLifecyle.getStates();
107 for (org.nuxeo.ecm.core.lifecycle.LifeCycleState nuxeoState : nuxeoStates) {
108 State tempState = new State();
109 tempState.setDescription(nuxeoState.getDescription());
110 tempState.setInitial(nuxeoState.isInitial());
111 tempState.setName(nuxeoState.getName());
112 // Now get the list of transitions
113 TransitionList transitionList = new TransitionList();
114 List<String> transitions = transitionList.getTransition();
115 Collection<String> nuxeoTransitions = nuxeoState.getAllowedStateTransitions();
116 for (String nuxeoTransition : nuxeoTransitions) {
117 transitions.add(nuxeoTransition);
119 tempState.setTransitionList(transitionList);
120 states.add(tempState);
122 result.setStateList(stateList);
124 // Finally, we create the transition definitions
125 TransitionDefList transitionDefList = new TransitionDefList();
126 List<TransitionDef> transitionDefs = transitionDefList.getTransitionDef();
127 Collection<org.nuxeo.ecm.core.lifecycle.LifeCycleTransition> nuxeoTransitionDefs = nuxeoLifecyle.getTransitions();
128 for (org.nuxeo.ecm.core.lifecycle.LifeCycleTransition nuxeoTransitionDef : nuxeoTransitionDefs) {
129 TransitionDef tempTransitionDef = new TransitionDef();
130 tempTransitionDef.setDescription(nuxeoTransitionDef.getDescription());
131 tempTransitionDef.setDestinationState(nuxeoTransitionDef.getDestinationStateName());
132 tempTransitionDef.setName(nuxeoTransitionDef.getName());
133 transitionDefs.add(tempTransitionDef);
135 result.setTransitionDefList(transitionDefList);
142 * Returns the the life cycle definition of the related Nuxeo document type for this handler.
144 * @see org.collectionspace.services.common.document.DocumentHandler#getLifecycle()
147 public Lifecycle getLifecycle() {
148 Lifecycle result = null;
150 String docTypeName = null;
152 docTypeName = this.getServiceContext().getDocumentType();
153 result = getLifecycle(docTypeName);
154 } catch (Exception e) {
155 if (logger.isTraceEnabled() == true) {
156 logger.trace("Could not retrieve lifecycle definition for Nuxeo doctype: " + docTypeName);
164 * Returns the the life cycle definition of the related Nuxeo document type for this handler.
166 * @see org.collectionspace.services.common.document.DocumentHandler#getLifecycle(java.lang.String)
169 public Lifecycle getLifecycle(String docTypeName) {
170 org.nuxeo.ecm.core.lifecycle.LifeCycle nuxeoLifecyle;
171 Lifecycle result = null;
174 LifeCycleService lifeCycleService = null;
176 lifeCycleService = NXCore.getLifeCycleService();
177 } catch (Exception e) {
181 String lifeCycleName;
182 lifeCycleName = lifeCycleService.getLifeCycleNameFor(docTypeName);
183 nuxeoLifecyle = lifeCycleService.getLifeCycleByName(lifeCycleName);
185 result = createCollectionSpaceLifecycle(nuxeoLifecyle);
186 // result = (Lifecycle)FileTools.getJaxbObjectFromFile(Lifecycle.class, "default-lifecycle.xml");
187 } catch (Exception e) {
188 // TODO Auto-generated catch block
189 logger.error("Could not retreive life cycle information for Nuxeo doctype: " + docTypeName, e);
196 * We're using the "name" field of Nuxeo's DocumentModel to store
199 public String getCsid(DocumentModel docModel) {
200 return NuxeoUtils.getCsid(docModel);
203 public String getUri(DocumentModel docModel) {
204 return getServiceContextPath()+getCsid(docModel);
207 public RepositoryClient<PoxPayloadIn, PoxPayloadOut> getRepositoryClient(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) {
208 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repositoryClient = RepositoryClientFactory.getInstance().getClient(ctx.getRepositoryClientName());
209 return repositoryClient;
213 * getRepositorySession returns Nuxeo Repository Session
216 public RepositoryInstanceInterface getRepositorySession() {
218 return repositorySession;
222 * setRepositorySession sets repository session
225 public void setRepositorySession(RepositoryInstanceInterface repoSession) {
226 this.repositorySession = repoSession;
230 public void handleCreate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
231 // TODO for sub-docs - check to see if the current service context is a multipart input,
232 // OR a docfragment, and call a variant to fill the DocModel.
233 fillAllParts(wrapDoc, Action.CREATE);
234 handleCoreValues(wrapDoc, Action.CREATE);
237 // TODO for sub-docs - Add completeCreate in which we look for set-aside doc fragments
238 // and create the subitems. We will create service contexts with the doc fragments
243 public void handleUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
244 // TODO for sub-docs - check to see if the current service context is a multipart input,
245 // OR a docfragment, and call a variant to fill the DocModel.
246 fillAllParts(wrapDoc, Action.UPDATE);
247 handleCoreValues(wrapDoc, Action.UPDATE);
251 public void handleGet(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
252 extractAllParts(wrapDoc);
256 public void handleGetAll(DocumentWrapper<DocumentModelList> wrapDoc) throws Exception {
257 Profiler profiler = new Profiler(this, 2);
259 setCommonPartList(extractCommonPartList(wrapDoc));
264 public abstract void completeUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception;
267 public abstract void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc) throws Exception;
270 public abstract T extractCommonPart(DocumentWrapper<DocumentModel> wrapDoc) throws Exception;
273 public abstract void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception;
276 public abstract void fillCommonPart(T obj, DocumentWrapper<DocumentModel> wrapDoc) throws Exception;
279 public abstract TL extractCommonPartList(DocumentWrapper<DocumentModelList> wrapDoc) throws Exception;
282 public abstract T getCommonPart();
285 public abstract void setCommonPart(T obj);
288 public abstract TL getCommonPartList();
291 public abstract void setCommonPartList(TL obj);
294 public DocumentFilter createDocumentFilter() {
295 DocumentFilter filter = new NuxeoDocumentFilter(this.getServiceContext());
300 * Gets the authority refs.
302 * @param docWrapper the doc wrapper
303 * @param authRefFields the auth ref fields
304 * @return the authority refs
305 * @throws PropertyException the property exception
307 abstract public AuthorityRefList getAuthorityRefs(String csid,
308 List<AuthRefConfigInfo> authRefsInfo) throws PropertyException, Exception;
311 * Subclasses should override this method if they need to customize their refname generation
313 protected RefName.RefNameInterface getRefName(ServiceContext ctx,
314 DocumentModel docModel) {
315 return getRefName(new DocumentWrapperImpl<DocumentModel>(docModel), ctx.getTenantName(), ctx.getServiceName());
319 * By default, we'll use the CSID as the short ID. Sub-classes can override this method if they want to use
320 * something else for a short ID.
323 * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#getRefName(org.collectionspace.services.common.document.DocumentWrapper, java.lang.String, java.lang.String)
326 protected RefName.RefNameInterface getRefName(DocumentWrapper<DocumentModel> docWrapper,
327 String tenantName, String serviceName) {
328 String csid = docWrapper.getWrappedObject().getName();
329 String refnameDisplayName = getRefnameDisplayName(docWrapper);
330 RefName.RefNameInterface refname = RefName.Authority.buildAuthority(tenantName, serviceName,
331 csid, null, refnameDisplayName);
335 private void handleCoreValues(DocumentWrapper<DocumentModel> docWrapper,
336 Action action) throws ClientException {
337 DocumentModel documentModel = docWrapper.getWrappedObject();
338 String now = GregorianCalendarDateTimeUtils.timestampUTC();
339 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
340 String userId = ctx.getUserId();
341 if (action == Action.CREATE) {
343 // Add the tenant ID value to the new entity
345 String tenantId = ctx.getTenantId();
346 documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
347 CollectionSpaceClient.COLLECTIONSPACE_CORE_TENANTID, tenantId);
349 // Add the uri value to the new entity
351 documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
352 CollectionSpaceClient.COLLECTIONSPACE_CORE_URI, getUri(documentModel));
354 // Add the CSID to the DublinCore title so we can see the CSID in the default
358 documentModel.setProperty("dublincore", "title",
359 documentModel.getName());
360 } catch (Exception x) {
361 if (logger.isWarnEnabled() == true) {
362 logger.warn("Could not set the Dublin Core 'title' field on document CSID:" +
363 documentModel.getName());
367 // Add createdAt timestamp and createdBy user
369 documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
370 CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_AT, now);
371 documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
372 CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_BY, userId);
375 if (action == Action.CREATE || action == Action.UPDATE) {
377 // Add/update the resource's refname
379 handleRefNameChanges(ctx, documentModel);
381 // Add updatedAt timestamp and updateBy user
383 documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
384 CollectionSpaceClient.COLLECTIONSPACE_CORE_UPDATED_AT, now);
385 documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
386 CollectionSpaceClient.COLLECTIONSPACE_CORE_UPDATED_BY, userId);
390 protected boolean hasRefNameUpdate() {
391 boolean result = false;
393 if (Tools.notBlank(newRefNameOnUpdate) && Tools.notBlank(oldRefNameOnUpdate)) {
394 if (newRefNameOnUpdate.equalsIgnoreCase(oldRefNameOnUpdate) == false) {
395 result = true; // refNames are different so updates are needed
398 String newDisplayNameOnUpdate = RefNameUtils.getDisplayName(newRefNameOnUpdate);
399 String oldDisplayNameOnUpdate = RefNameUtils.getDisplayName(oldRefNameOnUpdate);
401 if (StringUtils.equals(newDisplayNameOnUpdate, oldDisplayNameOnUpdate) == false) {
402 result = true; // display names are different so updates are needed
410 protected void handleRefNameChanges(ServiceContext ctx, DocumentModel docModel) throws ClientException {
411 // First get the old refName
412 this.oldRefNameOnUpdate = (String)docModel.getProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
413 CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME);
414 // Next, get the new refName
415 RefNameInterface refName = getRefName(ctx, docModel); // Sub-classes may override the getRefName() method called here.
416 if (refName != null) {
417 this.newRefNameOnUpdate = refName.toString();
419 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?
422 // Set the refName if it is an update or if the old refName was empty or null
424 if (hasRefNameUpdate() == true || this.oldRefNameOnUpdate == null) {
425 docModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
426 CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME, this.newRefNameOnUpdate);
431 * If we see the "rtSbj" query param then we need to perform a CMIS query. Currently, we have only one
432 * CMIS query, but we could add more. If we do, this method should look at the incoming request and corresponding
433 * query params to determine if we need to do a CMIS query
435 * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#isCMISQuery()
437 public boolean isCMISQuery() {
438 boolean result = false;
440 MultivaluedMap<String, String> queryParams = getServiceContext().getQueryParams();
442 // Look the query params to see if we need to make a CMSIS query.
444 String asSubjectCsid = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_SUBJECT);
445 String asOjectCsid = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_OBJECT);
446 String asEither = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_EITHER);
447 if (asSubjectCsid != null || asOjectCsid != null || asEither != null) {
455 * Creates the CMIS query from the service context. Each document handler is
456 * responsible for returning (can override) a valid CMIS query using the information in the
457 * current service context -which includes things like the query parameters,
460 * This method implementation supports three mutually exclusive cases. We will build a query
461 * that can find a document(s) 'A' in a relationship with another document
462 * 'B' where document 'B' has a CSID equal to the query param passed in and:
463 * 1. Document 'B' is the subject of the relationship
464 * 2. Document 'B' is the object of the relationship
465 * 3. Document 'B' is either the object or the subject of the relationship
466 * @throws DocumentException
469 public String getCMISQuery(QueryContext queryContext) throws DocumentException {
470 String result = null;
472 if (isCMISQuery() == true) {
474 // Build up the query arguments
476 String theOnClause = "";
477 String theWhereClause = "";
478 MultivaluedMap<String, String> queryParams = getServiceContext().getQueryParams();
479 String asSubjectCsid = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_SUBJECT);
480 String asObjectCsid = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_OBJECT);
481 String asEitherCsid = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_EITHER);
482 String matchObjDocTypes = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_MATCH_OBJ_DOCTYPES);
483 String selectDocType = (String)queryParams.getFirst(IQueryManager.SELECT_DOC_TYPE_FIELD);
485 //String docType = this.getServiceContext().getDocumentType();
486 // If this type in this tenant has been extended, be sure to use that so extension schema is visible.
487 String docType = NuxeoUtils.getTenantQualifiedDocType(this.getServiceContext());
488 if (selectDocType != null && !selectDocType.isEmpty()) {
489 docType = selectDocType;
491 String selectFields = IQueryManager.CMIS_TARGET_CSID + ", "
492 + IQueryManager.CMIS_TARGET_TITLE + ", "
493 + IRelationsManager.CMIS_CSPACE_RELATIONS_TITLE + ", "
494 + IRelationsManager.CMIS_CSPACE_RELATIONS_OBJECT_ID + ", "
495 + IRelationsManager.CMIS_CSPACE_RELATIONS_SUBJECT_ID;
496 String targetTable = docType + " " + IQueryManager.CMIS_TARGET_PREFIX;
497 String relTable = IRelationsManager.DOC_TYPE + " " + IQueryManager.CMIS_RELATIONS_PREFIX;
498 String relObjectCsidCol = IRelationsManager.CMIS_CSPACE_RELATIONS_OBJECT_ID;
499 String relSubjectCsidCol = IRelationsManager.CMIS_CSPACE_RELATIONS_SUBJECT_ID;
500 String targetCsidCol = IQueryManager.CMIS_TARGET_CSID;
503 // Create the "ON" and "WHERE" query clauses based on the params passed into the HTTP request.
505 // First come, first serve -the first match determines the "ON" and "WHERE" query clauses.
507 if (asSubjectCsid != null && !asSubjectCsid.isEmpty()) {
508 // 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.
509 theOnClause = relObjectCsidCol + " = " + targetCsidCol;
510 theWhereClause = relSubjectCsidCol + " = " + "'" + asSubjectCsid + "'";
511 } else if (asObjectCsid != null && !asObjectCsid.isEmpty()) {
512 // 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.
513 theOnClause = relSubjectCsidCol + " = " + targetCsidCol;
514 theWhereClause = relObjectCsidCol + " = " + "'" + asObjectCsid + "'";
515 } else if (asEitherCsid != null && !asEitherCsid.isEmpty()) {
516 theOnClause = relObjectCsidCol + " = " + targetCsidCol
517 + " OR " + relSubjectCsidCol + " = " + targetCsidCol;
518 theWhereClause = relSubjectCsidCol + " = " + "'" + asEitherCsid + "'"
519 + " OR " + relObjectCsidCol + " = " + "'" + asEitherCsid + "'";
521 //Since the call to isCMISQuery() return true, we should never get here.
522 logger.error("Attempt to make CMIS query failed because the HTTP request was missing valid query parameters.");
525 // Now consider a constraint on the object doc types (for search by service group)
526 if (matchObjDocTypes != null && !matchObjDocTypes.isEmpty()) {
527 // 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.
528 theWhereClause += " AND (" + IRelationsManager.CMIS_CSPACE_RELATIONS_OBJECT_TYPE
529 + " IN " + matchObjDocTypes + ")";
532 // This could later be in control of a queryParam, to omit if we want to see versions, or to
533 // only see old versions.
534 theWhereClause += IQueryManager.SEARCH_QUALIFIER_AND + IQueryManager.CMIS_JOIN_NUXEO_IS_VERSION_FILTER;
536 StringBuilder query = new StringBuilder();
537 // assemble the query from the string arguments
538 query.append("SELECT ");
539 query.append(selectFields);
540 query.append(" FROM " + targetTable + " JOIN " + relTable);
541 query.append(" ON " + theOnClause);
542 query.append(" WHERE " + theWhereClause);
545 NuxeoUtils.appendCMISOrderBy(query, queryContext);
546 } catch (Exception e) {
547 logger.error("Could not append ORDER BY clause to CMIS query", e);
551 // SELECT D.cmis:name, D.dc:title, R.dc:title, R.relations_common:subjectCsid
552 // FROM Dimension D JOIN Relation R
553 // ON R.relations_common:objectCsid = D.cmis:name
554 // WHERE R.relations_common:subjectCsid = '737527ec-a560-4776-99de'
555 // ORDER BY D.collectionspace_core:updatedAt DESC
557 result = query.toString();
558 if (logger.isDebugEnabled() == true && result != null) {
559 logger.debug("The CMIS query is: " + result);