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