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