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