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.spi.AuthNContext;
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.DocumentHandler.Action;
69 import org.collectionspace.services.common.document.DocumentFilter;
70 import org.collectionspace.services.client.IRelationsManager;
71 import org.collectionspace.services.common.relation.RelationResource;
72 import org.collectionspace.services.common.repository.RepositoryClient;
73 import org.collectionspace.services.common.security.SecurityUtils;
74 import org.collectionspace.services.common.storage.jpa.JpaStorageUtils;
75 import org.collectionspace.services.common.api.CommonAPI;
76 import org.collectionspace.services.common.api.RefNameUtils;
77 import org.collectionspace.services.common.api.Tools;
78 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils;
79 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthRefConfigInfo;
80 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthorityItemSpecifier;
81 import org.collectionspace.services.config.service.DocHandlerParams;
82 import org.collectionspace.services.config.service.ListResultField;
83 import org.collectionspace.services.config.service.ObjectPartType;
84 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
85 import org.collectionspace.services.relation.RelationsCommon;
86 import org.collectionspace.services.relation.RelationsCommonList;
87 import org.collectionspace.services.relation.RelationsDocListItem;
88 import org.collectionspace.services.relation.RelationshipType;
89 import org.dom4j.Element;
90 import org.nuxeo.ecm.core.api.DocumentModel;
91 import org.nuxeo.ecm.core.api.DocumentModelList;
92 import org.nuxeo.ecm.core.api.DocumentNotFoundException;
93 import org.nuxeo.ecm.core.api.impl.DataModelImpl;
94 import org.nuxeo.ecm.core.api.model.DocumentPart;
95 import org.nuxeo.ecm.core.api.model.Property;
96 import org.nuxeo.ecm.core.api.model.PropertyException;
97 import org.nuxeo.ecm.core.api.model.impl.ScalarProperty;
98 import org.slf4j.Logger;
99 import org.slf4j.LoggerFactory;
102 * RemoteDocumentModelHandler
104 * $LastChangedRevision: $
105 * $LastChangedDate: $
109 public abstract class RemoteDocumentModelHandlerImpl<T, TL>
110 extends DocumentModelHandler<T, TL> {
113 private final Logger logger = LoggerFactory.getLogger(RemoteDocumentModelHandlerImpl.class);
114 private final static String CR = "\r\n";
115 private final static String EMPTYSTR = "";
116 private static final String COLLECTIONSPACE_CORE_SCHEMA = CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA;
117 private static final String ACCOUNT_PERMISSION_COMMON_PART_NAME = AccountClient.SERVICE_COMMON_PART_NAME;
120 * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#setServiceContext(org.collectionspace.services.common.context.ServiceContext)
123 public void setServiceContext(ServiceContext ctx) { //FIXME: Apply proper generics to ServiceContext<PoxPayloadIn, PoxPayloadOut>
124 if (ctx instanceof MultipartServiceContext) {
125 super.setServiceContext(ctx);
127 throw new IllegalArgumentException("setServiceContext requires instance of "
128 + MultipartServiceContext.class.getName());
133 protected String getRefnameDisplayName(DocumentWrapper<DocumentModel> docWrapper) {
134 return getRefnameDisplayName(docWrapper.getWrappedObject());
137 private String getRefnameDisplayName(DocumentModel docModel) { // Look in the tenant bindings to see what field should be our display name for our refname value
138 String result = null;
139 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
141 DocHandlerParams.Params params = null;
143 params = ServiceConfigUtils.getDocHandlerParams(ctx);
144 ListResultField field = params.getRefnameDisplayNameField();
146 String schema = field.getSchema();
147 if (schema == null || schema.trim().isEmpty()) {
148 schema = ctx.getCommonPartLabel();
151 result = getStringValue(docModel, schema, field);
152 } catch (Exception e) {
153 if (logger.isWarnEnabled()) {
154 logger.warn(String.format("Call failed to getRefnameDisplayName() for class %s", this.getClass().getName()));
162 public boolean supportsHierarchy() {
163 boolean result = false;
165 DocHandlerParams.Params params = null;
167 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
168 params = ServiceConfigUtils.getDocHandlerParams(ctx);
169 Boolean bool = params.isSupportsHierarchy();
171 result = bool.booleanValue();
173 } catch (DocumentException e) {
174 // TODO Auto-generated catch block
175 String errMsg = String.format("Could not get document handler params from config bindings for class %s", this.getClass().getName());
176 if (logger.isWarnEnabled() == true) {
185 public boolean supportsVersioning() {
186 boolean result = false;
188 DocHandlerParams.Params params = null;
190 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
191 params = ServiceConfigUtils.getDocHandlerParams(ctx);
192 Boolean bool = params.isSupportsVersioning();
194 result = bool.booleanValue();
196 } catch (DocumentException e) {
197 // TODO Auto-generated catch block
198 String errMsg = String.format("Could not get document handler params from config bindings for class %s", this.getClass().getName());
199 if (logger.isWarnEnabled() == true) {
208 public void handleWorkflowTransition(ServiceContext ctx, DocumentWrapper<DocumentModel> wrapDoc, TransitionDef transitionDef)
210 // Do nothing by default, but children can override if they want. The real workflow transition happens in the WorkflowDocumentModelHandler class
214 public void completeCreate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
215 super.completeCreate(wrapDoc);
216 if (supportsHierarchy() == true) {
217 handleRelationsPayload(wrapDoc, false);
222 public void handleUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
223 DocumentModel docModel = wrapDoc.getWrappedObject();
224 String workflowState = WorkflowClient.WORKFLOWSTATE_LOCKED;
226 if (docModel.getCurrentLifeCycleState().contains(workflowState) == true) {
227 throw new ServiceException(HttpURLConnection.HTTP_FORBIDDEN,
228 "Cannot UPDATE a resource/record if it is in the workflow state: " + workflowState);
230 super.handleUpdate(wrapDoc);
235 public boolean handleDelete(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
236 DocumentModel docModel = wrapDoc.getWrappedObject();
237 String workflowState = WorkflowClient.WORKFLOWSTATE_LOCKED;
239 if (docModel.getCurrentLifeCycleState().contains(workflowState) == true) {
240 throw new ServiceException(HttpURLConnection.HTTP_FORBIDDEN,
241 "Cannot DELETE a resource/record if it is in the workflow state: " + workflowState);
243 return super.handleDelete(wrapDoc);
247 /* NOTE: The authority item doc handler overrides (after calling) this method. It performs refName updates. In this
248 * method we just update any and all relationship records that use refNames that have changed.
250 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#completeUpdate(org.collectionspace.services.common.document.DocumentWrapper)
253 public void completeUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
254 DocumentModel docModel = wrapDoc.getWrappedObject();
256 String[] schemas = docModel.getDeclaredSchemas();
257 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
258 for (String schema : schemas) {
259 ObjectPartType partMeta = partsMetaMap.get(schema);
260 if (partMeta == null) {
261 continue; // unknown part, ignore
263 Map<String, Object> unQObjectProperties = extractPart(docModel, schema, partMeta);
264 if(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA.equals(schema)) {
265 addExtraCoreValues(docModel, unQObjectProperties);
267 addOutputPart(unQObjectProperties, schema, partMeta);
271 // If the resource's service supports hierarchy then we need to perform a little more work
273 if (supportsHierarchy() == true) {
274 handleRelationsPayload(wrapDoc, true); // refNames in relations payload should refer to pre-updated record refName value
275 handleRefNameReferencesUpdate(); // if our refName changed, we need to update any and all relationship records that used the old one
280 * Adds the output part.
282 * @param unQObjectProperties the un q object properties
283 * @param schema the schema
284 * @param partMeta the part meta
285 * @throws Exception the exception
286 * MediaType.APPLICATION_XML_TYPE
288 protected void addOutputPart(Map<String, Object> unQObjectProperties, String schema, ObjectPartType partMeta)
290 Element doc = DocumentUtils.buildDocument(partMeta, schema,
291 unQObjectProperties);
292 if (logger.isTraceEnabled() == true) {
293 logger.trace(doc.asXML());
295 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
296 ctx.addOutputPart(schema, doc, partMeta.getContent().getContentType());
300 * Extract paging info.
302 * @param commonsList the commons list
304 * @throws Exception the exception
306 public TL extractPagingInfo(TL theCommonList, DocumentWrapper<DocumentModelList> wrapDoc)
308 AbstractCommonList commonList = (AbstractCommonList) theCommonList;
310 DocumentFilter docFilter = this.getDocumentFilter();
311 long pageSize = docFilter.getPageSize();
312 long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
313 // set the page size and page number
314 commonList.setPageNum(pageNum);
315 commonList.setPageSize(pageSize);
316 DocumentModelList docList = wrapDoc.getWrappedObject();
317 // Set num of items in list. this is useful to our testing framework.
318 commonList.setItemsInPage(docList.size());
319 // set the total result size
320 commonList.setTotalItems(docList.totalSize());
322 return (TL) commonList;
326 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#extractAllParts(org.collectionspace.services.common.document.DocumentWrapper)
329 public void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc)
332 DocumentModel docModel = wrapDoc.getWrappedObject();
333 String[] schemas = docModel.getDeclaredSchemas();
334 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
335 for (String schema : schemas) {
336 ObjectPartType partMeta = partsMetaMap.get(schema);
337 if (partMeta == null) {
338 continue; // unknown part, ignore
340 Map<String, Object> unQObjectProperties = extractPart(docModel, schema, partMeta);
341 if(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA.equals(schema)) {
342 addExtraCoreValues(docModel, unQObjectProperties);
344 addOutputPart(unQObjectProperties, schema, partMeta);
347 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
349 if (supportsHierarchy() == true) {
350 String showSiblings = ctx.getQueryParams().getFirst(CommonAPI.showSiblings_QP);
351 if (Tools.isTrue(showSiblings)) {
352 showSiblings(wrapDoc, ctx);
353 return; // actual result is returned on ctx.addOutputPart();
356 String showRelations = ctx.getQueryParams().getFirst(CommonAPI.showRelations_QP);
357 if (Tools.isTrue(showRelations)) {
358 showRelations(wrapDoc, ctx);
359 return; // actual result is returned on ctx.addOutputPart();
362 String showAllRelations = ctx.getQueryParams().getFirst(CommonAPI.showAllRelations_QP);
363 if (Tools.isTrue(showAllRelations)) {
364 showAllRelations(wrapDoc, ctx);
365 return; // actual result is returned on ctx.addOutputPart();
369 String currentUser = ctx.getUserId();
370 if (currentUser.equalsIgnoreCase(AuthNContext.ANONYMOUS_USER) == false) {
371 addAccountPermissionsPart();
375 private void addExtraCoreValues(DocumentModel docModel, Map<String, Object> unQObjectProperties)
377 unQObjectProperties.put(CollectionSpaceClient.COLLECTIONSPACE_CORE_WORKFLOWSTATE, docModel.getCurrentLifeCycleState());
380 private void addAccountPermissionsPart() throws Exception {
381 Profiler profiler = new Profiler("addAccountPermissionsPart():", 1);
384 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
385 String currentServiceName = ctx.getServiceName();
386 String workflowSubResource = "/";
387 JaxRsContext jaxRsContext = ctx.getJaxRsContext();
388 if (jaxRsContext != null) { // If not null then we're dealing with an authority item
389 String resourceName = SecurityUtils.getResourceName(jaxRsContext.getUriInfo());
390 workflowSubResource = workflowSubResource + resourceName + WorkflowClient.SERVICE_PATH + "/";
392 workflowSubResource = workflowSubResource + currentServiceName + WorkflowClient.SERVICE_AUTHZ_SUFFIX;
394 AccountPermission accountPermission = JpaStorageUtils.getAccountPermissions(JpaStorageUtils.CS_CURRENT_USER,
395 currentServiceName, workflowSubResource);
396 org.collectionspace.services.authorization.ObjectFactory objectFactory =
397 new org.collectionspace.services.authorization.ObjectFactory();
398 JAXBElement<AccountPermission> ap = objectFactory.createAccountPermission(accountPermission);
399 PayloadOutputPart accountPermissionPart = new PayloadOutputPart("account_permission", ap); // REM - "account_permission" should be using a constant and not a literal
400 ctx.addOutputPart(accountPermissionPart);
406 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#fillAllParts(org.collectionspace.services.common.document.DocumentWrapper)
409 public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
411 //TODO filling extension parts should be dynamic
412 //Nuxeo APIs lack to support stream/byte[] input, get/setting properties is
413 //not an ideal way of populating objects.
414 DocumentModel docModel = wrapDoc.getWrappedObject();
415 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
416 PoxPayloadIn input = ctx.getInput();
417 if (input == null || input.getParts().isEmpty()) {
418 String msg = String.format("No payload found for '%s' action.", action);
419 logger.error(msg + "Ctx=" + getServiceContext().toString());
420 throw new BadRequestException(msg);
423 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
425 //iterate over parts received and fill those parts
426 boolean werePartsFilled = false;
427 List<PayloadInputPart> inputParts = input.getParts();
428 for (PayloadInputPart part : inputParts) {
430 String partLabel = part.getLabel();
431 if (partLabel == null) {
432 String msg = "Part label is missing or empty!";
433 logger.error(msg + "Ctx=" + getServiceContext().toString());
434 throw new BadRequestException(msg);
437 //skip if the part is not in metadata or if it is a system part
438 ObjectPartType partMeta = partsMetaMap.get(partLabel);
439 if (partMeta == null || isSystemPart(partLabel)) {
442 fillPart(part, docModel, partMeta, action, ctx);
443 werePartsFilled = true;
446 if (logger.isTraceEnabled() && werePartsFilled == false) {
447 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.",
453 private boolean isSystemPart(String partLabel) {
454 boolean result = false;
456 if (partLabel != null && (partLabel.equalsIgnoreCase(COLLECTIONSPACE_CORE_SCHEMA) ||
457 partLabel.equalsIgnoreCase(ACCOUNT_PERMISSION_COMMON_PART_NAME))) {
465 * fillPart fills an XML part into given document model
466 * @param part to fill
467 * @param docModel for the given object
468 * @param partMeta metadata for the object to fill
471 protected void fillPart(PayloadInputPart part, DocumentModel docModel, ObjectPartType partMeta, Action action,
472 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) throws Exception {
473 // check if this is an xml part
474 if (part.getMediaType().equals(MediaType.APPLICATION_XML_TYPE)) {
475 Element element = part.getElementBody();
476 Map<String, Object> objectProps = DocumentUtils.parseProperties(partMeta, element, ctx);
477 if (action == Action.UPDATE) {
478 this.filterReadOnlyPropertiesForPart(objectProps, partMeta);
480 String schemaName = partMeta.getLabel();
481 docModel.setProperties(schemaName, objectProps);
482 Set<String> fieldNameSet = getFieldNamesSet(objectProps);
483 setFieldsDirty(docModel, schemaName, fieldNameSet); // Force Nuxeo to update EVERY field by marking them dirty/modified
489 * Due to an apparent Nuxeo bug, we need to explicitly mark all fields as dirty for updates
490 * to take place properly. See https://issues.collectionspace.org/browse/CSPACE-7124
492 * We should improve this code by marking dirty/modified only the fields that were part of the UPDATE/PUT request
493 * payload. The current code will mark any field with a name in the 'fieldNameSet' set. Since field names do not
494 * have to be unique in document/record, we might end up unnecessarily marking some fields as modified -not ideal but
495 * better than brute-force marking ALL fields as modified.
500 private void setFieldsDirty(DocumentModel docModel, String schemaName, Set<String> fieldNameSet) {
501 DataModelImpl dataModel = (DataModelImpl) docModel.getDataModel(schemaName);
502 DocumentPart documentPart = dataModel.getDocumentPart();
503 setFieldsDirty(documentPart.getChildren(), fieldNameSet);
507 * Recursively step through all the children and sub-children setting their dirty flags.
508 * See https://issues.collectionspace.org/browse/CSPACE-7124
511 private void setFieldsDirty(Collection<Property> children, Set<String> fieldNameSet) {
512 if (children != null && (children.size() > 0)) {
513 for (Property prop : children ) {
514 String propName = prop.getName();
515 logger.debug(propName);
516 if (prop.isPhantom() == false) {
517 if (prop.isScalar() == false) {
518 setFieldsDirty(prop.getChildren(), fieldNameSet);
519 } else if (fieldNameSet.contains(propName)) {
520 ScalarProperty scalarProp = (ScalarProperty)prop;
521 scalarProp.setIsModified();
529 * Gets the set of field names used in a map of properties. We'll use this make our workaround to a
530 * bug (See https://issues.collectionspace.org/browse/CSPACE-7124) more efficient by only marking fields
531 * with names in this set as modified -ie, needing persisting.
533 * Since the map of properties can contain other maps of properties as values, there can be duplicate
534 * field names in the 'objectProps' map. And since we're creating a set, there can be no duplicates in the result.
537 private Set<String> getFieldNamesSet(Map<String, Object> objectProps) {
538 HashSet<String> result = new HashSet<String>();
540 addFieldNamesToSet(result, objectProps);
545 private void addFieldNamesToSet(Set<String> fieldNameSet, List<Object> valueList) {
546 for (Object value : valueList) {
547 if (value instanceof List) {
548 addFieldNamesToSet(fieldNameSet, (List<Object>)value);
549 } else if (value instanceof Map) {
550 addFieldNamesToSet(fieldNameSet, (Map<String, Object>)value);
552 logger.debug(value.toString());
557 private void addFieldNamesToSet(Set<String> fieldNameSet, Map<String, Object> objectProps) {
558 for (String key : objectProps.keySet()) {
559 Object value = objectProps.get(key);
560 if (value instanceof Map) {
561 addFieldNamesToSet(fieldNameSet, (Map<String, Object>)value);
562 } else if (value instanceof List) {
563 addFieldNamesToSet(fieldNameSet, (List)value);
565 fieldNameSet.add(key);
571 * Filters out read only properties, so they cannot be set on update.
572 * TODO: add configuration support to do this generally
573 * @param objectProps the properties parsed from the update payload
574 * @param partMeta metadata for the object to fill
576 public void filterReadOnlyPropertiesForPart(
577 Map<String, Object> objectProps, ObjectPartType partMeta) {
578 // Should add in logic to filter most of the core items on update
579 if(partMeta.getLabel().equalsIgnoreCase(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA)) {
580 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_AT);
581 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_BY);
582 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_URI);
583 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_TENANTID);
584 // Note that the updatedAt/updatedBy fields are set internally
585 // in DocumentModelHandler.handleCoreValues().
590 * extractPart extracts an XML object from given DocumentModel
592 * @param schema of the object to extract
593 * @param partMeta metadata for the object to extract
596 protected Map<String, Object> extractPart(DocumentModel docModel, String schema)
598 return extractPart(docModel, schema, (Map<String, Object>)null);
602 * extractPart extracts an XML object from given DocumentModel
604 * @param schema of the object to extract
605 * @param partMeta metadata for the object to extract
609 protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
611 return extractPart(docModel, schema, partMeta, null);
615 * extractPart extracts an XML object from given DocumentModel
617 * @param schema of the object to extract
618 * @param partMeta metadata for the object to extract
621 protected Map<String, Object> extractPart(
622 DocumentModel docModel,
624 Map<String, Object> addToMap)
626 Map<String, Object> result = null;
628 Map<String, Object> objectProps = docModel.getProperties(schema);
629 if (objectProps != null) {
630 //unqualify properties before sending the doc over the wire (to save bandwidh)
631 //FIXME: is there a better way to avoid duplication of a Map/Collection?
632 Map<String, Object> unQObjectProperties =
633 (addToMap != null) ? addToMap : (new HashMap<String, Object>());
634 Set<Entry<String, Object>> qualifiedEntries = objectProps.entrySet();
635 for (Entry<String, Object> entry : qualifiedEntries) {
636 String unqProp = getUnQProperty(entry.getKey());
637 unQObjectProperties.put(unqProp, entry.getValue());
639 result = unQObjectProperties;
646 * extractPart extracts an XML object from given DocumentModel
648 * @param schema of the object to extract
649 * @param partMeta metadata for the object to extract
653 protected Map<String, Object> extractPart(
654 DocumentModel docModel, String schema, ObjectPartType partMeta,
655 Map<String, Object> addToMap)
657 Map<String, Object> result = null;
659 result = this.extractPart(docModel, schema, addToMap);
665 public String getStringPropertyFromDoc(
668 String propertyXPath ) throws DocumentNotFoundException, DocumentException {
669 RepositoryInstance repoSession = null;
670 boolean releaseRepoSession = false;
671 String returnValue = null;
674 RepositoryClientImpl repoClient = (RepositoryClientImpl)this.getRepositoryClient(ctx);
675 repoSession = this.getRepositorySession();
676 if (repoSession == null) {
677 repoSession = repoClient.getRepositorySession();
678 releaseRepoSession = true;
682 DocumentWrapper<DocumentModel> wrapper = repoClient.getDoc(repoSession, ctx, csid);
683 DocumentModel docModel = wrapper.getWrappedObject();
684 returnValue = (String) docModel.getPropertyValue(propertyXPath);
685 } catch (PropertyException pe) {
687 } catch (DocumentException de) {
689 } catch (Exception e) {
690 if (logger.isDebugEnabled()) {
691 logger.debug("Caught exception ", e);
693 throw new DocumentException(e);
695 if (releaseRepoSession && repoSession != null) {
696 repoClient.releaseRepositorySession(repoSession);
699 } catch (Exception e) {
700 if (logger.isDebugEnabled()) {
701 logger.debug("Caught exception ", e);
703 throw new DocumentException(e);
707 if (logger.isWarnEnabled() == true) {
708 logger.warn("Returned DocumentModel instance was created with a repository session that is now closed.");
717 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#getAuthorityRefs(org.collectionspace.services.common.document.DocumentWrapper, java.util.List)
720 public AuthorityRefList getAuthorityRefs(
722 List<AuthRefConfigInfo> authRefConfigInfoList) throws PropertyException, Exception {
724 AuthorityRefList authRefList = new AuthorityRefList();
725 AbstractCommonList commonList = (AbstractCommonList) authRefList;
727 DocumentFilter docFilter = this.getDocumentFilter();
728 long pageSize = docFilter.getPageSize();
729 long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
730 // set the page size and page number
731 commonList.setPageNum(pageNum);
732 commonList.setPageSize(pageSize);
734 List<AuthorityRefList.AuthorityRefItem> list = authRefList.getAuthorityRefItem();
737 int iFirstToUse = (int)(pageSize*pageNum);
738 int nFoundInPage = 0;
741 ArrayList<RefNameServiceUtils.AuthRefInfo> foundReferences
742 = new ArrayList<RefNameServiceUtils.AuthRefInfo>();
744 boolean releaseRepoSession = false;
745 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
746 RepositoryClientImpl repoClient = (RepositoryClientImpl)this.getRepositoryClient(ctx);
747 CoreSessionInterface repoSession = this.getRepositorySession();
748 if (repoSession == null) {
749 repoSession = repoClient.getRepositorySession(ctx);
750 releaseRepoSession = true;
751 this.setRepositorySession(repoSession); // we (the doc handler) should keep track of this repository session in case we need it
755 DocumentModel docModel = repoClient.getDoc(repoSession, ctx, csid).getWrappedObject();
756 RefNameServiceUtils.findAuthRefPropertiesInDoc(docModel, authRefConfigInfoList, null, foundReferences);
757 // Slightly goofy pagination support - how many refs do we expect from one object?
758 for(RefNameServiceUtils.AuthRefInfo ari:foundReferences) {
759 if ((nFoundTotal >= iFirstToUse) && (nFoundInPage < pageSize)) {
760 if(appendToAuthRefsList(ari, list)) {
769 if (releaseRepoSession == true) {
770 repoClient.releaseRepositorySession(ctx, repoSession);
774 // Set num of items in list. this is useful to our testing framework.
775 commonList.setItemsInPage(nFoundInPage);
776 // set the total result size
777 commonList.setTotalItems(nFoundTotal);
779 } catch (PropertyException pe) {
780 String msg = "Attempted to retrieve value for invalid or missing authority field. "
781 + "Check authority field properties in tenant bindings.";
782 logger.warn(msg, pe);
784 } catch (Exception e) {
785 if (logger.isDebugEnabled()) {
786 logger.debug("Caught exception in getAuthorityRefs", e);
788 Response response = Response.status(
789 Response.Status.INTERNAL_SERVER_ERROR).entity(
790 "Failed to retrieve authority references").type(
791 "text/plain").build();
792 throw new CSWebApplicationException(e, response);
798 private boolean appendToAuthRefsList(RefNameServiceUtils.AuthRefInfo ari,
799 List<AuthorityRefList.AuthorityRefItem> list)
801 String fieldName = ari.getQualifiedDisplayName();
803 String refNameValue = (String)ari.getProperty().getValue();
804 AuthorityRefList.AuthorityRefItem item = authorityRefListItem(fieldName, refNameValue);
805 if(item!=null) { // ignore garbage values.
809 } catch(PropertyException pe) {
810 String msg = "PropertyException on: "+ari.getProperty().getPath()+pe.getLocalizedMessage();
811 if (logger.isDebugEnabled()) {
812 logger.debug(msg, pe);
821 * Fill in all the values to be returned in the authrefs payload for this item.
823 * @param authRefFieldName
827 private AuthorityRefList.AuthorityRefItem authorityRefListItem(String authRefFieldName, String refName) {
829 // Find the CSID for the authority item
833 DocumentModel docModel = NuxeoUtils.getDocModelForRefName(getServiceContext(), refName, getServiceContext().getResourceMap());
834 csid = NuxeoUtils.getCsid(docModel);
835 } catch (Exception e1) {
836 String msg = String.format("Could not find CSID for authority reference with refname = %s.", refName);
837 if (logger.isDebugEnabled()) {
838 logger.debug(msg, e1);
844 AuthorityRefList.AuthorityRefItem ilistItem = new AuthorityRefList.AuthorityRefItem();
846 RefNameUtils.AuthorityTermInfo termInfo = RefNameUtils.parseAuthorityTermInfo(refName);
847 if (Tools.isEmpty(csid) == false) {
848 ilistItem.setCsid(csid);
850 ilistItem.setRefName(refName);
851 ilistItem.setAuthDisplayName(termInfo.inAuthority.displayName);
852 ilistItem.setItemDisplayName(termInfo.displayName);
853 ilistItem.setSourceField(authRefFieldName);
854 ilistItem.setUri(termInfo.getRelativeUri());
855 } catch (Exception e) {
857 String msg = String.format("Trouble parsing refName from value: %s in field: %s. Error message: %s.",
858 refName, authRefFieldName, e.getLocalizedMessage());
859 if (logger.isDebugEnabled()) {
860 logger.debug(msg, e);
870 * Returns the primary value from a list of values.
872 * Assumes that the first value is the primary value.
873 * This assumption may change when and if the primary value
874 * is identified explicitly.
876 * @param values a list of values.
877 * @param propertyName the name of a property through
878 * which the value can be extracted.
879 * @return the primary value.
880 protected String primaryValueFromMultivalue(List<Object> values, String propertyName) {
881 String primaryValue = "";
882 if (values == null || values.size() == 0) {
885 Object value = values.get(0);
886 if (value instanceof String) {
888 primaryValue = (String) value;
890 // Multivalue group of fields
891 } else if (value instanceof Map) {
893 Map map = (Map) value;
894 if (map.values().size() > 0) {
895 if (map.get(propertyName) != null) {
896 primaryValue = (String) map.get(propertyName);
901 logger.warn("Unexpected type for property " + propertyName
902 + " in multivalue list: not String or Map.");
909 * Gets a simple property from the document.
911 * For completeness, as this duplicates DocumentModel method.
913 * @param docModel The document model to get info from
914 * @param schema The name of the schema (part)
915 * @param propertyName The simple scalar property type
916 * @return property value as String
918 protected String getSimpleStringProperty(DocumentModel docModel, String schema, String propName) {
919 String xpath = "/"+schema+":"+propName;
921 return (String) NuxeoUtils.getProperyValue(docModel, xpath); //docModel.getPropertyValue(xpath);
922 } catch(PropertyException pe) {
923 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a simple String property?"
924 +pe.getLocalizedMessage());
925 } catch(ClassCastException cce) {
926 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a scalar String property?"
927 +cce.getLocalizedMessage());
928 } catch(Exception e) {
929 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
930 +e.getLocalizedMessage());
935 * Gets first of a repeating list of scalar values, as a String, from the document.
937 * @param docModel The document model to get info from
938 * @param schema The name of the schema (part)
939 * @param listName The name of the scalar list property
940 * @return first value in list, as a String, or empty string if the list is empty
942 protected String getFirstRepeatingStringProperty(DocumentModel docModel,
943 String schema, String listName) {
944 String xpath = "/" + schema + ":" + listName + "/[0]";
946 return (String) NuxeoUtils.getProperyValue(docModel, xpath); // docModel.getPropertyValue(xpath);
947 } catch (PropertyException pe) {
948 throw new RuntimeException("Problem retrieving property {" + xpath
949 + "}. Not a repeating scalar?" + pe.getLocalizedMessage());
950 } catch (IndexOutOfBoundsException ioobe) {
951 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
952 return ""; // gracefully handle missing elements
953 } catch (ClassCastException cce) {
954 throw new RuntimeException("Problem retrieving property {" + xpath
955 + "} as String. Not a repeating String property?"
956 + cce.getLocalizedMessage());
957 } catch (Exception e) {
958 throw new RuntimeException("Unknown problem retrieving property {"
959 + xpath + "}." + e.getLocalizedMessage());
964 * Gets first of a repeating list of scalar values, as a String, from the document.
966 * @param docModel The document model to get info from
967 * @param schema The name of the schema (part)
968 * @param listName The name of the scalar list property
969 * @return first value in list, as a String, or empty string if the list is empty
971 protected String getStringValueInPrimaryRepeatingComplexProperty(
972 DocumentModel docModel, String schema, String complexPropertyName, String fieldName) {
973 String result = null;
975 String xpath = "/" + NuxeoUtils.getPrimaryXPathPropertyName(schema, complexPropertyName, fieldName);
977 result = (String) NuxeoUtils.getProperyValue(docModel, xpath); //docModel.getPropertyValue(xpath);
978 } catch(PropertyException pe) {
979 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad propertyNames?"
980 +pe.getLocalizedMessage());
981 } catch(IndexOutOfBoundsException ioobe) {
982 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
983 result = ""; // gracefully handle missing elements
984 } catch(ClassCastException cce) {
985 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
986 +cce.getLocalizedMessage());
987 } catch(NullPointerException npe) {
988 // Getting here because of a bug in Nuxeo when value in repository is unknown/empty/null
989 logger.warn(String.format("Nuxeo repo unexpectedly returned an Null Pointer Exception when asked for the value of {%s}.",
991 } catch(Exception e) {
992 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
993 +e.getLocalizedMessage());
1000 * Gets XPath value from schema. Note that only "/" and "[n]" are
1001 * supported for xpath. Can omit grouping elements for repeating complex types,
1002 * e.g., "fieldList/[0]" can be used as shorthand for "fieldList/field[0]" and
1003 * "fieldGroupList/[0]/field" can be used as shorthand for "fieldGroupList/fieldGroup[0]/field".
1004 * If there are no entries for a list of scalars or for a list of complex types,
1005 * a 0 index expression (e.g., "fieldGroupList/[0]/field") will safely return an empty
1006 * string. A non-zero index will throw an IndexOutOfBoundsException if there are not
1007 * that many elements in the list.
1008 * N.B.: This does not follow the XPath spec - indices are 0-based, not 1-based.
1010 * @param docModel The document model to get info from
1011 * @param schema The name of the schema (part)
1012 * @param xpath The XPath expression (without schema prefix)
1013 * @return value the indicated property value as a String
1014 * @throws DocumentException
1016 protected Object getListResultValue(DocumentModel docModel, // REM - CSPACE-5133
1017 String schema, ListResultField field) throws DocumentException {
1018 Object result = null;
1020 result = NuxeoUtils.getXPathValue(docModel, schema, field.getXpath());
1025 protected String getStringValue(DocumentModel docModel,
1026 String schema, ListResultField field) throws DocumentException {
1027 String result = null;
1029 Object value = getListResultValue(docModel, schema, field);
1030 if (value != null && value instanceof String) {
1031 String strValue = (String) value;
1032 if (strValue.trim().isEmpty() == false) {
1040 protected void removeFromList(List<RelationsCommonList.RelationListItem> list, RelationsCommonList.RelationListItem item) {
1044 private void itemToString(StringBuilder sb, String prefix, RelationsCommonList.RelationListItem item ) {
1046 sb.append((item.getCsid()!= null)?item.getCsid():"NO CSID");
1048 sb.append((item.getSubject().getCsid()!=null)?item.getSubject().getCsid():item.getSubject().getRefName());
1050 sb.append(item.getPredicate());
1052 sb.append((item.getObject().getCsid()!=null)?item.getObject().getCsid():item.getObject().getRefName());
1056 private String dumpList(List<RelationsCommonList.RelationListItem> list, String label) {
1057 StringBuilder sb = new StringBuilder();
1059 if (list.size() > 0) {
1060 sb.append("=========== " + label + " ==========" + CR);
1062 for (RelationsCommonList.RelationListItem item : list) {
1063 itemToString(sb, "== ", item);
1066 return sb.toString();
1069 /** @return null on parent not found
1071 protected String getParentCSID(String thisCSID) throws Exception {
1072 String parentCSID = null;
1074 String predicate = RelationshipType.HAS_BROADER.value();
1075 RelationsCommonList parentListOuter = getRelations(thisCSID, null, predicate);
1076 List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
1077 if (parentList != null) {
1078 if (parentList.size() == 0) {
1081 RelationsCommonList.RelationListItem relationListItem = parentList.get(0);
1082 parentCSID = relationListItem.getObjectCsid();
1085 } catch (Exception e) {
1086 logger.error("Could not find parent for this: " + thisCSID, e);
1091 protected List<RelationsCommonList.RelationListItem> newRelationsCommonList() {
1092 List<RelationsCommonList.RelationListItem> result = new ArrayList<RelationsCommonList.RelationListItem>();
1096 public void showSiblings(DocumentWrapper<DocumentModel> wrapDoc,
1097 MultipartServiceContext ctx) throws Exception {
1098 String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
1099 String parentCSID = getParentCSID(thisCSID);
1100 if (parentCSID == null) {
1101 logger.warn("~~~~~\r\n~~~~ Could not find parent for this: " + thisCSID);
1105 String predicate = RelationshipType.HAS_BROADER.value();
1106 RelationsCommonList siblingListOuter = getRelations(null, parentCSID, predicate);
1107 List<RelationsCommonList.RelationListItem> siblingList = siblingListOuter.getRelationListItem();
1109 List<RelationsCommonList.RelationListItem> toRemoveList = newRelationsCommonList();
1112 RelationsCommonList.RelationListItem item = null;
1113 for (RelationsCommonList.RelationListItem sibling : siblingList) {
1114 if (thisCSID.equals(sibling.getSubjectCsid())) {
1115 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.
1118 //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.
1119 for (RelationsCommonList.RelationListItem self : toRemoveList) {
1120 removeFromList(siblingList, self);
1123 long siblingSize = siblingList.size();
1124 siblingListOuter.setTotalItems(siblingSize);
1125 siblingListOuter.setItemsInPage(siblingSize);
1126 if(logger.isTraceEnabled()) {
1127 String dump = dumpList(siblingList, "Siblings of: "+thisCSID);
1128 logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showSiblings ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1131 PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, siblingListOuter);
1132 ctx.addOutputPart(relationsPart);
1135 public void showRelations(DocumentWrapper<DocumentModel> wrapDoc,
1136 MultipartServiceContext ctx) throws Exception {
1137 String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
1139 String predicate = RelationshipType.HAS_BROADER.value();
1140 RelationsCommonList parentListOuter = getRelations(thisCSID, null, predicate);
1141 List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
1143 RelationsCommonList childrenListOuter = getRelations(null, thisCSID, predicate);
1144 List<RelationsCommonList.RelationListItem> childrenList = childrenListOuter.getRelationListItem();
1146 if(logger.isTraceEnabled()) {
1147 String dump = dumpLists(thisCSID, parentList, childrenList, null);
1148 logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showRelations ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1151 //Assume that there are more children than parents. Will be true for parent/child, but maybe not for other relations.
1152 //Now add all parents to our childrenList, to be able to return just one list of consolidated results.
1153 //Not optimal, but that's the current design spec.
1155 for (RelationsCommonList.RelationListItem parent : parentList) {
1156 childrenList.add(parent);
1159 long childrenSize = childrenList.size();
1160 childrenListOuter.setTotalItems(childrenSize);
1161 childrenListOuter.setItemsInPage(childrenListOuter.getItemsInPage() + added);
1163 PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, childrenListOuter);
1164 ctx.addOutputPart(relationsPart);
1167 public void showAllRelations(DocumentWrapper<DocumentModel> wrapDoc, MultipartServiceContext ctx) throws Exception {
1168 String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
1170 RelationsCommonList subjectListOuter = getRelations(thisCSID, null, null); // nulls are wildcards: predicate=*, and object=*
1171 List<RelationsCommonList.RelationListItem> subjectList = subjectListOuter.getRelationListItem();
1173 RelationsCommonList objectListOuter = getRelations(null, thisCSID, null); // nulls are wildcards: subject=*, and predicate=*
1174 List<RelationsCommonList.RelationListItem> objectList = objectListOuter.getRelationListItem();
1176 if(logger.isTraceEnabled()) {
1177 String dump = dumpLists(thisCSID, subjectList, objectList, null);
1178 logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showAllRelations ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1181 subjectList.addAll(objectList);
1183 //now subjectList actually has records BOTH where thisCSID is subject and object.
1184 long relatedSize = subjectList.size();
1185 subjectListOuter.setTotalItems(relatedSize);
1186 subjectListOuter.setItemsInPage(relatedSize);
1188 PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, subjectListOuter);
1189 ctx.addOutputPart(relationsPart);
1192 private String dumpLists(String itemCSID,
1193 List<RelationsCommonList.RelationListItem> parentList,
1194 List<RelationsCommonList.RelationListItem> childList,
1195 List<RelationsCommonList.RelationListItem> actionList) {
1196 StringBuilder sb = new StringBuilder();
1197 sb.append("itemCSID: " + itemCSID + CR);
1198 if(parentList!=null) {
1199 sb.append(dumpList(parentList, "parentList"));
1201 if(childList!=null) {
1202 sb.append(dumpList(childList, "childList"));
1204 if(actionList!=null) {
1205 sb.append(dumpList(actionList, "actionList"));
1207 return sb.toString();
1210 //================= TODO: move this to common, refactoring this and CollectionObjectResource.java
1211 public RelationsCommonList getRelations(String subjectCSID, String objectCSID, String predicate) throws Exception {
1212 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1213 MultivaluedMap<String, String> queryParams = ctx.getQueryParams();
1214 queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1215 queryParams.putSingle(IRelationsManager.SUBJECT_QP, subjectCSID);
1216 queryParams.putSingle(IRelationsManager.OBJECT_QP, objectCSID);
1218 RelationResource relationResource = new RelationResource(); //is this still acting like a singleton as it should be?
1219 RelationsCommonList relationsCommonList = relationResource.getList(ctx);
1220 return relationsCommonList;
1222 //============================= END TODO refactor ==========================
1224 // this method calls the RelationResource to have it create the relations and persist them.
1225 private void createRelations(List<RelationsCommonList.RelationListItem> inboundList,
1226 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) throws Exception {
1227 for (RelationsCommonList.RelationListItem item : inboundList) {
1228 RelationsCommon rc = new RelationsCommon();
1229 //rc.setCsid(item.getCsid());
1230 //todo: assignTo(item, rc);
1231 RelationsDocListItem itemSubject = item.getSubject();
1232 RelationsDocListItem itemObject = item.getObject();
1234 // Set at least one of CSID and refName for Subject and Object
1235 // Either value might be null for for each of Subject and Object
1236 String subjectCsid = itemSubject.getCsid();
1237 rc.setSubjectCsid(subjectCsid);
1239 String objCsid = itemObject.getCsid();
1240 rc.setObjectCsid(objCsid);
1242 rc.setSubjectRefName(itemSubject.getRefName());
1243 rc.setObjectRefName(itemObject.getRefName());
1245 rc.setRelationshipType(item.getPredicate());
1246 rc.setRelationshipMetaType(item.getRelationshipMetaType());
1247 //RelationshipType foo = (RelationshipType.valueOf(item.getPredicate())) ;
1248 //rc.setPredicate(foo); //this must be one of the type found in the enum in services/jaxb/src/main/resources/relations_common.xsd
1250 // This is superfluous, since it will be fetched by the Relations Create logic.
1251 rc.setSubjectDocumentType(itemSubject.getDocumentType());
1252 rc.setObjectDocumentType(itemObject.getDocumentType());
1254 // This is superfluous, since it will be fetched by the Relations Create logic.
1255 rc.setSubjectUri(itemSubject.getUri());
1256 rc.setObjectUri(itemObject.getUri());
1257 // May not have the info here. Only really require CSID or refName.
1258 // Rest is handled in the Relation create mechanism
1259 //uriPointsToSameAuthority(itemSubject.getUri(), itemObject.getUri());
1261 PoxPayloadOut payloadOut = new PoxPayloadOut(RelationClient.SERVICE_PAYLOAD_NAME);
1262 PayloadOutputPart outputPart = new PayloadOutputPart(RelationClient.SERVICE_COMMONPART_NAME, rc);
1263 payloadOut.addPart(outputPart);
1264 RelationResource relationResource = new RelationResource();
1265 Response res = relationResource.create(ctx, ctx.getResourceMap(),
1266 ctx.getUriInfo(), payloadOut.toXML()); //NOTE ui recycled from above to pass in unknown query params.
1270 // Note that item2 may be sparse (only refName, no CSID for subject or object)
1271 // But item1 must not be sparse
1272 private boolean itemsEqual(RelationsCommonList.RelationListItem item1, RelationsCommonList.RelationListItem item2) {
1273 if (item1 == null || item2 == null) {
1276 RelationsDocListItem subj1 = item1.getSubject();
1277 RelationsDocListItem subj2 = item2.getSubject();
1278 RelationsDocListItem obj1 = item1.getObject();
1279 RelationsDocListItem obj2 = item2.getObject();
1281 String subj1Csid = subj1.getCsid();
1282 String subj2Csid = subj2.getCsid();
1283 String subj1RefName = subj1.getRefName();
1284 String subj2RefName = subj2.getRefName();
1286 String obj1Csid = obj1.getCsid();
1287 String obj2Csid = obj2.getCsid();
1288 String obj1RefName = obj1.getRefName();
1289 String obj2RefName = obj2.getRefName();
1291 String item1Metatype = item1.getRelationshipMetaType();
1292 item1Metatype = item1Metatype != null ? item1Metatype : EMPTYSTR;
1294 String item2Metatype = item2.getRelationshipMetaType();
1295 item2Metatype = item2Metatype != null ? item2Metatype : EMPTYSTR;
1297 boolean isEqual = (subj1Csid.equals(subj2Csid) || ((subj2Csid==null) && subj1RefName.equals(subj2RefName)))
1298 && (obj1Csid.equals(obj2Csid) || ((obj2Csid==null) && obj1RefName.equals(obj2RefName)))
1299 // predicate is proper, but still allow relationshipType
1300 && (item1.getPredicate().equals(item2.getPredicate())
1301 || ((item2.getPredicate()==null) && item1.getRelationshipType().equals(item2.getRelationshipType())))
1302 // Allow missing docTypes, so long as they do not conflict
1303 && (obj1.getDocumentType().equals(obj2.getDocumentType()) || obj2.getDocumentType()==null)
1304 && (subj1.getDocumentType().equals(subj2.getDocumentType()) || subj2.getDocumentType()==null)
1305 && (item1Metatype.equalsIgnoreCase(item2Metatype));
1309 // Note that the item argument may be sparse (only refName, no CSID for subject or object)
1310 // But the list items must not be sparse
1311 private RelationsCommonList.RelationListItem findInList(
1312 List<RelationsCommonList.RelationListItem> list,
1313 RelationsCommonList.RelationListItem item) {
1314 RelationsCommonList.RelationListItem foundItem = null;
1315 for (RelationsCommonList.RelationListItem listItem : list) {
1316 if (itemsEqual(listItem, item)) { //equals must be defined, else
1317 foundItem = listItem;
1324 /** updateRelations strategy:
1327 go through inboundList, remove anything from childList that matches from childList
1328 go through inboundList, remove anything from parentList that matches from parentList
1329 go through parentList, delete all remaining
1330 go through childList, delete all remaining
1331 go through actionList, add all remaining.
1332 check for duplicate children
1333 check for more than one parent.
1335 inboundList parentList childList actionList
1336 ---------------- --------------- ---------------- ----------------
1337 child-a parent-c child-a child-b
1338 child-b parent-d child-c
1343 private RelationsCommonList updateRelations(
1344 String itemCSID, PoxPayloadIn input, DocumentWrapper<DocumentModel> wrapDoc, boolean forUpdate)
1346 if (logger.isTraceEnabled()) {
1347 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID);
1349 PayloadInputPart part = input.getPart(RelationClient.SERVICE_COMMON_LIST_NAME); //input.getPart("relations_common");
1351 return null; //nothing to do--they didn't send a list of relations.
1353 RelationsCommonList relationsCommonListBody = (RelationsCommonList) part.getBody();
1354 List<RelationsCommonList.RelationListItem> inboundList = relationsCommonListBody.getRelationListItem();
1355 List<RelationsCommonList.RelationListItem> actionList = newRelationsCommonList();
1356 List<RelationsCommonList.RelationListItem> childList = null;
1357 List<RelationsCommonList.RelationListItem> parentList = null;
1358 DocumentModel docModel = wrapDoc.getWrappedObject();
1359 String itemRefName = (String) docModel.getProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
1360 CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME);
1362 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1363 //Do magic replacement of ${itemCSID} and fix URI's.
1364 fixupInboundListItems(ctx, inboundList, docModel, itemCSID);
1366 final String HAS_BROADER = RelationshipType.HAS_BROADER.value();
1367 UriInfo uriInfo = ctx.getUriInfo();
1368 MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
1371 //Run getList() once as sent to get childListOuter:
1372 String predicate = RelationshipType.HAS_BROADER.value();
1373 queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1374 queryParams.putSingle(IRelationsManager.SUBJECT_QP, null);
1375 queryParams.putSingle(IRelationsManager.SUBJECT_TYPE_QP, null);
1376 queryParams.putSingle(IRelationsManager.OBJECT_QP, itemCSID);
1377 queryParams.putSingle(IRelationsManager.OBJECT_TYPE_QP, null);
1379 RelationResource relationResource = new RelationResource();
1380 RelationsCommonList childListOuter = relationResource.getList(ctx); // Knows all query params because they are in the context.
1382 //Now run getList() again, leaving predicate, swapping subject and object, to get parentListOuter.
1383 queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1384 queryParams.putSingle(IRelationsManager.SUBJECT_QP, itemCSID);
1385 queryParams.putSingle(IRelationsManager.OBJECT_QP, null);
1386 RelationsCommonList parentListOuter = relationResource.getList(ctx);
1389 childList = childListOuter.getRelationListItem();
1390 parentList = parentListOuter.getRelationListItem();
1392 if (parentList.size() > 1) {
1393 throw new Exception("Too many parents for object: " + itemCSID + " list: " + dumpList(parentList, "parentList"));
1396 if (logger.isTraceEnabled()) {
1397 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " got existing relations.");
1401 for (RelationsCommonList.RelationListItem inboundItem : inboundList) {
1402 // Note that the relations may specify the other (non-item) bit with a refName, not a CSID,
1403 // and so the CSID for those may be null
1404 if(inboundItem.getPredicate().equals(HAS_BROADER)) {
1405 // Look for parents and children
1406 if(itemCSID.equals(inboundItem.getObject().getCsid())
1407 || itemRefName.equals(inboundItem.getObject().getRefName())) {
1408 //then this is an item that says we have a child. That child is inboundItem
1409 RelationsCommonList.RelationListItem childItem =
1410 (childList == null) ? null : findInList(childList, inboundItem);
1411 if (childItem != null) {
1412 if (logger.isTraceEnabled()) {
1413 StringBuilder sb = new StringBuilder();
1414 itemToString(sb, "== Child: ", childItem);
1415 logger.trace("Found inboundChild in current child list: " + sb.toString());
1417 removeFromList(childList, childItem); //exists, just take it off delete list
1419 if (logger.isTraceEnabled()) {
1420 StringBuilder sb = new StringBuilder();
1421 itemToString(sb, "== Child: ", inboundItem);
1422 logger.trace("inboundChild not in current child list, will add: " + sb.toString());
1424 actionList.add(inboundItem); //doesn't exist as a child, but is a child. Add to additions list
1425 String newChildCsid = inboundItem.getSubject().getCsid();
1426 if(newChildCsid == null) {
1427 String newChildRefName = inboundItem.getSubject().getRefName();
1428 if (newChildRefName == null) {
1429 throw new RuntimeException("Child with no CSID or refName!");
1431 if (logger.isTraceEnabled()) {
1432 logger.trace("Fetching CSID for child with only refname: "+newChildRefName);
1434 DocumentModel newChildDocModel =
1435 NuxeoBasedResource.getDocModelForRefName(getServiceContext(),
1436 newChildRefName, getServiceContext().getResourceMap());
1437 newChildCsid = getCsid(newChildDocModel);
1439 ensureChildHasNoOtherParents(ctx, queryParams, newChildCsid);
1442 } else if (itemCSID.equals(inboundItem.getSubject().getCsid())
1443 || itemRefName.equals(inboundItem.getSubject().getRefName())) {
1444 //then this is an item that says we have a parent. inboundItem is that parent.
1445 RelationsCommonList.RelationListItem parentItem =
1446 (parentList == null) ? null : findInList(parentList, inboundItem);
1447 if (parentItem != null) {
1448 removeFromList(parentList, parentItem); //exists, just take it off delete list
1450 actionList.add(inboundItem); //doesn't exist as a parent, but is a parent. Add to additions list
1453 logger.error("Parent/Child Element didn't link to this item. inboundItem: " + inboundItem);
1456 logger.warn("Non-parent relation ignored. inboundItem: " + inboundItem);
1459 if (logger.isTraceEnabled()) {
1460 String dump = dumpLists(itemCSID, parentList, childList, actionList);
1461 logger.trace("~~~~~~~~~~~~~~~~~~~~~~dump~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1464 if (logger.isTraceEnabled()) {
1465 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " deleting "
1466 + parentList.size() + " existing parents and " + childList.size() + " existing children.");
1468 deleteRelations(parentList, ctx, "parentList"); //todo: there are items appearing on both lists....april 20.
1469 deleteRelations(childList, ctx, "childList");
1471 if (logger.isTraceEnabled()) {
1472 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " adding "
1473 + actionList.size() + " new parents and children.");
1475 createRelations(actionList, ctx);
1476 if (logger.isTraceEnabled()) {
1477 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " done.");
1479 // We return all elements on the inbound list, since we have just worked to make them exist in the system
1480 // and be non-redundant, etc. That list came from relationsCommonListBody, so it is still attached to it, just pass that back.
1481 return relationsCommonListBody;
1484 /** Performs substitution for ${itemCSID} (see CommonAPI.AuthorityItemCSID_REPLACE for constant)
1485 * and sets URI correctly for related items.
1486 * Operates directly on the items in the list. Does not change the list ordering, does not add or remove any items.
1488 protected void fixupInboundListItems(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1489 List<RelationsCommonList.RelationListItem> inboundList,
1490 DocumentModel docModel,
1491 String itemCSID) throws Exception {
1492 String thisURI = this.getUri(docModel);
1493 // WARNING: the two code blocks below are almost identical and seem to ask to be put in a generic method.
1494 // beware of the little diffs in inboundItem.setObjectCsid(itemCSID); and inboundItem.setSubjectCsid(itemCSID); in the two blocks.
1495 for (RelationsCommonList.RelationListItem inboundItem : inboundList) {
1496 RelationsDocListItem inboundItemObject = inboundItem.getObject();
1497 RelationsDocListItem inboundItemSubject = inboundItem.getSubject();
1499 if (CommonAPI.AuthorityItemCSID_REPLACE.equalsIgnoreCase(inboundItemObject.getCsid())) {
1500 inboundItem.setObjectCsid(itemCSID);
1501 inboundItemObject.setCsid(itemCSID);
1502 //inboundItemObject.setUri(getUri(docModel));
1505 String objectCsid = inboundItemObject.getCsid();
1506 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(getRepositorySession(), ctx, objectCsid); //null if not found.
1507 DocumentWrapper wrapper = new DocumentWrapperImpl(itemDocModel);
1508 String uri = this.getRepositoryClient(ctx).getDocURI(wrapper);
1509 inboundItemObject.setUri(uri); //CSPACE-4037
1512 //uriPointsToSameAuthority(thisURI, inboundItemObject.getUri()); //CSPACE-4042
1514 if (CommonAPI.AuthorityItemCSID_REPLACE.equalsIgnoreCase(inboundItemSubject.getCsid())) {
1515 inboundItem.setSubjectCsid(itemCSID);
1516 inboundItemSubject.setCsid(itemCSID);
1517 //inboundItemSubject.setUri(getUri(docModel));
1520 String subjectCsid = inboundItemSubject.getCsid();
1521 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(getRepositorySession(), ctx, subjectCsid); //null if not found.
1522 DocumentWrapper wrapper = new DocumentWrapperImpl(itemDocModel);
1523 String uri = this.getRepositoryClient(ctx).getDocURI(wrapper);
1524 inboundItemSubject.setUri(uri); //CSPACE-4037
1527 //uriPointsToSameAuthority(thisURI, inboundItemSubject.getUri()); //CSPACE-4042
1532 private void ensureChildHasNoOtherParents(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1533 MultivaluedMap<String, String> queryParams, String childCSID) {
1534 logger.trace("ensureChildHasNoOtherParents for: " + childCSID );
1535 queryParams.putSingle(IRelationsManager.SUBJECT_QP, childCSID);
1536 queryParams.putSingle(IRelationsManager.PREDICATE_QP, RelationshipType.HAS_BROADER.value());
1537 queryParams.putSingle(IRelationsManager.OBJECT_QP, null); //null means ANY
1539 RelationResource relationResource = new RelationResource();
1540 RelationsCommonList parentListOuter = relationResource.getList(ctx);
1541 List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
1542 //logger.warn("ensureChildHasNoOtherParents preparing to delete relations on "+childCSID+"\'s parent list: \r\n"+dumpList(parentList, "duplicate parent list"));
1543 deleteRelations(parentList, ctx, "parentList-delete");
1546 private void deleteRelations(List<RelationsCommonList.RelationListItem> list,
1547 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1550 for (RelationsCommonList.RelationListItem item : list) {
1551 RelationResource relationResource = new RelationResource();
1552 if(logger.isTraceEnabled()) {
1553 StringBuilder sb = new StringBuilder();
1554 itemToString(sb, "==== TO DELETE: ", item);
1555 logger.trace(sb.toString());
1557 Response res = relationResource.deleteWithParentCtx(ctx, item.getCsid());
1558 if (logger.isDebugEnabled()) {
1559 logger.debug("Status of authority item deleteRelations method call was: " + res.getStatus());
1562 } catch (Throwable t) {
1563 String msg = "Unable to deleteRelations: " + Tools.errorToString(t, true);
1568 // Note that we must do this after we have completed the Update, so that the repository has the
1569 // info for the item itself. The relations code must call into the repo to get info for each end.
1570 // This could be optimized to pass in the parent docModel, since it will often be one end.
1571 // Nevertheless, we should complete the item save before we do work on the relations, especially
1572 // since a save on Create might fail, and we would not want to create relations for something
1573 // that may not be created...
1574 private void handleRelationsPayload(DocumentWrapper<DocumentModel> wrapDoc, boolean forUpdate) throws Exception {
1575 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1576 PoxPayloadIn input = ctx.getInput();
1577 DocumentModel documentModel = (wrapDoc.getWrappedObject());
1578 String itemCsid = documentModel.getName();
1580 //Updates relations part
1581 RelationsCommonList relationsCommonList = updateRelations(itemCsid, input, wrapDoc, forUpdate);
1583 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
1584 ctx.setProperty(RelationClient.SERVICE_COMMON_LIST_NAME, payloadOutputPart);
1586 //now we add part for relations list
1587 //ServiceContext ctx = getServiceContext();
1588 //PayloadOutputPart foo = (PayloadOutputPart) ctx.getProperty(RelationClient.SERVICE_COMMON_LIST_NAME);
1589 ctx.getOutput().addPart(payloadOutputPart);
1593 * Checks to see if the refName has changed, and if so,
1594 * uses utilities to find all references and update them to use the new refName.
1597 protected void handleRefNameReferencesUpdate() throws Exception {
1598 if (hasRefNameUpdate() == true) {
1599 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1600 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient = getRepositoryClient(ctx);
1601 CoreSessionInterface repoSession = this.getRepositorySession();
1603 // Update all the relationship records that referred to the old refName
1604 RefNameServiceUtils.updateRefNamesInRelations(ctx, repoClient, repoSession,
1605 oldRefNameOnUpdate, newRefNameOnUpdate);
1609 protected String getRefNameUpdate() {
1610 String result = null;
1612 if (hasRefNameUpdate() == true) {
1613 result = newRefNameOnUpdate;
1614 if (logger.isDebugEnabled() == true) {
1615 logger.debug(String.format("There was a refName update. New: %s Old: %s" ,
1616 newRefNameOnUpdate, oldRefNameOnUpdate));
1624 * Note: The Vocabulary document handler overrides this method.
1626 protected String getRefPropName() {
1627 return ServiceBindingUtils.AUTH_REF_PROP;