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