001 /** 002 * Copyright 2005-2013 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.krad.uif.widget; 017 018 import org.apache.commons.lang.StringUtils; 019 import org.kuali.rice.core.api.CoreApiServiceLocator; 020 import org.kuali.rice.core.web.format.Formatter; 021 import org.kuali.rice.krad.datadictionary.parse.BeanTag; 022 import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute; 023 import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 024 import org.kuali.rice.krad.service.ModuleService; 025 import org.kuali.rice.krad.uif.UifConstants; 026 import org.kuali.rice.krad.uif.UifParameters; 027 import org.kuali.rice.krad.uif.component.BindingInfo; 028 import org.kuali.rice.krad.uif.component.Component; 029 import org.kuali.rice.krad.uif.element.Action; 030 import org.kuali.rice.krad.uif.element.Link; 031 import org.kuali.rice.krad.uif.field.DataField; 032 import org.kuali.rice.krad.uif.field.InputField; 033 import org.kuali.rice.krad.uif.util.LookupInquiryUtils; 034 import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 035 import org.kuali.rice.krad.uif.util.ViewModelUtils; 036 import org.kuali.rice.krad.uif.view.View; 037 import org.kuali.rice.krad.util.UrlFactory; 038 039 import java.security.GeneralSecurityException; 040 import java.util.HashMap; 041 import java.util.List; 042 import java.util.Map; 043 import java.util.Map.Entry; 044 import java.util.Properties; 045 046 /** 047 * Widget for rendering an Inquiry link or DirectInquiry action field 048 * 049 * <p> 050 * The inquiry widget will render a button for the field value when 051 * that field is editable. When read only the widget will create a link on the display value. 052 * It points to the associated inquiry view for the field. The inquiry can be configured to point to a certain 053 * {@code InquiryView}, or the framework will attempt to associate the 054 * field with a inquiry based on its metadata (in particular its 055 * relationships in the model). 056 * </p> 057 * 058 * @author Kuali Rice Team (rice.collab@kuali.org) 059 */ 060 @BeanTag(name = "inquiry-bean", parent = "Uif-Inquiry") 061 public class Inquiry extends WidgetBase { 062 private static final long serialVersionUID = -2154388007867302901L; 063 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(Inquiry.class); 064 065 public static final String INQUIRY_TITLE_PREFIX = "title.inquiry.url.value.prependtext"; 066 067 private String baseInquiryUrl; 068 069 private String dataObjectClassName; 070 private String viewName; 071 072 private Map<String, String> inquiryParameters; 073 074 private Link inquiryLink; 075 076 private Action directInquiryAction; 077 private boolean enableDirectInquiry; 078 079 private boolean adjustInquiryParameters; 080 private BindingInfo fieldBindingInfo; 081 082 private boolean parentReadOnly; 083 084 public Inquiry() { 085 super(); 086 087 inquiryParameters = new HashMap<String, String>(); 088 } 089 090 /** 091 * @see org.kuali.rice.krad.uif.widget.WidgetBase#performFinalize(org.kuali.rice.krad.uif.view.View, 092 * java.lang.Object, org.kuali.rice.krad.uif.component.Component) 093 */ 094 @Override 095 public void performFinalize(View view, Object model, Component parent) { 096 super.performFinalize(view, model, parent); 097 098 if (!isRender()) { 099 return; 100 } 101 102 // set render to false until we find an inquiry class 103 setRender(false); 104 105 // used to determine whether a normal or direct inquiry should be enabled 106 setParentReadOnly(parent.isReadOnly()); 107 108 // Do checks for inquiry when read only 109 if (isParentReadOnly()) { 110 if (StringUtils.isBlank(((DataField) parent).getBindingInfo().getBindingPath()) 111 || ((DataField) parent).getBindingInfo().getBindingPath().equals("null")) { 112 return; 113 } 114 115 // check if field value is null, if so no inquiry 116 try { 117 Object propertyValue = ObjectPropertyUtils.getPropertyValue(model, 118 ((DataField) parent).getBindingInfo().getBindingPath()); 119 120 if ((propertyValue == null) || StringUtils.isBlank(propertyValue.toString())) { 121 return; 122 } 123 } catch (Exception e) { 124 // if we can't get the value just swallow the exception and don't set an inquiry 125 return; 126 } 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 setupLink(view, model, (DataField) parent); 146 } 147 148 /** 149 * Get parent object and field name and build the inquiry link 150 * 151 * <p> 152 * This was moved from the performFinalize because overlapping and to be used 153 * by DirectInquiry. 154 * </p> 155 * 156 * @param view Container View 157 * @param model model 158 * @param field The parent Attribute field 159 */ 160 public void setupLink(View view, Object model, DataField field) { 161 String propertyName = field.getBindingInfo().getBindingName(); 162 163 // if class and parameters configured, build link from those 164 if (StringUtils.isNotBlank(getDataObjectClassName()) && (getInquiryParameters() != null) && 165 !getInquiryParameters().isEmpty()) { 166 Class<?> inquiryObjectClass; 167 try { 168 inquiryObjectClass = Class.forName(getDataObjectClassName()); 169 } catch (ClassNotFoundException e) { 170 LOG.error("Unable to get class for: " + getDataObjectClassName()); 171 throw new RuntimeException(e); 172 } 173 174 updateInquiryParameters(field.getBindingInfo()); 175 176 buildInquiryLink(model, propertyName, inquiryObjectClass, getInquiryParameters()); 177 } 178 // get inquiry class and parameters from view helper 179 else { 180 // get parent object for inquiry metadata 181 Object parentObject = ViewModelUtils.getParentObjectForMetadata(view, model, field); 182 view.getViewHelperService().buildInquiryLink(parentObject, propertyName, this); 183 } 184 } 185 186 /** 187 * Adjusts the path on the inquiry parameter property to match the binding 188 * path prefix of the given {@code BindingInfo} 189 * 190 * @param bindingInfo binding info instance to copy binding path prefix from 191 */ 192 public void updateInquiryParameters(BindingInfo bindingInfo) { 193 Map<String, String> adjustedInquiryParameters = new HashMap<String, String>(); 194 for (Entry<String, String> stringEntry : inquiryParameters.entrySet()) { 195 String toField = stringEntry.getValue(); 196 String adjustedFromFieldPath = bindingInfo.getPropertyAdjustedBindingPath(stringEntry.getKey()); 197 198 adjustedInquiryParameters.put(adjustedFromFieldPath, toField); 199 } 200 201 this.inquiryParameters = adjustedInquiryParameters; 202 } 203 204 /** 205 * Builds the inquiry link based on the given inquiry class and parameters 206 * 207 * @param dataObject parent object that contains the data (used to pull inquiry 208 * parameters) 209 * @param propertyName name of the property the inquiry is set on 210 * @param inquiryObjectClass class of the object the inquiry should point to 211 * @param inquiryParams map of key field mappings for the inquiry 212 */ 213 public void buildInquiryLink(Object dataObject, String propertyName, Class<?> inquiryObjectClass, 214 Map<String, String> inquiryParams) { 215 216 Properties urlParameters = new Properties(); 217 218 urlParameters.setProperty(UifParameters.DATA_OBJECT_CLASS_NAME, inquiryObjectClass.getName()); 219 urlParameters.setProperty(UifParameters.METHOD_TO_CALL, UifConstants.MethodToCallNames.START); 220 if(StringUtils.isNotBlank(this.viewName)){ 221 urlParameters.setProperty(UifParameters.VIEW_NAME, this.viewName); 222 } 223 // add inquiry specific parms to url 224 if (getInquiryLink().getLightBox() != null) { 225 getInquiryLink().getLightBox().setAddAppParms(true); 226 } 227 228 // configure inquiry when read only 229 if (isParentReadOnly()) { 230 for (Entry<String, String> inquiryParameter : inquiryParams.entrySet()) { 231 String parameterName = inquiryParameter.getKey(); 232 233 Object parameterValue = ObjectPropertyUtils.getPropertyValue(dataObject, parameterName); 234 235 // TODO: need general format util that uses spring 236 if (parameterValue == null) { 237 parameterValue = ""; 238 } else if (parameterValue instanceof java.sql.Date) { 239 if (Formatter.findFormatter(parameterValue.getClass()) != null) { 240 Formatter formatter = Formatter.getFormatter(parameterValue.getClass()); 241 parameterValue = formatter.format(parameterValue); 242 } 243 } else { 244 parameterValue = parameterValue.toString(); 245 } 246 247 // Encrypt value if it is a field that has restriction that prevents a value from being shown to 248 // user, because we don't want the browser history to store the restricted attributes value in the URL 249 if (KRADServiceLocatorWeb.getDataObjectAuthorizationService() 250 .attributeValueNeedsToBeEncryptedOnFormsAndLinks(inquiryObjectClass, 251 inquiryParameter.getValue())) { 252 try { 253 parameterValue = CoreApiServiceLocator.getEncryptionService().encrypt(parameterValue); 254 } catch (GeneralSecurityException e) { 255 throw new RuntimeException("Exception while trying to encrypted value for inquiry framework.", 256 e); 257 } 258 } 259 260 // add inquiry parameter to URL 261 urlParameters.put(inquiryParameter.getValue(), parameterValue); 262 } 263 264 /* build inquiry URL */ 265 String inquiryUrl; 266 267 // check for EBOs for an alternate inquiry URL 268 ModuleService responsibleModuleService = 269 KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(inquiryObjectClass); 270 if (responsibleModuleService != null && responsibleModuleService.isExternalizable(inquiryObjectClass)) { 271 inquiryUrl = responsibleModuleService.getExternalizableDataObjectInquiryUrl(inquiryObjectClass, 272 urlParameters); 273 } else { 274 inquiryUrl = UrlFactory.parameterizeUrl(getBaseInquiryUrl(), urlParameters); 275 } 276 277 getInquiryLink().setHref(inquiryUrl); 278 279 // set inquiry title 280 String linkTitle = createTitleText(inquiryObjectClass); 281 linkTitle = LookupInquiryUtils.getLinkTitleText(linkTitle, inquiryObjectClass, getInquiryParameters()); 282 getInquiryLink().setTitle(linkTitle); 283 284 setRender(true); 285 } 286 // configure direct inquiry when editable 287 else { 288 // Direct inquiry 289 String inquiryUrl = UrlFactory.parameterizeUrl(getBaseInquiryUrl(), urlParameters); 290 291 StringBuilder paramMapString = new StringBuilder(); 292 293 // Build parameter string using the actual names of the fields as on the html page 294 for (Entry<String, String> inquiryParameter : inquiryParams.entrySet()) { 295 String inquiryParameterFrom = inquiryParameter.getKey(); 296 297 if (adjustInquiryParameters && (fieldBindingInfo != null)) { 298 inquiryParameterFrom = fieldBindingInfo.getPropertyAdjustedBindingPath(inquiryParameterFrom); 299 } 300 301 paramMapString.append(inquiryParameterFrom); 302 paramMapString.append(":"); 303 paramMapString.append(inquiryParameter.getValue()); 304 paramMapString.append(","); 305 } 306 paramMapString.deleteCharAt(paramMapString.length() - 1); 307 308 // Check if lightbox is set. Get lightbox options. 309 String lightBoxOptions = ""; 310 boolean lightBoxShow = (getInquiryLink().getLightBox() != null); 311 if (lightBoxShow) { 312 lightBoxOptions = getInquiryLink().getLightBox().getTemplateOptionsJSString(); 313 } 314 315 // Create onlick script to open the inquiry window on the click event 316 // of the direct inquiry 317 StringBuilder onClickScript = new StringBuilder("showDirectInquiry(\""); 318 onClickScript.append(inquiryUrl); 319 onClickScript.append("\", \""); 320 onClickScript.append(paramMapString); 321 onClickScript.append("\", "); 322 onClickScript.append(lightBoxShow); 323 onClickScript.append(", "); 324 onClickScript.append(lightBoxOptions); 325 onClickScript.append(");"); 326 327 directInquiryAction.setPerformDirtyValidation(false); 328 directInquiryAction.setActionScript(onClickScript.toString()); 329 330 setRender(true); 331 } 332 } 333 334 /** 335 * Gets text to prepend to the inquiry link title 336 * 337 * @param dataObjectClass data object class being inquired into 338 * @return title prepend text 339 */ 340 public String createTitleText(Class<?> dataObjectClass) { 341 String titleText = ""; 342 343 String titlePrefixProp = CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString( 344 INQUIRY_TITLE_PREFIX); 345 if (StringUtils.isNotBlank(titlePrefixProp)) { 346 titleText += titlePrefixProp + " "; 347 } 348 349 String objectLabel = KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getDataObjectEntry( 350 dataObjectClass.getName()).getObjectLabel(); 351 if (StringUtils.isNotBlank(objectLabel)) { 352 titleText += objectLabel + " "; 353 } 354 355 return titleText; 356 } 357 358 /** 359 * @see org.kuali.rice.krad.uif.component.ComponentBase#getComponentsForLifecycle() 360 */ 361 @Override 362 public List<Component> getComponentsForLifecycle() { 363 List<Component> components = super.getComponentsForLifecycle(); 364 365 components.add(getInquiryLink()); 366 components.add(getDirectInquiryAction()); 367 368 return components; 369 } 370 371 /** 372 * Returns the URL for the inquiry for which parameters will be added 373 * 374 * <p> 375 * The base URL includes the domain, context, and controller mapping for the inquiry invocation. Parameters are 376 * then added based on configuration to complete the URL. This is generally defaulted to the application URL and 377 * internal KRAD servlet mapping, but can be changed to invoke another application such as the Rice standalone 378 * server 379 * </p> 380 * 381 * @return inquiry base URL 382 */ 383 @BeanTagAttribute(name="baseInquiryUrl") 384 public String getBaseInquiryUrl() { 385 return this.baseInquiryUrl; 386 } 387 388 /** 389 * Setter for the inquiry base url (domain, context, and controller) 390 * 391 * @param baseInquiryUrl 392 */ 393 public void setBaseInquiryUrl(String baseInquiryUrl) { 394 this.baseInquiryUrl = baseInquiryUrl; 395 } 396 397 /** 398 * Full class name the inquiry should be provided for 399 * 400 * <p> 401 * This is passed on to the inquiry request for the data object the lookup should be rendered for. This is then 402 * used by the inquiry framework to select the lookup view (if more than one inquiry view exists for the same 403 * data object class name, the {@link #getViewName()} property should be specified to select the view to render). 404 * </p> 405 * 406 * @return inquiry class name 407 */ 408 @BeanTagAttribute(name="dataObjectClassName") 409 public String getDataObjectClassName() { 410 return this.dataObjectClassName; 411 } 412 413 /** 414 * Setter for the class name that inquiry should be provided for 415 * 416 * @param dataObjectClassName 417 */ 418 public void setDataObjectClassName(String dataObjectClassName) { 419 this.dataObjectClassName = dataObjectClassName; 420 } 421 422 /** 423 * When multiple target inquiry views exists for the same data object class, the view name can be set to 424 * determine which one to use 425 * 426 * <p> 427 * When creating multiple inquiry views for the same data object class, the view name can be specified for the 428 * different versions (for example 'simple' and 'advanced'). When multiple inquiry views exist the view name must 429 * be sent with the data object class for the request. Note the view id can be alternatively used to uniquely 430 * identify the inquiry view 431 * </p> 432 */ 433 @BeanTagAttribute(name="viewName") 434 public String getViewName() { 435 return this.viewName; 436 } 437 438 /** 439 * Setter for the view name configured on the inquiry view that should be invoked by the inquiry widget 440 * 441 * @param viewName 442 */ 443 public void setViewName(String viewName) { 444 this.viewName = viewName; 445 } 446 447 /** 448 * Map that determines what properties from a calling view will be sent to properties on the inquiry data object 449 * 450 * <p> 451 * When invoking an inquiry view, a query is done against the inquiries configured data object and the resulting 452 * record is display. The values for the properties configured within the inquiry parameters Map will be 453 * pulled and passed along as values for the inquiry data object properties (thus they form the criteria for 454 * the inquiry) 455 * </p> 456 * 457 * @return mapping of calling view properties to inquiry data object properties 458 */ 459 @BeanTagAttribute(name="inquiryParameters",type= BeanTagAttribute.AttributeType.MAPVALUE) 460 public Map<String, String> getInquiryParameters() { 461 return this.inquiryParameters; 462 } 463 464 /** 465 * Setter for the map that determines what property values on the calling view will be sent to properties on the 466 * inquiry data object 467 * 468 * @param inquiryParameters 469 */ 470 public void setInquiryParameters(Map<String, String> inquiryParameters) { 471 this.inquiryParameters = inquiryParameters; 472 } 473 474 /** 475 * {@code Link} that will be rendered for an inquiry 476 * 477 * @return the inquiry link 478 */ 479 @BeanTagAttribute(name="inquiryLink",type= BeanTagAttribute.AttributeType.SINGLEBEAN) 480 public Link getInquiryLink() { 481 return this.inquiryLink; 482 } 483 484 /** 485 * Setter for the inquiry {@code Link} 486 * 487 * @param inquiryLink the inquiry {@link Link} object 488 */ 489 public void setInquiryLink(Link inquiryLink) { 490 this.inquiryLink = inquiryLink; 491 } 492 493 /** 494 * {@code Action} that will be rendered next to the field for a direct inquiry 495 * 496 * @return the directInquiryAction 497 */ 498 @BeanTagAttribute(name="directInquiryAction",type= BeanTagAttribute.AttributeType.SINGLEBEAN) 499 public Action getDirectInquiryAction() { 500 return this.directInquiryAction; 501 } 502 503 /** 504 * Setter for the direct inquiry {@code Action} 505 * 506 * @param directInquiryAction the direct inquiry {@link Action} 507 */ 508 public void setDirectInquiryAction(Action directInquiryAction) { 509 this.directInquiryAction = directInquiryAction; 510 } 511 512 /** 513 * Indicates that the direct inquiry will not be rendered 514 * 515 * @return true if the direct inquiry should be rendered, false if not 516 */ 517 @BeanTagAttribute(name="enableDirectInquiry") 518 public boolean isEnableDirectInquiry() { 519 return enableDirectInquiry; 520 } 521 522 /** 523 * Setter for the hideDirectInquiry flag 524 * 525 * @param enableDirectInquiry 526 */ 527 public void setEnableDirectInquiry(boolean enableDirectInquiry) { 528 this.enableDirectInquiry = enableDirectInquiry; 529 } 530 531 /** 532 * Determines whether a normal or direct inquiry should be enabled 533 * 534 * @return true if parent component is read only, false otherwise 535 */ 536 protected boolean isParentReadOnly() { 537 return parentReadOnly; 538 } 539 540 /** 541 * Determines whether a normal or direct inquiry should be enabled 542 * 543 * <p> 544 * Used by unit tests and internally 545 * </p> 546 * 547 * @param parentReadOnly true if parent component is read only, false otherwise 548 */ 549 protected void setParentReadOnly(boolean parentReadOnly) { 550 this.parentReadOnly = parentReadOnly; 551 } 552 }