View Javadoc

1   /**
2    * Copyright 2005-2013 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 com.google.common.collect.Lists;
19  import org.apache.commons.lang.StringUtils;
20  import org.kuali.rice.krad.datadictionary.parse.BeanTag;
21  import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
22  import org.kuali.rice.krad.uif.component.BindingInfo;
23  import org.kuali.rice.krad.uif.component.Component;
24  import org.kuali.rice.krad.uif.component.MethodInvokerConfig;
25  import org.kuali.rice.krad.uif.field.AttributeQuery;
26  import org.kuali.rice.krad.uif.field.InputField;
27  import org.kuali.rice.krad.uif.util.ScriptUtils;
28  import org.kuali.rice.krad.uif.view.View;
29  
30  import java.util.ArrayList;
31  import java.util.List;
32  
33  /**
34   * Widget that provides dynamic select options to the user as they
35   * are entering the value (also known as auto-complete)
36   *
37   * <p>
38   * Widget is backed by an <code>AttributeQuery</code> that provides
39   * the configuration for executing a query server side that will retrieve
40   * the valid option values
41   * </p>
42   *
43   * @author Kuali Rice Team (rice.collab@kuali.org)
44   */
45  @BeanTag(name = "suggest-bean", parent = "Uif-Suggest")
46  public class Suggest extends WidgetBase {
47      private static final long serialVersionUID = 7373706855319347225L;
48  
49      private AttributeQuery suggestQuery;
50  
51      private String valuePropertyName;
52      private String labelPropertyName;
53      private List<String> additionalPropertiesToReturn;
54  
55      private boolean returnFullQueryObject;
56  
57      private boolean retrieveAllSuggestions;
58      private List<Object> suggestOptions;
59  
60      private String suggestOptionsJsString;
61  
62      public Suggest() {
63          super();
64      }
65  
66      /**
67       * The following updates are done here:
68       *
69       * <ul>
70       * <li>Invoke expression evaluation on the suggestQuery</li>
71       * </ul>
72       */
73      public void performApplyModel(View view, Object model, Component parent) {
74          super.performApplyModel(view, model, parent);
75  
76          if (suggestQuery != null) {
77              view.getViewHelperService().getExpressionEvaluator().evaluateExpressionsOnConfigurable(view,
78                      suggestQuery, getContext());
79          }
80      }
81  
82      /**
83       * The following actions are performed:
84       *
85       * <ul>
86       * <li>Adjusts the query field mappings on the query based on the binding configuration of the field</li>
87       * <li>TODO: determine query if render is true and query is not set</li>
88       * </ul>
89       *
90       * @see org.kuali.rice.krad.uif.component.ComponentBase#performFinalize(org.kuali.rice.krad.uif.view.View,
91       *      java.lang.Object, org.kuali.rice.krad.uif.component.Component)
92       */
93      @Override
94      public void performFinalize(View view, Object model, Component parent) {
95          super.performFinalize(view, model, parent);
96  
97          // check for necessary configuration
98          if (!isSuggestConfigured()) {
99             setRender(false);
100         }
101 
102         if (!isRender()) {
103             return;
104         }
105 
106         if (retrieveAllSuggestions) {
107             if (suggestOptions == null || suggestOptions.isEmpty()) {
108                 // execute query method to retrieve up front suggestions
109                 if (suggestQuery.hasConfiguredMethod()) {
110                     retrieveSuggestOptions(view);
111                 }
112             } else {
113                 suggestOptionsJsString = ScriptUtils.translateValue(suggestOptions);
114             }
115         } else {
116             // adjust from side on query field mapping to match parent fields path
117             InputField field = (InputField) parent;
118 
119             BindingInfo bindingInfo = field.getBindingInfo();
120             suggestQuery.updateQueryFieldMapping(bindingInfo);
121         }
122     }
123 
124     /**
125      * Indicates whether the suggest widget has the necessary configuration to render
126      *
127      * @return true if the necessary configuration is present, false if not
128      */
129     public boolean isSuggestConfigured() {
130         if (StringUtils.isNotBlank(valuePropertyName) ||
131                 suggestQuery.hasConfiguredMethod() ||
132                 (suggestOptions != null && !suggestOptions.isEmpty())) {
133             return true;
134         }
135 
136         return false;
137     }
138 
139     /**
140      * Invokes the configured query method and sets the returned method value as the suggest options or
141      * suggest options JS string
142      *
143      * @param view view instance the suggest belongs to, used to get the view helper service if needed
144      */
145     protected void retrieveSuggestOptions(View view) {
146         String queryMethodToCall = suggestQuery.getQueryMethodToCall();
147         MethodInvokerConfig queryMethodInvoker = suggestQuery.getQueryMethodInvokerConfig();
148 
149         if (queryMethodInvoker == null) {
150             queryMethodInvoker = new MethodInvokerConfig();
151         }
152 
153         // if method not set on invoker, use queryMethodToCall, note staticMethod could be set(don't know since
154         // there is not a getter), if so it will override the target method in prepare
155         if (StringUtils.isBlank(queryMethodInvoker.getTargetMethod())) {
156             queryMethodInvoker.setTargetMethod(queryMethodToCall);
157         }
158 
159         // if target class or object not set, use view helper service
160         if ((queryMethodInvoker.getTargetClass() == null) && (queryMethodInvoker.getTargetObject() == null)) {
161             queryMethodInvoker.setTargetObject(view.getViewHelperService());
162         }
163 
164         try {
165             queryMethodInvoker.prepare();
166 
167             Object methodResult = queryMethodInvoker.invoke();
168             if (methodResult instanceof String) {
169                 suggestOptionsJsString = (String) methodResult;
170             } else if (methodResult instanceof List) {
171                 suggestOptions = (List<Object>) methodResult;
172                 suggestOptionsJsString = ScriptUtils.translateValue(suggestOptions);
173             } else {
174                 throw new RuntimeException("Suggest query method did not return List<String> for suggestions");
175             }
176         } catch (Exception e) {
177             throw new RuntimeException("Unable to invoke query method: " + queryMethodInvoker.getTargetMethod(), e);
178         }
179     }
180 
181     /**
182      * Attribute query instance the will be executed to provide
183      * the suggest options
184      *
185      * @return AttributeQuery
186      */
187     @BeanTagAttribute(name = "suggestQuery", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
188     public AttributeQuery getSuggestQuery() {
189         return suggestQuery;
190     }
191 
192     /**
193      * Setter for the suggest attribute query
194      *
195      * @param suggestQuery
196      */
197     public void setSuggestQuery(AttributeQuery suggestQuery) {
198         this.suggestQuery = suggestQuery;
199     }
200 
201     /**
202      * Name of the property on the query result object that provides
203      * the options for the suggest, values from this field will be
204      * collected and sent back on the result to provide as suggest options.
205      *
206      * <p>If a labelPropertyName is also set,
207      * the property specified by it will be used as the label the user selects (the suggestion), but the value will
208      * be the value retrieved by this property.  If only one of labelPropertyName or valuePropertyName is set,
209      * the property's value on the object will be used for both the value inserted on selection and the suggestion
210      * text (most default cases only a valuePropertyName would be set).</p>
211      *
212      * @return source property name
213      */
214     @BeanTagAttribute(name = "valuePropertyName")
215     public String getValuePropertyName() {
216         return valuePropertyName;
217     }
218 
219     /**
220      * Setter for the value property name
221      *
222      * @param valuePropertyName
223      */
224     public void setValuePropertyName(String valuePropertyName) {
225         this.valuePropertyName = valuePropertyName;
226     }
227 
228     /**
229      * Name of the property on the query result object that provides the label for the suggestion.
230      *
231      * <p>This should
232      * be set when the label that the user selects is different from the value that is inserted when a user selects a
233      * suggestion. If only one of labelPropertyName or valuePropertyName is set,
234      * the property's value on the object will be used for both the value inserted on selection and the suggestion
235      * text (most default cases only a valuePropertyName would be set).</p>
236      *
237      * @return labelPropertyName representing the property to use for the suggestion label of the item
238      */
239     @BeanTagAttribute(name = "labelPropertyName")
240     public String getLabelPropertyName() {
241         return labelPropertyName;
242     }
243 
244     /**
245      * Set the labelPropertyName
246      *
247      * @param labelPropertyName
248      */
249     public void setLabelPropertyName(String labelPropertyName) {
250         this.labelPropertyName = labelPropertyName;
251     }
252 
253     /**
254      * List of additional properties to return in the result objects to the plugin's success callback.
255      *
256      * <p>In most cases, this should not be set.  The main use case
257      * of setting this list is to use additional properties in the select function on the plugin's options, so
258      * it is only recommended that this property be set when doing heavy customization to the select function.
259      * This list is not used if the full result object is already being returned.</p>
260      *
261      * @return the list of additional properties to send back
262      */
263     @BeanTagAttribute(name = "additionalPropertiesToReturn", type = BeanTagAttribute.AttributeType.LISTVALUE)
264     public List<String> getAdditionalPropertiesToReturn() {
265         return additionalPropertiesToReturn;
266     }
267 
268     /**
269      * Set the list of additional properties to return to the plugin success callback results
270      *
271      * @param additionalPropertiesToReturn
272      */
273     public void setAdditionalPropertiesToReturn(List<String> additionalPropertiesToReturn) {
274         this.additionalPropertiesToReturn = additionalPropertiesToReturn;
275     }
276 
277     /**
278      * When set to true the results of a query method will be sent back as-is (in translated form) with all properties
279      * intact.
280      *
281      * <p>
282      * Note this is not supported for highly complex objects (ie, most auto-query objects - will throw exception).
283      * Intended usage of this flag is with custom query methods which return simple data objects.
284      * The query method can return a list of Strings which will be used for the suggestions, a list of objects
285      * with 'label' and 'value' properties, or a custom object.  In the case of using a customObject
286      * labelPropertyName or valuePropertyName MUST be specified (or both) OR the custom object must contain a
287      * property named "label" or "value" (or both) for the suggestions to appear.  In cases where this is not used,
288      * the data sent back represents a slim subset of the properties on the object.
289      * </p>
290      *
291      * @return true if the query method results should be used as the suggestions, false to assume
292      *         objects are returned and suggestions are formed using the source property name
293      */
294     @BeanTagAttribute(name = "returnFullQueryObject")
295     public boolean isReturnFullQueryObject() {
296         return returnFullQueryObject;
297     }
298 
299     /**
300      * Setter for the for returning the full object of the query
301      *
302      * @param returnFullQueryObject
303      */
304     public void setReturnFullQueryObject(boolean returnFullQueryObject) {
305         this.returnFullQueryObject = returnFullQueryObject;
306     }
307 
308     /**
309      * Indicates whether all suggest options should be retrieved up front and provide to the suggest
310      * widget as options locally
311      *
312      * <p>
313      * Use this for a small list of options to improve performance. The query will be performed on the client
314      * to filter the provider options based on the users input instead of doing a query each time
315      * </p>
316      *
317      * <p>
318      * When a query method is configured and this option set to true the method will be invoked to set the
319      * options. The query method should not take any arguments and should return the suggestion options
320      * List or the JS String as a result. If a query method is not configured the suggest options can be
321      * set through configuration or a view helper method (for example a component finalize method)
322      * </p>
323      *
324      * @return true to provide the suggest options initially, false to use ajax retrieval based on the
325      *         user's input
326      */
327     @BeanTagAttribute(name = "retrieveAllSuggestions")
328     public boolean isRetrieveAllSuggestions() {
329         return retrieveAllSuggestions;
330     }
331 
332     /**
333      * Setter for the retrieve all suggestions indicator
334      *
335      * @param retrieveAllSuggestions
336      */
337     public void setRetrieveAllSuggestions(boolean retrieveAllSuggestions) {
338         this.retrieveAllSuggestions = retrieveAllSuggestions;
339     }
340 
341     /**
342      * When {@link #isRetrieveAllSuggestions()} is true, this list provides the full list of suggestions
343      *
344      * <p>
345      * If a query method is configured that method will be invoked to populate this list, otherwise the
346      * list should be populated through configuration or the view helper
347      * </p>
348      *
349      * <p>
350      * The suggest options can either be a list of Strings, in which case the strings will be the suggested
351      * values. Or a list of objects. If the object does not have 'label' and 'value' properties, a custom render
352      * and select method must be provided
353      * </p>
354      *
355      * @return list of suggest options
356      */
357     @BeanTagAttribute(name = "suggestOptions", type = BeanTagAttribute.AttributeType.LISTBEAN)
358     public List<Object> getSuggestOptions() {
359         return suggestOptions;
360     }
361 
362     /**
363      * Setter for the list of suggest options
364      *
365      * @param suggestOptions
366      */
367     public void setSuggestOptions(List<Object> suggestOptions) {
368         this.suggestOptions = suggestOptions;
369     }
370 
371     /**
372      * Returns the suggest options as a JS String (set by the framework from method invocation)
373      *
374      * @return suggest options JS string
375      */
376     public String getSuggestOptionsJsString() {
377         if (StringUtils.isNotBlank(suggestOptionsJsString)) {
378             return this.suggestOptionsJsString;
379         }
380 
381         return "null";
382     }
383 
384     /**
385      * Sets suggest options javascript string
386      *
387      * @param suggestOptionsJsString
388      */
389     public void setSuggestOptionsJsString(String suggestOptionsJsString) {
390         this.suggestOptionsJsString = suggestOptionsJsString;
391     }
392 
393     /**
394      * @see org.kuali.rice.krad.uif.component.ComponentBase#copy()
395      */
396     @Override
397     protected <T> void copyProperties(T component) {
398         super.copyProperties(component);
399         Suggest suggestCopy = (Suggest) component;
400         suggestCopy.setValuePropertyName(this.getValuePropertyName());
401         suggestCopy.setLabelPropertyName(this.getLabelPropertyName());
402 
403         if(additionalPropertiesToReturn != null) {
404             suggestCopy.setAdditionalPropertiesToReturn(new ArrayList<String> (additionalPropertiesToReturn));
405         }
406 
407         suggestCopy.setReturnFullQueryObject(this.isReturnFullQueryObject());
408         suggestCopy.setRetrieveAllSuggestions(this.isRetrieveAllSuggestions());
409 
410         if (this.suggestQuery != null) {
411             suggestCopy.setSuggestQuery((AttributeQuery)this.suggestQuery.copy());
412         }
413 
414         suggestCopy.setSuggestOptions(this.getSuggestOptions());
415         suggestCopy.setSuggestOptionsJsString(this.suggestOptionsJsString);
416     }
417 }