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