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