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.WorkflowRuntimeException;
033import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
034import org.kuali.rice.kew.api.document.attribute.DocumentAttributeFactory;
035import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
036import org.kuali.rice.kew.api.document.search.DocumentSearchResult;
037import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
038import org.kuali.rice.kew.docsearch.DocumentSearchCustomizationMediator;
039import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
040import org.kuali.rice.kew.docsearch.dao.DocumentSearchDAO;
041import org.kuali.rice.kew.docsearch.service.DocumentSearchService;
042import org.kuali.rice.kew.doctype.SecuritySession;
043import org.kuali.rice.kew.doctype.bo.DocumentType;
044import org.kuali.rice.kew.exception.WorkflowServiceError;
045import org.kuali.rice.kew.exception.WorkflowServiceErrorException;
046import org.kuali.rice.kew.exception.WorkflowServiceErrorImpl;
047import org.kuali.rice.kew.framework.document.search.AttributeFields;
048import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration;
049import org.kuali.rice.kew.framework.document.search.DocumentSearchResultValue;
050import org.kuali.rice.kew.framework.document.search.DocumentSearchResultValues;
051import org.kuali.rice.kew.impl.document.search.DocumentSearchGenerator;
052import org.kuali.rice.kew.impl.document.search.DocumentSearchGeneratorImpl;
053import org.kuali.rice.kew.service.KEWServiceLocator;
054import org.kuali.rice.kew.useroptions.UserOptions;
055import org.kuali.rice.kew.useroptions.UserOptionsService;
056import org.kuali.rice.kew.util.Utilities;
057import org.kuali.rice.kim.api.group.Group;
058import org.kuali.rice.kim.api.services.KimApiServiceLocator;
059import org.kuali.rice.kns.service.DictionaryValidationService;
060import org.kuali.rice.kns.service.DataDictionaryService;
061import org.kuali.rice.kns.service.KNSServiceLocator;
062import org.kuali.rice.krad.util.GlobalVariables;
063
064import java.io.IOException;
065import java.text.SimpleDateFormat;
066import java.util.ArrayList;
067import java.util.Collection;
068import java.util.Collections;
069import java.util.HashMap;
070import java.util.HashSet;
071import java.util.LinkedHashMap;
072import java.util.List;
073import java.util.Map;
074import java.util.Set;
075
076public 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 = 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}