]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
29dbd50c2b456c145b123a329765e2338e9db435
[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.api.CommonAPI;
33 import org.collectionspace.services.common.api.RefName;
34 import org.collectionspace.services.common.api.Tools;
35 import org.collectionspace.services.common.context.MultipartServiceContext;
36 import org.collectionspace.services.common.context.ServiceContext;
37 import org.collectionspace.services.common.document.DocumentWrapper;
38 import org.collectionspace.services.common.document.DocumentWrapperImpl;
39 import org.collectionspace.services.common.relation.IRelationsManager;
40 import org.collectionspace.services.common.repository.RepositoryClient;
41 import org.collectionspace.services.common.repository.RepositoryClientFactory;
42 import org.collectionspace.services.common.service.ObjectPartType;
43 import org.collectionspace.services.common.vocabulary.AuthorityJAXBSchema;
44 import org.collectionspace.services.common.vocabulary.AuthorityItemJAXBSchema;
45 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils;
46 import org.collectionspace.services.nuxeo.client.java.DocHandlerBase;
47 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
48 import org.collectionspace.services.relation.RelationResource;
49 import org.collectionspace.services.relation.RelationsCommon;
50 import org.collectionspace.services.relation.RelationsCommonList;
51 import org.collectionspace.services.relation.RelationsDocListItem;
52 import org.collectionspace.services.relation.RelationshipType;
53 import org.nuxeo.ecm.core.api.DocumentModel;
54 import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 import javax.ws.rs.core.MultivaluedMap;
59 import javax.ws.rs.core.UriInfo;
60 import java.util.ArrayList;
61 import java.util.List;
62 import java.util.Map;
63 import java.util.regex.Matcher;
64 import java.util.regex.Pattern;
65
66 //import org.collectionspace.services.common.authority.AuthorityItemRelations;
67 /**
68  * AuthorityItemDocumentModelHandler
69  *
70  * $LastChangedRevision: $
71  * $LastChangedDate: $
72  */
73 public abstract class AuthorityItemDocumentModelHandler<AICommon>
74         extends DocHandlerBase<AICommon> {
75
76     private final Logger logger = LoggerFactory.getLogger(AuthorityItemDocumentModelHandler.class);
77     private String authorityItemCommonSchemaName;
78     /**
79      * inVocabulary is the parent Authority for this context
80      */
81     protected String inAuthority;
82     protected String authorityRefNameBase;
83     
84     // Used to determine when the displayName changes as part of the update.
85     protected String oldDisplayNameOnUpdate = null;
86     protected String oldRefNameOnUpdate = null;
87     protected String newRefNameOnUpdate = null;
88
89     public AuthorityItemDocumentModelHandler(String authorityItemCommonSchemaName) {
90         this.authorityItemCommonSchemaName = authorityItemCommonSchemaName;
91     }
92
93     public String getInAuthority() {
94         return inAuthority;
95     }
96
97     public void setInAuthority(String inAuthority) {
98         this.inAuthority = inAuthority;
99     }
100
101     /** Subclasses may override this to customize the URI segment. */
102     public String getAuthorityServicePath() {
103         return getServiceContext().getServiceName().toLowerCase();    // Laramie20110510 CSPACE-3932
104     }
105
106     @Override
107     public String getUri(DocumentModel docModel) {
108         // Laramie20110510 CSPACE-3932
109         String authorityServicePath = getAuthorityServicePath();
110         return "/" + authorityServicePath + '/' + inAuthority + '/' + AuthorityClient.ITEMS + '/' + getCsid(docModel);
111     }
112
113     public String getAuthorityRefNameBase() {
114         return this.authorityRefNameBase;
115     }
116
117     public void setAuthorityRefNameBase(String value) {
118         this.authorityRefNameBase = value;
119     }
120
121     /* (non-Javadoc)
122      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#handleCreate(org.collectionspace.services.common.document.DocumentWrapper)
123      */
124     @Override
125     public void handleCreate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
126         // first fill all the parts of the document
127         super.handleCreate(wrapDoc);
128         // Ensure we have required fields set properly
129         handleInAuthority(wrapDoc.getWrappedObject());
130         
131         handleComputedDisplayNames(wrapDoc.getWrappedObject());
132         String displayName = (String) wrapDoc.getWrappedObject().getProperty(authorityItemCommonSchemaName, 
133                                 AuthorityItemJAXBSchema.DISPLAY_NAME);
134         if(Tools.isEmpty(displayName)) {
135                 logger.warn("Creating Authority Item with no displayName!");
136         }
137         // CSPACE-3178:
138         handleDisplayNameAsShortIdentifier(wrapDoc.getWrappedObject(), authorityItemCommonSchemaName);
139         // refName includes displayName, so we force a correct value here.
140         updateRefnameForAuthorityItem(wrapDoc, authorityItemCommonSchemaName, getAuthorityRefNameBase());
141     }
142     
143     /* (non-Javadoc)
144      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#handleUpdate(org.collectionspace.services.common.document.DocumentWrapper)
145      */
146     @Override
147     public void handleUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
148         // First, get a copy of the old displayName
149         oldDisplayNameOnUpdate = (String) wrapDoc.getWrappedObject().getProperty(authorityItemCommonSchemaName, 
150                                                                                                                                 AuthorityItemJAXBSchema.DISPLAY_NAME);
151         oldRefNameOnUpdate = (String) wrapDoc.getWrappedObject().getProperty(authorityItemCommonSchemaName, 
152                                                                                                                                         AuthorityItemJAXBSchema.REF_NAME);
153         super.handleUpdate(wrapDoc);
154         handleComputedDisplayNames(wrapDoc.getWrappedObject());
155         String newDisplayName = (String) wrapDoc.getWrappedObject().getProperty(authorityItemCommonSchemaName, 
156                                                                                                                                         AuthorityItemJAXBSchema.DISPLAY_NAME);
157         if(newDisplayName != null && !newDisplayName.equals(oldDisplayNameOnUpdate)) {
158                 // Need to update the refName, and then fix all references.
159                 newRefNameOnUpdate = handleItemRefNameUpdateForDisplayName(wrapDoc.getWrappedObject(), newDisplayName);
160         } else {
161                 // Mark as not needing attention in completeUpdate phase.
162                 newRefNameOnUpdate = null;
163                 oldRefNameOnUpdate = null;
164         }
165     }
166
167     /**
168      * Handle display name.
169      *
170      * @param docModel the doc model
171      * @throws Exception the exception
172      */
173     protected void handleComputedDisplayNames(DocumentModel docModel) throws Exception {
174         // Do nothing by default.
175     }
176
177     /**
178      * Handle refName updates for changes to display name.
179      * Assumes refName is already correct. Just ensures it is right.
180      *
181      * @param docModel the doc model
182      * @throws Exception the exception
183      */
184     protected String handleItemRefNameUpdateForDisplayName(DocumentModel docModel,
185             String newDisplayName) throws Exception {
186         //String suppliedRefName = (String) docModel.getProperty(authorityItemCommonSchemaName, 
187         //                                                                                                              AuthorityItemJAXBSchema.REF_NAME);
188         RefName.AuthorityItem authItem = RefName.AuthorityItem.parse(oldRefNameOnUpdate);
189         if(authItem == null) {
190                 String err = "Authority Item has illegal refName: "+oldRefNameOnUpdate;
191                 logger.debug(err);
192                 throw new IllegalArgumentException(err);
193         }
194         authItem.displayName = newDisplayName;
195         String updatedRefName = authItem.toString();
196         docModel.setProperty(authorityItemCommonSchemaName, AuthorityItemJAXBSchema.REF_NAME, updatedRefName);
197         return updatedRefName;
198     }
199
200     
201     /**
202      * Checks to see if the refName has changed, and if so, 
203      * uses utilities to find all references and update them.
204      */
205     protected void handleItemRefNameReferenceUpdate() {
206         if(newRefNameOnUpdate != null && oldRefNameOnUpdate!= null) {
207                 // We have work to do.
208                 logger.debug("Need to find and update references to Item.");
209                 logger.debug("Old refName" + oldRefNameOnUpdate);
210                 logger.debug("New refName" + newRefNameOnUpdate);
211         }
212     }
213
214
215     private void handleDisplayNameAsShortIdentifier(DocumentModel docModel, String schemaName) throws Exception {
216         String shortIdentifier = (String) docModel.getProperty(schemaName, AuthorityItemJAXBSchema.SHORT_IDENTIFIER);
217         String displayName = (String) docModel.getProperty(schemaName, AuthorityItemJAXBSchema.DISPLAY_NAME);
218         String shortDisplayName = "";
219         try {
220             shortDisplayName = (String) docModel.getProperty(schemaName, AuthorityItemJAXBSchema.SHORT_DISPLAY_NAME);
221         } catch (PropertyNotFoundException pnfe) {
222             // Do nothing on exception. Some vocabulary schemas may not include a short display name.
223         }
224         if (Tools.isEmpty(shortIdentifier)) {
225             String generatedShortIdentifier = AuthorityIdentifierUtils.generateShortIdentifierFromDisplayName(displayName, shortDisplayName);
226             docModel.setProperty(schemaName, AuthorityItemJAXBSchema.SHORT_IDENTIFIER, generatedShortIdentifier);
227         }
228     }
229
230     protected void updateRefnameForAuthorityItem(DocumentWrapper<DocumentModel> wrapDoc,
231             String schemaName,
232             String authorityRefBaseName) throws Exception {
233         DocumentModel docModel = wrapDoc.getWrappedObject();
234         String suppliedRefName = (String) docModel.getProperty(schemaName, AuthorityItemJAXBSchema.REF_NAME);
235         // CSPACE-3178:
236         // Temporarily accept client-supplied refName values, rather than always generating such values.
237         // Remove first block and the surrounding 'if' statement when clients should no longer supply refName values.
238         if(!Tools.isEmpty(suppliedRefName) ) {
239                 // Supplied refName must at least be legal
240                 RefName.AuthorityItem item = RefName.AuthorityItem.parse(suppliedRefName);
241                 if(item==null) {
242                 logger.error("Passed refName for authority item not legal: "+suppliedRefName);
243                 suppliedRefName = null; // Clear this and compute a new one below.
244                 }
245         } 
246         // Recheck, in case we cleared it for being illegal
247         if(Tools.isEmpty(suppliedRefName) ) {
248             String shortIdentifier = (String) docModel.getProperty(schemaName, AuthorityItemJAXBSchema.SHORT_IDENTIFIER);
249             String displayName = (String) docModel.getProperty(schemaName, AuthorityItemJAXBSchema.DISPLAY_NAME);
250             if (Tools.isEmpty(authorityRefBaseName)) {
251                 throw new Exception("Could not create the refName for this authority term, because the refName for its authority parent was empty.");
252             }
253             RefName.Authority authority = RefName.Authority.parse(authorityRefBaseName);
254             String refName = RefName.buildAuthorityItem(authority, shortIdentifier, displayName).toString();
255             docModel.setProperty(schemaName, AuthorityItemJAXBSchema.REF_NAME, refName);
256         }
257     }
258
259     /**
260      * Check the logic around the parent pointer. Note that we only need do this on
261      * create, since we have logic to make this read-only on update. 
262      * 
263      * @param docModel
264      * 
265      * @throws Exception the exception
266      */
267     private void handleInAuthority(DocumentModel docModel) throws Exception {
268         docModel.setProperty(authorityItemCommonSchemaName,
269                 AuthorityItemJAXBSchema.IN_AUTHORITY, inAuthority);
270     }
271
272
273     /* (non-Javadoc)
274      * @see org.collectionspace.services.nuxeo.client.java.RemoteDocumentModelHandlerImpl#extractPart(org.nuxeo.ecm.core.api.DocumentModel, java.lang.String, org.collectionspace.services.common.service.ObjectPartType)
275      */
276     @Override
277     protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
278             throws Exception {
279         Map<String, Object> unQObjectProperties = super.extractPart(docModel, schema, partMeta);
280
281         // Add the CSID to the common part
282         if (partMeta.getLabel().equalsIgnoreCase(authorityItemCommonSchemaName)) {
283             String csid = getCsid(docModel);//NuxeoUtils.extractId(docModel.getPathAsString());
284             unQObjectProperties.put("csid", csid);
285         }
286
287         return unQObjectProperties;
288     }
289
290     /**
291      * Filters out selected values supplied in an update request.
292      * 
293      * For example, filters out AuthorityItemJAXBSchema.IN_AUTHORITY, to ensure
294      * that the link to the item's parent remains untouched.
295      * 
296      * @param objectProps the properties filtered out from the update payload
297      * @param partMeta metadata for the object to fill
298      */
299     @Override
300     public void filterReadOnlyPropertiesForPart(
301             Map<String, Object> objectProps, ObjectPartType partMeta) {
302         super.filterReadOnlyPropertiesForPart(objectProps, partMeta);
303         String commonPartLabel = getServiceContext().getCommonPartLabel();
304         if (partMeta.getLabel().equalsIgnoreCase(commonPartLabel)) {
305             objectProps.remove(AuthorityItemJAXBSchema.IN_AUTHORITY);
306             objectProps.remove(AuthorityItemJAXBSchema.CSID);
307             objectProps.remove(AuthorityJAXBSchema.SHORT_IDENTIFIER);
308             // Enable when clients should no longer supply refName values
309             // objectProps.remove(AuthorityItemJAXBSchema.REF_NAME); // CSPACE-3178
310
311         }
312     }
313
314     @Override
315     public void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
316         MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
317         super.extractAllParts(wrapDoc);
318
319         String showSiblings = ctx.getQueryParams().getFirst(CommonAPI.showSiblings_QP);
320         if (Tools.isTrue(showSiblings)) {
321             showSiblings(wrapDoc, ctx);
322             return;   // actual result is returned on ctx.addOutputPart();
323         }
324
325         String showRelations = ctx.getQueryParams().getFirst(CommonAPI.showRelations_QP);
326         if (Tools.isTrue(showRelations)) {
327             showRelations(wrapDoc, ctx);
328             return;   // actual result is returned on ctx.addOutputPart();
329         }
330
331         String showAllRelations = ctx.getQueryParams().getFirst(CommonAPI.showAllRelations_QP);
332         if (Tools.isTrue(showAllRelations)) {
333             showAllRelations(wrapDoc, ctx);
334             return;   // actual result is returned on ctx.addOutputPart();
335         }
336     }
337
338     /** @return null on parent not found
339      */
340     protected String getParentCSID(String thisCSID) throws Exception {
341         String parentCSID = null;
342         try {
343             String predicate = RelationshipType.HAS_BROADER.value();
344             RelationsCommonList parentListOuter = getRelations(thisCSID, null, predicate);
345             List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
346             if (parentList != null) {
347                 if (parentList.size() == 0) {
348                     return null;
349                 }
350                 RelationsCommonList.RelationListItem relationListItem = parentList.get(0);
351                 parentCSID = relationListItem.getObjectCsid();
352             }
353             return parentCSID;
354         } catch (Exception e) {
355             logger.error("Could not find parent for this: " + thisCSID, e);
356             return null;
357         }
358     }
359
360     public void showRelations(DocumentWrapper<DocumentModel> wrapDoc,
361             MultipartServiceContext ctx) throws Exception {
362         String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
363
364         String predicate = RelationshipType.HAS_BROADER.value();
365         RelationsCommonList parentListOuter = getRelations(thisCSID, null, predicate);
366         List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
367
368         RelationsCommonList childrenListOuter = getRelations(null, thisCSID, predicate);
369         List<RelationsCommonList.RelationListItem> childrenList = childrenListOuter.getRelationListItem();
370
371         //Assume that there are more children than parents.  Will be true for parent/child, but maybe not for other relations.
372         //Now add all parents to our childrenList, to be able to return just one list of consolidated results.
373         //Not optimal, but that's the current design spec.
374         long added = 0;
375         for (RelationsCommonList.RelationListItem parent : parentList) {
376             childrenList.add(parent);
377             added++;
378         }
379         long childrenSize = childrenList.size();
380         childrenListOuter.setTotalItems(childrenSize);
381         childrenListOuter.setItemsInPage(childrenListOuter.getItemsInPage() + added);
382
383         PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, childrenListOuter);
384         ctx.addOutputPart(relationsPart);
385     }
386
387     public void showSiblings(DocumentWrapper<DocumentModel> wrapDoc,
388             MultipartServiceContext ctx) throws Exception {
389         String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
390         String parentCSID = getParentCSID(thisCSID);
391         if (parentCSID == null) {
392             logger.warn("~~~~~\r\n~~~~ Could not find parent for this: " + thisCSID);
393             return;
394         }
395
396         String predicate = RelationshipType.HAS_BROADER.value();
397         RelationsCommonList siblingListOuter = getRelations(null, parentCSID, predicate);
398         List<RelationsCommonList.RelationListItem> siblingList = siblingListOuter.getRelationListItem();
399
400         List<RelationsCommonList.RelationListItem> toRemoveList = newList();
401
402
403         RelationsCommonList.RelationListItem item = null;
404         for (RelationsCommonList.RelationListItem sibling : siblingList) {
405             if (thisCSID.equals(sibling.getSubjectCsid())) {
406                 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.
407             }
408         }
409         //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.
410         for (RelationsCommonList.RelationListItem self : toRemoveList) {
411             removeFromList(siblingList, self);
412         }
413
414         long siblingSize = siblingList.size();
415         siblingListOuter.setTotalItems(siblingSize);
416         siblingListOuter.setItemsInPage(siblingSize);
417
418         PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, siblingListOuter);
419         ctx.addOutputPart(relationsPart);
420     }
421
422     public void showAllRelations(DocumentWrapper<DocumentModel> wrapDoc, MultipartServiceContext ctx) throws Exception {
423         String thisCSID = NuxeoUtils.getCsid(wrapDoc.getWrappedObject());
424
425         RelationsCommonList subjectListOuter = getRelations(thisCSID, null, null);   //  nulls are wildcards:  predicate=*, and object=*
426         List<RelationsCommonList.RelationListItem> subjectList = subjectListOuter.getRelationListItem();
427
428         RelationsCommonList objectListOuter = getRelations(null, thisCSID, null);   //  nulls are wildcards:  subject=*, and predicate=*
429         List<RelationsCommonList.RelationListItem> objectList = objectListOuter.getRelationListItem();
430
431         //  MERGE LISTS:
432         subjectList.addAll(objectList);
433
434         //now subjectList actually has records BOTH where thisCSID is subject and object.
435         long relatedSize = subjectList.size();
436         subjectListOuter.setTotalItems(relatedSize);
437         subjectListOuter.setItemsInPage(relatedSize);
438
439         PayloadOutputPart relationsPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, subjectListOuter);
440         ctx.addOutputPart(relationsPart);
441     }
442
443     public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
444         super.fillAllParts(wrapDoc, action);
445         /*
446         ServiceContext ctx = getServiceContext();
447         PoxPayloadIn input = (PoxPayloadIn) ctx.getInput();
448         DocumentModel documentModel = (wrapDoc.getWrappedObject());
449         String itemCsid = documentModel.getName();
450
451         //UPDATE and CREATE will call.   Updates relations part
452         RelationsCommonList relationsCommonList = updateRelations(itemCsid, input, wrapDoc);
453
454         PayloadOutputPart payloadOutputPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, relationsCommonList);
455         ctx.setProperty(RelationClient.SERVICE_COMMON_LIST_NAME, payloadOutputPart);
456         */
457     }
458
459     public void completeCreate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
460         super.completeCreate(wrapDoc);
461         handleRelationsPayload(wrapDoc, false);
462     }
463
464     public void completeUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
465         super.completeUpdate(wrapDoc);
466         handleRelationsPayload(wrapDoc, true);
467         handleItemRefNameReferenceUpdate();
468     }
469     
470     // Note that we must do this after we have completed the Update, so that the repository has the
471     // info for the item itself. The relations code must call into the repo to get info for each end.
472     // This could be optimized to pass in the parent docModel, since it will often be one end.
473     // Nevertheless, we should complete the item save before we do work on the relations, especially
474     // since a save on Create might fail, and we would not want to create relations for something
475     // that may not be created...
476     private void handleRelationsPayload(DocumentWrapper<DocumentModel> wrapDoc, boolean fUpdate) throws Exception {
477         ServiceContext ctx = getServiceContext();
478         PoxPayloadIn input = (PoxPayloadIn) ctx.getInput();
479         DocumentModel documentModel = (wrapDoc.getWrappedObject());
480         String itemCsid = documentModel.getName();
481
482         //Updates relations part
483         RelationsCommonList relationsCommonList = updateRelations(itemCsid, input, wrapDoc, fUpdate);
484
485         PayloadOutputPart payloadOutputPart = new PayloadOutputPart(RelationClient.SERVICE_COMMON_LIST_NAME, relationsCommonList);
486         ctx.setProperty(RelationClient.SERVICE_COMMON_LIST_NAME, payloadOutputPart);
487
488         //now we add part for relations list
489         //ServiceContext ctx = getServiceContext();
490         //PayloadOutputPart foo = (PayloadOutputPart) ctx.getProperty(RelationClient.SERVICE_COMMON_LIST_NAME);
491         ((PoxPayloadOut) ctx.getOutput()).addPart(payloadOutputPart);
492     }
493
494     /**  updateRelations strategy:
495     
496     go through inboundList, remove anything from childList that matches  from childList
497     go through inboundList, remove anything from parentList that matches  from parentList
498     go through parentList, delete all remaining
499     go through childList, delete all remaining
500     go through actionList, add all remaining.
501     check for duplicate children
502     check for more than one parent.
503     
504     inboundList                           parentList                      childList          actionList
505     ----------------                          ---------------                  ----------------       ----------------
506     child-a                                   parent-c                        child-a             child-b
507     child-b                                   parent-d                        child-c
508     parent-a
509      */
510     private RelationsCommonList updateRelations(
511                 String itemCSID, PoxPayloadIn input, DocumentWrapper<DocumentModel> wrapDoc, boolean fUpdate)
512             throws Exception {
513         if(logger.isTraceEnabled()) {
514                 logger.trace("AuthItemDocHndler.updateRelations for: "+itemCSID);
515         }
516         PayloadInputPart part = input.getPart(RelationClient.SERVICE_COMMON_LIST_NAME);        //input.getPart("relations_common");
517         if (part == null) {
518             return null;  //nothing to do--they didn't send a list of relations.
519         }
520         RelationsCommonList relationsCommonListBody = (RelationsCommonList) part.getBody();
521         List<RelationsCommonList.RelationListItem> inboundList = relationsCommonListBody.getRelationListItem();
522         List<RelationsCommonList.RelationListItem> actionList = newList();
523         List<RelationsCommonList.RelationListItem> childList = null;
524         List<RelationsCommonList.RelationListItem> parentList = null;
525         DocumentModel docModel = wrapDoc.getWrappedObject();
526
527         ServiceContext ctx = getServiceContext();
528         //Do magic replacement of ${itemCSID} and fix URI's.
529         fixupInboundListItems(ctx, inboundList, docModel, itemCSID);
530
531         String HAS_BROADER = RelationshipType.HAS_BROADER.value();
532         UriInfo uriInfo = ctx.getUriInfo();
533         MultivaluedMap queryParams = uriInfo.getQueryParameters();
534
535         if(fUpdate) {
536                 //Run getList() once as sent to get childListOuter:
537                 String predicate = RelationshipType.HAS_BROADER.value();
538                 queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
539                 queryParams.putSingle(IRelationsManager.SUBJECT_QP, null);
540                 queryParams.putSingle(IRelationsManager.SUBJECT_TYPE_QP, null);
541                 queryParams.putSingle(IRelationsManager.OBJECT_QP, itemCSID);
542                 queryParams.putSingle(IRelationsManager.OBJECT_TYPE_QP, null);
543                 RelationsCommonList childListOuter = (new RelationResource()).getList(ctx.getUriInfo());    //magically knows all query params because they are in the context.
544         
545                 //Now run getList() again, leaving predicate, swapping subject and object, to get parentListOuter.
546                 queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
547                 queryParams.putSingle(IRelationsManager.SUBJECT_QP, itemCSID);
548                 queryParams.putSingle(IRelationsManager.OBJECT_QP, null);
549                 RelationsCommonList parentListOuter = (new RelationResource()).getList(ctx.getUriInfo());
550         
551
552                 childList = childListOuter.getRelationListItem();
553                 parentList = parentListOuter.getRelationListItem();
554
555                 if (parentList.size() > 1) {
556                     throw new Exception("Too many parents for object: " + itemCSID + " list: " + dumpList(parentList, "parentList"));
557                 }
558         
559                 if(logger.isTraceEnabled()) {
560                         logger.trace("AuthItemDocHndler.updateRelations for: "+itemCSID+" got existing relations.");
561                 }
562         }
563
564
565         for (RelationsCommonList.RelationListItem inboundItem : inboundList) {
566                 // Note that the relations may specify the other (non-item) bit with a refName, not a CSID,
567                 // and so the CSID for those may be null
568             if (itemCSID.equals(inboundItem.getObject().getCsid()) 
569                         && inboundItem.getPredicate().equals(HAS_BROADER)) {
570                 //then this is an item that says we have a child.  That child is inboundItem
571                 RelationsCommonList.RelationListItem childItem = 
572                                 (childList==null)?null:findInList(childList, inboundItem);
573                 if (childItem != null) {
574                     removeFromList(childList, childItem);    //exists, just take it off delete list
575                 } else {
576                     actionList.add(inboundItem);   //doesn't exist as a child, but is a child.  Add to additions list
577                 }
578                 ensureChildHasNoOtherParents(ctx, queryParams, inboundItem.getSubject().getCsid());
579
580             } else if (itemCSID.equals(inboundItem.getSubject().getCsid())
581                         && inboundItem.getPredicate().equals(HAS_BROADER)) {
582                 //then this is an item that says we have a parent.  inboundItem is that parent.
583                 RelationsCommonList.RelationListItem parentItem = 
584                                 (parentList==null)?null:findInList(parentList, inboundItem);
585                 if (parentItem != null) {
586                     removeFromList(parentList, parentItem);    //exists, just take it off delete list
587                 } else {
588                     actionList.add(inboundItem);   //doesn't exist as a parent, but is a parent. Add to additions list
589                 }
590             } else {
591                 logger.warn("Element didn't match parent or child, but may have partial fields that match. inboundItem: " + inboundItem);
592                 //not dealing with: hasNarrower or any other predicate.
593             }
594         }
595         if(logger.isTraceEnabled()) {
596                 String dump = dumpLists(itemCSID, parentList, childList, actionList);
597                 //System.out.println("====dump====="+CR+dump);
598                 logger.trace("~~~~~~~~~~~~~~~~~~~~~~dump~~~~~~~~~~~~~~~~~~~~~~~~" + CR + dump);
599         }
600         if(fUpdate) {
601             if(logger.isTraceEnabled()) {
602                 logger.trace("AuthItemDocHndler.updateRelations for: "+itemCSID+" deleting "
603                                         +parentList.size()+" existing parents and "+childList.size()+" existing children.");
604             }
605                 deleteRelations(parentList, ctx, "parentList");               //todo: there are items appearing on both lists....april 20.
606                 deleteRelations(childList, ctx, "childList");
607         }
608         if(logger.isTraceEnabled()) {
609                 logger.trace("AuthItemDocHndler.updateRelations for: "+itemCSID+" adding "
610                                 +actionList.size()+" new parents and children.");
611         }
612         createRelations(actionList, ctx);
613         if(logger.isTraceEnabled()) {
614                 logger.trace("AuthItemDocHndler.updateRelations for: "+itemCSID+" done.");
615         }
616         //We return all elements on the inbound list, since we have just worked to make them exist in the system
617         // and be non-redundant, etc.  That list came from relationsCommonListBody, so it is still attached to it, just pass that back.
618         return relationsCommonListBody;
619     }
620
621     private void ensureChildHasNoOtherParents(ServiceContext ctx, MultivaluedMap queryParams, String childCSID) {
622         queryParams.putSingle(IRelationsManager.SUBJECT_QP, childCSID);
623         queryParams.putSingle(IRelationsManager.PREDICATE_QP, RelationshipType.HAS_BROADER.value());
624         queryParams.putSingle(IRelationsManager.OBJECT_QP, null);  //null means ANY
625         RelationsCommonList parentListOuter = (new RelationResource()).getList(ctx.getUriInfo());
626         List<RelationsCommonList.RelationListItem> parentList = parentListOuter.getRelationListItem();
627         //logger.warn("ensureChildHasNoOtherParents preparing to delete relations on "+childCSID+"\'s parent list: \r\n"+dumpList(parentList, "duplicate parent list"));
628         deleteRelations(parentList, ctx, "parentList-delete");
629     }
630
631     private String dumpLists(String itemCSID,
632             List<RelationsCommonList.RelationListItem> parentList,
633             List<RelationsCommonList.RelationListItem> childList,
634             List<RelationsCommonList.RelationListItem> actionList) {
635         StringBuffer sb = new StringBuffer();
636         sb.append("itemCSID: " + itemCSID + CR);
637         sb.append(dumpList(parentList, "parentList"));
638         sb.append(dumpList(childList, "childList"));
639         sb.append(dumpList(actionList, "actionList"));
640         return sb.toString();
641     }
642     private final static String CR = "\r\n";
643     private final static String T = " ";
644
645     private String dumpList(List<RelationsCommonList.RelationListItem> list, String label) {
646         StringBuffer sb = new StringBuffer();
647         String s;
648         if (list.size() > 0) {
649             sb.append("=========== " + label + " ==========" + CR);
650         }
651         for (RelationsCommonList.RelationListItem item : list) {
652             s =
653                     T + item.getSubject().getCsid() //+T4 + item.getSubject().getUri()
654                     + T + item.getPredicate()
655                     + T + item.getObject().getCsid() //+T4  + item.getObject().getUri()
656                     + CR //+"subject:{"+item.getSubject()+"}\r\n object:{"+item.getObject()+"}"
657                     //+ CR + "relation-record: {"+item+"}"
658                     ;
659             sb.append(s);
660
661         }
662         return sb.toString();
663     }
664
665     /** Performs substitution for ${itemCSID} (see CommonAPI.AuthorityItemCSID_REPLACE for constant)
666      *   and sets URI correctly for related items.
667      *   Operates directly on the items in the list.  Does not change the list ordering, does not add or remove any items.
668      */
669     protected void fixupInboundListItems(ServiceContext ctx,
670             List<RelationsCommonList.RelationListItem> inboundList,
671             DocumentModel docModel,
672             String itemCSID) throws Exception {
673         String thisURI = this.getUri(docModel);
674         // WARNING:  the two code blocks below are almost identical  and seem to ask to be put in a generic method.
675         //                    beware of the little diffs in  inboundItem.setObjectCsid(itemCSID); and   inboundItem.setSubjectCsid(itemCSID); in the two blocks.
676         for (RelationsCommonList.RelationListItem inboundItem : inboundList) {
677             RelationsDocListItem inboundItemObject = inboundItem.getObject();
678             RelationsDocListItem inboundItemSubject = inboundItem.getSubject();
679
680             if (CommonAPI.AuthorityItemCSID_REPLACE.equalsIgnoreCase(inboundItemObject.getCsid())) {
681                 inboundItem.setObjectCsid(itemCSID);
682                 inboundItemObject.setCsid(itemCSID);
683                 //inboundItemObject.setUri(getUri(docModel));
684             } else {
685                 /*
686                 String objectCsid = inboundItemObject.getCsid();
687                 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(getRepositorySession(), ctx, objectCsid);    //null if not found.
688                 DocumentWrapper wrapper = new DocumentWrapperImpl(itemDocModel);
689                 String uri = this.getRepositoryClient(ctx).getDocURI(wrapper);
690                 inboundItemObject.setUri(uri);    //CSPACE-4037
691                 */
692             }
693             //uriPointsToSameAuthority(thisURI, inboundItemObject.getUri());    //CSPACE-4042
694
695             if (CommonAPI.AuthorityItemCSID_REPLACE.equalsIgnoreCase(inboundItemSubject.getCsid())) {
696                 inboundItem.setSubjectCsid(itemCSID);
697                 inboundItemSubject.setCsid(itemCSID);
698                 //inboundItemSubject.setUri(getUri(docModel));
699             } else {
700                 /*
701                 String subjectCsid = inboundItemSubject.getCsid();
702                 DocumentModel itemDocModel = NuxeoUtils.getDocFromCsid(getRepositorySession(), ctx, subjectCsid);    //null if not found.
703                 DocumentWrapper wrapper = new DocumentWrapperImpl(itemDocModel);
704                 String uri = this.getRepositoryClient(ctx).getDocURI(wrapper);
705                 inboundItemSubject.setUri(uri);    //CSPACE-4037
706                 */
707             }
708             //uriPointsToSameAuthority(thisURI, inboundItemSubject.getUri());  //CSPACE-4042
709
710         }
711     }
712
713     // this method calls the RelationResource to have it create the relations and persist them.
714     private void createRelations(List<RelationsCommonList.RelationListItem> inboundList, ServiceContext ctx) throws Exception {
715         for (RelationsCommonList.RelationListItem item : inboundList) {
716             RelationsCommon rc = new RelationsCommon();
717             //rc.setCsid(item.getCsid());
718             //todo: assignTo(item, rc);
719             RelationsDocListItem itemSubject = item.getSubject();
720             RelationsDocListItem itemObject = item.getObject();
721
722             // Set at least one of CSID and refName for Subject and Object
723             // Either value might be null for for each of Subject and Object 
724             String subjectCsid = itemSubject.getCsid();
725             rc.setSubjectCsid(subjectCsid);
726             rc.setDocumentId1(subjectCsid); // populate legacy field for backward compatibility
727
728             String objCsid = itemObject.getCsid();
729             rc.setObjectCsid(objCsid);
730             rc.setDocumentId2(objCsid); // populate legacy field for backward compatibility
731             
732             rc.setSubjectRefName(itemSubject.getRefName());
733             rc.setObjectRefName(itemObject.getRefName());
734
735             rc.setRelationshipType(item.getPredicate());
736             //RelationshipType  foo = (RelationshipType.valueOf(item.getPredicate())) ;
737             //rc.setPredicate(foo);     //this must be one of the type found in the enum in  services/jaxb/src/main/resources/relations_common.xsd
738
739             // This is superfluous, since it will be fetched by the Relations Create logic.
740             rc.setSubjectDocumentType(itemSubject.getDocumentType());
741             rc.setObjectDocumentType(itemObject.getDocumentType());
742             // populate legacy fields for backward compatibility
743             rc.setDocumentType1(itemSubject.getDocumentType());
744             rc.setDocumentType2(itemObject.getDocumentType());
745
746             // This is superfluous, since it will be fetched by the Relations Create logic.
747             rc.setSubjectUri(itemSubject.getUri());
748             rc.setObjectUri(itemObject.getUri());
749             // May not have the info here. Only really require CSID or refName. 
750             // Rest is handled in the Relation create mechanism
751             //uriPointsToSameAuthority(itemSubject.getUri(), itemObject.getUri());
752
753             PoxPayloadOut payloadOut = new PoxPayloadOut(RelationClient.SERVICE_PAYLOAD_NAME);
754             PayloadOutputPart outputPart = new PayloadOutputPart(RelationClient.SERVICE_COMMONPART_NAME, rc);
755             payloadOut.addPart(outputPart);
756             //System.out.println("\r\n==== TO CREATE: "+rc.getDocumentId1()+"==>"+rc.getPredicate()+"==>"+rc.getDocumentId2());
757             RelationResource relationResource = new RelationResource();
758             Object res = relationResource.create(ctx.getResourceMap(), 
759                         ctx.getUriInfo(), payloadOut.toXML());    //NOTE ui recycled from above to pass in unknown query params.
760         }
761     }
762
763     private void deleteRelations(List<RelationsCommonList.RelationListItem> list, ServiceContext ctx, String listName) {
764         try {
765             //if (list.size()>0){ logger.info("==== deleteRelations from : "+listName); }
766             for (RelationsCommonList.RelationListItem item : list) {
767                 RelationResource relationResource = new RelationResource();
768                 //logger.info("==== TO DELETE: " + item.getCsid() + ": " + item.getSubject().getCsid() + "--" + item.getPredicate() + "-->" + item.getObject().getCsid());
769                 Object res = relationResource.delete(item.getCsid());
770             }
771         } catch (Throwable t) {
772             String msg = "Unable to deleteRelations: " + Tools.errorToString(t, true);
773             logger.error(msg);
774         }
775     }
776
777     private List<RelationsCommonList.RelationListItem> newList() {
778         List<RelationsCommonList.RelationListItem> result = new ArrayList<RelationsCommonList.RelationListItem>();
779         return result;
780     }
781
782     protected List<RelationsCommonList.RelationListItem> cloneList(List<RelationsCommonList.RelationListItem> inboundList) {
783         List<RelationsCommonList.RelationListItem> result = newList();
784         for (RelationsCommonList.RelationListItem item : inboundList) {
785             result.add(item);
786         }
787         return result;
788     }
789
790     private RelationsCommonList.RelationListItem findInList(List<RelationsCommonList.RelationListItem> list, RelationsCommonList.RelationListItem item) {
791         for (RelationsCommonList.RelationListItem listItem : list) {
792             if (itemsEqual(listItem, item)) {   //equals must be defined, else
793                 return listItem;
794             }
795         }
796         return null;
797     }
798
799     private boolean itemsEqual(RelationsCommonList.RelationListItem item, RelationsCommonList.RelationListItem item2) {
800         if (item == null || item2 == null) {
801             return false;
802         }
803         RelationsDocListItem subj1 = item.getSubject();
804         RelationsDocListItem subj2 = item2.getSubject();
805         RelationsDocListItem obj1 = item.getObject();
806         RelationsDocListItem obj2 = item2.getObject();
807
808         return (subj1.getCsid().equals(subj2.getCsid()))
809                 && (obj1.getCsid().equals(obj1.getCsid()))
810                 && ((item.getPredicate().equals(item2.getPredicate()))
811                 && (item.getRelationshipType().equals(item2.getRelationshipType())))
812                 && (obj1.getDocumentType().equals(obj2.getDocumentType()))
813                 && (subj1.getDocumentType().equals(subj2.getDocumentType()));
814     }
815
816     private void removeFromList(List<RelationsCommonList.RelationListItem> list, RelationsCommonList.RelationListItem item) {
817         list.remove(item);
818     }
819
820     /* don't even THINK of re-using this method.
821      * String example_uri = "/locationauthorities/7ec60f01-84ab-4908-9a6a/items/a5466530-713f-43b4-bc05";
822      */
823     private String extractInAuthorityCSID(String uri) {
824         String IN_AUTHORITY_REGEX = "/(.*?)/(.*?)/(.*)";
825         Pattern p = Pattern.compile(IN_AUTHORITY_REGEX);
826         Matcher m = p.matcher(uri);
827         if (m.find()) {
828             if (m.groupCount() < 3) {
829                 logger.warn("REGEX-WRONG-GROUPCOUNT looking in " + uri);
830                 return "";
831             } else {
832                 //String service = m.group(1);
833                 String inauth = m.group(2);
834                 //String theRest = m.group(3);
835                 return inauth;
836                 //print("service:"+service+", inauth:"+inauth+", rest:"+rest);
837             }
838         } else {
839             logger.warn("REGEX-NOT-MATCHED looking in " + uri);
840             return "";
841         }
842     }
843
844     //ensures CSPACE-4042
845     protected void uriPointsToSameAuthority(String thisURI, String inboundItemURI) throws Exception {
846         String authorityCSID = extractInAuthorityCSID(thisURI);
847         String authorityCSIDForInbound = extractInAuthorityCSID(inboundItemURI);
848         if (Tools.isBlank(authorityCSID)
849                 || Tools.isBlank(authorityCSIDForInbound)
850                 || (!authorityCSID.equalsIgnoreCase(authorityCSIDForInbound))) {
851             throw new Exception("Item URI " + thisURI + " must point to same authority as related item: " + inboundItemURI);
852         }
853     }
854
855     //================= TODO: move this to common, refactoring this and  CollectionObjectResource.java
856     public RelationsCommonList getRelations(String subjectCSID, String objectCSID, String predicate) throws Exception {
857         ServiceContext ctx = getServiceContext();
858         MultivaluedMap queryParams = ctx.getQueryParams();
859         queryParams.putSingle(IRelationsManager.PREDICATE_QP, predicate);
860         queryParams.putSingle(IRelationsManager.SUBJECT_QP, subjectCSID);
861         queryParams.putSingle(IRelationsManager.OBJECT_QP, objectCSID);
862
863         RelationResource relationResource = new RelationResource();
864         RelationsCommonList relationsCommonList = relationResource.getList(ctx.getUriInfo());
865         return relationsCommonList;
866     }
867     //============================= END TODO refactor ==========================
868 }