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