001    /**
002     * Copyright 2005-2014 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.kew.docsearch.service.impl;
017    
018    import org.apache.commons.collections.CollectionUtils;
019    import org.apache.commons.lang.StringUtils;
020    import org.joda.time.DateTime;
021    import org.joda.time.MutableDateTime;
022    import org.kuali.rice.core.api.CoreApiServiceLocator;
023    import org.kuali.rice.core.api.config.property.ConfigContext;
024    import org.kuali.rice.core.api.config.property.ConfigurationService;
025    import org.kuali.rice.core.api.reflect.ObjectDefinition;
026    import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
027    import org.kuali.rice.core.api.uif.RemotableAttributeError;
028    import org.kuali.rice.core.api.uif.RemotableAttributeField;
029    import org.kuali.rice.core.api.util.ConcreteKeyValue;
030    import org.kuali.rice.core.api.util.KeyValue;
031    import org.kuali.rice.kew.api.KewApiConstants;
032    import org.kuali.rice.kew.api.WorkflowRuntimeException;
033    import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
034    import org.kuali.rice.kew.api.document.attribute.DocumentAttributeFactory;
035    import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
036    import org.kuali.rice.kew.api.document.search.DocumentSearchResult;
037    import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
038    import org.kuali.rice.kew.docsearch.DocumentSearchCustomizationMediator;
039    import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
040    import org.kuali.rice.kew.docsearch.dao.DocumentSearchDAO;
041    import org.kuali.rice.kew.docsearch.service.DocumentSearchService;
042    import org.kuali.rice.kew.doctype.SecuritySession;
043    import org.kuali.rice.kew.doctype.bo.DocumentType;
044    import org.kuali.rice.kew.exception.WorkflowServiceError;
045    import org.kuali.rice.kew.exception.WorkflowServiceErrorException;
046    import org.kuali.rice.kew.exception.WorkflowServiceErrorImpl;
047    import org.kuali.rice.kew.framework.document.search.AttributeFields;
048    import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration;
049    import org.kuali.rice.kew.framework.document.search.DocumentSearchResultValue;
050    import org.kuali.rice.kew.framework.document.search.DocumentSearchResultValues;
051    import org.kuali.rice.kew.impl.document.search.DocumentSearchGenerator;
052    import org.kuali.rice.kew.impl.document.search.DocumentSearchGeneratorImpl;
053    import org.kuali.rice.kew.service.KEWServiceLocator;
054    import org.kuali.rice.kew.useroptions.UserOptions;
055    import org.kuali.rice.kew.useroptions.UserOptionsService;
056    import org.kuali.rice.kew.util.Utilities;
057    import org.kuali.rice.kim.api.group.Group;
058    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
059    import org.kuali.rice.kns.service.DictionaryValidationService;
060    import org.kuali.rice.kns.service.DataDictionaryService;
061    import org.kuali.rice.kns.service.KNSServiceLocator;
062    import org.kuali.rice.krad.util.GlobalVariables;
063    
064    import java.io.IOException;
065    import java.text.SimpleDateFormat;
066    import java.util.ArrayList;
067    import java.util.Collection;
068    import java.util.Collections;
069    import java.util.HashMap;
070    import java.util.HashSet;
071    import java.util.LinkedHashMap;
072    import java.util.List;
073    import java.util.Map;
074    import java.util.Set;
075    
076    public class DocumentSearchServiceImpl implements DocumentSearchService {
077    
078            private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DocumentSearchServiceImpl.class);
079    
080            private static final int MAX_SEARCH_ITEMS = 5;
081            private static final String LAST_SEARCH_ORDER_OPTION = "DocSearch.LastSearch.Order";
082            private static final String NAMED_SEARCH_ORDER_BASE = "DocSearch.NamedSearch.";
083            private static final String LAST_SEARCH_BASE_NAME = "DocSearch.LastSearch.Holding";
084        private static final String DOC_SEARCH_CRITERIA_CLASS = "org.kuali.rice.kew.api.document.search.DocumentSearchCriteria";
085        private static final String DATA_TYPE_DATE = "datetime";
086    
087            private volatile ConfigurationService kualiConfigurationService;
088        private DocumentSearchCustomizationMediator documentSearchCustomizationMediator;
089    
090            private DocumentSearchDAO docSearchDao;
091            private UserOptionsService userOptionsService;
092    
093        private static DictionaryValidationService dictionaryValidationService;
094        private static DataDictionaryService dataDictionaryService;
095    
096            public void setDocumentSearchDAO(DocumentSearchDAO docSearchDao) {
097                    this.docSearchDao = docSearchDao;
098            }
099    
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 = new ArrayList<UserOptions>(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    }