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.impl.document.search; 017 018import org.apache.commons.beanutils.PropertyUtils; 019import org.apache.commons.lang.ArrayUtils; 020import org.apache.commons.lang.BooleanUtils; 021import org.apache.commons.lang.ObjectUtils; 022import org.apache.commons.lang.StringUtils; 023import org.joda.time.DateTime; 024import org.kuali.rice.core.api.CoreApiServiceLocator; 025import org.kuali.rice.core.api.config.property.Config; 026import org.kuali.rice.core.api.config.property.ConfigContext; 027import org.kuali.rice.core.api.search.SearchOperator; 028import org.kuali.rice.core.api.uif.RemotableAttributeField; 029import org.kuali.rice.core.api.util.KeyValue; 030import org.kuali.rice.core.api.util.RiceKeyConstants; 031import org.kuali.rice.core.api.util.type.KualiDecimal; 032import org.kuali.rice.core.api.util.type.KualiPercent; 033import org.kuali.rice.core.web.format.Formatter; 034import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator; 035import org.kuali.rice.kew.api.KEWPropertyConstants; 036import org.kuali.rice.kew.api.KewApiConstants; 037import org.kuali.rice.kew.api.document.attribute.DocumentAttribute; 038import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria; 039import org.kuali.rice.kew.api.document.search.DocumentSearchCriteriaContract; 040import org.kuali.rice.kew.api.document.search.DocumentSearchResult; 041import org.kuali.rice.kew.api.document.search.DocumentSearchResults; 042import org.kuali.rice.kew.docsearch.DocumentSearchCriteriaProcessor; 043import org.kuali.rice.kew.docsearch.DocumentSearchCriteriaProcessorKEWAdapter; 044import org.kuali.rice.kew.docsearch.service.DocumentSearchService; 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.framework.document.search.DocumentSearchCriteriaConfiguration; 049import org.kuali.rice.kew.framework.document.search.DocumentSearchResultSetConfiguration; 050import org.kuali.rice.kew.framework.document.search.StandardResultField; 051import org.kuali.rice.kew.impl.document.search.DocumentSearchCriteriaBo; 052import org.kuali.rice.kew.impl.document.search.DocumentSearchCriteriaTranslator; 053import org.kuali.rice.kew.impl.document.search.FormFields; 054import org.kuali.rice.kew.lookup.valuefinder.SavedSearchValuesFinder; 055import org.kuali.rice.kew.service.KEWServiceLocator; 056import org.kuali.rice.kew.user.UserUtils; 057import org.kuali.rice.kim.api.identity.Person; 058import org.kuali.rice.kns.datadictionary.BusinessObjectEntry; 059import org.kuali.rice.kns.lookup.HtmlData; 060import org.kuali.rice.kns.lookup.KualiLookupableHelperServiceImpl; 061import org.kuali.rice.kns.lookup.LookupUtils; 062import org.kuali.rice.kns.util.FieldUtils; 063import org.kuali.rice.kns.web.struts.form.LookupForm; 064import org.kuali.rice.kns.web.ui.Column; 065import org.kuali.rice.kns.web.ui.Field; 066import org.kuali.rice.kns.web.ui.ResultRow; 067import org.kuali.rice.kns.web.ui.Row; 068import org.kuali.rice.krad.UserSession; 069import org.kuali.rice.krad.bo.BusinessObject; 070import org.kuali.rice.krad.exception.ValidationException; 071import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 072import org.kuali.rice.krad.util.GlobalVariables; 073import org.kuali.rice.krad.util.KRADConstants; 074 075import java.lang.reflect.InvocationTargetException; 076import java.math.BigDecimal; 077import java.text.MessageFormat; 078import java.util.ArrayList; 079import java.util.Collection; 080import java.util.Collections; 081import java.util.HashMap; 082import java.util.List; 083import java.util.Map; 084import java.util.regex.Matcher; 085import java.util.regex.Pattern; 086 087/** 088 * Implementation of lookupable helper service which handles the complex lookup behavior required by the KEW 089 * document search screen. 090 * 091 * @author Kuali Rice Team (rice.collab@kuali.org) 092 */ 093public class DocumentSearchCriteriaBoLookupableHelperService extends KualiLookupableHelperServiceImpl { 094 095 static final String SAVED_SEARCH_NAME_PARAM = "savedSearchToLoadAndExecute"; 096 static final String DOCUMENT_TYPE_NAME_PARAM = "documentTypeName"; 097 098 // warning message keys 099 100 private static final String EXCEED_THRESHOLD_MESSAGE_KEY = "docsearch.DocumentSearchService.exceededThreshold"; 101 private static final String SECURITY_FILTERED_MESSAGE_KEY = "docsearch.DocumentSearchService.securityFiltered"; 102 private static final String EXCEED_THRESHOLD_AND_SECURITY_FILTERED_MESSAGE_KEY = "docsearch.DocumentSearchService.exceededThresholdAndSecurityFiltered"; 103 104 private static final boolean DOCUMENT_HANDLER_POPUP_DEFAULT = true; 105 private static final boolean ROUTE_LOG_POPUP_DEFAULT = true; 106 107 // injected services 108 109 private DocumentSearchService documentSearchService; 110 private DocumentSearchCriteriaProcessor documentSearchCriteriaProcessor; 111 private DocumentSearchCriteriaTranslator documentSearchCriteriaTranslator; 112 113 // These two fields are *only* used to pass side-channel information across the superclass API boundary between 114 // performLookup and getSearchResultsHelper. 115 // (in theory these could be replaced with some threadlocal subterfuge, but keeping as-is for simplicity) 116 private DocumentSearchResults searchResults = null; 117 private DocumentSearchCriteria criteria = null; 118 119 @Override 120 protected List<? extends BusinessObject> getSearchResultsHelper(Map<String, String> fieldValues, boolean unbounded) { 121 criteria = loadCriteria(fieldValues); 122 searchResults = null; 123 try { 124 searchResults = KEWServiceLocator.getDocumentSearchService().lookupDocuments(GlobalVariables.getUserSession().getPrincipalId(), criteria); 125 if (searchResults.isCriteriaModified()) { 126 criteria = searchResults.getCriteria(); 127 } 128 } catch (WorkflowServiceErrorException wsee) { 129 for (WorkflowServiceError workflowServiceError : (List<WorkflowServiceError>) wsee.getServiceErrors()) { 130 if (workflowServiceError.getMessageMap() != null && workflowServiceError.getMessageMap().hasErrors()) { 131 // merge the message maps 132 GlobalVariables.getMessageMap().merge(workflowServiceError.getMessageMap()); 133 } else { 134 GlobalVariables.getMessageMap().putError(workflowServiceError.getMessage(), RiceKeyConstants.ERROR_CUSTOM, workflowServiceError.getMessage()); 135 } 136 } 137 } 138 139 if (!GlobalVariables.getMessageMap().hasNoErrors() || searchResults == null) { 140 throw new ValidationException("error with doc search"); 141 } 142 143 populateResultWarningMessages(searchResults); 144 145 List<DocumentSearchResult> individualSearchResults = searchResults.getSearchResults(); 146 147 setBackLocation(fieldValues.get(KRADConstants.BACK_LOCATION)); 148 setDocFormKey(fieldValues.get(KRADConstants.DOC_FORM_KEY)); 149 150 applyCriteriaChangesToFields(criteria); 151 152 return populateSearchResults(individualSearchResults); 153 154 } 155 156 /** 157 * Inspects the lookup results to determine if any warning messages should be published to the message map. 158 */ 159 protected void populateResultWarningMessages(DocumentSearchResults searchResults) { 160 // check various warning conditions 161 boolean overThreshold = searchResults.isOverThreshold(); 162 int numFiltered = searchResults.getNumberOfSecurityFilteredResults(); 163 int numResults = searchResults.getSearchResults().size(); 164 if (overThreshold && numFiltered > 0) { 165 GlobalVariables.getMessageMap().putWarning(KRADConstants.GLOBAL_MESSAGES, EXCEED_THRESHOLD_AND_SECURITY_FILTERED_MESSAGE_KEY, String.valueOf(numResults), String.valueOf(numFiltered)); 166 } else if (numFiltered > 0) { 167 GlobalVariables.getMessageMap().putWarning(KRADConstants.GLOBAL_MESSAGES, SECURITY_FILTERED_MESSAGE_KEY, String.valueOf(numFiltered)); 168 } else if (overThreshold) { 169 GlobalVariables.getMessageMap().putWarning(KRADConstants.GLOBAL_MESSAGES, EXCEED_THRESHOLD_MESSAGE_KEY, String.valueOf(numResults)); 170 } 171 } 172 173 /** 174 * Applies changes that might have happened to the criteria back to the fields so that they show up on the form. 175 * Namely, this handles populating the form with today's date if the create date was not filled in on the form. 176 */ 177 protected void applyCriteriaChangesToFields(DocumentSearchCriteriaContract criteria) { 178 Field field = getFormFields().getField(KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + "dateCreated"); 179 if (field != null && StringUtils.isEmpty(field.getPropertyValue())) { 180 if (criteria.getDateCreatedFrom() != null) { 181 field.setPropertyValue(CoreApiServiceLocator.getDateTimeService().toDateString(criteria.getDateCreatedFrom().toDate())); 182 } 183 } 184 } 185 186 // CURRENT_USER token pattern: CURRENT_USER(.type) surrounded by positive lookahead/lookbehind for non-alphanum terminal tokens 187 // (to support expression operators) 188 private static final Pattern CURRENT_USER_PATTERN = Pattern.compile("(?<=[\\s\\p{Punct}]|^)CURRENT_USER(\\.\\w+)?(?=[\\s\\p{Punct}]|$)"); 189 190 protected static String replaceCurrentUserToken(String value, Person person) { 191 Matcher matcher = CURRENT_USER_PATTERN.matcher(value); 192 boolean matched = false; 193 StringBuffer sb = new StringBuffer(); 194 while (matcher.find()) { 195 matched = true; 196 String idType = "principalName"; 197 if (matcher.groupCount() > 0) { 198 String group = matcher.group(1); 199 if (group != null) { 200 idType = group.substring(1); // discard period after CURRENT_USER 201 } 202 } 203 String idValue = UserUtils.getIdValue(idType, person); 204 if (!StringUtils.isBlank(idValue)) { 205 value = idValue; 206 } else { 207 value = matcher.group(); 208 } 209 matcher.appendReplacement(sb, value); 210 211 } 212 matcher.appendTail(sb); 213 return matched ? sb.toString() : null; 214 } 215 216 /** 217 * Cleans up various issues with fieldValues coming from the lookup form (namely, that they don't include 218 * multi-valued field values!). Handles these by adding them comma-separated. 219 */ 220 protected static Map<String, String> cleanupFieldValues(Map<String, String> fieldValues, Map<String, String[]> parameters) { 221 Map<String, String> cleanedUpFieldValues = new HashMap<String, String>(fieldValues); 222 if (ArrayUtils.isNotEmpty(parameters.get(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE))) { 223 cleanedUpFieldValues.put(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE, 224 StringUtils.join(parameters.get(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE), ",")); 225 } 226 if (ArrayUtils.isNotEmpty(parameters.get(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_DOC_STATUS))) { 227 cleanedUpFieldValues.put(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_DOC_STATUS, 228 StringUtils.join(parameters.get(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_DOC_STATUS), ",")); 229 } 230 Map<String, String> documentAttributeFieldValues = new HashMap<String, String>(); 231 for (String parameterName : parameters.keySet()) { 232 if (parameterName.contains(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX)) { 233 String[] value = parameters.get(parameterName); 234 if (ArrayUtils.isNotEmpty(value)) { 235 if ( parameters.containsKey(parameterName + KRADConstants.CHECKBOX_PRESENT_ON_FORM_ANNOTATION)) { 236 documentAttributeFieldValues.put(parameterName, "Y"); 237 } else { 238 documentAttributeFieldValues.put(parameterName, StringUtils.join(value, " " + SearchOperator.OR.op() + " ")); 239 } 240 } 241 } 242 } 243 // if any of the document attributes are range values, process them 244 documentAttributeFieldValues.putAll(LookupUtils.preProcessRangeFields(documentAttributeFieldValues)); 245 cleanedUpFieldValues.putAll(documentAttributeFieldValues); 246 247 replaceCurrentUserInFields(cleanedUpFieldValues); 248 249 return cleanedUpFieldValues; 250 } 251 252 protected static void replaceCurrentUserInFields(Map<String, String> fields) { 253 Person person = GlobalVariables.getUserSession().getPerson(); 254 // replace the dynamic CURRENT_USER token 255 for (Map.Entry<String, String> entry: fields.entrySet()) { 256 if (StringUtils.isNotEmpty(entry.getValue())) { 257 String replaced = replaceCurrentUserToken(entry.getValue(), person); 258 if (replaced != null) { 259 entry.setValue(replaced); 260 } 261 } 262 } 263 } 264 265 /** 266 * Loads the document search criteria from the given map of field values as submitted from the search screen, and 267 * populates the current form Rows/Fields with the saved criteria fields 268 */ 269 protected DocumentSearchCriteria loadCriteria(Map<String, String> fieldValues) { 270 fieldValues = cleanupFieldValues(fieldValues, getParameters()); 271 String[] savedSearchToLoad = getParameters().get(SAVED_SEARCH_NAME_PARAM); 272 boolean savedSearch = savedSearchToLoad != null && savedSearchToLoad.length > 0 && StringUtils.isNotBlank(savedSearchToLoad[0]); 273 if (savedSearch) { 274 DocumentSearchCriteria criteria = getDocumentSearchService().getNamedSearchCriteria(GlobalVariables.getUserSession().getPrincipalId(), savedSearchToLoad[0]); 275 if (criteria != null) { 276 getFormFields().setFieldValues(getDocumentSearchCriteriaTranslator().translateCriteriaToFields(criteria)); 277 return criteria; 278 } 279 } 280 // either it wasn't a saved search or the saved search failed to resolve 281 return getDocumentSearchCriteriaTranslator().translateFieldsToCriteria(fieldValues); 282 } 283 284 protected List<DocumentSearchCriteriaBo> populateSearchResults(List<DocumentSearchResult> lookupResults) { 285 List<DocumentSearchCriteriaBo> searchResults = new ArrayList<DocumentSearchCriteriaBo>(); 286 for (DocumentSearchResult searchResult : lookupResults) { 287 DocumentSearchCriteriaBo result = new DocumentSearchCriteriaBo(); 288 result.populateFromDocumentSearchResult(searchResult); 289 searchResults.add(result); 290 } 291 return searchResults; 292 } 293 294 @Override 295 public Collection<? extends BusinessObject> performLookup(LookupForm lookupForm, Collection<ResultRow> resultTable, boolean bounded) { 296 Collection<? extends BusinessObject> lookupResult = super.performLookup(lookupForm, resultTable, bounded); 297 postProcessResults(resultTable, this.searchResults); 298 return lookupResult; 299 } 300 301 /** 302 * Overrides a Field value; sets a fallback/restored value if there is no new value 303 */ 304 protected void overrideFieldValue(Field field, Map<String, String[]> newValues, Map<String, String[]> oldValues) { 305 if (StringUtils.isNotBlank(field.getPropertyName())) { 306 if (newValues.get(field.getPropertyName()) != null) { 307 getFormFields().setFieldValue(field, newValues.get(field.getPropertyName())); 308 } else if (oldValues.get(field.getPropertyName()) != null) { 309 getFormFields().setFieldValue(field, oldValues.get(field.getPropertyName())); 310 } 311 } 312 } 313 314 /** 315 * Handles toggling between form views. 316 * Reads and sets the Rows state. 317 */ 318 protected void toggleFormView() { 319 Map<String,String[]> fieldValues = new HashMap<String,String[]>(); 320 Map<String, String[]> savedValues = getFormFields().getFieldValues(); 321 322 // the original implementation saved the form values and then re-applied them 323 // we do the same here, however I suspect we may be able to avoid this re-application 324 // of existing values 325 326 for (Field field: getFormFields().getFields()) { 327 overrideFieldValue(field, this.getParameters(), savedValues); 328 // if we are sure this does not depend on or cause side effects in other fields 329 // then this phase can be extracted and these loops simplified 330 applyFieldAuthorizationsFromNestedLookups(field); 331 fieldValues.put(field.getPropertyName(), new String[] { field.getPropertyValue() }); 332 } 333 334 // checkForAdditionalFields generates the form (setRows) 335 if (checkForAdditionalFieldsMultiValued(fieldValues)) { 336 for (Field field: getFormFields().getFields()) { 337 overrideFieldValue(field, this.getParameters(), savedValues); 338 fieldValues.put(field.getPropertyName(), new String[] { field.getPropertyValue() }); 339 } 340 } 341 342 // unset the clear search param, since this is not really a state, but just an action 343 // it can never be toggled "off", just "on" 344 getFormFields().setFieldValue(DocumentSearchCriteriaProcessorKEWAdapter.CLEARSAVED_SEARCH_FIELD, ""); 345 } 346 347 /** 348 * Loads a saved search 349 * @return returns true on success to run the loaded search, false on error. 350 */ 351 protected boolean loadSavedSearch(boolean ignoreErrors) { 352 Map<String,String[]> fieldValues = new HashMap<String,String[]>(); 353 354 String savedSearchName = getSavedSearchName(); 355 if(StringUtils.isEmpty(savedSearchName) || "*ignore*".equals(savedSearchName)) { 356 if(!ignoreErrors) { 357 GlobalVariables.getMessageMap().putError(SAVED_SEARCH_NAME_PARAM, RiceKeyConstants.ERROR_CUSTOM, "You must select a saved search"); 358 } else { 359 //if we're ignoring errors and we got an error just return, no reason to continue. Also set false to indicate not to perform lookup 360 return false; 361 } 362 getFormFields().setFieldValue(SAVED_SEARCH_NAME_PARAM, ""); 363 } 364 if (!GlobalVariables.getMessageMap().hasNoErrors()) { 365 throw new ValidationException("errors in search criteria"); 366 } 367 368 DocumentSearchCriteria criteria = KEWServiceLocator.getDocumentSearchService().getSavedSearchCriteria(GlobalVariables.getUserSession().getPrincipalId(), savedSearchName); 369 370 // get the document type 371 String docTypeName = criteria.getDocumentTypeName(); 372 373 // update the parameters to include whether or not this is an advanced search 374 if(this.getParameters().containsKey(KRADConstants.ADVANCED_SEARCH_FIELD)) { 375 Map<String, String[]> parameters = this.getParameters(); 376 String[] params = (String[])parameters.get(KRADConstants.ADVANCED_SEARCH_FIELD); 377 if (ArrayUtils.isNotEmpty(params)) { 378 params[0] = criteria.getIsAdvancedSearch(); 379 this.setParameters(parameters); 380 } 381 } 382 383 // and set the rows based on doc type 384 setRows(docTypeName); 385 386 // clear the name of the search in the form 387 //fieldValues.put(SAVED_SEARCH_NAME_PARAM, new String[0]); 388 389 // set the custom document attribute values on the search form 390 for (Map.Entry<String, List<String>> entry: criteria.getDocumentAttributeValues().entrySet()) { 391 fieldValues.put(entry.getKey(), entry.getValue().toArray(new String[entry.getValue().size()])); 392 } 393 394 // sets the field values on the form, trying criteria object properties if a field value is not present in the map 395 for (Field field : getFormFields().getFields()) { 396 if (field.getPropertyName() != null && !field.getPropertyName().equals("")) { 397 // UI Fields know whether they are single or multiple value 398 // just set both so they can make the determination and render appropriately 399 String[] values = null; 400 if (fieldValues.get(field.getPropertyName()) != null) { 401 values = fieldValues.get(field.getPropertyName()); 402 } else { 403 //may be on the root of the criteria object, try looking there: 404 try { 405 if (field.isRanged() && field.isDatePicker()) { 406 if (field.getPropertyName().startsWith(KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX)) { 407 String lowerBoundName = field.getPropertyName().replace(KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX, "") + "From"; 408 Object lowerBoundDate = PropertyUtils.getProperty(criteria, lowerBoundName); 409 if (lowerBoundDate != null) { 410 values = new String[] { CoreApiServiceLocator.getDateTimeService().toDateTimeString(((org.joda.time.DateTime)lowerBoundDate).toDate()) }; 411 } 412 } else { 413 // the upper bound prefix may or may not be on the propertyName. Using "replace" just in case. 414 String upperBoundName = field.getPropertyName().replace(KRADConstants.LOOKUP_RANGE_UPPER_BOUND_PROPERTY_PREFIX, "") + "To"; 415 Object upperBoundDate = PropertyUtils.getProperty(criteria, upperBoundName); 416 if (upperBoundDate != null) { 417 values = new String[] { CoreApiServiceLocator.getDateTimeService().toDateTimeString( 418 ((org.joda.time.DateTime)upperBoundDate) 419 .toDate()) }; 420 } 421 } 422 } else { 423 values = new String[] { ObjectUtils.toString(PropertyUtils.getProperty(criteria, field.getPropertyName())) }; 424 } 425 } catch (IllegalAccessException e) { 426 e.printStackTrace(); 427 } catch (InvocationTargetException e) { 428 e.printStackTrace(); 429 } catch (NoSuchMethodException e) { 430 // e.printStackTrace(); 431 //hmm what to do here, we should be able to find everything either in the search atts or at the base as far as I know. 432 } 433 } 434 if (values != null) { 435 getFormFields().setFieldValue(field, values); 436 } 437 } 438 } 439 440 return true; 441 } 442 443 /** 444 * Performs custom document search/lookup actions. 445 * 1) switching between simple/detailed search 446 * 2) switching between non-superuser/superuser search 447 * 3) clearing saved search results 448 * 4) restoring a saved search and executing the search 449 * @param ignoreErrors 450 * @return whether to rerun the previous search; false in cases 1-3 because we are just updating the form 451 */ 452 @Override 453 public boolean performCustomAction(boolean ignoreErrors) { 454 //boolean isConfigAction = isAdvancedSearch() || isSuperUserSearch() || isClearSavedSearch(); 455 if (isClearSavedSearch()) { 456 KEWServiceLocator.getDocumentSearchService().clearNamedSearches(GlobalVariables.getUserSession().getPrincipalId()); 457 return false; 458 } 459 else if (getSavedSearchName() != null) { 460 return loadSavedSearch(ignoreErrors); 461 } else { 462 toggleFormView(); 463 // Finally, return false to prevent the search from being performed and to skip the other custom processing below. 464 return false; 465 } 466 } 467 468 /** 469 * Custom implementation of getInquiryUrl that sets up doc handler link. 470 */ 471 @Override 472 public HtmlData getInquiryUrl(BusinessObject bo, String propertyName) { 473 DocumentSearchCriteriaBo criteriaBo = (DocumentSearchCriteriaBo)bo; 474 if (KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_DOCUMENT_ID.equals(propertyName)) { 475 return generateDocumentHandlerUrl(criteriaBo.getDocumentId(), criteriaBo.getDocumentType(), 476 isSuperUserSearch()); 477 } else if (KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_ROUTE_LOG.equals(propertyName)) { 478 return generateRouteLogUrl(criteriaBo.getDocumentId()); 479 } else if(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_INITIATOR_DISPLAY_NAME.equals(propertyName)) { 480 return generateInitiatorUrl(criteriaBo.getInitiatorPerson()); 481 } 482 return super.getInquiryUrl(bo, propertyName); 483 } 484 485 /** 486 * Generates the appropriate document handler url for the given document. If superUserSearch is true then a super 487 * user doc handler link will be generated if the document type policy allows it. 488 */ 489 protected HtmlData.AnchorHtmlData generateDocumentHandlerUrl(String documentId, DocumentType documentType, boolean superUserSearch) { 490 HtmlData.AnchorHtmlData link = new HtmlData.AnchorHtmlData(); 491 link.setDisplayText(documentId); 492 if (isDocumentHandlerPopup()) { 493 link.setTarget("_blank"); 494 }else{ 495 link.setTarget("_self"); 496 } 497 String url = ConfigContext.getCurrentContextConfig().getProperty(Config.KEW_URL) + "/"; 498 if (superUserSearch) { 499 if (documentType.getUseWorkflowSuperUserDocHandlerUrl().getPolicyValue().booleanValue()) { 500 url += "SuperUser.do?methodToCall=displaySuperUserDocument&documentId=" + documentId; 501 } else { 502 url = KewApiConstants.DOC_HANDLER_REDIRECT_PAGE 503 + "?" + KewApiConstants.COMMAND_PARAMETER + "=" 504 + KewApiConstants.SUPERUSER_COMMAND + "&" 505 + KewApiConstants.DOCUMENT_ID_PARAMETER + "=" 506 + documentId; 507 } 508 } else { 509 url += KewApiConstants.DOC_HANDLER_REDIRECT_PAGE + "?" 510 + KewApiConstants.COMMAND_PARAMETER + "=" 511 + KewApiConstants.DOCSEARCH_COMMAND + "&" 512 + KewApiConstants.DOCUMENT_ID_PARAMETER + "=" 513 + documentId; 514 } 515 link.setHref(url); 516 return link; 517 } 518 519 protected HtmlData.AnchorHtmlData generateRouteLogUrl(String documentId) { 520 HtmlData.AnchorHtmlData link = new HtmlData.AnchorHtmlData(); 521 // KULRICE-6822 Route log link target parameter always causing pop-up 522 if (isRouteLogPopup()) { 523 link.setTarget("_blank"); 524 } 525 else { 526 link.setTarget("_self"); 527 } 528 link.setDisplayText("Route Log for document " + documentId); 529 String url = ConfigContext.getCurrentContextConfig().getProperty(Config.KEW_URL) + "/" + 530 "RouteLog.do?documentId=" + documentId; 531 link.setHref(url); 532 return link; 533 } 534 535 protected HtmlData.AnchorHtmlData generateInitiatorUrl(Person person) { 536 HtmlData.AnchorHtmlData link = new HtmlData.AnchorHtmlData(); 537 if ( person == null || StringUtils.isBlank(person.getPrincipalId()) ) { 538 return link; 539 } 540 if (isRouteLogPopup()) { 541 link.setTarget("_blank"); 542 } 543 else { 544 link.setTarget("_self"); 545 } 546 link.setDisplayText("Initiator Inquiry for User with ID:" + person.getPrincipalId()); 547 String url = ConfigContext.getCurrentContextConfig().getProperty(Config.KIM_URL) + "/" + 548 "identityManagementPersonInquiry.do?principalId=" + person.getPrincipalId(); 549 link.setHref(url); 550 return link; 551 } 552 553 /** 554 * Returns true if the document handler should open in a new window. 555 */ 556 protected boolean isDocumentHandlerPopup() { 557 return BooleanUtils.toBooleanDefaultIfNull( 558 CoreFrameworkServiceLocator.getParameterService().getParameterValueAsBoolean( 559 KewApiConstants.KEW_NAMESPACE, 560 KRADConstants.DetailTypes.DOCUMENT_SEARCH_DETAIL_TYPE, 561 KewApiConstants.DOCUMENT_SEARCH_DOCUMENT_POPUP_IND), 562 DOCUMENT_HANDLER_POPUP_DEFAULT); 563 } 564 565 /** 566 * Returns true if the route log should open in a new window. 567 */ 568 public boolean isRouteLogPopup() { 569 return BooleanUtils.toBooleanDefaultIfNull( 570 CoreFrameworkServiceLocator.getParameterService().getParameterValueAsBoolean(KewApiConstants.KEW_NAMESPACE, 571 KRADConstants.DetailTypes.DOCUMENT_SEARCH_DETAIL_TYPE, 572 KewApiConstants.DOCUMENT_SEARCH_ROUTE_LOG_POPUP_IND), ROUTE_LOG_POPUP_DEFAULT); 573 } 574 575 /** 576 * Parses a boolean request parameter 577 */ 578 protected boolean isFlagSet(String flagName) { 579 if(this.getParameters().containsKey(flagName)) { 580 String[] params = (String[])this.getParameters().get(flagName); 581 if (ArrayUtils.isNotEmpty(params)) { 582 return "YES".equalsIgnoreCase(params[0]); 583 } 584 } 585 return false; 586 } 587 588 /** 589 * Returns true if the current search being executed is a super user search. 590 */ 591 protected boolean isSuperUserSearch() { 592 return isFlagSet(DocumentSearchCriteriaProcessorKEWAdapter.SUPERUSER_SEARCH_FIELD); 593 } 594 595 /** 596 * Returns true if the current search being executed is an "advanced" search. 597 */ 598 protected boolean isAdvancedSearch() { 599 return isFlagSet(KRADConstants.ADVANCED_SEARCH_FIELD); 600 } 601 602 /** 603 * Returns true if the current "search" being executed is an "clear" search. 604 */ 605 protected boolean isClearSavedSearch() { 606 return isFlagSet(DocumentSearchCriteriaProcessorKEWAdapter.CLEARSAVED_SEARCH_FIELD); 607 } 608 609 protected String getSavedSearchName() { 610 String[] savedSearchName = getParameters().get(SAVED_SEARCH_NAME_PARAM); 611 if (savedSearchName != null && savedSearchName.length > 0) { 612 return savedSearchName[0]; 613 } 614 return null; 615 } 616 617 /** 618 * Override setRows in order to post-process and add documenttype-dependent fields 619 */ 620 @Override 621 protected void setRows() { 622 this.setRows(null); 623 } 624 625 /** 626 * Returns wrapper around current form fields 627 */ 628 protected FormFields getFormFields() { 629 return new FormFields(this.getRows()); 630 } 631 632 /** 633 * Sets the rows for the search criteria. This method will delegate to the DocumentSearchCriteriaProcessor 634 * in order to pull in fields for custom search attributes. 635 * 636 * @param documentTypeName the name of the document type currently entered on the form, if this is a valid document 637 * type then it may have search attribute fields that need to be displayed; documentType name may also be loaded 638 * via a saved search 639 */ 640 protected void setRows(String documentTypeName) { 641 // Always call superclass to regenerate the rows since state may have changed (namely, documentTypeName parsed from params) 642 super.setRows(); 643 644 List<Row> lookupRows = new ArrayList<Row>(); 645 //copy the current rows 646 for (Row row : getRows()) { 647 lookupRows.add(row); 648 } 649 //clear out 650 getRows().clear(); 651 652 DocumentType docType = getValidDocumentType(documentTypeName); 653 654 boolean advancedSearch = isAdvancedSearch(); 655 boolean superUserSearch = isSuperUserSearch(); 656 657 //call get rows 658 List<Row> rows = getDocumentSearchCriteriaProcessor().getRows(docType,lookupRows, advancedSearch, superUserSearch); 659 660 BusinessObjectEntry boe = (BusinessObjectEntry) KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(this.getBusinessObjectClass().getName()); 661 int numCols = boe.getLookupDefinition().getNumOfColumns(); 662 if(numCols == 0) { 663 numCols = KRADConstants.DEFAULT_NUM_OF_COLUMNS; 664 } 665 666 super.getRows().addAll(FieldUtils.wrapFields(new FormFields(rows).getFieldList(), numCols)); 667 668 } 669 670 /** 671 * Checks for a valid document type with the given name in a case-sensitive manner. 672 * 673 * @return the DocumentType which matches the given name or null if no valid document type could be found 674 */ 675 private DocumentType getValidDocumentType(String documentTypeName) { 676 if (StringUtils.isNotEmpty(documentTypeName)) { 677 DocumentType documentType = KEWServiceLocator.getDocumentTypeService().findByNameCaseInsensitive(documentTypeName.trim()); 678 if (documentType != null && documentType.isActive()) { 679 return documentType; 680 } 681 } 682 return null; 683 } 684 685 private static String TOGGLE_BUTTON = "<input type='image' name=''{0}'' id=''{0}'' class='tinybutton' src=''..{1}/images/tinybutton-{2}search.gif'' alt=''{3} search'' title=''{3} search''/>"; 686 687 @Override 688 public String getSupplementalMenuBar() { 689 boolean advancedSearch = isAdvancedSearch(); 690 boolean superUserSearch = isSuperUserSearch(); 691 StringBuilder suppMenuBar = new StringBuilder(); 692 693 // Add the detailed-search-toggling button. 694 // to mimic previous behavior, basic search button is shown both when currently rendering detailed search AND super user search 695 // as super user search is essentially a detailed search 696 String type = advancedSearch ? "basic" : "detailed"; 697 suppMenuBar.append(MessageFormat.format(TOGGLE_BUTTON, "toggleAdvancedSearch", KewApiConstants.WEBAPP_DIRECTORY, type, type)); 698 699 // Add the superuser-search-toggling button. 700 suppMenuBar.append(" "); 701 suppMenuBar.append(MessageFormat.format(TOGGLE_BUTTON, "toggleSuperUserSearch", KewApiConstants.WEBAPP_DIRECTORY, superUserSearch ? "nonsupu" : "superuser", superUserSearch ? "non-superuser" : "superuser")); 702 703 // Add the "clear saved searches" button. 704 suppMenuBar.append(" "); 705 suppMenuBar.append(MessageFormat.format(TOGGLE_BUTTON, DocumentSearchCriteriaProcessorKEWAdapter.CLEARSAVED_SEARCH_FIELD, KewApiConstants.WEBAPP_DIRECTORY, "clearsaved", "clear saved searches")); 706 707 // Wire up the onblur for document type name 708 suppMenuBar.append("<script type=\"text/javascript\">" 709 + " jQuery(document).ready(function () {" 710 + " jQuery(\"#documentTypeName\").blur(function () { validateDocTypeAndRefresh( this ); });" 711 + "});</script>"); 712 713 return suppMenuBar.toString(); 714 } 715 716 @Override 717 public boolean shouldDisplayHeaderNonMaintActions() { 718 return true; 719 } 720 721 @Override 722 public boolean shouldDisplayLookupCriteria() { 723 return true; 724 } 725 726 /** 727 * Determines if there should be more search fields rendered based on already entered search criteria, and 728 * generates additional form rows. 729 */ 730 @Override 731 public boolean checkForAdditionalFields(Map<String, String> fieldValues) { 732 return checkForAdditionalFieldsForDocumentType(fieldValues.get(DOCUMENT_TYPE_NAME_PARAM)); 733 } 734 735 private boolean checkForAdditionalFieldsMultiValued(Map<String, String[]> fieldValues) { 736 String[] valArray = fieldValues.get(DOCUMENT_TYPE_NAME_PARAM); 737 String val = null; 738 if (valArray != null && valArray.length > 0) { 739 val = valArray[0]; 740 } 741 return checkForAdditionalFieldsForDocumentType(val); 742 } 743 744 private boolean checkForAdditionalFieldsForDocumentType(String documentTypeName) { 745 if (StringUtils.isNotBlank(documentTypeName)) { 746 setRows(documentTypeName); 747 } 748 return true; 749 } 750 751 @Override 752 public Field getExtraField() { 753 SavedSearchValuesFinder savedSearchValuesFinder = new SavedSearchValuesFinder(); 754 List<KeyValue> savedSearchValues = savedSearchValuesFinder.getKeyValues(); 755 Field savedSearch = new Field(); 756 savedSearch.setPropertyName(SAVED_SEARCH_NAME_PARAM); 757 savedSearch.setFieldType(Field.DROPDOWN_SCRIPT); 758 savedSearch.setScript("customLookupChanged()"); 759 savedSearch.setFieldValidValues(savedSearchValues); 760 savedSearch.setFieldLabel("Saved Searches"); 761 return savedSearch; 762 } 763 764 @Override 765 public void performClear(LookupForm lookupForm) { 766 //KULRICE-7709 Convert dateCreated value to range before loadCriteria 767 Map<String, String> formFields = LookupUtils.preProcessRangeFields(lookupForm.getFields()); 768 DocumentSearchCriteria criteria = loadCriteria(formFields); 769 super.performClear(lookupForm); 770 repopulateSearchTypeFlags(); 771 DocumentType documentType = getValidDocumentType(criteria.getDocumentTypeName()); 772 if (documentType != null) { 773 DocumentSearchCriteria clearedCriteria = documentSearchService.clearCriteria(documentType, criteria); 774 applyCriteriaChangesToFields(DocumentSearchCriteria.Builder.create(clearedCriteria)); 775 } 776 } 777 778 /** 779 * Repopulate the fields indicating advanced/superuser search type. 780 */ 781 protected void repopulateSearchTypeFlags() { 782 boolean advancedSearch = isAdvancedSearch(); 783 boolean superUserSearch = isSuperUserSearch(); 784 int fieldsRepopulated = 0; 785 Map<String, String[]> values = new HashMap<String, String[]>(); 786 values.put(KRADConstants.ADVANCED_SEARCH_FIELD, new String[] { advancedSearch ? "YES" : "NO" }); 787 values.put(DocumentSearchCriteriaProcessorKEWAdapter.SUPERUSER_SEARCH_FIELD, new String[] { superUserSearch ? "YES" : "NO" }); 788 getFormFields().setFieldValues(values); 789 } 790 791 /** 792 * Takes a collection of result rows and does final processing on them. 793 */ 794 protected void postProcessResults(Collection<ResultRow> resultRows, DocumentSearchResults searchResults) { 795 if (resultRows.size() != searchResults.getSearchResults().size()) { 796 throw new IllegalStateException("Encountered a mismatch between ResultRow items and document search results " 797 + resultRows.size() + " != " + searchResults.getSearchResults().size()); 798 } 799 DocumentType documentType = getValidDocumentType(criteria.getDocumentTypeName()); 800 DocumentSearchResultSetConfiguration resultSetConfiguration = null; 801 DocumentSearchCriteriaConfiguration criteriaConfiguration = null; 802 if (documentType != null) { 803 resultSetConfiguration = KEWServiceLocator.getDocumentSearchCustomizationMediator().customizeResultSetConfiguration(documentType, criteria); 804 criteriaConfiguration = KEWServiceLocator.getDocumentSearchCustomizationMediator().getDocumentSearchCriteriaConfiguration(documentType); 805 } 806 int index = 0; 807 for (ResultRow resultRow : resultRows) { 808 DocumentSearchResult searchResult = searchResults.getSearchResults().get(index); 809 executeColumnCustomization(resultRow, searchResult, resultSetConfiguration, criteriaConfiguration); 810 index++; 811 } 812 } 813 814 /** 815 * Executes customization of columns, could include removing certain columns or adding additional columns to the 816 * result row (in cases where columns are added by document search customization, such as searchable attributes). 817 */ 818 protected void executeColumnCustomization(ResultRow resultRow, DocumentSearchResult searchResult, 819 DocumentSearchResultSetConfiguration resultSetConfiguration, 820 DocumentSearchCriteriaConfiguration criteriaConfiguration) { 821 if (resultSetConfiguration == null) { 822 resultSetConfiguration = DocumentSearchResultSetConfiguration.Builder.create().build(); 823 } 824 if (criteriaConfiguration == null) { 825 criteriaConfiguration = DocumentSearchCriteriaConfiguration.Builder.create().build(); 826 } 827 List<StandardResultField> standardFieldsToRemove = resultSetConfiguration.getStandardResultFieldsToRemove(); 828 if (standardFieldsToRemove == null) { 829 standardFieldsToRemove = Collections.emptyList(); 830 } 831 List<Column> newColumns = new ArrayList<Column>(); 832 for (Column standardColumn : resultRow.getColumns()) { 833 if (!standardFieldsToRemove.contains(StandardResultField.fromFieldName(standardColumn.getPropertyName()))) { 834 newColumns.add(standardColumn); 835 // modify the route log column so that xml values are not escaped (allows for the route log <img ...> to be 836 // rendered properly) 837 if (standardColumn.getPropertyName().equals( 838 KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_ROUTE_LOG)) { 839 standardColumn.setEscapeXMLValue(false); 840 } 841 } 842 } 843 844 // determine which document attribute fields should be added 845 List<RemotableAttributeField> searchAttributeFields = criteriaConfiguration.getFlattenedSearchAttributeFields(); 846 List<String> additionalFieldNamesToInclude = new ArrayList<String>(); 847 if (!resultSetConfiguration.isOverrideSearchableAttributes()) { 848 for (RemotableAttributeField searchAttributeField : searchAttributeFields) { 849 // TODO - KULRICE-5738 - add check here to make sure the searchable attribute should be displayed in result set 850 // right now this is default always including all searchable attributes! 851 if (searchAttributeField.getAttributeLookupSettings() == null || 852 searchAttributeField.getAttributeLookupSettings().isInResults()) { 853 additionalFieldNamesToInclude.add(searchAttributeField.getName()); 854 } 855 } 856 } 857 if (resultSetConfiguration.getCustomFieldNamesToAdd() != null) { 858 additionalFieldNamesToInclude.addAll(resultSetConfiguration.getCustomFieldNamesToAdd()); 859 } 860 861 // now assemble the custom columns 862 List<Column> customColumns = new ArrayList<Column>(); 863 List<Column> additionalAttributeColumns = FieldUtils.constructColumnsFromAttributeFields(resultSetConfiguration.getAdditionalAttributeFields()); 864 865 outer:for (String additionalFieldNameToInclude : additionalFieldNamesToInclude) { 866 // search the search attribute fields 867 for (RemotableAttributeField searchAttributeField : searchAttributeFields) { 868 if (additionalFieldNameToInclude.equals(searchAttributeField.getName())) { 869 Column searchAttributeColumn = FieldUtils.constructColumnFromAttributeField(searchAttributeField); 870 wrapDocumentAttributeColumnName(searchAttributeColumn); 871 customColumns.add(searchAttributeColumn); 872 continue outer; 873 } 874 } 875 for (Column additionalAttributeColumn : additionalAttributeColumns) { 876 if (additionalFieldNameToInclude.equals(additionalAttributeColumn.getPropertyName())) { 877 wrapDocumentAttributeColumnName(additionalAttributeColumn); 878 customColumns.add(additionalAttributeColumn); 879 continue outer; 880 } 881 } 882 LOG.warn("Failed to locate a proper column definition for requested additional field to include in" 883 + "result set with name '" 884 + additionalFieldNameToInclude 885 + "'"); 886 } 887 populateCustomColumns(customColumns, searchResult); 888 889 // if there is an action custom column, always put that before any other field 890 for (Column column : customColumns){ 891 if (column.getColumnTitle().equals(KRADConstants.ACTIONS_COLUMN_TITLE)){ 892 newColumns.add(0, column); 893 customColumns.remove(column); 894 break; 895 } 896 } 897 898 // now merge the custom columns into the standard columns right before the route log (if the route log column wasn't removed!) 899 if (newColumns.isEmpty() || !StandardResultField.ROUTE_LOG.isFieldNameValid(newColumns.get(newColumns.size() - 1).getPropertyName())) { 900 newColumns.addAll(customColumns); 901 } else { 902 newColumns.addAll(newColumns.size() - 1, customColumns); 903 } 904 resultRow.setColumns(newColumns); 905 } 906 907 protected void populateCustomColumns(List<Column> customColumns, DocumentSearchResult searchResult) { 908 for (Column customColumn : customColumns) { 909 DocumentAttribute documentAttribute = searchResult.getSingleDocumentAttributeByName(customColumn.getPropertyName()); 910 if (documentAttribute != null && documentAttribute.getValue() != null) { 911 wrapDocumentAttributeColumnName(customColumn); 912 // list moving forward if the attribute has more than one value 913 Formatter formatter = customColumn.getFormatter(); 914 Object attributeValue = documentAttribute.getValue(); 915 if (formatter.getPropertyType().equals(KualiDecimal.class) 916 && documentAttribute.getValue() instanceof BigDecimal) { 917 attributeValue = new KualiDecimal((BigDecimal)attributeValue); 918 } else if (formatter.getPropertyType().equals(KualiPercent.class) 919 && documentAttribute.getValue() instanceof BigDecimal) { 920 attributeValue = new KualiPercent((BigDecimal)attributeValue); 921 } 922 customColumn.setPropertyValue(formatter.format(attributeValue).toString()); 923 924 //populate the custom column columnAnchor because it is used for determining if the result field is displayed 925 //as static string or links 926 HtmlData anchor = customColumn.getColumnAnchor(); 927 if (anchor != null && anchor instanceof HtmlData.AnchorHtmlData){ 928 HtmlData.AnchorHtmlData anchorHtml = (HtmlData.AnchorHtmlData)anchor; 929 if (StringUtils.isEmpty(anchorHtml.getHref()) && StringUtils.isEmpty(anchorHtml.getTitle())){ 930 customColumn.setColumnAnchor(new HtmlData.AnchorHtmlData(formatter.format(attributeValue).toString(), documentAttribute.getName())); 931 } 932 } 933 } 934 } 935 } 936 937 private void wrapDocumentAttributeColumnName(Column column) { 938 // TODO - comment out for now, not sure we really want to do this... 939 //column.setPropertyName(DOCUMENT_ATTRIBUTE_PROPERTY_NAME_PREFIX + column.getPropertyName()); 940 } 941 942 public void setDocumentSearchService(DocumentSearchService documentSearchService) { 943 this.documentSearchService = documentSearchService; 944 } 945 946 public DocumentSearchService getDocumentSearchService() { 947 return documentSearchService; 948 } 949 950 public DocumentSearchCriteriaProcessor getDocumentSearchCriteriaProcessor() { 951 return documentSearchCriteriaProcessor; 952 } 953 954 public void setDocumentSearchCriteriaProcessor(DocumentSearchCriteriaProcessor documentSearchCriteriaProcessor) { 955 this.documentSearchCriteriaProcessor = documentSearchCriteriaProcessor; 956 } 957 958 public DocumentSearchCriteriaTranslator getDocumentSearchCriteriaTranslator() { 959 return documentSearchCriteriaTranslator; 960 } 961 962 public void setDocumentSearchCriteriaTranslator(DocumentSearchCriteriaTranslator documentSearchCriteriaTranslator) { 963 this.documentSearchCriteriaTranslator = documentSearchCriteriaTranslator; 964 } 965}