View Javadoc

1   /*
2    * $Id: Autocompleter.java 651946 2008-04-27 13:41:38Z apetrelli $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  package org.apache.struts2.dojo.components;
23  
24  import java.util.Random;
25  
26  import javax.servlet.http.HttpServletRequest;
27  import javax.servlet.http.HttpServletResponse;
28  
29  import org.apache.struts2.components.ComboBox;
30  import org.apache.struts2.views.annotations.StrutsTag;
31  import org.apache.struts2.views.annotations.StrutsTagAttribute;
32  import org.apache.struts2.views.annotations.StrutsTagSkipInheritance;
33  
34  import com.opensymphony.xwork2.util.ValueStack;
35  
36  /***
37   * <!-- START SNIPPET: javadoc -->
38   * <p>The autocomplete tag is a combobox that can autocomplete text entered on the input box. If an action
39   * is used to populate the autocompleter, the output of the action must be a well formed JSON string. </p>
40   * <p>The autocompleter follows this rule to find its datasource:<p>
41   * <p>1. If the response is an array, assume that it contains 2-dimension array elements, like:
42   * <pre>
43   * [
44   *      ["Alabama", "AL"],
45   *      ["Alaska", "AK"]
46   * ]
47   * </pre>
48   * <p>2. If a value is specified in the "dataFieldName" attribute, and the response has a field with that
49   * name, assume that's the datasource, which can be an array of 2-dimension array elements, or a map, 
50   * like (assuming dataFieldName="state"):</p>
51   * <pre>
52   * {
53   *      "state" : [
54   *           ["Alabama","AL"],
55   *           ["Alaska","AK"]
56   *      ]
57   * }     
58   * or
59   * {
60   *      "state" : {
61   *            "Alabama" : "AL",
62   *            "Alaska" : "AK"
63   *      }
64   * }
65   * </pre>
66   * </pre>
67   * <p>3. If there is a field that starts with the value specified on the "name" attribute, assume 
68   * that's the datasource, like (assuming name="state"):</p>
69   * <pre>
70   * {
71   *      "states" : [
72   *           ["Alabama","AL"],
73   *           ["Alaska","AK"]
74   *      ]
75   * }
76   * </pre>
77   * <p>4. Use first array that is found, like:<p>
78   * <pre>
79   * {
80   *      "anything" : [
81   *            ["Alabama", "AL"],
82   *            ["Alaska", "AK"]
83   *     ]       
84   * }
85   * <p>5. If the response is a map, use it (recommended as it is the easiest one to generate):
86   * <pre>
87   * {
88   *      "Alabama" : "AL",
89   *      "Alaska" : "AK"
90   * }
91   * </pre>
92   * <!-- END SNIPPET: javadoc -->
93   * <p>Examples</p>
94   * <!-- START SNIPPET: example1 -->
95   * &lt;sx:autocompleter name="autocompleter1" href="%{jsonList}"/&gt;
96   * <!-- END SNIPPET: example1 -->
97   * 
98   * <!-- START SNIPPET: example2 -->
99   * &lt;s:autocompleter name="test"  list="{'apple','banana','grape','pear'}" autoComplete="false"/&gt;
100  * <!-- END SNIPPET: example2 -->
101  * 
102  * <!-- START SNIPPET: example3 -->
103  * &lt;sx:autocompleter name="mvc" href="%{jsonList}" loadOnTextChange="true" loadMinimumCount="3"/&gt;
104  * 
105  * The text entered on the autocompleter is passed as a parameter to the url specified in "href", like (text is "struts"):
106  *  
107  * http://host/example/myaction.do?mvc=struts
108  * <!-- END SNIPPET: example3 -->
109  * 
110  * <!-- START SNIPPET: example4 -->
111  * &lt;form id="selectForm"&gt;
112  *      &lt;sx:autocompleter  name="select" list="{'fruits','colors'}"  valueNotifyTopics="/changed" /&gt;
113  * &lt;/form&gt;  
114  * &lt;sx:autocompleter  href="%{jsonList}" formId="selectForm" listenTopics="/changed"/&gt;
115  * <!-- END SNIPPET: example4 -->
116  * 
117  * <!-- START SNIPPET: example5 -->
118  * &lt;sx:autocompleter  href="%{jsonList}" id="auto"/&gt;
119  * &lt;script type="text/javascript"&gt;
120  *   function getValues() {
121  *      var autoCompleter = dojo.widget.byId("auto");
122  *      
123  *      //key (in the states example above, "AL")
124  *      var key = autoCompleter.getSelectedKey();
125  *      alert(key);
126  *      
127  *      //value (in the states example above, "Alabama")
128  *      var value = autoCompleter.getSelectedValue();
129  *      alert(value);
130  *      
131  *      //text currently on the textbox (anything the user typed)
132  *      var text = autoCompleter.getText();
133  *      alert(text);
134  *   }
135  * 
136  *   function setValues() {
137  *      var autoCompleter = dojo.widget.byId("auto");
138  *      
139  *      //key (key will be set to "AL" and value to "Alabama")
140  *      autoCompleter.setSelectedKey("AL");
141  *      
142  *      //value (key will be set to "AL" and value to "Alabama")
143  *      autoCompleter.setAllValues("AL", "Alabama");
144  *   }
145  * &lt;/script&gt;
146  * <!-- END SNIPPET: example5 -->
147  * 
148  * <!-- START SNIPPET: example6 -->
149  * &lt;script type="text/javascript"&gt;
150  * dojo.event.topic.subscribe("/before", function(event, widget){
151  *     alert('inside a topic event. before request');
152  *     //event: set event.cancel = true, to cancel request
153  *     //widget: widget that published the topic
154  * });
155  * &lt;/script&gt;         
156  * 
157  * &lt;sx:autocompleter beforeNotifyTopics="/before" href="%{#ajaxTest} /&gt;
158  * <!-- END SNIPPET: example6 -->
159  * 
160  * <!-- START SNIPPET: example7 -->
161  * &lt;script type="text/javascript"&gt;
162  * dojo.event.topic.subscribe("/after", function(data, request, widget){
163  *     alert('inside a topic event. after request');
164  *     //data : JavaScript object from parsing response
165  *     //request: XMLHttpRequest object
166  *     //widget: widget that published the topic
167  * });
168  * &lt;/script&gt;        
169  * 
170  * &lt;sx:autocompleter afterNotifyTopics="/after" href="%{#ajaxTest}" /&gt;
171  * <!-- END SNIPPET: example7 -->
172  * 
173  * <!-- START SNIPPET: example8-->
174  * &lt;script type="text/javascript"&gt;
175  * dojo.event.topic.subscribe("/error", function(error, request, widget){
176  *     alert('inside a topic event. on error');
177  *     //error : error object (error.message has the error message)
178  *     //request: XMLHttpRequest object
179  *     //widget: widget that published the topic
180  * });
181  * &lt;/script&gt;
182  * 
183  * &lt;sx:autocompleter errorNotifyTopics="/error" href="%{#ajaxTest}" /&gt;
184  * <!-- END SNIPPET: example8 -->
185  * 
186  * <!-- START SNIPPET: example9 -->
187  * &lt;script type="text/javascript"&gt;
188  * dojo.event.topic.subscribe("/value", function(value, key, text, widget){
189  *     alert('inside a topic event. after value changed');
190  *     //value : selected value (like "Florida" in example above)
191  *     //key: selected key (like "FL" in example above)
192  *     //text: text typed into textbox
193  *     //widget: widget that published the topic
194  * });
195  * &lt;/script&gt;   
196  * 
197  * &lt;sx:autocompleter valueNotifyTopics="/value" href="%{#ajaxTest}" /&gt;
198  * <!-- END SNIPPET: example9 -->
199  */
200 @StrutsTag(name="autocompleter", tldTagClass="org.apache.struts2.dojo.views.jsp.ui.AutocompleterTag", description="Renders a combobox with autocomplete and AJAX capabilities")
201 public class Autocompleter extends ComboBox {
202     public static final String TEMPLATE = "autocompleter";
203     final private static String COMPONENT_NAME = Autocompleter.class.getName();
204     private final static transient Random RANDOM = new Random();
205 
206     protected String forceValidOption;
207     protected String searchType;
208     protected String autoComplete;
209     protected String delay;
210     protected String disabled;
211     protected String href;
212     protected String dropdownWidth;
213     protected String dropdownHeight;
214     protected String formId;
215     protected String formFilter;
216     protected String listenTopics;
217     protected String notifyTopics;
218     protected String indicator;
219     protected String loadOnTextChange;
220     protected String loadMinimumCount;
221     protected String showDownArrow;
222     protected String templateCssPath;
223     protected String iconPath;
224     protected String keyName;
225     protected String dataFieldName;
226     protected String beforeNotifyTopics;
227     protected String afterNotifyTopics;
228     protected String errorNotifyTopics;
229     protected String valueNotifyTopics;
230     protected String resultsLimit;
231     protected String transport;
232     protected String preload;
233     protected String keyValue;
234         
235     public Autocompleter(ValueStack stack, HttpServletRequest request,
236             HttpServletResponse response) {
237         super(stack, request, response);
238     }
239 
240     protected String getDefaultTemplate() {
241         return TEMPLATE;
242     }
243 
244     public String getComponentName() {
245         return COMPONENT_NAME;
246     }
247 
248 
249     public void evaluateExtraParams() {
250         super.evaluateExtraParams();
251 
252         if (forceValidOption != null)
253             addParameter("forceValidOption", findValue(forceValidOption,
254                     Boolean.class));
255         if (searchType != null) {
256             String type =  findString(searchType);
257             if(type != null)
258                 addParameter("searchType", type.toUpperCase());
259         }
260         if (autoComplete != null)
261             addParameter("autoComplete", findValue(autoComplete, Boolean.class));
262         if (delay != null)
263             addParameter("delay", findValue(delay, Integer.class));
264         if (disabled != null)
265             addParameter("disabled", findValue(disabled, Boolean.class));
266         if (href != null) {
267             addParameter("href", findString(href));
268             addParameter("mode", "remote");
269         }
270         if (dropdownHeight != null)
271             addParameter("dropdownHeight", findValue(dropdownHeight, Integer.class));
272         if (dropdownWidth != null)
273             addParameter("dropdownWidth", findValue(dropdownWidth, Integer.class));
274         if (formFilter != null)
275           addParameter("formFilter", findString(formFilter));
276         if (formId != null)
277           addParameter("formId", findString(formId));
278         if (listenTopics != null)
279           addParameter("listenTopics", findString(listenTopics));
280         if (notifyTopics != null)
281           addParameter("notifyTopics", findString(notifyTopics));
282         if (indicator != null)
283             addParameter("indicator", findString(indicator));
284         if (loadOnTextChange != null)
285             addParameter("loadOnTextChange", findValue(loadOnTextChange, Boolean.class));
286         if (loadMinimumCount != null)
287             addParameter("loadMinimumCount", findValue(loadMinimumCount, Integer.class));
288         if (showDownArrow != null)
289             addParameter("showDownArrow", findValue(showDownArrow, Boolean.class));
290         else
291             addParameter("showDownArrow", Boolean.TRUE);
292         if (templateCssPath != null)
293             addParameter("templateCssPath", findString(templateCssPath));
294         if (iconPath != null)
295             addParameter("iconPath", findString(iconPath));
296         if (dataFieldName != null)
297             addParameter("dataFieldName", findString(dataFieldName));
298         if (keyName != null)
299             addParameter("keyName", findString(keyName));
300         else {
301             keyName = name + "Key";
302             addParameter("keyName", findString(keyName));
303         }
304         if (transport != null)
305             addParameter("transport", findString(transport));
306         if (preload != null)
307             addParameter("preload", findValue(preload, Boolean.class));
308         
309         if (keyValue != null)
310             addParameter("nameKeyValue", findString(keyValue));
311         else {
312             String keyNameExpr = "%{" + keyName + "}";
313             addParameter("nameKeyValue", findString(keyNameExpr));
314         }
315         
316         
317         if (beforeNotifyTopics != null)
318             addParameter("beforeNotifyTopics", findString(beforeNotifyTopics));
319         if (afterNotifyTopics != null)
320             addParameter("afterNotifyTopics", findString(afterNotifyTopics));
321         if (errorNotifyTopics != null)
322             addParameter("errorNotifyTopics", findString(errorNotifyTopics));
323         if (valueNotifyTopics != null)
324             addParameter("valueNotifyTopics", findString(valueNotifyTopics));
325         if (resultsLimit != null)
326             addParameter("searchLimit", findString(resultsLimit));
327         
328         // generate a random ID if not explicitly set and not parsing the content
329         Boolean parseContent = (Boolean)stack.getContext().get(Head.PARSE_CONTENT);
330         boolean generateId = (parseContent != null ? !parseContent : true);
331         
332         addParameter("pushId", generateId);
333         if ((this.id == null || this.id.length() == 0) && generateId) {
334             // resolves Math.abs(Integer.MIN_VALUE) issue reported by FindBugs 
335             // http://findbugs.sourceforge.net/bugDescriptions.html#RV_ABSOLUTE_VALUE_OF_RANDOM_INT
336             int nextInt = RANDOM.nextInt();
337             nextInt = nextInt == Integer.MIN_VALUE ? Integer.MAX_VALUE : Math.abs(nextInt);  
338             this.id = "widget_" + String.valueOf(nextInt);
339         }
340     }
341 
342     @Override
343     @StrutsTagSkipInheritance
344     public void setTheme(String theme) {
345         super.setTheme(theme);
346     }
347     
348     @Override
349     public String getTheme() {
350         return "ajax";
351     }
352     
353     protected Object findListValue() {
354         return (list != null) ? findValue(list, Object.class) : null;
355     }
356 
357     @StrutsTagAttribute(description="Whether autocompleter should make suggestion on the textbox", type="Boolean", defaultValue="false")
358     public void setAutoComplete(String autoComplete) {
359         this.autoComplete = autoComplete;
360     }
361 
362     @StrutsTagAttribute(description="Enable or disable autocompleter", type="Boolean", defaultValue="false")
363     public void setDisabled(String disabled) {
364         this.disabled = disabled;
365     }
366 
367     @StrutsTagAttribute(description="Force selection to be one of the options", type="Boolean", defaultValue="false")
368     public void setForceValidOption(String forceValidOption) {
369         this.forceValidOption = forceValidOption;
370     }
371 
372     @StrutsTagAttribute(description="The URL used to load the options")
373     public void setHref(String href) {
374         this.href = href;
375     }
376 
377     @StrutsTagAttribute(description="Delay before making the search", type="Integer", defaultValue="100")
378     public void setDelay(String searchDelay) {
379         this.delay = searchDelay;
380     }
381 
382     @StrutsTagAttribute(description="how the search must be performed, options are: 'startstring', 'startword' " +
383                 "and 'substring'", defaultValue="stringstart")
384     public void setSearchType(String searchType) {
385         this.searchType = searchType;
386     }
387 
388     @StrutsTagAttribute(description="Dropdown's height in pixels", type="Integer", defaultValue="120")
389     public void setDropdownHeight(String height) {
390         this.dropdownHeight = height;
391     }
392 
393     @StrutsTagAttribute(description="Dropdown's width", type="Integer", defaultValue="same as textbox")
394     public void setDropdownWidth(String width) {
395         this.dropdownWidth = width;
396     }
397 
398     @StrutsTagAttribute(description="Function name used to filter the fields of the form")
399     public void setFormFilter(String formFilter) {
400       this.formFilter = formFilter;
401     }
402 
403     @StrutsTagAttribute(description="Form id whose fields will be serialized and passed as parameters")
404     public void setFormId(String formId) {
405       this.formId = formId;
406     }
407 
408     @StrutsTagAttribute(description="Topic that will trigger a reload")
409     public void setListenTopics(String listenTopics) {
410       this.listenTopics = listenTopics;
411     }
412 
413     @StrutsTagAttribute(description="Topics that will be published when content is reloaded")
414     public void setNotifyTopics(String onValueChangedPublishTopic) {
415       this.notifyTopics = onValueChangedPublishTopic;
416     }
417 
418     @StrutsTagAttribute(description="Id of element that will be shown while request is made")
419     public void setIndicator(String indicator) {
420         this.indicator = indicator;
421     }
422 
423     @StrutsTagAttribute(description="Minimum number of characters that will force the content to be loaded", type="Integer", defaultValue="3")
424     public void setLoadMinimumCount(String loadMinimumCount) {
425         this.loadMinimumCount = loadMinimumCount;
426     }
427 
428     @StrutsTagAttribute(description="Options will be reloaded everytime a character is typed on the textbox", type="Boolean", defaultValue="true")
429     public void setLoadOnTextChange(String loadOnType) {
430         this.loadOnTextChange = loadOnType;
431     }
432 
433     @StrutsTagAttribute(description="Show or hide the down arrow button", type="Boolean", defaultValue="true")
434     public void setShowDownArrow(String showDownArrow) {
435         this.showDownArrow = showDownArrow;
436     }
437 
438     // Override as not required
439     @StrutsTagAttribute(description="Iteratable source to populate from.")
440     public void setList(String list) {
441         super.setList(list);
442     }
443     
444     @StrutsTagAttribute(description="Template css path")
445     public void setTemplateCssPath(String templateCssPath) {
446         this.templateCssPath = templateCssPath;
447     }
448     
449     @StrutsTagAttribute(description="Path to icon used for the dropdown")
450     public void setIconPath(String iconPath) {
451         this.iconPath = iconPath;
452     }
453     
454     @StrutsTagAttribute(description="Name of the field to which the selected key will be assigned")
455     public void setKeyName(String keyName) {
456        this.keyName = keyName;
457     }
458 
459     @StrutsTagAttribute(description="Name of the field in the returned JSON object that contains the data array", defaultValue="Value specified in 'name'")
460     public void setDataFieldName(String dataFieldName) {
461         this.dataFieldName = dataFieldName;
462     }
463     
464     @StrutsTagAttribute(description="The css class to use for element")
465     public void setCssClass(String cssClass) {
466         super.setCssClass(cssClass);
467     }
468 
469     @StrutsTagAttribute(description="The css style to use for element")
470     public void setCssStyle(String cssStyle) {
471         super.setCssStyle(cssStyle);
472     }
473 
474     @StrutsTagAttribute(description="The id to use for the element")
475     public void setId(String id) {
476         super.setId(id);
477     }
478 
479     @StrutsTagAttribute(description="The name to set for element")
480     public void setName(String name) {
481         super.setName(name);
482     }
483 
484     @StrutsTagAttribute(description="Preset the value of input element")
485     public void setValue(String arg0) {
486         super.setValue(arg0);
487     }
488     
489     @StrutsTagAttribute(description="Comma delimmited list of topics that will published after the request(if the request succeeds)")
490     public void setAfterNotifyTopics(String afterNotifyTopics) {
491         this.afterNotifyTopics = afterNotifyTopics;
492     }
493 
494     @StrutsTagAttribute(description="Comma delimmited list of topics that will published before the request")
495     public void setBeforeNotifyTopics(String beforeNotifyTopics) {
496         this.beforeNotifyTopics = beforeNotifyTopics;
497     }
498 
499     @StrutsTagAttribute(description="Comma delimmited list of topics that will published after the request(if the request fails)")
500     public void setErrorNotifyTopics(String errorNotifyTopics) {
501         this.errorNotifyTopics = errorNotifyTopics;
502     }
503 
504     @StrutsTagAttribute(description="Comma delimmited list of topics that will published when a value is selected")
505     public void setValueNotifyTopics(String valueNotifyTopics) {
506         this.valueNotifyTopics = valueNotifyTopics;
507     }
508     
509     @StrutsTagAttribute(description="Limit how many results are shown as autocompletion options, set to -1 for unlimited results", defaultValue="30")
510     public void setResultsLimit(String resultsLimit) {
511         this.resultsLimit = resultsLimit;
512     }
513     
514     @StrutsTagAttribute(description="Transport used by Dojo to make the request", defaultValue="XMLHTTPTransport")
515     public void setTransport(String transport) {
516         this.transport = transport;
517     }
518     
519     @StrutsTagAttribute(description="Load options when page is loaded", type="Boolean", defaultValue="true")
520     public void setPreload(String preload) {
521         this.preload = preload;
522     }
523 
524     @StrutsTagAttribute(description="Initial key value")
525     public void setKeyValue(String keyValue) {
526         this.keyValue = keyValue;
527     }
528 }