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