001/** 002 * Copyright 2005-2015 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.krad.uif.widget; 017 018import org.apache.commons.lang.StringUtils; 019import org.kuali.rice.core.api.CoreApiServiceLocator; 020import org.kuali.rice.core.api.encryption.EncryptionService; 021import org.kuali.rice.krad.datadictionary.DataObjectEntry; 022import org.kuali.rice.krad.datadictionary.parse.BeanTag; 023import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute; 024import org.kuali.rice.krad.messages.MessageService; 025import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 026import org.kuali.rice.krad.service.ModuleService; 027import org.kuali.rice.krad.uif.UifConstants; 028import org.kuali.rice.krad.uif.UifParameters; 029import org.kuali.rice.krad.uif.component.BindingInfo; 030import org.kuali.rice.krad.uif.component.Component; 031import org.kuali.rice.krad.uif.element.Action; 032import org.kuali.rice.krad.uif.element.Link; 033import org.kuali.rice.krad.uif.field.DataField; 034import org.kuali.rice.krad.uif.field.InputField; 035import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle; 036import org.kuali.rice.krad.uif.util.LifecycleElement; 037import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 038import org.kuali.rice.krad.uif.util.ViewModelUtils; 039import org.kuali.rice.krad.util.KRADConstants; 040import org.kuali.rice.krad.util.KRADUtils; 041import org.kuali.rice.krad.util.UrlFactory; 042 043import java.security.GeneralSecurityException; 044import java.util.ArrayList; 045import java.util.HashMap; 046import java.util.List; 047import java.util.Map; 048import java.util.Map.Entry; 049import java.util.Properties; 050 051/** 052 * Widget for rendering an Inquiry link or DirectInquiry action field 053 * 054 * <p> 055 * The inquiry widget will render a button for the field value when 056 * that field is editable. When read only the widget will create a link on the display value. 057 * It points to the associated inquiry view for the field. The inquiry can be configured to point to a certain 058 * {@code InquiryView}, or the framework will attempt to associate the field with a inquiry based on 059 * its metadata (in particular its relationships in the model). 060 * </p> 061 * 062 * @author Kuali Rice Team (rice.collab@kuali.org) 063 */ 064@BeanTag(name = "inquiry", parent = "Uif-Inquiry") 065public class Inquiry extends WidgetBase { 066 private static final long serialVersionUID = -2154388007867302901L; 067 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(Inquiry.class); 068 069 public static final String INQUIRY_TITLE_PREFIX = "title.inquiry.url.actiontext"; 070 public static final String INQUIRY_TITLE_POSTFIX = "title.inquiry.url.value.prependtext"; 071 072 private String baseInquiryUrl; 073 074 private String dataObjectClassName; 075 private String viewName; 076 077 private Map<String, String> inquiryParameters; 078 079 private Link inquiryLink; 080 081 private Action directInquiryAction; 082 private boolean enableDirectInquiry; 083 084 private boolean adjustInquiryParameters; 085 private BindingInfo fieldBindingInfo; 086 087 private boolean parentReadOnly; 088 089 public Inquiry() { 090 super(); 091 092 inquiryParameters = new HashMap<String, String>(); 093 } 094 095 /** 096 * Inherits readOnly from parent if not explicitly populated. 097 * 098 * {@inheritDoc} 099 */ 100 @Override 101 public void afterEvaluateExpression() { 102 super.afterEvaluateExpression(); 103 104 if (getReadOnly() == null) { 105 Component parent = ViewLifecycle.getPhase().getParent(); 106 setReadOnly(parent == null ? null : parent.getReadOnly()); 107 } 108 } 109 110 /** 111 * {@inheritDoc} 112 */ 113 @Override 114 public void performFinalize(Object model, LifecycleElement parent) { 115 super.performFinalize(model, parent); 116 117 if (!isRender()) { 118 return; 119 } 120 121 // set render to false until we find an inquiry class 122 setRender(false); 123 124 // used to determine whether a normal or direct inquiry should be enabled 125 if (parent instanceof Component) { 126 setParentReadOnly(((Component) parent).getReadOnly().booleanValue()); 127 } 128 129 // Do checks for direct inquiry when editable 130 if (!isParentReadOnly() && parent instanceof InputField) { 131 if (!enableDirectInquiry) { 132 return; 133 } 134 135 // determine whether inquiry parameters will need adjusted 136 if (StringUtils.isBlank(getDataObjectClassName()) 137 || (getInquiryParameters() == null) 138 || getInquiryParameters().isEmpty()) { 139 // if inquiry parameters not given, they will not be adjusted by super 140 adjustInquiryParameters = true; 141 fieldBindingInfo = ((InputField) parent).getBindingInfo(); 142 } 143 } 144 145 if (parent instanceof DataField) { 146 setupLink(model, (DataField) parent); 147 } 148 149 if (isRender() && !isParentReadOnly() && parent instanceof InputField && enableDirectInquiry) { 150 ((InputField) parent).addPostInputAddon(directInquiryAction); 151 } 152 } 153 154 /** 155 * Get parent object and field name and build the inquiry link 156 * 157 * <p> 158 * This was moved from the performFinalize because overlapping and to be used 159 * by DirectInquiry. 160 * </p> 161 * 162 * @param model model 163 * @param field The parent Attribute field 164 */ 165 private void setupLink(Object model, DataField field) { 166 String propertyName = field.getBindingInfo().getBindingName(); 167 168 // if class and parameters configured, build link from those 169 if (StringUtils.isNotBlank(getDataObjectClassName()) && (getInquiryParameters() != null) && 170 !getInquiryParameters().isEmpty()) { 171 Class<?> inquiryObjectClass; 172 try { 173 inquiryObjectClass = Class.forName(getDataObjectClassName()); 174 } catch (ClassNotFoundException e) { 175 LOG.error("Unable to get class for: " + getDataObjectClassName()); 176 throw new RuntimeException(e); 177 } 178 179 updateInquiryParameters(field.getBindingInfo()); 180 181 buildInquiryLink(model, propertyName, inquiryObjectClass, getInquiryParameters()); 182 } 183 // get inquiry class and parameters from view helper 184 else { 185 // get parent object for inquiry metadata 186 ViewLifecycle viewLifecycle = ViewLifecycle.getActiveLifecycle(); 187 188 Object parentObject = ViewModelUtils.getParentObjectForMetadata(viewLifecycle.getView(), model, field); 189 if (parentObject != null) { 190 viewLifecycle.getHelper().buildInquiryLink(parentObject, propertyName, this); 191 } 192 } 193 } 194 195 /** 196 * Adjusts the path on the inquiry parameter property to match the binding 197 * path prefix of the given {@code BindingInfo} 198 * 199 * @param bindingInfo binding info instance to copy binding path prefix from 200 */ 201 public void updateInquiryParameters(BindingInfo bindingInfo) { 202 Map<String, String> adjustedInquiryParameters = new HashMap<String, String>(); 203 for (Entry<String, String> stringEntry : inquiryParameters.entrySet()) { 204 String toField = stringEntry.getValue(); 205 String adjustedFromFieldPath = bindingInfo.getPropertyAdjustedBindingPath(stringEntry.getKey()); 206 207 adjustedInquiryParameters.put(adjustedFromFieldPath, toField); 208 } 209 210 this.inquiryParameters = adjustedInquiryParameters; 211 } 212 213 /** 214 * Builds the inquiry link based on the given inquiry class and parameters 215 * 216 * @param dataObject parent object that contains the data (used to pull inquiry 217 * parameters) 218 * @param propertyName name of the property the inquiry is set on 219 * @param inquiryObjectClass class of the object the inquiry should point to 220 * @param inquiryParams map of key field mappings for the inquiry 221 */ 222 @SuppressWarnings("deprecation") 223 public void buildInquiryLink(Object dataObject, String propertyName, Class<?> inquiryObjectClass, 224 Map<String, String> inquiryParams) { 225 MessageService messageService = KRADServiceLocatorWeb.getMessageService(); 226 227 Properties urlParameters = new Properties(); 228 Map<String,String> inquiryKeyValues = new HashMap<String, String>(); 229 230 urlParameters.setProperty(UifParameters.DATA_OBJECT_CLASS_NAME, inquiryObjectClass.getName()); 231 urlParameters.setProperty(UifParameters.METHOD_TO_CALL, UifConstants.MethodToCallNames.START); 232 if (StringUtils.isNotBlank(this.viewName)) { 233 urlParameters.setProperty(UifParameters.VIEW_NAME, this.viewName); 234 } 235 236 // configure inquiry when read only 237 if (isParentReadOnly()) { 238 for (Entry<String, String> inquiryParameter : inquiryParams.entrySet()) { 239 String parameterName = inquiryParameter.getKey(); 240 String parameterPropertyName = inquiryParameter.getValue(); 241 String parameterValue = StringUtils.defaultString( 242 ObjectPropertyUtils.getPropertyValueAsText(dataObject, parameterName)); 243 244 // check to see whether the property is secure 245 boolean isSecure = KRADUtils.isSecure(propertyName, dataObject.getClass()); 246 247 // add the raw value to the title key values if it is not secure 248 if (!isSecure) { 249 inquiryKeyValues.put(parameterPropertyName, parameterValue); 250 } 251 252 // encrypt the value if it is secure 253 if (isSecure) { 254 try { 255 if (CoreApiServiceLocator.getEncryptionService().isEnabled()) { 256 parameterValue = CoreApiServiceLocator.getEncryptionService().encrypt(parameterValue) 257 + EncryptionService.ENCRYPTION_POST_PREFIX; 258 } 259 } catch (GeneralSecurityException e) { 260 String message = "Unable to encrypt value for property name: " + parameterPropertyName; 261 LOG.error(message); 262 263 throw new RuntimeException(message, e); 264 } 265 } 266 267 // add the encrypted value to the URL key values 268 urlParameters.put(parameterPropertyName, parameterValue); 269 } 270 271 /* build inquiry URL */ 272 String inquiryUrl; 273 274 // check for EBOs for an alternate inquiry URL 275 ModuleService responsibleModuleService = 276 KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(inquiryObjectClass); 277 if (responsibleModuleService != null && responsibleModuleService.isExternalizable(inquiryObjectClass)) { 278 inquiryUrl = responsibleModuleService.getExternalizableDataObjectInquiryUrl(inquiryObjectClass, 279 urlParameters); 280 } else { 281 inquiryUrl = UrlFactory.parameterizeUrl(getBaseInquiryUrl(), urlParameters); 282 } 283 284 getInquiryLink().setHref(inquiryUrl); 285 286 // set inquiry title 287 getInquiryLink().setTitle(createTitleText(inquiryObjectClass, inquiryKeyValues)); 288 289 setRender(true); 290 } 291 // configure direct inquiry when editable 292 else { 293 // Direct inquiry 294 String inquiryUrl = UrlFactory.parameterizeUrl(getBaseInquiryUrl(), urlParameters); 295 296 StringBuilder paramMapStringBuilder = new StringBuilder(); 297 298 // Build parameter string using the actual names of the fields as on the html page 299 for (Entry<String, String> inquiryParameter : inquiryParams.entrySet()) { 300 String inquiryParameterFrom = inquiryParameter.getKey(); 301 302 if (adjustInquiryParameters && (fieldBindingInfo != null)) { 303 inquiryParameterFrom = fieldBindingInfo.getPropertyAdjustedBindingPath(inquiryParameterFrom); 304 } 305 306 ViewLifecycle viewLifecycle = ViewLifecycle.getActiveLifecycle(); 307 308 // Make sure our inquiry parameters are included as a rendered property path 309 if(!viewLifecycle.getViewPostMetadata().getAllRenderedPropertyPaths().contains(inquiryParameterFrom.toString())){ 310 setRender(false); 311 return; 312 } 313 314 paramMapStringBuilder.append(inquiryParameterFrom); 315 paramMapStringBuilder.append(":"); 316 paramMapStringBuilder.append(inquiryParameter.getValue()); 317 paramMapStringBuilder.append(","); 318 319 } 320 String paramMapString = StringUtils.removeEnd(paramMapStringBuilder.toString(), ","); 321 322 // Check if showing in dialog 323 if (!getInquiryLink().isOpenInDialog()) { 324 String title = this.getTitle(); 325 if (StringUtils.isNotBlank(title)) { 326 this.setTitle(title + " - " + messageService.getMessageText("accessibility.link.opensTab")); 327 } 328 else{ 329 this.setTitle(messageService.getMessageText("accessibility.link.opensTab")); 330 } 331 } 332 333 // Create onlick script to open the inquiry window on the click event 334 // of the direct inquiry 335 StringBuilder onClickScript = new StringBuilder("showDirectInquiry(\""); 336 onClickScript.append(inquiryUrl); 337 onClickScript.append("\", \""); 338 onClickScript.append(paramMapString); 339 onClickScript.append("\", "); 340 onClickScript.append(getInquiryLink().isOpenInDialog()); 341 onClickScript.append(", \""); 342 onClickScript.append(getInquiryLink().getLinkDialogId()); 343 onClickScript.append("\");"); 344 345 directInquiryAction.setPerformDirtyValidation(false); 346 String actionScript = ""; 347 if (StringUtils.isNotEmpty(directInquiryAction.getActionScript())) { 348 actionScript += directInquiryAction.getActionScript(); 349 } 350 directInquiryAction.setActionScript(actionScript + onClickScript.toString()); 351 352 setRender(true); 353 } 354 } 355 356 /** 357 * Gets text to prepend to the inquiry link title 358 * 359 * @param dataObjectClass data object class being inquired into 360 * @return inquiry link title 361 */ 362 public String createTitleText(Class<?> dataObjectClass, Map<String,String> inquiryKeyValues) { 363 // use manually configured title if exists 364 if (StringUtils.isNotBlank(getTitle())) { 365 return getTitle(); 366 } 367 368 List<String> titleTexts = new ArrayList<String>(); 369 370 // get the title prefix 371 String titlePrefix = CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString( 372 INQUIRY_TITLE_PREFIX); 373 374 // if the title prefix is available, add it to the title text 375 if (StringUtils.isNotBlank(titlePrefix)) { 376 titleTexts.add(titlePrefix); 377 } 378 379 // get the data object label 380 DataObjectEntry dataObjectEntry = KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary() 381 .getDataObjectEntry(dataObjectClass.getName()); 382 String dataObjectLabel = dataObjectEntry != null ? dataObjectEntry.getObjectLabel() : null; 383 384 // if the data object label is available, then add it to the title text 385 if (StringUtils.isNotBlank(dataObjectLabel)) { 386 titleTexts.add(dataObjectLabel); 387 } 388 389 // get the prepend text configuration 390 String titleUrlPrependText = CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString( 391 KRADConstants.Lookup.TITLE_ACTION_URL_PREPENDTEXT_PROPERTY); 392 393 // if the prepend text is available and there are primary key values, then add it to the link label 394 if (StringUtils.isNotBlank(titleUrlPrependText) && !inquiryKeyValues.isEmpty()) { 395 titleTexts.add(titleUrlPrependText); 396 } 397 398 String titleText = StringUtils.defaultIfBlank(StringUtils.join(titleTexts, " "), StringUtils.EMPTY); 399 400 return KRADUtils.buildAttributeTitleString(titleText, dataObjectClass, inquiryKeyValues); 401 } 402 403 /** 404 * Returns the URL for the inquiry for which parameters will be added 405 * 406 * <p> 407 * The base URL includes the domain, context, and controller mapping for the inquiry invocation. Parameters are 408 * then added based on configuration to complete the URL. This is generally defaulted to the application URL and 409 * internal KRAD servlet mapping, but can be changed to invoke another application such as the Rice standalone 410 * server 411 * </p> 412 * 413 * @return inquiry base URL 414 */ 415 @BeanTagAttribute 416 public String getBaseInquiryUrl() { 417 return this.baseInquiryUrl; 418 } 419 420 /** 421 * Setter for the inquiry base url (domain, context, and controller) 422 * 423 * @param baseInquiryUrl 424 */ 425 public void setBaseInquiryUrl(String baseInquiryUrl) { 426 this.baseInquiryUrl = baseInquiryUrl; 427 } 428 429 /** 430 * Full class name the inquiry should be provided for 431 * 432 * <p> 433 * This is passed on to the inquiry request for the data object the lookup should be rendered for. This is then 434 * used by the inquiry framework to select the lookup view (if more than one inquiry view exists for the same 435 * data object class name, the {@link #getViewName()} property should be specified to select the view to render). 436 * </p> 437 * 438 * @return inquiry class name 439 */ 440 @BeanTagAttribute 441 public String getDataObjectClassName() { 442 return this.dataObjectClassName; 443 } 444 445 /** 446 * Setter for the class name that inquiry should be provided for 447 * 448 * @param dataObjectClassName 449 */ 450 public void setDataObjectClassName(String dataObjectClassName) { 451 this.dataObjectClassName = dataObjectClassName; 452 } 453 454 /** 455 * When multiple target inquiry views exists for the same data object class, the view name can be set to 456 * determine which one to use 457 * 458 * <p> 459 * When creating multiple inquiry views for the same data object class, the view name can be specified for the 460 * different versions (for example 'simple' and 'advanced'). When multiple inquiry views exist the view name must 461 * be sent with the data object class for the request. Note the view id can be alternatively used to uniquely 462 * identify the inquiry view 463 * </p> 464 * @return view name 465 */ 466 @BeanTagAttribute 467 public String getViewName() { 468 return this.viewName; 469 } 470 471 /** 472 * Setter for the view name configured on the inquiry view that should be invoked by the inquiry widget 473 * 474 * @param viewName 475 */ 476 public void setViewName(String viewName) { 477 this.viewName = viewName; 478 } 479 480 /** 481 * Map that determines what properties from a calling view will be sent to properties on the inquiry data object 482 * 483 * <p> 484 * When invoking an inquiry view, a query is done against the inquiries configured data object and the resulting 485 * record is display. The values for the properties configured within the inquiry parameters Map will be 486 * pulled and passed along as values for the inquiry data object properties (thus they form the criteria for 487 * the inquiry) 488 * </p> 489 * 490 * @return mapping of calling view properties to inquiry data object properties 491 */ 492 @BeanTagAttribute 493 public Map<String, String> getInquiryParameters() { 494 return this.inquiryParameters; 495 } 496 497 /** 498 * Setter for the map that determines what property values on the calling view will be sent to properties on the 499 * inquiry data object 500 * 501 * @param inquiryParameters 502 */ 503 public void setInquiryParameters(Map<String, String> inquiryParameters) { 504 this.inquiryParameters = inquiryParameters; 505 } 506 507 /** 508 * {@code Link} that will be rendered for an inquiry 509 * 510 * @return the inquiry link 511 */ 512 @BeanTagAttribute 513 public Link getInquiryLink() { 514 return this.inquiryLink; 515 } 516 517 /** 518 * Setter for the inquiry {@code Link} 519 * 520 * @param inquiryLink the inquiry {@link Link} object 521 */ 522 public void setInquiryLink(Link inquiryLink) { 523 this.inquiryLink = inquiryLink; 524 } 525 526 /** 527 * {@code Action} that will be rendered next to the field for a direct inquiry 528 * 529 * @return the directInquiryAction 530 */ 531 @BeanTagAttribute 532 public Action getDirectInquiryAction() { 533 return this.directInquiryAction; 534 } 535 536 /** 537 * Setter for the direct inquiry {@code Action} 538 * 539 * @param directInquiryAction the direct inquiry {@link Action} 540 */ 541 public void setDirectInquiryAction(Action directInquiryAction) { 542 this.directInquiryAction = directInquiryAction; 543 } 544 545 /** 546 * Indicates that the direct inquiry will not be rendered 547 * 548 * @return true if the direct inquiry should be rendered, false if not 549 */ 550 @BeanTagAttribute 551 public boolean isEnableDirectInquiry() { 552 return enableDirectInquiry; 553 } 554 555 /** 556 * Setter for the hideDirectInquiry flag 557 * 558 * @param enableDirectInquiry 559 */ 560 public void setEnableDirectInquiry(boolean enableDirectInquiry) { 561 this.enableDirectInquiry = enableDirectInquiry; 562 } 563 564 /** 565 * Determines whether a normal or direct inquiry should be enabled 566 * 567 * @return true if parent component is read only, false otherwise 568 */ 569 protected boolean isParentReadOnly() { 570 return parentReadOnly; 571 } 572 573 /** 574 * Determines whether a normal or direct inquiry should be enabled 575 * 576 * <p> 577 * Used by unit tests and internally 578 * </p> 579 * 580 * @param parentReadOnly true if parent component is read only, false otherwise 581 */ 582 protected void setParentReadOnly(boolean parentReadOnly) { 583 this.parentReadOnly = parentReadOnly; 584 } 585 586 /** 587 * Determines whether inquiry parameters adjusted 588 * 589 * @return true if adjusted 590 */ 591 public boolean isAdjustInquiryParameters() { 592 return adjustInquiryParameters; 593 } 594 595 /** 596 * Determines whether inquiry parameters adjusted 597 * 598 * <p> 599 * Used internally 600 * </p> 601 * 602 * @param adjustInquiryParameters 603 */ 604 protected void setAdjustInquiryParameters(boolean adjustInquiryParameters) { 605 this.adjustInquiryParameters = adjustInquiryParameters; 606 } 607 608 /** 609 * Sets the field binding information 610 * 611 * <p> 612 * Sets the field binding information 613 * </p> 614 * 615 * @param fieldBindingInfo 616 */ 617 protected void setFieldBindingInfo(BindingInfo fieldBindingInfo) { 618 this.fieldBindingInfo = fieldBindingInfo; 619 } 620}