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