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.net.HttpURLConnection;
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.HashMap;
30 import java.util.HashSet;
31 import java.util.List;
33 import java.util.Map.Entry;
36 import javax.ws.rs.core.MediaType;
37 import javax.ws.rs.core.MultivaluedMap;
38 import javax.ws.rs.core.Response;
39 import javax.ws.rs.core.UriInfo;
40 import javax.xml.bind.JAXBElement;
42 import org.collectionspace.authentication.AuthN;
43 import org.collectionspace.services.authorization.AccountPermission;
44 import org.collectionspace.services.jaxb.AbstractCommonList;
45 import org.collectionspace.services.lifecycle.TransitionDef;
46 import org.collectionspace.services.client.AccountClient;
47 import org.collectionspace.services.client.CollectionSpaceClient;
48 import org.collectionspace.services.client.PayloadInputPart;
49 import org.collectionspace.services.client.PayloadOutputPart;
50 import org.collectionspace.services.client.PoxPayloadIn;
51 import org.collectionspace.services.client.PoxPayloadOut;
52 import org.collectionspace.services.client.Profiler;
53 import org.collectionspace.services.client.RelationClient;
54 import org.collectionspace.services.client.workflow.WorkflowClient;
55 import org.collectionspace.services.common.CSWebApplicationException;
56 import org.collectionspace.services.common.NuxeoBasedResource;
57 import org.collectionspace.services.common.ServiceException;
58 import org.collectionspace.services.common.authorityref.AuthorityRefList;
59 import org.collectionspace.services.common.config.ServiceConfigUtils;
60 import org.collectionspace.services.common.context.JaxRsContext;
61 import org.collectionspace.services.common.context.MultipartServiceContext;
62 import org.collectionspace.services.common.context.ServiceBindingUtils;
63 import org.collectionspace.services.common.context.ServiceContext;
64 import org.collectionspace.services.common.document.BadRequestException;
65 import org.collectionspace.services.common.document.DocumentException;
66 import org.collectionspace.services.common.document.DocumentUtils;
67 import org.collectionspace.services.common.document.DocumentWrapper;
68 import org.collectionspace.services.common.document.DocumentFilter;
69 import org.collectionspace.services.client.IRelationsManager;
70 import org.collectionspace.services.common.relation.RelationResource;
71 import org.collectionspace.services.common.repository.RepositoryClient;
72 import org.collectionspace.services.common.security.SecurityUtils;
73 import org.collectionspace.services.common.storage.jpa.JpaStorageUtils;
74 import org.collectionspace.services.common.api.CommonAPI;
75 import org.collectionspace.services.common.api.RefNameUtils;
76 import org.collectionspace.services.common.api.Tools;
77 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils;
78 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthRefConfigInfo;
79 import org.collectionspace.services.config.service.DocHandlerParams;
80 import org.collectionspace.services.config.service.ListResultField;
81 import org.collectionspace.services.config.service.ObjectPartType;
82 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
83 import org.collectionspace.services.relation.RelationsCommon;
84 import org.collectionspace.services.relation.RelationsCommonList;
85 import org.collectionspace.services.relation.RelationsDocListItem;
86 import org.collectionspace.services.relation.RelationshipType;
88 import org.dom4j.Element;
89 import org.nuxeo.ecm.core.api.DocumentModel;
90 import org.nuxeo.ecm.core.api.DocumentModelList;
91 import org.nuxeo.ecm.core.api.impl.DataModelImpl;
92 import org.nuxeo.ecm.core.api.model.DocumentPart;
93 import org.nuxeo.ecm.core.api.model.Property;
94 import org.nuxeo.ecm.core.api.model.PropertyException;
95 import org.nuxeo.ecm.core.api.model.impl.ScalarProperty;
96 import org.slf4j.Logger;
97 import org.slf4j.LoggerFactory;
100 * RemoteDocumentModelHandler
102 * $LastChangedRevision: $
103 * $LastChangedDate: $
107 public abstract class RemoteDocumentModelHandlerImpl<T, TL>
108 extends DocumentModelHandler<T, TL> {
111 private final Logger logger = LoggerFactory.getLogger(RemoteDocumentModelHandlerImpl.class);
112 private final static String CR = "\r\n";
113 private final static String EMPTYSTR = "";
114 private static final String COLLECTIONSPACE_CORE_SCHEMA = CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA;
115 private static final String ACCOUNT_PERMISSION_COMMON_PART_NAME = AccountClient.SERVICE_COMMON_PART_NAME;
118 * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#setServiceContext(org.collectionspace.services.common.context.ServiceContext)
121 public void setServiceContext(ServiceContext ctx) { //FIXME: Apply proper generics to ServiceContext<PoxPayloadIn, PoxPayloadOut>
122 if (ctx instanceof MultipartServiceContext) {
123 super.setServiceContext(ctx);
125 throw new IllegalArgumentException("setServiceContext requires instance of "
126 + MultipartServiceContext.class.getName());
131 protected String getRefnameDisplayName(DocumentWrapper<DocumentModel> docWrapper) {
132 return getRefnameDisplayName(docWrapper.getWrappedObject());
135 private String getRefnameDisplayName(DocumentModel docModel) { // Look in the tenant bindings to see what field should be our display name for our refname value
136 String result = null;
137 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
139 DocHandlerParams.Params params = null;
141 params = ServiceConfigUtils.getDocHandlerParams(ctx);
142 ListResultField field = params.getRefnameDisplayNameField();
144 String schema = field.getSchema();
145 if (schema == null || schema.trim().isEmpty()) {
146 schema = ctx.getCommonPartLabel();
149 result = getStringValue(docModel, schema, field);
150 } catch (Exception e) {
151 if (logger.isWarnEnabled()) {
152 logger.warn(String.format("Call failed to getRefnameDisplayName() for class %s", this.getClass().getName()));
160 public boolean supportsHierarchy() {
161 boolean result = false;
163 DocHandlerParams.Params params = null;
165 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
166 params = ServiceConfigUtils.getDocHandlerParams(ctx);
167 Boolean bool = params.isSupportsHierarchy();
169 result = bool.booleanValue();
171 } catch (DocumentException e) {
172 // TODO Auto-generated catch block
173 String errMsg = String.format("Could not get document handler params from config bindings for class %s", this.getClass().getName());
174 if (logger.isWarnEnabled() == true) {
183 public boolean supportsVersioning() {
184 boolean result = false;
186 DocHandlerParams.Params params = null;
188 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
189 params = ServiceConfigUtils.getDocHandlerParams(ctx);
190 Boolean bool = params.isSupportsVersioning();
192 result = bool.booleanValue();
194 } catch (DocumentException e) {
195 // TODO Auto-generated catch block
196 String errMsg = String.format("Could not get document handler params from config bindings for class %s", this.getClass().getName());
197 if (logger.isWarnEnabled() == true) {
206 public void handleWorkflowTransition(ServiceContext ctx, DocumentWrapper<DocumentModel> wrapDoc, TransitionDef transitionDef)
208 // Do nothing by default, but children can override if they want. The real workflow transition happens in the WorkflowDocumentModelHandler class
212 public void completeCreate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
213 super.completeCreate(wrapDoc);
214 if (supportsHierarchy() == true) {
215 handleRelationsPayload(wrapDoc, false);
220 public void handleUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
221 DocumentModel docModel = wrapDoc.getWrappedObject();
222 String workflowState = WorkflowClient.WORKFLOWSTATE_LOCKED;
224 if (docModel.getCurrentLifeCycleState().contains(workflowState) == true) {
225 throw new ServiceException(HttpURLConnection.HTTP_FORBIDDEN,
226 "Cannot UPDATE a resource/record if it is in the workflow state: " + workflowState);
228 super.handleUpdate(wrapDoc);
233 public boolean handleDelete(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
234 DocumentModel docModel = wrapDoc.getWrappedObject();
235 String workflowState = WorkflowClient.WORKFLOWSTATE_LOCKED;
237 if (docModel.getCurrentLifeCycleState().contains(workflowState) == true) {
238 throw new ServiceException(HttpURLConnection.HTTP_FORBIDDEN,
239 "Cannot DELETE a resource/record if it is in the workflow state: " + workflowState);
241 return super.handleDelete(wrapDoc);
245 /* NOTE: The authority item doc handler overrides (after calling) this method. It performs refName updates. In this
246 * method we just update any and all relationship records that use refNames that have changed.
248 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#completeUpdate(org.collectionspace.services.common.document.DocumentWrapper)
251 public void completeUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
252 DocumentModel docModel = wrapDoc.getWrappedObject();
254 String[] schemas = docModel.getDeclaredSchemas();
255 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
256 for (String schema : schemas) {
257 ObjectPartType partMeta = partsMetaMap.get(schema);
258 if (partMeta == null) {
259 continue; // unknown part, ignore
261 Map<String, Object> unQObjectProperties = extractPart(docModel, schema, partMeta);
262 if(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA.equals(schema)) {
263 addExtraCoreValues(docModel, unQObjectProperties);
265 addOutputPart(unQObjectProperties, schema, partMeta);
269 // If the resource's service supports hierarchy then we need to perform a little more work
271 if (supportsHierarchy() == true) {
272 handleRelationsPayload(wrapDoc, true); // refNames in relations payload should refer to pre-updated record refName value
273 handleRefNameReferencesUpdate(); // if our refName changed, we need to update any and all relationship records that used the old one
278 * Adds the output part.
280 * @param unQObjectProperties the un q object properties
281 * @param schema the schema
282 * @param partMeta the part meta
283 * @throws Exception the exception
284 * MediaType.APPLICATION_XML_TYPE
286 protected void addOutputPart(Map<String, Object> unQObjectProperties, String schema, ObjectPartType partMeta)
288 Element doc = DocumentUtils.buildDocument(partMeta, schema,
289 unQObjectProperties);
290 if (logger.isTraceEnabled() == true) {
291 logger.trace(doc.asXML());
293 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
294 ctx.addOutputPart(schema, doc, partMeta.getContent().getContentType());
298 * Extract paging info.
300 * @param commonsList the commons list
302 * @throws Exception the exception
304 public TL extractPagingInfo(TL theCommonList, DocumentWrapper<DocumentModelList> wrapDoc)
306 AbstractCommonList commonList = (AbstractCommonList) theCommonList;
308 DocumentFilter docFilter = this.getDocumentFilter();
309 long pageSize = docFilter.getPageSize();
310 long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
311 // set the page size and page number
312 commonList.setPageNum(pageNum);
313 commonList.setPageSize(pageSize);
314 DocumentModelList docList = wrapDoc.getWrappedObject();
315 // Set num of items in list. this is useful to our testing framework.
316 commonList.setItemsInPage(docList.size());
317 // set the total result size
318 commonList.setTotalItems(docList.totalSize());
320 return (TL) commonList;
324 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#extractAllParts(org.collectionspace.services.common.document.DocumentWrapper)
327 public void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc)
330 DocumentModel docModel = wrapDoc.getWrappedObject();
331 String[] schemas = docModel.getDeclaredSchemas();
332 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
333 for (String schema : schemas) {
334 ObjectPartType partMeta = partsMetaMap.get(schema);
335 if (partMeta == null) {
336 continue; // unknown part, ignore
338 Map<String, Object> unQObjectProperties = extractPart(docModel, schema, partMeta);
339 if(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA.equals(schema)) {
340 addExtraCoreValues(docModel, unQObjectProperties);
342 addOutputPart(unQObjectProperties, schema, partMeta);
345 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
347 if (supportsHierarchy() == true) {
348 String showSiblings = ctx.getQueryParams().getFirst(CommonAPI.showSiblings_QP);
349 if (Tools.isTrue(showSiblings)) {
350 showSiblings(wrapDoc, ctx);
351 return; // actual result is returned on ctx.addOutputPart();
354 String showRelations = ctx.getQueryParams().getFirst(CommonAPI.showRelations_QP);
355 if (Tools.isTrue(showRelations)) {
356 showRelations(wrapDoc, ctx);
357 return; // actual result is returned on ctx.addOutputPart();
360 String showAllRelations = ctx.getQueryParams().getFirst(CommonAPI.showAllRelations_QP);
361 if (Tools.isTrue(showAllRelations)) {
362 showAllRelations(wrapDoc, ctx);
363 return; // actual result is returned on ctx.addOutputPart();
367 String currentUser = ctx.getUserId();
368 if (currentUser.equalsIgnoreCase(AuthN.ANONYMOUS_USER) == false) {
369 addAccountPermissionsPart();
373 private void addExtraCoreValues(DocumentModel docModel, Map<String, Object> unQObjectProperties)
375 unQObjectProperties.put(CollectionSpaceClient.COLLECTIONSPACE_CORE_WORKFLOWSTATE, docModel.getCurrentLifeCycleState());
378 private void addAccountPermissionsPart() throws Exception {
379 Profiler profiler = new Profiler("addAccountPermissionsPart():", 1);
382 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
383 String currentServiceName = ctx.getServiceName();
384 String workflowSubResource = "/";
385 JaxRsContext jaxRsContext = ctx.getJaxRsContext();
386 if (jaxRsContext != null) { // If not null then we're dealing with an authority item
387 String resourceName = SecurityUtils.getResourceName(jaxRsContext.getUriInfo());
388 workflowSubResource = workflowSubResource + resourceName + WorkflowClient.SERVICE_PATH + "/";
390 workflowSubResource = workflowSubResource + currentServiceName + WorkflowClient.SERVICE_AUTHZ_SUFFIX;
392 AccountPermission accountPermission = JpaStorageUtils.getAccountPermissions(JpaStorageUtils.CS_CURRENT_USER,
393 currentServiceName, workflowSubResource);
394 org.collectionspace.services.authorization.ObjectFactory objectFactory =
395 new org.collectionspace.services.authorization.ObjectFactory();
396 JAXBElement<AccountPermission> ap = objectFactory.createAccountPermission(accountPermission);
397 PayloadOutputPart accountPermissionPart = new PayloadOutputPart("account_permission", ap); // REM - "account_permission" should be using a constant and not a literal
398 ctx.addOutputPart(accountPermissionPart);
404 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#fillAllParts(org.collectionspace.services.common.document.DocumentWrapper)
407 public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
409 //TODO filling extension parts should be dynamic
410 //Nuxeo APIs lack to support stream/byte[] input, get/setting properties is
411 //not an ideal way of populating objects.
412 DocumentModel docModel = wrapDoc.getWrappedObject();
413 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
414 PoxPayloadIn input = ctx.getInput();
415 if (input == null || input.getParts().isEmpty()) {
416 String msg = String.format("No payload found for '%s' action.", action);
417 logger.error(msg + "Ctx=" + getServiceContext().toString());
418 throw new BadRequestException(msg);
421 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
423 //iterate over parts received and fill those parts
424 boolean werePartsFilled = false;
425 List<PayloadInputPart> inputParts = input.getParts();
426 for (PayloadInputPart part : inputParts) {
428 String partLabel = part.getLabel();
429 if (partLabel == null) {
430 String msg = "Part label is missing or empty!";
431 logger.error(msg + "Ctx=" + getServiceContext().toString());
432 throw new BadRequestException(msg);
435 //skip if the part is not in metadata or if it is a system part
436 ObjectPartType partMeta = partsMetaMap.get(partLabel);
437 if (partMeta == null || isSystemPart(partLabel)) {
440 fillPart(part, docModel, partMeta, action, ctx);
441 werePartsFilled = true;
444 if (logger.isTraceEnabled() && werePartsFilled == false) {
445 String msg = String.format("%s request had no XML payload parts processed in the request. Could be a payload with only relations-common-list request.",
451 private boolean isSystemPart(String partLabel) {
452 boolean result = false;
454 if (partLabel != null && (partLabel.equalsIgnoreCase(COLLECTIONSPACE_CORE_SCHEMA) ||
455 partLabel.equalsIgnoreCase(ACCOUNT_PERMISSION_COMMON_PART_NAME))) {
463 * fillPart fills an XML part into given document model
464 * @param part to fill
465 * @param docModel for the given object
466 * @param partMeta metadata for the object to fill
469 protected void fillPart(PayloadInputPart part, DocumentModel docModel, ObjectPartType partMeta, Action action,
470 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) throws Exception {
471 // check if this is an xml part
472 if (part.getMediaType().equals(MediaType.APPLICATION_XML_TYPE)) {
473 Element element = part.getElementBody();
474 Map<String, Object> objectProps = DocumentUtils.parseProperties(partMeta, element, ctx);
475 if (action == Action.UPDATE) {
476 this.filterReadOnlyPropertiesForPart(objectProps, partMeta);
478 String schemaName = partMeta.getLabel();
479 docModel.setProperties(schemaName, objectProps);
480 Set<String> fieldNameSet = getFieldNamesSet(objectProps);
481 setFieldsDirty(docModel, schemaName, fieldNameSet); // Force Nuxeo to update EVERY field by marking them dirty/modified
487 * Due to an apparent Nuxeo bug, we need to explicitly mark all fields as dirty for updates
488 * to take place properly. See https://issues.collectionspace.org/browse/CSPACE-7124
490 * We should improve this code by marking dirty/modified only the fields that were part of the UPDATE/PUT request
491 * payload. The current code will mark any field with a name in the 'fieldNameSet' set. Since field names do not
492 * have to be unique in document/record, we might end up unnecessarily marking some fields as modified -not ideal but
493 * better than brute-force marking ALL fields as modified.
498 private void setFieldsDirty(DocumentModel docModel, String schemaName, Set<String> fieldNameSet) {
499 DataModelImpl dataModel = (DataModelImpl) docModel.getDataModel(schemaName);
500 DocumentPart documentPart = dataModel.getDocumentPart();
501 setFieldsDirty(documentPart.getChildren(), fieldNameSet);
505 * Recursively step through all the children and sub-children setting their dirty flags.
506 * See https://issues.collectionspace.org/browse/CSPACE-7124
509 private void setFieldsDirty(Collection<Property> children, Set<String> fieldNameSet) {
510 if (children != null && (children.size() > 0)) {
511 for (Property prop : children ) {
512 String propName = prop.getName();
513 logger.debug(propName);
514 if (prop.isPhantom() == false) {
515 if (prop.isScalar() == false) {
516 setFieldsDirty(prop.getChildren(), fieldNameSet);
517 } else if (fieldNameSet.contains(propName)) {
518 ScalarProperty scalarProp = (ScalarProperty)prop;
519 scalarProp.setIsModified();
527 * Gets the set of field names used in a map of properties. We'll use this make our workaround to a
528 * bug (See https://issues.collectionspace.org/browse/CSPACE-7124) more efficient by only marking fields
529 * with names in this set as modified -ie, needing persisting.
531 * Since the map of properties can contain other maps of properties as values, there can be duplicate
532 * field names in the 'objectProps' map. And since we're creating a set, there can be no duplicates in the result.
535 private Set<String> getFieldNamesSet(Map<String, Object> objectProps) {
536 HashSet<String> result = new HashSet<String>();
538 addFieldNamesToSet(result, objectProps);
543 private void addFieldNamesToSet(Set<String> fieldNameSet, List<Object> valueList) {
544 for (Object value : valueList) {
545 if (value instanceof List) {
546 addFieldNamesToSet(fieldNameSet, (List<Object>)value);
547 } else if (value instanceof Map) {
548 addFieldNamesToSet(fieldNameSet, (Map<String, Object>)value);
550 logger.debug(value.toString());
555 private void addFieldNamesToSet(Set<String> fieldNameSet, Map<String, Object> objectProps) {
556 for (String key : objectProps.keySet()) {
557 Object value = objectProps.get(key);
558 if (value instanceof Map) {
559 addFieldNamesToSet(fieldNameSet, (Map<String, Object>)value);
560 } else if (value instanceof List) {
561 addFieldNamesToSet(fieldNameSet, (List)value);
563 fieldNameSet.add(key);
569 * Filters out read only properties, so they cannot be set on update.
570 * TODO: add configuration support to do this generally
571 * @param objectProps the properties parsed from the update payload
572 * @param partMeta metadata for the object to fill
574 public void filterReadOnlyPropertiesForPart(
575 Map<String, Object> objectProps, ObjectPartType partMeta) {
576 // Should add in logic to filter most of the core items on update
577 if(partMeta.getLabel().equalsIgnoreCase(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA)) {
578 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_AT);
579 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_BY);
580 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_URI);
581 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_TENANTID);
582 // Note that the updatedAt/updatedBy fields are set internally
583 // in DocumentModelHandler.handleCoreValues().
588 * extractPart extracts an XML object from given DocumentModel
590 * @param schema of the object to extract
591 * @param partMeta metadata for the object to extract
594 protected Map<String, Object> extractPart(DocumentModel docModel, String schema)
596 return extractPart(docModel, schema, (Map<String, Object>)null);
600 * extractPart extracts an XML object from given DocumentModel
602 * @param schema of the object to extract
603 * @param partMeta metadata for the object to extract
607 protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
609 return extractPart(docModel, schema, partMeta, null);
613 * extractPart extracts an XML object from given DocumentModel
615 * @param schema of the object to extract
616 * @param partMeta metadata for the object to extract
619 protected Map<String, Object> extractPart(
620 DocumentModel docModel,
622 Map<String, Object> addToMap)
624 Map<String, Object> result = null;
626 Map<String, Object> objectProps = docModel.getProperties(schema);
627 if (objectProps != null) {
628 //unqualify properties before sending the doc over the wire (to save bandwidh)
629 //FIXME: is there a better way to avoid duplication of a Map/Collection?
630 Map<String, Object> unQObjectProperties =
631 (addToMap != null) ? addToMap : (new HashMap<String, Object>());
632 Set<Entry<String, Object>> qualifiedEntries = objectProps.entrySet();
633 for (Entry<String, Object> entry : qualifiedEntries) {
634 String unqProp = getUnQProperty(entry.getKey());
635 unQObjectProperties.put(unqProp, entry.getValue());
637 result = unQObjectProperties;
644 * extractPart extracts an XML object from given DocumentModel
646 * @param schema of the object to extract
647 * @param partMeta metadata for the object to extract
651 protected Map<String, Object> extractPart(
652 DocumentModel docModel, String schema, ObjectPartType partMeta,
653 Map<String, Object> addToMap)
655 Map<String, Object> result = null;
657 result = this.extractPart(docModel, schema, addToMap);
663 public String getStringPropertyFromDoc(
666 String propertyXPath ) throws DocumentNotFoundException, DocumentException {
667 RepositoryInstance repoSession = null;
668 boolean releaseRepoSession = false;
669 String returnValue = null;
672 RepositoryClientImpl repoClient = (RepositoryClientImpl)this.getRepositoryClient(ctx);
673 repoSession = this.getRepositorySession();
674 if (repoSession == null) {
675 repoSession = repoClient.getRepositorySession();
676 releaseRepoSession = true;
680 DocumentWrapper<DocumentModel> wrapper = repoClient.getDoc(repoSession, ctx, csid);
681 DocumentModel docModel = wrapper.getWrappedObject();
682 returnValue = (String) docModel.getPropertyValue(propertyXPath);
683 } catch (PropertyException pe) {
685 } catch (DocumentException de) {
687 } catch (Exception e) {
688 if (logger.isDebugEnabled()) {
689 logger.debug("Caught exception ", e);
691 throw new DocumentException(e);
693 if (releaseRepoSession && repoSession != null) {
694 repoClient.releaseRepositorySession(repoSession);
697 } catch (Exception e) {
698 if (logger.isDebugEnabled()) {
699 logger.debug("Caught exception ", e);
701 throw new DocumentException(e);
705 if (logger.isWarnEnabled() == true) {
706 logger.warn("Returned DocumentModel instance was created with a repository session that is now closed.");
715 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#getAuthorityRefs(org.collectionspace.services.common.document.DocumentWrapper, java.util.List)
718 public AuthorityRefList getAuthorityRefs(
720 List<AuthRefConfigInfo> authRefConfigInfoList) throws PropertyException, Exception {
722 AuthorityRefList authRefList = new AuthorityRefList();
723 AbstractCommonList commonList = (AbstractCommonList) authRefList;
725 DocumentFilter docFilter = this.getDocumentFilter();
726 long pageSize = docFilter.getPageSize();
727 long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
728 // set the page size and page number
729 commonList.setPageNum(pageNum);
730 commonList.setPageSize(pageSize);
732 List<AuthorityRefList.AuthorityRefItem> list = authRefList.getAuthorityRefItem();
735 int iFirstToUse = (int)(pageSize*pageNum);
736 int nFoundInPage = 0;
739 ArrayList<RefNameServiceUtils.AuthRefInfo> foundReferences
740 = new ArrayList<RefNameServiceUtils.AuthRefInfo>();
742 boolean releaseRepoSession = false;
743 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
744 RepositoryClientImpl repoClient = (RepositoryClientImpl)this.getRepositoryClient(ctx);
745 CoreSessionInterface repoSession = this.getRepositorySession();
746 if (repoSession == null) {
747 repoSession = repoClient.getRepositorySession(ctx);
748 releaseRepoSession = true;
749 this.setRepositorySession(repoSession); // we (the doc handler) should keep track of this repository session in case we need it
753 DocumentModel docModel = repoClient.getDoc(repoSession, ctx, csid).getWrappedObject();
754 RefNameServiceUtils.findAuthRefPropertiesInDoc(docModel, authRefConfigInfoList, null, foundReferences);
755 // Slightly goofy pagination support - how many refs do we expect from one object?
756 for(RefNameServiceUtils.AuthRefInfo ari:foundReferences) {
757 if ((nFoundTotal >= iFirstToUse) && (nFoundInPage < pageSize)) {
758 if(appendToAuthRefsList(ari, list)) {
767 if (releaseRepoSession == true) {
768 repoClient.releaseRepositorySession(ctx, repoSession);
772 // Set num of items in list. this is useful to our testing framework.
773 commonList.setItemsInPage(nFoundInPage);
774 // set the total result size
775 commonList.setTotalItems(nFoundTotal);
777 } catch (PropertyException pe) {
778 String msg = "Attempted to retrieve value for invalid or missing authority field. "
779 + "Check authority field properties in tenant bindings.";
780 logger.warn(msg, pe);
782 } catch (Exception e) {
783 if (logger.isDebugEnabled()) {
784 logger.debug("Caught exception in getAuthorityRefs", e);
786 Response response = Response.status(
787 Response.Status.INTERNAL_SERVER_ERROR).entity(
788 "Failed to retrieve authority references").type(
789 "text/plain").build();
790 throw new CSWebApplicationException(e, response);
796 private boolean appendToAuthRefsList(RefNameServiceUtils.AuthRefInfo ari,
797 List<AuthorityRefList.AuthorityRefItem> list)
799 String fieldName = ari.getQualifiedDisplayName();
801 String refNameValue = (String)ari.getProperty().getValue();
802 AuthorityRefList.AuthorityRefItem item = authorityRefListItem(fieldName, refNameValue);
803 if(item!=null) { // ignore garbage values.
807 } catch(PropertyException pe) {
808 String msg = "PropertyException on: "+ari.getProperty().getPath()+pe.getLocalizedMessage();
809 if (logger.isDebugEnabled()) {
810 logger.debug(msg, pe);
819 * Fill in all the values to be returned in the authrefs payload for this item.
821 * @param authRefFieldName
825 private AuthorityRefList.AuthorityRefItem authorityRefListItem(String authRefFieldName, String refName) {
827 // Find the CSID for the authority item
831 DocumentModel docModel = NuxeoUtils.getDocModelForRefName(getServiceContext(), refName, getServiceContext().getResourceMap());
832 csid = NuxeoUtils.getCsid(docModel);
833 } catch (Exception e1) {
834 String msg = String.format("Could not find CSID for authority reference with refname = %s.", refName);
835 if (logger.isDebugEnabled()) {
836 logger.debug(msg, e1);
842 AuthorityRefList.AuthorityRefItem ilistItem = new AuthorityRefList.AuthorityRefItem();
844 RefNameUtils.AuthorityTermInfo termInfo = RefNameUtils.parseAuthorityTermInfo(refName);
845 if (Tools.isEmpty(csid) == false) {
846 ilistItem.setCsid(csid);
848 ilistItem.setRefName(refName);
849 ilistItem.setAuthDisplayName(termInfo.inAuthority.displayName);
850 ilistItem.setItemDisplayName(termInfo.displayName);
851 ilistItem.setSourceField(authRefFieldName);
852 ilistItem.setUri(termInfo.getRelativeUri());
853 } catch (Exception e) {
855 String msg = String.format("Trouble parsing refName from value: %s in field: %s. Error message: %s.",
856 refName, authRefFieldName, e.getLocalizedMessage());
857 if (logger.isDebugEnabled()) {
858 logger.debug(msg, e);
868 * Returns the primary value from a list of values.
870 * Assumes that the first value is the primary value.
871 * This assumption may change when and if the primary value
872 * is identified explicitly.
874 * @param values a list of values.
875 * @param propertyName the name of a property through
876 * which the value can be extracted.
877 * @return the primary value.
878 protected String primaryValueFromMultivalue(List<Object> values, String propertyName) {
879 String primaryValue = "";
880 if (values == null || values.size() == 0) {
883 Object value = values.get(0);
884 if (value instanceof String) {
886 primaryValue = (String) value;
888 // Multivalue group of fields
889 } else if (value instanceof Map) {
891 Map map = (Map) value;
892 if (map.values().size() > 0) {
893 if (map.get(propertyName) != null) {
894 primaryValue = (String) map.get(propertyName);
899 logger.warn("Unexpected type for property " + propertyName
900 + " in multivalue list: not String or Map.");
907 * Gets a simple property from the document.
909 * For completeness, as this duplicates DocumentModel method.
911 * @param docModel The document model to get info from
912 * @param schema The name of the schema (part)
913 * @param propertyName The simple scalar property type
914 * @return property value as String
916 protected String getSimpleStringProperty(DocumentModel docModel, String schema, String propName) {
917 String xpath = "/"+schema+":"+propName;
919 return (String) NuxeoUtils.getProperyValue(docModel, xpath); //docModel.getPropertyValue(xpath);
920 } catch(PropertyException pe) {
921 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a simple String property?"
922 +pe.getLocalizedMessage());
923 } catch(ClassCastException cce) {
924 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a scalar String property?"
925 +cce.getLocalizedMessage());
926 } catch(Exception e) {
927 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
928 +e.getLocalizedMessage());
933 * Gets first of a repeating list of scalar values, as a String, from the document.
935 * @param docModel The document model to get info from
936 * @param schema The name of the schema (part)
937 * @param listName The name of the scalar list property
938 * @return first value in list, as a String, or empty string if the list is empty
940 protected String getFirstRepeatingStringProperty(DocumentModel docModel,
941 String schema, String listName) {
942 String xpath = "/" + schema + ":" + listName + "/[0]";
944 return (String) NuxeoUtils.getProperyValue(docModel, xpath); // docModel.getPropertyValue(xpath);
945 } catch (PropertyException pe) {
946 throw new RuntimeException("Problem retrieving property {" + xpath
947 + "}. Not a repeating scalar?" + pe.getLocalizedMessage());
948 } catch (IndexOutOfBoundsException ioobe) {
949 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
950 return ""; // gracefully handle missing elements
951 } catch (ClassCastException cce) {
952 throw new RuntimeException("Problem retrieving property {" + xpath
953 + "} as String. Not a repeating String property?"
954 + cce.getLocalizedMessage());
955 } catch (Exception e) {
956 throw new RuntimeException("Unknown problem retrieving property {"
957 + xpath + "}." + e.getLocalizedMessage());
962 * Gets first of a repeating list of scalar values, as a String, from the document.
964 * @param docModel The document model to get info from
965 * @param schema The name of the schema (part)
966 * @param listName The name of the scalar list property
967 * @return first value in list, as a String, or empty string if the list is empty
969 protected String getStringValueInPrimaryRepeatingComplexProperty(
970 DocumentModel docModel, String schema, String complexPropertyName, String fieldName) {
971 String result = null;
973 String xpath = "/" + NuxeoUtils.getPrimaryXPathPropertyName(schema, complexPropertyName, fieldName);
975 result = (String) NuxeoUtils.getProperyValue(docModel, xpath); //docModel.getPropertyValue(xpath);
976 } catch(PropertyException pe) {
977 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad propertyNames?"
978 +pe.getLocalizedMessage());
979 } catch(IndexOutOfBoundsException ioobe) {
980 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
981 result = ""; // gracefully handle missing elements
982 } catch(ClassCastException cce) {
983 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
984 +cce.getLocalizedMessage());
985 } catch(NullPointerException npe) {
986 // Getting here because of a bug in Nuxeo when value in repository is unknown/empty/null
987 logger.warn(String.format("Nuxeo repo unexpectedly returned an Null Pointer Exception when asked for the value of {%s}.",
989 } catch(Exception e) {
990 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
991 +e.getLocalizedMessage());
998 * Gets XPath value from schema. Note that only "/" and "[n]" are
999 * supported for xpath. Can omit grouping elements for repeating complex types,
1000 * e.g., "fieldList/[0]" can be used as shorthand for "fieldList/field[0]" and
1001 * "fieldGroupList/[0]/field" can be used as shorthand for "fieldGroupList/fieldGroup[0]/field".
1002 * If there are no entries for a list of scalars or for a list of complex types,
1003 * a 0 index expression (e.g., "fieldGroupList/[0]/field") will safely return an empty
1004 * string. A non-zero index will throw an IndexOutOfBoundsException if there are not
1005 * that many elements in the list.
1006 * N.B.: This does not follow the XPath spec - indices are 0-based, not 1-based.
1008 * @param docModel The document model to get info from
1009 * @param schema The name of the schema (part)
1010 * @param xpath The XPath expression (without schema prefix)
1011 * @return value the indicated property value as a String
1012 * @throws DocumentException
1014 protected Object getListResultValue(DocumentModel docModel, // REM - CSPACE-5133
1015 String schema, ListResultField field) throws DocumentException {
1016 Object result = null;
1018 result = NuxeoUtils.getXPathValue(docModel, schema, field.getXpath());
1023 protected String getStringValue(DocumentModel docModel,
1024 String schema, ListResultField field) throws DocumentException {
1025 String result = null;
1027 Object value = getListResultValue(docModel, schema, field);
1028 if (value != null && value instanceof String) {
1029 String strValue = (String) value;
1030 if (strValue.trim().isEmpty() == false) {
1038 protected void removeFromList(List<RelationsCommonList.RelationListItem> list, RelationsCommonList.RelationListItem item) {
1042 private void itemToString(StringBuilder sb, String prefix, RelationsCommonList.RelationListItem item ) {
1044 sb.append((item.getCsid()!= null)?item.getCsid():"NO CSID");
1046 sb.append((item.getSubject().getCsid()!=null)?item.getSubject().getCsid():item.getSubject().getRefName());
1048 sb.append(item.getPredicate());
1050 sb.append((item.getObject().getCsid()!=null)?item.getObject().getCsid():item.getObject().getRefName());
1054 private String dumpList(List<RelationsCommonList.RelationListItem> list, String label) {
1055 StringBuilder sb = new StringBuilder();
1057 if (list.size() > 0) {
1058 sb.append("=========== " + label + " ==========" + CR);
1060 for (RelationsCommonList.RelationListItem item : list) {
1061 itemToString(sb, "== ", item);
1064 return sb.toString();
1067 /** @return null on parent not found
1069 protected String getParentCSID(String thisCSID) throws Exception {
1070 String parentCSID = null;
1072 String predicate = RelationshipType.HAS_BROADER.value();
1073 RelationsCommonList parentListOuter = getRelations(thisCSID, null, predicate);
1074 List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
1075 if (parentList != null) {
1076 if (parentList.size() == 0) {
1079 RelationsCommonList.RelationListItem relationListItem = parentList.get(0);
1080 parentCSID = relationListItem.getObjectCsid();
1083 } catch (Exception e) {
1084 logger.error("Could not find parent for this: " + thisCSID, e);
1089 protected List<RelationsCommonList.RelationListItem> newRelationsCommonList() {
1090 List<RelationsCommonList.RelationListItem> result = new ArrayList<RelationsCommonList.RelationListItem>();
1094 public void showSiblings(DocumentWrapper<DocumentModel> wrapDoc,
1095 MultipartServiceContext ctx) throws Exception {
1096 String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
1097 String parentCSID = getParentCSID(thisCSID);
1098 if (parentCSID == null) {
1099 logger.warn("~~~~~\r\n~~~~ Could not find parent for this: " + thisCSID);
1103 String predicate = RelationshipType.HAS_BROADER.value();
1104 RelationsCommonList siblingListOuter = getRelations(null, parentCSID, predicate);
1105 List<RelationsCommonList.RelationListItem> siblingList = siblingListOuter.getRelationListItem();
1107 List<RelationsCommonList.RelationListItem> toRemoveList = newRelationsCommonList();
1110 RelationsCommonList.RelationListItem item = null;
1111 for (RelationsCommonList.RelationListItem sibling : siblingList) {
1112 if (thisCSID.equals(sibling.getSubjectCsid())) {
1113 toRemoveList.add(sibling); //IS_A copy of the main item, i.e. I have a parent that is my parent, so I'm in the list from the above query.
1116 //rather than create an immutable iterator, I'm just putting the items to remove on a separate list, then looping over that list and removing.
1117 for (RelationsCommonList.RelationListItem self : toRemoveList) {
1118 removeFromList(siblingList, self);
1121 long siblingSize = siblingList.size();
1122 siblingListOuter.setTotalItems(siblingSize);
1123 siblingListOuter.setItemsInPage(siblingSize);
1124 if(logger.isTraceEnabled()) {
1125 String dump = dumpList(siblingList, "Siblings of: "+thisCSID);
1126 logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showSiblings ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1129 PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, siblingListOuter);
1130 ctx.addOutputPart(relationsPart);
1133 public void showRelations(DocumentWrapper<DocumentModel> wrapDoc,
1134 MultipartServiceContext ctx) throws Exception {
1135 String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
1137 String predicate = RelationshipType.HAS_BROADER.value();
1138 RelationsCommonList parentListOuter = getRelations(thisCSID, null, predicate);
1139 List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
1141 RelationsCommonList childrenListOuter = getRelations(null, thisCSID, predicate);
1142 List<RelationsCommonList.RelationListItem> childrenList = childrenListOuter.getRelationListItem();
1144 if(logger.isTraceEnabled()) {
1145 String dump = dumpLists(thisCSID, parentList, childrenList, null);
1146 logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showRelations ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1149 //Assume that there are more children than parents. Will be true for parent/child, but maybe not for other relations.
1150 //Now add all parents to our childrenList, to be able to return just one list of consolidated results.
1151 //Not optimal, but that's the current design spec.
1153 for (RelationsCommonList.RelationListItem parent : parentList) {
1154 childrenList.add(parent);
1157 long childrenSize = childrenList.size();
1158 childrenListOuter.setTotalItems(childrenSize);
1159 childrenListOuter.setItemsInPage(childrenListOuter.getItemsInPage() + added);
1161 PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, childrenListOuter);
1162 ctx.addOutputPart(relationsPart);
1165 public void showAllRelations(DocumentWrapper<DocumentModel> wrapDoc, MultipartServiceContext ctx) throws Exception {
1166 String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
1168 RelationsCommonList subjectListOuter = getRelations(thisCSID, null, null); // nulls are wildcards: predicate=*, and object=*
1169 List<RelationsCommonList.RelationListItem> subjectList = subjectListOuter.getRelationListItem();
1171 RelationsCommonList objectListOuter = getRelations(null, thisCSID, null); // nulls are wildcards: subject=*, and predicate=*
1172 List<RelationsCommonList.RelationListItem> objectList = objectListOuter.getRelationListItem();
1174 if(logger.isTraceEnabled()) {
1175 String dump = dumpLists(thisCSID, subjectList, objectList, null);
1176 logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showAllRelations ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1179 subjectList.addAll(objectList);
1181 //now subjectList actually has records BOTH where thisCSID is subject and object.
1182 long relatedSize = subjectList.size();
1183 subjectListOuter.setTotalItems(relatedSize);
1184 subjectListOuter.setItemsInPage(relatedSize);
1186 PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, subjectListOuter);
1187 ctx.addOutputPart(relationsPart);
1190 private String dumpLists(String itemCSID,
1191 List<RelationsCommonList.RelationListItem> parentList,
1192 List<RelationsCommonList.RelationListItem> childList,
1193 List<RelationsCommonList.RelationListItem> actionList) {
1194 StringBuilder sb = new StringBuilder();
1195 sb.append("itemCSID: " + itemCSID + CR);
1196 if(parentList!=null) {
1197 sb.append(dumpList(parentList, "parentList"));
1199 if(childList!=null) {
1200 sb.append(dumpList(childList, "childList"));
1202 if(actionList!=null) {
1203 sb.append(dumpList(actionList, "actionList"));
1205 return sb.toString();
1208 //================= TODO: move this to common, refactoring this and CollectionObjectResource.java
1209 public RelationsCommonList getRelations(String subjectCSID, String objectCSID, String predicate) throws Exception {
1210 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1211 MultivaluedMap<String, String> queryParams = ctx.getQueryParams();
1212 queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1213 queryParams.putSingle(IRelationsManager.SUBJECT_QP, subjectCSID);
1214 queryParams.putSingle(IRelationsManager.OBJECT_QP, objectCSID);
1216 RelationResource relationResource = new RelationResource(); //is this still acting like a singleton as it should be?
1217 RelationsCommonList relationsCommonList = relationResource.getList(ctx);
1218 return relationsCommonList;
1220 //============================= END TODO refactor ==========================
1222 // this method calls the RelationResource to have it create the relations and persist them.
1223 private void createRelations(List<RelationsCommonList.RelationListItem> inboundList,
1224 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) throws Exception {
1225 for (RelationsCommonList.RelationListItem item : inboundList) {
1226 RelationsCommon rc = new RelationsCommon();
1227 //rc.setCsid(item.getCsid());
1228 //todo: assignTo(item, rc);
1229 RelationsDocListItem itemSubject = item.getSubject();
1230 RelationsDocListItem itemObject = item.getObject();
1232 // Set at least one of CSID and refName for Subject and Object
1233 // Either value might be null for for each of Subject and Object
1234 String subjectCsid = itemSubject.getCsid();
1235 rc.setSubjectCsid(subjectCsid);
1237 String objCsid = itemObject.getCsid();
1238 rc.setObjectCsid(objCsid);
1240 rc.setSubjectRefName(itemSubject.getRefName());
1241 rc.setObjectRefName(itemObject.getRefName());
1243 rc.setRelationshipType(item.getPredicate());
1244 rc.setRelationshipMetaType(item.getRelationshipMetaType());
1245 //RelationshipType foo = (RelationshipType.valueOf(item.getPredicate())) ;
1246 //rc.setPredicate(foo); //this must be one of the type found in the enum in services/jaxb/src/main/resources/relations_common.xsd
1248 // This is superfluous, since it will be fetched by the Relations Create logic.
1249 rc.setSubjectDocumentType(itemSubject.getDocumentType());
1250 rc.setObjectDocumentType(itemObject.getDocumentType());
1252 // This is superfluous, since it will be fetched by the Relations Create logic.
1253 rc.setSubjectUri(itemSubject.getUri());
1254 rc.setObjectUri(itemObject.getUri());
1255 // May not have the info here. Only really require CSID or refName.
1256 // Rest is handled in the Relation create mechanism
1257 //uriPointsToSameAuthority(itemSubject.getUri(), itemObject.getUri());
1259 PoxPayloadOut payloadOut = new PoxPayloadOut(RelationClient.SERVICE_PAYLOAD_NAME);
1260 PayloadOutputPart outputPart = new PayloadOutputPart(RelationClient.SERVICE_COMMONPART_NAME, rc);
1261 payloadOut.addPart(outputPart);
1262 RelationResource relationResource = new RelationResource();
1263 Response res = relationResource.create(ctx, ctx.getResourceMap(),
1264 ctx.getUriInfo(), payloadOut.toXML()); //NOTE ui recycled from above to pass in unknown query params.
1268 // Note that item2 may be sparse (only refName, no CSID for subject or object)
1269 // But item1 must not be sparse
1270 private boolean itemsEqual(RelationsCommonList.RelationListItem item1, RelationsCommonList.RelationListItem item2) {
1271 if (item1 == null || item2 == null) {
1274 RelationsDocListItem subj1 = item1.getSubject();
1275 RelationsDocListItem subj2 = item2.getSubject();
1276 RelationsDocListItem obj1 = item1.getObject();
1277 RelationsDocListItem obj2 = item2.getObject();
1279 String subj1Csid = subj1.getCsid();
1280 String subj2Csid = subj2.getCsid();
1281 String subj1RefName = subj1.getRefName();
1282 String subj2RefName = subj2.getRefName();
1284 String obj1Csid = obj1.getCsid();
1285 String obj2Csid = obj2.getCsid();
1286 String obj1RefName = obj1.getRefName();
1287 String obj2RefName = obj2.getRefName();
1289 String item1Metatype = item1.getRelationshipMetaType();
1290 item1Metatype = item1Metatype != null ? item1Metatype : EMPTYSTR;
1292 String item2Metatype = item2.getRelationshipMetaType();
1293 item2Metatype = item2Metatype != null ? item2Metatype : EMPTYSTR;
1295 boolean isEqual = (subj1Csid.equals(subj2Csid) || ((subj2Csid==null) && subj1RefName.equals(subj2RefName)))
1296 && (obj1Csid.equals(obj2Csid) || ((obj2Csid==null) && obj1RefName.equals(obj2RefName)))
1297 // predicate is proper, but still allow relationshipType
1298 && (item1.getPredicate().equals(item2.getPredicate())
1299 || ((item2.getPredicate()==null) && item1.getRelationshipType().equals(item2.getRelationshipType())))
1300 // Allow missing docTypes, so long as they do not conflict
1301 && (obj1.getDocumentType().equals(obj2.getDocumentType()) || obj2.getDocumentType()==null)
1302 && (subj1.getDocumentType().equals(subj2.getDocumentType()) || subj2.getDocumentType()==null)
1303 && (item1Metatype.equalsIgnoreCase(item2Metatype));
1307 // Note that the item argument may be sparse (only refName, no CSID for subject or object)
1308 // But the list items must not be sparse
1309 private RelationsCommonList.RelationListItem findInList(
1310 List<RelationsCommonList.RelationListItem> list,
1311 RelationsCommonList.RelationListItem item) {
1312 RelationsCommonList.RelationListItem foundItem = null;
1313 for (RelationsCommonList.RelationListItem listItem : list) {
1314 if (itemsEqual(listItem, item)) { //equals must be defined, else
1315 foundItem = listItem;
1322 /** updateRelations strategy:
1325 go through inboundList, remove anything from childList that matches from childList
1326 go through inboundList, remove anything from parentList that matches from parentList
1327 go through parentList, delete all remaining
1328 go through childList, delete all remaining
1329 go through actionList, add all remaining.
1330 check for duplicate children
1331 check for more than one parent.
1333 inboundList parentList childList actionList
1334 ---------------- --------------- ---------------- ----------------
1335 child-a parent-c child-a child-b
1336 child-b parent-d child-c
1341 private RelationsCommonList updateRelations(
1342 String itemCSID, PoxPayloadIn input, DocumentWrapper<DocumentModel> wrapDoc, boolean forUpdate)
1344 if (logger.isTraceEnabled()) {
1345 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID);
1347 PayloadInputPart part = input.getPart(RelationClient.SERVICE_COMMON_LIST_NAME); //input.getPart("relations_common");
1349 return null; //nothing to do--they didn't send a list of relations.
1351 RelationsCommonList relationsCommonListBody = (RelationsCommonList) part.getBody();
1352 List<RelationsCommonList.RelationListItem> inboundList = relationsCommonListBody.getRelationListItem();
1353 List<RelationsCommonList.RelationListItem> actionList = newRelationsCommonList();
1354 List<RelationsCommonList.RelationListItem> childList = null;
1355 List<RelationsCommonList.RelationListItem> parentList = null;
1356 DocumentModel docModel = wrapDoc.getWrappedObject();
1357 String itemRefName = (String) docModel.getProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
1358 CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME);
1360 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1361 //Do magic replacement of ${itemCSID} and fix URI's.
1362 fixupInboundListItems(ctx, inboundList, docModel, itemCSID);
1364 final String HAS_BROADER = RelationshipType.HAS_BROADER.value();
1365 UriInfo uriInfo = ctx.getUriInfo();
1366 MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
1369 //Run getList() once as sent to get childListOuter:
1370 String predicate = RelationshipType.HAS_BROADER.value();
1371 queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1372 queryParams.putSingle(IRelationsManager.SUBJECT_QP, null);
1373 queryParams.putSingle(IRelationsManager.SUBJECT_TYPE_QP, null);
1374 queryParams.putSingle(IRelationsManager.OBJECT_QP, itemCSID);
1375 queryParams.putSingle(IRelationsManager.OBJECT_TYPE_QP, null);
1377 RelationResource relationResource = new RelationResource();
1378 RelationsCommonList childListOuter = relationResource.getList(ctx); // Knows all query params because they are in the context.
1380 //Now run getList() again, leaving predicate, swapping subject and object, to get parentListOuter.
1381 queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1382 queryParams.putSingle(IRelationsManager.SUBJECT_QP, itemCSID);
1383 queryParams.putSingle(IRelationsManager.OBJECT_QP, null);
1384 RelationsCommonList parentListOuter = relationResource.getList(ctx);
1387 childList = childListOuter.getRelationListItem();
1388 parentList = parentListOuter.getRelationListItem();
1390 if (parentList.size() > 1) {
1391 throw new Exception("Too many parents for object: " + itemCSID + " list: " + dumpList(parentList, "parentList"));
1394 if (logger.isTraceEnabled()) {
1395 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " got existing relations.");
1399 for (RelationsCommonList.RelationListItem inboundItem : inboundList) {
1400 // Note that the relations may specify the other (non-item) bit with a refName, not a CSID,
1401 // and so the CSID for those may be null
1402 if(inboundItem.getPredicate().equals(HAS_BROADER)) {
1403 // Look for parents and children
1404 if(itemCSID.equals(inboundItem.getObject().getCsid())
1405 || itemRefName.equals(inboundItem.getObject().getRefName())) {
1406 //then this is an item that says we have a child. That child is inboundItem
1407 RelationsCommonList.RelationListItem childItem =
1408 (childList == null) ? null : findInList(childList, inboundItem);
1409 if (childItem != null) {
1410 if (logger.isTraceEnabled()) {
1411 StringBuilder sb = new StringBuilder();
1412 itemToString(sb, "== Child: ", childItem);
1413 logger.trace("Found inboundChild in current child list: " + sb.toString());
1415 removeFromList(childList, childItem); //exists, just take it off delete list
1417 if (logger.isTraceEnabled()) {
1418 StringBuilder sb = new StringBuilder();
1419 itemToString(sb, "== Child: ", inboundItem);
1420 logger.trace("inboundChild not in current child list, will add: " + sb.toString());
1422 actionList.add(inboundItem); //doesn't exist as a child, but is a child. Add to additions list
1423 String newChildCsid = inboundItem.getSubject().getCsid();
1424 if(newChildCsid == null) {
1425 String newChildRefName = inboundItem.getSubject().getRefName();
1426 if (newChildRefName == null) {
1427 throw new RuntimeException("Child with no CSID or refName!");
1429 if (logger.isTraceEnabled()) {
1430 logger.trace("Fetching CSID for child with only refname: "+newChildRefName);
1432 DocumentModel newChildDocModel =
1433 NuxeoBasedResource.getDocModelForRefName(getServiceContext(),
1434 newChildRefName, getServiceContext().getResourceMap());
1435 newChildCsid = getCsid(newChildDocModel);
1437 ensureChildHasNoOtherParents(ctx, queryParams, newChildCsid);
1440 } else if (itemCSID.equals(inboundItem.getSubject().getCsid())
1441 || itemRefName.equals(inboundItem.getSubject().getRefName())) {
1442 //then this is an item that says we have a parent. inboundItem is that parent.
1443 RelationsCommonList.RelationListItem parentItem =
1444 (parentList == null) ? null : findInList(parentList, inboundItem);
1445 if (parentItem != null) {
1446 removeFromList(parentList, parentItem); //exists, just take it off delete list
1448 actionList.add(inboundItem); //doesn't exist as a parent, but is a parent. Add to additions list
1451 logger.error("Parent/Child Element didn't link to this item. inboundItem: " + inboundItem);
1454 logger.warn("Non-parent relation ignored. inboundItem: " + inboundItem);
1457 if (logger.isTraceEnabled()) {
1458 String dump = dumpLists(itemCSID, parentList, childList, actionList);
1459 logger.trace("~~~~~~~~~~~~~~~~~~~~~~dump~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1462 if (logger.isTraceEnabled()) {
1463 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " deleting "
1464 + parentList.size() + " existing parents and " + childList.size() + " existing children.");
1466 deleteRelations(parentList, ctx, "parentList"); //todo: there are items appearing on both lists....april 20.
1467 deleteRelations(childList, ctx, "childList");
1469 if (logger.isTraceEnabled()) {
1470 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " adding "
1471 + actionList.size() + " new parents and children.");
1473 createRelations(actionList, ctx);
1474 if (logger.isTraceEnabled()) {
1475 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " done.");
1477 // We return all elements on the inbound list, since we have just worked to make them exist in the system
1478 // and be non-redundant, etc. That list came from relationsCommonListBody, so it is still attached to it, just pass that back.
1479 return relationsCommonListBody;
1482 /** Performs substitution for ${itemCSID} (see CommonAPI.AuthorityItemCSID_REPLACE for constant)
1483 * and sets URI correctly for related items.
1484 * Operates directly on the items in the list. Does not change the list ordering, does not add or remove any items.
1486 protected void fixupInboundListItems(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1487 List<RelationsCommonList.RelationListItem> inboundList,
1488 DocumentModel docModel,
1489 String itemCSID) throws Exception {
1490 String thisURI = this.getUri(docModel);
1491 // WARNING: the two code blocks below are almost identical and seem to ask to be put in a generic method.
1492 // beware of the little diffs in inboundItem.setObjectCsid(itemCSID); and inboundItem.setSubjectCsid(itemCSID); in the two blocks.
1493 for (RelationsCommonList.RelationListItem inboundItem : inboundList) {
1494 RelationsDocListItem inboundItemObject = inboundItem.getObject();
1495 RelationsDocListItem inboundItemSubject = inboundItem.getSubject();
1497 if (CommonAPI.AuthorityItemCSID_REPLACE.equalsIgnoreCase(inboundItemObject.getCsid())) {
1498 inboundItem.setObjectCsid(itemCSID);
1499 inboundItemObject.setCsid(itemCSID);
1500 //inboundItemObject.setUri(getUri(docModel));
1503 String objectCsid = inboundItemObject.getCsid();
1504 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(getRepositorySession(), ctx, objectCsid); //null if not found.
1505 DocumentWrapper wrapper = new DocumentWrapperImpl(itemDocModel);
1506 String uri = this.getRepositoryClient(ctx).getDocURI(wrapper);
1507 inboundItemObject.setUri(uri); //CSPACE-4037
1510 //uriPointsToSameAuthority(thisURI, inboundItemObject.getUri()); //CSPACE-4042
1512 if (CommonAPI.AuthorityItemCSID_REPLACE.equalsIgnoreCase(inboundItemSubject.getCsid())) {
1513 inboundItem.setSubjectCsid(itemCSID);
1514 inboundItemSubject.setCsid(itemCSID);
1515 //inboundItemSubject.setUri(getUri(docModel));
1518 String subjectCsid = inboundItemSubject.getCsid();
1519 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(getRepositorySession(), ctx, subjectCsid); //null if not found.
1520 DocumentWrapper wrapper = new DocumentWrapperImpl(itemDocModel);
1521 String uri = this.getRepositoryClient(ctx).getDocURI(wrapper);
1522 inboundItemSubject.setUri(uri); //CSPACE-4037
1525 //uriPointsToSameAuthority(thisURI, inboundItemSubject.getUri()); //CSPACE-4042
1530 private void ensureChildHasNoOtherParents(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1531 MultivaluedMap<String, String> queryParams, String childCSID) {
1532 logger.trace("ensureChildHasNoOtherParents for: " + childCSID );
1533 queryParams.putSingle(IRelationsManager.SUBJECT_QP, childCSID);
1534 queryParams.putSingle(IRelationsManager.PREDICATE_QP, RelationshipType.HAS_BROADER.value());
1535 queryParams.putSingle(IRelationsManager.OBJECT_QP, null); //null means ANY
1537 RelationResource relationResource = new RelationResource();
1538 RelationsCommonList parentListOuter = relationResource.getList(ctx);
1539 List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
1540 //logger.warn("ensureChildHasNoOtherParents preparing to delete relations on "+childCSID+"\'s parent list: \r\n"+dumpList(parentList, "duplicate parent list"));
1541 deleteRelations(parentList, ctx, "parentList-delete");
1544 private void deleteRelations(List<RelationsCommonList.RelationListItem> list,
1545 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1548 for (RelationsCommonList.RelationListItem item : list) {
1549 RelationResource relationResource = new RelationResource();
1550 if(logger.isTraceEnabled()) {
1551 StringBuilder sb = new StringBuilder();
1552 itemToString(sb, "==== TO DELETE: ", item);
1553 logger.trace(sb.toString());
1555 Response res = relationResource.deleteWithParentCtx(ctx, item.getCsid());
1556 if (logger.isDebugEnabled()) {
1557 logger.debug("Status of authority item deleteRelations method call was: " + res.getStatus());
1560 } catch (Throwable t) {
1561 String msg = "Unable to deleteRelations: " + Tools.errorToString(t, true);
1566 // Note that we must do this after we have completed the Update, so that the repository has the
1567 // info for the item itself. The relations code must call into the repo to get info for each end.
1568 // This could be optimized to pass in the parent docModel, since it will often be one end.
1569 // Nevertheless, we should complete the item save before we do work on the relations, especially
1570 // since a save on Create might fail, and we would not want to create relations for something
1571 // that may not be created...
1572 private void handleRelationsPayload(DocumentWrapper<DocumentModel> wrapDoc, boolean forUpdate) throws Exception {
1573 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1574 PoxPayloadIn input = ctx.getInput();
1575 DocumentModel documentModel = (wrapDoc.getWrappedObject());
1576 String itemCsid = documentModel.getName();
1578 //Updates relations part
1579 RelationsCommonList relationsCommonList = updateRelations(itemCsid, input, wrapDoc, forUpdate);
1581 PayloadOutputPart payloadOutputPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, relationsCommonList); //FIXME: REM - We should check for a null relationsCommonList and not create the new common list payload
1582 ctx.setProperty(RelationClient.SERVICE_COMMON_LIST_NAME, payloadOutputPart);
1584 //now we add part for relations list
1585 //ServiceContext ctx = getServiceContext();
1586 //PayloadOutputPart foo = (PayloadOutputPart) ctx.getProperty(RelationClient.SERVICE_COMMON_LIST_NAME);
1587 ctx.getOutput().addPart(payloadOutputPart);
1591 * Checks to see if the refName has changed, and if so,
1592 * uses utilities to find all references and update them to use the new refName.
1595 protected void handleRefNameReferencesUpdate() throws Exception {
1596 if (hasRefNameUpdate() == true) {
1597 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1598 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient = getRepositoryClient(ctx);
1599 CoreSessionInterface repoSession = this.getRepositorySession();
1601 // Update all the relationship records that referred to the old refName
1602 RefNameServiceUtils.updateRefNamesInRelations(ctx, repoClient, repoSession,
1603 oldRefNameOnUpdate, newRefNameOnUpdate);
1607 protected String getRefNameUpdate() {
1608 String result = null;
1610 if (hasRefNameUpdate() == true) {
1611 result = newRefNameOnUpdate;
1612 if (logger.isDebugEnabled() == true) {
1613 logger.debug(String.format("There was a refName update. New: %s Old: %s" ,
1614 newRefNameOnUpdate, oldRefNameOnUpdate));
1622 * Note: The Vocabulary document handler overrides this method.
1624 protected String getRefPropName() {
1625 return ServiceBindingUtils.AUTH_REF_PROP;