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