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