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 currentUser.equalsIgnoreCase(AuthN.SPRING_ADMIN_USER) == false) {
370 addAccountPermissionsPart();
374 private void addExtraCoreValues(DocumentModel docModel, Map<String, Object> unQObjectProperties)
376 unQObjectProperties.put(CollectionSpaceClient.COLLECTIONSPACE_CORE_WORKFLOWSTATE, docModel.getCurrentLifeCycleState());
379 private void addAccountPermissionsPart() throws Exception {
380 Profiler profiler = new Profiler("addAccountPermissionsPart():", 1);
383 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
384 String currentServiceName = ctx.getServiceName();
385 String workflowSubResource = "/";
386 JaxRsContext jaxRsContext = ctx.getJaxRsContext();
387 if (jaxRsContext != null) { // If not null then we're dealing with an authority item
388 String resourceName = SecurityUtils.getResourceName(jaxRsContext.getUriInfo());
389 workflowSubResource = workflowSubResource + resourceName + WorkflowClient.SERVICE_PATH + "/";
391 workflowSubResource = workflowSubResource + currentServiceName + WorkflowClient.SERVICE_AUTHZ_SUFFIX;
393 AccountPermission accountPermission = JpaStorageUtils.getAccountPermissions(JpaStorageUtils.CS_CURRENT_USER,
394 currentServiceName, workflowSubResource);
395 org.collectionspace.services.authorization.ObjectFactory objectFactory =
396 new org.collectionspace.services.authorization.ObjectFactory();
397 JAXBElement<AccountPermission> ap = objectFactory.createAccountPermission(accountPermission);
398 PayloadOutputPart accountPermissionPart = new PayloadOutputPart("account_permission", ap); // REM - "account_permission" should be using a constant and not a literal
399 ctx.addOutputPart(accountPermissionPart);
405 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#fillAllParts(org.collectionspace.services.common.document.DocumentWrapper)
408 public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
410 //TODO filling extension parts should be dynamic
411 //Nuxeo APIs lack to support stream/byte[] input, get/setting properties is
412 //not an ideal way of populating objects.
413 DocumentModel docModel = wrapDoc.getWrappedObject();
414 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
415 PoxPayloadIn input = ctx.getInput();
416 if (input == null || input.getParts().isEmpty()) {
417 String msg = String.format("No payload found for '%s' action.", action);
418 logger.error(msg + "Ctx=" + getServiceContext().toString());
419 throw new BadRequestException(msg);
422 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
424 //iterate over parts received and fill those parts
425 boolean werePartsFilled = false;
426 List<PayloadInputPart> inputParts = input.getParts();
427 for (PayloadInputPart part : inputParts) {
429 String partLabel = part.getLabel();
430 if (partLabel == null) {
431 String msg = "Part label is missing or empty!";
432 logger.error(msg + "Ctx=" + getServiceContext().toString());
433 throw new BadRequestException(msg);
436 //skip if the part is not in metadata or if it is a system part
437 ObjectPartType partMeta = partsMetaMap.get(partLabel);
438 if (partMeta == null || isSystemPart(partLabel)) {
441 fillPart(part, docModel, partMeta, action, ctx);
442 werePartsFilled = true;
445 if (logger.isTraceEnabled() && werePartsFilled == false) {
446 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.",
452 private boolean isSystemPart(String partLabel) {
453 boolean result = false;
455 if (partLabel != null && (partLabel.equalsIgnoreCase(COLLECTIONSPACE_CORE_SCHEMA) ||
456 partLabel.equalsIgnoreCase(ACCOUNT_PERMISSION_COMMON_PART_NAME))) {
464 * fillPart fills an XML part into given document model
465 * @param part to fill
466 * @param docModel for the given object
467 * @param partMeta metadata for the object to fill
470 protected void fillPart(PayloadInputPart part, DocumentModel docModel, ObjectPartType partMeta, Action action,
471 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) throws Exception {
472 // check if this is an xml part
473 if (part.getMediaType().equals(MediaType.APPLICATION_XML_TYPE)) {
474 Element element = part.getElementBody();
475 Map<String, Object> objectProps = DocumentUtils.parseProperties(partMeta, element, ctx);
476 if (action == Action.UPDATE) {
477 this.filterReadOnlyPropertiesForPart(objectProps, partMeta);
479 String schemaName = partMeta.getLabel();
480 docModel.setProperties(schemaName, objectProps);
481 Set<String> fieldNameSet = getFieldNamesSet(objectProps);
482 setFieldsDirty(docModel, schemaName, fieldNameSet); // Force Nuxeo to update EVERY field by marking them dirty/modified
488 * Due to an apparent Nuxeo bug, we need to explicitly mark all fields as dirty for updates
489 * to take place properly. See https://issues.collectionspace.org/browse/CSPACE-7124
491 * We should improve this code by marking dirty/modified only the fields that were part of the UPDATE/PUT request
492 * payload. The current code will mark any field with a name in the 'fieldNameSet' set. Since field names do not
493 * have to be unique in document/record, we might end up unnecessarily marking some fields as modified -not ideal but
494 * better than brute-force marking ALL fields as modified.
499 private void setFieldsDirty(DocumentModel docModel, String schemaName, Set<String> fieldNameSet) {
500 DataModelImpl dataModel = (DataModelImpl) docModel.getDataModel(schemaName);
501 DocumentPart documentPart = dataModel.getDocumentPart();
502 setFieldsDirty(documentPart.getChildren(), fieldNameSet);
506 * Recursively step through all the children and sub-children setting their dirty flags.
507 * See https://issues.collectionspace.org/browse/CSPACE-7124
510 private void setFieldsDirty(Collection<Property> children, Set<String> fieldNameSet) {
511 if (children != null && (children.size() > 0)) {
512 for (Property prop : children ) {
513 String propName = prop.getName();
514 logger.debug(propName);
515 if (prop.isPhantom() == false) {
516 if (prop.isScalar() == false) {
517 setFieldsDirty(prop.getChildren(), fieldNameSet);
518 } else if (fieldNameSet.contains(propName)) {
519 ScalarProperty scalarProp = (ScalarProperty)prop;
520 scalarProp.setIsModified();
528 * Gets the set of field names used in a map of properties. We'll use this make our workaround to a
529 * bug (See https://issues.collectionspace.org/browse/CSPACE-7124) more efficient by only marking fields
530 * with names in this set as modified -ie, needing persisting.
532 * Since the map of properties can contain other maps of properties as values, there can be duplicate
533 * field names in the 'objectProps' map. And since we're creating a set, there can be no duplicates in the result.
536 private Set<String> getFieldNamesSet(Map<String, Object> objectProps) {
537 HashSet<String> result = new HashSet<String>();
539 addFieldNamesToSet(result, objectProps);
544 private void addFieldNamesToSet(Set<String> fieldNameSet, List<Object> valueList) {
545 for (Object value : valueList) {
546 if (value instanceof List) {
547 addFieldNamesToSet(fieldNameSet, (List<Object>)value);
548 } else if (value instanceof Map) {
549 addFieldNamesToSet(fieldNameSet, (Map<String, Object>)value);
551 logger.debug(value.toString());
556 private void addFieldNamesToSet(Set<String> fieldNameSet, Map<String, Object> objectProps) {
557 for (String key : objectProps.keySet()) {
558 Object value = objectProps.get(key);
559 if (value instanceof Map) {
560 addFieldNamesToSet(fieldNameSet, (Map<String, Object>)value);
561 } else if (value instanceof List) {
562 addFieldNamesToSet(fieldNameSet, (List)value);
564 fieldNameSet.add(key);
570 * Filters out read only properties, so they cannot be set on update.
571 * TODO: add configuration support to do this generally
572 * @param objectProps the properties parsed from the update payload
573 * @param partMeta metadata for the object to fill
575 public void filterReadOnlyPropertiesForPart(
576 Map<String, Object> objectProps, ObjectPartType partMeta) {
577 // Should add in logic to filter most of the core items on update
578 if(partMeta.getLabel().equalsIgnoreCase(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA)) {
579 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_AT);
580 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_BY);
581 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_URI);
582 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_TENANTID);
583 // Note that the updatedAt/updatedBy fields are set internally
584 // in DocumentModelHandler.handleCoreValues().
589 * extractPart extracts an XML object from given DocumentModel
591 * @param schema of the object to extract
592 * @param partMeta metadata for the object to extract
595 protected Map<String, Object> extractPart(DocumentModel docModel, String schema)
597 return extractPart(docModel, schema, (Map<String, Object>)null);
601 * extractPart extracts an XML object from given DocumentModel
603 * @param schema of the object to extract
604 * @param partMeta metadata for the object to extract
608 protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
610 return extractPart(docModel, schema, partMeta, null);
614 * extractPart extracts an XML object from given DocumentModel
616 * @param schema of the object to extract
617 * @param partMeta metadata for the object to extract
620 protected Map<String, Object> extractPart(
621 DocumentModel docModel,
623 Map<String, Object> addToMap)
625 Map<String, Object> result = null;
627 Map<String, Object> objectProps = docModel.getProperties(schema);
628 if (objectProps != null) {
629 //unqualify properties before sending the doc over the wire (to save bandwidh)
630 //FIXME: is there a better way to avoid duplication of a Map/Collection?
631 Map<String, Object> unQObjectProperties =
632 (addToMap != null) ? addToMap : (new HashMap<String, Object>());
633 Set<Entry<String, Object>> qualifiedEntries = objectProps.entrySet();
634 for (Entry<String, Object> entry : qualifiedEntries) {
635 String unqProp = getUnQProperty(entry.getKey());
636 unQObjectProperties.put(unqProp, entry.getValue());
638 result = unQObjectProperties;
645 * extractPart extracts an XML object from given DocumentModel
647 * @param schema of the object to extract
648 * @param partMeta metadata for the object to extract
652 protected Map<String, Object> extractPart(
653 DocumentModel docModel, String schema, ObjectPartType partMeta,
654 Map<String, Object> addToMap)
656 Map<String, Object> result = null;
658 result = this.extractPart(docModel, schema, addToMap);
664 public String getStringPropertyFromDoc(
667 String propertyXPath ) throws DocumentNotFoundException, DocumentException {
668 RepositoryInstance repoSession = null;
669 boolean releaseRepoSession = false;
670 String returnValue = null;
673 RepositoryClientImpl repoClient = (RepositoryClientImpl)this.getRepositoryClient(ctx);
674 repoSession = this.getRepositorySession();
675 if (repoSession == null) {
676 repoSession = repoClient.getRepositorySession();
677 releaseRepoSession = true;
681 DocumentWrapper<DocumentModel> wrapper = repoClient.getDoc(repoSession, ctx, csid);
682 DocumentModel docModel = wrapper.getWrappedObject();
683 returnValue = (String) docModel.getPropertyValue(propertyXPath);
684 } catch (PropertyException pe) {
686 } catch (DocumentException de) {
688 } catch (Exception e) {
689 if (logger.isDebugEnabled()) {
690 logger.debug("Caught exception ", e);
692 throw new DocumentException(e);
694 if (releaseRepoSession && repoSession != null) {
695 repoClient.releaseRepositorySession(repoSession);
698 } catch (Exception e) {
699 if (logger.isDebugEnabled()) {
700 logger.debug("Caught exception ", e);
702 throw new DocumentException(e);
706 if (logger.isWarnEnabled() == true) {
707 logger.warn("Returned DocumentModel instance was created with a repository session that is now closed.");
716 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#getAuthorityRefs(org.collectionspace.services.common.document.DocumentWrapper, java.util.List)
719 public AuthorityRefList getAuthorityRefs(
721 List<AuthRefConfigInfo> authRefConfigInfoList) throws PropertyException, Exception {
723 AuthorityRefList authRefList = new AuthorityRefList();
724 AbstractCommonList commonList = (AbstractCommonList) authRefList;
726 DocumentFilter docFilter = this.getDocumentFilter();
727 long pageSize = docFilter.getPageSize();
728 long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
729 // set the page size and page number
730 commonList.setPageNum(pageNum);
731 commonList.setPageSize(pageSize);
733 List<AuthorityRefList.AuthorityRefItem> list = authRefList.getAuthorityRefItem();
736 int iFirstToUse = (int)(pageSize*pageNum);
737 int nFoundInPage = 0;
740 ArrayList<RefNameServiceUtils.AuthRefInfo> foundReferences
741 = new ArrayList<RefNameServiceUtils.AuthRefInfo>();
743 boolean releaseRepoSession = false;
744 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
745 RepositoryClientImpl repoClient = (RepositoryClientImpl)this.getRepositoryClient(ctx);
746 CoreSessionInterface repoSession = this.getRepositorySession();
747 if (repoSession == null) {
748 repoSession = repoClient.getRepositorySession(ctx);
749 releaseRepoSession = true;
750 this.setRepositorySession(repoSession); // we (the doc handler) should keep track of this repository session in case we need it
754 DocumentModel docModel = repoClient.getDoc(repoSession, ctx, csid).getWrappedObject();
755 RefNameServiceUtils.findAuthRefPropertiesInDoc(docModel, authRefConfigInfoList, null, foundReferences);
756 // Slightly goofy pagination support - how many refs do we expect from one object?
757 for(RefNameServiceUtils.AuthRefInfo ari:foundReferences) {
758 if ((nFoundTotal >= iFirstToUse) && (nFoundInPage < pageSize)) {
759 if(appendToAuthRefsList(ari, list)) {
768 if (releaseRepoSession == true) {
769 repoClient.releaseRepositorySession(ctx, repoSession);
773 // Set num of items in list. this is useful to our testing framework.
774 commonList.setItemsInPage(nFoundInPage);
775 // set the total result size
776 commonList.setTotalItems(nFoundTotal);
778 } catch (PropertyException pe) {
779 String msg = "Attempted to retrieve value for invalid or missing authority field. "
780 + "Check authority field properties in tenant bindings.";
781 logger.warn(msg, pe);
783 } catch (Exception e) {
784 if (logger.isDebugEnabled()) {
785 logger.debug("Caught exception in getAuthorityRefs", e);
787 Response response = Response.status(
788 Response.Status.INTERNAL_SERVER_ERROR).entity(
789 "Failed to retrieve authority references").type(
790 "text/plain").build();
791 throw new CSWebApplicationException(e, response);
797 private boolean appendToAuthRefsList(RefNameServiceUtils.AuthRefInfo ari,
798 List<AuthorityRefList.AuthorityRefItem> list)
800 String fieldName = ari.getQualifiedDisplayName();
802 String refNameValue = (String)ari.getProperty().getValue();
803 AuthorityRefList.AuthorityRefItem item = authorityRefListItem(fieldName, refNameValue);
804 if(item!=null) { // ignore garbage values.
808 } catch(PropertyException pe) {
809 String msg = "PropertyException on: "+ari.getProperty().getPath()+pe.getLocalizedMessage();
810 if (logger.isDebugEnabled()) {
811 logger.debug(msg, pe);
820 * Fill in all the values to be returned in the authrefs payload for this item.
822 * @param authRefFieldName
826 private AuthorityRefList.AuthorityRefItem authorityRefListItem(String authRefFieldName, String refName) {
828 // Find the CSID for the authority item
832 DocumentModel docModel = NuxeoUtils.getDocModelForRefName(getServiceContext(), refName, getServiceContext().getResourceMap());
833 csid = NuxeoUtils.getCsid(docModel);
834 } catch (Exception e1) {
835 String msg = String.format("Could not find CSID for authority reference with refname = %s.", refName);
836 if (logger.isDebugEnabled()) {
837 logger.debug(msg, e1);
843 AuthorityRefList.AuthorityRefItem ilistItem = new AuthorityRefList.AuthorityRefItem();
845 RefNameUtils.AuthorityTermInfo termInfo = RefNameUtils.parseAuthorityTermInfo(refName);
846 if (Tools.isEmpty(csid) == false) {
847 ilistItem.setCsid(csid);
849 ilistItem.setRefName(refName);
850 ilistItem.setAuthDisplayName(termInfo.inAuthority.displayName);
851 ilistItem.setItemDisplayName(termInfo.displayName);
852 ilistItem.setSourceField(authRefFieldName);
853 ilistItem.setUri(termInfo.getRelativeUri());
854 } catch (Exception e) {
856 String msg = String.format("Trouble parsing refName from value: %s in field: %s. Error message: %s.",
857 refName, authRefFieldName, e.getLocalizedMessage());
858 if (logger.isDebugEnabled()) {
859 logger.debug(msg, e);
869 * Returns the primary value from a list of values.
871 * Assumes that the first value is the primary value.
872 * This assumption may change when and if the primary value
873 * is identified explicitly.
875 * @param values a list of values.
876 * @param propertyName the name of a property through
877 * which the value can be extracted.
878 * @return the primary value.
879 protected String primaryValueFromMultivalue(List<Object> values, String propertyName) {
880 String primaryValue = "";
881 if (values == null || values.size() == 0) {
884 Object value = values.get(0);
885 if (value instanceof String) {
887 primaryValue = (String) value;
889 // Multivalue group of fields
890 } else if (value instanceof Map) {
892 Map map = (Map) value;
893 if (map.values().size() > 0) {
894 if (map.get(propertyName) != null) {
895 primaryValue = (String) map.get(propertyName);
900 logger.warn("Unexpected type for property " + propertyName
901 + " in multivalue list: not String or Map.");
908 * Gets a simple property from the document.
910 * For completeness, as this duplicates DocumentModel method.
912 * @param docModel The document model to get info from
913 * @param schema The name of the schema (part)
914 * @param propertyName The simple scalar property type
915 * @return property value as String
917 protected String getSimpleStringProperty(DocumentModel docModel, String schema, String propName) {
918 String xpath = "/"+schema+":"+propName;
920 return (String) NuxeoUtils.getProperyValue(docModel, xpath); //docModel.getPropertyValue(xpath);
921 } catch(PropertyException pe) {
922 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a simple String property?"
923 +pe.getLocalizedMessage());
924 } catch(ClassCastException cce) {
925 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a scalar String property?"
926 +cce.getLocalizedMessage());
927 } catch(Exception e) {
928 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
929 +e.getLocalizedMessage());
934 * Gets first of a repeating list of scalar values, as a String, from the document.
936 * @param docModel The document model to get info from
937 * @param schema The name of the schema (part)
938 * @param listName The name of the scalar list property
939 * @return first value in list, as a String, or empty string if the list is empty
941 protected String getFirstRepeatingStringProperty(DocumentModel docModel,
942 String schema, String listName) {
943 String xpath = "/" + schema + ":" + listName + "/[0]";
945 return (String) NuxeoUtils.getProperyValue(docModel, xpath); // docModel.getPropertyValue(xpath);
946 } catch (PropertyException pe) {
947 throw new RuntimeException("Problem retrieving property {" + xpath
948 + "}. Not a repeating scalar?" + pe.getLocalizedMessage());
949 } catch (IndexOutOfBoundsException ioobe) {
950 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
951 return ""; // gracefully handle missing elements
952 } catch (ClassCastException cce) {
953 throw new RuntimeException("Problem retrieving property {" + xpath
954 + "} as String. Not a repeating String property?"
955 + cce.getLocalizedMessage());
956 } catch (Exception e) {
957 throw new RuntimeException("Unknown problem retrieving property {"
958 + xpath + "}." + e.getLocalizedMessage());
963 * Gets first of a repeating list of scalar values, as a String, from the document.
965 * @param docModel The document model to get info from
966 * @param schema The name of the schema (part)
967 * @param listName The name of the scalar list property
968 * @return first value in list, as a String, or empty string if the list is empty
970 protected String getStringValueInPrimaryRepeatingComplexProperty(
971 DocumentModel docModel, String schema, String complexPropertyName, String fieldName) {
972 String result = null;
974 String xpath = "/" + NuxeoUtils.getPrimaryXPathPropertyName(schema, complexPropertyName, fieldName);
976 result = (String) NuxeoUtils.getProperyValue(docModel, xpath); //docModel.getPropertyValue(xpath);
977 } catch(PropertyException pe) {
978 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad propertyNames?"
979 +pe.getLocalizedMessage());
980 } catch(IndexOutOfBoundsException ioobe) {
981 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
982 result = ""; // gracefully handle missing elements
983 } catch(ClassCastException cce) {
984 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
985 +cce.getLocalizedMessage());
986 } catch(NullPointerException npe) {
987 // Getting here because of a bug in Nuxeo when value in repository is unknown/empty/null
988 logger.warn(String.format("Nuxeo repo unexpectedly returned an Null Pointer Exception when asked for the value of {%s}.",
990 } catch(Exception e) {
991 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
992 +e.getLocalizedMessage());
999 * Gets XPath value from schema. Note that only "/" and "[n]" are
1000 * supported for xpath. Can omit grouping elements for repeating complex types,
1001 * e.g., "fieldList/[0]" can be used as shorthand for "fieldList/field[0]" and
1002 * "fieldGroupList/[0]/field" can be used as shorthand for "fieldGroupList/fieldGroup[0]/field".
1003 * If there are no entries for a list of scalars or for a list of complex types,
1004 * a 0 index expression (e.g., "fieldGroupList/[0]/field") will safely return an empty
1005 * string. A non-zero index will throw an IndexOutOfBoundsException if there are not
1006 * that many elements in the list.
1007 * N.B.: This does not follow the XPath spec - indices are 0-based, not 1-based.
1009 * @param docModel The document model to get info from
1010 * @param schema The name of the schema (part)
1011 * @param xpath The XPath expression (without schema prefix)
1012 * @return value the indicated property value as a String
1013 * @throws DocumentException
1015 protected Object getListResultValue(DocumentModel docModel, // REM - CSPACE-5133
1016 String schema, ListResultField field) throws DocumentException {
1017 Object result = null;
1019 result = NuxeoUtils.getXPathValue(docModel, schema, field.getXpath());
1024 protected String getStringValue(DocumentModel docModel,
1025 String schema, ListResultField field) throws DocumentException {
1026 String result = null;
1028 Object value = getListResultValue(docModel, schema, field);
1029 if (value != null && value instanceof String) {
1030 String strValue = (String) value;
1031 if (strValue.trim().isEmpty() == false) {
1039 protected void removeFromList(List<RelationsCommonList.RelationListItem> list, RelationsCommonList.RelationListItem item) {
1043 private void itemToString(StringBuilder sb, String prefix, RelationsCommonList.RelationListItem item ) {
1045 sb.append((item.getCsid()!= null)?item.getCsid():"NO CSID");
1047 sb.append((item.getSubject().getCsid()!=null)?item.getSubject().getCsid():item.getSubject().getRefName());
1049 sb.append(item.getPredicate());
1051 sb.append((item.getObject().getCsid()!=null)?item.getObject().getCsid():item.getObject().getRefName());
1055 private String dumpList(List<RelationsCommonList.RelationListItem> list, String label) {
1056 StringBuilder sb = new StringBuilder();
1058 if (list.size() > 0) {
1059 sb.append("=========== " + label + " ==========" + CR);
1061 for (RelationsCommonList.RelationListItem item : list) {
1062 itemToString(sb, "== ", item);
1065 return sb.toString();
1068 /** @return null on parent not found
1070 protected String getParentCSID(String thisCSID) throws Exception {
1071 String parentCSID = null;
1073 String predicate = RelationshipType.HAS_BROADER.value();
1074 RelationsCommonList parentListOuter = getRelations(thisCSID, null, predicate);
1075 List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
1076 if (parentList != null) {
1077 if (parentList.size() == 0) {
1080 RelationsCommonList.RelationListItem relationListItem = parentList.get(0);
1081 parentCSID = relationListItem.getObjectCsid();
1084 } catch (Exception e) {
1085 logger.error("Could not find parent for this: " + thisCSID, e);
1090 protected List<RelationsCommonList.RelationListItem> newRelationsCommonList() {
1091 List<RelationsCommonList.RelationListItem> result = new ArrayList<RelationsCommonList.RelationListItem>();
1095 public void showSiblings(DocumentWrapper<DocumentModel> wrapDoc,
1096 MultipartServiceContext ctx) throws Exception {
1097 String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
1098 String parentCSID = getParentCSID(thisCSID);
1099 if (parentCSID == null) {
1100 logger.warn("~~~~~\r\n~~~~ Could not find parent for this: " + thisCSID);
1104 String predicate = RelationshipType.HAS_BROADER.value();
1105 RelationsCommonList siblingListOuter = getRelations(null, parentCSID, predicate);
1106 List<RelationsCommonList.RelationListItem> siblingList = siblingListOuter.getRelationListItem();
1108 List<RelationsCommonList.RelationListItem> toRemoveList = newRelationsCommonList();
1111 RelationsCommonList.RelationListItem item = null;
1112 for (RelationsCommonList.RelationListItem sibling : siblingList) {
1113 if (thisCSID.equals(sibling.getSubjectCsid())) {
1114 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.
1117 //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.
1118 for (RelationsCommonList.RelationListItem self : toRemoveList) {
1119 removeFromList(siblingList, self);
1122 long siblingSize = siblingList.size();
1123 siblingListOuter.setTotalItems(siblingSize);
1124 siblingListOuter.setItemsInPage(siblingSize);
1125 if(logger.isTraceEnabled()) {
1126 String dump = dumpList(siblingList, "Siblings of: "+thisCSID);
1127 logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showSiblings ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1130 PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, siblingListOuter);
1131 ctx.addOutputPart(relationsPart);
1134 public void showRelations(DocumentWrapper<DocumentModel> wrapDoc,
1135 MultipartServiceContext ctx) throws Exception {
1136 String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
1138 String predicate = RelationshipType.HAS_BROADER.value();
1139 RelationsCommonList parentListOuter = getRelations(thisCSID, null, predicate);
1140 List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
1142 RelationsCommonList childrenListOuter = getRelations(null, thisCSID, predicate);
1143 List<RelationsCommonList.RelationListItem> childrenList = childrenListOuter.getRelationListItem();
1145 if(logger.isTraceEnabled()) {
1146 String dump = dumpLists(thisCSID, parentList, childrenList, null);
1147 logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showRelations ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1150 //Assume that there are more children than parents. Will be true for parent/child, but maybe not for other relations.
1151 //Now add all parents to our childrenList, to be able to return just one list of consolidated results.
1152 //Not optimal, but that's the current design spec.
1154 for (RelationsCommonList.RelationListItem parent : parentList) {
1155 childrenList.add(parent);
1158 long childrenSize = childrenList.size();
1159 childrenListOuter.setTotalItems(childrenSize);
1160 childrenListOuter.setItemsInPage(childrenListOuter.getItemsInPage() + added);
1162 PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, childrenListOuter);
1163 ctx.addOutputPart(relationsPart);
1166 public void showAllRelations(DocumentWrapper<DocumentModel> wrapDoc, MultipartServiceContext ctx) throws Exception {
1167 String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
1169 RelationsCommonList subjectListOuter = getRelations(thisCSID, null, null); // nulls are wildcards: predicate=*, and object=*
1170 List<RelationsCommonList.RelationListItem> subjectList = subjectListOuter.getRelationListItem();
1172 RelationsCommonList objectListOuter = getRelations(null, thisCSID, null); // nulls are wildcards: subject=*, and predicate=*
1173 List<RelationsCommonList.RelationListItem> objectList = objectListOuter.getRelationListItem();
1175 if(logger.isTraceEnabled()) {
1176 String dump = dumpLists(thisCSID, subjectList, objectList, null);
1177 logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showAllRelations ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1180 subjectList.addAll(objectList);
1182 //now subjectList actually has records BOTH where thisCSID is subject and object.
1183 long relatedSize = subjectList.size();
1184 subjectListOuter.setTotalItems(relatedSize);
1185 subjectListOuter.setItemsInPage(relatedSize);
1187 PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, subjectListOuter);
1188 ctx.addOutputPart(relationsPart);
1191 private String dumpLists(String itemCSID,
1192 List<RelationsCommonList.RelationListItem> parentList,
1193 List<RelationsCommonList.RelationListItem> childList,
1194 List<RelationsCommonList.RelationListItem> actionList) {
1195 StringBuilder sb = new StringBuilder();
1196 sb.append("itemCSID: " + itemCSID + CR);
1197 if(parentList!=null) {
1198 sb.append(dumpList(parentList, "parentList"));
1200 if(childList!=null) {
1201 sb.append(dumpList(childList, "childList"));
1203 if(actionList!=null) {
1204 sb.append(dumpList(actionList, "actionList"));
1206 return sb.toString();
1209 //================= TODO: move this to common, refactoring this and CollectionObjectResource.java
1210 public RelationsCommonList getRelations(String subjectCSID, String objectCSID, String predicate) throws Exception {
1211 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1212 MultivaluedMap<String, String> queryParams = ctx.getQueryParams();
1213 queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1214 queryParams.putSingle(IRelationsManager.SUBJECT_QP, subjectCSID);
1215 queryParams.putSingle(IRelationsManager.OBJECT_QP, objectCSID);
1217 RelationResource relationResource = new RelationResource(); //is this still acting like a singleton as it should be?
1218 RelationsCommonList relationsCommonList = relationResource.getList(ctx);
1219 return relationsCommonList;
1221 //============================= END TODO refactor ==========================
1223 // this method calls the RelationResource to have it create the relations and persist them.
1224 private void createRelations(List<RelationsCommonList.RelationListItem> inboundList,
1225 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) throws Exception {
1226 for (RelationsCommonList.RelationListItem item : inboundList) {
1227 RelationsCommon rc = new RelationsCommon();
1228 //rc.setCsid(item.getCsid());
1229 //todo: assignTo(item, rc);
1230 RelationsDocListItem itemSubject = item.getSubject();
1231 RelationsDocListItem itemObject = item.getObject();
1233 // Set at least one of CSID and refName for Subject and Object
1234 // Either value might be null for for each of Subject and Object
1235 String subjectCsid = itemSubject.getCsid();
1236 rc.setSubjectCsid(subjectCsid);
1238 String objCsid = itemObject.getCsid();
1239 rc.setObjectCsid(objCsid);
1241 rc.setSubjectRefName(itemSubject.getRefName());
1242 rc.setObjectRefName(itemObject.getRefName());
1244 rc.setRelationshipType(item.getPredicate());
1245 rc.setRelationshipMetaType(item.getRelationshipMetaType());
1246 //RelationshipType foo = (RelationshipType.valueOf(item.getPredicate())) ;
1247 //rc.setPredicate(foo); //this must be one of the type found in the enum in services/jaxb/src/main/resources/relations_common.xsd
1249 // This is superfluous, since it will be fetched by the Relations Create logic.
1250 rc.setSubjectDocumentType(itemSubject.getDocumentType());
1251 rc.setObjectDocumentType(itemObject.getDocumentType());
1253 // This is superfluous, since it will be fetched by the Relations Create logic.
1254 rc.setSubjectUri(itemSubject.getUri());
1255 rc.setObjectUri(itemObject.getUri());
1256 // May not have the info here. Only really require CSID or refName.
1257 // Rest is handled in the Relation create mechanism
1258 //uriPointsToSameAuthority(itemSubject.getUri(), itemObject.getUri());
1260 PoxPayloadOut payloadOut = new PoxPayloadOut(RelationClient.SERVICE_PAYLOAD_NAME);
1261 PayloadOutputPart outputPart = new PayloadOutputPart(RelationClient.SERVICE_COMMONPART_NAME, rc);
1262 payloadOut.addPart(outputPart);
1263 RelationResource relationResource = new RelationResource();
1264 Response res = relationResource.create(ctx, ctx.getResourceMap(),
1265 ctx.getUriInfo(), payloadOut.toXML()); //NOTE ui recycled from above to pass in unknown query params.
1269 // Note that item2 may be sparse (only refName, no CSID for subject or object)
1270 // But item1 must not be sparse
1271 private boolean itemsEqual(RelationsCommonList.RelationListItem item1, RelationsCommonList.RelationListItem item2) {
1272 if (item1 == null || item2 == null) {
1275 RelationsDocListItem subj1 = item1.getSubject();
1276 RelationsDocListItem subj2 = item2.getSubject();
1277 RelationsDocListItem obj1 = item1.getObject();
1278 RelationsDocListItem obj2 = item2.getObject();
1280 String subj1Csid = subj1.getCsid();
1281 String subj2Csid = subj2.getCsid();
1282 String subj1RefName = subj1.getRefName();
1283 String subj2RefName = subj2.getRefName();
1285 String obj1Csid = obj1.getCsid();
1286 String obj2Csid = obj2.getCsid();
1287 String obj1RefName = obj1.getRefName();
1288 String obj2RefName = obj2.getRefName();
1290 String item1Metatype = item1.getRelationshipMetaType();
1291 item1Metatype = item1Metatype != null ? item1Metatype : EMPTYSTR;
1293 String item2Metatype = item2.getRelationshipMetaType();
1294 item2Metatype = item2Metatype != null ? item2Metatype : EMPTYSTR;
1296 boolean isEqual = (subj1Csid.equals(subj2Csid) || ((subj2Csid==null) && subj1RefName.equals(subj2RefName)))
1297 && (obj1Csid.equals(obj2Csid) || ((obj2Csid==null) && obj1RefName.equals(obj2RefName)))
1298 // predicate is proper, but still allow relationshipType
1299 && (item1.getPredicate().equals(item2.getPredicate())
1300 || ((item2.getPredicate()==null) && item1.getRelationshipType().equals(item2.getRelationshipType())))
1301 // Allow missing docTypes, so long as they do not conflict
1302 && (obj1.getDocumentType().equals(obj2.getDocumentType()) || obj2.getDocumentType()==null)
1303 && (subj1.getDocumentType().equals(subj2.getDocumentType()) || subj2.getDocumentType()==null)
1304 && (item1Metatype.equalsIgnoreCase(item2Metatype));
1308 // Note that the item argument may be sparse (only refName, no CSID for subject or object)
1309 // But the list items must not be sparse
1310 private RelationsCommonList.RelationListItem findInList(
1311 List<RelationsCommonList.RelationListItem> list,
1312 RelationsCommonList.RelationListItem item) {
1313 RelationsCommonList.RelationListItem foundItem = null;
1314 for (RelationsCommonList.RelationListItem listItem : list) {
1315 if (itemsEqual(listItem, item)) { //equals must be defined, else
1316 foundItem = listItem;
1323 /** updateRelations strategy:
1326 go through inboundList, remove anything from childList that matches from childList
1327 go through inboundList, remove anything from parentList that matches from parentList
1328 go through parentList, delete all remaining
1329 go through childList, delete all remaining
1330 go through actionList, add all remaining.
1331 check for duplicate children
1332 check for more than one parent.
1334 inboundList parentList childList actionList
1335 ---------------- --------------- ---------------- ----------------
1336 child-a parent-c child-a child-b
1337 child-b parent-d child-c
1342 private RelationsCommonList updateRelations(
1343 String itemCSID, PoxPayloadIn input, DocumentWrapper<DocumentModel> wrapDoc, boolean forUpdate)
1345 if (logger.isTraceEnabled()) {
1346 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID);
1348 PayloadInputPart part = input.getPart(RelationClient.SERVICE_COMMON_LIST_NAME); //input.getPart("relations_common");
1350 return null; //nothing to do--they didn't send a list of relations.
1352 RelationsCommonList relationsCommonListBody = (RelationsCommonList) part.getBody();
1353 List<RelationsCommonList.RelationListItem> inboundList = relationsCommonListBody.getRelationListItem();
1354 List<RelationsCommonList.RelationListItem> actionList = newRelationsCommonList();
1355 List<RelationsCommonList.RelationListItem> childList = null;
1356 List<RelationsCommonList.RelationListItem> parentList = null;
1357 DocumentModel docModel = wrapDoc.getWrappedObject();
1358 String itemRefName = (String) docModel.getProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
1359 CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME);
1361 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1362 //Do magic replacement of ${itemCSID} and fix URI's.
1363 fixupInboundListItems(ctx, inboundList, docModel, itemCSID);
1365 final String HAS_BROADER = RelationshipType.HAS_BROADER.value();
1366 UriInfo uriInfo = ctx.getUriInfo();
1367 MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
1370 //Run getList() once as sent to get childListOuter:
1371 String predicate = RelationshipType.HAS_BROADER.value();
1372 queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1373 queryParams.putSingle(IRelationsManager.SUBJECT_QP, null);
1374 queryParams.putSingle(IRelationsManager.SUBJECT_TYPE_QP, null);
1375 queryParams.putSingle(IRelationsManager.OBJECT_QP, itemCSID);
1376 queryParams.putSingle(IRelationsManager.OBJECT_TYPE_QP, null);
1378 RelationResource relationResource = new RelationResource();
1379 RelationsCommonList childListOuter = relationResource.getList(ctx); // Knows all query params because they are in the context.
1381 //Now run getList() again, leaving predicate, swapping subject and object, to get parentListOuter.
1382 queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1383 queryParams.putSingle(IRelationsManager.SUBJECT_QP, itemCSID);
1384 queryParams.putSingle(IRelationsManager.OBJECT_QP, null);
1385 RelationsCommonList parentListOuter = relationResource.getList(ctx);
1388 childList = childListOuter.getRelationListItem();
1389 parentList = parentListOuter.getRelationListItem();
1391 if (parentList.size() > 1) {
1392 throw new Exception("Too many parents for object: " + itemCSID + " list: " + dumpList(parentList, "parentList"));
1395 if (logger.isTraceEnabled()) {
1396 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " got existing relations.");
1400 for (RelationsCommonList.RelationListItem inboundItem : inboundList) {
1401 // Note that the relations may specify the other (non-item) bit with a refName, not a CSID,
1402 // and so the CSID for those may be null
1403 if(inboundItem.getPredicate().equals(HAS_BROADER)) {
1404 // Look for parents and children
1405 if(itemCSID.equals(inboundItem.getObject().getCsid())
1406 || itemRefName.equals(inboundItem.getObject().getRefName())) {
1407 //then this is an item that says we have a child. That child is inboundItem
1408 RelationsCommonList.RelationListItem childItem =
1409 (childList == null) ? null : findInList(childList, inboundItem);
1410 if (childItem != null) {
1411 if (logger.isTraceEnabled()) {
1412 StringBuilder sb = new StringBuilder();
1413 itemToString(sb, "== Child: ", childItem);
1414 logger.trace("Found inboundChild in current child list: " + sb.toString());
1416 removeFromList(childList, childItem); //exists, just take it off delete list
1418 if (logger.isTraceEnabled()) {
1419 StringBuilder sb = new StringBuilder();
1420 itemToString(sb, "== Child: ", inboundItem);
1421 logger.trace("inboundChild not in current child list, will add: " + sb.toString());
1423 actionList.add(inboundItem); //doesn't exist as a child, but is a child. Add to additions list
1424 String newChildCsid = inboundItem.getSubject().getCsid();
1425 if(newChildCsid == null) {
1426 String newChildRefName = inboundItem.getSubject().getRefName();
1427 if (newChildRefName == null) {
1428 throw new RuntimeException("Child with no CSID or refName!");
1430 if (logger.isTraceEnabled()) {
1431 logger.trace("Fetching CSID for child with only refname: "+newChildRefName);
1433 DocumentModel newChildDocModel =
1434 NuxeoBasedResource.getDocModelForRefName(getServiceContext(),
1435 newChildRefName, getServiceContext().getResourceMap());
1436 newChildCsid = getCsid(newChildDocModel);
1438 ensureChildHasNoOtherParents(ctx, queryParams, newChildCsid);
1441 } else if (itemCSID.equals(inboundItem.getSubject().getCsid())
1442 || itemRefName.equals(inboundItem.getSubject().getRefName())) {
1443 //then this is an item that says we have a parent. inboundItem is that parent.
1444 RelationsCommonList.RelationListItem parentItem =
1445 (parentList == null) ? null : findInList(parentList, inboundItem);
1446 if (parentItem != null) {
1447 removeFromList(parentList, parentItem); //exists, just take it off delete list
1449 actionList.add(inboundItem); //doesn't exist as a parent, but is a parent. Add to additions list
1452 logger.error("Parent/Child Element didn't link to this item. inboundItem: " + inboundItem);
1455 logger.warn("Non-parent relation ignored. inboundItem: " + inboundItem);
1458 if (logger.isTraceEnabled()) {
1459 String dump = dumpLists(itemCSID, parentList, childList, actionList);
1460 logger.trace("~~~~~~~~~~~~~~~~~~~~~~dump~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1463 if (logger.isTraceEnabled()) {
1464 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " deleting "
1465 + parentList.size() + " existing parents and " + childList.size() + " existing children.");
1467 deleteRelations(parentList, ctx, "parentList"); //todo: there are items appearing on both lists....april 20.
1468 deleteRelations(childList, ctx, "childList");
1470 if (logger.isTraceEnabled()) {
1471 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " adding "
1472 + actionList.size() + " new parents and children.");
1474 createRelations(actionList, ctx);
1475 if (logger.isTraceEnabled()) {
1476 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " done.");
1478 // We return all elements on the inbound list, since we have just worked to make them exist in the system
1479 // and be non-redundant, etc. That list came from relationsCommonListBody, so it is still attached to it, just pass that back.
1480 return relationsCommonListBody;
1483 /** Performs substitution for ${itemCSID} (see CommonAPI.AuthorityItemCSID_REPLACE for constant)
1484 * and sets URI correctly for related items.
1485 * Operates directly on the items in the list. Does not change the list ordering, does not add or remove any items.
1487 protected void fixupInboundListItems(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1488 List<RelationsCommonList.RelationListItem> inboundList,
1489 DocumentModel docModel,
1490 String itemCSID) throws Exception {
1491 String thisURI = this.getUri(docModel);
1492 // WARNING: the two code blocks below are almost identical and seem to ask to be put in a generic method.
1493 // beware of the little diffs in inboundItem.setObjectCsid(itemCSID); and inboundItem.setSubjectCsid(itemCSID); in the two blocks.
1494 for (RelationsCommonList.RelationListItem inboundItem : inboundList) {
1495 RelationsDocListItem inboundItemObject = inboundItem.getObject();
1496 RelationsDocListItem inboundItemSubject = inboundItem.getSubject();
1498 if (CommonAPI.AuthorityItemCSID_REPLACE.equalsIgnoreCase(inboundItemObject.getCsid())) {
1499 inboundItem.setObjectCsid(itemCSID);
1500 inboundItemObject.setCsid(itemCSID);
1501 //inboundItemObject.setUri(getUri(docModel));
1504 String objectCsid = inboundItemObject.getCsid();
1505 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(getRepositorySession(), ctx, objectCsid); //null if not found.
1506 DocumentWrapper wrapper = new DocumentWrapperImpl(itemDocModel);
1507 String uri = this.getRepositoryClient(ctx).getDocURI(wrapper);
1508 inboundItemObject.setUri(uri); //CSPACE-4037
1511 //uriPointsToSameAuthority(thisURI, inboundItemObject.getUri()); //CSPACE-4042
1513 if (CommonAPI.AuthorityItemCSID_REPLACE.equalsIgnoreCase(inboundItemSubject.getCsid())) {
1514 inboundItem.setSubjectCsid(itemCSID);
1515 inboundItemSubject.setCsid(itemCSID);
1516 //inboundItemSubject.setUri(getUri(docModel));
1519 String subjectCsid = inboundItemSubject.getCsid();
1520 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(getRepositorySession(), ctx, subjectCsid); //null if not found.
1521 DocumentWrapper wrapper = new DocumentWrapperImpl(itemDocModel);
1522 String uri = this.getRepositoryClient(ctx).getDocURI(wrapper);
1523 inboundItemSubject.setUri(uri); //CSPACE-4037
1526 //uriPointsToSameAuthority(thisURI, inboundItemSubject.getUri()); //CSPACE-4042
1531 private void ensureChildHasNoOtherParents(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1532 MultivaluedMap<String, String> queryParams, String childCSID) {
1533 logger.trace("ensureChildHasNoOtherParents for: " + childCSID );
1534 queryParams.putSingle(IRelationsManager.SUBJECT_QP, childCSID);
1535 queryParams.putSingle(IRelationsManager.PREDICATE_QP, RelationshipType.HAS_BROADER.value());
1536 queryParams.putSingle(IRelationsManager.OBJECT_QP, null); //null means ANY
1538 RelationResource relationResource = new RelationResource();
1539 RelationsCommonList parentListOuter = relationResource.getList(ctx);
1540 List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
1541 //logger.warn("ensureChildHasNoOtherParents preparing to delete relations on "+childCSID+"\'s parent list: \r\n"+dumpList(parentList, "duplicate parent list"));
1542 deleteRelations(parentList, ctx, "parentList-delete");
1545 private void deleteRelations(List<RelationsCommonList.RelationListItem> list,
1546 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1549 for (RelationsCommonList.RelationListItem item : list) {
1550 RelationResource relationResource = new RelationResource();
1551 if(logger.isTraceEnabled()) {
1552 StringBuilder sb = new StringBuilder();
1553 itemToString(sb, "==== TO DELETE: ", item);
1554 logger.trace(sb.toString());
1556 Response res = relationResource.deleteWithParentCtx(ctx, item.getCsid());
1557 if (logger.isDebugEnabled()) {
1558 logger.debug("Status of authority item deleteRelations method call was: " + res.getStatus());
1561 } catch (Throwable t) {
1562 String msg = "Unable to deleteRelations: " + Tools.errorToString(t, true);
1567 // Note that we must do this after we have completed the Update, so that the repository has the
1568 // info for the item itself. The relations code must call into the repo to get info for each end.
1569 // This could be optimized to pass in the parent docModel, since it will often be one end.
1570 // Nevertheless, we should complete the item save before we do work on the relations, especially
1571 // since a save on Create might fail, and we would not want to create relations for something
1572 // that may not be created...
1573 private void handleRelationsPayload(DocumentWrapper<DocumentModel> wrapDoc, boolean forUpdate) throws Exception {
1574 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1575 PoxPayloadIn input = ctx.getInput();
1576 DocumentModel documentModel = (wrapDoc.getWrappedObject());
1577 String itemCsid = documentModel.getName();
1579 //Updates relations part
1580 RelationsCommonList relationsCommonList = updateRelations(itemCsid, input, wrapDoc, forUpdate);
1582 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
1583 ctx.setProperty(RelationClient.SERVICE_COMMON_LIST_NAME, payloadOutputPart);
1585 //now we add part for relations list
1586 //ServiceContext ctx = getServiceContext();
1587 //PayloadOutputPart foo = (PayloadOutputPart) ctx.getProperty(RelationClient.SERVICE_COMMON_LIST_NAME);
1588 ctx.getOutput().addPart(payloadOutputPart);
1592 * Checks to see if the refName has changed, and if so,
1593 * uses utilities to find all references and update them to use the new refName.
1596 protected void handleRefNameReferencesUpdate() throws Exception {
1597 if (hasRefNameUpdate() == true) {
1598 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1599 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient = getRepositoryClient(ctx);
1600 CoreSessionInterface repoSession = this.getRepositorySession();
1602 // Update all the relationship records that referred to the old refName
1603 RefNameServiceUtils.updateRefNamesInRelations(ctx, repoClient, repoSession,
1604 oldRefNameOnUpdate, newRefNameOnUpdate);
1608 protected String getRefNameUpdate() {
1609 String result = null;
1611 if (hasRefNameUpdate() == true) {
1612 result = newRefNameOnUpdate;
1613 if (logger.isDebugEnabled() == true) {
1614 logger.debug(String.format("There was a refName update. New: %s Old: %s" ,
1615 newRefNameOnUpdate, oldRefNameOnUpdate));
1623 * Note: The Vocabulary document handler overrides this method.
1625 protected String getRefPropName() {
1626 return ServiceBindingUtils.AUTH_REF_PROP;