]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
78c134e5200e2a8ea6f4a1a2c030916c2cd60896
[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 (logger.isTraceEnabled() && werePartsFilled == false) {
412                 String msg = String.format("%s request had no XML payload parts processed in the request.  Could be a payload with only relations-common-list request.", 
413                                 action.toString());
414                 logger.trace(msg);
415         }
416     }
417
418     private boolean isSystemPart(String partLabel) {
419         boolean result = false;
420         
421         if (partLabel != null && (partLabel.equalsIgnoreCase(COLLECTIONSPACE_CORE_SCHEMA) ||
422                         partLabel.equalsIgnoreCase(ACCOUNT_PERMISSION_COMMON_PART_NAME))) {
423                 result = true;
424         }
425         
426                 return result;
427         }
428
429         /**
430      * fillPart fills an XML part into given document model
431      * @param part to fill
432      * @param docModel for the given object
433      * @param partMeta metadata for the object to fill
434      * @throws Exception
435      */
436     protected void fillPart(PayloadInputPart part, DocumentModel docModel,
437             ObjectPartType partMeta, Action action, ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx)
438             throws Exception {
439         //check if this is an xml part
440         if (part.getMediaType().equals(MediaType.APPLICATION_XML_TYPE)) {
441                 Element element = part.getElementBody();
442             Map<String, Object> objectProps = DocumentUtils.parseProperties(partMeta, element, ctx);
443                 if (action == Action.UPDATE) {
444                     this.filterReadOnlyPropertiesForPart(objectProps, partMeta);
445                 }
446                 docModel.setProperties(partMeta.getLabel(), objectProps);
447             }
448         }
449
450     /**
451      * Filters out read only properties, so they cannot be set on update.
452      * TODO: add configuration support to do this generally
453      * @param objectProps the properties parsed from the update payload
454      * @param partMeta metadata for the object to fill
455      */
456     public void filterReadOnlyPropertiesForPart(
457             Map<String, Object> objectProps, ObjectPartType partMeta) {
458         // Should add in logic to filter most of the core items on update
459         if(partMeta.getLabel().equalsIgnoreCase(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA)) {
460                 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_AT);
461                 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_BY);
462                 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_URI);
463                 objectProps.remove(CollectionSpaceClient.COLLECTIONSPACE_CORE_TENANTID);
464                 // Note that the updatedAt/updatedBy fields are set internally
465                 // in DocumentModelHandler.handleCoreValues().
466         }
467     }
468
469     /**
470      * extractPart extracts an XML object from given DocumentModel
471      * @param docModel
472      * @param schema of the object to extract
473      * @param partMeta metadata for the object to extract
474      * @throws Exception
475      */
476     protected Map<String, Object> extractPart(DocumentModel docModel, String schema)
477             throws Exception {
478         return extractPart(docModel, schema, (Map<String, Object>)null);
479     }
480     
481     /**
482      * extractPart extracts an XML object from given DocumentModel
483      * @param docModel
484      * @param schema of the object to extract
485      * @param partMeta metadata for the object to extract
486      * @throws Exception
487      */
488     @Deprecated
489     protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
490             throws Exception {
491         return extractPart(docModel, schema, partMeta, null);
492     }    
493
494     /**
495      * extractPart extracts an XML object from given DocumentModel
496      * @param docModel
497      * @param schema of the object to extract
498      * @param partMeta metadata for the object to extract
499      * @throws Exception
500      */
501     protected Map<String, Object> extractPart(
502             DocumentModel docModel, 
503             String schema,
504             Map<String, Object> addToMap)
505             throws Exception {
506         Map<String, Object> result = null;
507
508         Map<String, Object> objectProps = docModel.getProperties(schema);
509         if (objectProps != null) {
510                 //unqualify properties before sending the doc over the wire (to save bandwidh)
511                 //FIXME: is there a better way to avoid duplication of a Map/Collection?
512                 Map<String, Object> unQObjectProperties =
513                         (addToMap != null) ? addToMap : (new HashMap<String, Object>());
514                 Set<Entry<String, Object>> qualifiedEntries = objectProps.entrySet();
515                 for (Entry<String, Object> entry : qualifiedEntries) {
516                     String unqProp = getUnQProperty(entry.getKey());
517                     unQObjectProperties.put(unqProp, entry.getValue());
518                 }
519                 result = unQObjectProperties;
520         }
521
522         return result;
523     }
524     
525     /**
526      * extractPart extracts an XML object from given DocumentModel
527      * @param docModel
528      * @param schema of the object to extract
529      * @param partMeta metadata for the object to extract
530      * @throws Exception
531      */
532     @Deprecated
533     protected Map<String, Object> extractPart(
534             DocumentModel docModel, String schema, ObjectPartType partMeta,
535             Map<String, Object> addToMap)
536             throws Exception {
537         Map<String, Object> result = null;
538
539         result = this.extractPart(docModel, schema, addToMap);
540
541         return result;
542     }
543     
544     /* 
545     public String getStringPropertyFromDoc(
546                 ServiceContext ctx,
547                 String csid,
548                 String propertyXPath ) throws DocumentNotFoundException, DocumentException {
549         RepositoryInstance repoSession = null;
550         boolean releaseRepoSession = false;
551         String returnValue = null;
552
553         try{ 
554                 RepositoryClientImpl repoClient = (RepositoryClientImpl)this.getRepositoryClient(ctx);
555                 repoSession = this.getRepositorySession();
556                 if (repoSession == null) {
557                         repoSession = repoClient.getRepositorySession();
558                         releaseRepoSession = true;
559                 }
560
561                 try {
562                         DocumentWrapper<DocumentModel> wrapper = repoClient.getDoc(repoSession, ctx, csid);
563                         DocumentModel docModel = wrapper.getWrappedObject();
564                         returnValue = (String) docModel.getPropertyValue(propertyXPath);
565                 } catch (PropertyException pe) {
566                         throw pe;
567                 } catch (DocumentException de) {
568                         throw de;
569                 } catch (Exception e) {
570                         if (logger.isDebugEnabled()) {
571                                 logger.debug("Caught exception ", e);
572                         }
573                         throw new DocumentException(e);
574                 } finally {
575                         if (releaseRepoSession && repoSession != null) {
576                                 repoClient.releaseRepositorySession(repoSession);
577                         }
578                 }
579         } catch (Exception e) {
580                 if (logger.isDebugEnabled()) {
581                         logger.debug("Caught exception ", e);
582                 }
583                 throw new DocumentException(e);
584         }               
585
586
587         if (logger.isWarnEnabled() == true) {
588                 logger.warn("Returned DocumentModel instance was created with a repository session that is now closed.");
589         }
590         return returnValue;
591     }
592      */
593
594     
595
596     /* (non-Javadoc)
597      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#getAuthorityRefs(org.collectionspace.services.common.document.DocumentWrapper, java.util.List)
598      */
599     @Override
600     public AuthorityRefList getAuthorityRefs(
601             String csid,
602             List<AuthRefConfigInfo> authRefConfigInfoList) throws PropertyException, Exception {
603
604         AuthorityRefList authRefList = new AuthorityRefList();
605         AbstractCommonList commonList = (AbstractCommonList) authRefList;
606         
607         DocumentFilter docFilter = this.getDocumentFilter();
608         long pageSize = docFilter.getPageSize();
609         long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
610         // set the page size and page number
611         commonList.setPageNum(pageNum);
612         commonList.setPageSize(pageSize);
613         
614         List<AuthorityRefList.AuthorityRefItem> list = authRefList.getAuthorityRefItem();
615
616         try {
617                 int iFirstToUse = (int)(pageSize*pageNum);
618                 int nFoundInPage = 0;
619                 int nFoundTotal = 0;
620                 
621                 ArrayList<RefNameServiceUtils.AuthRefInfo> foundReferences 
622                         = new ArrayList<RefNameServiceUtils.AuthRefInfo>();
623                 
624                 boolean releaseRepoSession = false;
625                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
626                 RepositoryClientImpl repoClient = (RepositoryClientImpl)this.getRepositoryClient(ctx);
627                 CoreSessionInterface repoSession = this.getRepositorySession();
628                 if (repoSession == null) {
629                         repoSession = repoClient.getRepositorySession(ctx);
630                         releaseRepoSession = true;
631                         this.setRepositorySession(repoSession); // we (the doc handler) should keep track of this repository session in case we need it
632                 }
633                 
634                 try {
635                         DocumentModel docModel = repoClient.getDoc(repoSession, ctx, csid).getWrappedObject();
636                         RefNameServiceUtils.findAuthRefPropertiesInDoc(docModel, authRefConfigInfoList, null, foundReferences);
637                         // Slightly goofy pagination support - how many refs do we expect from one object?
638                         for(RefNameServiceUtils.AuthRefInfo ari:foundReferences) {
639                                 if ((nFoundTotal >= iFirstToUse) && (nFoundInPage < pageSize)) {
640                                         if(appendToAuthRefsList(ari, list)) {
641                                                 nFoundInPage++;
642                                         nFoundTotal++;
643                                         }
644                                 } else {
645                                         nFoundTotal++;
646                                 }
647                         }
648                 } finally {
649                         if (releaseRepoSession == true) {
650                                 repoClient.releaseRepositorySession(ctx, repoSession);
651                         }
652                 }
653                 
654             // Set num of items in list. this is useful to our testing framework.
655             commonList.setItemsInPage(nFoundInPage);
656             // set the total result size
657             commonList.setTotalItems(nFoundTotal);
658             
659         } catch (PropertyException pe) {
660             String msg = "Attempted to retrieve value for invalid or missing authority field. "
661                     + "Check authority field properties in tenant bindings.";
662             logger.warn(msg, pe);
663             throw pe;
664         } catch (Exception e) {
665             if (logger.isDebugEnabled()) {
666                 logger.debug("Caught exception in getAuthorityRefs", e);
667             }
668             Response response = Response.status(
669                     Response.Status.INTERNAL_SERVER_ERROR).entity(
670                     "Failed to retrieve authority references").type(
671                     "text/plain").build();
672             throw new CSWebApplicationException(e, response);
673         }
674
675         return authRefList;
676     }
677
678     private boolean appendToAuthRefsList(RefNameServiceUtils.AuthRefInfo ari, 
679                                                 List<AuthorityRefList.AuthorityRefItem> list)
680             throws Exception {
681         String fieldName = ari.getQualifiedDisplayName();
682         try {
683                         String refNameValue = (String)ari.getProperty().getValue();
684                         AuthorityRefList.AuthorityRefItem item = authorityRefListItem(fieldName, refNameValue);
685                         if(item!=null) {        // ignore garbage values.
686                                 list.add(item);
687                                 return true;
688                         }
689         } catch(PropertyException pe) {
690                 String msg = "PropertyException on: "+ari.getProperty().getPath()+pe.getLocalizedMessage();
691                 if (logger.isDebugEnabled()) {
692                         logger.debug(msg, pe);
693                 } else {
694                         logger.error(msg);
695                 }
696         }
697         return false;
698     }
699
700     /**
701      * Fill in all the values to be returned in the authrefs payload for this item.
702      * 
703      * @param authRefFieldName
704      * @param refName
705      * @return
706      */
707     private AuthorityRefList.AuthorityRefItem authorityRefListItem(String authRefFieldName, String refName) {
708         //
709         // Find the CSID for the authority item
710         //
711         String csid = null;
712         try {
713                         DocumentModel docModel = NuxeoUtils.getDocModelForRefName(getServiceContext(), refName, getServiceContext().getResourceMap());
714                         csid = NuxeoUtils.getCsid(docModel);
715                 } catch (Exception e1) {
716                         String msg = String.format("Could not find CSID for authority reference with refname = %s.", refName);
717                         if (logger.isDebugEnabled()) {
718                                 logger.debug(msg, e1);
719                         } else {
720                                 logger.error(msg);
721                         }
722                 }
723         
724         AuthorityRefList.AuthorityRefItem ilistItem = new AuthorityRefList.AuthorityRefItem();
725         try {
726             RefNameUtils.AuthorityTermInfo termInfo = RefNameUtils.parseAuthorityTermInfo(refName);
727             if (Tools.isEmpty(csid) == false) {
728                 ilistItem.setCsid(csid);
729             }
730             ilistItem.setRefName(refName);
731             ilistItem.setAuthDisplayName(termInfo.inAuthority.displayName);
732             ilistItem.setItemDisplayName(termInfo.displayName);
733             ilistItem.setSourceField(authRefFieldName);
734             ilistItem.setUri(termInfo.getRelativeUri());
735         } catch (Exception e) {
736                 ilistItem = null;
737                 String msg = String.format("Trouble parsing refName from value: %s in field: %s. Error message: %s.",
738                                 refName, authRefFieldName, e.getLocalizedMessage());
739                 if (logger.isDebugEnabled()) {
740                         logger.debug(msg, e);
741                 } else {
742                         logger.error(msg);
743                 }
744         }
745         
746         return ilistItem;
747     }
748
749     /**
750      * Returns the primary value from a list of values.
751      *
752      * Assumes that the first value is the primary value.
753      * This assumption may change when and if the primary value
754      * is identified explicitly.
755      *
756      * @param values a list of values.
757      * @param propertyName the name of a property through
758      *     which the value can be extracted.
759      * @return the primary value.
760     protected String primaryValueFromMultivalue(List<Object> values, String propertyName) {
761         String primaryValue = "";
762         if (values == null || values.size() == 0) {
763             return primaryValue;
764         }
765         Object value = values.get(0);
766         if (value instanceof String) {
767             if (value != null) {
768                 primaryValue = (String) value;
769             }
770        // Multivalue group of fields
771        } else if (value instanceof Map) {
772             if (value != null) {
773                 Map map = (Map) value;
774                 if (map.values().size() > 0) {
775                     if (map.get(propertyName) != null) {
776                       primaryValue = (String) map.get(propertyName);
777                     }
778                 }
779             }
780        } else {
781             logger.warn("Unexpected type for property " + propertyName
782                     + " in multivalue list: not String or Map.");
783        }
784        return primaryValue;
785     }
786      */
787
788     /**
789      * Gets a simple property from the document.
790      *
791      * For completeness, as this duplicates DocumentModel method. 
792      *
793      * @param docModel The document model to get info from
794      * @param schema The name of the schema (part)
795      * @param propertyName The simple scalar property type
796      * @return property value as String
797      */
798     protected String getSimpleStringProperty(DocumentModel docModel, String schema, String propName) {
799         String xpath = "/"+schema+":"+propName;
800         try {
801                 return (String) NuxeoUtils.getProperyValue(docModel, xpath); //docModel.getPropertyValue(xpath);
802         } catch(PropertyException pe) {
803                 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a simple String property?"
804                                 +pe.getLocalizedMessage());
805         } catch(ClassCastException cce) {
806                 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a scalar String property?"
807                                 +cce.getLocalizedMessage());
808         } catch(Exception e) {
809                 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
810                                 +e.getLocalizedMessage());
811         }
812     }
813
814     /**
815      * Gets first of a repeating list of scalar values, as a String, from the document.
816      *
817      * @param docModel The document model to get info from
818      * @param schema The name of the schema (part)
819      * @param listName The name of the scalar list property
820      * @return first value in list, as a String, or empty string if the list is empty
821      */
822         protected String getFirstRepeatingStringProperty(DocumentModel docModel,
823                         String schema, String listName) {
824                 String xpath = "/" + schema + ":" + listName + "/[0]";
825                 try {
826                         return (String) NuxeoUtils.getProperyValue(docModel, xpath); // docModel.getPropertyValue(xpath);
827                 } catch (PropertyException pe) {
828                         throw new RuntimeException("Problem retrieving property {" + xpath
829                                         + "}. Not a repeating scalar?" + pe.getLocalizedMessage());
830                 } catch (IndexOutOfBoundsException ioobe) {
831                         // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
832                         return ""; // gracefully handle missing elements
833                 } catch (ClassCastException cce) {
834                         throw new RuntimeException("Problem retrieving property {" + xpath
835                                         + "} as String. Not a repeating String property?"
836                                         + cce.getLocalizedMessage());
837                 } catch (Exception e) {
838                         throw new RuntimeException("Unknown problem retrieving property {"
839                                         + xpath + "}." + e.getLocalizedMessage());
840                 }
841         }
842    
843     /**
844      * Gets first of a repeating list of scalar values, as a String, from the document.
845      *
846      * @param docModel The document model to get info from
847      * @param schema The name of the schema (part)
848      * @param listName The name of the scalar list property
849      * @return first value in list, as a String, or empty string if the list is empty
850      */
851     protected String getStringValueInPrimaryRepeatingComplexProperty(
852                 DocumentModel docModel, String schema, String complexPropertyName, String fieldName) {          
853         String result = null;
854         
855         String xpath = "/" + NuxeoUtils.getPrimaryXPathPropertyName(schema, complexPropertyName, fieldName);
856         try {
857                 result = (String) NuxeoUtils.getProperyValue(docModel, xpath); //docModel.getPropertyValue(xpath);
858         } catch(PropertyException pe) {
859                 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad propertyNames?"
860                                 +pe.getLocalizedMessage());
861         } catch(IndexOutOfBoundsException ioobe) {
862                 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
863                 result = "";    // gracefully handle missing elements
864         } catch(ClassCastException cce) {
865                 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
866                                 +cce.getLocalizedMessage());
867         } catch(NullPointerException npe) {
868                 // Getting here because of a bug in Nuxeo when value in repository is unknown/empty/null
869                 logger.warn(String.format("Nuxeo repo unexpectedly returned an Null Pointer Exception when asked for the value of {%s}.",
870                                 xpath));
871         } catch(Exception e) {
872                 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
873                                 +e.getLocalizedMessage());
874         }
875         
876         return result;
877     }
878    
879     /**
880      * Gets XPath value from schema. Note that only "/" and "[n]" are
881      * supported for xpath. Can omit grouping elements for repeating complex types, 
882      * e.g., "fieldList/[0]" can be used as shorthand for "fieldList/field[0]" and
883      * "fieldGroupList/[0]/field" can be used as shorthand for "fieldGroupList/fieldGroup[0]/field".
884      * If there are no entries for a list of scalars or for a list of complex types, 
885      * a 0 index expression (e.g., "fieldGroupList/[0]/field") will safely return an empty
886      * string. A non-zero index will throw an IndexOutOfBoundsException if there are not
887      * that many elements in the list. 
888      * N.B.: This does not follow the XPath spec - indices are 0-based, not 1-based.
889      *
890      * @param docModel The document model to get info from
891      * @param schema The name of the schema (part)
892      * @param xpath The XPath expression (without schema prefix)
893      * @return value the indicated property value as a String
894      * @throws DocumentException 
895      */
896         protected Object getListResultValue(DocumentModel docModel, // REM - CSPACE-5133
897                         String schema, ListResultField field) throws DocumentException {
898                 Object result = null;
899
900                 result = NuxeoUtils.getXPathValue(docModel, schema, field.getXpath());
901                 
902                 return result;
903         }
904         
905         protected String getStringValue(DocumentModel docModel,
906                         String schema, ListResultField field) throws DocumentException {
907                 String result = null;
908                 
909                 Object value = getListResultValue(docModel, schema, field);
910                 if (value != null && value instanceof String) {
911                         String strValue = (String) value;
912                         if (strValue.trim().isEmpty() == false) {
913                                 result = strValue;
914                         }
915                 }
916                 
917                 return result;
918         }
919    
920     protected void removeFromList(List<RelationsCommonList.RelationListItem> list, RelationsCommonList.RelationListItem item) {
921         list.remove(item);
922     }
923         
924     private void itemToString(StringBuilder sb, String prefix, RelationsCommonList.RelationListItem item ) {
925         sb.append(prefix);
926                 sb.append((item.getCsid()!= null)?item.getCsid():"NO CSID");
927         sb.append(": ["); 
928         sb.append((item.getSubject().getCsid()!=null)?item.getSubject().getCsid():item.getSubject().getRefName());
929         sb.append("]--");
930         sb.append(item.getPredicate());
931         sb.append("-->["); 
932         sb.append((item.getObject().getCsid()!=null)?item.getObject().getCsid():item.getObject().getRefName());
933         sb.append("]");
934     }
935     
936     private String dumpList(List<RelationsCommonList.RelationListItem> list, String label) {
937         StringBuilder sb = new StringBuilder();
938         String s;
939         if (list.size() > 0) {
940             sb.append("=========== " + label + " ==========" + CR);
941         }
942         for (RelationsCommonList.RelationListItem item : list) {
943                 itemToString(sb, "==  ", item);
944                 sb.append(CR);
945         }
946         return sb.toString();
947     }
948     
949     /** @return null on parent not found
950      */
951     protected String getParentCSID(String thisCSID) throws Exception {
952         String parentCSID = null;
953         try {
954             String predicate = RelationshipType.HAS_BROADER.value();
955             RelationsCommonList parentListOuter = getRelations(thisCSID, null, predicate);
956             List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
957             if (parentList != null) {
958                 if (parentList.size() == 0) {
959                     return null;
960                 }
961                 RelationsCommonList.RelationListItem relationListItem = parentList.get(0);
962                 parentCSID = relationListItem.getObjectCsid();
963             }
964             return parentCSID;
965         } catch (Exception e) {
966             logger.error("Could not find parent for this: " + thisCSID, e);
967             return null;
968         }
969     }
970     
971     protected List<RelationsCommonList.RelationListItem> newRelationsCommonList() {
972         List<RelationsCommonList.RelationListItem> result = new ArrayList<RelationsCommonList.RelationListItem>();
973         return result;
974     }
975
976     public void showSiblings(DocumentWrapper<DocumentModel> wrapDoc,
977             MultipartServiceContext ctx) throws Exception {
978         String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
979         String parentCSID = getParentCSID(thisCSID);
980         if (parentCSID == null) {
981             logger.warn("~~~~~\r\n~~~~ Could not find parent for this: " + thisCSID);
982             return;
983         }
984
985         String predicate = RelationshipType.HAS_BROADER.value();
986         RelationsCommonList siblingListOuter = getRelations(null, parentCSID, predicate);
987         List<RelationsCommonList.RelationListItem> siblingList = siblingListOuter.getRelationListItem();
988
989         List<RelationsCommonList.RelationListItem> toRemoveList = newRelationsCommonList();
990
991
992         RelationsCommonList.RelationListItem item = null;
993         for (RelationsCommonList.RelationListItem sibling : siblingList) {
994             if (thisCSID.equals(sibling.getSubjectCsid())) {
995                 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.
996             }
997         }
998         //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.
999         for (RelationsCommonList.RelationListItem self : toRemoveList) {
1000             removeFromList(siblingList, self);
1001         }
1002
1003         long siblingSize = siblingList.size();
1004         siblingListOuter.setTotalItems(siblingSize);
1005         siblingListOuter.setItemsInPage(siblingSize);
1006         if(logger.isTraceEnabled()) {
1007             String dump = dumpList(siblingList, "Siblings of: "+thisCSID);
1008             logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showSiblings ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1009         }
1010
1011         PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, siblingListOuter);
1012         ctx.addOutputPart(relationsPart);
1013     }
1014     
1015     public void showRelations(DocumentWrapper<DocumentModel> wrapDoc,
1016             MultipartServiceContext ctx) throws Exception {
1017         String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
1018
1019         String predicate = RelationshipType.HAS_BROADER.value();
1020         RelationsCommonList parentListOuter = getRelations(thisCSID, null, predicate);
1021         List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
1022
1023         RelationsCommonList childrenListOuter = getRelations(null, thisCSID, predicate);
1024         List<RelationsCommonList.RelationListItem> childrenList = childrenListOuter.getRelationListItem();
1025
1026         if(logger.isTraceEnabled()) {
1027             String dump = dumpLists(thisCSID, parentList, childrenList, null);
1028             logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showRelations ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1029         }
1030         
1031         //Assume that there are more children than parents.  Will be true for parent/child, but maybe not for other relations.
1032         //Now add all parents to our childrenList, to be able to return just one list of consolidated results.
1033         //Not optimal, but that's the current design spec.
1034         long added = 0;
1035         for (RelationsCommonList.RelationListItem parent : parentList) {
1036             childrenList.add(parent);
1037             added++;
1038         }
1039         long childrenSize = childrenList.size();
1040         childrenListOuter.setTotalItems(childrenSize);
1041         childrenListOuter.setItemsInPage(childrenListOuter.getItemsInPage() + added);
1042
1043         PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, childrenListOuter);
1044         ctx.addOutputPart(relationsPart);
1045     }
1046     
1047     public void showAllRelations(DocumentWrapper<DocumentModel> wrapDoc, MultipartServiceContext ctx) throws Exception {
1048         String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
1049
1050         RelationsCommonList subjectListOuter = getRelations(thisCSID, null, null);   //  nulls are wildcards:  predicate=*, and object=*
1051         List<RelationsCommonList.RelationListItem> subjectList = subjectListOuter.getRelationListItem();
1052
1053         RelationsCommonList objectListOuter = getRelations(null, thisCSID, null);   //  nulls are wildcards:  subject=*, and predicate=*
1054         List<RelationsCommonList.RelationListItem> objectList = objectListOuter.getRelationListItem();
1055
1056         if(logger.isTraceEnabled()) {
1057             String dump = dumpLists(thisCSID, subjectList, objectList, null);
1058             logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showAllRelations ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1059         }
1060         //  MERGE LISTS:
1061         subjectList.addAll(objectList);
1062
1063         //now subjectList actually has records BOTH where thisCSID is subject and object.
1064         long relatedSize = subjectList.size();
1065         subjectListOuter.setTotalItems(relatedSize);
1066         subjectListOuter.setItemsInPage(relatedSize);
1067
1068         PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, subjectListOuter);
1069         ctx.addOutputPart(relationsPart);
1070     }
1071
1072     private String dumpLists(String itemCSID,
1073             List<RelationsCommonList.RelationListItem> parentList,
1074             List<RelationsCommonList.RelationListItem> childList,
1075             List<RelationsCommonList.RelationListItem> actionList) {
1076         StringBuilder sb = new StringBuilder();
1077         sb.append("itemCSID: " + itemCSID + CR);
1078         if(parentList!=null) {
1079                 sb.append(dumpList(parentList, "parentList"));
1080         }
1081         if(childList!=null) {
1082                 sb.append(dumpList(childList, "childList"));
1083         }
1084         if(actionList!=null) {
1085                 sb.append(dumpList(actionList, "actionList"));
1086         }
1087         return sb.toString();
1088     }
1089     
1090     //================= TODO: move this to common, refactoring this and  CollectionObjectResource.java
1091     public RelationsCommonList getRelations(String subjectCSID, String objectCSID, String predicate) throws Exception {
1092         ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1093         MultivaluedMap<String, String> queryParams = ctx.getQueryParams();
1094         queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1095         queryParams.putSingle(IRelationsManager.SUBJECT_QP, subjectCSID);
1096         queryParams.putSingle(IRelationsManager.OBJECT_QP, objectCSID);
1097
1098         RelationResource relationResource = new RelationResource(); //is this still acting like a singleton as it should be?
1099         RelationsCommonList relationsCommonList = relationResource.getList(ctx);
1100         return relationsCommonList;
1101     }
1102     //============================= END TODO refactor ==========================
1103     
1104     // this method calls the RelationResource to have it create the relations and persist them.
1105     private void createRelations(List<RelationsCommonList.RelationListItem> inboundList,
1106                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) throws Exception {
1107         for (RelationsCommonList.RelationListItem item : inboundList) {
1108             RelationsCommon rc = new RelationsCommon();
1109             //rc.setCsid(item.getCsid());
1110             //todo: assignTo(item, rc);
1111             RelationsDocListItem itemSubject = item.getSubject();
1112             RelationsDocListItem itemObject = item.getObject();
1113
1114             // Set at least one of CSID and refName for Subject and Object
1115             // Either value might be null for for each of Subject and Object 
1116             String subjectCsid = itemSubject.getCsid();
1117             rc.setSubjectCsid(subjectCsid);
1118
1119             String objCsid = itemObject.getCsid();
1120             rc.setObjectCsid(objCsid);
1121
1122             rc.setSubjectRefName(itemSubject.getRefName());
1123             rc.setObjectRefName(itemObject.getRefName());
1124
1125             rc.setRelationshipType(item.getPredicate());
1126             rc.setRelationshipMetaType(item.getRelationshipMetaType());
1127             //RelationshipType  foo = (RelationshipType.valueOf(item.getPredicate())) ;
1128             //rc.setPredicate(foo);     //this must be one of the type found in the enum in  services/jaxb/src/main/resources/relations_common.xsd
1129
1130             // This is superfluous, since it will be fetched by the Relations Create logic.
1131             rc.setSubjectDocumentType(itemSubject.getDocumentType());
1132             rc.setObjectDocumentType(itemObject.getDocumentType());
1133
1134             // This is superfluous, since it will be fetched by the Relations Create logic.
1135             rc.setSubjectUri(itemSubject.getUri());
1136             rc.setObjectUri(itemObject.getUri());
1137             // May not have the info here. Only really require CSID or refName. 
1138             // Rest is handled in the Relation create mechanism
1139             //uriPointsToSameAuthority(itemSubject.getUri(), itemObject.getUri());
1140
1141             PoxPayloadOut payloadOut = new PoxPayloadOut(RelationClient.SERVICE_PAYLOAD_NAME);
1142             PayloadOutputPart outputPart = new PayloadOutputPart(RelationClient.SERVICE_COMMONPART_NAME, rc);
1143             payloadOut.addPart(outputPart);
1144             RelationResource relationResource = new RelationResource();
1145             Response res = relationResource.create(ctx, ctx.getResourceMap(),
1146                     ctx.getUriInfo(), payloadOut.toXML());    //NOTE ui recycled from above to pass in unknown query params.
1147         }
1148     }
1149     
1150     // Note that item2 may be sparse (only refName, no CSID for subject or object)
1151     // But item1 must not be sparse 
1152     private boolean itemsEqual(RelationsCommonList.RelationListItem item1, RelationsCommonList.RelationListItem item2) {
1153         if (item1 == null || item2 == null) {
1154             return false;
1155         }
1156         RelationsDocListItem subj1 = item1.getSubject();
1157         RelationsDocListItem subj2 = item2.getSubject();
1158         RelationsDocListItem obj1 = item1.getObject();
1159         RelationsDocListItem obj2 = item2.getObject();
1160         
1161         String subj1Csid = subj1.getCsid();
1162         String subj2Csid = subj2.getCsid();
1163         String subj1RefName = subj1.getRefName();
1164         String subj2RefName = subj2.getRefName();
1165
1166         String obj1Csid = obj1.getCsid();
1167         String obj2Csid = obj2.getCsid();
1168         String obj1RefName = obj1.getRefName();
1169         String obj2RefName = obj2.getRefName();
1170         
1171         String item1Metatype = item1.getRelationshipMetaType();
1172         item1Metatype = item1Metatype != null ? item1Metatype : EMPTYSTR;
1173         
1174         String item2Metatype = item2.getRelationshipMetaType();
1175         item2Metatype = item2Metatype != null ? item2Metatype : EMPTYSTR;
1176         
1177         boolean isEqual = (subj1Csid.equals(subj2Csid) || ((subj2Csid==null)  && subj1RefName.equals(subj2RefName)))
1178                 && (obj1Csid.equals(obj2Csid)   || ((obj2Csid==null)   && obj1RefName.equals(obj2RefName)))
1179                 // predicate is proper, but still allow relationshipType
1180                 && (item1.getPredicate().equals(item2.getPredicate())
1181                         ||  ((item2.getPredicate()==null)  && item1.getRelationshipType().equals(item2.getRelationshipType())))
1182                 // Allow missing docTypes, so long as they do not conflict
1183                 && (obj1.getDocumentType().equals(obj2.getDocumentType()) || obj2.getDocumentType()==null)
1184                 && (subj1.getDocumentType().equals(subj2.getDocumentType()) || subj2.getDocumentType()==null)
1185                 && (item1Metatype.equalsIgnoreCase(item2Metatype));
1186         return isEqual;
1187     }
1188     
1189     // Note that the item argument may be sparse (only refName, no CSID for subject or object)
1190     // But the list items must not be sparse
1191     private RelationsCommonList.RelationListItem findInList(
1192                 List<RelationsCommonList.RelationListItem> list, 
1193                 RelationsCommonList.RelationListItem item) {
1194         RelationsCommonList.RelationListItem foundItem = null;
1195         for (RelationsCommonList.RelationListItem listItem : list) {
1196             if (itemsEqual(listItem, item)) {   //equals must be defined, else
1197                 foundItem = listItem;
1198                 break;
1199             }
1200         }
1201         return foundItem;
1202     }
1203     
1204     /**  updateRelations strategy:
1205      *
1206      *
1207     go through inboundList, remove anything from childList that matches  from childList
1208     go through inboundList, remove anything from parentList that matches  from parentList
1209     go through parentList, delete all remaining
1210     go through childList, delete all remaining
1211     go through actionList, add all remaining.
1212     check for duplicate children
1213     check for more than one parent.
1214     
1215     inboundList                           parentList                      childList          actionList
1216     ----------------                          ---------------                  ----------------       ----------------
1217     child-a                                   parent-c                        child-a             child-b
1218     child-b                                   parent-d                        child-c
1219     parent-a
1220      *
1221      *
1222      */
1223     private RelationsCommonList updateRelations(
1224             String itemCSID, PoxPayloadIn input, DocumentWrapper<DocumentModel> wrapDoc, boolean forUpdate)
1225             throws Exception {
1226         if (logger.isTraceEnabled()) {
1227             logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID);
1228         }
1229         PayloadInputPart part = input.getPart(RelationClient.SERVICE_COMMON_LIST_NAME);        //input.getPart("relations_common");
1230         if (part == null) {
1231             return null;  //nothing to do--they didn't send a list of relations.
1232         }
1233         RelationsCommonList relationsCommonListBody = (RelationsCommonList) part.getBody();
1234         List<RelationsCommonList.RelationListItem> inboundList = relationsCommonListBody.getRelationListItem();
1235         List<RelationsCommonList.RelationListItem> actionList = newRelationsCommonList();
1236         List<RelationsCommonList.RelationListItem> childList = null;
1237         List<RelationsCommonList.RelationListItem> parentList = null;
1238         DocumentModel docModel = wrapDoc.getWrappedObject();
1239                 String itemRefName = (String) docModel.getProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
1240                         CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME);
1241
1242                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1243         //Do magic replacement of ${itemCSID} and fix URI's.
1244         fixupInboundListItems(ctx, inboundList, docModel, itemCSID);
1245
1246         final String HAS_BROADER = RelationshipType.HAS_BROADER.value();
1247         UriInfo uriInfo = ctx.getUriInfo();
1248         MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
1249
1250         if (forUpdate) {
1251             //Run getList() once as sent to get childListOuter:
1252             String predicate = RelationshipType.HAS_BROADER.value();
1253             queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1254             queryParams.putSingle(IRelationsManager.SUBJECT_QP, null);
1255             queryParams.putSingle(IRelationsManager.SUBJECT_TYPE_QP, null);
1256             queryParams.putSingle(IRelationsManager.OBJECT_QP, itemCSID);
1257             queryParams.putSingle(IRelationsManager.OBJECT_TYPE_QP, null);
1258             
1259             RelationResource relationResource = new RelationResource();
1260             RelationsCommonList childListOuter = relationResource.getList(ctx);    // Knows all query params because they are in the context.
1261
1262             //Now run getList() again, leaving predicate, swapping subject and object, to get parentListOuter.
1263             queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1264             queryParams.putSingle(IRelationsManager.SUBJECT_QP, itemCSID);
1265             queryParams.putSingle(IRelationsManager.OBJECT_QP, null);
1266             RelationsCommonList parentListOuter = relationResource.getList(ctx);
1267
1268
1269             childList = childListOuter.getRelationListItem();
1270             parentList = parentListOuter.getRelationListItem();
1271
1272             if (parentList.size() > 1) {
1273                 throw new Exception("Too many parents for object: " + itemCSID + " list: " + dumpList(parentList, "parentList"));
1274             }
1275
1276             if (logger.isTraceEnabled()) {
1277                 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " got existing relations.");
1278             }
1279         }
1280
1281         for (RelationsCommonList.RelationListItem inboundItem : inboundList) {
1282             // Note that the relations may specify the other (non-item) bit with a refName, not a CSID,
1283             // and so the CSID for those may be null
1284             if(inboundItem.getPredicate().equals(HAS_BROADER)) {
1285                 // Look for parents and children
1286                 if(itemCSID.equals(inboundItem.getObject().getCsid())
1287                                 || itemRefName.equals(inboundItem.getObject().getRefName())) {
1288                         //then this is an item that says we have a child.  That child is inboundItem
1289                         RelationsCommonList.RelationListItem childItem =
1290                                         (childList == null) ? null : findInList(childList, inboundItem);
1291                         if (childItem != null) {
1292                         if (logger.isTraceEnabled()) {
1293                                 StringBuilder sb = new StringBuilder();
1294                                 itemToString(sb, "== Child: ", childItem);
1295                             logger.trace("Found inboundChild in current child list: " + sb.toString());
1296                         }
1297                                 removeFromList(childList, childItem);    //exists, just take it off delete list
1298                         } else {
1299                         if (logger.isTraceEnabled()) {
1300                                 StringBuilder sb = new StringBuilder();
1301                                 itemToString(sb, "== Child: ", inboundItem);
1302                             logger.trace("inboundChild not in current child list, will add: " + sb.toString());
1303                         }
1304                                 actionList.add(inboundItem);   //doesn't exist as a child, but is a child.  Add to additions list
1305                                 String newChildCsid = inboundItem.getSubject().getCsid();
1306                                 if(newChildCsid == null) {
1307                                         String newChildRefName = inboundItem.getSubject().getRefName();
1308                                         if (newChildRefName == null) {
1309                                                 throw new RuntimeException("Child with no CSID or refName!");
1310                                         }
1311                             if (logger.isTraceEnabled()) {
1312                                 logger.trace("Fetching CSID for child with only refname: "+newChildRefName);
1313                             }
1314                                 DocumentModel newChildDocModel = 
1315                                         NuxeoBasedResource.getDocModelForRefName(getServiceContext(), 
1316                                                         newChildRefName, getServiceContext().getResourceMap());
1317                                 newChildCsid = getCsid(newChildDocModel);
1318                                 }
1319                                 ensureChildHasNoOtherParents(ctx, queryParams, newChildCsid);
1320                         }
1321
1322                 } else if (itemCSID.equals(inboundItem.getSubject().getCsid())
1323                                         || itemRefName.equals(inboundItem.getSubject().getRefName())) {
1324                         //then this is an item that says we have a parent.  inboundItem is that parent.
1325                         RelationsCommonList.RelationListItem parentItem =
1326                                         (parentList == null) ? null : findInList(parentList, inboundItem);
1327                         if (parentItem != null) {
1328                                 removeFromList(parentList, parentItem);    //exists, just take it off delete list
1329                         } else {
1330                                 actionList.add(inboundItem);   //doesn't exist as a parent, but is a parent. Add to additions list
1331                         }
1332                 } else {
1333                     logger.error("Parent/Child Element didn't link to this item. inboundItem: " + inboundItem);
1334                 }
1335             } else {
1336                 logger.warn("Non-parent relation ignored. inboundItem: " + inboundItem);
1337             }
1338         }
1339         if (logger.isTraceEnabled()) {
1340             String dump = dumpLists(itemCSID, parentList, childList, actionList);
1341             logger.trace("~~~~~~~~~~~~~~~~~~~~~~dump~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
1342         }
1343         if (forUpdate) {
1344             if (logger.isTraceEnabled()) {
1345                 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " deleting "
1346                         + parentList.size() + " existing parents and " + childList.size() + " existing children.");
1347             }
1348             deleteRelations(parentList, ctx, "parentList");               //todo: there are items appearing on both lists....april 20.
1349             deleteRelations(childList, ctx, "childList");
1350         }
1351         if (logger.isTraceEnabled()) {
1352             logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " adding "
1353                     + actionList.size() + " new parents and children.");
1354         }
1355         createRelations(actionList, ctx);
1356         if (logger.isTraceEnabled()) {
1357             logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " done.");
1358         }
1359         // We return all elements on the inbound list, since we have just worked to make them exist in the system
1360         // and be non-redundant, etc.  That list came from relationsCommonListBody, so it is still attached to it, just pass that back.
1361         return relationsCommonListBody;
1362     }
1363     
1364     /** Performs substitution for ${itemCSID} (see CommonAPI.AuthorityItemCSID_REPLACE for constant)
1365      *   and sets URI correctly for related items.
1366      *   Operates directly on the items in the list.  Does not change the list ordering, does not add or remove any items.
1367      */
1368     protected void fixupInboundListItems(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1369             List<RelationsCommonList.RelationListItem> inboundList,
1370             DocumentModel docModel,
1371             String itemCSID) throws Exception {
1372         String thisURI = this.getUri(docModel);
1373         // WARNING:  the two code blocks below are almost identical  and seem to ask to be put in a generic method.
1374         //                    beware of the little diffs in  inboundItem.setObjectCsid(itemCSID); and   inboundItem.setSubjectCsid(itemCSID); in the two blocks.
1375         for (RelationsCommonList.RelationListItem inboundItem : inboundList) {
1376             RelationsDocListItem inboundItemObject = inboundItem.getObject();
1377             RelationsDocListItem inboundItemSubject = inboundItem.getSubject();
1378
1379             if (CommonAPI.AuthorityItemCSID_REPLACE.equalsIgnoreCase(inboundItemObject.getCsid())) {
1380                 inboundItem.setObjectCsid(itemCSID);
1381                 inboundItemObject.setCsid(itemCSID);
1382                 //inboundItemObject.setUri(getUri(docModel));
1383             } else {
1384                 /*
1385                 String objectCsid = inboundItemObject.getCsid();
1386                 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(getRepositorySession(), ctx, objectCsid);    //null if not found.
1387                 DocumentWrapper wrapper = new DocumentWrapperImpl(itemDocModel);
1388                 String uri = this.getRepositoryClient(ctx).getDocURI(wrapper);
1389                 inboundItemObject.setUri(uri);    //CSPACE-4037
1390                  */
1391             }
1392             //uriPointsToSameAuthority(thisURI, inboundItemObject.getUri());    //CSPACE-4042
1393
1394             if (CommonAPI.AuthorityItemCSID_REPLACE.equalsIgnoreCase(inboundItemSubject.getCsid())) {
1395                 inboundItem.setSubjectCsid(itemCSID);
1396                 inboundItemSubject.setCsid(itemCSID);
1397                 //inboundItemSubject.setUri(getUri(docModel));
1398             } else {
1399                 /*
1400                 String subjectCsid = inboundItemSubject.getCsid();
1401                 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(getRepositorySession(), ctx, subjectCsid);    //null if not found.
1402                 DocumentWrapper wrapper = new DocumentWrapperImpl(itemDocModel);
1403                 String uri = this.getRepositoryClient(ctx).getDocURI(wrapper);
1404                 inboundItemSubject.setUri(uri);    //CSPACE-4037
1405                  */
1406             }
1407             //uriPointsToSameAuthority(thisURI, inboundItemSubject.getUri());  //CSPACE-4042
1408
1409         }
1410     }
1411
1412     private void ensureChildHasNoOtherParents(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1413                 MultivaluedMap<String, String> queryParams, String childCSID) {
1414         logger.trace("ensureChildHasNoOtherParents for: " + childCSID );
1415         queryParams.putSingle(IRelationsManager.SUBJECT_QP, childCSID);
1416         queryParams.putSingle(IRelationsManager.PREDICATE_QP, RelationshipType.HAS_BROADER.value());
1417         queryParams.putSingle(IRelationsManager.OBJECT_QP, null);  //null means ANY
1418         
1419         RelationResource relationResource = new RelationResource();
1420         RelationsCommonList parentListOuter = relationResource.getList(ctx);
1421         List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
1422         //logger.warn("ensureChildHasNoOtherParents preparing to delete relations on "+childCSID+"\'s parent list: \r\n"+dumpList(parentList, "duplicate parent list"));
1423         deleteRelations(parentList, ctx, "parentList-delete");
1424     }
1425
1426     private void deleteRelations(List<RelationsCommonList.RelationListItem> list,
1427                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1428                 String listName) {
1429         try {
1430             for (RelationsCommonList.RelationListItem item : list) {
1431                 RelationResource relationResource = new RelationResource();
1432                 if(logger.isTraceEnabled()) {
1433                         StringBuilder sb = new StringBuilder();
1434                         itemToString(sb, "==== TO DELETE: ", item);
1435                         logger.trace(sb.toString());
1436                 }
1437                 Response res = relationResource.deleteWithParentCtx(ctx, item.getCsid());
1438                 if (logger.isDebugEnabled()) {
1439                         logger.debug("Status of authority item deleteRelations method call was: " + res.getStatus());
1440                 }
1441             }
1442         } catch (Throwable t) {
1443             String msg = "Unable to deleteRelations: " + Tools.errorToString(t, true);
1444             logger.error(msg);
1445         }
1446     }
1447     
1448     // Note that we must do this after we have completed the Update, so that the repository has the
1449     // info for the item itself. The relations code must call into the repo to get info for each end.
1450     // This could be optimized to pass in the parent docModel, since it will often be one end.
1451     // Nevertheless, we should complete the item save before we do work on the relations, especially
1452     // since a save on Create might fail, and we would not want to create relations for something
1453     // that may not be created...
1454     private void handleRelationsPayload(DocumentWrapper<DocumentModel> wrapDoc, boolean forUpdate) throws Exception {
1455         ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1456         PoxPayloadIn input = ctx.getInput();
1457         DocumentModel documentModel = (wrapDoc.getWrappedObject());
1458         String itemCsid = documentModel.getName();
1459
1460         //Updates relations part
1461         RelationsCommonList relationsCommonList = updateRelations(itemCsid, input, wrapDoc, forUpdate);
1462
1463         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
1464         ctx.setProperty(RelationClient.SERVICE_COMMON_LIST_NAME, payloadOutputPart);
1465
1466         //now we add part for relations list
1467         //ServiceContext ctx = getServiceContext();
1468         //PayloadOutputPart foo = (PayloadOutputPart) ctx.getProperty(RelationClient.SERVICE_COMMON_LIST_NAME);
1469         ctx.getOutput().addPart(payloadOutputPart);
1470     }
1471
1472     /**
1473      * Checks to see if the refName has changed, and if so, 
1474      * uses utilities to find all references and update them to use the new refName.
1475      * @throws Exception 
1476      */
1477     protected void handleRefNameReferencesUpdate() throws Exception {
1478         if (hasRefNameUpdate() == true) {
1479             ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
1480             RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient = getRepositoryClient(ctx);
1481             CoreSessionInterface repoSession = this.getRepositorySession();
1482             
1483             // Update all the relationship records that referred to the old refName
1484             RefNameServiceUtils.updateRefNamesInRelations(ctx, repoClient, repoSession,
1485                     oldRefNameOnUpdate, newRefNameOnUpdate);
1486         }
1487     }
1488     
1489     protected String getRefNameUpdate() {
1490         String result = null;
1491         
1492         if (hasRefNameUpdate() == true) {
1493                 result = newRefNameOnUpdate;
1494                 if (logger.isDebugEnabled() == true) {
1495                         logger.debug(String.format("There was a refName update.  New: %s Old: %s" ,
1496                                         newRefNameOnUpdate, oldRefNameOnUpdate));
1497                 }
1498         }
1499         
1500         return result;
1501     }
1502     
1503     /*
1504      * Note: The Vocabulary document handler overrides this method.
1505      */
1506     protected String getRefPropName() {
1507         return ServiceBindingUtils.AUTH_REF_PROP;
1508     }
1509
1510     
1511     
1512 }