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.relation.nuxeo;
26 import java.util.HashMap;
27 import java.util.Iterator;
28 import java.net.HttpURLConnection;
30 import org.collectionspace.services.client.PoxPayloadIn;
31 import org.collectionspace.services.client.PoxPayloadOut;
32 import org.collectionspace.services.client.RelationClient;
33 import org.collectionspace.services.common.NuxeoBasedResource;
34 import org.collectionspace.services.common.ServiceException;
35 import org.collectionspace.services.common.ServiceMain;
36 import org.collectionspace.services.common.api.Tools;
37 import org.collectionspace.services.common.config.TenantBindingConfigReaderImpl;
38 import org.collectionspace.services.common.context.ServiceBindingUtils;
39 import org.collectionspace.services.common.document.DocumentNotFoundException;
40 import org.collectionspace.services.common.document.DocumentWrapper;
41 import org.collectionspace.services.common.document.InvalidDocumentException;
42 import org.collectionspace.services.common.relation.RelationJAXBSchema;
43 import org.collectionspace.services.common.relation.nuxeo.RelationConstants;
44 import org.collectionspace.services.common.context.ServiceContext;
45 import org.collectionspace.services.lifecycle.TransitionDef;
46 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
47 import org.collectionspace.services.relation.RelationsCommon;
48 import org.collectionspace.services.relation.RelationsCommonList;
49 import org.collectionspace.services.relation.RelationsCommonList.RelationListItem;
50 import org.collectionspace.services.relation.RelationsDocListItem;
51 import org.collectionspace.services.client.CollectionSpaceClient;
52 import org.collectionspace.services.client.workflow.WorkflowClient;
53 import org.collectionspace.services.config.service.ServiceBindingType;
54 import org.collectionspace.services.nuxeo.client.java.RemoteDocumentModelHandlerImpl;
55 import org.collectionspace.services.nuxeo.client.java.CoreSessionInterface;
56 import org.collectionspace.services.nuxeo.client.java.NuxeoRepositoryClientImpl;
58 import org.nuxeo.ecm.core.api.ClientException;
59 import org.nuxeo.ecm.core.api.DocumentModel;
60 import org.nuxeo.ecm.core.api.DocumentModelList;
61 import org.nuxeo.ecm.core.api.model.PropertyException;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
67 * RelationDocumentModelHandler
69 * $LastChangedRevision: $
72 public class RelationDocumentModelHandler
73 extends RemoteDocumentModelHandlerImpl<RelationsCommon, RelationsCommonList> {
75 private final Logger logger = LoggerFactory.getLogger(RelationDocumentModelHandler.class);
77 * relation is used to stash JAXB object to use when handle is called
78 * for Action.CREATE, Action.UPDATE or Action.GET
80 private RelationsCommon relation;
82 * relationList is stashed when handle is called
85 private RelationsCommonList relationList;
87 private static final String ERROR_TERMS_IN_WORKFLOWSTATE = "Cannot modify a relationship if either end is in the workflow state: ";
90 * Will return 'true' if either the subject's or object's current workflow state *contain* the passed in workflow
94 * - will return 'true' if the subject's workflow state is "replicated_deleted" and the passed in workflow state is "replicated" or "deleted".
95 * - will return 'true' if the subject's or object's workflow state is "locked" and the passed in workflow state is "locked"
97 private boolean subjectOrObjectInWorkflowState(DocumentWrapper<DocumentModel> wrapDoc, String workflowState) throws ServiceException {
98 boolean result = false;
99 DocumentModel relationDocModel = wrapDoc.getWrappedObject();
100 String errMsg = ERROR_TERMS_IN_WORKFLOWSTATE + workflowState;
102 CoreSessionInterface repoSession = this.getRepositorySession();
104 DocumentModel subjectDocModel = getSubjectOrObjectDocModel(repoSession, relationDocModel, SUBJ_DOC_MODEL);
105 DocumentModel objectDocModel = getSubjectOrObjectDocModel(repoSession, relationDocModel, OBJ_DOC_MODEL);
106 if (subjectDocModel.getCurrentLifeCycleState().contains(workflowState) ||
107 objectDocModel.getCurrentLifeCycleState().contains(workflowState)) {
110 } catch (Exception e) {
111 if (logger.isInfoEnabled() == true) {
112 logger.info(errMsg, e);
121 * Until we rework the RepositoryClient to handle the workflow transition (just like it does for 'create', 'get', 'update', and 'delete'), this method will only check to see
122 * if the transition is allowed. Until then, the WorkflowDocumentModelHandler class does the actual workflow transition.
124 * @see org.collectionspace.services.nuxeo.client.java.RemoteDocumentModelHandlerImpl#handleWorkflowTransition(org.collectionspace.services.common.document.DocumentWrapper, org.collectionspace.services.lifecycle.TransitionDef)
126 public void handleWorkflowTransition(ServiceContext ctx, DocumentWrapper<DocumentModel> wrapDoc,
127 TransitionDef transitionDef) throws Exception {
128 if (subjectOrObjectInWorkflowState(wrapDoc, WorkflowClient.WORKFLOWSTATE_LOCKED) == true) {
129 throw new ServiceException(HttpURLConnection.HTTP_FORBIDDEN,
130 "Cannot change a relationship if either end of it is in the workflow state: "
131 + WorkflowClient.WORKFLOWSTATE_LOCKED);
134 // Toggle the 'active' flag of the relationship record -needed to correctly apply a uniqueness constrain on rows in the relations_common table
136 String transitionName = transitionDef.getName();
137 if (transitionName.equalsIgnoreCase(WorkflowClient.WORKFLOWTRANSITION_UNDELETE)) {
138 DocumentModel doc = wrapDoc.getWrappedObject();
139 doc.setProperty(RelationClient.SERVICE_COMMONPART_NAME, RelationJAXBSchema.RELATIONSHIP_ACTIVE, Boolean.TRUE);
140 } else if (transitionName.equalsIgnoreCase(WorkflowClient.WORKFLOWTRANSITION_DELETE)) {
141 DocumentModel doc = wrapDoc.getWrappedObject();
142 doc.setProperty(RelationClient.SERVICE_COMMONPART_NAME, RelationJAXBSchema.RELATIONSHIP_ACTIVE, Boolean.FALSE);
149 public void handleCreate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
150 // Merge in the data from the payload
151 super.handleCreate(wrapDoc);
153 // And take care of ensuring all the values for the relation info are correct
154 populateSubjectAndObjectValues(wrapDoc);
156 // We can't create a relationship record if either the subject or the object is in a locked workflow state
157 String workflowState = WorkflowClient.WORKFLOWSTATE_LOCKED;
158 if (subjectOrObjectInWorkflowState(wrapDoc, workflowState) == true) {
159 throw new ServiceException(HttpURLConnection.HTTP_FORBIDDEN,
160 "Cannot create a relationship if either end is in the workflow state: " + workflowState);
165 public void handleUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
166 // Merge in the data from the payload
167 super.handleUpdate(wrapDoc);
169 // And take care of ensuring all the values for the relation info are correct
170 populateSubjectAndObjectValues(wrapDoc);
174 public boolean handleDelete(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
175 boolean result = true;
177 String workflowState = WorkflowClient.WORKFLOWSTATE_LOCKED;
178 // Neither the subject nor the object can be locked
179 if (subjectOrObjectInWorkflowState(wrapDoc, workflowState) == false) {
180 super.handleDelete(wrapDoc);
182 throw new ServiceException(HttpURLConnection.HTTP_FORBIDDEN,
183 "Cannot delete a relationship if either end is in the workflow state: " + workflowState);
189 private void populateSubjectAndObjectValues(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
190 // Obtain document models for the subject and object of the relation, so that
191 // we ensure we have value docType, URI info. If the docModels support refNames,
192 // we will also set those.
193 // Note that this introduces another caching problem...
194 DocumentModel relationDocModel = wrapDoc.getWrappedObject();
195 CoreSessionInterface repoSession = this.getRepositorySession();
197 DocumentModel subjectDocModel = getSubjectOrObjectDocModel(repoSession, relationDocModel, SUBJ_DOC_MODEL);
198 DocumentModel objectDocModel = getSubjectOrObjectDocModel(repoSession, relationDocModel, OBJ_DOC_MODEL);
200 // Use values from the subject and object document models to populate the
201 // relevant fields of the relation's own document model.
202 if (subjectDocModel != null) {
203 populateSubjectOrObjectValues(relationDocModel, subjectDocModel, SUBJ_DOC_MODEL);
205 if (objectDocModel != null) {
206 populateSubjectOrObjectValues(relationDocModel, objectDocModel, OBJ_DOC_MODEL);
211 public RelationsCommon getCommonPart() {
216 public void setCommonPart(RelationsCommon theRelation) {
217 this.relation = theRelation;
220 /**get associated Relation (for index/GET_ALL)
223 public RelationsCommonList getCommonPartList() {
228 public void setCommonPartList(RelationsCommonList theRelationList) {
229 this.relationList = theRelationList;
233 public RelationsCommon extractCommonPart(DocumentWrapper<DocumentModel> wrapDoc)
235 throw new UnsupportedOperationException();
239 public void fillCommonPart(RelationsCommon theRelation, DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
240 throw new UnsupportedOperationException();
244 public RelationsCommonList extractCommonPartList(DocumentWrapper<DocumentModelList> wrapDoc) throws Exception {
245 RelationsCommonList relList = this.extractPagingInfo(new RelationsCommonList(), wrapDoc);
246 relList.setFieldsReturned("subjectCsid|relationshipType|predicateDisplayName|relationshipMetaType|objectCsid|uri|csid|subject|object");
247 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
248 String serviceContextPath = getServiceContextPath();
250 TenantBindingConfigReaderImpl tReader = ServiceMain.getInstance().getTenantBindingConfigReader();
251 String serviceName = getServiceContext().getServiceName().toLowerCase();
252 ServiceBindingType sbt = tReader.getServiceBinding(ctx.getTenantId(), serviceName);
254 Iterator<DocumentModel> iter = wrapDoc.getWrappedObject().iterator();
255 while (iter.hasNext()) {
256 DocumentModel docModel = iter.next();
257 RelationListItem relListItem = getRelationListItem(ctx, sbt, tReader, docModel, serviceContextPath);
258 relList.getRelationListItem().add(relListItem);
263 /** Gets the relation list item, looking up the subject and object documents, and getting summary
264 * info via the objectName and objectNumber properties in tenant-bindings.
266 * @param sbt the ServiceBindingType of Relations service
267 * @param tReader the tenant-bindings reader, for looking up docnumber and docname
268 * @param docModel the doc model
269 * @param serviceContextPath the service context path
270 * @return the relation list item, with nested subject and object summary info.
271 * @throws Exception the exception
273 private RelationListItem getRelationListItem(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
274 ServiceBindingType sbt,
275 TenantBindingConfigReaderImpl tReader,
276 DocumentModel docModel,
277 String serviceContextPath) throws Exception {
278 RelationListItem relationListItem = new RelationListItem();
279 String id = getCsid(docModel);
280 relationListItem.setCsid(id);
282 relationListItem.setSubjectCsid((String) docModel.getProperty(ctx.getCommonPartLabel(),
283 RelationJAXBSchema.SUBJECT_CSID));
285 String predicate = (String) docModel.getProperty(ctx.getCommonPartLabel(),
286 RelationJAXBSchema.RELATIONSHIP_TYPE);
287 relationListItem.setRelationshipType(predicate);
288 relationListItem.setPredicate(predicate); //predicate is new name for relationshipType.
289 relationListItem.setPredicateDisplayName((String) docModel.getProperty(ctx.getCommonPartLabel(),
290 RelationJAXBSchema.RELATIONSHIP_TYPE_DISPLAYNAME));
292 relationListItem.setRelationshipMetaType((String) docModel.getProperty(ctx.getCommonPartLabel(),
293 RelationJAXBSchema.RELATIONSHIP_META_TYPE));
294 relationListItem.setObjectCsid((String) docModel.getProperty(ctx.getCommonPartLabel(),
295 RelationJAXBSchema.OBJECT_CSID));
297 relationListItem.setUri(serviceContextPath + id);
299 //Now fill in summary info for the related docs: subject and object.
300 String subjectCsid = relationListItem.getSubjectCsid();
301 String subjectDocumentType = (String) docModel.getProperty(ctx.getCommonPartLabel(),
302 RelationJAXBSchema.SUBJECT_DOCTYPE);
303 RelationsDocListItem subject = createRelationsDocListItem(ctx, sbt, subjectCsid, tReader, subjectDocumentType);
305 String subjectUri = (String) docModel.getProperty(ctx.getCommonPartLabel(),
306 RelationJAXBSchema.SUBJECT_URI);
307 subject.setUri(subjectUri);
308 String subjectRefName = (String) docModel.getProperty(ctx.getCommonPartLabel(),
309 RelationJAXBSchema.SUBJECT_REFNAME);
310 subject.setRefName(subjectRefName);
311 relationListItem.setSubject(subject);
313 String objectCsid = relationListItem.getObjectCsid();
314 String objectDocumentType = (String) docModel.getProperty(ctx.getCommonPartLabel(),
315 RelationJAXBSchema.OBJECT_DOCTYPE);
316 RelationsDocListItem object = createRelationsDocListItem(ctx, sbt, objectCsid, tReader, objectDocumentType);
318 String objectUri = (String) docModel.getProperty(ctx.getCommonPartLabel(),
319 RelationJAXBSchema.OBJECT_URI);
320 object.setUri(objectUri);
321 String objectRefName = (String) docModel.getProperty(ctx.getCommonPartLabel(),
322 RelationJAXBSchema.OBJECT_REFNAME);
323 object.setRefName(objectRefName);
324 relationListItem.setObject(object);
326 return relationListItem;
329 // DocumentModel itemDocModel = docModelFromCSID(ctx, itemCsid);
330 protected RelationsDocListItem createRelationsDocListItem(
331 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
332 ServiceBindingType sbt,
334 TenantBindingConfigReaderImpl tReader,
335 String documentType) throws Exception {
336 RelationsDocListItem item = new RelationsDocListItem();
337 item.setDocumentType(documentType);//this one comes from the record, as subjectDocumentType, objectDocumentType.
338 item.setCsid(itemCsid);
340 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(ctx, this.getRepositorySession(), itemCsid); //null if not found.
341 if (itemDocModel != null) {
342 String itemDocType = itemDocModel.getDocumentType().getName();
343 itemDocType = ServiceBindingUtils.getUnqualifiedTenantDocType(itemDocType);
344 if (Tools.isBlank(documentType)) {
345 item.setDocumentType(itemDocType);
348 //TODO: ensure that itemDocType is really the entry point, i.e. servicename==doctype
349 //ServiceBindingType itemSbt2 = tReader.getServiceBinding(ctx.getTenantId(), itemDocType);
350 String propName = "ERROR-FINDING-PROP-VALUE";
351 ServiceBindingType itemSbt = tReader.getServiceBindingForDocType(ctx.getTenantId(), itemDocType);
353 propName = ServiceBindingUtils.getPropertyValue(itemSbt, ServiceBindingUtils.OBJ_NAME_PROP);
354 String itemDocname = ServiceBindingUtils.getMappedFieldInDoc(itemSbt, ServiceBindingUtils.OBJ_NAME_PROP, itemDocModel);
355 if (propName == null || itemDocname == null) {
357 item.setName(itemDocname);
359 } catch (Throwable t) {
360 logger.error("====Error finding objectNameProperty: " + itemDocModel + " field " + ServiceBindingUtils.OBJ_NAME_PROP + "=" + propName
361 + " not found in itemDocType: " + itemDocType + " inner: " + t.getMessage());
363 propName = "ERROR-FINDING-PROP-VALUE";
365 propName = ServiceBindingUtils.getPropertyValue(itemSbt, ServiceBindingUtils.OBJ_NUMBER_PROP);
366 String itemDocnumber = ServiceBindingUtils.getMappedFieldInDoc(itemSbt, ServiceBindingUtils.OBJ_NUMBER_PROP, itemDocModel);
368 if (propName == null || itemDocnumber == null) {
370 item.setNumber(itemDocnumber);
372 } catch (Throwable t) {
373 logger.error("====Error finding objectNumberProperty: " + ServiceBindingUtils.OBJ_NUMBER_PROP + "=" + propName
374 + " not found in itemDocType: " + itemDocType + " inner: " + t.getMessage());
377 item.setError("INVALID: related object is absent");
378 // Laramie20110510 CSPACE-3739 throw the exception for 3739, otherwise, don't throw it.
379 //throw new Exception("INVALID: related object is absent "+itemCsid);
385 public String getQProperty(String prop) {
386 return "/" + RelationConstants.NUXEO_SCHEMA_ROOT_ELEMENT + "/" + prop;
389 private final boolean SUBJ_DOC_MODEL = true;
390 private final boolean OBJ_DOC_MODEL = false;
392 private DocumentModel getSubjectOrObjectDocModel(
393 CoreSessionInterface repoSession,
394 DocumentModel relationDocModel,
395 boolean fSubject) throws Exception {
396 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
398 // Get the document model for the object of the relation.
399 String commonPartLabel = ctx.getCommonPartLabel();
402 DocumentModel docModel = null;
403 // FIXME: Currently assumes that the object CSID is valid if present
404 // in the incoming payload.
406 csid = (String) relationDocModel.getProperty(commonPartLabel,
407 (fSubject?RelationJAXBSchema.SUBJECT_CSID:RelationJAXBSchema.OBJECT_CSID));
408 } catch (PropertyException pe) {
409 // Per CSPACE-4468, ignore any property exception here.
410 // The objectCsid and/or subjectCsid field in a relation record
411 // can now be null (missing), because a refName value can be
412 // provided as an alternate identifier.
414 if (Tools.notBlank(csid)) {
415 NuxeoRepositoryClientImpl nuxeoRepoClient = (NuxeoRepositoryClientImpl)getRepositoryClient(ctx);
416 DocumentWrapper<DocumentModel> docWrapper = nuxeoRepoClient.getDocFromCsid(ctx, repoSession, csid);
417 docModel = docWrapper.getWrappedObject();
418 } else { // if (Tools.isBlank(objectCsid)) {
420 refName = (String) relationDocModel.getProperty(commonPartLabel,
421 (fSubject?RelationJAXBSchema.SUBJECT_REFNAME:RelationJAXBSchema.OBJECT_REFNAME));
422 docModel = NuxeoBasedResource.getDocModelForRefName(ctx, refName, ctx.getResourceMap());
423 } catch (Exception e) {
424 throw new InvalidDocumentException(
425 "Relation record must have a CSID or refName to identify the object of the relation.", e);
429 throw new DocumentNotFoundException("RelationDMH.getSubjectOrObjectDocModel could not find doc with CSID: "
430 +csid+" and/or refName: "+refName );
435 private void populateSubjectOrObjectValues(
436 DocumentModel relationDocModel,
437 DocumentModel subjectOrObjectDocModel,
439 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
441 HashMap<String,Object> properties = new HashMap<String,Object>();
443 String doctype = subjectOrObjectDocModel.getDocumentType().getName();
444 doctype = ServiceBindingUtils.getUnqualifiedTenantDocType(doctype);
445 properties.put((fSubject?RelationJAXBSchema.SUBJECT_DOCTYPE:RelationJAXBSchema.OBJECT_DOCTYPE),
448 String csid = (String) subjectOrObjectDocModel.getName();
449 properties.put((fSubject?RelationJAXBSchema.SUBJECT_CSID:RelationJAXBSchema.OBJECT_CSID),
452 String uri = (String) subjectOrObjectDocModel.getProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
453 CollectionSpaceClient.COLLECTIONSPACE_CORE_URI);
454 properties.put((fSubject?RelationJAXBSchema.SUBJECT_URI:RelationJAXBSchema.OBJECT_URI),
458 String common_schema = getCommonSchemaNameForDocType(doctype);
460 if(common_schema!=null) {
461 String refname = (String)subjectOrObjectDocModel.getProperty(common_schema,
463 properties.put((fSubject?RelationJAXBSchema.SUBJECT_REFNAME:RelationJAXBSchema.OBJECT_REFNAME),
467 String refname = (String)
468 subjectOrObjectDocModel.getProperty(
469 CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
470 CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME);
471 properties.put((fSubject?
472 RelationJAXBSchema.SUBJECT_REFNAME
473 :RelationJAXBSchema.OBJECT_REFNAME),
475 } catch (ClientException ce) {
476 throw new RuntimeException(
477 "populateSubjectOrObjectValues: Problem fetching field " + ce.getLocalizedMessage());
480 // FIXME: Call below is based solely on Nuxeo API docs; have not yet verified that it correctly updates existing
481 // property values in the target document model.
483 relationDocModel.setProperties(ctx.getCommonPartLabel(), properties);
484 } catch (ClientException ce) {
485 throw new RuntimeException(
486 "populateSubjectValues: Problem setting fields " + ce.getLocalizedMessage());
491 public boolean supportsWorkflowStates() {