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