1 /**
2 * Copyright 2005-2014 The Kuali Foundation
3 *
4 * Licensed under the Educational Community License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.opensource.org/licenses/ecl2.php
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.kuali.rice.krad.uif.widget;
17
18 import java.io.Serializable;
19 import java.util.List;
20
21 import org.apache.commons.lang.StringUtils;
22 import org.kuali.rice.krad.datadictionary.parse.BeanTag;
23 import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
24 import org.kuali.rice.krad.uif.component.BindingInfo;
25 import org.kuali.rice.krad.uif.component.MethodInvokerConfig;
26 import org.kuali.rice.krad.uif.field.AttributeQuery;
27 import org.kuali.rice.krad.uif.field.InputField;
28 import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
29 import org.kuali.rice.krad.uif.util.LifecycleElement;
30 import org.kuali.rice.krad.uif.util.ScriptUtils;
31 import org.kuali.rice.krad.uif.view.View;
32
33 /**
34 * Widget that provides dynamic select options to the user as they are entering the value (also known
35 * as auto-complete).
36 *
37 * <p>Widget is backed by an {@link org.kuali.rice.krad.uif.field.AttributeQuery} that provides the configuration
38 * for executing a query server side that will retrieve the valid option values.</p>
39 *
40 * @author Kuali Rice Team (rice.collab@kuali.org)
41 */
42 @BeanTag(name = "suggest-bean", parent = "Uif-Suggest")
43 public class Suggest extends WidgetBase {
44 private static final long serialVersionUID = 7373706855319347225L;
45
46 private AttributeQuery suggestQuery;
47
48 private String valuePropertyName;
49 private String labelPropertyName;
50 private List<String> additionalPropertiesToReturn;
51
52 private boolean returnFullQueryObject;
53
54 private boolean retrieveAllSuggestions;
55 private List<Object> suggestOptions;
56
57 private String suggestOptionsJsString;
58
59 public Suggest() {
60 super();
61 }
62
63 /**
64 * The following updates are done here:
65 *
66 * <ul>
67 * <li>Invoke expression evaluation on the suggestQuery</li>
68 * </ul>
69 *
70 * {@inheritDoc}
71 */
72 public void performApplyModel(Object model, LifecycleElement parent) {
73 super.performApplyModel(model, parent);
74
75 if (suggestQuery != null) {
76 ViewLifecycle.getExpressionEvaluator().evaluateExpressionsOnConfigurable(ViewLifecycle.getView(),
77 suggestQuery, getContext());
78 }
79 }
80
81 /**
82 * The following actions are performed:
83 *
84 * <ul>
85 * <li>Adjusts the query field mappings on the query based on the binding configuration of the field</li>
86 * <li>TODO: determine query if render is true and query is not set</li>
87 * </ul>
88 *
89 * {@inheritDoc}
90 */
91 @Override
92 public void performFinalize(Object model, LifecycleElement parent) {
93 super.performFinalize(model, parent);
94
95 // check for necessary configuration
96 if (!isSuggestConfigured()) {
97 setRender(false);
98 }
99
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(name = "suggestQuery", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
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(name = "valuePropertyName")
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(name = "labelPropertyName")
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(name = "additionalPropertiesToReturn", type = BeanTagAttribute.AttributeType.LISTVALUE)
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(name = "returnFullQueryObject")
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(name = "retrieveAllSuggestions")
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(name = "suggestOptions", type = BeanTagAttribute.AttributeType.LISTBEAN)
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 }