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 }