]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
c3d895e53c07b2467bde3bc6486175efcb0e8b79
[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 java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.regex.Matcher;
32 import java.util.regex.Pattern;
33 import java.util.regex.PatternSyntaxException;
34
35 import javax.ws.rs.core.MultivaluedMap;
36
37 import org.collectionspace.services.client.AuthorityClient;
38 import org.collectionspace.services.client.IQueryManager;
39 import org.collectionspace.services.client.PoxPayloadIn;
40 import org.collectionspace.services.client.PoxPayloadOut;
41 import org.collectionspace.services.client.workflow.WorkflowClient;
42 import org.collectionspace.services.common.api.RefName;
43 import org.collectionspace.services.common.api.Tools;
44 import org.collectionspace.services.common.authorityref.AuthorityRefDocList;
45 import org.collectionspace.services.common.context.MultipartServiceContext;
46 import org.collectionspace.services.common.context.ServiceContext;
47 import org.collectionspace.services.common.document.DocumentException;
48 import org.collectionspace.services.common.document.DocumentFilter;
49 import org.collectionspace.services.common.document.DocumentNotFoundException;
50 import org.collectionspace.services.common.document.DocumentReferenceException;
51 import org.collectionspace.services.common.document.DocumentWrapper;
52 import org.collectionspace.services.common.repository.RepositoryClient;
53 import org.collectionspace.services.common.vocabulary.AuthorityItemJAXBSchema;
54 import org.collectionspace.services.common.vocabulary.AuthorityJAXBSchema;
55 import org.collectionspace.services.common.vocabulary.AuthorityResource;
56 import org.collectionspace.services.common.vocabulary.AuthorityServiceUtils;
57 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils;
58 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthorityItemSpecifier;
59 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.SpecifierForm;
60 import org.collectionspace.services.config.service.ListResultField;
61 import org.collectionspace.services.config.service.ObjectPartType;
62 import org.collectionspace.services.lifecycle.TransitionDef;
63 import org.collectionspace.services.nuxeo.client.java.CoreSessionInterface;
64 import org.collectionspace.services.nuxeo.client.java.NuxeoDocumentModelHandler;
65 import org.collectionspace.services.nuxeo.client.java.NuxeoRepositoryClientImpl;
66 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
67 import org.collectionspace.services.relation.RelationsCommonList;
68 import org.collectionspace.services.vocabulary.VocabularyItemJAXBSchema;
69 import org.nuxeo.ecm.core.api.ClientException;
70 import org.nuxeo.ecm.core.api.DocumentModel;
71 import org.nuxeo.ecm.core.api.model.PropertyException;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
74
75 //import org.collectionspace.services.common.authority.AuthorityItemRelations;
76 /**
77  * AuthorityItemDocumentModelHandler
78  *
79  * $LastChangedRevision: $
80  * $LastChangedDate: $
81  */
82 public abstract class AuthorityItemDocumentModelHandler<AICommon>
83                 extends NuxeoDocumentModelHandler<AICommon> {
84
85         private final Logger logger = LoggerFactory.getLogger(AuthorityItemDocumentModelHandler.class);
86
87         private static final Integer PAGE_SIZE_FROM_QUERYPARAMS = null;
88         private static final Integer PAGE_NUM_FROM_QUERYPARAMS = null;
89
90         protected String authorityCommonSchemaName;
91         protected String authorityItemCommonSchemaName;
92         private String authorityItemTermGroupXPathBase;
93
94         private boolean shouldUpdateSASFields = true;
95         private boolean syncHierarchicalRelationships = false;
96         private boolean isProposed = false; // used by local authority to propose a new shared item. Allows local deployments to use new terms until they become official
97         private boolean isSAS = false; // used to indicate if the authority item originated as a SAS item
98         private boolean shouldUpdateRevNumber = true; // by default we should update the revision number -not true on synchronization with SAS
99         /**
100          * inVocabulary is the parent Authority for this context
101          */
102         protected String inAuthority = null;
103         protected boolean wildcardedAuthorityRequest = false;
104         protected String authorityRefNameBase = null;
105         // Used to determine when the displayName changes as part of the update.
106         protected String oldDisplayNameOnUpdate = null;
107         private final static String LIST_SUFFIX = "List";
108         private final static String ZERO_OR_MORE_ANY_CHAR_REGEX = ".*";
109
110         public AuthorityItemDocumentModelHandler(String authorityCommonSchemaName, String authorityItemCommonSchemaName) {
111                 this.authorityItemCommonSchemaName = authorityItemCommonSchemaName;
112         }
113
114         abstract public String getParentCommonSchemaName();
115
116         //
117         // Getter and Setter for 'shouldUpdateSASFields'
118         //
119         public boolean getShouldUpdateSASFields() {
120                 return shouldUpdateSASFields;
121         }
122
123         public void setshouldUpdateSASFields(boolean flag) {
124                 shouldUpdateSASFields = flag;
125         }
126
127         //
128         // Getter and Setter for 'proposed'
129         //
130         public boolean getIsProposed() {
131                 return this.isProposed;
132         }
133
134         public void setIsProposed(boolean flag) {
135                 this.isProposed = flag;
136         }
137
138         //
139         // Getter and Setter for 'isSAS'
140         //
141         public boolean getIsSASItem() {
142                 return this.isSAS;
143         }
144
145         public void setIsSASItem(boolean flag) {
146                 this.isSAS = flag;
147         }
148
149         //
150         // Getter and Setter for 'shouldUpdateRevNumber'
151         //
152         public boolean getShouldUpdateRevNumber() {
153                 return this.shouldUpdateRevNumber;
154         }
155
156         public void setShouldUpdateRevNumber(boolean flag) {
157                 this.shouldUpdateRevNumber = flag;
158         }
159
160         //
161         // Getter and Setter for deciding if we need to synch hierarchical relationships
162         //
163         public boolean getShouldSyncHierarchicalRelationships() {
164                 return this.syncHierarchicalRelationships;
165         }
166
167         public void setShouldSyncHierarchicalRelationships(boolean flag) {
168                 this.syncHierarchicalRelationships = flag;
169         }
170
171         @Override
172         public void prepareSync() throws Exception {
173                 this.setShouldUpdateRevNumber(AuthorityServiceUtils.DONT_UPDATE_REV);  // Never update rev nums on sync operations
174         }
175
176         @Override
177         protected String getRefnameDisplayName(DocumentWrapper<DocumentModel> docWrapper) {
178                 String result = null;
179
180                 DocumentModel docModel = docWrapper.getWrappedObject();
181                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
182                 RefName.AuthorityItem refname = (RefName.AuthorityItem)getRefName(ctx, docModel);
183                 result = refname.getDisplayName();
184
185                 return result;
186         }
187
188         /*
189          * After calling this method successfully, the document model will contain an updated refname and short ID
190          * (non-Javadoc)
191          * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#getRefName(org.collectionspace.services.common.context.ServiceContext, org.nuxeo.ecm.core.api.DocumentModel)
192          */
193         @Override
194         public RefName.RefNameInterface getRefName(ServiceContext ctx,
195                         DocumentModel docModel) {
196                 RefName.RefNameInterface refname = null;
197
198                 try {
199                         String displayName = getPrimaryDisplayName(docModel, authorityItemCommonSchemaName,
200                                         getItemTermInfoGroupXPathBase(), AuthorityItemJAXBSchema.TERM_DISPLAY_NAME);
201                         if (Tools.isEmpty(displayName)) {
202                                 throw new Exception("The displayName for this authority term was empty or not set.");
203                         }
204
205                         String shortIdentifier = (String) docModel.getProperty(authorityItemCommonSchemaName, AuthorityItemJAXBSchema.SHORT_IDENTIFIER);
206                         if (Tools.isEmpty(shortIdentifier)) {
207                                 // We didn't find a short ID in the payload request, so we need to synthesize one.
208                                 shortIdentifier = handleDisplayNameAsShortIdentifier(docModel); // updates the document model with the new short ID as a side-effect
209                         }
210
211                         String authorityRefBaseName = getAuthorityRefNameBase();
212                         if (Tools.isEmpty(authorityRefBaseName)) {
213                                 throw new Exception("Could not create the refName for this authority term, because the refName for its authority parent was empty.");
214                         }
215
216                         // Create the items refname using the parent's as a base
217                         RefName.Authority parentsRefName = RefName.Authority.parse(authorityRefBaseName);
218                         refname = RefName.buildAuthorityItem(parentsRefName, shortIdentifier, displayName);
219                         // Now update the document model with the refname value
220                         String refNameStr = refname.toString();
221                         docModel.setProperty(authorityItemCommonSchemaName, AuthorityItemJAXBSchema.REF_NAME, refNameStr); // REM - This field is deprecated now that the refName is part of the collection_space core schema
222
223                 } catch (Exception e) {
224                         logger.error(e.getMessage(), e);
225                 }
226
227                 return refname;
228         }
229
230         public void setInAuthority(String inAuthority) {
231                 this.inAuthority = inAuthority;
232         }
233
234    public String getInAuthorityCsid() {
235                 return this.inAuthority;
236         }
237
238         /** Subclasses may override this to customize the URI segment. */
239         public String getAuthorityServicePath() {
240                 return getServiceContext().getServiceName().toLowerCase();      // Laramie20110510 CSPACE-3932
241         }
242
243         @Override
244         public String getUri(DocumentModel docModel) {
245                 // Laramie20110510 CSPACE-3932
246                 String authorityServicePath = getAuthorityServicePath();
247                 if(inAuthority==null) { // Only true with the first document model received, on queries to wildcarded authorities
248                         wildcardedAuthorityRequest = true;
249                 }
250                 // If this search crosses multiple authorities, get the inAuthority value
251                 // from each record, rather than using the cached value from the first record
252                 if(wildcardedAuthorityRequest) {
253                         try {
254                                 inAuthority = (String) docModel.getProperty(authorityItemCommonSchemaName,
255                                         AuthorityItemJAXBSchema.IN_AUTHORITY);
256                         } catch (ClientException pe) {
257                                 throw new RuntimeException("Could not get parent specifier for item!");
258                         }
259                 }
260                 return "/" + authorityServicePath + '/' + inAuthority + '/' + AuthorityClient.ITEMS + '/' + getCsid(docModel);
261         }
262
263         protected String getAuthorityRefNameBase() {
264                 return this.authorityRefNameBase;
265         }
266
267         public void setAuthorityRefNameBase(String value) {
268                 this.authorityRefNameBase = value;
269         }
270
271         /*
272          * Note: the Vocabulary service's VocabularyItemDocumentModelHandler class overrides this method.
273          */
274         protected ListResultField getListResultsDisplayNameField() {
275                 ListResultField result = new ListResultField();
276                 // Per CSPACE-5132, the name of this element remains 'displayName'
277                 // for backwards compatibility, although its value is obtained
278                 // from the termDisplayName field.
279                 //
280                 // Update: this name is now being changed to 'termDisplayName', both
281                 // because this is the actual field name and because the app layer
282                 // work to convert over to this field is underway. Per Patrick, the
283                 // app layer treats lists, in at least some context(s), as sparse record
284                 // payloads, and thus fields in list results must all be present in
285                 // (i.e. represent a strict subset of the fields in) record schemas.
286                 // - ADR 2012-05-11
287                 //
288                 //
289                 // In CSPACE-5134, these list results will change substantially
290                 // to return display names for both the preferred term and for
291                 // each non-preferred term (if any).
292                 result.setElement(AuthorityItemJAXBSchema.TERM_DISPLAY_NAME);
293                 result.setXpath(NuxeoUtils.getPrimaryXPathPropertyName(
294                                 authorityItemCommonSchemaName, getItemTermInfoGroupXPathBase(), AuthorityItemJAXBSchema.TERM_DISPLAY_NAME));
295
296                 return result;
297         }
298
299         /*
300          * Note: the Vocabulary service's VocabularyItemDocumentModelHandler class overrides this method.
301          */
302         protected ListResultField getListResultsTermStatusField() {
303                 ListResultField result = new ListResultField();
304
305                 result.setElement(AuthorityItemJAXBSchema.TERM_STATUS);
306                 result.setXpath(NuxeoUtils.getPrimaryXPathPropertyName(
307                                 authorityItemCommonSchemaName, getItemTermInfoGroupXPathBase(), AuthorityItemJAXBSchema.TERM_STATUS));
308
309                 return result;
310         }
311
312         private boolean isTermDisplayName(String elName) {
313                 return AuthorityItemJAXBSchema.TERM_DISPLAY_NAME.equals(elName) || VocabularyItemJAXBSchema.DISPLAY_NAME.equals(elName);
314         }
315
316         /*
317          * (non-Javadoc)
318          * @see org.collectionspace.services.nuxeo.client.java.DocHandlerBase#getListItemsArray()
319          *
320          * Note: We're updating the "global" service and tenant bindings instance here -the list instance here is
321          * a reference to the tenant bindings instance in the singleton ServiceMain.
322          */
323         @Override
324         public List<ListResultField> getListItemsArray() throws DocumentException {
325                 List<ListResultField> list = super.getListItemsArray();
326
327                 // One-time initialization for each authority item service.
328                 if (isListItemArrayExtended() == false) {
329                         synchronized(AuthorityItemDocumentModelHandler.class) {
330                                 if (isListItemArrayExtended() == false) {
331                                         int nFields = list.size();
332                                         // Ensure that each item in a list of Authority items includes
333                                         // a set of common fields, so we do not depend upon configuration
334                                         // for general logic.
335                                          List<Integer> termDisplayNamePositionsInList = new ArrayList<>();
336                                                    boolean hasShortId = false;
337                                         boolean hasTermStatus = false;
338                                         for (int i = 0; i < nFields; i++) {
339                                                 ListResultField field = list.get(i);
340                                                 String elName = field.getElement();
341                                                 if (isTermDisplayName(elName) == true) {
342                                                         termDisplayNamePositionsInList.add(i);
343                                                 } else if (AuthorityItemJAXBSchema.SHORT_IDENTIFIER.equals(elName)) {
344                                                         hasShortId = true;
345                                                 } else if (AuthorityItemJAXBSchema.TERM_STATUS.equals(elName)) {
346                                                         hasTermStatus = true;
347                                                 }
348                                         }
349
350                                         ListResultField field;
351
352                                                 // Certain fields in authority item list results
353                                                 // are handled specially here
354
355                                                 // Term display name
356                                                 //
357                                                 // Ignore (throw out) any configuration entries that
358                                                 // specify how the termDisplayName field should be
359                                                 // emitted in authority item lists. This field will
360                                                 // be handled in a standardized manner (see block below).
361                                                 if (termDisplayNamePositionsInList.isEmpty() == false) {
362                                                         // Remove matching items starting at the end of the list
363                                                         // and moving towards the start, so that reshuffling of
364                                                         // list order doesn't alter the positions of earlier items
365                                                         Collections.sort(termDisplayNamePositionsInList, Collections.reverseOrder());
366                                                         for (int i : termDisplayNamePositionsInList) {
367                                                                 list.remove(i);
368                                                         }
369                                                 }
370                                                 // termDisplayName values in authority item lists
371                                                 // will be handled via code that emits display names
372                                                 // for both the preferred term and all non-preferred
373                                                 // terms (if any). The following is a placeholder
374                                                 // entry that will trigger this code. See the
375                                                 // getListResultValue() method in this class.
376                                         field = getListResultsDisplayNameField();
377                                         list.add(field);
378
379                                                 // Short identifier
380                                         if (!hasShortId) {
381                                                 field = new ListResultField();
382                                                 field.setElement(AuthorityItemJAXBSchema.SHORT_IDENTIFIER);
383                                                 field.setXpath(AuthorityItemJAXBSchema.SHORT_IDENTIFIER);
384                                                 list.add(field);
385                                         }
386
387                                                 // Term status
388                                         if (!hasTermStatus) {
389                                                 field = getListResultsTermStatusField();
390                                                 list.add(field);
391                                         }
392
393                                 }
394
395                                 setListItemArrayExtended(true);
396                         } // end of synchronized block
397                 }
398
399                 return list;
400         }
401
402         /**
403          * We consider workflow state changes as a change that should bump the revision number.
404          * Warning: This method might change the transitionDef's transtionName value
405          */
406         @Override
407         public void handleWorkflowTransition(ServiceContext ctx, DocumentWrapper<DocumentModel> wrapDoc, TransitionDef transitionDef) throws Exception {
408                 // Decide whether or not to update the revision number
409                 if (this.getShouldUpdateRevNumber() == true) { // We don't update the rev number of synchronization requests
410                         updateRevNumbers(wrapDoc);
411                 }
412                 //
413                 // We can't delete an authority item that has referencing records.
414                 //
415                 DocumentModel docModel = wrapDoc.getWrappedObject();
416                 if (transitionDef.getName().equalsIgnoreCase(WorkflowClient.WORKFLOWTRANSITION_DELETE)) {
417                         AuthorityRefDocList refsToAllObjects = getReferencingObjectsForStateTransitions(ctx, docModel, RefObjsSearchType.ALL);
418                         AuthorityRefDocList refsToSoftDeletedObjects = getReferencingObjectsForStateTransitions(ctx, docModel, RefObjsSearchType.DELETED_ONLY);
419                         if (refsToAllObjects.getTotalItems() > 0) {
420                                 if (refsToAllObjects.getTotalItems() > refsToSoftDeletedObjects.getTotalItems()) {
421                                         //
422                                         // If the number of refs to active objects is greater than the number of refs to
423                                         // soft deleted objects then we can't delete the item.
424                                         //
425                                         logger.error(String.format("Cannot delete authority item CSID='%s' because it still has records in the system that are referencing it.",
426                                                         docModel.getName()));
427                                         if (logger.isWarnEnabled() == true) {
428                                                 logReferencingObjects(docModel, refsToAllObjects);
429                                         }
430
431                                         throw new DocumentReferenceException(String.format("Cannot delete authority item '%s' because it still has records in the system that are referencing it.  See the service layer log file for details.",
432                                                         docModel.getName()));
433                                 }
434                         }
435                 }
436         }
437
438         @Override
439         public boolean handleSync(DocumentWrapper<Object> wrapDoc) throws Exception {
440                 return handleItemSync(wrapDoc);
441         }
442
443                 /**
444          *
445          * @param wrapDoc
446          * @return
447          * @throws Exception
448          */
449         @SuppressWarnings({ "rawtypes", "unchecked" })
450         protected boolean handleItemSync(DocumentWrapper<Object> wrapDoc) throws Exception {
451                 boolean result = false;
452                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
453
454                 //
455                 // Get information about the local authority item so we can compare with corresponding item on the shared authority server
456                 //
457                 AuthorityItemSpecifier authorityItemSpecifier = (AuthorityItemSpecifier) wrapDoc.getWrappedObject();
458                 DocumentModel itemDocModel = NuxeoUtils.getDocFromSpecifier(ctx, getRepositorySession(), getAuthorityItemCommonSchemaName(),
459                                 authorityItemSpecifier);
460                 if (itemDocModel == null) {
461                         throw new DocumentNotFoundException(String.format("Could not find authority item resource with CSID='%s'",
462                                         authorityItemSpecifier.getItemSpecifier().value));
463                 }
464                 Long localItemRev = (Long) NuxeoUtils.getProperyValue(itemDocModel, AuthorityItemJAXBSchema.REV);
465                 Boolean localIsProposed = (Boolean) NuxeoUtils.getProperyValue(itemDocModel, AuthorityItemJAXBSchema.PROPOSED);
466                 String localItemCsid = itemDocModel.getName();
467                 String localItemWorkflowState = itemDocModel.getCurrentLifeCycleState();
468                 String itemShortId = (String) NuxeoUtils.getProperyValue(itemDocModel, AuthorityItemJAXBSchema.SHORT_IDENTIFIER);
469
470                 //
471                 // Now get the item's Authority (the parent) information
472                 //
473                 DocumentModel authorityDocModel = NuxeoUtils.getDocFromSpecifier(ctx, getRepositorySession(), authorityCommonSchemaName,
474                                 authorityItemSpecifier.getParentSpecifier());
475                 String authorityShortId = (String) NuxeoUtils.getProperyValue(authorityDocModel, AuthorityJAXBSchema.SHORT_IDENTIFIER);
476                 String localParentCsid = authorityDocModel.getName();
477                 String remoteClientConfigName = (String)NuxeoUtils.getProperyValue(authorityDocModel, AuthorityJAXBSchema.REMOTECLIENT_CONFIG_NAME);
478
479                 //
480                 // Using the short IDs of the local authority and item, create URN specifiers and retrieve the SAS authority item
481                 //
482                 AuthorityItemSpecifier sasAuthorityItemSpecifier = new AuthorityItemSpecifier(SpecifierForm.URN_NAME, authorityShortId, itemShortId);
483                 // Get the shared authority server's copy
484                 PoxPayloadIn sasPayloadIn = AuthorityServiceUtils.requestPayloadInFromRemoteServer(sasAuthorityItemSpecifier,
485                                 remoteClientConfigName, getAuthorityServicePath(), getEntityResponseType(), AuthorityClient.INCLUDE_RELATIONS);
486                 Long sasRev = getRevision(sasPayloadIn);
487                 String sasWorkflowState = getWorkflowState(sasPayloadIn);
488                 //
489                 // If the shared authority item is newer, update our local copy
490                 //
491                 if (sasRev > localItemRev || localIsProposed || ctx.shouldForceSync()) {
492                         AuthorityResource authorityResource = (AuthorityResource) ctx.getResource(getAuthorityServicePath());
493
494                         // Remove remote uris and csids from relations, and remove relations to items that don't exist locally.
495                         sasPayloadIn = AuthorityServiceUtils.localizeRelations(ctx, authorityResource, localParentCsid, authorityItemSpecifier.getItemSpecifier(), sasPayloadIn);
496
497                         // Localize domain name parts of refnames in the payload.
498                         sasPayloadIn = AuthorityServiceUtils.localizeRefNameDomains(ctx, sasPayloadIn);
499
500                         PoxPayloadOut payloadOut = authorityResource.updateAuthorityItem(ctx,
501                                         ctx.getResourceMap(),
502                                         ctx.getUriInfo(),
503                                         localParentCsid,                                                 // parent's CSID
504                                         localItemCsid,                                                   // item's CSID
505                                         sasPayloadIn,                                                   // the payload from the remote SAS
506                                         AuthorityServiceUtils.DONT_UPDATE_REV,  // don't update the parent's revision number
507                                         AuthorityServiceUtils.NOT_PROPOSED,             // The items is not proposed, make it a real SAS item now
508                                         AuthorityServiceUtils.SAS_ITEM);                // Since we're sync'ing, this must be a SAS item
509                         if (payloadOut != null) {
510                                 ctx.setOutput(payloadOut);
511                                 result = true;
512                         }
513                 }
514                 //
515                 // Check to see if we need to update the local items's workflow state to reflect that of the remote's
516                 //
517                 List<String> transitionList = AuthorityServiceUtils.getTransitionList(sasWorkflowState, localItemWorkflowState);
518
519                 if (transitionList.isEmpty() == false) {
520                         AuthorityResource authorityResource = (AuthorityResource) ctx.getResource(getAuthorityServicePath()); // Get the authority (parent) client not the item client
521                         //
522                         // We need to move the local item to the SAS workflow state.  This might involve multiple transitions.
523                         //
524                         try {
525                                 for (String transition:transitionList) {
526                                         authorityResource.updateItemWorkflowWithTransition(ctx, localParentCsid, localItemCsid, transition, AuthorityServiceUtils.DONT_UPDATE_REV, AuthorityServiceUtils.DONT_ROLLBACK_ON_EXCEPTION);
527                                 }
528                         } catch (DocumentReferenceException de) {
529                                 //
530                                 // This exception means we tried unsuccessfully to soft-delete (workflow transition 'delete') an item that still has references to it from other records.
531                                 //
532                                 logger.info(String.format("Failed to soft-delete %s (transition from %s to %s): item is referenced, and will be deprecated instead", localItemCsid, localItemWorkflowState, sasWorkflowState));
533
534                                 // One or more of the transitions may have succeeded, so refresh the document model to make sure it
535                                 // reflects the current workflow state.
536                                 itemDocModel.refresh();
537
538                                 // Since we can't soft-delete it, we need to mark it as deprecated since it is soft-deleted on the SAS.
539                                 AuthorityServiceUtils.setAuthorityItemDeprecated(ctx, authorityResource, localParentCsid, localItemCsid, itemDocModel);
540                         }
541
542                         result = true;
543                 }
544
545                 return result;
546         }
547
548         /* (non-Javadoc)
549          * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#handleCreate(org.collectionspace.services.common.document.DocumentWrapper)
550          */
551         @Override
552         public void handleCreate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
553                 // first fill all the parts of the document, refname and short ID get set as well
554                 super.handleCreate(wrapDoc);
555                 // Ensure we have required fields set properly
556                 handleInAuthority(wrapDoc.getWrappedObject());
557         }
558
559         enum RefObjsSearchType {
560                 ALL, NON_DELETED, DELETED_ONLY
561         }
562
563         /*
564          * This method gets called after the primary update to an authority item has happened.  If the authority item's refName
565          * has changed, then we need to updated all the records that use that refname with the new/updated version
566          *
567          * (non-Javadoc)
568          */
569         @SuppressWarnings({ "rawtypes", "unchecked" })
570         @Override
571         public boolean handleDelete(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
572                 boolean result = true;
573
574                 ServiceContext ctx = getServiceContext();
575                 DocumentModel docModel = wrapDoc.getWrappedObject();
576
577                 AuthorityRefDocList refsToAllObjects = getReferencingObjectsForStateTransitions(ctx, docModel, RefObjsSearchType.ALL);
578                 AuthorityRefDocList refsToSoftDeletedObjects = getReferencingObjectsForStateTransitions(ctx, docModel, RefObjsSearchType.DELETED_ONLY);
579                 if (refsToAllObjects.getTotalItems() > 0) {
580                         if (refsToAllObjects.getTotalItems() > refsToSoftDeletedObjects.getTotalItems()) {
581                                 //
582                                 // If the number of refs to active objects is greater than the number of refs to
583                                 // soft deleted objects then we can't delete the item.
584                                 //
585                                 logger.error(String.format("Cannot delete authority item CSID='%s' because it still has %d records in the system that are referencing it.",
586                                                 docModel.getName(), refsToSoftDeletedObjects.getTotalItems()));
587                                 if (logger.isWarnEnabled() == true) {
588                                         logReferencingObjects(docModel, refsToAllObjects);
589                                 }
590
591                                 throw new DocumentReferenceException(String.format("Cannot delete authority item '%s' because it still has records in the system that are referencing it.  See the service layer log file for details.",
592                                                 docModel.getName()));
593                         } else {
594                                 //
595                                 // If all the refs are to soft-deleted objects, we should soft-delete this authority item instead of hard-deleting it and instead of failing.
596                                 //
597                                 String parentCsid = (String) NuxeoUtils.getProperyValue(docModel, AuthorityItemJAXBSchema.IN_AUTHORITY);
598                                 String itemCsid = docModel.getName();
599                                 AuthorityResource authorityResource = (AuthorityResource) ctx.getResource(getAuthorityServicePath());
600                                 authorityResource.updateItemWorkflowWithTransition(ctx, parentCsid, itemCsid, WorkflowClient.WORKFLOWTRANSITION_DELETE,
601                                                 this.getShouldUpdateRevNumber());
602                                 result = false; // Don't delete since we just soft-deleted it.
603                         }
604                 }
605
606                 //
607                 // Since we've changed the state of the parent by deleting (or soft-deleting) one of its items, we might need to update the parent rev number
608                 //
609                 if (getShouldUpdateRevNumber() == true) {
610                         updateRevNumbers(wrapDoc);
611                 }
612
613                 return result;
614         }
615
616         /**
617          * Checks to see if an authority item has referencing objects.
618          *
619          * @param ctx
620          * @param docModel
621          * @return
622          * @throws Exception
623          */
624         @SuppressWarnings("rawtypes")
625         private AuthorityRefDocList getReferencingObjectsForStateTransitions(
626                         ServiceContext ctx,
627                         DocumentModel docModel,
628                         RefObjsSearchType searchType) throws Exception {
629                 AuthorityRefDocList referenceList = null;
630
631                 if (ctx.getUriInfo() == null) {
632                         //
633                         // We need a UriInfo object so we can pass "query" params to the AuthorityResource's getReferencingObjects() method
634                         //
635                         ctx.setUriInfo(this.getServiceContext().getUriInfo()); // try to get a UriInfo instance from the handler's context
636                 }
637
638                 //
639                 // Since the call to get referencing objects might indirectly use the WorkflowClient.WORKFLOW_QUERY_NONDELETED query param, we need to
640                 // temporarily remove that query param if it is set.  If set, we'll save the value and reset once we're finished.
641                 //
642                 boolean doesContainValue = ctx.getUriInfo().getQueryParameters().containsKey(WorkflowClient.WORKFLOW_QUERY_DELETED_QP);
643                 String previousValue = ctx.getUriInfo().getQueryParameters().getFirst(WorkflowClient.WORKFLOW_QUERY_DELETED_QP);
644
645                 try {
646                         if (doesContainValue) {
647                                 ctx.getUriInfo().getQueryParameters().remove(WorkflowClient.WORKFLOW_QUERY_DELETED_QP);
648                         }
649                         AuthorityResource authorityResource = (AuthorityResource)ctx.getResource(getAuthorityServicePath());
650                         referenceList = getReferencingObjects(authorityResource, ctx, docModel, searchType, PAGE_NUM_FROM_QUERYPARAMS, PAGE_SIZE_FROM_QUERYPARAMS, true, true); // useDefaultOrderByClause=true, computeTotal=true
651                 } finally {
652                         if (doesContainValue) {
653                                 ctx.getUriInfo().getQueryParameters().addFirst(WorkflowClient.WORKFLOW_QUERY_DELETED_QP, previousValue);
654                         }
655                 }
656
657                 return referenceList;
658         }
659
660         @SuppressWarnings("rawtypes")
661         private AuthorityRefDocList getReferencingObjectsForMarkingTerm(
662                         ServiceContext ctx,
663                         DocumentModel docModel,
664                         RefObjsSearchType searchType) throws Exception {
665                 AuthorityRefDocList referenceList = null;
666
667                 if (ctx.getUriInfo() == null) {
668                         //
669                         // We need a UriInfo object so we can pass "query" params to the AuthorityResource's getReferencingObjects() method
670                         //
671                         ctx.setUriInfo(this.getServiceContext().getUriInfo()); // try to get a UriInfo instance from the handler's context
672                 }
673
674                 //
675                 // Since the call to get referencing objects might indirectly use the WorkflowClient.WORKFLOW_QUERY_NONDELETED query param, we need to
676                 // temporarily remove that query param if it is set.  If set, we'll save the value and reset once we're finished.
677                 //
678                 boolean doesContainValue = ctx.getUriInfo().getQueryParameters().containsKey(WorkflowClient.WORKFLOW_QUERY_DELETED_QP);
679                 String previousValue = ctx.getUriInfo().getQueryParameters().getFirst(WorkflowClient.WORKFLOW_QUERY_DELETED_QP);
680
681                 try {
682                         if (doesContainValue) {
683                                 ctx.getUriInfo().getQueryParameters().remove(WorkflowClient.WORKFLOW_QUERY_DELETED_QP);
684                         }
685                         AuthorityResource authorityResource = (AuthorityResource)ctx.getResource(getAuthorityServicePath());
686                         referenceList = getReferencingObjects(authorityResource, ctx, docModel, searchType, 0, 1, false, false);  // pageNum=0, pageSize=1, useDefaultOrderClause=false, computeTotal=false
687                 } finally {
688                         if (doesContainValue) {
689                                 ctx.getUriInfo().getQueryParameters().addFirst(WorkflowClient.WORKFLOW_QUERY_DELETED_QP, previousValue);
690                         }
691                 }
692
693                 return referenceList;
694         }
695
696         @SuppressWarnings({ "rawtypes", "unchecked" })
697         private AuthorityRefDocList getReferencingObjects(
698                         AuthorityResource authorityResource,
699                         ServiceContext ctx,
700                         DocumentModel docModel,
701                         RefObjsSearchType searchType,
702                         Integer pageNum,
703                         Integer pageSize,
704                         boolean useDefaultOrderByClause,
705                         boolean computeTotal) throws Exception {
706                 AuthorityRefDocList result = null;
707
708                 String inAuthorityCsid = (String) docModel.getProperty(authorityItemCommonSchemaName, AuthorityItemJAXBSchema.IN_AUTHORITY);
709                 String itemCsid = docModel.getName();
710
711                 try {
712                         switch (searchType) {
713                                 case ALL:
714                                         // By default, get get everything
715                                         break;
716                                 case NON_DELETED:
717                                         // Get only non-deleted objects
718                                         ctx.getUriInfo().getQueryParameters().addFirst(WorkflowClient.WORKFLOW_QUERY_DELETED_QP, Boolean.FALSE.toString());  // Add the wf_deleted=false query param to exclude soft-deleted items
719                                         break;
720                                 case DELETED_ONLY:
721                                         // Get only deleted objects
722                                         ctx.getUriInfo().getQueryParameters().addFirst(WorkflowClient.WORKFLOW_QUERY_ONLY_DELETED_QP, Boolean.TRUE.toString());  // Add the wf_only_deleted query param to get only soft-deleted items
723                                         break;
724                         }
725                         result = authorityResource.getReferencingObjects(ctx, inAuthorityCsid, itemCsid, ctx.getUriInfo(), pageNum, pageSize, useDefaultOrderByClause, computeTotal);
726
727                 } finally {
728                         //
729                         // Cleanup query params
730                         //
731                         switch (searchType) {
732                                 case ALL:
733                                         break;
734                                 case NON_DELETED:
735                                         ctx.getUriInfo().getQueryParameters().remove(WorkflowClient.WORKFLOWSTATE_DELETED);
736                                         break;
737                                 case DELETED_ONLY:
738                                         ctx.getUriInfo().getQueryParameters().remove(WorkflowClient.WORKFLOW_QUERY_ONLY_DELETED_QP);
739                                         break;
740                         }
741                 }
742
743                 return result;
744         }
745
746         private void logReferencingObjects(DocumentModel docModel, AuthorityRefDocList refObjs) {
747                 List<AuthorityRefDocList.AuthorityRefDocItem> items = refObjs.getAuthorityRefDocItem();
748                 logger.warn(String.format("The authority item CSID='%s' has the following references:", docModel.getName()));
749                 int i = 0;
750                 for (AuthorityRefDocList.AuthorityRefDocItem item : items) {
751                         if (item.getWorkflowState().contains(WorkflowClient.WORKFLOWSTATE_DELETED) == false) {
752                                 logger.warn(docModel.getName() + " referenced by : list-item[" + i + "] "
753                                                 + item.getDocType() + "("
754                                                 + item.getDocId() + ") Name:["
755                                                 + item.getDocName() + "] Number:["
756                                                 + item.getDocNumber() + "] in field:["
757                                                 + item.getSourceField() + "]");
758                                 i++;
759                         }
760                 }
761         }
762
763         /*
764          * This method gets called after the primary update to an authority item has happened.  If the authority item's refName
765          * has changed, then we need to updated all the records that use that refname with the new/updated version
766          *
767          * (non-Javadoc)
768          * @see org.collectionspace.services.nuxeo.client.java.RemoteDocumentModelHandlerImpl#completeUpdate(org.collectionspace.services.common.document.DocumentWrapper)
769          */
770         public void completeUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
771                 // Must call our super class' version first
772                 super.completeUpdate(wrapDoc);
773
774                 //
775                 // Look for and update authority references with the updated refName
776                 //
777                 if (hasRefNameUpdate() == true) {
778                         // We have work to do.
779                         if (logger.isDebugEnabled()) {
780                                 final String EOL = System.getProperty("line.separator");
781                                 logger.debug("Need to find and update references to authority item." + EOL
782                                                 + "   Old refName" + oldRefNameOnUpdate + EOL
783                                                 + "   New refName" + newRefNameOnUpdate);
784                         }
785                         ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
786                         RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient = getRepositoryClient(ctx);
787                         CoreSessionInterface repoSession = this.getRepositorySession();
788
789                         // Update all the existing records that have a field with the old refName in it
790                         int nUpdated = RefNameServiceUtils.updateAuthorityRefDocs(ctx, repoClient, repoSession,
791                                         oldRefNameOnUpdate, newRefNameOnUpdate, getRefPropName());
792
793                         // Finished so log a message.
794                         if (logger.isDebugEnabled()) {
795                                 logger.debug("Updated " + nUpdated + " instances of oldRefName to newRefName");
796                         }
797                 }
798         }
799
800         /*
801          * Note that the Vocabulary service's document-model for items overrides this method.
802          */
803         protected String getPrimaryDisplayName(DocumentModel docModel, String schema,
804                         String complexPropertyName, String fieldName) {
805                 String result = null;
806
807                 result = getStringValueInPrimaryRepeatingComplexProperty(docModel, schema, complexPropertyName, fieldName);
808
809                 return result;
810         }
811
812         /* (non-Javadoc)
813          * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#handleUpdate(org.collectionspace.services.common.document.DocumentWrapper)
814          */
815         @Override
816         // FIXME: Once we remove the refName field from the authority item schemas, we can remove this override method since our super does everthing for us now.
817         @Deprecated
818         public void handleUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
819                 // Must call our super's version first, this updates the core schema and the relationship records to deal with possible refName changes/update
820                 super.handleUpdate(wrapDoc);
821                 if (this.hasRefNameUpdate() == true) {
822                         DocumentModel docModel = wrapDoc.getWrappedObject();
823                         docModel.setProperty(authorityItemCommonSchemaName, AuthorityItemJAXBSchema.REF_NAME, this.newRefNameOnUpdate); // This field is deprecated since it is now a duplicate of what is in the collectionspace_core:refName field
824                 }
825         }
826
827         //
828         // Handles both update calls (PUTS) AND create calls (POSTS)
829         //
830         public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
831                 super.fillAllParts(wrapDoc, action);
832                 DocumentModel documentModel = wrapDoc.getWrappedObject();
833
834                 //
835                 // Update the record's revision number on both CREATE and UPDATE actions (as long as it is NOT a SAS authority item)
836                 //
837                 Boolean propertyValue = (Boolean) documentModel.getProperty(authorityItemCommonSchemaName, AuthorityItemJAXBSchema.SAS);
838                 boolean isMarkedAsSASItem = propertyValue != null ? propertyValue : false;
839                 if (this.getShouldUpdateRevNumber() == true && !isMarkedAsSASItem) { // We won't update rev numbers on synchronization with SAS items and on local changes to SAS items
840                         updateRevNumbers(wrapDoc);
841                 }
842
843                 if (getShouldUpdateSASFields() == true) {
844                         //
845                         // If this is a proposed item (not part of the SAS), mark it as such
846                         //
847                         documentModel.setProperty(authorityItemCommonSchemaName, AuthorityItemJAXBSchema.PROPOSED,
848                                         new Boolean(this.getIsProposed()));
849                         //
850                         // If it is a SAS authority item, mark it as such
851                         //
852                         documentModel.setProperty(authorityItemCommonSchemaName, AuthorityItemJAXBSchema.SAS,
853                                         new Boolean(this.getIsSASItem()));
854                 }
855         }
856
857         /**
858          * Update the revision number of both the item and the item's parent.
859          * @param wrapDoc
860          * @throws Exception
861          */
862         protected void updateRevNumbers(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
863                 DocumentModel documentModel = wrapDoc.getWrappedObject();
864                 Long rev = (Long)documentModel.getProperty(authorityItemCommonSchemaName, AuthorityItemJAXBSchema.REV);
865                 if (rev == null) {
866                         rev = (long)0;
867                 } else {
868                         rev++;
869                 }
870                 documentModel.setProperty(authorityItemCommonSchemaName, AuthorityItemJAXBSchema.REV, rev);
871                 //
872                 // Next, update the inAuthority (the parent's) rev number
873                 //
874                 String inAuthorityCsid = this.getInAuthorityCsid();
875                 if (inAuthorityCsid == null) {
876                         // When inAuthorityCsid is null, it usually means we're performing and update or synch with the SAS
877                         inAuthorityCsid = (String)documentModel.getProperty(authorityItemCommonSchemaName, AuthorityItemJAXBSchema.IN_AUTHORITY);
878                 }
879                 DocumentModel inAuthorityDocModel = NuxeoUtils.getDocFromCsid(getServiceContext(), getRepositorySession(), inAuthorityCsid);
880                 if (inAuthorityDocModel != null) {
881                         Long parentRev = (Long)inAuthorityDocModel.getProperty(getParentCommonSchemaName(), AuthorityJAXBSchema.REV);
882                         if (parentRev == null) {
883                                 parentRev = new Long(0);
884                         }
885                            parentRev++;
886                            inAuthorityDocModel.setProperty(getParentCommonSchemaName(), AuthorityJAXBSchema.REV, parentRev);
887                            getRepositorySession().saveDocument(inAuthorityDocModel);
888                 } else {
889                         logger.warn(String.format("Containing authority '%s' for item '%s' has been deleted.  Item is orphaned, so revision numbers can't be updated.",
890                                         inAuthorityCsid, documentModel.getName()));
891                 }
892         }
893
894         /**
895          * If no short identifier was provided in the input payload, generate a
896          * short identifier from the preferred term display name or term name.
897          */
898         private String handleDisplayNameAsShortIdentifier(DocumentModel docModel) throws Exception {
899                 String result = (String) docModel.getProperty(authorityItemCommonSchemaName,
900                                 AuthorityItemJAXBSchema.SHORT_IDENTIFIER);
901
902                 if (Tools.isEmpty(result)) {
903                         String termDisplayName = getPrimaryDisplayName(
904                                         docModel, authorityItemCommonSchemaName,
905                                         getItemTermInfoGroupXPathBase(),
906                                         AuthorityItemJAXBSchema.TERM_DISPLAY_NAME);
907
908                         String termName = getPrimaryDisplayName(
909                                         docModel, authorityItemCommonSchemaName,
910                                         getItemTermInfoGroupXPathBase(),
911                                         AuthorityItemJAXBSchema.TERM_NAME);
912
913                         String generatedShortIdentifier = AuthorityIdentifierUtils.generateShortIdentifierFromDisplayName(termDisplayName,
914                                                         termName);
915                         docModel.setProperty(authorityItemCommonSchemaName, AuthorityItemJAXBSchema.SHORT_IDENTIFIER,
916                                         generatedShortIdentifier);
917                         result = generatedShortIdentifier;
918                 }
919
920                 return result;
921         }
922
923         /**
924          * Generate a refName for the authority item from the short identifier
925          * and display name.
926          *
927          * All refNames for authority items are generated.  If a client supplies
928          * a refName, it will be overwritten during create (per this method)
929          * or discarded during update (per filterReadOnlyPropertiesForPart).
930          *
931          * @see #filterReadOnlyPropertiesForPart(Map<String, Object>, org.collectionspace.services.common.service.ObjectPartType)
932          *
933          */
934         protected String updateRefnameForAuthorityItem(DocumentModel docModel,
935                         String schemaName) throws Exception {
936                 String result = null;
937
938                 RefName.RefNameInterface refname = getRefName(getServiceContext(), docModel);
939                 String refNameStr = refname.toString();
940                 docModel.setProperty(schemaName, AuthorityItemJAXBSchema.REF_NAME, refNameStr);
941                 result = refNameStr;
942
943                 return result;
944         }
945
946         /**
947          * Check the logic around the parent pointer. Note that we only need do this on
948          * create, since we have logic to make this read-only on update.
949          *
950          * @param docModel
951          *
952          * @throws Exception the exception
953          */
954         private void handleInAuthority(DocumentModel docModel) throws Exception {
955                 if (inAuthority == null) { // Only happens on queries to wildcarded authorities
956                         throw new IllegalStateException("Trying to Create an object with no inAuthority value!");
957                 }
958                 docModel.setProperty(authorityItemCommonSchemaName, AuthorityItemJAXBSchema.IN_AUTHORITY, inAuthority);
959         }
960
961         /**
962          * Returns a list of records that reference this authority item
963          *
964          * @param ctx
965          * @param uriTemplateRegistry
966          * @param serviceTypes
967          * @param propertyName
968          * @param itemcsid
969          * @return
970          * @throws Exception
971          */
972         public AuthorityRefDocList getReferencingObjects(
973                         ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
974                         List<String> serviceTypes,
975                         String propertyName,
976                         String itemcsid,
977                         Integer pageNum,
978                         Integer pageSize,
979                         boolean useDefaultOrderByClause,
980                         boolean computeTotal) throws Exception {
981                 AuthorityRefDocList authRefDocList = null;
982                 CoreSessionInterface repoSession = (CoreSessionInterface) ctx.getCurrentRepositorySession();
983                 boolean releaseRepoSession = false;
984
985                 try {
986                         NuxeoRepositoryClientImpl repoClient = (NuxeoRepositoryClientImpl)this.getRepositoryClient(ctx);
987                         repoSession = this.getRepositorySession();
988                         if (repoSession == null) {
989                                 repoSession = repoClient.getRepositorySession(ctx);
990                                 releaseRepoSession = true;
991                         }
992                         DocumentFilter myFilter = getDocumentFilter();
993                         if (pageSize != null) {
994                                 myFilter.setPageSize(pageSize);
995                         }
996                         if (pageNum != null) {
997                                 myFilter.setStartPage(pageNum);
998                         }
999                         myFilter.setUseDefaultOrderByClause(useDefaultOrderByClause);
1000
1001                         try {
1002                                 DocumentWrapper<DocumentModel> wrapper = repoClient.getDoc(repoSession, ctx, itemcsid);
1003                                 DocumentModel docModel = wrapper.getWrappedObject();
1004                                 String refName = (String) NuxeoUtils.getProperyValue(docModel, AuthorityItemJAXBSchema.REF_NAME); //docModel.getPropertyValue(AuthorityItemJAXBSchema.REF_NAME);
1005                                 authRefDocList = RefNameServiceUtils.getAuthorityRefDocs(
1006                                                 repoSession,
1007                                                 ctx,
1008                                                 repoClient,
1009                                                 serviceTypes,
1010                                                 refName,
1011                                                 propertyName,
1012                                                 myFilter,
1013                                                 useDefaultOrderByClause,
1014                                                 computeTotal /*computeTotal*/);
1015                         } catch (PropertyException pe) {
1016                                 throw pe;
1017                         } catch (DocumentException de) {
1018                                 throw de;
1019                         } catch (Exception e) {
1020                                 if (logger.isDebugEnabled()) {
1021                                         logger.debug("Caught exception ", e);
1022                                 }
1023                                 throw new DocumentException(e);
1024                         } finally {
1025                                 // If we got/aquired a new seesion then we're responsible for releasing it.
1026                                 if (releaseRepoSession && repoSession != null) {
1027                                         repoClient.releaseRepositorySession(ctx, repoSession);
1028                                 }
1029                         }
1030                 } catch (Exception e) {
1031                         if (logger.isDebugEnabled()) {
1032                                 logger.debug("Caught exception ", e);
1033                         }
1034                         throw new DocumentException(e);
1035                 }
1036
1037                 return authRefDocList;
1038         }
1039
1040         /* (non-Javadoc)
1041          * @see org.collectionspace.services.nuxeo.client.java.RemoteDocumentModelHandlerImpl#extractPart(org.nuxeo.ecm.core.api.DocumentModel, java.lang.String, org.collectionspace.services.common.service.ObjectPartType)
1042          */
1043         @Override
1044         protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
1045                         throws Exception {
1046                 Map<String, Object> unQObjectProperties = super.extractPart(docModel, schema, partMeta);
1047
1048                 // Add the CSID to the common part, since they may have fetched via the shortId.
1049                 if (partMeta.getLabel().equalsIgnoreCase(authorityItemCommonSchemaName)) {
1050                         String csid = getCsid(docModel);//NuxeoUtils.extractId(docModel.getPathAsString());
1051                         unQObjectProperties.put("csid", csid);
1052                 }
1053
1054                 return unQObjectProperties;
1055         }
1056
1057         /**
1058          * Filters out selected values supplied in an update request.
1059          *
1060          * For example, filters out AuthorityItemJAXBSchema.IN_AUTHORITY, to ensure
1061          * that the link to the item's parent remains untouched.
1062          *
1063          * @param objectProps the properties filtered out from the update payload
1064          * @param partMeta metadata for the object to fill
1065          */
1066         @Override
1067         public void filterReadOnlyPropertiesForPart(
1068                         Map<String, Object> objectProps, ObjectPartType partMeta) {
1069                 super.filterReadOnlyPropertiesForPart(objectProps, partMeta);
1070                 String commonPartLabel = getServiceContext().getCommonPartLabel();
1071                 if (partMeta.getLabel().equalsIgnoreCase(commonPartLabel)) {
1072                         objectProps.remove(AuthorityItemJAXBSchema.IN_AUTHORITY);
1073                         objectProps.remove(AuthorityItemJAXBSchema.CSID);
1074                         if (getServiceContext().shouldForceUpdateRefnameReferences() == false) {
1075                                 objectProps.remove(AuthorityJAXBSchema.SHORT_IDENTIFIER);
1076                         }
1077                         objectProps.remove(AuthorityItemJAXBSchema.REF_NAME);
1078                 }
1079         }
1080
1081         /**
1082          * Returns the items in a list of term display names whose names contain
1083          * a partial term (as might be submitted in a search query, for instance).
1084          * @param termDisplayNameList a list of term display names.
1085          * @param partialTerm a partial term display name; that is, a portion
1086          * of a display name that might be expected to match 0-n terms in the list.
1087          * @return a list of term display names that matches the partial term.
1088          * Matches are case-insensitive. As well, before matching is performed, any
1089          * special-purpose characters that may appear in the partial term (such as
1090          * wildcards and anchor characters) are filtered out from both compared terms.
1091          */
1092         protected List<String> getPartialTermDisplayNameMatches(List<String> termDisplayNameList, String partialTerm) {
1093                 List<String> result = new ArrayList<>();
1094                 String partialTermMatchExpression = filterAnchorAndWildcardChars(partialTerm).toLowerCase();
1095                 try {
1096                         for (String termDisplayName : termDisplayNameList) {
1097                                 if (termDisplayName.toLowerCase()
1098                                                 .matches(partialTermMatchExpression) == true) {
1099                                                 result.add(termDisplayName);
1100                                 }
1101                         }
1102                 } catch (PatternSyntaxException pse) {
1103                         logger.warn("Error in regex match pattern '%s' for term display names: %s",
1104                                         partialTermMatchExpression, pse.getMessage());
1105                 }
1106                 return result;
1107         }
1108
1109         /**
1110          * Filters user-supplied anchor and wildcard characters in a string,
1111          * replacing them with equivalent regular expressions.
1112          * @param term a term in which to filter anchor and wildcard characters.
1113          * @return the term with those characters filtered.
1114          */
1115         protected String filterAnchorAndWildcardChars(String term) {
1116                 if (Tools.isBlank(term)) {
1117                         return term;
1118                 }
1119                 if (term.length() < 3) {
1120                         return term;
1121                 }
1122                 if (logger.isTraceEnabled()) {
1123                         logger.trace(String.format("Term = %s", term));
1124                 }
1125                 Boolean anchorAtStart = false;
1126                 Boolean anchorAtEnd = false;
1127                 String filteredTerm;
1128                 StringBuilder filteredTermBuilder = new StringBuilder(term);
1129                 // Term contains no anchor or wildcard characters.
1130                 if ( (! term.contains(NuxeoRepositoryClientImpl.USER_SUPPLIED_ANCHOR_CHAR))
1131                                 && (! term.contains(NuxeoRepositoryClientImpl.USER_SUPPLIED_WILDCARD)) ) {
1132                         filteredTerm = term;
1133                 } else {
1134                         // Term contains at least one such character.
1135                         try {
1136                                 // Filter the starting anchor or wildcard character, if any.
1137                                 String firstChar = filteredTermBuilder.substring(0,1);
1138                                 switch (firstChar) {
1139                                         case NuxeoRepositoryClientImpl.USER_SUPPLIED_ANCHOR_CHAR:
1140                                                 anchorAtStart = true;
1141                                                 break;
1142                                         case NuxeoRepositoryClientImpl.USER_SUPPLIED_WILDCARD:
1143                                                 filteredTermBuilder.deleteCharAt(0);
1144                                                 break;
1145                                 }
1146                                 if (logger.isTraceEnabled()) {
1147                                         logger.trace(String.format("After first char filtering = %s", filteredTermBuilder.toString()));
1148                                 }
1149                                 // Filter the ending anchor or wildcard character, if any.
1150                                 int lastPos = filteredTermBuilder.length() - 1;
1151                                 String lastChar = filteredTermBuilder.substring(lastPos);
1152                                 switch (lastChar) {
1153                                         case NuxeoRepositoryClientImpl.USER_SUPPLIED_ANCHOR_CHAR:
1154                                                 filteredTermBuilder.deleteCharAt(lastPos);
1155                                                 filteredTermBuilder.insert(filteredTermBuilder.length(), NuxeoRepositoryClientImpl.ENDING_ANCHOR_CHAR);
1156                                                 anchorAtEnd = true;
1157                                                 break;
1158                                         case NuxeoRepositoryClientImpl.USER_SUPPLIED_WILDCARD:
1159                                                 filteredTermBuilder.deleteCharAt(lastPos);
1160                                                 break;
1161                                 }
1162                                 if (logger.isTraceEnabled()) {
1163                                         logger.trace(String.format("After last char filtering = %s", filteredTermBuilder.toString()));
1164                                 }
1165                                 filteredTerm = filteredTermBuilder.toString();
1166                                 // Filter all other wildcards, if any.
1167                                 filteredTerm = filteredTerm.replaceAll(NuxeoRepositoryClientImpl.USER_SUPPLIED_WILDCARD_REGEX, ZERO_OR_MORE_ANY_CHAR_REGEX);
1168                                 if (logger.isTraceEnabled()) {
1169                                         logger.trace(String.format("After replacing user wildcards = %s", filteredTerm));
1170                                 }
1171                         } catch (Exception e) {
1172                                 logger.warn(String.format("Error filtering anchor and wildcard characters from string: %s", e.getMessage()));
1173                                 return term;
1174                         }
1175                 }
1176                 // Wrap the term in beginning and ending regex wildcards, unless a
1177                 // starting or ending anchor character was present.
1178                 return (anchorAtStart ? "" : ZERO_OR_MORE_ANY_CHAR_REGEX)
1179                                 + filteredTerm
1180                                 + (anchorAtEnd ? "" : ZERO_OR_MORE_ANY_CHAR_REGEX);
1181         }
1182
1183         @SuppressWarnings("unchecked")
1184         private List<String> getPartialTermDisplayNameMatches(DocumentModel docModel, // REM - CSPACE-5133
1185                         String schema, ListResultField field, String partialTerm) {
1186                 List<String> result = null;
1187
1188                 String xpath = field.getXpath(); // results in something like "persons_common:personTermGroupList/[0]/termDisplayName"
1189                 int endOfTermGroup = xpath.lastIndexOf("/[0]/");
1190                 String propertyName = endOfTermGroup != -1 ? xpath.substring(0, endOfTermGroup) : xpath; // it may not be multivalued so the xpath passed in would be the property name
1191                 Object value = null;
1192
1193                 try {
1194                         value = docModel.getProperty(schema, propertyName);
1195                 } catch (Exception e) {
1196                         logger.error("Could not extract term display name with property = "
1197                                         + propertyName, e);
1198                 }
1199
1200                 if (value != null && value instanceof ArrayList) {
1201                         ArrayList<HashMap<String, Object>> termGroupList = (ArrayList<HashMap<String, Object>>)value;
1202                         int arrayListSize = termGroupList.size();
1203                         if (arrayListSize > 1) { // if there's only 1 element in the list then we've already matched the primary term's display name
1204                                 List<String> displayNameList = new ArrayList<String>();
1205                                 for (int i = 1; i < arrayListSize; i++) { // start at 1, skip the primary term's displayName since we will always return it
1206                                         HashMap<String, Object> map = (HashMap<String, Object>)termGroupList.get(i);
1207                                         String termDisplayName = (String) map.get(AuthorityItemJAXBSchema.TERM_DISPLAY_NAME);
1208                                         displayNameList.add(i - 1, termDisplayName);
1209                                 }
1210
1211                                 result = getPartialTermDisplayNameMatches(displayNameList, partialTerm);
1212                         }
1213                 }
1214
1215                 return result;
1216         }
1217
1218         private boolean isTermReferenced(DocumentModel docModel) throws Exception {
1219                 boolean result = false;
1220
1221                 AuthorityRefDocList referenceList = null;
1222
1223                 String wf_deletedStr = (String) getServiceContext().getQueryParams().getFirst(WorkflowClient.WORKFLOW_QUERY_DELETED_QP);
1224                 if (wf_deletedStr != null && Tools.isFalse(wf_deletedStr)) {
1225                         //
1226                         // if query param 'wf_deleted=false', we won't count references to soft-deleted records
1227                         //
1228                         referenceList = getReferencingObjectsForMarkingTerm(getServiceContext(), docModel, RefObjsSearchType.NON_DELETED);
1229                 } else {
1230                         //
1231                         // if query param 'wf_deleted=true' or missing, we count references to soft-deleted and active records
1232                         //
1233                         referenceList = getReferencingObjectsForMarkingTerm(getServiceContext(), docModel, RefObjsSearchType.ALL);
1234                 }
1235
1236                 if (referenceList.getTotalItems() > 0) {
1237                         result = true;
1238                 }
1239
1240                 return result;
1241         }
1242
1243         @SuppressWarnings("unchecked")
1244         @Override
1245         protected Object getListResultValue(DocumentModel docModel, // REM - CSPACE-5133
1246                         String schema, ListResultField field) throws DocumentException {
1247                 Object result = null;
1248                 String fieldXPath = field.getXpath();
1249
1250                 if (fieldXPath.equalsIgnoreCase(AuthorityClient.REFERENCED) == false) {
1251                         result = NuxeoUtils.getXPathValue(docModel, schema, field.getXpath());
1252                 } else {
1253                         //
1254                         // Check to see if the request is asking us to mark terms as referenced or not.
1255                         //
1256                         String markIfReferencedStr = (String) getServiceContext().getQueryParams().getFirst(AuthorityClient.MARK_IF_REFERENCED_QP);
1257                         if (Tools.isTrue(markIfReferencedStr) == false) {
1258                                 return null; // leave the <referenced> element as null since they're not asking for it
1259                         } else try {
1260                                 return Boolean.toString(isTermReferenced(docModel)); // set the <referenced> element to either 'true' or 'false'
1261                         } catch (Exception e) {
1262                                 String msg = String.format("Failed while trying to find records referencing term CSID='%s'.", docModel.getName());
1263                                 throw new DocumentException(msg, e);
1264                         }
1265                 }
1266
1267                 //
1268                 // Special handling of list item values for authority items (only)
1269                 // takes place here:
1270                 //
1271                 // If the list result field is the termDisplayName element,
1272                 // check whether a partial term matching query was made.
1273                 // If it was, emit values for both the preferred (aka primary)
1274                 // term and for all non-preferred terms, if any.
1275                 //
1276                 String elName = field.getElement();
1277                 if (isTermDisplayName(elName) == true) {
1278                         MultivaluedMap<String, String> queryParams = this.getServiceContext().getQueryParams();
1279                         String partialTerm = queryParams != null ? queryParams.getFirst(IQueryManager.SEARCH_TYPE_PARTIALTERM) : null;
1280                         if (partialTerm != null && partialTerm.trim().isEmpty() == false) {
1281                                 String primaryTermDisplayName = (String)result;
1282                                 List<String> matches = getPartialTermDisplayNameMatches(docModel, schema, field, partialTerm);
1283                                 if (matches != null && matches.isEmpty() == false) {
1284                                         matches.add(0, primaryTermDisplayName); // insert the primary term's display name at the beginning of the list
1285                                         result = matches; // set the result to a list of matching term display names with the primary term's display name at the beginning
1286                                 }
1287                         }
1288                 }
1289
1290                 return result;
1291         }
1292
1293         @Override
1294         public void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
1295                 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
1296                 super.extractAllParts(wrapDoc);
1297         }
1298
1299         protected List<RelationsCommonList.RelationListItem> cloneList(List<RelationsCommonList.RelationListItem> inboundList) {
1300                 List<RelationsCommonList.RelationListItem> result = newRelationsCommonList();
1301                 for (RelationsCommonList.RelationListItem item : inboundList) {
1302                         result.add(item);
1303                 }
1304                 return result;
1305         }
1306
1307
1308         /* don't even THINK of re-using this method.
1309          * String example_uri = "/locationauthorities/7ec60f01-84ab-4908-9a6a/items/a5466530-713f-43b4-bc05";
1310          */
1311         @Deprecated
1312         private String extractInAuthorityCSID(String uri) {
1313                 String IN_AUTHORITY_REGEX = "/(.*?)/(.*?)/(.*)";
1314                 Pattern p = Pattern.compile(IN_AUTHORITY_REGEX);
1315                 Matcher m = p.matcher(uri);
1316                 if (m.find()) {
1317                         if (m.groupCount() < 3) {
1318                                 logger.warn("REGEX-WRONG-GROUPCOUNT looking in " + uri);
1319                                 return "";
1320                         } else {
1321                                 //String service = m.group(1);
1322                                 String inauth = m.group(2);
1323                                 //String theRest = m.group(3);
1324                                 return inauth;
1325                                 //print("service:"+service+", inauth:"+inauth+", rest:"+rest);
1326                         }
1327                 } else {
1328                         logger.warn("REGEX-NOT-MATCHED looking in " + uri);
1329                         return "";
1330                 }
1331         }
1332
1333         //ensures CSPACE-4042
1334         protected void uriPointsToSameAuthority(String thisURI, String inboundItemURI) throws Exception {
1335                 String authorityCSID = extractInAuthorityCSID(thisURI);
1336                 String authorityCSIDForInbound = extractInAuthorityCSID(inboundItemURI);
1337                 if (Tools.isBlank(authorityCSID)
1338                                 || Tools.isBlank(authorityCSIDForInbound)
1339                                 || (!authorityCSID.equalsIgnoreCase(authorityCSIDForInbound))) {
1340                         throw new Exception("Item URI " + thisURI + " must point to same authority as related item: " + inboundItemURI);
1341                 }
1342         }
1343
1344         public String getItemTermInfoGroupXPathBase() {
1345                 return authorityItemTermGroupXPathBase;
1346         }
1347
1348         public void setItemTermInfoGroupXPathBase(String itemTermInfoGroupXPathBase) {
1349                 authorityItemTermGroupXPathBase = itemTermInfoGroupXPathBase;
1350         }
1351
1352         protected String getAuthorityItemCommonSchemaName() {
1353                 return authorityItemCommonSchemaName;
1354         }
1355
1356         // @Override
1357         public boolean isJDBCQuery() {
1358                 boolean result = false;
1359
1360                 MultivaluedMap<String, String> queryParams = getServiceContext().getQueryParams();
1361                 //
1362                 // Look the query params to see if we need to make a SQL query.
1363                 //
1364                 String partialTerm = queryParams.getFirst(IQueryManager.SEARCH_TYPE_PARTIALTERM);
1365                 if (partialTerm != null && partialTerm.trim().isEmpty() == false) {
1366                         result = true;
1367                 }
1368
1369                 return result;
1370         }
1371
1372         // By convention, the name of the database table that contains
1373         // repeatable term information group records is derivable from
1374         // an existing XPath base value, by removing a suffix and converting
1375         // to lowercase
1376         protected String getTermGroupTableName() {
1377                 String termInfoGroupListName = getItemTermInfoGroupXPathBase();
1378                 return termInfoGroupListName.substring(0, termInfoGroupListName.lastIndexOf(LIST_SUFFIX)).toLowerCase();
1379         }
1380
1381         protected String getInAuthorityValue() {
1382                 String inAuthorityValue = getInAuthorityCsid();
1383                 if (Tools.notBlank(inAuthorityValue)) {
1384                         return inAuthorityValue;
1385                 } else {
1386                         return AuthorityResource.PARENT_WILDCARD;
1387                 }
1388         }
1389
1390         @Override
1391         public Map<String,String> getJDBCQueryParams() {
1392                 // FIXME: Get all of the following values from appropriate external constants.
1393                 // At present, these are duplicated in both RepositoryClientImpl
1394                 // and in AuthorityItemDocumentModelHandler.
1395                 final String TERM_GROUP_LIST_NAME = "TERM_GROUP_LIST_NAME";
1396                 final String TERM_GROUP_TABLE_NAME_PARAM = "TERM_GROUP_TABLE_NAME";
1397                 final String IN_AUTHORITY_PARAM = "IN_AUTHORITY";
1398
1399                 Map<String,String> params = super.getJDBCQueryParams();
1400                 params.put(TERM_GROUP_LIST_NAME, getItemTermInfoGroupXPathBase());
1401                 params.put(TERM_GROUP_TABLE_NAME_PARAM, getTermGroupTableName());
1402                 params.put(IN_AUTHORITY_PARAM, getInAuthorityValue());
1403                 return params;
1404         }
1405
1406 }