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.krad.uif.widget; 017 018import java.io.Serializable; 019import java.util.List; 020 021import org.apache.commons.lang.StringUtils; 022import org.kuali.rice.krad.datadictionary.parse.BeanTag; 023import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute; 024import org.kuali.rice.krad.uif.component.BindingInfo; 025import org.kuali.rice.krad.uif.component.MethodInvokerConfig; 026import org.kuali.rice.krad.uif.field.AttributeQuery; 027import org.kuali.rice.krad.uif.field.InputField; 028import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle; 029import org.kuali.rice.krad.uif.util.LifecycleElement; 030import org.kuali.rice.krad.uif.util.ScriptUtils; 031import org.kuali.rice.krad.uif.view.View; 032 033/** 034 * Widget that provides dynamic select options to the user as they are entering the value (also known 035 * as auto-complete). 036 * 037 * <p>Widget is backed by an {@link org.kuali.rice.krad.uif.field.AttributeQuery} that provides the configuration 038 * for executing a query server side that will retrieve the valid option values.</p> 039 * 040 * @author Kuali Rice Team (rice.collab@kuali.org) 041 */ 042@BeanTag(name = "suggest", parent = "Uif-Suggest") 043public class Suggest extends WidgetBase { 044 private static final long serialVersionUID = 7373706855319347225L; 045 046 private AttributeQuery suggestQuery; 047 048 private String valuePropertyName; 049 private String labelPropertyName; 050 private List<String> additionalPropertiesToReturn; 051 052 private boolean returnFullQueryObject; 053 054 private boolean retrieveAllSuggestions; 055 private List<Object> suggestOptions; 056 057 private String suggestOptionsJsString; 058 059 public Suggest() { 060 super(); 061 } 062 063 /** 064 * The following updates are done here: 065 * 066 * <ul> 067 * <li>Invoke expression evaluation on the suggestQuery</li> 068 * </ul> 069 * 070 * {@inheritDoc} 071 */ 072 public void performApplyModel(Object model, LifecycleElement parent) { 073 super.performApplyModel(model, parent); 074 075 if (suggestQuery != null) { 076 ViewLifecycle.getExpressionEvaluator().evaluateExpressionsOnConfigurable(ViewLifecycle.getView(), 077 suggestQuery, getContext()); 078 } 079 } 080 081 /** 082 * The following actions are performed: 083 * 084 * <ul> 085 * <li>Adjusts the query field mappings on the query based on the binding configuration of the field</li> 086 * <li>TODO: determine query if render is true and query is not set</li> 087 * </ul> 088 * 089 * {@inheritDoc} 090 */ 091 @Override 092 public void performFinalize(Object model, LifecycleElement parent) { 093 super.performFinalize(model, parent); 094 095 // check for necessary configuration 096 if (!isSuggestConfigured()) { 097 setRender(false); 098 } 099 100 if (!isRender()) { 101 return; 102 } 103 104 if (retrieveAllSuggestions) { 105 if (suggestOptions == null || suggestOptions.isEmpty()) { 106 // execute query method to retrieve up front suggestions 107 if (suggestQuery.hasConfiguredMethod()) { 108 retrieveSuggestOptions(ViewLifecycle.getView()); 109 } 110 } else { 111 suggestOptionsJsString = ScriptUtils.translateValue(suggestOptions); 112 } 113 } else { 114 // adjust from side on query field mapping to match parent fields path 115 InputField field = (InputField) parent; 116 117 BindingInfo bindingInfo = field.getBindingInfo(); 118 suggestQuery.updateQueryFieldMapping(bindingInfo); 119 120 if (suggestQuery != null) { 121 suggestQuery.defaultQueryTarget(ViewLifecycle.getHelper()); 122 } 123 } 124 } 125 126 /** 127 * Indicates whether the suggest widget has the necessary configuration to render 128 * 129 * @return true if the necessary configuration is present, false if not 130 */ 131 public boolean isSuggestConfigured() { 132 if (StringUtils.isNotBlank(valuePropertyName) || suggestQuery.hasConfiguredMethod() || 133 (suggestOptions != null && !suggestOptions.isEmpty())) { 134 return true; 135 } 136 137 return false; 138 } 139 140 /** 141 * Invokes the configured query method and sets the returned method value as the suggest options or 142 * suggest options JS string 143 * 144 * @param view view instance the suggest belongs to, used to get the view helper service if needed 145 */ 146 protected void retrieveSuggestOptions(View view) { 147 String queryMethodToCall = suggestQuery.getQueryMethodToCall(); 148 MethodInvokerConfig queryMethodInvoker = suggestQuery.getQueryMethodInvokerConfig(); 149 150 if (queryMethodInvoker == null) { 151 queryMethodInvoker = new MethodInvokerConfig(); 152 } 153 154 // if method not set on invoker, use queryMethodToCall, note staticMethod could be set(don't know since 155 // there is not a getter), if so it will override the target method in prepare 156 if (StringUtils.isBlank(queryMethodInvoker.getTargetMethod())) { 157 queryMethodInvoker.setTargetMethod(queryMethodToCall); 158 } 159 160 // if target class or object not set, use view helper service 161 if ((queryMethodInvoker.getTargetClass() == null) && (queryMethodInvoker.getTargetObject() == null)) { 162 queryMethodInvoker.setTargetObject(view.getViewHelperService()); 163 } 164 165 try { 166 queryMethodInvoker.prepare(); 167 168 Object methodResult = queryMethodInvoker.invoke(); 169 if (methodResult instanceof String) { 170 suggestOptionsJsString = (String) methodResult; 171 } else if (methodResult instanceof List) { 172 suggestOptions = (List<Object>) methodResult; 173 suggestOptionsJsString = ScriptUtils.translateValue(suggestOptions); 174 } else { 175 throw new RuntimeException("Suggest query method did not return List<String> for suggestions"); 176 } 177 } catch (Exception e) { 178 throw new RuntimeException("Unable to invoke query method: " + queryMethodInvoker.getTargetMethod(), e); 179 } 180 } 181 182 /** 183 * Returns object containing post data to store for the suggest request. 184 * 185 * @return suggest post data instance 186 */ 187 public SuggestPostData getPostData() { 188 return new SuggestPostData(this); 189 } 190 191 /** 192 * Attribute query instance the will be executed to provide 193 * the suggest options 194 * 195 * @return AttributeQuery 196 */ 197 @BeanTagAttribute(type = BeanTagAttribute.AttributeType.DIRECTORBYTYPE) 198 public AttributeQuery getSuggestQuery() { 199 return suggestQuery; 200 } 201 202 /** 203 * Setter for the suggest attribute query 204 * 205 * @param suggestQuery 206 */ 207 public void setSuggestQuery(AttributeQuery suggestQuery) { 208 this.suggestQuery = suggestQuery; 209 } 210 211 /** 212 * Name of the property on the query result object that provides 213 * the options for the suggest, values from this field will be 214 * collected and sent back on the result to provide as suggest options. 215 * 216 * <p>If a labelPropertyName is also set, 217 * the property specified by it will be used as the label the user selects (the suggestion), but the value will 218 * be the value retrieved by this property. If only one of labelPropertyName or valuePropertyName is set, 219 * the property's value on the object will be used for both the value inserted on selection and the suggestion 220 * text (most default cases only a valuePropertyName would be set).</p> 221 * 222 * @return source property name 223 */ 224 @BeanTagAttribute 225 public String getValuePropertyName() { 226 return valuePropertyName; 227 } 228 229 /** 230 * Setter for the value property name 231 * 232 * @param valuePropertyName 233 */ 234 public void setValuePropertyName(String valuePropertyName) { 235 this.valuePropertyName = valuePropertyName; 236 } 237 238 /** 239 * Name of the property on the query result object that provides the label for the suggestion. 240 * 241 * <p>This should 242 * be set when the label that the user selects is different from the value that is inserted when a user selects a 243 * suggestion. If only one of labelPropertyName or valuePropertyName is set, 244 * the property's value on the object will be used for both the value inserted on selection and the suggestion 245 * text (most default cases only a valuePropertyName would be set).</p> 246 * 247 * @return labelPropertyName representing the property to use for the suggestion label of the item 248 */ 249 @BeanTagAttribute 250 public String getLabelPropertyName() { 251 return labelPropertyName; 252 } 253 254 /** 255 * Set the labelPropertyName 256 * 257 * @param labelPropertyName 258 */ 259 public void setLabelPropertyName(String labelPropertyName) { 260 this.labelPropertyName = labelPropertyName; 261 } 262 263 /** 264 * List of additional properties to return in the result objects to the plugin's success callback. 265 * 266 * <p>In most cases, this should not be set. The main use case 267 * of setting this list is to use additional properties in the select function on the plugin's options, so 268 * it is only recommended that this property be set when doing heavy customization to the select function. 269 * This list is not used if the full result object is already being returned.</p> 270 * 271 * @return the list of additional properties to send back 272 */ 273 @BeanTagAttribute 274 public List<String> getAdditionalPropertiesToReturn() { 275 return additionalPropertiesToReturn; 276 } 277 278 /** 279 * Set the list of additional properties to return to the plugin success callback results 280 * 281 * @param additionalPropertiesToReturn 282 */ 283 public void setAdditionalPropertiesToReturn(List<String> additionalPropertiesToReturn) { 284 this.additionalPropertiesToReturn = additionalPropertiesToReturn; 285 } 286 287 /** 288 * When set to true the results of a query method will be sent back as-is (in translated form) with all properties 289 * intact. 290 * 291 * <p> 292 * Note this is not supported for highly complex objects (ie, most auto-query objects - will throw exception). 293 * Intended usage of this flag is with custom query methods which return simple data objects. 294 * The query method can return a list of Strings which will be used for the suggestions, a list of objects 295 * with 'label' and 'value' properties, or a custom object. In the case of using a customObject 296 * labelPropertyName or valuePropertyName MUST be specified (or both) OR the custom object must contain a 297 * property named "label" or "value" (or both) for the suggestions to appear. In cases where this is not used, 298 * the data sent back represents a slim subset of the properties on the object. 299 * </p> 300 * 301 * @return true if the query method results should be used as the suggestions, false to assume 302 * objects are returned and suggestions are formed using the source property name 303 */ 304 @BeanTagAttribute 305 public boolean isReturnFullQueryObject() { 306 return returnFullQueryObject; 307 } 308 309 /** 310 * Setter for the for returning the full object of the query 311 * 312 * @param returnFullQueryObject 313 */ 314 public void setReturnFullQueryObject(boolean returnFullQueryObject) { 315 this.returnFullQueryObject = returnFullQueryObject; 316 } 317 318 /** 319 * Indicates whether all suggest options should be retrieved up front and provide to the suggest 320 * widget as options locally 321 * 322 * <p> 323 * Use this for a small list of options to improve performance. The query will be performed on the client 324 * to filter the provider options based on the users input instead of doing a query each time 325 * </p> 326 * 327 * <p> 328 * When a query method is configured and this option set to true the method will be invoked to set the 329 * options. The query method should not take any arguments and should return the suggestion options 330 * List or the JS String as a result. If a query method is not configured the suggest options can be 331 * set through configuration or a view helper method (for example a component finalize method) 332 * </p> 333 * 334 * @return true to provide the suggest options initially, false to use ajax retrieval based on the 335 * user's input 336 */ 337 @BeanTagAttribute 338 public boolean isRetrieveAllSuggestions() { 339 return retrieveAllSuggestions; 340 } 341 342 /** 343 * Setter for the retrieve all suggestions indicator 344 * 345 * @param retrieveAllSuggestions 346 */ 347 public void setRetrieveAllSuggestions(boolean retrieveAllSuggestions) { 348 this.retrieveAllSuggestions = retrieveAllSuggestions; 349 } 350 351 /** 352 * When {@link #isRetrieveAllSuggestions()} is true, this list provides the full list of suggestions 353 * 354 * <p> 355 * If a query method is configured that method will be invoked to populate this list, otherwise the 356 * list should be populated through configuration or the view helper 357 * </p> 358 * 359 * <p> 360 * The suggest options can either be a list of Strings, in which case the strings will be the suggested 361 * values. Or a list of objects. If the object does not have 'label' and 'value' properties, a custom render 362 * and select method must be provided 363 * </p> 364 * 365 * @return list of suggest options 366 */ 367 @BeanTagAttribute 368 public List<Object> getSuggestOptions() { 369 return suggestOptions; 370 } 371 372 /** 373 * Setter for the list of suggest options 374 * 375 * @param suggestOptions 376 */ 377 public void setSuggestOptions(List<Object> suggestOptions) { 378 this.suggestOptions = suggestOptions; 379 } 380 381 /** 382 * Returns the suggest options as a JS String (set by the framework from method invocation) 383 * 384 * @return suggest options JS string 385 */ 386 public String getSuggestOptionsJsString() { 387 if (StringUtils.isNotBlank(suggestOptionsJsString)) { 388 return this.suggestOptionsJsString; 389 } 390 391 return "null"; 392 } 393 394 /** 395 * Sets suggest options javascript string 396 * 397 * @param suggestOptionsJsString 398 */ 399 public void setSuggestOptionsJsString(String suggestOptionsJsString) { 400 this.suggestOptionsJsString = suggestOptionsJsString; 401 } 402 403 /** 404 * Holds post data for the suggest component. 405 */ 406 public static class SuggestPostData implements Serializable { 407 private static final long serialVersionUID = 997780560864981128L; 408 409 private String id; 410 411 private AttributeQuery suggestQuery; 412 413 private String valuePropertyName; 414 private String labelPropertyName; 415 private List<String> additionalPropertiesToReturn; 416 private boolean returnFullQueryObject; 417 private boolean retrieveAllSuggestions; 418 419 /** 420 * Constructor taking suggest widget to pull post data from. 421 * 422 * @param suggest component instance to pull data 423 */ 424 public SuggestPostData(Suggest suggest) { 425 this.id = suggest.getId(); 426 this.suggestQuery = suggest.getSuggestQuery(); 427 this.valuePropertyName = suggest.getValuePropertyName(); 428 this.labelPropertyName = suggest.getLabelPropertyName(); 429 this.additionalPropertiesToReturn = suggest.getAdditionalPropertiesToReturn(); 430 this.returnFullQueryObject = suggest.isReturnFullQueryObject(); 431 this.retrieveAllSuggestions = suggest.isRetrieveAllSuggestions(); 432 } 433 434 /** 435 * @see org.kuali.rice.krad.uif.util.LifecycleElement#getId() 436 */ 437 public String getId() { 438 return id; 439 } 440 441 /** 442 * @see Suggest#getSuggestQuery() 443 */ 444 public AttributeQuery getSuggestQuery() { 445 return suggestQuery; 446 } 447 448 /** 449 * @see Suggest#getValuePropertyName() 450 */ 451 public String getValuePropertyName() { 452 return valuePropertyName; 453 } 454 455 /** 456 * @see Suggest#getLabelPropertyName() 457 */ 458 public String getLabelPropertyName() { 459 return labelPropertyName; 460 } 461 462 /** 463 * @see Suggest#getAdditionalPropertiesToReturn() 464 */ 465 public List<String> getAdditionalPropertiesToReturn() { 466 return additionalPropertiesToReturn; 467 } 468 469 /** 470 * @see Suggest#isReturnFullQueryObject() 471 */ 472 public boolean isReturnFullQueryObject() { 473 return returnFullQueryObject; 474 } 475 476 /** 477 * @see Suggest#isRetrieveAllSuggestions() 478 */ 479 public boolean isRetrieveAllSuggestions() { 480 return retrieveAllSuggestions; 481 } 482 } 483}