View Javadoc
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", 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(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 }