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