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