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