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