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