View Javadoc

1   /**
2    * Copyright 2005-2011 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.docsearch.service.impl;
17  
18  import org.apache.commons.collections.CollectionUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.joda.time.DateTime;
21  import org.joda.time.MutableDateTime;
22  import org.kuali.rice.core.api.CoreApiServiceLocator;
23  import org.kuali.rice.core.api.config.property.ConfigContext;
24  import org.kuali.rice.core.api.config.property.ConfigurationService;
25  import org.kuali.rice.core.api.reflect.ObjectDefinition;
26  import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
27  import org.kuali.rice.core.api.uif.RemotableAttributeError;
28  import org.kuali.rice.core.api.uif.RemotableAttributeField;
29  import org.kuali.rice.core.api.util.ConcreteKeyValue;
30  import org.kuali.rice.core.api.util.KeyValue;
31  import org.kuali.rice.kew.api.WorkflowRuntimeException;
32  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
33  import org.kuali.rice.kew.api.document.search.DocumentSearchResult;
34  import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
35  import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
36  import org.kuali.rice.kew.docsearch.DocumentSearchCustomizationMediator;
37  import org.kuali.rice.kew.framework.document.search.AttributeFields;
38  import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
39  import org.kuali.rice.kew.api.document.attribute.DocumentAttributeFactory;
40  import org.kuali.rice.kew.docsearch.dao.DocumentSearchDAO;
41  import org.kuali.rice.kew.docsearch.service.DocumentSearchService;
42  import org.kuali.rice.kew.doctype.SecuritySession;
43  import org.kuali.rice.kew.doctype.bo.DocumentType;
44  import org.kuali.rice.kew.exception.WorkflowServiceError;
45  import org.kuali.rice.kew.exception.WorkflowServiceErrorException;
46  import org.kuali.rice.kew.exception.WorkflowServiceErrorImpl;
47  import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration;
48  import org.kuali.rice.kew.framework.document.search.DocumentSearchResultValue;
49  import org.kuali.rice.kew.framework.document.search.DocumentSearchResultValues;
50  import org.kuali.rice.kew.impl.document.search.DocumentSearchGenerator;
51  import org.kuali.rice.kew.impl.document.search.DocumentSearchGeneratorImpl;
52  import org.kuali.rice.kew.service.KEWServiceLocator;
53  import org.kuali.rice.kew.useroptions.UserOptions;
54  import org.kuali.rice.kew.useroptions.UserOptionsService;
55  import org.kuali.rice.kew.api.KewApiConstants;
56  import org.kuali.rice.kim.api.group.Group;
57  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
58  import org.kuali.rice.krad.service.KRADServiceLocator;
59  import org.kuali.rice.krad.util.GlobalVariables;
60  
61  import java.io.IOException;
62  import java.util.ArrayList;
63  import java.util.Collection;
64  import java.util.Collections;
65  import java.util.HashMap;
66  import java.util.HashSet;
67  import java.util.LinkedHashMap;
68  import java.util.List;
69  import java.util.Map;
70  import java.util.Set;
71  
72  public class DocumentSearchServiceImpl implements DocumentSearchService {
73  
74  	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DocumentSearchServiceImpl.class);
75  
76  	private static final int MAX_SEARCH_ITEMS = 5;
77  	private static final String LAST_SEARCH_ORDER_OPTION = "DocSearch.LastSearch.Order";
78  	private static final String NAMED_SEARCH_ORDER_BASE = "DocSearch.NamedSearch.";
79  	private static final String LAST_SEARCH_BASE_NAME = "DocSearch.LastSearch.Holding";
80  
81  	private volatile ConfigurationService kualiConfigurationService;
82      private DocumentSearchCustomizationMediator documentSearchCustomizationMediator;
83  
84  	private DocumentSearchDAO docSearchDao;
85  	private UserOptionsService userOptionsService;
86  
87  	public void setDocumentSearchDAO(DocumentSearchDAO docSearchDao) {
88  		this.docSearchDao = docSearchDao;
89  	}
90  
91  	public void setUserOptionsService(UserOptionsService userOptionsService) {
92  		this.userOptionsService = userOptionsService;
93  	}
94  
95      public void setDocumentSearchCustomizationMediator(DocumentSearchCustomizationMediator documentSearchCustomizationMediator) {
96          this.documentSearchCustomizationMediator = documentSearchCustomizationMediator;
97      }
98  
99      protected DocumentSearchCustomizationMediator getDocumentSearchCustomizationMediator() {
100         return this.documentSearchCustomizationMediator;
101     }
102 
103 	public void clearNamedSearches(String principalId) {
104 		String[] clearListNames = { NAMED_SEARCH_ORDER_BASE + "%", LAST_SEARCH_BASE_NAME + "%", LAST_SEARCH_ORDER_OPTION + "%" };
105         for (String clearListName : clearListNames)
106         {
107             List<UserOptions> records = userOptionsService.findByUserQualified(principalId, clearListName);
108             for (UserOptions userOptions : records) {
109                 userOptionsService.deleteUserOptions((UserOptions) userOptions);
110             }
111         }
112 	}
113 
114     public DocumentSearchCriteria getNamedSearchCriteria(String principalId, String searchName) {
115         return getSavedSearchCriteria(principalId, NAMED_SEARCH_ORDER_BASE + searchName);
116     }
117 
118     public DocumentSearchCriteria getSavedSearchCriteria(String principalId, String searchName) {
119         UserOptions savedSearch = userOptionsService.findByOptionId(searchName, principalId);
120         if (savedSearch == null) {
121             return null;
122         }
123         return getCriteriaFromSavedSearch(savedSearch);
124     }
125 
126     protected DocumentSearchCriteria getCriteriaFromSavedSearch(UserOptions savedSearch) {
127         String optionValue = savedSearch.getOptionVal();
128         try {
129             return DocumentSearchInternalUtils.unmarshalDocumentSearchCriteria(optionValue);
130         } catch (IOException e) {
131             throw new WorkflowRuntimeException("Failed to load saved search for name '" + savedSearch.getOptionId() + "'", e);
132         }
133     }
134 
135     private String getOptionCriteriaField(UserOptions userOption, String fieldName) {
136         String value = userOption.getOptionVal();
137         if (value != null) {
138             String[] fields = value.split(",,");
139             for (String field : fields)
140             {
141                 if (field.startsWith(fieldName + "="))
142                 {
143                     return field.substring(field.indexOf(fieldName) + fieldName.length() + 1, field.length());
144                 }
145             }
146         }
147         return null;
148     }
149 
150     @Override
151 	public DocumentSearchResults lookupDocuments(String principalId, DocumentSearchCriteria criteria) {
152 		DocumentSearchGenerator docSearchGenerator = getStandardDocumentSearchGenerator();
153 		DocumentType documentType = KEWServiceLocator.getDocumentTypeService().findByName(criteria.getDocumentTypeName());
154         DocumentSearchCriteria.Builder criteriaBuilder = DocumentSearchCriteria.Builder.create(criteria);
155         validateDocumentSearchCriteria(docSearchGenerator, criteriaBuilder);
156         DocumentSearchCriteria builtCriteria = applyCriteriaCustomizations(documentType, criteriaBuilder.build());
157         builtCriteria = applyCriteriaDefaults(builtCriteria);
158         boolean criteriaModified = !criteria.equals(builtCriteria);
159         List<RemotableAttributeField> searchFields = determineSearchFields(documentType);
160         DocumentSearchResults.Builder searchResults = docSearchDao.findDocuments(docSearchGenerator, builtCriteria, criteriaModified, searchFields);
161         if (documentType != null) {
162             DocumentSearchResultValues resultValues = getDocumentSearchCustomizationMediator().customizeResults(documentType, builtCriteria, searchResults.build());
163             if (resultValues != null && CollectionUtils.isNotEmpty(resultValues.getResultValues())) {
164                 Map<String, DocumentSearchResultValue> resultValueMap = new HashMap<String, DocumentSearchResultValue>();
165                 for (DocumentSearchResultValue resultValue : resultValues.getResultValues()) {
166                     resultValueMap.put(resultValue.getDocumentId(), resultValue);
167                 }
168                 for (DocumentSearchResult.Builder result : searchResults.getSearchResults()) {
169                     DocumentSearchResultValue value = resultValueMap.get(result.getDocument().getDocumentId());
170                     if (value != null) {
171                         applyResultCustomization(result, value);
172                     }
173                 }
174             }
175         }
176 
177         if (StringUtils.isNotBlank(principalId) && !searchResults.getSearchResults().isEmpty()) {
178             DocumentSearchResults builtResults = searchResults.build();
179             Set<String> authorizedDocumentIds = KEWServiceLocator.getDocumentSecurityService().documentSearchResultAuthorized(
180                     principalId, builtResults, new SecuritySession(principalId));
181             if (CollectionUtils.isNotEmpty(authorizedDocumentIds)) {
182                 int numFiltered = 0;
183                 List<DocumentSearchResult.Builder> finalResults = new ArrayList<DocumentSearchResult.Builder>();
184                 for (DocumentSearchResult.Builder result : searchResults.getSearchResults()) {
185                     if (authorizedDocumentIds.contains(result.getDocument().getDocumentId())) {
186                         finalResults.add(result);
187                     } else {
188                         numFiltered++;
189                     }
190                 }
191                 searchResults.setSearchResults(finalResults);
192                 searchResults.setNumberOfSecurityFilteredResults(numFiltered);
193             } else {
194                 searchResults.setNumberOfSecurityFilteredResults(searchResults.getSearchResults().size());
195                 searchResults.setSearchResults(Collections.<DocumentSearchResult.Builder>emptyList());
196             }
197         }
198         saveSearch(principalId, builtCriteria);
199         return searchResults.build();
200 	}
201 
202     protected void applyResultCustomization(DocumentSearchResult.Builder result, DocumentSearchResultValue value) {
203         Map<String, List<DocumentAttribute.AbstractBuilder<?>>> customizedAttributeMap =
204                 new LinkedHashMap<String, List<DocumentAttribute.AbstractBuilder<?>>>();
205         for (DocumentAttribute customizedAttribute : value.getDocumentAttributes()) {
206             List<DocumentAttribute.AbstractBuilder<?>> attributesForName = customizedAttributeMap.get(value.getDocumentId());
207             if (attributesForName == null) {
208                 attributesForName = new ArrayList<DocumentAttribute.AbstractBuilder<?>>();
209                 customizedAttributeMap.put(value.getDocumentId(), attributesForName);
210             }
211             attributesForName.add(DocumentAttributeFactory.loadContractIntoBuilder(customizedAttribute));
212         }
213         // keep track of what we've already applied customizations for, since those will replace existing attributes with that name
214         Set<String> documentAttributeNamesCustomized = new HashSet<String>();
215         List<DocumentAttribute.AbstractBuilder<?>> newDocumentAttributes = new ArrayList<DocumentAttribute.AbstractBuilder<?>>();
216         for (DocumentAttribute.AbstractBuilder<?> documentAttribute : result.getDocumentAttributes()) {
217             String name = documentAttribute.getName();
218             if (!documentAttributeNamesCustomized.contains(name) && customizedAttributeMap.containsKey(name)) {
219                 documentAttributeNamesCustomized.add(name);
220                 newDocumentAttributes.addAll(customizedAttributeMap.get(name));
221             }
222         }
223     }
224 
225 
226     /**
227      * Applies any document type-specific customizations to the lookup criteria.  If no customizations are configured
228      * for the document type, this method will simply return the criteria that is passed to it.  If
229      * the given DocumentType is null, then this method will also simply return the criteria that is passed to it.
230      */
231     protected DocumentSearchCriteria applyCriteriaCustomizations(DocumentType documentType, DocumentSearchCriteria criteria) {
232         if (documentType == null) {
233             return criteria;
234         }
235         DocumentSearchCriteria customizedCriteria = getDocumentSearchCustomizationMediator().customizeCriteria(documentType, criteria);
236         if (customizedCriteria != null) {
237             return customizedCriteria;
238         }
239         return criteria;
240     }
241 
242     protected DocumentSearchCriteria applyCriteriaDefaults(DocumentSearchCriteria criteria) {
243         DocumentSearchCriteria.Builder comparisonCriteria = createEmptyComparisonCriteria(criteria);
244         boolean isCriteriaEmpty = criteria.equals(comparisonCriteria.build());
245         boolean isTitleOnly = false;
246         if (!isCriteriaEmpty) {
247             comparisonCriteria.setTitle(criteria.getTitle());
248             isTitleOnly = criteria.equals(comparisonCriteria.build());
249         }
250 
251         if (isCriteriaEmpty || isTitleOnly) {
252             DocumentSearchCriteria.Builder criteriaBuilder = DocumentSearchCriteria.Builder.create(criteria);
253             Integer defaultCreateDateDaysAgoValue = null;
254             if (isCriteriaEmpty) {
255                 // if they haven't set any criteria, default the from created date to today minus days from constant variable
256                 defaultCreateDateDaysAgoValue = KewApiConstants.DOCUMENT_SEARCH_NO_CRITERIA_CREATE_DATE_DAYS_AGO;
257             } else if (isTitleOnly) {
258                 // If the document title is the only field which was entered, we want to set the "from" date to be X
259                 // days ago.  This will allow for a more efficient query.
260                 defaultCreateDateDaysAgoValue = KewApiConstants.DOCUMENT_SEARCH_DOC_TITLE_CREATE_DATE_DAYS_AGO;
261             }
262             if (defaultCreateDateDaysAgoValue != null) {
263                 // add a default create date
264                 MutableDateTime mutableDateTime = new MutableDateTime();
265                 mutableDateTime.addDays(defaultCreateDateDaysAgoValue.intValue());
266                 criteriaBuilder.setDateCreatedFrom(mutableDateTime.toDateTime());
267             }
268             criteria = criteriaBuilder.build();
269         }
270         return criteria;
271     }
272 
273     protected DocumentSearchCriteria.Builder createEmptyComparisonCriteria(DocumentSearchCriteria criteria) {
274         DocumentSearchCriteria.Builder builder = DocumentSearchCriteria.Builder.create();
275         // copy over the fields that shouldn't be considered when determining if the criteria is empty
276         builder.setSaveName(criteria.getSaveName());
277         builder.setStartAtIndex(criteria.getStartAtIndex());
278         builder.setMaxResults(criteria.getMaxResults());
279         return builder;
280     }
281 
282     protected List<RemotableAttributeField> determineSearchFields(DocumentType documentType) {
283         List<RemotableAttributeField> searchFields = new ArrayList<RemotableAttributeField>();
284         if (documentType != null) {
285             DocumentSearchCriteriaConfiguration searchConfiguration =
286                     getDocumentSearchCustomizationMediator().getDocumentSearchCriteriaConfiguration(documentType);
287             if (searchConfiguration != null) {
288                 List<AttributeFields> attributeFields = searchConfiguration.getSearchAttributeFields();
289                 if (attributeFields != null) {
290                     for (AttributeFields fields : attributeFields) {
291                         searchFields.addAll(fields.getRemotableAttributeFields());
292                     }
293                 }
294             }
295         }
296         return searchFields;
297     }
298 
299     public DocumentSearchGenerator getStandardDocumentSearchGenerator() {
300 	String searchGeneratorClass = ConfigContext.getCurrentContextConfig().getProperty(KewApiConstants.STANDARD_DOC_SEARCH_GENERATOR_CLASS_CONFIG_PARM);
301 	if (searchGeneratorClass == null){
302 	    return new DocumentSearchGeneratorImpl();
303 	}
304     	return (DocumentSearchGenerator)GlobalResourceLoader.getObject(new ObjectDefinition(searchGeneratorClass));
305     }
306 
307     @Override
308     public void validateDocumentSearchCriteria(DocumentSearchGenerator docSearchGenerator, DocumentSearchCriteria.Builder criteria) {
309         List<WorkflowServiceError> errors = this.validateWorkflowDocumentSearchCriteria(criteria);
310         List<RemotableAttributeError> searchAttributeErrors = docSearchGenerator.validateSearchableAttributes(criteria);
311         if (!CollectionUtils.isEmpty(searchAttributeErrors)) {
312             // attribute errors are fully materialized error messages, so the only "key" that makes sense is to use "error.custom"
313             for (RemotableAttributeError searchAttributeError : searchAttributeErrors) {
314                 for (String errorMessage : searchAttributeError.getErrors()) {
315                     WorkflowServiceError error = new WorkflowServiceErrorImpl(errorMessage, "error.custom", errorMessage);
316                     errors.add(error);
317                 }
318             }
319         }
320         if (!errors.isEmpty() || !GlobalVariables.getMessageMap().hasNoErrors()) {
321             throw new WorkflowServiceErrorException("Document Search Validation Errors", errors);
322         }
323     }
324 
325     protected List<WorkflowServiceError> validateWorkflowDocumentSearchCriteria(DocumentSearchCriteria.Builder criteria) {
326         List<WorkflowServiceError> errors = new ArrayList<WorkflowServiceError>();
327 
328         // trim the principal names, validation isn't really necessary, because if not found, no results will be
329         // returned.
330         criteria.setApproverPrincipalName(trimCriteriaValue(criteria.getApproverPrincipalName()));
331         criteria.setViewerPrincipalName(trimCriteriaValue(criteria.getViewerPrincipalName()));
332         criteria.setInitiatorPrincipalName(trimCriteriaValue(criteria.getInitiatorPrincipalName()));
333         validateGroupCriteria(criteria, errors);
334         criteria.setDocumentId(criteria.getDocumentId());
335         return errors;
336     }
337 
338     private String trimCriteriaValue(String criteriaValue) {
339         if (StringUtils.isNotBlank(criteriaValue)) {
340             criteriaValue = criteriaValue.trim();
341         }
342         if (StringUtils.isBlank(criteriaValue)) {
343             return null;
344         }
345         return criteriaValue;
346     }
347 
348     private void validateGroupCriteria(DocumentSearchCriteria.Builder criteria, List<WorkflowServiceError> errors) {
349         if (StringUtils.isNotBlank(criteria.getViewerGroupId())) {
350             Group group = KimApiServiceLocator.getGroupService().getGroup(criteria.getViewerGroupId());
351             if (group == null) {
352                 errors.add(new WorkflowServiceErrorImpl("Workgroup Viewer Name is not a workgroup", "docsearch.DocumentSearchService.workgroup.viewer"));
353             }
354         } else {
355             criteria.setViewerGroupId(null);
356         }
357     }
358 
359     @Override
360 	public List<KeyValue> getNamedSearches(String principalId) {
361 		List<UserOptions> namedSearches = userOptionsService.findByUserQualified(principalId, NAMED_SEARCH_ORDER_BASE + "%");
362 		List<KeyValue> sortedNamedSearches = new ArrayList<KeyValue>(0);
363 		if (!namedSearches.isEmpty()) {
364 			Collections.sort(namedSearches);
365 			for (UserOptions namedSearch : namedSearches) {
366 				KeyValue keyValue = new ConcreteKeyValue(namedSearch.getOptionId(), namedSearch.getOptionId().substring(NAMED_SEARCH_ORDER_BASE.length(), namedSearch.getOptionId().length()));
367 				sortedNamedSearches.add(keyValue);
368 			}
369 		}
370 		return sortedNamedSearches;
371 	}
372 
373     @Override
374 	public List<KeyValue> getMostRecentSearches(String principalId) {
375 		UserOptions order = userOptionsService.findByOptionId(LAST_SEARCH_ORDER_OPTION, principalId);
376 		List<KeyValue> sortedMostRecentSearches = new ArrayList<KeyValue>();
377 		if (order != null && order.getOptionVal() != null && !"".equals(order.getOptionVal())) {
378 			List<UserOptions> mostRecentSearches = userOptionsService.findByUserQualified(principalId, LAST_SEARCH_BASE_NAME + "%");
379 			String[] ordered = order.getOptionVal().split(",");
380             for (String anOrdered : ordered) {
381                 UserOptions matchingOption = null;
382                 for (UserOptions option : mostRecentSearches) {
383                     if (anOrdered.equals(option.getOptionId())) {
384                         matchingOption = option;
385                         break;
386                     }
387                 }
388                 if (matchingOption != null) {
389                     DocumentSearchCriteria matchingCriteria = getCriteriaFromSavedSearch(matchingOption);
390                 	sortedMostRecentSearches.add(new ConcreteKeyValue(anOrdered, getSavedSearchAbbreviatedString(matchingCriteria)));
391                 }
392             }
393 		}
394 		return sortedMostRecentSearches;
395 	}
396 
397     public DocumentSearchCriteria clearCriteria(DocumentType documentType, DocumentSearchCriteria criteria) {
398         DocumentSearchCriteria clearedCriteria = getDocumentSearchCustomizationMediator().customizeClearCriteria(
399                 documentType, criteria);
400         if (clearedCriteria == null) {
401             clearedCriteria = getStandardDocumentSearchGenerator().clearSearch(criteria);
402         }
403         return clearedCriteria;
404     }
405 
406     protected String getSavedSearchAbbreviatedString(DocumentSearchCriteria criteria) {
407         Map<String, String> abbreviatedStringMap = new LinkedHashMap<String, String>();
408         addAbbreviatedString(abbreviatedStringMap, "Doc Type", criteria.getDocumentTypeName());
409         addAbbreviatedString(abbreviatedStringMap, "Initiator", criteria.getInitiatorPrincipalName());
410         addAbbreviatedString(abbreviatedStringMap, "Doc Id", criteria.getDocumentId());
411         addAbbreviatedRangeString(abbreviatedStringMap, "Created", criteria.getDateCreatedFrom(),
412                 criteria.getDateCreatedTo());
413         addAbbreviatedString(abbreviatedStringMap, "Title", criteria.getTitle());
414         addAbbreviatedString(abbreviatedStringMap, "App Doc Id", criteria.getApplicationDocumentId());
415         addAbbreviatedRangeString(abbreviatedStringMap, "Approved", criteria.getDateApprovedFrom(),
416                 criteria.getDateApprovedTo());
417         addAbbreviatedRangeString(abbreviatedStringMap, "Modified", criteria.getDateLastModifiedFrom(), criteria.getDateLastModifiedTo());
418         addAbbreviatedRangeString(abbreviatedStringMap, "Finalized", criteria.getDateFinalizedFrom(), criteria.getDateFinalizedTo());
419         addAbbreviatedRangeString(abbreviatedStringMap, "App Doc Status Changed", criteria.getDateApplicationDocumentStatusChangedFrom(), criteria.getDateApplicationDocumentStatusChangedTo());
420         addAbbreviatedString(abbreviatedStringMap, "Approver", criteria.getApproverPrincipalName());
421         addAbbreviatedString(abbreviatedStringMap, "Viewer", criteria.getViewerPrincipalName());
422         addAbbreviatedString(abbreviatedStringMap, "Group Viewer", criteria.getViewerGroupId());
423         addAbbreviatedString(abbreviatedStringMap, "Node", criteria.getRouteNodeName());
424         addAbbreviatedMultiValuedString(abbreviatedStringMap, "Status", criteria.getDocumentStatuses());
425         addAbbreviatedMultiValuedString(abbreviatedStringMap, "Category", criteria.getDocumentStatusCategories());
426         for (String documentAttributeName : criteria.getDocumentAttributeValues().keySet()) {
427             addAbbreviatedMultiValuedString(abbreviatedStringMap, documentAttributeName, criteria.getDocumentAttributeValues().get(documentAttributeName));
428         }
429         StringBuilder stringBuilder = new StringBuilder();
430         int iteration = 0;
431         for (String label : abbreviatedStringMap.keySet()) {
432             stringBuilder.append(label).append("=").append(abbreviatedStringMap.get(label));
433             if (iteration < abbreviatedStringMap.keySet().size()) {
434                 stringBuilder.append("; ");
435             }
436         }
437         return stringBuilder.toString();
438     }
439 
440     protected void addAbbreviatedString(Map<String, String> abbreviatedStringMap, String label, String value) {
441         if (StringUtils.isNotBlank(value)) {
442             abbreviatedStringMap.put(label, value);
443         }
444     }
445 
446     protected void addAbbreviatedMultiValuedString(Map<String, String> abbreviatedStringMap, String label, Collection<? extends Object> values) {
447         if (CollectionUtils.isNotEmpty(values)) {
448             List<String> stringValues = new ArrayList<String>();
449             for (Object value : values) {
450                 stringValues.add(value.toString());
451             }
452             abbreviatedStringMap.put(label, StringUtils.join(stringValues, ","));
453         }
454     }
455 
456     protected void addAbbreviatedRangeString(Map<String, String> abbreviatedStringMap, String label, DateTime dateFrom, DateTime dateTo) {
457         if (dateFrom != null || dateTo != null) {
458             StringBuilder abbreviatedString = new StringBuilder();
459             if (dateFrom != null) {
460                 abbreviatedString.append(CoreApiServiceLocator.getDateTimeService().toDateString(dateFrom.toDate()));
461             }
462             abbreviatedString.append("..");
463             if (dateTo != null) {
464                 abbreviatedString.append(CoreApiServiceLocator.getDateTimeService().toDateString(dateTo.toDate()));
465             }
466             abbreviatedStringMap.put(label, abbreviatedString.toString());
467         }
468     }
469 
470     /**
471      * Saves a DocumentSearchCriteria into the UserOptions.  This method operates in one of two ways:
472      * 1) The search is named: the criteria is saved under NAMED_SEARCH_ORDER_BASE + <name>
473      * 2) The search is unnamed: the criteria is given a name that indicates its order, which is saved in a second user option
474      *    which contains a list of these names comprising recent searches
475      * @param principalId the user to save the criteria under
476      * @param criteria the doc lookup criteria
477      */
478     private void saveSearch(String principalId, DocumentSearchCriteria criteria) {
479         if (StringUtils.isBlank(principalId)) {
480             return;
481         }
482 
483         // TODO - KULRICE-5832 - need to add support for whether or not the saved search is a detailed/advanced search, this was originally stored with savedSearchString in Rice 1.x
484 
485         try {
486             String savedSearchString = DocumentSearchInternalUtils.marshalDocumentSearchCriteria(criteria);
487 
488             if (StringUtils.isNotBlank(criteria.getSaveName())) {
489                 userOptionsService.save(principalId, NAMED_SEARCH_ORDER_BASE + criteria.getSaveName(), savedSearchString);
490             } else {
491                 // first determine the current ordering
492                 UserOptions searchOrder = userOptionsService.findByOptionId(LAST_SEARCH_ORDER_OPTION, principalId);
493                 // no previous searches, save under first id
494                 if (searchOrder == null) {
495                     userOptionsService.save(principalId, LAST_SEARCH_BASE_NAME + "0", savedSearchString);
496                     userOptionsService.save(principalId, LAST_SEARCH_ORDER_OPTION, LAST_SEARCH_BASE_NAME + "0");
497                 } else {
498                     String[] currentOrder = searchOrder.getOptionVal().split(",");
499                     // we have reached MAX_SEARCH_ITEMS
500                     if (currentOrder.length == MAX_SEARCH_ITEMS) {
501                         // move the last item to the front of the list, and save
502                         // over this key with the new criteria
503                         // [5,4,3,2,1] => [1,5,4,3,2]
504                         String searchName = currentOrder[currentOrder.length - 1];
505                         String[] newOrder = new String[MAX_SEARCH_ITEMS];
506                         newOrder[0] = searchName;
507                         for (int i = 0; i < currentOrder.length - 1; i++) {
508                             newOrder[i + 1] = currentOrder[i];
509                         }
510                         // rejoins items with comma separator...
511                         String newSearchOrder = "";
512                         for (String aNewOrder : newOrder) {
513                             if (!"".equals(newSearchOrder)) {
514                                 newSearchOrder += ",";
515                             }
516                             newSearchOrder += aNewOrder;
517                         }
518                         // save the search string under the searchName (which used to be the last name in the list)
519                         userOptionsService.save(principalId, searchName, savedSearchString);
520                         userOptionsService.save(principalId, LAST_SEARCH_ORDER_OPTION, newSearchOrder);
521                     } else {
522                         // saves the search to the front of the list with incremented index
523                         // [3,2,1] => [4,3,2,1]
524                         // here we need to do a push to identify the highest used number which is from the
525                         // first one in the array, and then add one to it, and push the rest back one
526                         int absMax = 0;
527                         for (String aCurrentOrder : currentOrder) {
528                             int current = new Integer(aCurrentOrder.substring(LAST_SEARCH_BASE_NAME.length(),
529                                     aCurrentOrder.length()));
530                             if (current > absMax) {
531                                 absMax = current;
532                             }
533                         }
534                         String searchName = LAST_SEARCH_BASE_NAME + ++absMax;
535                         String[] newOrder = new String[currentOrder.length + 1];
536                         newOrder[0] = searchName;
537                         for (int i = 0; i < currentOrder.length; i++) {
538                             newOrder[i + 1] = currentOrder[i];
539                         }
540                         String newSearchOrder = "";
541                         for (String aNewOrder : newOrder) {
542                             if (!"".equals(newSearchOrder)) {
543                                 newSearchOrder += ",";
544                             }
545                             newSearchOrder += aNewOrder;
546                         }
547                         userOptionsService.save(principalId, searchName, savedSearchString);
548                         userOptionsService.save(principalId, LAST_SEARCH_ORDER_OPTION, newSearchOrder);
549                     }
550                 }
551             }
552         } catch (Exception e) {
553             // we don't want the failure when saving a search to affect the ability of the document search to succeed
554             // and return it's results, so just log and return
555             LOG.error("Unable to save search due to exception", e);
556         }
557     }
558 
559 	public ConfigurationService getKualiConfigurationService() {
560 		if (kualiConfigurationService == null) {
561 			kualiConfigurationService = KRADServiceLocator.getKualiConfigurationService();
562 		}
563 		return kualiConfigurationService;
564 	}
565 
566 }