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