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