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