View Javadoc

1   /**
2    * Copyright 2005-2012 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.KewApiConstants;
32  import org.kuali.rice.kew.api.WorkflowRuntimeException;
33  import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
34  import org.kuali.rice.kew.api.document.attribute.DocumentAttributeFactory;
35  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
36  import org.kuali.rice.kew.api.document.search.DocumentSearchResult;
37  import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
38  import org.kuali.rice.kew.docsearch.DocumentSearchCustomizationMediator;
39  import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
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.AttributeFields;
48  import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration;
49  import org.kuali.rice.kew.framework.document.search.DocumentSearchResultValue;
50  import org.kuali.rice.kew.framework.document.search.DocumentSearchResultValues;
51  import org.kuali.rice.kew.impl.document.search.DocumentSearchGenerator;
52  import org.kuali.rice.kew.impl.document.search.DocumentSearchGeneratorImpl;
53  import org.kuali.rice.kew.service.KEWServiceLocator;
54  import org.kuali.rice.kew.useroptions.UserOptions;
55  import org.kuali.rice.kew.useroptions.UserOptionsService;
56  import org.kuali.rice.kew.util.Utilities;
57  import org.kuali.rice.kim.api.group.Group;
58  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
59  import org.kuali.rice.kns.service.DictionaryValidationService;
60  import org.kuali.rice.kns.service.DataDictionaryService;
61  import org.kuali.rice.kns.service.KNSServiceLocator;
62  import org.kuali.rice.krad.service.KRADServiceLocator;
63  import org.kuali.rice.krad.util.GlobalVariables;
64  
65  import java.io.IOException;
66  import java.text.SimpleDateFormat;
67  import java.util.ArrayList;
68  import java.util.Collection;
69  import java.util.Collections;
70  import java.util.HashMap;
71  import java.util.HashSet;
72  import java.util.LinkedHashMap;
73  import java.util.List;
74  import java.util.Map;
75  import java.util.Set;
76  
77  public class DocumentSearchServiceImpl implements DocumentSearchService {
78  
79  	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DocumentSearchServiceImpl.class);
80  
81  	private static final int MAX_SEARCH_ITEMS = 5;
82  	private static final String LAST_SEARCH_ORDER_OPTION = "DocSearch.LastSearch.Order";
83  	private static final String NAMED_SEARCH_ORDER_BASE = "DocSearch.NamedSearch.";
84  	private static final String LAST_SEARCH_BASE_NAME = "DocSearch.LastSearch.Holding";
85      private static final String DOC_SEARCH_CRITERIA_CLASS = "org.kuali.rice.kew.api.document.search.DocumentSearchCriteria";
86      private static final String DATA_TYPE_DATE = "datetime";
87  
88  	private volatile ConfigurationService kualiConfigurationService;
89      private DocumentSearchCustomizationMediator documentSearchCustomizationMediator;
90  
91  	private DocumentSearchDAO docSearchDao;
92  	private UserOptionsService userOptionsService;
93  
94      private static DictionaryValidationService dictionaryValidationService;
95      private static DataDictionaryService dataDictionaryService;
96  
97  	public void setDocumentSearchDAO(DocumentSearchDAO docSearchDao) {
98  		this.docSearchDao = docSearchDao;
99  	}
100 
101 	public void setUserOptionsService(UserOptionsService userOptionsService) {
102 		this.userOptionsService = userOptionsService;
103 	}
104 
105     public void setDocumentSearchCustomizationMediator(DocumentSearchCustomizationMediator documentSearchCustomizationMediator) {
106         this.documentSearchCustomizationMediator = documentSearchCustomizationMediator;
107     }
108 
109     protected DocumentSearchCustomizationMediator getDocumentSearchCustomizationMediator() {
110         return this.documentSearchCustomizationMediator;
111     }
112 
113 	public void clearNamedSearches(String principalId) {
114 		String[] clearListNames = { NAMED_SEARCH_ORDER_BASE + "%", LAST_SEARCH_BASE_NAME + "%", LAST_SEARCH_ORDER_OPTION + "%" };
115         for (String clearListName : clearListNames)
116         {
117             List<UserOptions> records = userOptionsService.findByUserQualified(principalId, clearListName);
118             for (UserOptions userOptions : records) {
119                 userOptionsService.deleteUserOptions((UserOptions) userOptions);
120             }
121         }
122 	}
123 
124     public DocumentSearchCriteria getNamedSearchCriteria(String principalId, String searchName) {
125         return getSavedSearchCriteria(principalId, NAMED_SEARCH_ORDER_BASE + searchName);
126     }
127 
128     public DocumentSearchCriteria getSavedSearchCriteria(String principalId, String searchName) {
129         UserOptions savedSearch = userOptionsService.findByOptionId(searchName, principalId);
130         if (savedSearch == null) {
131             return null;
132         }
133         return getCriteriaFromSavedSearch(savedSearch);
134     }
135 
136     protected DocumentSearchCriteria getCriteriaFromSavedSearch(UserOptions savedSearch) {
137         String optionValue = savedSearch.getOptionVal();
138         try {
139             return DocumentSearchInternalUtils.unmarshalDocumentSearchCriteria(optionValue);
140         } catch (IOException e) {
141             throw new WorkflowRuntimeException("Failed to load saved search for name '" + savedSearch.getOptionId() + "'", e);
142         }
143     }
144 
145     private String getOptionCriteriaField(UserOptions userOption, String fieldName) {
146         String value = userOption.getOptionVal();
147         if (value != null) {
148             String[] fields = value.split(",,");
149             for (String field : fields)
150             {
151                 if (field.startsWith(fieldName + "="))
152                 {
153                     return field.substring(field.indexOf(fieldName) + fieldName.length() + 1, field.length());
154                 }
155             }
156         }
157         return null;
158     }
159 
160     @Override
161 	public DocumentSearchResults lookupDocuments(String principalId, DocumentSearchCriteria criteria) {
162 		DocumentSearchGenerator docSearchGenerator = getStandardDocumentSearchGenerator();
163 		DocumentType documentType = KEWServiceLocator.getDocumentTypeService().findByName(criteria.getDocumentTypeName());
164         DocumentSearchCriteria.Builder criteriaBuilder = DocumentSearchCriteria.Builder.create(criteria);
165         validateDocumentSearchCriteria(docSearchGenerator, criteriaBuilder);
166         DocumentSearchCriteria builtCriteria = applyCriteriaCustomizations(documentType, criteriaBuilder.build());
167         builtCriteria = applyCriteriaDefaults(builtCriteria);
168         boolean criteriaModified = !criteria.equals(builtCriteria);
169         List<RemotableAttributeField> searchFields = determineSearchFields(documentType);
170         DocumentSearchResults.Builder searchResults = docSearchDao.findDocuments(docSearchGenerator, builtCriteria, criteriaModified, searchFields);
171         if (documentType != null) {
172             DocumentSearchResultValues resultValues = getDocumentSearchCustomizationMediator().customizeResults(documentType, builtCriteria, searchResults.build());
173             if (resultValues != null && CollectionUtils.isNotEmpty(resultValues.getResultValues())) {
174                 Map<String, DocumentSearchResultValue> resultValueMap = new HashMap<String, DocumentSearchResultValue>();
175                 for (DocumentSearchResultValue resultValue : resultValues.getResultValues()) {
176                     resultValueMap.put(resultValue.getDocumentId(), resultValue);
177                 }
178                 for (DocumentSearchResult.Builder result : searchResults.getSearchResults()) {
179                     DocumentSearchResultValue value = resultValueMap.get(result.getDocument().getDocumentId());
180                     if (value != null) {
181                         applyResultCustomization(result, value);
182                     }
183                 }
184             }
185         }
186 
187         if (StringUtils.isNotBlank(principalId) && !searchResults.getSearchResults().isEmpty()) {
188             DocumentSearchResults builtResults = searchResults.build();
189             Set<String> authorizedDocumentIds = KEWServiceLocator.getDocumentSecurityService().documentSearchResultAuthorized(
190                     principalId, builtResults, new SecuritySession(principalId));
191             if (CollectionUtils.isNotEmpty(authorizedDocumentIds)) {
192                 int numFiltered = 0;
193                 List<DocumentSearchResult.Builder> finalResults = new ArrayList<DocumentSearchResult.Builder>();
194                 for (DocumentSearchResult.Builder result : searchResults.getSearchResults()) {
195                     if (authorizedDocumentIds.contains(result.getDocument().getDocumentId())) {
196                         finalResults.add(result);
197                     } else {
198                         numFiltered++;
199                     }
200                 }
201                 searchResults.setSearchResults(finalResults);
202                 searchResults.setNumberOfSecurityFilteredResults(numFiltered);
203             } else {
204                 searchResults.setNumberOfSecurityFilteredResults(searchResults.getSearchResults().size());
205                 searchResults.setSearchResults(Collections.<DocumentSearchResult.Builder>emptyList());
206             }
207         }
208         saveSearch(principalId, builtCriteria);
209         return searchResults.build();
210 	}
211 
212     protected void applyResultCustomization(DocumentSearchResult.Builder result, DocumentSearchResultValue value) {
213         Map<String, List<DocumentAttribute.AbstractBuilder<?>>> customizedAttributeMap =
214                 new LinkedHashMap<String, List<DocumentAttribute.AbstractBuilder<?>>>();
215         for (DocumentAttribute customizedAttribute : value.getDocumentAttributes()) {
216             List<DocumentAttribute.AbstractBuilder<?>> attributesForName = customizedAttributeMap.get(value.getDocumentId());
217             if (attributesForName == null) {
218                 attributesForName = new ArrayList<DocumentAttribute.AbstractBuilder<?>>();
219                 customizedAttributeMap.put(value.getDocumentId(), attributesForName);
220             }
221             attributesForName.add(DocumentAttributeFactory.loadContractIntoBuilder(customizedAttribute));
222         }
223         // keep track of what we've already applied customizations for, since those will replace existing attributes with that name
224         Set<String> documentAttributeNamesCustomized = new HashSet<String>();
225         List<DocumentAttribute.AbstractBuilder<?>> newDocumentAttributes = new ArrayList<DocumentAttribute.AbstractBuilder<?>>();
226         for (DocumentAttribute.AbstractBuilder<?> documentAttribute : result.getDocumentAttributes()) {
227             String name = documentAttribute.getName();
228             if (!documentAttributeNamesCustomized.contains(name) && customizedAttributeMap.containsKey(name)) {
229                 documentAttributeNamesCustomized.add(name);
230                 newDocumentAttributes.addAll(customizedAttributeMap.get(name));
231             }
232         }
233     }
234 
235 
236     /**
237      * Applies any document type-specific customizations to the lookup criteria.  If no customizations are configured
238      * for the document type, this method will simply return the criteria that is passed to it.  If
239      * the given DocumentType is null, then this method will also simply return the criteria that is passed to it.
240      */
241     protected DocumentSearchCriteria applyCriteriaCustomizations(DocumentType documentType, DocumentSearchCriteria criteria) {
242         if (documentType == null) {
243             return criteria;
244         }
245         DocumentSearchCriteria customizedCriteria = getDocumentSearchCustomizationMediator().customizeCriteria(documentType, criteria);
246         if (customizedCriteria != null) {
247             return customizedCriteria;
248         }
249         return criteria;
250     }
251 
252     protected DocumentSearchCriteria applyCriteriaDefaults(DocumentSearchCriteria criteria) {
253         DocumentSearchCriteria.Builder comparisonCriteria = createEmptyComparisonCriteria(criteria);
254         boolean isCriteriaEmpty = criteria.equals(comparisonCriteria.build());
255         boolean isTitleOnly = false;
256         if (!isCriteriaEmpty) {
257             comparisonCriteria.setTitle(criteria.getTitle());
258             isTitleOnly = criteria.equals(comparisonCriteria.build());
259         }
260 
261         if (isCriteriaEmpty || isTitleOnly) {
262             DocumentSearchCriteria.Builder criteriaBuilder = DocumentSearchCriteria.Builder.create(criteria);
263             Integer defaultCreateDateDaysAgoValue = null;
264             if (isCriteriaEmpty) {
265                 // if they haven't set any criteria, default the from created date to today minus days from constant variable
266                 defaultCreateDateDaysAgoValue = KewApiConstants.DOCUMENT_SEARCH_NO_CRITERIA_CREATE_DATE_DAYS_AGO;
267             } else if (isTitleOnly) {
268                 // If the document title is the only field which was entered, we want to set the "from" date to be X
269                 // days ago.  This will allow for a more efficient query.
270                 defaultCreateDateDaysAgoValue = KewApiConstants.DOCUMENT_SEARCH_DOC_TITLE_CREATE_DATE_DAYS_AGO;
271             }
272             if (defaultCreateDateDaysAgoValue != null) {
273                 // add a default create date
274                 MutableDateTime mutableDateTime = new MutableDateTime();
275                 mutableDateTime.addDays(defaultCreateDateDaysAgoValue.intValue());
276                 criteriaBuilder.setDateCreatedFrom(mutableDateTime.toDateTime());
277             }
278             criteria = criteriaBuilder.build();
279         }
280         return criteria;
281     }
282 
283     protected DocumentSearchCriteria.Builder createEmptyComparisonCriteria(DocumentSearchCriteria criteria) {
284         DocumentSearchCriteria.Builder builder = DocumentSearchCriteria.Builder.create();
285         // copy over the fields that shouldn't be considered when determining if the criteria is empty
286         builder.setSaveName(criteria.getSaveName());
287         builder.setStartAtIndex(criteria.getStartAtIndex());
288         builder.setMaxResults(criteria.getMaxResults());
289         builder.setIsAdvancedSearch(criteria.getIsAdvancedSearch());
290         return builder;
291     }
292 
293     protected List<RemotableAttributeField> determineSearchFields(DocumentType documentType) {
294         List<RemotableAttributeField> searchFields = new ArrayList<RemotableAttributeField>();
295         if (documentType != null) {
296             DocumentSearchCriteriaConfiguration searchConfiguration =
297                     getDocumentSearchCustomizationMediator().getDocumentSearchCriteriaConfiguration(documentType);
298             if (searchConfiguration != null) {
299                 List<AttributeFields> attributeFields = searchConfiguration.getSearchAttributeFields();
300                 if (attributeFields != null) {
301                     for (AttributeFields fields : attributeFields) {
302                         searchFields.addAll(fields.getRemotableAttributeFields());
303                     }
304                 }
305             }
306         }
307         return searchFields;
308     }
309 
310     public DocumentSearchGenerator getStandardDocumentSearchGenerator() {
311 	String searchGeneratorClass = ConfigContext.getCurrentContextConfig().getProperty(KewApiConstants.STANDARD_DOC_SEARCH_GENERATOR_CLASS_CONFIG_PARM);
312 	if (searchGeneratorClass == null){
313 	    return new DocumentSearchGeneratorImpl();
314 	}
315     	return (DocumentSearchGenerator)GlobalResourceLoader.getObject(new ObjectDefinition(searchGeneratorClass));
316     }
317 
318     @Override
319     public void validateDocumentSearchCriteria(DocumentSearchGenerator docSearchGenerator, DocumentSearchCriteria.Builder criteria) {
320         List<WorkflowServiceError> errors = this.validateWorkflowDocumentSearchCriteria(criteria);
321         List<RemotableAttributeError> searchAttributeErrors = docSearchGenerator.validateSearchableAttributes(criteria);
322         if (!CollectionUtils.isEmpty(searchAttributeErrors)) {
323             // attribute errors are fully materialized error messages, so the only "key" that makes sense is to use "error.custom"
324             for (RemotableAttributeError searchAttributeError : searchAttributeErrors) {
325                 for (String errorMessage : searchAttributeError.getErrors()) {
326                     WorkflowServiceError error = new WorkflowServiceErrorImpl(errorMessage, "error.custom", errorMessage);
327                     errors.add(error);
328                 }
329             }
330         }
331         if (!errors.isEmpty() || !GlobalVariables.getMessageMap().hasNoErrors()) {
332             throw new WorkflowServiceErrorException("Document Search Validation Errors", errors);
333         }
334     }
335 
336     protected List<WorkflowServiceError> validateWorkflowDocumentSearchCriteria(DocumentSearchCriteria.Builder criteria) {
337         List<WorkflowServiceError> errors = new ArrayList<WorkflowServiceError>();
338 
339         // trim the principal names, validation isn't really necessary, because if not found, no results will be
340         // returned.
341         criteria.setApproverPrincipalName(trimCriteriaValue(criteria.getApproverPrincipalName()));
342         criteria.setViewerPrincipalName(trimCriteriaValue(criteria.getViewerPrincipalName()));
343         criteria.setInitiatorPrincipalName(trimCriteriaValue(criteria.getInitiatorPrincipalName()));
344         validateGroupCriteria(criteria, errors);
345         criteria.setDocumentId(criteria.getDocumentId());
346 
347         // validate any dates
348         boolean compareDatePairs = true;
349         if (criteria.getDateCreatedFrom() == null) {
350             compareDatePairs = false;
351         }
352         else {
353             if (!validateDate("dateCreatedFrom", criteria.getDateCreatedFrom().toString(), "dateCreatedFrom")) {
354                 compareDatePairs = false;
355             } else {
356                 criteria.setDateCreatedFrom(criteria.getDateCreatedFrom());
357             }
358         }
359         if (criteria.getDateCreatedTo() == null) {
360              compareDatePairs = false;
361         }
362         else {
363             if (!validateDate("dateCreatedTo", criteria.getDateCreatedTo().toString(), "dateCreatedTo")) {
364                 compareDatePairs = false;
365             } else {
366                 criteria.setDateCreatedTo(criteria.getDateCreatedTo());
367             }
368         }
369         if (compareDatePairs) {
370             if (!checkDateRanges(new SimpleDateFormat("MM/dd/yyyy").format(criteria.getDateCreatedFrom().toDate()), new SimpleDateFormat("MM/dd/yyyy").format(criteria.getDateCreatedTo().toDate()))) {
371                 errors.add(new WorkflowServiceErrorImpl("The Date Created From (Date Created) must not have a \"From\" date that occurs after the \"To\" date.", "docsearch.DocumentSearchService.dateCreatedRange"));
372             }
373         }
374 
375         compareDatePairs = true;
376         if (criteria.getDateApprovedFrom() == null) {
377             compareDatePairs = false;
378         }
379         else {
380             if (!validateDate("dateApprovedFrom", criteria.getDateApprovedFrom().toString(), "dateApprovedFrom")) {
381                 compareDatePairs = false;
382             } else {
383                 criteria.setDateApprovedFrom(criteria.getDateApprovedFrom());
384             }
385         }
386         if (criteria.getDateApprovedTo() == null) {
387             compareDatePairs = false;
388         }
389         else {
390             if (!validateDate("dateApprovedTo", criteria.getDateApprovedTo().toString(), "dateApprovedTo")) {
391                 compareDatePairs = false;
392             } else {
393                 criteria.setDateApprovedTo(criteria.getDateApprovedTo());
394             }
395         }
396         if (compareDatePairs) {
397             if (!checkDateRanges(new SimpleDateFormat("MM/dd/yyyy").format(criteria.getDateApprovedFrom().toDate()), new SimpleDateFormat("MM/dd/yyyy").format(criteria.getDateApprovedTo().toDate()))) {
398             	errors.add(new WorkflowServiceErrorImpl("The Date Approved From (Date Approved) must not have a \"From\" date that occurs after the \"To\" date.", "docsearch.DocumentSearchService.dateApprovedRange"));
399             }
400         }
401 
402         compareDatePairs = true;
403         if (criteria.getDateFinalizedFrom() == null) {
404             compareDatePairs = false;
405         }
406         else {
407             if (!validateDate("dateFinalizedFrom", criteria.getDateFinalizedFrom().toString(), "dateFinalizedFrom")) {
408                 compareDatePairs = false;
409             } else {
410                 criteria.setDateFinalizedFrom(criteria.getDateFinalizedFrom());
411             }
412         }
413         if (criteria.getDateFinalizedTo() == null) {
414             compareDatePairs = false;
415         }
416         else {
417             if (!validateDate("dateFinalizedTo", criteria.getDateFinalizedTo().toString(), "dateFinalizedTo")) {
418                 compareDatePairs = false;
419             } else {
420                 criteria.setDateFinalizedTo(criteria.getDateFinalizedTo());
421             }
422         }
423         if (compareDatePairs) {
424             if (!checkDateRanges(new SimpleDateFormat("MM/dd/yyyy").format(criteria.getDateFinalizedFrom().toDate()), new SimpleDateFormat("MM/dd/yyyy").format(criteria.getDateFinalizedTo().toDate()))) {
425             	errors.add(new WorkflowServiceErrorImpl("The Date Finalized From (Date Finalized) must not have a \"From\" date that occurs after the \"To\" date.", "docsearch.DocumentSearchService.dateFinalizedRange"));
426             }
427         }
428 
429         compareDatePairs = true;
430         if (criteria.getDateLastModifiedFrom() == null) {
431             compareDatePairs = false;
432         }
433         else {
434             if (!validateDate("dateLastModifiedFrom", criteria.getDateLastModifiedFrom().toString(), "dateLastModifiedFrom")) {
435                 compareDatePairs = false;
436             } else {
437                 criteria.setDateLastModifiedFrom(criteria.getDateLastModifiedFrom());
438             }
439         }
440         if (criteria.getDateLastModifiedTo() == null) {
441             compareDatePairs = false;
442         }
443         else {
444             if (!validateDate("dateLastModifiedTo", criteria.getDateLastModifiedTo().toString(), "dateLastModifiedTo")) {
445                 compareDatePairs = false;
446             } else {
447                 criteria.setDateLastModifiedTo(criteria.getDateLastModifiedTo());
448             }
449         }
450         if (compareDatePairs) {
451             if (!checkDateRanges(new SimpleDateFormat("MM/dd/yyyy").format(criteria.getDateLastModifiedFrom().toDate()), new SimpleDateFormat("MM/dd/yyyy").format(criteria.getDateLastModifiedTo().toDate()))) {
452                 errors.add(new WorkflowServiceErrorImpl("The Date Last Modified From (Date Last Modified) must not have a \"From\" date that occurs after the \"To\" date.", "docsearch.DocumentSearchService.dateLastModifiedRange"));
453             }
454         }
455         return errors;
456     }
457 
458     private boolean validateDate(String dateFieldName, String dateFieldValue, String dateFieldErrorKey) {
459 		// Validates the date format via the dictionary validation service. If validation fails, the validation service adds an error to the message map.
460 		int oldErrorCount = GlobalVariables.getMessageMap().getErrorCount();
461 		getDictionaryValidationService().validateAttributeFormat(DOC_SEARCH_CRITERIA_CLASS, dateFieldName, dateFieldValue, DATA_TYPE_DATE, dateFieldErrorKey);
462 		return (GlobalVariables.getMessageMap().getErrorCount() <= oldErrorCount);
463 	}
464 
465     public static DictionaryValidationService getDictionaryValidationService() {
466 		if (dictionaryValidationService == null) {
467 			dictionaryValidationService = KNSServiceLocator.getDictionaryValidationService();
468 		}
469 		return dictionaryValidationService;
470 	}
471 
472     public static DataDictionaryService getDataDictionaryService() {
473 		if (dataDictionaryService == null) {
474 			dataDictionaryService = KNSServiceLocator.getDataDictionaryService();
475 		}
476 		return dataDictionaryService;
477 	}
478 
479     private boolean checkDateRanges(String fromDate, String toDate) {
480 		return Utilities.checkDateRanges(fromDate, toDate);
481 	}
482     private String trimCriteriaValue(String criteriaValue) {
483         if (StringUtils.isNotBlank(criteriaValue)) {
484             criteriaValue = criteriaValue.trim();
485         }
486         if (StringUtils.isBlank(criteriaValue)) {
487             return null;
488         }
489         return criteriaValue;
490     }
491 
492     private void validateGroupCriteria(DocumentSearchCriteria.Builder criteria, List<WorkflowServiceError> errors) {
493         if (StringUtils.isNotBlank(criteria.getGroupViewerId())) {
494             Group group = KimApiServiceLocator.getGroupService().getGroup(criteria.getGroupViewerId());
495             if (group == null) {
496                 errors.add(new WorkflowServiceErrorImpl("Workgroup Viewer Name is not a workgroup", "docsearch.DocumentSearchService.workgroup.viewer"));
497             }
498         } else {
499             criteria.setGroupViewerId(null);
500         }
501     }
502 
503     @Override
504 	public List<KeyValue> getNamedSearches(String principalId) {
505 		List<UserOptions> namedSearches = userOptionsService.findByUserQualified(principalId, NAMED_SEARCH_ORDER_BASE + "%");
506 		List<KeyValue> sortedNamedSearches = new ArrayList<KeyValue>(0);
507 		if (!namedSearches.isEmpty()) {
508 			Collections.sort(namedSearches);
509 			for (UserOptions namedSearch : namedSearches) {
510 				KeyValue keyValue = new ConcreteKeyValue(namedSearch.getOptionId(), namedSearch.getOptionId().substring(NAMED_SEARCH_ORDER_BASE.length(), namedSearch.getOptionId().length()));
511 				sortedNamedSearches.add(keyValue);
512 			}
513 		}
514 		return sortedNamedSearches;
515 	}
516 
517     @Override
518 	public List<KeyValue> getMostRecentSearches(String principalId) {
519 		UserOptions order = userOptionsService.findByOptionId(LAST_SEARCH_ORDER_OPTION, principalId);
520 		List<KeyValue> sortedMostRecentSearches = new ArrayList<KeyValue>();
521 		if (order != null && order.getOptionVal() != null && !"".equals(order.getOptionVal())) {
522 			List<UserOptions> mostRecentSearches = userOptionsService.findByUserQualified(principalId, LAST_SEARCH_BASE_NAME + "%");
523 			String[] ordered = order.getOptionVal().split(",");
524             for (String anOrdered : ordered) {
525                 UserOptions matchingOption = null;
526                 for (UserOptions option : mostRecentSearches) {
527                     if (anOrdered.equals(option.getOptionId())) {
528                         matchingOption = option;
529                         break;
530                     }
531                 }
532                 if (matchingOption != null) {
533                     DocumentSearchCriteria matchingCriteria = getCriteriaFromSavedSearch(matchingOption);
534                 	sortedMostRecentSearches.add(new ConcreteKeyValue(anOrdered, getSavedSearchAbbreviatedString(matchingCriteria)));
535                 }
536             }
537 		}
538 		return sortedMostRecentSearches;
539 	}
540 
541     public DocumentSearchCriteria clearCriteria(DocumentType documentType, DocumentSearchCriteria criteria) {
542         DocumentSearchCriteria clearedCriteria = getDocumentSearchCustomizationMediator().customizeClearCriteria(
543                 documentType, criteria);
544         if (clearedCriteria == null) {
545             clearedCriteria = getStandardDocumentSearchGenerator().clearSearch(criteria);
546         }
547         return clearedCriteria;
548     }
549 
550     protected String getSavedSearchAbbreviatedString(DocumentSearchCriteria criteria) {
551         Map<String, String> abbreviatedStringMap = new LinkedHashMap<String, String>();
552         addAbbreviatedString(abbreviatedStringMap, "Doc Type", criteria.getDocumentTypeName());
553         addAbbreviatedString(abbreviatedStringMap, "Initiator", criteria.getInitiatorPrincipalName());
554         addAbbreviatedString(abbreviatedStringMap, "Doc Id", criteria.getDocumentId());
555         addAbbreviatedRangeString(abbreviatedStringMap, "Created", criteria.getDateCreatedFrom(),
556                 criteria.getDateCreatedTo());
557         addAbbreviatedString(abbreviatedStringMap, "Title", criteria.getTitle());
558         addAbbreviatedString(abbreviatedStringMap, "App Doc Id", criteria.getApplicationDocumentId());
559         addAbbreviatedRangeString(abbreviatedStringMap, "Approved", criteria.getDateApprovedFrom(),
560                 criteria.getDateApprovedTo());
561         addAbbreviatedRangeString(abbreviatedStringMap, "Modified", criteria.getDateLastModifiedFrom(), criteria.getDateLastModifiedTo());
562         addAbbreviatedRangeString(abbreviatedStringMap, "Finalized", criteria.getDateFinalizedFrom(), criteria.getDateFinalizedTo());
563         addAbbreviatedRangeString(abbreviatedStringMap, "App Doc Status Changed", criteria.getDateApplicationDocumentStatusChangedFrom(), criteria.getDateApplicationDocumentStatusChangedTo());
564         addAbbreviatedString(abbreviatedStringMap, "Approver", criteria.getApproverPrincipalName());
565         addAbbreviatedString(abbreviatedStringMap, "Viewer", criteria.getViewerPrincipalName());
566         addAbbreviatedString(abbreviatedStringMap, "Group Viewer", criteria.getGroupViewerId());
567         addAbbreviatedString(abbreviatedStringMap, "Node", criteria.getRouteNodeName());
568         addAbbreviatedMultiValuedString(abbreviatedStringMap, "Status", criteria.getDocumentStatuses());
569         addAbbreviatedMultiValuedString(abbreviatedStringMap, "Category", criteria.getDocumentStatusCategories());
570         for (String documentAttributeName : criteria.getDocumentAttributeValues().keySet()) {
571             addAbbreviatedMultiValuedString(abbreviatedStringMap, documentAttributeName, criteria.getDocumentAttributeValues().get(documentAttributeName));
572         }
573         StringBuilder stringBuilder = new StringBuilder();
574         int iteration = 0;
575         for (String label : abbreviatedStringMap.keySet()) {
576             stringBuilder.append(label).append("=").append(abbreviatedStringMap.get(label));
577             if (iteration < abbreviatedStringMap.keySet().size()) {
578                 stringBuilder.append("; ");
579             }
580         }
581         return stringBuilder.toString();
582     }
583 
584     protected void addAbbreviatedString(Map<String, String> abbreviatedStringMap, String label, String value) {
585         if (StringUtils.isNotBlank(value)) {
586             abbreviatedStringMap.put(label, value);
587         }
588     }
589 
590     protected void addAbbreviatedMultiValuedString(Map<String, String> abbreviatedStringMap, String label, Collection<? extends Object> values) {
591         if (CollectionUtils.isNotEmpty(values)) {
592             List<String> stringValues = new ArrayList<String>();
593             for (Object value : values) {
594                 stringValues.add(value.toString());
595             }
596             abbreviatedStringMap.put(label, StringUtils.join(stringValues, ","));
597         }
598     }
599 
600     protected void addAbbreviatedRangeString(Map<String, String> abbreviatedStringMap, String label, DateTime dateFrom, DateTime dateTo) {
601         if (dateFrom != null || dateTo != null) {
602             StringBuilder abbreviatedString = new StringBuilder();
603             if (dateFrom != null) {
604                 abbreviatedString.append(CoreApiServiceLocator.getDateTimeService().toDateString(dateFrom.toDate()));
605             }
606             abbreviatedString.append("..");
607             if (dateTo != null) {
608                 abbreviatedString.append(CoreApiServiceLocator.getDateTimeService().toDateString(dateTo.toDate()));
609             }
610             abbreviatedStringMap.put(label, abbreviatedString.toString());
611         }
612     }
613 
614     /**
615      * Saves a DocumentSearchCriteria into the UserOptions.  This method operates in one of two ways:
616      * 1) The search is named: the criteria is saved under NAMED_SEARCH_ORDER_BASE + <name>
617      * 2) The search is unnamed: the criteria is given a name that indicates its order, which is saved in a second user option
618      *    which contains a list of these names comprising recent searches
619      * @param principalId the user to save the criteria under
620      * @param criteria the doc lookup criteria
621      */
622     private void saveSearch(String principalId, DocumentSearchCriteria criteria) {
623         if (StringUtils.isBlank(principalId)) {
624             return;
625         }
626 
627         try {
628             String savedSearchString = DocumentSearchInternalUtils.marshalDocumentSearchCriteria(criteria);
629 
630             if (StringUtils.isNotBlank(criteria.getSaveName())) {
631                 userOptionsService.save(principalId, NAMED_SEARCH_ORDER_BASE + criteria.getSaveName(), savedSearchString);
632             } else {
633                 // first determine the current ordering
634                 UserOptions searchOrder = userOptionsService.findByOptionId(LAST_SEARCH_ORDER_OPTION, principalId);
635                 // no previous searches, save under first id
636                 if (searchOrder == null) {
637                     userOptionsService.save(principalId, LAST_SEARCH_BASE_NAME + "0", savedSearchString);
638                     userOptionsService.save(principalId, LAST_SEARCH_ORDER_OPTION, LAST_SEARCH_BASE_NAME + "0");
639                 } else {
640                     String[] currentOrder = searchOrder.getOptionVal().split(",");
641                     // we have reached MAX_SEARCH_ITEMS
642                     if (currentOrder.length == MAX_SEARCH_ITEMS) {
643                         // move the last item to the front of the list, and save
644                         // over this key with the new criteria
645                         // [5,4,3,2,1] => [1,5,4,3,2]
646                         String searchName = currentOrder[currentOrder.length - 1];
647                         String[] newOrder = new String[MAX_SEARCH_ITEMS];
648                         newOrder[0] = searchName;
649                         for (int i = 0; i < currentOrder.length - 1; i++) {
650                             newOrder[i + 1] = currentOrder[i];
651                         }
652                         // rejoins items with comma separator...
653                         String newSearchOrder = "";
654                         for (String aNewOrder : newOrder) {
655                             if (!"".equals(newSearchOrder)) {
656                                 newSearchOrder += ",";
657                             }
658                             newSearchOrder += aNewOrder;
659                         }
660                         // save the search string under the searchName (which used to be the last name in the list)
661                         userOptionsService.save(principalId, searchName, savedSearchString);
662                         userOptionsService.save(principalId, LAST_SEARCH_ORDER_OPTION, newSearchOrder);
663                     } else {
664                         // saves the search to the front of the list with incremented index
665                         // [3,2,1] => [4,3,2,1]
666                         // here we need to do a push to identify the highest used number which is from the
667                         // first one in the array, and then add one to it, and push the rest back one
668                         int absMax = 0;
669                         for (String aCurrentOrder : currentOrder) {
670                             int current = new Integer(aCurrentOrder.substring(LAST_SEARCH_BASE_NAME.length(),
671                                     aCurrentOrder.length()));
672                             if (current > absMax) {
673                                 absMax = current;
674                             }
675                         }
676                         String searchName = LAST_SEARCH_BASE_NAME + ++absMax;
677                         String[] newOrder = new String[currentOrder.length + 1];
678                         newOrder[0] = searchName;
679                         for (int i = 0; i < currentOrder.length; i++) {
680                             newOrder[i + 1] = currentOrder[i];
681                         }
682                         String newSearchOrder = "";
683                         for (String aNewOrder : newOrder) {
684                             if (!"".equals(newSearchOrder)) {
685                                 newSearchOrder += ",";
686                             }
687                             newSearchOrder += aNewOrder;
688                         }
689                         userOptionsService.save(principalId, searchName, savedSearchString);
690                         userOptionsService.save(principalId, LAST_SEARCH_ORDER_OPTION, newSearchOrder);
691                     }
692                 }
693             }
694         } catch (Exception e) {
695             // we don't want the failure when saving a search to affect the ability of the document search to succeed
696             // and return it's results, so just log and return
697             LOG.error("Unable to save search due to exception", e);
698         }
699     }
700 
701 	public ConfigurationService getKualiConfigurationService() {
702 		if (kualiConfigurationService == null) {
703 			kualiConfigurationService = KRADServiceLocator.getKualiConfigurationService();
704 		}
705 		return kualiConfigurationService;
706 	}
707 
708 }