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