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