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