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