]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
fb58dcd1af884995606e2ae04a163b8570eada43
[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.common.vocabulary.nuxeo;
25
26 import org.collectionspace.services.client.AuthorityClient;
27 import org.collectionspace.services.client.PayloadInputPart;
28 import org.collectionspace.services.client.PayloadOutputPart;
29 import org.collectionspace.services.client.PoxPayloadIn;
30 import org.collectionspace.services.client.PoxPayloadOut;
31 import org.collectionspace.services.client.RelationClient;
32 import org.collectionspace.services.common.ResourceBase;
33 import org.collectionspace.services.common.ServiceMessages;
34 import org.collectionspace.services.common.api.CommonAPI;
35 import org.collectionspace.services.common.api.RefName;
36 import org.collectionspace.services.common.api.Tools;
37 import org.collectionspace.services.common.authorityref.AuthorityRefDocList;
38 import org.collectionspace.services.common.context.MultipartServiceContext;
39 import org.collectionspace.services.common.context.ServiceBindingUtils;
40 import org.collectionspace.services.common.context.ServiceContext;
41 import org.collectionspace.services.common.document.DocumentException;
42 import org.collectionspace.services.common.document.DocumentFilter;
43 import org.collectionspace.services.common.document.DocumentWrapper;
44 import org.collectionspace.services.common.document.DocumentWrapperImpl;
45 import org.collectionspace.services.common.relation.IRelationsManager;
46 import org.collectionspace.services.common.repository.RepositoryClient;
47 import org.collectionspace.services.common.repository.RepositoryClientFactory;
48 import org.collectionspace.services.common.vocabulary.AuthorityJAXBSchema;
49 import org.collectionspace.services.common.vocabulary.AuthorityItemJAXBSchema;
50 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils;
51 import org.collectionspace.services.config.service.ListResultField;
52 import org.collectionspace.services.config.service.ObjectPartType;
53 import org.collectionspace.services.nuxeo.client.java.DocHandlerBase;
54 import org.collectionspace.services.nuxeo.client.java.RemoteDocumentModelHandlerImpl;
55 import org.collectionspace.services.nuxeo.client.java.RepositoryJavaClientImpl;
56 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
57 import org.collectionspace.services.relation.RelationResource;
58 import org.collectionspace.services.relation.RelationsCommon;
59 import org.collectionspace.services.relation.RelationsCommonList;
60 import org.collectionspace.services.relation.RelationsDocListItem;
61 import org.collectionspace.services.relation.RelationshipType;
62 import org.nuxeo.ecm.core.api.ClientException;
63 import org.nuxeo.ecm.core.api.DocumentModel;
64 import org.nuxeo.ecm.core.api.model.PropertyException;
65 import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
66 import org.nuxeo.ecm.core.api.repository.RepositoryInstance;
67 import org.nuxeo.runtime.transaction.TransactionHelper;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
70
71 import javax.ws.rs.PathParam;
72 import javax.ws.rs.WebApplicationException;
73 import javax.ws.rs.core.Context;
74 import javax.ws.rs.core.MultivaluedMap;
75 import javax.ws.rs.core.Response;
76 import javax.ws.rs.core.UriInfo;
77 import java.util.ArrayList;
78 import java.util.List;
79 import java.util.Map;
80 import java.util.regex.Matcher;
81 import java.util.regex.Pattern;
82 import org.collectionspace.services.nuxeo.client.java.DocumentModelHandler;
83
84 //import org.collectionspace.services.common.authority.AuthorityItemRelations;
85 /**
86  * AuthorityItemDocumentModelHandler
87  *
88  * $LastChangedRevision: $
89  * $LastChangedDate: $
90  */
91 public abstract class AuthorityItemDocumentModelHandler<AICommon>
92         extends DocHandlerBase<AICommon> {
93
94     private final Logger logger = LoggerFactory.getLogger(AuthorityItemDocumentModelHandler.class);
95     private String authorityItemCommonSchemaName;
96     private String authorityItemTermGroupXPathBase;
97     /**
98      * inVocabulary is the parent Authority for this context
99      */
100     protected String inAuthority = null;
101     protected String authorityRefNameBase = null;
102     // Used to determine when the displayName changes as part of the update.
103     protected String oldDisplayNameOnUpdate = null;
104     protected String oldRefNameOnUpdate = null;
105     protected String newRefNameOnUpdate = null;
106
107     public AuthorityItemDocumentModelHandler(String authorityItemCommonSchemaName) {
108         this.authorityItemCommonSchemaName = authorityItemCommonSchemaName;
109     }
110
111     public void setInAuthority(String inAuthority) {
112         this.inAuthority = inAuthority;
113     }
114
115     /** Subclasses may override this to customize the URI segment. */
116     public String getAuthorityServicePath() {
117         return getServiceContext().getServiceName().toLowerCase();    // Laramie20110510 CSPACE-3932
118     }
119
120     @Override
121     public String getUri(DocumentModel docModel) {
122         // Laramie20110510 CSPACE-3932
123         String authorityServicePath = getAuthorityServicePath();
124         if(inAuthority==null) { // Only happens on queries to wildcarded authorities
125                 try {
126                         inAuthority = (String) docModel.getProperty(authorityItemCommonSchemaName,
127                         AuthorityItemJAXBSchema.IN_AUTHORITY);
128                 } catch (ClientException pe) {
129                         throw new RuntimeException("Could not get parent specifier for item!");
130                 }
131         }
132         return "/" + authorityServicePath + '/' + inAuthority + '/' + AuthorityClient.ITEMS + '/' + getCsid(docModel);
133     }
134
135     protected String getAuthorityRefNameBase() {
136         return this.authorityRefNameBase;
137     }
138
139     public void setAuthorityRefNameBase(String value) {
140         this.authorityRefNameBase = value;
141     }
142
143     @Override
144     public List<ListResultField> getListItemsArray() throws DocumentException {
145         List<ListResultField> list = super.getListItemsArray();
146         int nFields = list.size();
147         // Ensure that each item in a list of Authority items includes
148         // a set of common fields, so we do not depend upon configuration
149         // for general logic.
150         boolean hasDisplayName = false;
151         boolean hasShortId = false;
152         boolean hasRefName = false;
153         boolean hasTermStatus = false;
154         for (int i = 0; i < nFields; i++) {
155             ListResultField field = list.get(i);
156             String elName = field.getElement();
157             if (AuthorityItemJAXBSchema.TERM_DISPLAY_NAME.equals(elName)) {
158                 hasDisplayName = true;
159             } else if (AuthorityItemJAXBSchema.SHORT_IDENTIFIER.equals(elName)) {
160                 hasShortId = true;
161             } else if (AuthorityItemJAXBSchema.REF_NAME.equals(elName)) {
162                 hasRefName = true;
163             } else if (AuthorityItemJAXBSchema.TERM_STATUS.equals(elName)) {
164                 hasTermStatus = true;
165             }
166         }
167         ListResultField field;
168         if (!hasDisplayName) {
169             field = new ListResultField();
170             // Per CSPACE-5132, the name of this element remains 'displayName'
171             // for backwards compatibility, although its value is obtained
172             // from the termDisplayName field.
173             //
174             // In CSPACE-5134, these list results will change substantially
175             // to return display names for both the preferred term and for
176             // each non-preferred term (if any).
177             field.setElement(AuthorityItemJAXBSchema.DISPLAY_NAME);
178             field.setXpath(NuxeoUtils.getPrimaryXPathPropertyName(
179                     authorityItemCommonSchemaName, getItemTermInfoGroupXPathBase(), AuthorityItemJAXBSchema.TERM_DISPLAY_NAME));
180             list.add(field);
181         }
182         if (!hasShortId) {
183             field = new ListResultField();
184             field.setElement(AuthorityItemJAXBSchema.SHORT_IDENTIFIER);
185             field.setXpath(AuthorityItemJAXBSchema.SHORT_IDENTIFIER);
186             list.add(field);
187         }
188         if (!hasRefName) {
189             field = new ListResultField();
190             field.setElement(AuthorityItemJAXBSchema.REF_NAME);
191             field.setXpath(AuthorityItemJAXBSchema.REF_NAME);
192             list.add(field);
193         }
194         if (!hasTermStatus) {
195             field = new ListResultField();
196             field.setElement(AuthorityItemJAXBSchema.TERM_STATUS);
197             field.setXpath(NuxeoUtils.getPrimaryXPathPropertyName(
198                     authorityItemCommonSchemaName, getItemTermInfoGroupXPathBase(), AuthorityItemJAXBSchema.TERM_STATUS));
199             list.add(field);
200         }
201         return list;
202
203     }
204
205
206     /* (non-Javadoc)
207      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#handleCreate(org.collectionspace.services.common.document.DocumentWrapper)
208      */
209     @Override
210     public void handleCreate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
211         // first fill all the parts of the document
212         super.handleCreate(wrapDoc);
213         // Ensure we have required fields set properly
214         handleInAuthority(wrapDoc.getWrappedObject());
215
216         // CSPACE-4813
217         /*
218         handleComputedDisplayNames(wrapDoc.getWrappedObject());
219         String displayName = (String) wrapDoc.getWrappedObject().getProperty(authorityItemCommonSchemaName,
220                 AuthorityItemJAXBSchema.DISPLAY_NAME);
221         if (Tools.isEmpty(displayName)) {
222             logger.warn("Creating Authority Item with no displayName!");
223         }
224         *
225         */        
226         
227         // CSPACE-3178:
228         // handleDisplayNameAsShortIdentifier(wrapDoc.getWrappedObject(), authorityItemCommonSchemaName);
229         // refName includes displayName, so we force a correct value here.
230         updateRefnameForAuthorityItem(wrapDoc, authorityItemCommonSchemaName, getAuthorityRefNameBase());
231     }
232
233     /* (non-Javadoc)
234      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#handleUpdate(org.collectionspace.services.common.document.DocumentWrapper)
235      */
236     @Override
237     public void handleUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
238         // First, get a copy of the old displayName
239         // oldDisplayNameOnUpdate = (String) wrapDoc.getWrappedObject().getProperty(authorityItemCommonSchemaName,
240         //        AuthorityItemJAXBSchema.DISPLAY_NAME);
241         oldDisplayNameOnUpdate = (String) getStringValueInPrimaryRepeatingComplexProperty(
242                 wrapDoc.getWrappedObject(), authorityItemCommonSchemaName,
243                 getItemTermInfoGroupXPathBase(),
244                 AuthorityItemJAXBSchema.TERM_DISPLAY_NAME);
245         oldRefNameOnUpdate = (String) wrapDoc.getWrappedObject().getProperty(authorityItemCommonSchemaName,
246                 AuthorityItemJAXBSchema.REF_NAME);
247         super.handleUpdate(wrapDoc);
248         // handleComputedDisplayNames(wrapDoc.getWrappedObject());
249         // String newDisplayName = (String) wrapDoc.getWrappedObject().getProperty(authorityItemCommonSchemaName,
250         //        AuthorityItemJAXBSchema.DISPLAY_NAME);
251         String newDisplayName = (String) getStringValueInPrimaryRepeatingComplexProperty(
252                 wrapDoc.getWrappedObject(), authorityItemCommonSchemaName,
253                 this.authorityItemTermGroupXPathBase,
254                 AuthorityItemJAXBSchema.TERM_DISPLAY_NAME);
255         if (newDisplayName != null && !newDisplayName.equals(oldDisplayNameOnUpdate)) {
256             // Need to update the refName, and then fix all references.
257             newRefNameOnUpdate = handleItemRefNameUpdateForDisplayName(wrapDoc.getWrappedObject(), newDisplayName);
258         } else {
259             // Mark as not needing attention in completeUpdate phase.
260             newRefNameOnUpdate = null;
261             oldRefNameOnUpdate = null;
262         }
263     }
264
265     /**
266      * Handle display name.
267      *
268      * @param docModel the doc model
269      * @throws Exception the exception
270      */
271     protected void handleComputedDisplayNames(DocumentModel docModel) throws Exception {
272         // Do nothing by default.
273     }
274
275     /**
276      * Handle refName updates for changes to display name.
277      * Assumes refName is already correct. Just ensures it is right.
278      *
279      * @param docModel the doc model
280      * @param newDisplayName the new display name
281      * @throws Exception the exception
282      */
283     protected String handleItemRefNameUpdateForDisplayName(DocumentModel docModel,
284             String newDisplayName) throws Exception {
285         RefName.AuthorityItem authItem = RefName.AuthorityItem.parse(oldRefNameOnUpdate);
286         if (authItem == null) {
287             String err = "Authority Item has illegal refName: " + oldRefNameOnUpdate;
288             logger.debug(err);
289             throw new IllegalArgumentException(err);
290         }
291         authItem.displayName = newDisplayName;
292         String updatedRefName = authItem.toString();
293         docModel.setProperty(authorityItemCommonSchemaName, AuthorityItemJAXBSchema.REF_NAME, updatedRefName);
294         return updatedRefName;
295     }
296     
297     protected String getRefPropName() {
298         return ServiceBindingUtils.AUTH_REF_PROP;
299     }
300
301     /**
302      * Checks to see if the refName has changed, and if so, 
303      * uses utilities to find all references and update them.
304      * @throws Exception 
305      */
306     protected void handleItemRefNameReferenceUpdate() throws Exception {
307         if (newRefNameOnUpdate != null && oldRefNameOnUpdate != null) {
308             // We have work to do.
309             if (logger.isDebugEnabled()) {
310                 String eol = System.getProperty("line.separator");
311                 logger.debug("Need to find and update references to Item." + eol
312                         + "   Old refName" + oldRefNameOnUpdate + eol
313                         + "   New refName" + newRefNameOnUpdate);
314             }
315             ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
316             RepositoryClient repoClient = getRepositoryClient(ctx);
317             String refNameProp = getRefPropName();
318
319             int nUpdated = RefNameServiceUtils.updateAuthorityRefDocs(ctx, repoClient, this.getRepositorySession(),
320                     oldRefNameOnUpdate, newRefNameOnUpdate, refNameProp);
321             if (logger.isDebugEnabled()) {
322                 logger.debug("Updated " + nUpdated + " instances of oldRefName to newRefName");
323             }
324         }
325     }
326
327     /**
328      * If no short identifier was provided in the input payload, generate a
329      * short identifier from the preferred term display name or term name.
330      */
331     private void handleDisplayNameAsShortIdentifier(DocumentModel docModel, String schemaName) throws Exception {
332         String shortIdentifier = (String) docModel.getProperty(schemaName, AuthorityItemJAXBSchema.SHORT_IDENTIFIER);
333         String termDisplayName =
334                 (String) getStringValueInPrimaryRepeatingComplexProperty(
335                     docModel, authorityItemCommonSchemaName,
336                     getItemTermInfoGroupXPathBase(),
337                     AuthorityItemJAXBSchema.TERM_DISPLAY_NAME);
338         String termName = 
339                 (String) getStringValueInPrimaryRepeatingComplexProperty(
340                     docModel, authorityItemCommonSchemaName,
341                     getItemTermInfoGroupXPathBase(),
342                     AuthorityItemJAXBSchema.TERM_NAME);
343         if (Tools.isEmpty(shortIdentifier)) {
344             String generatedShortIdentifier =
345                     AuthorityIdentifierUtils.generateShortIdentifierFromDisplayName(termDisplayName, termName);
346             docModel.setProperty(schemaName, AuthorityItemJAXBSchema.SHORT_IDENTIFIER, generatedShortIdentifier);
347         }
348     }
349
350     /**
351      * Generate a refName for the authority item from the short identifier
352      * and display name.
353      * 
354      * All refNames for authority items are generated.  If a client supplies
355      * a refName, it will be overwritten during create (per this method) 
356      * or discarded during update (per filterReadOnlyPropertiesForPart).
357      * 
358      * @see #filterReadOnlyPropertiesForPart(Map<String, Object>, org.collectionspace.services.common.service.ObjectPartType)
359      * 
360      */
361     protected void updateRefnameForAuthorityItem(DocumentWrapper<DocumentModel> wrapDoc,
362             String schemaName,
363             String authorityRefBaseName) throws Exception {
364         DocumentModel docModel = wrapDoc.getWrappedObject();
365         String shortIdentifier = (String) docModel.getProperty(schemaName, AuthorityItemJAXBSchema.SHORT_IDENTIFIER);
366         String displayName =
367                 (String) getStringValueInPrimaryRepeatingComplexProperty(
368                     docModel, authorityItemCommonSchemaName,
369                     getItemTermInfoGroupXPathBase(),
370                     AuthorityItemJAXBSchema.TERM_DISPLAY_NAME);
371         if (Tools.isEmpty(authorityRefBaseName)) {
372             throw new Exception("Could not create the refName for this authority term, because the refName for its authority parent was empty.");
373         }
374         RefName.Authority authority = RefName.Authority.parse(authorityRefBaseName);
375         String refName = RefName.buildAuthorityItem(authority, shortIdentifier, displayName).toString();
376         docModel.setProperty(schemaName, AuthorityItemJAXBSchema.REF_NAME, refName);
377     }
378
379     /**
380      * Check the logic around the parent pointer. Note that we only need do this on
381      * create, since we have logic to make this read-only on update. 
382      * 
383      * @param docModel
384      * 
385      * @throws Exception the exception
386      */
387     private void handleInAuthority(DocumentModel docModel) throws Exception {
388         if(inAuthority==null) { // Only happens on queries to wildcarded authorities
389                 throw new IllegalStateException("Trying to Create an object with no inAuthority value!");
390         }
391         docModel.setProperty(authorityItemCommonSchemaName,
392                 AuthorityItemJAXBSchema.IN_AUTHORITY, inAuthority);
393     }
394     
395     
396     public AuthorityRefDocList getReferencingObjects(
397                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
398                 List<String> serviceTypes,
399                 String propertyName,
400             String itemcsid) throws Exception {
401         AuthorityRefDocList authRefDocList = null;
402         RepositoryInstance repoSession = null;
403         boolean releaseRepoSession = false;
404         
405         try { 
406                 RepositoryJavaClientImpl repoClient = (RepositoryJavaClientImpl)this.getRepositoryClient(ctx);
407                 repoSession = this.getRepositorySession();
408                 if (repoSession == null) {
409                         repoSession = repoClient.getRepositorySession();
410                         releaseRepoSession = true;
411                 }
412             DocumentFilter myFilter = getDocumentFilter();
413
414                 try {
415                         DocumentWrapper<DocumentModel> wrapper = repoClient.getDoc(repoSession, ctx, itemcsid);
416                         DocumentModel docModel = wrapper.getWrappedObject();
417                         String refName = (String) docModel.getPropertyValue(AuthorityItemJAXBSchema.REF_NAME);
418                 authRefDocList = RefNameServiceUtils.getAuthorityRefDocs(
419                                 repoSession, ctx, repoClient,
420                         serviceTypes,
421                         refName,
422                         propertyName,
423                         myFilter.getPageSize(), myFilter.getStartPage(), true /*computeTotal*/);
424                 } catch (PropertyException pe) {
425                         throw pe;
426                 } catch (DocumentException de) {
427                         throw de;
428                 } catch (Exception e) {
429                         if (logger.isDebugEnabled()) {
430                                 logger.debug("Caught exception ", e);
431                         }
432                         throw new DocumentException(e);
433                 } finally {
434                         if (releaseRepoSession && repoSession != null) {
435                                 repoClient.releaseRepositorySession(repoSession);
436                         }
437                 }
438         } catch (Exception e) {
439                 if (logger.isDebugEnabled()) {
440                         logger.debug("Caught exception ", e);
441                 }
442                 throw new DocumentException(e);
443         }               
444         return authRefDocList;
445     }
446
447
448
449
450     /* (non-Javadoc)
451      * @see org.collectionspace.services.nuxeo.client.java.RemoteDocumentModelHandlerImpl#extractPart(org.nuxeo.ecm.core.api.DocumentModel, java.lang.String, org.collectionspace.services.common.service.ObjectPartType)
452      */
453     @Override
454     protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
455             throws Exception {
456         Map<String, Object> unQObjectProperties = super.extractPart(docModel, schema, partMeta);
457
458         // Add the CSID to the common part, since they may have fetched via the shortId.
459         if (partMeta.getLabel().equalsIgnoreCase(authorityItemCommonSchemaName)) {
460             String csid = getCsid(docModel);//NuxeoUtils.extractId(docModel.getPathAsString());
461             unQObjectProperties.put("csid", csid);
462         }
463
464         return unQObjectProperties;
465     }
466
467     /**
468      * Filters out selected values supplied in an update request.
469      * 
470      * For example, filters out AuthorityItemJAXBSchema.IN_AUTHORITY, to ensure
471      * that the link to the item's parent remains untouched.
472      * 
473      * @param objectProps the properties filtered out from the update payload
474      * @param partMeta metadata for the object to fill
475      */
476     @Override
477     public void filterReadOnlyPropertiesForPart(
478             Map<String, Object> objectProps, ObjectPartType partMeta) {
479         super.filterReadOnlyPropertiesForPart(objectProps, partMeta);
480         String commonPartLabel = getServiceContext().getCommonPartLabel();
481         if (partMeta.getLabel().equalsIgnoreCase(commonPartLabel)) {
482             objectProps.remove(AuthorityItemJAXBSchema.IN_AUTHORITY);
483             objectProps.remove(AuthorityItemJAXBSchema.CSID);
484             objectProps.remove(AuthorityJAXBSchema.SHORT_IDENTIFIER);
485             objectProps.remove(AuthorityItemJAXBSchema.REF_NAME);
486         }
487     }
488
489     @Override
490     public void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
491         MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
492         super.extractAllParts(wrapDoc);
493
494         String showSiblings = ctx.getQueryParams().getFirst(CommonAPI.showSiblings_QP);
495         if (Tools.isTrue(showSiblings)) {
496             showSiblings(wrapDoc, ctx);
497             return;   // actual result is returned on ctx.addOutputPart();
498         }
499
500         String showRelations = ctx.getQueryParams().getFirst(CommonAPI.showRelations_QP);
501         if (Tools.isTrue(showRelations)) {
502             showRelations(wrapDoc, ctx);
503             return;   // actual result is returned on ctx.addOutputPart();
504         }
505
506         String showAllRelations = ctx.getQueryParams().getFirst(CommonAPI.showAllRelations_QP);
507         if (Tools.isTrue(showAllRelations)) {
508             showAllRelations(wrapDoc, ctx);
509             return;   // actual result is returned on ctx.addOutputPart();
510         }
511     }
512
513     /** @return null on parent not found
514      */
515     protected String getParentCSID(String thisCSID) throws Exception {
516         String parentCSID = null;
517         try {
518             String predicate = RelationshipType.HAS_BROADER.value();
519             RelationsCommonList parentListOuter = getRelations(thisCSID, null, predicate);
520             List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
521             if (parentList != null) {
522                 if (parentList.size() == 0) {
523                     return null;
524                 }
525                 RelationsCommonList.RelationListItem relationListItem = parentList.get(0);
526                 parentCSID = relationListItem.getObjectCsid();
527             }
528             return parentCSID;
529         } catch (Exception e) {
530             logger.error("Could not find parent for this: " + thisCSID, e);
531             return null;
532         }
533     }
534
535     public void showRelations(DocumentWrapper<DocumentModel> wrapDoc,
536             MultipartServiceContext ctx) throws Exception {
537         String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
538
539         String predicate = RelationshipType.HAS_BROADER.value();
540         RelationsCommonList parentListOuter = getRelations(thisCSID, null, predicate);
541         List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
542
543         RelationsCommonList childrenListOuter = getRelations(null, thisCSID, predicate);
544         List<RelationsCommonList.RelationListItem> childrenList = childrenListOuter.getRelationListItem();
545
546         if(logger.isTraceEnabled()) {
547             String dump = dumpLists(thisCSID, parentList, childrenList, null);
548             logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showRelations ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
549         }
550         
551         //Assume that there are more children than parents.  Will be true for parent/child, but maybe not for other relations.
552         //Now add all parents to our childrenList, to be able to return just one list of consolidated results.
553         //Not optimal, but that's the current design spec.
554         long added = 0;
555         for (RelationsCommonList.RelationListItem parent : parentList) {
556             childrenList.add(parent);
557             added++;
558         }
559         long childrenSize = childrenList.size();
560         childrenListOuter.setTotalItems(childrenSize);
561         childrenListOuter.setItemsInPage(childrenListOuter.getItemsInPage() + added);
562
563         PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, childrenListOuter);
564         ctx.addOutputPart(relationsPart);
565     }
566
567     public void showSiblings(DocumentWrapper<DocumentModel> wrapDoc,
568             MultipartServiceContext ctx) throws Exception {
569         String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
570         String parentCSID = getParentCSID(thisCSID);
571         if (parentCSID == null) {
572             logger.warn("~~~~~\r\n~~~~ Could not find parent for this: " + thisCSID);
573             return;
574         }
575
576         String predicate = RelationshipType.HAS_BROADER.value();
577         RelationsCommonList siblingListOuter = getRelations(null, parentCSID, predicate);
578         List<RelationsCommonList.RelationListItem> siblingList = siblingListOuter.getRelationListItem();
579
580         List<RelationsCommonList.RelationListItem> toRemoveList = newList();
581
582
583         RelationsCommonList.RelationListItem item = null;
584         for (RelationsCommonList.RelationListItem sibling : siblingList) {
585             if (thisCSID.equals(sibling.getSubjectCsid())) {
586                 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.
587             }
588         }
589         //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.
590         for (RelationsCommonList.RelationListItem self : toRemoveList) {
591             removeFromList(siblingList, self);
592         }
593
594         long siblingSize = siblingList.size();
595         siblingListOuter.setTotalItems(siblingSize);
596         siblingListOuter.setItemsInPage(siblingSize);
597         if(logger.isTraceEnabled()) {
598             String dump = dumpList(siblingList, "Siblings of: "+thisCSID);
599             logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showSiblings ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
600         }
601
602         PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, siblingListOuter);
603         ctx.addOutputPart(relationsPart);
604     }
605
606     public void showAllRelations(DocumentWrapper<DocumentModel> wrapDoc, MultipartServiceContext ctx) throws Exception {
607         String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
608
609         RelationsCommonList subjectListOuter = getRelations(thisCSID, null, null);   //  nulls are wildcards:  predicate=*, and object=*
610         List<RelationsCommonList.RelationListItem> subjectList = subjectListOuter.getRelationListItem();
611
612         RelationsCommonList objectListOuter = getRelations(null, thisCSID, null);   //  nulls are wildcards:  subject=*, and predicate=*
613         List<RelationsCommonList.RelationListItem> objectList = objectListOuter.getRelationListItem();
614
615         if(logger.isTraceEnabled()) {
616             String dump = dumpLists(thisCSID, subjectList, objectList, null);
617             logger.trace("~~~~~~~~~~~~~~~~~~~~~~ showAllRelations ~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
618         }
619         //  MERGE LISTS:
620         subjectList.addAll(objectList);
621
622         //now subjectList actually has records BOTH where thisCSID is subject and object.
623         long relatedSize = subjectList.size();
624         subjectListOuter.setTotalItems(relatedSize);
625         subjectListOuter.setItemsInPage(relatedSize);
626
627         PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, subjectListOuter);
628         ctx.addOutputPart(relationsPart);
629     }
630
631     public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
632         super.fillAllParts(wrapDoc, action);
633         /*
634         ServiceContext ctx = getServiceContext();
635         PoxPayloadIn input = (PoxPayloadIn) ctx.getInput();
636         DocumentModel documentModel = (wrapDoc.getWrappedObject());
637         String itemCsid = documentModel.getName();
638         
639         //UPDATE and CREATE will call.   Updates relations part
640         RelationsCommonList relationsCommonList = updateRelations(itemCsid, input, wrapDoc);
641         
642         PayloadOutputPart payloadOutputPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, relationsCommonList);
643         ctx.setProperty(RelationClient.SERVICE_COMMON_LIST_NAME, payloadOutputPart);
644          */
645     }
646
647     public void completeCreate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
648         super.completeCreate(wrapDoc);
649         handleRelationsPayload(wrapDoc, false);
650     }
651
652     public void completeUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
653         super.completeUpdate(wrapDoc);
654         handleRelationsPayload(wrapDoc, true);
655         handleItemRefNameReferenceUpdate();
656     }
657
658     // Note that we must do this after we have completed the Update, so that the repository has the
659     // info for the item itself. The relations code must call into the repo to get info for each end.
660     // This could be optimized to pass in the parent docModel, since it will often be one end.
661     // Nevertheless, we should complete the item save before we do work on the relations, especially
662     // since a save on Create might fail, and we would not want to create relations for something
663     // that may not be created...
664     private void handleRelationsPayload(DocumentWrapper<DocumentModel> wrapDoc, boolean fUpdate) throws Exception {
665         ServiceContext ctx = getServiceContext();
666         PoxPayloadIn input = (PoxPayloadIn) ctx.getInput();
667         DocumentModel documentModel = (wrapDoc.getWrappedObject());
668         String itemCsid = documentModel.getName();
669
670         //Updates relations part
671         RelationsCommonList relationsCommonList = updateRelations(itemCsid, input, wrapDoc, fUpdate);
672
673         PayloadOutputPart payloadOutputPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, relationsCommonList);
674         ctx.setProperty(RelationClient.SERVICE_COMMON_LIST_NAME, payloadOutputPart);
675
676         //now we add part for relations list
677         //ServiceContext ctx = getServiceContext();
678         //PayloadOutputPart foo = (PayloadOutputPart) ctx.getProperty(RelationClient.SERVICE_COMMON_LIST_NAME);
679         ((PoxPayloadOut) ctx.getOutput()).addPart(payloadOutputPart);
680     }
681
682     /**  updateRelations strategy:
683     
684     go through inboundList, remove anything from childList that matches  from childList
685     go through inboundList, remove anything from parentList that matches  from parentList
686     go through parentList, delete all remaining
687     go through childList, delete all remaining
688     go through actionList, add all remaining.
689     check for duplicate children
690     check for more than one parent.
691     
692     inboundList                           parentList                      childList          actionList
693     ----------------                          ---------------                  ----------------       ----------------
694     child-a                                   parent-c                        child-a             child-b
695     child-b                                   parent-d                        child-c
696     parent-a
697      */
698     private RelationsCommonList updateRelations(
699             String itemCSID, PoxPayloadIn input, DocumentWrapper<DocumentModel> wrapDoc, boolean fUpdate)
700             throws Exception {
701         if (logger.isTraceEnabled()) {
702             logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID);
703         }
704         PayloadInputPart part = input.getPart(RelationClient.SERVICE_COMMON_LIST_NAME);        //input.getPart("relations_common");
705         if (part == null) {
706             return null;  //nothing to do--they didn't send a list of relations.
707         }
708         RelationsCommonList relationsCommonListBody = (RelationsCommonList) part.getBody();
709         List<RelationsCommonList.RelationListItem> inboundList = relationsCommonListBody.getRelationListItem();
710         List<RelationsCommonList.RelationListItem> actionList = newList();
711         List<RelationsCommonList.RelationListItem> childList = null;
712         List<RelationsCommonList.RelationListItem> parentList = null;
713         DocumentModel docModel = wrapDoc.getWrappedObject();
714                 String itemRefName = (String) docModel.getPropertyValue(AuthorityItemJAXBSchema.REF_NAME);
715
716         ServiceContext ctx = getServiceContext();
717         //Do magic replacement of ${itemCSID} and fix URI's.
718         fixupInboundListItems(ctx, inboundList, docModel, itemCSID);
719
720         String HAS_BROADER = RelationshipType.HAS_BROADER.value();
721         UriInfo uriInfo = ctx.getUriInfo();
722         MultivaluedMap queryParams = uriInfo.getQueryParameters();
723
724         if (fUpdate) {
725             //Run getList() once as sent to get childListOuter:
726             String predicate = RelationshipType.HAS_BROADER.value();
727             queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
728             queryParams.putSingle(IRelationsManager.SUBJECT_QP, null);
729             queryParams.putSingle(IRelationsManager.SUBJECT_TYPE_QP, null);
730             queryParams.putSingle(IRelationsManager.OBJECT_QP, itemCSID);
731             queryParams.putSingle(IRelationsManager.OBJECT_TYPE_QP, null);
732             RelationsCommonList childListOuter = (new RelationResource()).getList(ctx.getUriInfo());    //magically knows all query params because they are in the context.
733
734             //Now run getList() again, leaving predicate, swapping subject and object, to get parentListOuter.
735             queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
736             queryParams.putSingle(IRelationsManager.SUBJECT_QP, itemCSID);
737             queryParams.putSingle(IRelationsManager.OBJECT_QP, null);
738             RelationsCommonList parentListOuter = (new RelationResource()).getList(ctx.getUriInfo());
739
740
741             childList = childListOuter.getRelationListItem();
742             parentList = parentListOuter.getRelationListItem();
743
744             if (parentList.size() > 1) {
745                 throw new Exception("Too many parents for object: " + itemCSID + " list: " + dumpList(parentList, "parentList"));
746             }
747
748             if (logger.isTraceEnabled()) {
749                 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " got existing relations.");
750             }
751         }
752
753
754         for (RelationsCommonList.RelationListItem inboundItem : inboundList) {
755             // Note that the relations may specify the other (non-item) bit with a refName, not a CSID,
756             // and so the CSID for those may be null
757             if(inboundItem.getPredicate().equals(HAS_BROADER)) {
758                 // Look for parents and children
759                 if(itemCSID.equals(inboundItem.getObject().getCsid())
760                                 || itemRefName.equals(inboundItem.getObject().getRefName())) {
761                         //then this is an item that says we have a child.  That child is inboundItem
762                         RelationsCommonList.RelationListItem childItem =
763                                         (childList == null) ? null : findInList(childList, inboundItem);
764                         if (childItem != null) {
765                         if (logger.isTraceEnabled()) {
766                                 StringBuilder sb = new StringBuilder();
767                                 itemToString(sb, "== Child: ", childItem);
768                             logger.trace("Found inboundChild in current child list: " + sb.toString());
769                         }
770                                 removeFromList(childList, childItem);    //exists, just take it off delete list
771                         } else {
772                         if (logger.isTraceEnabled()) {
773                                 StringBuilder sb = new StringBuilder();
774                                 itemToString(sb, "== Child: ", inboundItem);
775                             logger.trace("inboundChild not in current child list, will add: " + sb.toString());
776                         }
777                                 actionList.add(inboundItem);   //doesn't exist as a child, but is a child.  Add to additions list
778                                 String newChildCsid = inboundItem.getSubject().getCsid();
779                                 if(newChildCsid == null) {
780                                         String newChildRefName = inboundItem.getSubject().getRefName();
781                                         if(newChildRefName==null) {
782                                                 throw new RuntimeException("Child with no CSID or refName!");
783                                         }
784                             if (logger.isTraceEnabled()) {
785                                 logger.trace("Fetching CSID for child with only refname: "+newChildRefName);
786                             }
787                                 DocumentModel newChildDocModel = 
788                                         ResourceBase.getDocModelForRefName(this.getRepositorySession(), 
789                                                         newChildRefName, getServiceContext().getResourceMap());
790                                 newChildCsid = getCsid(newChildDocModel);
791                                 }
792                                 ensureChildHasNoOtherParents(ctx, queryParams, newChildCsid);
793                         }
794
795                 } else if (itemCSID.equals(inboundItem.getSubject().getCsid())
796                                         || itemRefName.equals(inboundItem.getSubject().getRefName())) {
797                         //then this is an item that says we have a parent.  inboundItem is that parent.
798                         RelationsCommonList.RelationListItem parentItem =
799                                         (parentList == null) ? null : findInList(parentList, inboundItem);
800                         if (parentItem != null) {
801                                 removeFromList(parentList, parentItem);    //exists, just take it off delete list
802                         } else {
803                                 actionList.add(inboundItem);   //doesn't exist as a parent, but is a parent. Add to additions list
804                         }
805                 } else {
806                     logger.error("Parent/Child Element didn't link to this item. inboundItem: " + inboundItem);
807                 }
808             } else {
809                 logger.warn("Non-parent relation ignored. inboundItem: " + inboundItem);
810             }
811         }
812         if (logger.isTraceEnabled()) {
813             String dump = dumpLists(itemCSID, parentList, childList, actionList);
814             logger.trace("~~~~~~~~~~~~~~~~~~~~~~dump~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
815         }
816         if (fUpdate) {
817             if (logger.isTraceEnabled()) {
818                 logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " deleting "
819                         + parentList.size() + " existing parents and " + childList.size() + " existing children.");
820             }
821             deleteRelations(parentList, ctx, "parentList");               //todo: there are items appearing on both lists....april 20.
822             deleteRelations(childList, ctx, "childList");
823         }
824         if (logger.isTraceEnabled()) {
825             logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " adding "
826                     + actionList.size() + " new parents and children.");
827         }
828         createRelations(actionList, ctx);
829         if (logger.isTraceEnabled()) {
830             logger.trace("AuthItemDocHndler.updateRelations for: " + itemCSID + " done.");
831         }
832         //We return all elements on the inbound list, since we have just worked to make them exist in the system
833         // and be non-redundant, etc.  That list came from relationsCommonListBody, so it is still attached to it, just pass that back.
834         return relationsCommonListBody;
835     }
836
837     private void ensureChildHasNoOtherParents(ServiceContext ctx, MultivaluedMap queryParams, String childCSID) {
838         logger.trace("ensureChildHasNoOtherParents for: " + childCSID );
839         queryParams.putSingle(IRelationsManager.SUBJECT_QP, childCSID);
840         queryParams.putSingle(IRelationsManager.PREDICATE_QP, RelationshipType.HAS_BROADER.value());
841         queryParams.putSingle(IRelationsManager.OBJECT_QP, null);  //null means ANY
842         RelationsCommonList parentListOuter = (new RelationResource()).getList(ctx.getUriInfo());
843         List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
844         //logger.warn("ensureChildHasNoOtherParents preparing to delete relations on "+childCSID+"\'s parent list: \r\n"+dumpList(parentList, "duplicate parent list"));
845         deleteRelations(parentList, ctx, "parentList-delete");
846     }
847
848     
849     private void itemToString(StringBuilder sb, String prefix, RelationsCommonList.RelationListItem item ) {
850         sb.append(prefix);
851                 sb.append((item.getCsid()!= null)?item.getCsid():"NO CSID");
852         sb.append(": ["); 
853         sb.append((item.getSubject().getCsid()!=null)?item.getSubject().getCsid():item.getSubject().getRefName());
854         sb.append("]--");
855         sb.append(item.getPredicate());
856         sb.append("-->["); 
857         sb.append((item.getObject().getCsid()!=null)?item.getObject().getCsid():item.getObject().getRefName());
858         sb.append("]");
859     }
860     
861     private String dumpLists(String itemCSID,
862             List<RelationsCommonList.RelationListItem> parentList,
863             List<RelationsCommonList.RelationListItem> childList,
864             List<RelationsCommonList.RelationListItem> actionList) {
865         StringBuilder sb = new StringBuilder();
866         sb.append("itemCSID: " + itemCSID + CR);
867         if(parentList!=null) {
868                 sb.append(dumpList(parentList, "parentList"));
869         }
870         if(childList!=null) {
871                 sb.append(dumpList(childList, "childList"));
872         }
873         if(actionList!=null) {
874                 sb.append(dumpList(actionList, "actionList"));
875         }
876         return sb.toString();
877     }
878     private final static String CR = "\r\n";
879     private final static String T = " ";
880
881     private String dumpList(List<RelationsCommonList.RelationListItem> list, String label) {
882         StringBuilder sb = new StringBuilder();
883         String s;
884         if (list.size() > 0) {
885             sb.append("=========== " + label + " ==========" + CR);
886         }
887         for (RelationsCommonList.RelationListItem item : list) {
888                 itemToString(sb, "==  ", item);
889                 sb.append(CR);
890         }
891         return sb.toString();
892     }
893
894     /** Performs substitution for ${itemCSID} (see CommonAPI.AuthorityItemCSID_REPLACE for constant)
895      *   and sets URI correctly for related items.
896      *   Operates directly on the items in the list.  Does not change the list ordering, does not add or remove any items.
897      */
898     protected void fixupInboundListItems(ServiceContext ctx,
899             List<RelationsCommonList.RelationListItem> inboundList,
900             DocumentModel docModel,
901             String itemCSID) throws Exception {
902         String thisURI = this.getUri(docModel);
903         // WARNING:  the two code blocks below are almost identical  and seem to ask to be put in a generic method.
904         //                    beware of the little diffs in  inboundItem.setObjectCsid(itemCSID); and   inboundItem.setSubjectCsid(itemCSID); in the two blocks.
905         for (RelationsCommonList.RelationListItem inboundItem : inboundList) {
906             RelationsDocListItem inboundItemObject = inboundItem.getObject();
907             RelationsDocListItem inboundItemSubject = inboundItem.getSubject();
908
909             if (CommonAPI.AuthorityItemCSID_REPLACE.equalsIgnoreCase(inboundItemObject.getCsid())) {
910                 inboundItem.setObjectCsid(itemCSID);
911                 inboundItemObject.setCsid(itemCSID);
912                 //inboundItemObject.setUri(getUri(docModel));
913             } else {
914                 /*
915                 String objectCsid = inboundItemObject.getCsid();
916                 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(getRepositorySession(), ctx, objectCsid);    //null if not found.
917                 DocumentWrapper wrapper = new DocumentWrapperImpl(itemDocModel);
918                 String uri = this.getRepositoryClient(ctx).getDocURI(wrapper);
919                 inboundItemObject.setUri(uri);    //CSPACE-4037
920                  */
921             }
922             //uriPointsToSameAuthority(thisURI, inboundItemObject.getUri());    //CSPACE-4042
923
924             if (CommonAPI.AuthorityItemCSID_REPLACE.equalsIgnoreCase(inboundItemSubject.getCsid())) {
925                 inboundItem.setSubjectCsid(itemCSID);
926                 inboundItemSubject.setCsid(itemCSID);
927                 //inboundItemSubject.setUri(getUri(docModel));
928             } else {
929                 /*
930                 String subjectCsid = inboundItemSubject.getCsid();
931                 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(getRepositorySession(), ctx, subjectCsid);    //null if not found.
932                 DocumentWrapper wrapper = new DocumentWrapperImpl(itemDocModel);
933                 String uri = this.getRepositoryClient(ctx).getDocURI(wrapper);
934                 inboundItemSubject.setUri(uri);    //CSPACE-4037
935                  */
936             }
937             //uriPointsToSameAuthority(thisURI, inboundItemSubject.getUri());  //CSPACE-4042
938
939         }
940     }
941
942     // this method calls the RelationResource to have it create the relations and persist them.
943     private void createRelations(List<RelationsCommonList.RelationListItem> inboundList, ServiceContext ctx) throws Exception {
944         for (RelationsCommonList.RelationListItem item : inboundList) {
945             RelationsCommon rc = new RelationsCommon();
946             //rc.setCsid(item.getCsid());
947             //todo: assignTo(item, rc);
948             RelationsDocListItem itemSubject = item.getSubject();
949             RelationsDocListItem itemObject = item.getObject();
950
951             // Set at least one of CSID and refName for Subject and Object
952             // Either value might be null for for each of Subject and Object 
953             String subjectCsid = itemSubject.getCsid();
954             rc.setSubjectCsid(subjectCsid);
955
956             String objCsid = itemObject.getCsid();
957             rc.setObjectCsid(objCsid);
958
959             rc.setSubjectRefName(itemSubject.getRefName());
960             rc.setObjectRefName(itemObject.getRefName());
961
962             rc.setRelationshipType(item.getPredicate());
963             //RelationshipType  foo = (RelationshipType.valueOf(item.getPredicate())) ;
964             //rc.setPredicate(foo);     //this must be one of the type found in the enum in  services/jaxb/src/main/resources/relations_common.xsd
965
966             // This is superfluous, since it will be fetched by the Relations Create logic.
967             rc.setSubjectDocumentType(itemSubject.getDocumentType());
968             rc.setObjectDocumentType(itemObject.getDocumentType());
969
970             // This is superfluous, since it will be fetched by the Relations Create logic.
971             rc.setSubjectUri(itemSubject.getUri());
972             rc.setObjectUri(itemObject.getUri());
973             // May not have the info here. Only really require CSID or refName. 
974             // Rest is handled in the Relation create mechanism
975             //uriPointsToSameAuthority(itemSubject.getUri(), itemObject.getUri());
976
977             PoxPayloadOut payloadOut = new PoxPayloadOut(RelationClient.SERVICE_PAYLOAD_NAME);
978             PayloadOutputPart outputPart = new PayloadOutputPart(RelationClient.SERVICE_COMMONPART_NAME, rc);
979             payloadOut.addPart(outputPart);
980             RelationResource relationResource = new RelationResource();
981             Object res = relationResource.create(ctx.getResourceMap(),
982                     ctx.getUriInfo(), payloadOut.toXML());    //NOTE ui recycled from above to pass in unknown query params.
983         }
984     }
985
986     private void deleteRelations(List<RelationsCommonList.RelationListItem> list, ServiceContext ctx, String listName) {
987         try {
988             for (RelationsCommonList.RelationListItem item : list) {
989                 RelationResource relationResource = new RelationResource();
990                 if(logger.isTraceEnabled()) {
991                         StringBuilder sb = new StringBuilder();
992                         itemToString(sb, "==== TO DELETE: ", item);
993                         logger.trace(sb.toString());
994                 }
995                 Object res = relationResource.delete(item.getCsid());
996             }
997         } catch (Throwable t) {
998             String msg = "Unable to deleteRelations: " + Tools.errorToString(t, true);
999             logger.error(msg);
1000         }
1001     }
1002
1003     private List<RelationsCommonList.RelationListItem> newList() {
1004         List<RelationsCommonList.RelationListItem> result = new ArrayList<RelationsCommonList.RelationListItem>();
1005         return result;
1006     }
1007
1008     protected List<RelationsCommonList.RelationListItem> cloneList(List<RelationsCommonList.RelationListItem> inboundList) {
1009         List<RelationsCommonList.RelationListItem> result = newList();
1010         for (RelationsCommonList.RelationListItem item : inboundList) {
1011             result.add(item);
1012         }
1013         return result;
1014     }
1015
1016     // Note that the item argument may be sparse (only refName, no CSID for subject or object)
1017     // But the list items must not be sparse
1018     private RelationsCommonList.RelationListItem findInList(
1019                 List<RelationsCommonList.RelationListItem> list, 
1020                 RelationsCommonList.RelationListItem item) {
1021         RelationsCommonList.RelationListItem foundItem = null;
1022         for (RelationsCommonList.RelationListItem listItem : list) {
1023             if (itemsEqual(listItem, item)) {   //equals must be defined, else
1024                 foundItem = listItem;
1025                 break;
1026             }
1027         }
1028         return foundItem;
1029     }
1030
1031     // Note that item2 may be sparse (only refName, no CSID for subject or object)
1032     // But item1 must not be sparse 
1033     private boolean itemsEqual(RelationsCommonList.RelationListItem item1, RelationsCommonList.RelationListItem item2) {
1034         if (item1 == null || item2 == null) {
1035             return false;
1036         }
1037         RelationsDocListItem subj1 = item1.getSubject();
1038         RelationsDocListItem subj2 = item2.getSubject();
1039         RelationsDocListItem obj1 = item1.getObject();
1040         RelationsDocListItem obj2 = item2.getObject();
1041         String subj1Csid = subj1.getCsid();
1042         String subj2Csid = subj2.getCsid();
1043         String subj1RefName = subj1.getRefName();
1044         String subj2RefName = subj2.getRefName();
1045
1046         String obj1Csid = obj1.getCsid();
1047         String obj2Csid = obj2.getCsid();
1048         String obj1RefName = obj1.getRefName();
1049         String obj2RefName = obj2.getRefName();
1050
1051         boolean isEqual = 
1052                            (subj1Csid.equals(subj2Csid) || ((subj2Csid==null)  && subj1RefName.equals(subj2RefName)))
1053                 && (obj1Csid.equals(obj1Csid)   || ((obj2Csid==null)   && obj1RefName.equals(obj2RefName)))
1054                 // predicate is proper, but still allow relationshipType
1055                 && (item1.getPredicate().equals(item2.getPredicate())
1056                         ||  ((item2.getPredicate()==null)  && item1.getRelationshipType().equals(item2.getRelationshipType())))
1057                 // Allow missing docTypes, so long as they do not conflict
1058                 && (obj1.getDocumentType().equals(obj2.getDocumentType()) || obj2.getDocumentType()==null)
1059                 && (subj1.getDocumentType().equals(subj2.getDocumentType()) || subj2.getDocumentType()==null);
1060         return isEqual;
1061     }
1062
1063     private void removeFromList(List<RelationsCommonList.RelationListItem> list, RelationsCommonList.RelationListItem item) {
1064         list.remove(item);
1065     }
1066
1067     /* don't even THINK of re-using this method.
1068      * String example_uri = "/locationauthorities/7ec60f01-84ab-4908-9a6a/items/a5466530-713f-43b4-bc05";
1069      */
1070     private String extractInAuthorityCSID(String uri) {
1071         String IN_AUTHORITY_REGEX = "/(.*?)/(.*?)/(.*)";
1072         Pattern p = Pattern.compile(IN_AUTHORITY_REGEX);
1073         Matcher m = p.matcher(uri);
1074         if (m.find()) {
1075             if (m.groupCount() < 3) {
1076                 logger.warn("REGEX-WRONG-GROUPCOUNT looking in " + uri);
1077                 return "";
1078             } else {
1079                 //String service = m.group(1);
1080                 String inauth = m.group(2);
1081                 //String theRest = m.group(3);
1082                 return inauth;
1083                 //print("service:"+service+", inauth:"+inauth+", rest:"+rest);
1084             }
1085         } else {
1086             logger.warn("REGEX-NOT-MATCHED looking in " + uri);
1087             return "";
1088         }
1089     }
1090
1091     //ensures CSPACE-4042
1092     protected void uriPointsToSameAuthority(String thisURI, String inboundItemURI) throws Exception {
1093         String authorityCSID = extractInAuthorityCSID(thisURI);
1094         String authorityCSIDForInbound = extractInAuthorityCSID(inboundItemURI);
1095         if (Tools.isBlank(authorityCSID)
1096                 || Tools.isBlank(authorityCSIDForInbound)
1097                 || (!authorityCSID.equalsIgnoreCase(authorityCSIDForInbound))) {
1098             throw new Exception("Item URI " + thisURI + " must point to same authority as related item: " + inboundItemURI);
1099         }
1100     }
1101
1102     //================= TODO: move this to common, refactoring this and  CollectionObjectResource.java
1103     public RelationsCommonList getRelations(String subjectCSID, String objectCSID, String predicate) throws Exception {
1104         ServiceContext ctx = getServiceContext();
1105         MultivaluedMap queryParams = ctx.getQueryParams();
1106         queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
1107         queryParams.putSingle(IRelationsManager.SUBJECT_QP, subjectCSID);
1108         queryParams.putSingle(IRelationsManager.OBJECT_QP, objectCSID);
1109
1110         RelationResource relationResource = new RelationResource();
1111         RelationsCommonList relationsCommonList = relationResource.getList(ctx.getUriInfo());
1112         return relationsCommonList;
1113     }
1114     //============================= END TODO refactor ==========================
1115
1116     public String getItemTermInfoGroupXPathBase() {
1117         return this.authorityItemTermGroupXPathBase;
1118     }
1119         
1120     public void setItemTermInfoGroupXPathBase(String itemTermInfoGroupXPathBase) {
1121         this.authorityItemTermGroupXPathBase = itemTermInfoGroupXPathBase;
1122     }
1123 }