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