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