]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
5252a4d506cc24519965b88d8e72e82b0b4784d2
[tmp/jakarta-migration.git] /
1 /**
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:
5
6  *  http://www.collectionspace.org
7  *  http://wiki.collectionspace.org
8
9  *  Copyright 2009 University of California at Berkeley
10
11  *  Licensed under the Educational Community License (ECL), Version 2.0.
12  *  You may not use this file except in compliance with this License.
13
14  *  You may obtain a copy of the ECL 2.0 License at
15
16  *  https://source.collectionspace.org/collection-space/LICENSE.txt
17
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.
23  */
24 package org.collectionspace.services.nuxeo.client.java;
25
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Map.Entry;
31 import java.util.Set;
32
33 import javax.ws.rs.core.MediaType;
34 import javax.ws.rs.core.MultivaluedMap;
35 import javax.ws.rs.core.Response;
36 import javax.ws.rs.core.UriInfo;
37 import javax.xml.bind.JAXBElement;
38
39 import org.collectionspace.authentication.spi.AuthNContext;
40 import org.collectionspace.services.authorization.AccountPermission;
41 import org.collectionspace.services.jaxb.AbstractCommonList;
42 import org.collectionspace.services.lifecycle.TransitionDef;
43 import org.collectionspace.services.client.AccountClient;
44 import org.collectionspace.services.client.CollectionSpaceClient;
45 import org.collectionspace.services.client.PayloadInputPart;
46 import org.collectionspace.services.client.PayloadOutputPart;
47 import org.collectionspace.services.client.PoxPayloadIn;
48 import org.collectionspace.services.client.PoxPayloadOut;
49 import org.collectionspace.services.client.Profiler;
50 import org.collectionspace.services.client.RelationClient;
51 import org.collectionspace.services.client.workflow.WorkflowClient;
52 import org.collectionspace.services.common.CSWebApplicationException;
53 import org.collectionspace.services.common.NuxeoBasedResource;
54 import org.collectionspace.services.common.authorityref.AuthorityRefList;
55 import org.collectionspace.services.common.config.ServiceConfigUtils;
56 import org.collectionspace.services.common.context.JaxRsContext;
57 import org.collectionspace.services.common.context.MultipartServiceContext;
58 import org.collectionspace.services.common.context.ServiceBindingUtils;
59 import org.collectionspace.services.common.context.ServiceContext;
60 import org.collectionspace.services.common.document.BadRequestException;
61 import org.collectionspace.services.common.document.DocumentException;
62 import org.collectionspace.services.common.document.DocumentUtils;
63 import org.collectionspace.services.common.document.DocumentWrapper;
64 import org.collectionspace.services.common.document.DocumentFilter;
65 import org.collectionspace.services.client.IRelationsManager;
66 import org.collectionspace.services.common.relation.RelationResource;
67 import org.collectionspace.services.common.repository.RepositoryClient;
68 import org.collectionspace.services.common.security.SecurityUtils;
69 import org.collectionspace.services.common.storage.jpa.JpaStorageUtils;
70 import org.collectionspace.services.common.api.CommonAPI;
71 import org.collectionspace.services.common.api.RefNameUtils;
72 import org.collectionspace.services.common.api.Tools;
73 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils;
74 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthRefConfigInfo;
75 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthorityItemSpecifier;
76 import org.collectionspace.services.config.service.DocHandlerParams;
77 import org.collectionspace.services.config.service.ListResultField;
78 import org.collectionspace.services.config.service.ObjectPartType;
79 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
80 import org.collectionspace.services.relation.RelationsCommon;
81 import org.collectionspace.services.relation.RelationsCommonList;
82 import org.collectionspace.services.relation.RelationsDocListItem;
83 import org.collectionspace.services.relation.RelationshipType;
84 import org.dom4j.Element;
85 import org.nuxeo.ecm.core.api.DocumentModel;
86 import org.nuxeo.ecm.core.api.DocumentModelList;
87 import org.nuxeo.ecm.core.api.DocumentNotFoundException;
88 import org.nuxeo.ecm.core.api.model.PropertyException;
89 import org.slf4j.Logger;
90 import org.slf4j.LoggerFactory;
91
92 /**
93  * RemoteDocumentModelHandler
94  *
95  * $LastChangedRevision: $
96  * $LastChangedDate: $
97  * @param <T> 
98  * @param <TL> 
99  */
100 public abstract class   RemoteDocumentModelHandlerImpl<T, TL>
101         extends DocumentModelHandler<T, TL> {
102
103     /** The logger. */
104     private final Logger logger = LoggerFactory.getLogger(RemoteDocumentModelHandlerImpl.class);
105     private final static String CR = "\r\n";
106     private final static String EMPTYSTR = "";
107         private static final String COLLECTIONSPACE_CORE_SCHEMA = CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA;
108         private static final String ACCOUNT_PERMISSION_COMMON_PART_NAME = AccountClient.SERVICE_COMMON_PART_NAME;
109     
110     /* (non-Javadoc)
111      * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#setServiceContext(org.collectionspace.services.common.context.ServiceContext)
112      */
113     @Override
114     public void setServiceContext(ServiceContext ctx) {  //FIXME: Apply proper generics to ServiceContext<PoxPayloadIn, PoxPayloadOut>
115         if (ctx instanceof MultipartServiceContext) {
116             super.setServiceContext(ctx);
117         } else {
118             throw new IllegalArgumentException("setServiceContext requires instance of "
119                     + MultipartServiceContext.class.getName());
120         }
121     }
122     
123     @Override
124     protected String getRefnameDisplayName(DocumentWrapper<DocumentModel> docWrapper) {
125         return getRefnameDisplayName(docWrapper.getWrappedObject());
126     }
127         
128         private String getRefnameDisplayName(DocumentModel docModel) { // Look in the tenant bindings to see what field should be our display name for our refname value
129                 String result = null;
130                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
131                 
132         DocHandlerParams.Params params = null;
133         try {
134                         params = ServiceConfigUtils.getDocHandlerParams(ctx);
135                         ListResultField field = params.getRefnameDisplayNameField();
136                         
137                         String schema = field.getSchema();
138                         if (schema == null || schema.trim().isEmpty()) {
139                                 schema = ctx.getCommonPartLabel();
140                         }
141                         
142                         result = getStringValue(docModel, schema, field);
143                 } catch (Exception e) {
144                         if (logger.isWarnEnabled()) {
145                                 logger.warn(String.format("Call failed to getRefnameDisplayName() for class %s", this.getClass().getName()));
146                         }
147                 }
148                 
149                 return result;
150         }
151         
152     @Override
153     public boolean supportsHierarchy() {
154         boolean result = false;
155         
156         DocHandlerParams.Params params = null;
157         try {
158                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
159                         params = ServiceConfigUtils.getDocHandlerParams(ctx);
160                         Boolean bool = params.isSupportsHierarchy();
161                         if (bool != null) {
162                                 result = bool.booleanValue();
163                         }
164                 } catch (DocumentException e) {
165                         // TODO Auto-generated catch block
166                         String errMsg = String.format("Could not get document handler params from config bindings for class %s", this.getClass().getName());
167                         if (logger.isWarnEnabled() == true) {
168                                 logger.warn(errMsg);
169                         }
170                 }
171         
172         return result;
173     }
174     
175     @Override
176     public boolean supportsVersioning() {
177         boolean result = false;
178         
179         DocHandlerParams.Params params = null;
180         try {
181                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
182                         params = ServiceConfigUtils.getDocHandlerParams(ctx);
183                         Boolean bool = params.isSupportsVersioning();
184                         if (bool != null) {
185                                 result = bool.booleanValue();
186                         }
187                 } catch (DocumentException e) {
188                         // TODO Auto-generated catch block
189                         String errMsg = String.format("Could not get document handler params from config bindings for class %s", this.getClass().getName());
190                         if (logger.isWarnEnabled() == true) {
191                                 logger.warn(errMsg);
192                         }
193                 }
194         
195         return result;
196     }
197     
198         @Override
199         public void handleWorkflowTransition(ServiceContext ctx, DocumentWrapper<DocumentModel> wrapDoc, TransitionDef transitionDef)
200                         throws Exception {
201                 // Do nothing by default, but children can override if they want.  The real workflow transition happens in the WorkflowDocumentModelHandler class
202         }
203         
204     @Override
205     public void completeCreate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
206         super.completeCreate(wrapDoc);
207         if (supportsHierarchy() == true) {
208                 handleRelationsPayload(wrapDoc, false);
209         }
210     }
211         
212     /* NOTE: The authority item doc handler overrides (after calling) this method.  It performs refName updates.  In this
213      * method we just update any and all relationship records that use refNames that have changed.
214      * (non-Javadoc)
215      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#completeUpdate(org.collectionspace.services.common.document.DocumentWrapper)
216      */
217     @Override
218     public void completeUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
219         DocumentModel docModel = wrapDoc.getWrappedObject();
220         // We need to return at least those document part(s) and corresponding payloads that were received
221         Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
222         MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
223         PoxPayloadIn input = ctx.getInput();
224         if (input != null) {
225                 List<PayloadInputPart> inputParts = ctx.getInput().getParts();
226                 for (PayloadInputPart part : inputParts) {
227                     String partLabel = part.getLabel();
228                 try{
229                     ObjectPartType partMeta = partsMetaMap.get(partLabel);
230                     // CSPACE-4030 - generates NPE if the part is missing.
231                     if(partMeta!=null) {
232                             Map<String, Object> unQObjectProperties = extractPart(docModel, partLabel, partMeta);
233                             if(unQObjectProperties!=null) {
234                                 addOutputPart(unQObjectProperties, partLabel, partMeta);
235                             }
236                     }
237                 } catch (Throwable t){
238                     logger.error("Unable to addOutputPart: " + partLabel
239                                                + " in serviceContextPath: "+this.getServiceContextPath()
240                                                + " with URI: " + this.getServiceContext().getUriInfo().getPath()
241                                                + " error: " + t);
242                 }
243                 }
244         } else {
245                 if (logger.isWarnEnabled() == true) {
246                         logger.warn("MultipartInput part was null for document id = " +
247                                         docModel.getName());
248                 }
249         }
250         //
251         //  If the resource's service supports hierarchy then we need to perform a little more work
252         //
253         if (supportsHierarchy() == true) {
254             handleRelationsPayload(wrapDoc, true); // refNames in relations payload should refer to pre-updated record refName value
255             handleRefNameReferencesUpdate(); // if our refName changed, we need to update any and all relationship records that used the old one
256         }
257     }
258
259     /**
260      * Adds the output part.
261      *
262      * @param unQObjectProperties the un q object properties
263      * @param schema the schema
264      * @param partMeta the part meta
265      * @throws Exception the exception
266      * MediaType.APPLICATION_XML_TYPE
267      */
268     protected void addOutputPart(Map<String, Object> unQObjectProperties, String schema, ObjectPartType partMeta)
269             throws Exception {
270         Element doc = DocumentUtils.buildDocument(partMeta, schema,
271                 unQObjectProperties);
272         if (logger.isTraceEnabled() == true) {
273             logger.trace(doc.asXML());
274         }
275         MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
276         ctx.addOutputPart(schema, doc, partMeta.getContent().getContentType());
277     }
278     
279     /**
280      * Extract paging info.
281      *
282      * @param commonsList the commons list
283      * @return the tL
284      * @throws Exception the exception
285      */
286     public TL extractPagingInfo(TL theCommonList, DocumentWrapper<DocumentModelList> wrapDoc)
287             throws Exception {
288         AbstractCommonList commonList = (AbstractCommonList) theCommonList;
289
290         DocumentFilter docFilter = this.getDocumentFilter();
291         long pageSize = docFilter.getPageSize();
292         long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
293         // set the page size and page number
294         commonList.setPageNum(pageNum);
295         commonList.setPageSize(pageSize);
296         DocumentModelList docList = wrapDoc.getWrappedObject();
297         // Set num of items in list. this is useful to our testing framework.
298         commonList.setItemsInPage(docList.size());
299         // set the total result size
300         commonList.setTotalItems(docList.totalSize());
301
302         return (TL) commonList;
303     }
304
305     /* (non-Javadoc)
306      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#extractAllParts(org.collectionspace.services.common.document.DocumentWrapper)
307      */
308     @Override
309     public void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc)
310             throws Exception {
311
312         DocumentModel docModel = wrapDoc.getWrappedObject();
313         String[] schemas = docModel.getDeclaredSchemas();
314         Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
315         for (String schema : schemas) {
316             ObjectPartType partMeta = partsMetaMap.get(schema);
317             if (partMeta == null) {
318                 continue; // unknown part, ignore
319             }
320             Map<String, Object> unQObjectProperties = extractPart(docModel, schema, partMeta);
321             if(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA.equals(schema)) {
322                 addExtraCoreValues(docModel, unQObjectProperties);
323             }
324             addOutputPart(unQObjectProperties, schema, partMeta);
325         }
326         
327         MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
328
329         if (supportsHierarchy() == true) {
330             String showSiblings = ctx.getQueryParams().getFirst(CommonAPI.showSiblings_QP);
331             if (Tools.isTrue(showSiblings)) {
332                 showSiblings(wrapDoc, ctx);
333                 return;   // actual result is returned on ctx.addOutputPart();
334             }
335
336             String showRelations = ctx.getQueryParams().getFirst(CommonAPI.showRelations_QP);
337             if (Tools.isTrue(showRelations)) {
338                 showRelations(wrapDoc, ctx);
339                 return;   // actual result is returned on ctx.addOutputPart();
340             }
341
342             String showAllRelations = ctx.getQueryParams().getFirst(CommonAPI.showAllRelations_QP);
343             if (Tools.isTrue(showAllRelations)) {
344                 showAllRelations(wrapDoc, ctx);
345                 return;   // actual result is returned on ctx.addOutputPart();
346             }
347         }
348         
349         String currentUser = ctx.getUserId();
350         if (currentUser.equalsIgnoreCase(AuthNContext.ANONYMOUS_USER) == false) {
351                 addAccountPermissionsPart();
352         }
353     }
354     
355     private void addExtraCoreValues(DocumentModel docModel, Map<String, Object> unQObjectProperties)
356                 throws Exception {
357         unQObjectProperties.put(CollectionSpaceClient.COLLECTIONSPACE_CORE_WORKFLOWSTATE, docModel.getCurrentLifeCycleState());
358     }
359     
360     private void addAccountPermissionsPart() throws Exception {
361         Profiler profiler = new Profiler("addAccountPermissionsPart():", 1);
362         profiler.start();
363         
364         MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
365         String currentServiceName = ctx.getServiceName();
366         String workflowSubResource = "/";
367         JaxRsContext jaxRsContext = ctx.getJaxRsContext();
368         if (jaxRsContext != null) { // If not null then we're dealing with an authority item
369                 String resourceName = SecurityUtils.getResourceName(jaxRsContext.getUriInfo());
370                 workflowSubResource = workflowSubResource + resourceName + WorkflowClient.SERVICE_PATH + "/";
371         } else {
372                 workflowSubResource = workflowSubResource + currentServiceName + WorkflowClient.SERVICE_AUTHZ_SUFFIX;
373         }
374         AccountPermission accountPermission = JpaStorageUtils.getAccountPermissions(JpaStorageUtils.CS_CURRENT_USER,
375                         currentServiceName, workflowSubResource);
376         org.collectionspace.services.authorization.ObjectFactory objectFactory =
377                 new org.collectionspace.services.authorization.ObjectFactory();
378         JAXBElement<AccountPermission> ap = objectFactory.createAccountPermission(accountPermission);
379         PayloadOutputPart accountPermissionPart = new PayloadOutputPart("account_permission", ap); // REM - "account_permission" should be using a constant and not a literal
380         ctx.addOutputPart(accountPermissionPart);
381         
382         profiler.stop();
383     }
384
385     /* (non-Javadoc)
386      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#fillAllParts(org.collectionspace.services.common.document.DocumentWrapper)
387      */
388     @Override
389     public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
390
391         //TODO filling extension parts should be dynamic
392         //Nuxeo APIs lack to support stream/byte[] input, get/setting properties is
393         //not an ideal way of populating objects.
394         DocumentModel docModel = wrapDoc.getWrappedObject();
395         MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
396         PoxPayloadIn input = ctx.getInput();
397         if (input == null || input.getParts().isEmpty()) {
398             String msg = String.format("No payload found for '%s' action.", action);
399             logger.error(msg + "Ctx=" + getServiceContext().toString());
400             throw new BadRequestException(msg);
401         }
402
403         Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
404
405         //iterate over parts received and fill those parts
406         List<PayloadInputPart> inputParts = input.getParts();
407         for (PayloadInputPart part : inputParts) {
408
409             String partLabel = part.getLabel();
410             if (partLabel == null) {
411                 String msg = "Part label is missing or empty!";
412                 logger.error(msg + "Ctx=" + getServiceContext().toString());
413                 throw new BadRequestException(msg);
414             }
415
416             //skip if the part is not in metadata or if it is a system part
417             ObjectPartType partMeta = partsMetaMap.get(partLabel);
418             if (partMeta == null || isSystemPart(partLabel)) {
419                 continue;
420             }
421             fillPart(part, docModel, partMeta, action, ctx);
422         }
423     }
424
425     private boolean isSystemPart(String partLabel) {
426         boolean result = false;
427         
428         if (partLabel != null && (partLabel.equalsIgnoreCase(COLLECTIONSPACE_CORE_SCHEMA) ||
429                         partLabel.equalsIgnoreCase(ACCOUNT_PERMISSION_COMMON_PART_NAME))) {
430                 result = true;
431         }
432         
433                 return result;
434         }
435
436         /**
437      * fillPart fills an XML part into given document model
438      * @param part to fill
439      * @param docModel for the given object
440      * @param partMeta metadata for the object to fill
441      * @throws Exception
442      */
443     protected void fillPart(PayloadInputPart part, DocumentModel docModel,
444             ObjectPartType partMeta, Action action, ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx)
445             throws Exception {
446         //check if this is an xml part
447         if (part.getMediaType().equals(MediaType.APPLICATION_XML_TYPE)) {
448                 Element element = part.getElementBody();
449             Map<String, Object> objectProps = DocumentUtils.parseProperties(partMeta, element, ctx);
450                 if (action == Action.UPDATE) {
451                     this.filterReadOnlyPropertiesForPart(objectProps, partMeta);
452                 }
453                 docModel.setProperties(partMeta.getLabel(), objectProps);
454             }
455         }
456
457     /**
458      * Filters out read only properties, so they cannot be set on update.
459      * TODO: add configuration support to do this generally
460      * @param objectProps the properties parsed from the update payload
461      * @param partMeta metadata for the object to fill
462      */
463     public void filterReadOnlyPropertiesForPart(
464             Map<String, Object> objectProps, ObjectPartType partMeta) {
465         // Should add in logic to filter most of the core items on update
466         if(partMeta.getLabel().equalsIgnoreCase(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA)) {
467                 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_AT);
468                 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_BY);
469                 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_URI);
470                 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_TENANTID);
471                 // Note that the updatedAt/updatedBy fields are set internally
472                 // in DocumentModelHandler.handleCoreValues().
473         }
474     }
475
476     /**
477      * extractPart extracts an XML object from given DocumentModel
478      * @param docModel
479      * @param schema of the object to extract
480      * @param partMeta metadata for the object to extract
481      * @throws Exception
482      */
483     protected Map<String, Object> extractPart(DocumentModel docModel, String schema)
484             throws Exception {
485         return extractPart(docModel, schema, (Map<String, Object>)null);
486     }
487     
488     /**
489      * extractPart extracts an XML object from given DocumentModel
490      * @param docModel
491      * @param schema of the object to extract
492      * @param partMeta metadata for the object to extract
493      * @throws Exception
494      */
495     @Deprecated
496     protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
497             throws Exception {
498         return extractPart(docModel, schema, partMeta, null);
499     }    
500
501     /**
502      * extractPart extracts an XML object from given DocumentModel
503      * @param docModel
504      * @param schema of the object to extract
505      * @param partMeta metadata for the object to extract
506      * @throws Exception
507      */
508     protected Map<String, Object> extractPart(
509             DocumentModel docModel, 
510             String schema,
511             Map<String, Object> addToMap)
512             throws Exception {
513         Map<String, Object> result = null;
514
515         Map<String, Object> objectProps = docModel.getProperties(schema);
516         if (objectProps != null) {
517                 //unqualify properties before sending the doc over the wire (to save bandwidh)
518                 //FIXME: is there a better way to avoid duplication of a Map/Collection?
519                 Map<String, Object> unQObjectProperties =
520                         (addToMap != null) ? addToMap : (new HashMap<String, Object>());
521                 Set<Entry<String, Object>> qualifiedEntries = objectProps.entrySet();
522                 for (Entry<String, Object> entry : qualifiedEntries) {
523                     String unqProp = getUnQProperty(entry.getKey());
524                     unQObjectProperties.put(unqProp, entry.getValue());
525                 }
526                 result = unQObjectProperties;
527         }
528
529         return result;
530     }
531     
532     /**
533      * extractPart extracts an XML object from given DocumentModel
534      * @param docModel
535      * @param schema of the object to extract
536      * @param partMeta metadata for the object to extract
537      * @throws Exception
538      */
539     @Deprecated
540     protected Map<String, Object> extractPart(
541             DocumentModel docModel, String schema, ObjectPartType partMeta,
542             Map<String, Object> addToMap)
543             throws Exception {
544         Map<String, Object> result = null;
545
546         result = this.extractPart(docModel, schema, addToMap);
547
548         return result;
549     }
550     
551     /* 
552     public String getStringPropertyFromDoc(
553                 ServiceContext ctx,
554                 String csid,
555                 String propertyXPath ) throws DocumentNotFoundException, DocumentException {
556         RepositoryInstance repoSession = null;
557         boolean releaseRepoSession = false;
558         String returnValue = null;
559
560         try{ 
561                 RepositoryClientImpl repoClient = (RepositoryClientImpl)this.getRepositoryClient(ctx);
562                 repoSession = this.getRepositorySession();
563                 if (repoSession == null) {
564                         repoSession = repoClient.getRepositorySession();
565                         releaseRepoSession = true;
566                 }
567
568                 try {
569                         DocumentWrapper<DocumentModel> wrapper = repoClient.getDoc(repoSession, ctx, csid);
570                         DocumentModel docModel = wrapper.getWrappedObject();
571                         returnValue = (String) docModel.getPropertyValue(propertyXPath);
572                 } catch (PropertyException pe) {
573                         throw pe;
574                 } catch (DocumentException de) {
575                         throw de;
576                 } catch (Exception e) {
577                         if (logger.isDebugEnabled()) {
578                                 logger.debug("Caught exception ", e);
579                         }
580                         throw new DocumentException(e);
581                 } finally {
582                         if (releaseRepoSession && repoSession != null) {
583                                 repoClient.releaseRepositorySession(repoSession);
584                         }
585                 }
586         } catch (Exception e) {
587                 if (logger.isDebugEnabled()) {
588                         logger.debug("Caught exception ", e);
589                 }
590                 throw new DocumentException(e);
591         }               
592
593
594         if (logger.isWarnEnabled() == true) {
595                 logger.warn("Returned DocumentModel instance was created with a repository session that is now closed.");
596         }
597         return returnValue;
598     }
599      */
600
601     
602
603     /* (non-Javadoc)
604      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#getAuthorityRefs(org.collectionspace.services.common.document.DocumentWrapper, java.util.List)
605      */
606     @Override
607     public AuthorityRefList getAuthorityRefs(
608             String csid,
609             List<AuthRefConfigInfo> authRefConfigInfoList) throws PropertyException, Exception {
610
611         AuthorityRefList authRefList = new AuthorityRefList();
612         AbstractCommonList commonList = (AbstractCommonList) authRefList;
613         
614         DocumentFilter docFilter = this.getDocumentFilter();
615         long pageSize = docFilter.getPageSize();
616         long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
617         // set the page size and page number
618         commonList.setPageNum(pageNum);
619         commonList.setPageSize(pageSize);
620         
621         List<AuthorityRefList.AuthorityRefItem> list = authRefList.getAuthorityRefItem();
622
623         try {
624                 int iFirstToUse = (int)(pageSize*pageNum);
625                 int nFoundInPage = 0;
626                 int nFoundTotal = 0;
627                 
628                 ArrayList<RefNameServiceUtils.AuthRefInfo> foundReferences 
629                         = new ArrayList<RefNameServiceUtils.AuthRefInfo>();
630                 
631                 boolean releaseRepoSession = false;
632                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
633                 RepositoryClientImpl repoClient = (RepositoryClientImpl)this.getRepositoryClient(ctx);
634                 CoreSessionInterface repoSession = this.getRepositorySession();
635                 if (repoSession == null) {
636                         repoSession = repoClient.getRepositorySession(ctx);
637                         releaseRepoSession = true;
638                         this.setRepositorySession(repoSession); // we (the doc handler) should keep track of this repository session in case we need it
639                 }
640                 
641                 try {
642                         DocumentModel docModel = repoClient.getDoc(repoSession, ctx, csid).getWrappedObject();
643                         RefNameServiceUtils.findAuthRefPropertiesInDoc(docModel, authRefConfigInfoList, null, foundReferences);
644                         // Slightly goofy pagination support - how many refs do we expect from one object?
645                         for(RefNameServiceUtils.AuthRefInfo ari:foundReferences) {
646                                 if ((nFoundTotal >= iFirstToUse) && (nFoundInPage < pageSize)) {
647                                         if(appendToAuthRefsList(ari, list)) {
648                                                 nFoundInPage++;
649                                         nFoundTotal++;
650                                         }
651                                 } else {
652                                         nFoundTotal++;
653                                 }
654                         }
655                 } finally {
656                         if (releaseRepoSession == true) {
657                                 repoClient.releaseRepositorySession(ctx, repoSession);
658                         }
659                 }
660                 
661             // Set num of items in list. this is useful to our testing framework.
662             commonList.setItemsInPage(nFoundInPage);
663             // set the total result size
664             commonList.setTotalItems(nFoundTotal);
665             
666         } catch (PropertyException pe) {
667             String msg = "Attempted to retrieve value for invalid or missing authority field. "
668                     + "Check authority field properties in tenant bindings.";
669             logger.warn(msg, pe);
670             throw pe;
671         } catch (Exception e) {
672             if (logger.isDebugEnabled()) {
673                 logger.debug("Caught exception in getAuthorityRefs", e);
674             }
675             Response response = Response.status(
676                     Response.Status.INTERNAL_SERVER_ERROR).entity(
677                     "Failed to retrieve authority references").type(
678                     "text/plain").build();
679             throw new CSWebApplicationException(e, response);
680         }
681
682         return authRefList;
683     }
684
685     private boolean appendToAuthRefsList(RefNameServiceUtils.AuthRefInfo ari, 
686                                                 List<AuthorityRefList.AuthorityRefItem> list)
687             throws Exception {
688         String fieldName = ari.getQualifiedDisplayName();
689         try {
690                         String refNameValue = (String)ari.getProperty().getValue();
691                         AuthorityRefList.AuthorityRefItem item = authorityRefListItem(fieldName, refNameValue);
692                         if(item!=null) {        // ignore garbage values.
693                                 list.add(item);
694                                 return true;
695                         }
696         } catch(PropertyException pe) {
697                 String msg = "PropertyException on: "+ari.getProperty().getPath()+pe.getLocalizedMessage();
698                 if (logger.isDebugEnabled()) {
699                         logger.debug(msg, pe);
700                 } else {
701                         logger.error(msg);
702                 }
703         }
704         return false;
705     }
706
707     /**
708      * Fill in all the values to be returned in the authrefs payload for this item.
709      * 
710      * @param authRefFieldName
711      * @param refName
712      * @return
713      */
714     private AuthorityRefList.AuthorityRefItem authorityRefListItem(String authRefFieldName, String refName) {
715         //
716         // Find the CSID for the authority item
717         //
718         String csid = null;
719         try {
720                         DocumentModel docModel = NuxeoUtils.getDocModelForRefName(getServiceContext(), refName, getServiceContext().getResourceMap());
721                         csid = NuxeoUtils.getCsid(docModel);
722                 } catch (Exception e1) {
723                         String msg = String.format("Could not find CSID for authority reference with refname = %s.", refName);
724                         if (logger.isDebugEnabled()) {
725                                 logger.debug(msg, e1);
726                         } else {
727                                 logger.error(msg);
728                         }
729                 }
730         
731         AuthorityRefList.AuthorityRefItem ilistItem = new AuthorityRefList.AuthorityRefItem();
732         try {
733             RefNameUtils.AuthorityTermInfo termInfo = RefNameUtils.parseAuthorityTermInfo(refName);
734             if (Tools.isEmpty(csid) == false) {
735                 ilistItem.setCsid(csid);
736             }
737             ilistItem.setRefName(refName);
738             ilistItem.setAuthDisplayName(termInfo.inAuthority.displayName);
739             ilistItem.setItemDisplayName(termInfo.displayName);
740             ilistItem.setSourceField(authRefFieldName);
741             ilistItem.setUri(termInfo.getRelativeUri());
742         } catch (Exception e) {
743                 ilistItem = null;
744                 String msg = String.format("Trouble parsing refName from value: %s in field: %s. Error message: %s.",
745                                 refName, authRefFieldName + e.getLocalizedMessage());
746                 if (logger.isDebugEnabled()) {
747                         logger.debug(msg, e);
748                 } else {
749                         logger.error(msg);
750                 }
751         }
752         
753         return ilistItem;
754     }
755
756     /**
757      * Returns the primary value from a list of values.
758      *
759      * Assumes that the first value is the primary value.
760      * This assumption may change when and if the primary value
761      * is identified explicitly.
762      *
763      * @param values a list of values.
764      * @param propertyName the name of a property through
765      *     which the value can be extracted.
766      * @return the primary value.
767     protected String primaryValueFromMultivalue(List<Object> values, String propertyName) {
768         String primaryValue = "";
769         if (values == null || values.size() == 0) {
770             return primaryValue;
771         }
772         Object value = values.get(0);
773         if (value instanceof String) {
774             if (value != null) {
775                 primaryValue = (String) value;
776             }
777        // Multivalue group of fields
778        } else if (value instanceof Map) {
779             if (value != null) {
780                 Map map = (Map) value;
781                 if (map.values().size() > 0) {
782                     if (map.get(propertyName) != null) {
783                       primaryValue = (String) map.get(propertyName);
784                     }
785                 }
786             }
787        } else {
788             logger.warn("Unexpected type for property " + propertyName
789                     + " in multivalue list: not String or Map.");
790        }
791        return primaryValue;
792     }
793      */
794
795     /**
796      * Gets a simple property from the document.
797      *
798      * For completeness, as this duplicates DocumentModel method. 
799      *
800      * @param docModel The document model to get info from
801      * @param schema The name of the schema (part)
802      * @param propertyName The simple scalar property type
803      * @return property value as String
804      */
805     protected String getSimpleStringProperty(DocumentModel docModel, String schema, String propName) {
806         String xpath = "/"+schema+":"+propName;
807         try {
808                 return (String) NuxeoUtils.getProperyValue(docModel, xpath); //docModel.getPropertyValue(xpath);
809         } catch(PropertyException pe) {
810                 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a simple String property?"
811                                 +pe.getLocalizedMessage());
812         } catch(ClassCastException cce) {
813                 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a scalar String property?"
814                                 +cce.getLocalizedMessage());
815         } catch(Exception e) {
816                 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
817                                 +e.getLocalizedMessage());
818         }
819     }
820
821     /**
822      * Gets first of a repeating list of scalar values, as a String, from the document.
823      *
824      * @param docModel The document model to get info from
825      * @param schema The name of the schema (part)
826      * @param listName The name of the scalar list property
827      * @return first value in list, as a String, or empty string if the list is empty
828      */
829         protected String getFirstRepeatingStringProperty(DocumentModel docModel,
830                         String schema, String listName) {
831                 String xpath = "/" + schema + ":" + listName + "/[0]";
832                 try {
833                         return (String) NuxeoUtils.getProperyValue(docModel, xpath); // docModel.getPropertyValue(xpath);
834                 } catch (PropertyException pe) {
835                         throw new RuntimeException("Problem retrieving property {" + xpath
836                                         + "}. Not a repeating scalar?" + pe.getLocalizedMessage());
837                 } catch (IndexOutOfBoundsException ioobe) {
838                         // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
839                         return ""; // gracefully handle missing elements
840                 } catch (ClassCastException cce) {
841                         throw new RuntimeException("Problem retrieving property {" + xpath
842                                         + "} as String. Not a repeating String property?"
843                                         + cce.getLocalizedMessage());
844                 } catch (Exception e) {
845                         throw new RuntimeException("Unknown problem retrieving property {"
846                                         + xpath + "}." + e.getLocalizedMessage());
847                 }
848         }
849    
850     /**
851      * Gets first of a repeating list of scalar values, as a String, from the document.
852      *
853      * @param docModel The document model to get info from
854      * @param schema The name of the schema (part)
855      * @param listName The name of the scalar list property
856      * @return first value in list, as a String, or empty string if the list is empty
857      */
858     protected String getStringValueInPrimaryRepeatingComplexProperty(
859                 DocumentModel docModel, String schema, String complexPropertyName, String fieldName) {          
860         String result = null;
861         
862         String xpath = "/" + NuxeoUtils.getPrimaryXPathPropertyName(schema, complexPropertyName, fieldName);
863         try {
864                 result = (String) NuxeoUtils.getProperyValue(docModel, xpath); //docModel.getPropertyValue(xpath);
865         } catch(PropertyException pe) {
866                 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad propertyNames?"
867                                 +pe.getLocalizedMessage());
868         } catch(IndexOutOfBoundsException ioobe) {
869                 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
870                 result = "";    // gracefully handle missing elements
871         } catch(ClassCastException cce) {
872                 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
873                                 +cce.getLocalizedMessage());
874         } catch(NullPointerException npe) {
875                 // Getting here because of a bug in Nuxeo when value in repository is unknown/empty/null
876                 logger.warn(String.format("Nuxeo repo unexpectedly returned an Null Pointer Exception when asked for the value of {%s}.",
877                                 xpath));
878         } catch(Exception e) {
879                 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
880                                 +e.getLocalizedMessage());
881         }
882         
883         return result;
884     }
885    
886     /**
887      * Gets XPath value from schema. Note that only "/" and "[n]" are
888      * supported for xpath. Can omit grouping elements for repeating complex types, 
889      * e.g., "fieldList/[0]" can be used as shorthand for "fieldList/field[0]" and
890      * "fieldGroupList/[0]/field" can be used as shorthand for "fieldGroupList/fieldGroup[0]/field".
891      * If there are no entries for a list of scalars or for a list of complex types, 
892      * a 0 index expression (e.g., "fieldGroupList/[0]/field") will safely return an empty
893      * string. A non-zero index will throw an IndexOutOfBoundsException if there are not
894      * that many elements in the list. 
895      * N.B.: This does not follow the XPath spec - indices are 0-based, not 1-based.
896      *
897      * @param docModel The document model to get info from
898      * @param schema The name of the schema (part)
899      * @param xpath The XPath expression (without schema prefix)
900      * @return value the indicated property value as a String
901      * @throws DocumentException 
902      */
903         protected Object getListResultValue(DocumentModel docModel, // REM - CSPACE-5133
904                         String schema, ListResultField field) throws DocumentException {
905                 Object result = null;
906
907                 result = NuxeoUtils.getXPathValue(docModel, schema, field.getXpath());
908                 
909                 return result;
910         }
911         
912         protected String getStringValue(DocumentModel docModel,
913                         String schema, ListResultField field) throws DocumentException {
914                 String result = null;
915                 
916                 Object value = getListResultValue(docModel, schema, field);
917                 if (value != null && value instanceof String) {
918                         String strValue = (String) value;
919                         if (strValue.trim().isEmpty() == false) {
920                                 result = strValue;
921                         }
922                 }
923                 
924                 return result;
925         }
926    
927     protected void removeFromList(List<RelationsCommonList.RelationListItem> list, RelationsCommonList.RelationListItem item) {
928         list.remove(item);
929     }
930         
931     private void itemToString(StringBuilder sb, String prefix, RelationsCommonList.RelationListItem item ) {
932         sb.append(prefix);
933                 sb.append((item.getCsid()!= null)?item.getCsid():"NO CSID");
934         sb.append(": ["); 
935         sb.append((item.getSubject().getCsid()!=null)?item.getSubject().getCsid():item.getSubject().getRefName());
936         sb.append("]--");
937         sb.append(item.getPredicate());
938         sb.append("-->["); 
939         sb.append((item.getObject().getCsid()!=null)?item.getObject().getCsid():item.getObject().getRefName());
940         sb.append("]");
941     }
942     
943     private String dumpList(List<RelationsCommonList.RelationListItem> list, String label) {
944         StringBuilder sb = new StringBuilder();
945         String s;
946         if (list.size() > 0) {
947             sb.append("=========== " + label + " ==========" + CR);
948         }
949         for (RelationsCommonList.RelationListItem item : list) {
950                 itemToString(sb, "==  ", item);
951                 sb.append(CR);
952         }
953         return sb.toString();
954     }
955     
956     /** @return null on parent not found
957      */
958     protected String getParentCSID(String thisCSID) throws Exception {
959         String parentCSID = null;
960         try {
961             String predicate = RelationshipType.HAS_BROADER.value();
962             RelationsCommonList parentListOuter = getRelations(thisCSID, null, predicate);
963             List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
964             if (parentList != null) {
965                 if (parentList.size() == 0) {
966                     return null;
967                 }
968                 RelationsCommonList.RelationListItem relationListItem = parentList.get(0);
969                 parentCSID = relationListItem.getObjectCsid();
970             }
971             return parentCSID;
972         } catch (Exception e) {
973             logger.error("Could not find parent for this: " + thisCSID, e);
974             return null;
975         }
976     }
977     
978     protected List<RelationsCommonList.RelationListItem> newRelationsCommonList() {
979         List<RelationsCommonList.RelationListItem> result = new ArrayList<RelationsCommonList.RelationListItem>();
980         return result;
981     }
982
983     public void showSiblings(DocumentWrapper<DocumentModel> wrapDoc,
984             MultipartServiceContext ctx) throws Exception {
985         String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
986         String parentCSID = getParentCSID(thisCSID);
987         if (parentCSID == null) {
988             logger.warn("~~~~~\r\n~~~~ Could not find parent for this: " + thisCSID);
989             return;
990         }
991
992         String predicate = RelationshipType.HAS_BROADER.value();
993         RelationsCommonList siblingListOuter = getRelations(null, parentCSID, predicate);
994         List<RelationsCommonList.RelationListItem> siblingList = siblingListOuter.getRelationListItem();
995
996         List<RelationsCommonList.RelationListItem> toRemoveList = newRelationsCommonList();
997
998
999         RelationsCommonList.RelationListItem item = null;
1000         for (RelationsCommonList.RelationListItem sibling : siblingList) {
1001             if (thisCSID.equals(sibling.getSubjectCsid())) {
1002                 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.
1003             }
1004         }
1005         //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.
1006         for (RelationsCommonList.RelationListItem self : toRemoveList) {
1007             removeFromList(siblingList, self);
1008         }
1009
1010         long siblingSize = siblingList.size();
1011         siblingListOuter.setTotalItems(siblingSize);
1012         siblingListOuter.setItemsInPage(siblingSize);
1013         if(logger.isTraceEnabled()) {
1014             String dump = dumpList(siblingList, "Siblings of: "+thisCSID);
1015             logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showSiblings ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1016         }
1017
1018         PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, siblingListOuter);
1019         ctx.addOutputPart(relationsPart);
1020     }
1021     
1022     public void showRelations(DocumentWrapper<DocumentModel> wrapDoc,
1023             MultipartServiceContext ctx) throws Exception {
1024         String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
1025
1026         String predicate = RelationshipType.HAS_BROADER.value();
1027         RelationsCommonList parentListOuter = getRelations(thisCSID, null, predicate);
1028         List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
1029
1030         RelationsCommonList childrenListOuter = getRelations(null, thisCSID, predicate);
1031         List<RelationsCommonList.RelationListItem> childrenList = childrenListOuter.getRelationListItem();
1032
1033         if(logger.isTraceEnabled()) {
1034             String dump = dumpLists(thisCSID, parentList, childrenList, null);
1035             logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showRelations ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1036         }
1037         
1038         //Assume that there are more children than parents.  Will be true for parent/child, but maybe not for other relations.
1039         //Now add all parents to our childrenList, to be able to return just one list of consolidated results.
1040         //Not optimal, but that's the current design spec.
1041         long added = 0;
1042         for (RelationsCommonList.RelationListItem parent : parentList) {
1043             childrenList.add(parent);
1044             added++;
1045         }
1046         long childrenSize = childrenList.size();
1047         childrenListOuter.setTotalItems(childrenSize);
1048         childrenListOuter.setItemsInPage(childrenListOuter.getItemsInPage() + added);
1049
1050         PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, childrenListOuter);
1051         ctx.addOutputPart(relationsPart);
1052     }
1053     
1054     public void showAllRelations(DocumentWrapper<DocumentModel> wrapDoc, MultipartServiceContext ctx) throws Exception {
1055         String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
1056
1057         RelationsCommonList subjectListOuter = getRelations(thisCSID, null, null);   //  nulls are wildcards:  predicate=*, and object=*
1058         List<RelationsCommonList.RelationListItem> subjectList = subjectListOuter.getRelationListItem();
1059
1060         RelationsCommonList objectListOuter = getRelations(null, thisCSID, null);   //  nulls are wildcards:  subject=*, and predicate=*
1061         List<RelationsCommonList.RelationListItem> objectList = objectListOuter.getRelationListItem();
1062
1063         if(logger.isTraceEnabled()) {
1064             String dump = dumpLists(thisCSID, subjectList, objectList, null);
1065             logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showAllRelations ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1066         }
1067         //  MERGE LISTS:
1068         subjectList.addAll(objectList);
1069
1070         //now subjectList actually has records BOTH where thisCSID is subject and object.
1071         long relatedSize = subjectList.size();
1072         subjectListOuter.setTotalItems(relatedSize);
1073         subjectListOuter.setItemsInPage(relatedSize);
1074
1075         PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, subjectListOuter);
1076         ctx.addOutputPart(relationsPart);
1077     }
1078
1079     private String dumpLists(String itemCSID,
1080             List<RelationsCommonList.RelationListItem> parentList,
1081             List<RelationsCommonList.RelationListItem> childList,
1082             List<RelationsCommonList.RelationListItem> actionList) {
1083         StringBuilder sb = new StringBuilder();
1084         sb.append("itemCSID: " + itemCSID + CR);
1085         if(parentList!=null) {
1086                 sb.append(dumpList(parentList, "parentList"));
1087         }
1088         if(childList!=null) {
1089                 sb.append(dumpList(childList, "childList"));
1090         }
1091         if(actionList!=null) {
1092                 sb.append(dumpList(actionList, "actionList"));
1093         }
1094         return sb.toString();
1095     }
1096     
1097     //================= TODO: move this to common, refactoring this and  CollectionObjectResource.java
1098     public RelationsCommonList getRelations(String subjectCSID, String objectCSID, String predicate) throws Exception {
1099         ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1100         MultivaluedMap<String, String> queryParams = ctx.getQueryParams();
1101         queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1102         queryParams.putSingle(IRelationsManager.SUBJECT_QP, subjectCSID);
1103         queryParams.putSingle(IRelationsManager.OBJECT_QP, objectCSID);
1104
1105         RelationResource relationResource = new RelationResource(); //is this still acting like a singleton as it should be?
1106         RelationsCommonList relationsCommonList = relationResource.getList(ctx);
1107         return relationsCommonList;
1108     }
1109     //============================= END TODO refactor ==========================
1110     
1111     // this method calls the RelationResource to have it create the relations and persist them.
1112     private void createRelations(List<RelationsCommonList.RelationListItem> inboundList,
1113                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) throws Exception {
1114         for (RelationsCommonList.RelationListItem item : inboundList) {
1115             RelationsCommon rc = new RelationsCommon();
1116             //rc.setCsid(item.getCsid());
1117             //todo: assignTo(item, rc);
1118             RelationsDocListItem itemSubject = item.getSubject();
1119             RelationsDocListItem itemObject = item.getObject();
1120
1121             // Set at least one of CSID and refName for Subject and Object
1122             // Either value might be null for for each of Subject and Object 
1123             String subjectCsid = itemSubject.getCsid();
1124             rc.setSubjectCsid(subjectCsid);
1125
1126             String objCsid = itemObject.getCsid();
1127             rc.setObjectCsid(objCsid);
1128
1129             rc.setSubjectRefName(itemSubject.getRefName());
1130             rc.setObjectRefName(itemObject.getRefName());
1131
1132             rc.setRelationshipType(item.getPredicate());
1133             rc.setRelationshipMetaType(item.getRelationshipMetaType());
1134             //RelationshipType  foo = (RelationshipType.valueOf(item.getPredicate())) ;
1135             //rc.setPredicate(foo);     //this must be one of the type found in the enum in  services/jaxb/src/main/resources/relations_common.xsd
1136
1137             // This is superfluous, since it will be fetched by the Relations Create logic.
1138             rc.setSubjectDocumentType(itemSubject.getDocumentType());
1139             rc.setObjectDocumentType(itemObject.getDocumentType());
1140
1141             // This is superfluous, since it will be fetched by the Relations Create logic.
1142             rc.setSubjectUri(itemSubject.getUri());
1143             rc.setObjectUri(itemObject.getUri());
1144             // May not have the info here. Only really require CSID or refName. 
1145             // Rest is handled in the Relation create mechanism
1146             //uriPointsToSameAuthority(itemSubject.getUri(), itemObject.getUri());
1147
1148             PoxPayloadOut payloadOut = new PoxPayloadOut(RelationClient.SERVICE_PAYLOAD_NAME);
1149             PayloadOutputPart outputPart = new PayloadOutputPart(RelationClient.SERVICE_COMMONPART_NAME, rc);
1150             payloadOut.addPart(outputPart);
1151             RelationResource relationResource = new RelationResource();
1152             Response res = relationResource.create(ctx, ctx.getResourceMap(),
1153                     ctx.getUriInfo(), payloadOut.toXML());    //NOTE ui recycled from above to pass in unknown query params.
1154         }
1155     }
1156     
1157     // Note that item2 may be sparse (only refName, no CSID for subject or object)
1158     // But item1 must not be sparse 
1159     private boolean itemsEqual(RelationsCommonList.RelationListItem item1, RelationsCommonList.RelationListItem item2) {
1160         if (item1 == null || item2 == null) {
1161             return false;
1162         }
1163         RelationsDocListItem subj1 = item1.getSubject();
1164         RelationsDocListItem subj2 = item2.getSubject();
1165         RelationsDocListItem obj1 = item1.getObject();
1166         RelationsDocListItem obj2 = item2.getObject();
1167         
1168         String subj1Csid = subj1.getCsid();
1169         String subj2Csid = subj2.getCsid();
1170         String subj1RefName = subj1.getRefName();
1171         String subj2RefName = subj2.getRefName();
1172
1173         String obj1Csid = obj1.getCsid();
1174         String obj2Csid = obj2.getCsid();
1175         String obj1RefName = obj1.getRefName();
1176         String obj2RefName = obj2.getRefName();
1177         
1178         String item1Metatype = item1.getRelationshipMetaType();
1179         item1Metatype = item1Metatype != null ? item1Metatype : EMPTYSTR;
1180         
1181         String item2Metatype = item2.getRelationshipMetaType();
1182         item2Metatype = item2Metatype != null ? item2Metatype : EMPTYSTR;
1183         
1184         boolean isEqual = (subj1Csid.equals(subj2Csid) || ((subj2Csid==null)  && subj1RefName.equals(subj2RefName)))
1185                 && (obj1Csid.equals(obj2Csid)   || ((obj2Csid==null)   && obj1RefName.equals(obj2RefName)))
1186                 // predicate is proper, but still allow relationshipType
1187                 && (item1.getPredicate().equals(item2.getPredicate())
1188                         ||  ((item2.getPredicate()==null)  && item1.getRelationshipType().equals(item2.getRelationshipType())))
1189                 // Allow missing docTypes, so long as they do not conflict
1190                 && (obj1.getDocumentType().equals(obj2.getDocumentType()) || obj2.getDocumentType()==null)
1191                 && (subj1.getDocumentType().equals(subj2.getDocumentType()) || subj2.getDocumentType()==null)
1192                 && (item1Metatype.equalsIgnoreCase(item2Metatype));
1193         return isEqual;
1194     }
1195     
1196     // Note that the item argument may be sparse (only refName, no CSID for subject or object)
1197     // But the list items must not be sparse
1198     private RelationsCommonList.RelationListItem findInList(
1199                 List<RelationsCommonList.RelationListItem> list, 
1200                 RelationsCommonList.RelationListItem item) {
1201         RelationsCommonList.RelationListItem foundItem = null;
1202         for (RelationsCommonList.RelationListItem listItem : list) {
1203             if (itemsEqual(listItem, item)) {   //equals must be defined, else
1204                 foundItem = listItem;
1205                 break;
1206             }
1207         }
1208         return foundItem;
1209     }
1210     
1211     /**  updateRelations strategy:
1212      *
1213      *
1214     go through inboundList, remove anything from childList that matches  from childList
1215     go through inboundList, remove anything from parentList that matches  from parentList
1216     go through parentList, delete all remaining
1217     go through childList, delete all remaining
1218     go through actionList, add all remaining.
1219     check for duplicate children
1220     check for more than one parent.
1221     
1222     inboundList                           parentList                      childList          actionList
1223     ----------------                          ---------------                  ----------------       ----------------
1224     child-a                                   parent-c                        child-a             child-b
1225     child-b                                   parent-d                        child-c
1226     parent-a
1227      *
1228      *
1229      */
1230     private RelationsCommonList updateRelations(
1231             String itemCSID, PoxPayloadIn input, DocumentWrapper<DocumentModel> wrapDoc, boolean forUpdate)
1232             throws Exception {
1233         if (logger.isTraceEnabled()) {
1234             logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID);
1235         }
1236         PayloadInputPart part = input.getPart(RelationClient.SERVICE_COMMON_LIST_NAME);        //input.getPart("relations_common");
1237         if (part == null) {
1238             return null;  //nothing to do--they didn't send a list of relations.
1239         }
1240         RelationsCommonList relationsCommonListBody = (RelationsCommonList) part.getBody();
1241         List<RelationsCommonList.RelationListItem> inboundList = relationsCommonListBody.getRelationListItem();
1242         List<RelationsCommonList.RelationListItem> actionList = newRelationsCommonList();
1243         List<RelationsCommonList.RelationListItem> childList = null;
1244         List<RelationsCommonList.RelationListItem> parentList = null;
1245         DocumentModel docModel = wrapDoc.getWrappedObject();
1246                 String itemRefName = (String) docModel.getProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
1247                         CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME);
1248
1249                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1250         //Do magic replacement of ${itemCSID} and fix URI's.
1251         fixupInboundListItems(ctx, inboundList, docModel, itemCSID);
1252
1253         final String HAS_BROADER = RelationshipType.HAS_BROADER.value();
1254         UriInfo uriInfo = ctx.getUriInfo();
1255         MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
1256
1257         if (forUpdate) {
1258             //Run getList() once as sent to get childListOuter:
1259             String predicate = RelationshipType.HAS_BROADER.value();
1260             queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1261             queryParams.putSingle(IRelationsManager.SUBJECT_QP, null);
1262             queryParams.putSingle(IRelationsManager.SUBJECT_TYPE_QP, null);
1263             queryParams.putSingle(IRelationsManager.OBJECT_QP, itemCSID);
1264             queryParams.putSingle(IRelationsManager.OBJECT_TYPE_QP, null);
1265             
1266             RelationResource relationResource = new RelationResource();
1267             RelationsCommonList childListOuter = relationResource.getList(ctx);    // Knows all query params because they are in the context.
1268
1269             //Now run getList() again, leaving predicate, swapping subject and object, to get parentListOuter.
1270             queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1271             queryParams.putSingle(IRelationsManager.SUBJECT_QP, itemCSID);
1272             queryParams.putSingle(IRelationsManager.OBJECT_QP, null);
1273             RelationsCommonList parentListOuter = relationResource.getList(ctx);
1274
1275
1276             childList = childListOuter.getRelationListItem();
1277             parentList = parentListOuter.getRelationListItem();
1278
1279             if (parentList.size() > 1) {
1280                 throw new Exception("Too many parents for object: " + itemCSID + " list: " + dumpList(parentList, "parentList"));
1281             }
1282
1283             if (logger.isTraceEnabled()) {
1284                 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " got existing relations.");
1285             }
1286         }
1287
1288         for (RelationsCommonList.RelationListItem inboundItem : inboundList) {
1289             // Note that the relations may specify the other (non-item) bit with a refName, not a CSID,
1290             // and so the CSID for those may be null
1291             if(inboundItem.getPredicate().equals(HAS_BROADER)) {
1292                 // Look for parents and children
1293                 if(itemCSID.equals(inboundItem.getObject().getCsid())
1294                                 || itemRefName.equals(inboundItem.getObject().getRefName())) {
1295                         //then this is an item that says we have a child.  That child is inboundItem
1296                         RelationsCommonList.RelationListItem childItem =
1297                                         (childList == null) ? null : findInList(childList, inboundItem);
1298                         if (childItem != null) {
1299                         if (logger.isTraceEnabled()) {
1300                                 StringBuilder sb = new StringBuilder();
1301                                 itemToString(sb, "== Child: ", childItem);
1302                             logger.trace("Found inboundChild in current child list: " + sb.toString());
1303                         }
1304                                 removeFromList(childList, childItem);    //exists, just take it off delete list
1305                         } else {
1306                         if (logger.isTraceEnabled()) {
1307                                 StringBuilder sb = new StringBuilder();
1308                                 itemToString(sb, "== Child: ", inboundItem);
1309                             logger.trace("inboundChild not in current child list, will add: " + sb.toString());
1310                         }
1311                                 actionList.add(inboundItem);   //doesn't exist as a child, but is a child.  Add to additions list
1312                                 String newChildCsid = inboundItem.getSubject().getCsid();
1313                                 if(newChildCsid == null) {
1314                                         String newChildRefName = inboundItem.getSubject().getRefName();
1315                                         if (newChildRefName == null) {
1316                                                 throw new RuntimeException("Child with no CSID or refName!");
1317                                         }
1318                             if (logger.isTraceEnabled()) {
1319                                 logger.trace("Fetching CSID for child with only refname: "+newChildRefName);
1320                             }
1321                                 DocumentModel newChildDocModel = 
1322                                         NuxeoBasedResource.getDocModelForRefName(getServiceContext(), 
1323                                                         newChildRefName, getServiceContext().getResourceMap());
1324                                 newChildCsid = getCsid(newChildDocModel);
1325                                 }
1326                                 ensureChildHasNoOtherParents(ctx, queryParams, newChildCsid);
1327                         }
1328
1329                 } else if (itemCSID.equals(inboundItem.getSubject().getCsid())
1330                                         || itemRefName.equals(inboundItem.getSubject().getRefName())) {
1331                         //then this is an item that says we have a parent.  inboundItem is that parent.
1332                         RelationsCommonList.RelationListItem parentItem =
1333                                         (parentList == null) ? null : findInList(parentList, inboundItem);
1334                         if (parentItem != null) {
1335                                 removeFromList(parentList, parentItem);    //exists, just take it off delete list
1336                         } else {
1337                                 actionList.add(inboundItem);   //doesn't exist as a parent, but is a parent. Add to additions list
1338                         }
1339                 } else {
1340                     logger.error("Parent/Child Element didn't link to this item. inboundItem: " + inboundItem);
1341                 }
1342             } else {
1343                 logger.warn("Non-parent relation ignored. inboundItem: " + inboundItem);
1344             }
1345         }
1346         if (logger.isTraceEnabled()) {
1347             String dump = dumpLists(itemCSID, parentList, childList, actionList);
1348             logger.trace("~~~~~~~~~~~~~~~~~~~~~~dump~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1349         }
1350         if (forUpdate) {
1351             if (logger.isTraceEnabled()) {
1352                 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " deleting "
1353                         + parentList.size() + " existing parents and " + childList.size() + " existing children.");
1354             }
1355             deleteRelations(parentList, ctx, "parentList");               //todo: there are items appearing on both lists....april 20.
1356             deleteRelations(childList, ctx, "childList");
1357         }
1358         if (logger.isTraceEnabled()) {
1359             logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " adding "
1360                     + actionList.size() + " new parents and children.");
1361         }
1362         createRelations(actionList, ctx);
1363         if (logger.isTraceEnabled()) {
1364             logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " done.");
1365         }
1366         // We return all elements on the inbound list, since we have just worked to make them exist in the system
1367         // and be non-redundant, etc.  That list came from relationsCommonListBody, so it is still attached to it, just pass that back.
1368         return relationsCommonListBody;
1369     }
1370     
1371     /** Performs substitution for ${itemCSID} (see CommonAPI.AuthorityItemCSID_REPLACE for constant)
1372      *   and sets URI correctly for related items.
1373      *   Operates directly on the items in the list.  Does not change the list ordering, does not add or remove any items.
1374      */
1375     protected void fixupInboundListItems(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1376             List<RelationsCommonList.RelationListItem> inboundList,
1377             DocumentModel docModel,
1378             String itemCSID) throws Exception {
1379         String thisURI = this.getUri(docModel);
1380         // WARNING:  the two code blocks below are almost identical  and seem to ask to be put in a generic method.
1381         //                    beware of the little diffs in  inboundItem.setObjectCsid(itemCSID); and   inboundItem.setSubjectCsid(itemCSID); in the two blocks.
1382         for (RelationsCommonList.RelationListItem inboundItem : inboundList) {
1383             RelationsDocListItem inboundItemObject = inboundItem.getObject();
1384             RelationsDocListItem inboundItemSubject = inboundItem.getSubject();
1385
1386             if (CommonAPI.AuthorityItemCSID_REPLACE.equalsIgnoreCase(inboundItemObject.getCsid())) {
1387                 inboundItem.setObjectCsid(itemCSID);
1388                 inboundItemObject.setCsid(itemCSID);
1389                 //inboundItemObject.setUri(getUri(docModel));
1390             } else {
1391                 /*
1392                 String objectCsid = inboundItemObject.getCsid();
1393                 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(getRepositorySession(), ctx, objectCsid);    //null if not found.
1394                 DocumentWrapper wrapper = new DocumentWrapperImpl(itemDocModel);
1395                 String uri = this.getRepositoryClient(ctx).getDocURI(wrapper);
1396                 inboundItemObject.setUri(uri);    //CSPACE-4037
1397                  */
1398             }
1399             //uriPointsToSameAuthority(thisURI, inboundItemObject.getUri());    //CSPACE-4042
1400
1401             if (CommonAPI.AuthorityItemCSID_REPLACE.equalsIgnoreCase(inboundItemSubject.getCsid())) {
1402                 inboundItem.setSubjectCsid(itemCSID);
1403                 inboundItemSubject.setCsid(itemCSID);
1404                 //inboundItemSubject.setUri(getUri(docModel));
1405             } else {
1406                 /*
1407                 String subjectCsid = inboundItemSubject.getCsid();
1408                 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(getRepositorySession(), ctx, subjectCsid);    //null if not found.
1409                 DocumentWrapper wrapper = new DocumentWrapperImpl(itemDocModel);
1410                 String uri = this.getRepositoryClient(ctx).getDocURI(wrapper);
1411                 inboundItemSubject.setUri(uri);    //CSPACE-4037
1412                  */
1413             }
1414             //uriPointsToSameAuthority(thisURI, inboundItemSubject.getUri());  //CSPACE-4042
1415
1416         }
1417     }
1418
1419     private void ensureChildHasNoOtherParents(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1420                 MultivaluedMap<String, String> queryParams, String childCSID) {
1421         logger.trace("ensureChildHasNoOtherParents for: " + childCSID );
1422         queryParams.putSingle(IRelationsManager.SUBJECT_QP, childCSID);
1423         queryParams.putSingle(IRelationsManager.PREDICATE_QP, RelationshipType.HAS_BROADER.value());
1424         queryParams.putSingle(IRelationsManager.OBJECT_QP, null);  //null means ANY
1425         
1426         RelationResource relationResource = new RelationResource();
1427         RelationsCommonList parentListOuter = relationResource.getList(ctx);
1428         List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
1429         //logger.warn("ensureChildHasNoOtherParents preparing to delete relations on "+childCSID+"\'s parent list: \r\n"+dumpList(parentList, "duplicate parent list"));
1430         deleteRelations(parentList, ctx, "parentList-delete");
1431     }
1432
1433     private void deleteRelations(List<RelationsCommonList.RelationListItem> list,
1434                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1435                 String listName) {
1436         try {
1437             for (RelationsCommonList.RelationListItem item : list) {
1438                 RelationResource relationResource = new RelationResource();
1439                 if(logger.isTraceEnabled()) {
1440                         StringBuilder sb = new StringBuilder();
1441                         itemToString(sb, "==== TO DELETE: ", item);
1442                         logger.trace(sb.toString());
1443                 }
1444                 Response res = relationResource.deleteWithParentCtx(ctx, item.getCsid());
1445                 if (logger.isDebugEnabled()) {
1446                         logger.debug("Status of authority item deleteRelations method call was: " + res.getStatus());
1447                 }
1448             }
1449         } catch (Throwable t) {
1450             String msg = "Unable to deleteRelations: " + Tools.errorToString(t, true);
1451             logger.error(msg);
1452         }
1453     }
1454     
1455     // Note that we must do this after we have completed the Update, so that the repository has the
1456     // info for the item itself. The relations code must call into the repo to get info for each end.
1457     // This could be optimized to pass in the parent docModel, since it will often be one end.
1458     // Nevertheless, we should complete the item save before we do work on the relations, especially
1459     // since a save on Create might fail, and we would not want to create relations for something
1460     // that may not be created...
1461     private void handleRelationsPayload(DocumentWrapper<DocumentModel> wrapDoc, boolean forUpdate) throws Exception {
1462         ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1463         PoxPayloadIn input = ctx.getInput();
1464         DocumentModel documentModel = (wrapDoc.getWrappedObject());
1465         String itemCsid = documentModel.getName();
1466
1467         //Updates relations part
1468         RelationsCommonList relationsCommonList = updateRelations(itemCsid, input, wrapDoc, forUpdate);
1469
1470         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
1471         ctx.setProperty(RelationClient.SERVICE_COMMON_LIST_NAME, payloadOutputPart);
1472
1473         //now we add part for relations list
1474         //ServiceContext ctx = getServiceContext();
1475         //PayloadOutputPart foo = (PayloadOutputPart) ctx.getProperty(RelationClient.SERVICE_COMMON_LIST_NAME);
1476         ctx.getOutput().addPart(payloadOutputPart);
1477     }
1478
1479     /**
1480      * Checks to see if the refName has changed, and if so, 
1481      * uses utilities to find all references and update them to use the new refName.
1482      * @throws Exception 
1483      */
1484     protected void handleRefNameReferencesUpdate() throws Exception {
1485         if (hasRefNameUpdate() == true) {
1486             ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1487             RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient = getRepositoryClient(ctx);
1488             CoreSessionInterface repoSession = this.getRepositorySession();
1489             
1490             // Update all the relationship records that referred to the old refName
1491             RefNameServiceUtils.updateRefNamesInRelations(ctx, repoClient, repoSession,
1492                     oldRefNameOnUpdate, newRefNameOnUpdate);
1493         }
1494     }
1495     
1496     protected String getRefNameUpdate() {
1497         String result = null;
1498         
1499         if (hasRefNameUpdate() == true) {
1500                 result = newRefNameOnUpdate;
1501                 if (logger.isDebugEnabled() == true) {
1502                         logger.debug(String.format("There was a refName update.  New: %s Old: %s" ,
1503                                         newRefNameOnUpdate, oldRefNameOnUpdate));
1504                 }
1505         }
1506         
1507         return result;
1508     }
1509     
1510     /*
1511      * Note: The Vocabulary document handler overrides this method.
1512      */
1513     protected String getRefPropName() {
1514         return ServiceBindingUtils.AUTH_REF_PROP;
1515     }
1516
1517     
1518     
1519 }