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