View Javadoc
1   package com.randomnoun.common.webapp.taglib;
2   
3   /* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
4    * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
5    */
6   
7   import java.io.*;
8   import java.text.*;
9   import java.util.*;
10  
11  import jakarta.servlet.jsp.*;
12  
13  import org.apache.log4j.Logger;
14  
15  import com.randomnoun.common.Struct;
16  import com.randomnoun.common.ErrorList;
17  import com.randomnoun.common.Text;
18  
19  /**
20   * Render a dynamic HTML SELECT tag using information from a structured list.
21   * 
22   * <p>This class is intended to be used in a JSP 2.0 container, i.e. EL expressions
23   * will have already been evaluated by the container before they reach this class.
24   *
25   * <p>Attributes defined for this tag (in common.tld) are:
26   * <ul>
27   * <li> name - the NAME element of the generated SELECT tag.
28   * <li> value - the current value of the SELECT tag.
29   * <li> data - a reference to a structured list containing both display and value columns,
30   *   or a list of Strings. An empty list can be forced by setting an empty data attribute.
31   * <li> valueColumn - the name of the column in the list to use to populate OPTION values
32   * <li> displayColumn - the name of the column in the list to use to populate OPTION display text
33   *   (will default to valueColumn if left blank)
34   * <li> firstOption - specifies a display value for the first option for a list; this
35   *   option corresponds to a null value. Used to generate options like
36   *   '(please select...)' at the top of a select box.
37   * <li> bundle - the resource bundle used to internationalise display column text
38   * <li> bundleFormat - a MessageFormat used to convert a value into a bundle key
39   * </ul>
40   *
41   * Only one of 'value' and 'valueFromRequest' can be set.
42   *
43   * <p>Additional attributes that are passed directly through to HTML:
44   * See {@link StandardHtmlTag} for a list of additional attributes which
45   * will be directly generated as HTML.
46   *
47   * 
48   *
49   */
50  public class SelectTag
51      extends StandardHtmlTag
52  {
53      
54      
55  
56      /** Logger for this class */
57      private final static Logger logger = Logger.getLogger(SelectTag.class);
58  
59      /** The name of the SELECT tag. */
60      private String name;
61  
62      /** The current value of the SELECT tag. */
63      private String value;
64  
65      /** The value of the 'data' attribute */
66      private String dataString;
67  
68      /** A reference to a structured list (evaluated from dataString). */
69      private List data;
70  
71      /** The value of the 'bundle' attribute */
72      private String bundleString;
73  
74      /** The value of the 'bundlePattern' attribute */
75      private String bundleFormat;
76  
77      /** A reference to a resource bundle (evaluated from bundleString). */
78      private ResourceBundle bundle;
79  
80      /** The name of the column in the list to use to populate OPTION values */
81      private String valueColumn;
82  
83      /** The name of the column in the list to use to populate OPTION display text */
84      private String displayColumn;
85  
86      /** Some display text for a first OPTION of the SELECT tag */
87      private String firstOption;
88      
89      /** If non-null, and the displayValue or value to be displayed is a Date, will format the 
90       * value with a SimpleDateFormatter object with this pattern
91       */
92      private String formatDate;
93      
94      /* ***********************************************************************
95       * Tag Library attribute methods
96       */
97  
98      /** Sets the name of the SELECT tag.
99       *  @param value the name of the SELECT tag.
100      */
101     public void setName(String name)
102     {
103         this.name = name;
104     }
105 
106     /** Retrieves the name of the SELECT tag.
107      *  @return   the name of the SELECT tag.
108      */
109     public String getName()
110     {
111         return name;
112     }
113 
114     /** Sets the current value of the SELECT tag.
115      *  @param value the current value of the SELECT tag.
116      */
117     public void setValue(String value)
118     {
119         this.value = value;
120     }
121 
122     /** Retrieves the current value of the SELECT tag.
123      *  @return   the current value of the SELECT tag.
124      */
125     public String getValue()
126     {
127         return value;
128     }
129 
130     /** Sets the structured list used to populate the SELECT tag.
131      *  @param value the structured list used to populate the SELECT tag.
132      */
133     public void setData(Object data)
134     {
135         this.data = (List) data;
136     }
137 
138     /** Retrieves the structured list used to populate the SELECT tag.
139      *  @return   the structured list used to populate the SELECT tag.
140      */
141     public Object getData()
142     {
143         return dataString;
144     }
145 
146     /** Sets the resource bundle used to internationalise display column text
147      *  @param value the resource bundle used to internationalise display column text
148      */
149     public void setBundle(String bundleString)
150     {
151         this.bundleString = bundleString;
152     }
153 
154     /** Retrieves the resource bundle used to internationalise display column text
155      *  @return   the resource bundle used to internationalise display column text
156      */
157     public String getBundle()
158     {
159         return bundleString;
160     }
161 
162     /** Sets how the resource bundle will be used to retrieve i18n text
163      *  @param value how the resource bundle will be used to retrieve i18n text
164      */
165     public void setBundleFormat(String bundleFormat)
166     {
167         this.bundleFormat = bundleFormat;
168     }
169 
170     /** Retrieves how the resource bundle will be used to retrieve i18n text
171      *  @return   how the resource bundle will be used to retrieve i18n text
172      */
173     public String bundlePattern()
174     {
175         return bundleFormat;
176     }
177 
178     /** Sets the column name in the 'data' structured list used to represent
179      *    OPTION values in the generated SELECT HTML.
180      *  @param value the column name in the 'data' structured list used to represent
181      *    OPTION values in the generated SELECT HTML.
182      */
183     public void setValueColumn(String valueColumn)
184     {
185         this.valueColumn = valueColumn;
186     }
187 
188     /** Retrieves the column name in the 'data' structured list used to represent
189      *    OPTION values in the generated SELECT HTML.
190      *  @return   the column name in the 'data' structured list used to represent
191      *    OPTION values in the generated SELECT HTML.
192      */
193     public String getValueColumn()
194     {
195         return valueColumn;
196     }
197 
198     /** Sets the column name in the 'data' structured list used to represent
199      *    OPTION values in the generated SELECT HTML.
200      *  @param value the column name in the 'data' structured list used to represent
201      *    OPTION values in the generated SELECT HTML.
202      */
203     public void setDisplayColumn(String displayColumn)
204     {
205         this.displayColumn = displayColumn;
206     }
207 
208     /** Retrieves the column name in the 'data' structured list used to represent
209      *    OPTION display text in the generated SELECT HTML.
210      *  @return   the column name in the 'data' structured list used to represent
211      *    OPTION display text in the generated SELECT HTML.
212      */
213     public String getDisplayColumn()
214     {
215         return displayColumn;
216     }
217 
218     /** Sets the display text for a first OPTION of the SELECT tag.
219      *  @param value the display text for a first OPTION of the SELECT tag.
220      */
221     public void setFirstOption(String firstOption)
222     {
223         this.firstOption = firstOption;
224     }
225 
226     /** Retrieves the display text for a first OPTION of the SELECT tag.
227      *  @return   the display text for a first OPTION of the SELECT tag.
228      */
229     public String getFirstOption()
230     {
231         return firstOption;
232     }
233     
234     /** Sets the date pattern to use to format Date objects
235      *  @param value the date pattern to use to format Date objects
236      */
237     public void setFormatDate(String formatDate)
238     {
239         this.formatDate = formatDate;
240     }
241 
242     /** Retrieves the date pattern to use to format Date objects
243      *  @return   the date pattern to use to format Date objects
244      */
245     public String getFormatDate()
246     {
247         return formatDate;
248     }    
249 
250     /** Sets the name of the multiple attribute.
251      *  @param value the name of the multiple attribute.
252      */
253     public void setMultiple(String multiple)
254     {
255     	this.attributes.put("multiple", multiple);
256     }
257 
258     /** Retrieves the name of the multiple attribute.
259      *  @return   the name of the multiple attribute.
260      */
261     public String getMultiple()
262     {
263     	return (String) attributes.get("multiple");
264     }
265 
266     
267     
268     
269 
270     /* ***********************************************************************
271      * End of tag library attribute methods
272      */
273 
274     /** Start custom taglibrary processing. Sets internal private variables based on
275      *  HttpSession values and defaults, and emits the generating HTML. The body
276      *  of this tag will not be evaluated, as declared in the the .tld file.
277      *
278      *  @todo escape HTML attributes and values
279      *
280      *  @return This method always returns TagSupport.SKIP_BODY, as required by the
281      *   JSP Taglib specification for empty tags
282      */
283     public int doStartTag()
284         throws JspException
285     {
286         try {
287         	SimpleDateFormat sdf = null;
288             evaluateAttributes();
289             if (formatDate!=null) {
290             	sdf = new SimpleDateFormat(formatDate);
291             }
292             JspWriter out = pageContext.getOut();
293 
294             // use value column for display if not explicitly set
295             if (displayColumn == null) {
296                 displayColumn = valueColumn;
297             }
298 
299             // remove quotes from name
300             if (name.startsWith("\"")) {
301                 name = name.substring(1);
302             }
303             if (name.endsWith("\"")) {
304                 name = name.substring(0, name.length() - 1);
305             }
306 
307             // determine styling based on whether this field is in error
308             ErrorList errors = null;
309             try {
310                 errors = (ErrorList)pageContext.getAttribute("errors",
311                   PageContext.REQUEST_SCOPE);
312             } catch (ClassCastException cce) {
313                 // just ignore these.
314             }
315 
316             String fieldStyle;
317             boolean hasError = false;
318             fieldStyle = "inputField";
319             if (errors != null && errors.hasErrorOn(name)) {
320                 fieldStyle = "inputFieldError";
321                 hasError = true;
322             }
323             if (attributes.containsKey("class")) {
324                 attributes.put("class", fieldStyle + " " + attributes.get("class"));
325             } else {
326                 attributes.put("class", fieldStyle);
327             }
328 
329             // generate the HTML
330             if (hasError) {
331                 out.print("<span class=\"inputFieldError\">");
332             }
333             out.print("<select name=\"" + name + "\" id=\"" + name + "\" " + this.getAttributeString() + ">");
334             if (firstOption != null) {
335                 out.println("<option value=\"\">" + Text.escapeHtml(firstOption) +
336                     "</option>\n");
337             }
338 
339             // generate select options
340             if (data != null) {
341                 for (Iterator i = data.iterator(); i.hasNext();) {
342                     Object obj = i.next();
343                     Object displayObj = null;
344                     Object valueObj = null;
345                     String valueText = "";
346                     String displayText = null;
347 
348                     // could generalise the first two cases here
349                     if (obj instanceof Map) {
350                         Map row = (Map) obj;
351                         if (row.get(displayColumn) != null) {
352                         	displayObj = row.get(displayColumn);
353                         	if (displayObj instanceof Date && sdf!=null) {
354                             	displayText = sdf.format((Date) displayObj);
355                             } else {
356                             	displayText = displayObj.toString();
357                             }
358                         }
359                         
360                         valueObj = row.get(valueColumn);
361                         if (valueObj == null) {
362                             throw new IllegalStateException(
363                                 "Null value encountered in SelectTag data '" + dataString + "'");
364                         }
365                         if (valueObj instanceof Date && sdf!=null) {
366                         	valueText = sdf.format((Date) valueObj);
367                         } else {
368                         	valueText = valueObj.toString();
369                         }
370                         if (displayText == null) {
371                             displayText = valueText;
372                         }
373                     } else if (displayColumn!=null && valueColumn!=null) {
374                     	valueObj = Struct.getValue(obj, valueColumn);
375                     	displayObj = (Struct.getValue(obj, displayColumn));
376                         if (valueObj == null) {
377                             throw new IllegalStateException(
378                                 "Null value encountered in SelectTag data '" + dataString + "'");
379                         }
380                     	if (valueObj instanceof Date && sdf!=null) {
381                         	valueText = sdf.format((Date) valueObj);
382                         } else {
383                         	valueText = valueObj.toString();
384                         }
385                     	if (displayObj instanceof Date && sdf!=null) {
386                         	displayText = sdf.format((Date) displayObj);
387                         } else {
388                         	displayText = displayObj.toString();
389                         }
390                 	} else {
391                     	if (obj instanceof Date && sdf!=null) {
392                         	valueText = sdf.format((Date) obj);
393                         } else {
394                         	valueText = obj.toString();
395                         }
396                         displayText = valueText;
397                     }
398 
399                     if (bundle != null) {
400                         String bundleKey = displayText;
401                         if (bundleFormat != null) {
402                             bundleKey = MessageFormat.format(bundleFormat,
403                                     new Object[] { displayText });
404                         }
405 
406                         try {
407                             displayText = bundle.getString(bundleKey);
408                         } catch (MissingResourceException mre) {
409                             // just use the bundle key in this case
410                             // displayText = "???" + bundleKey + "???";
411                             displayText = bundleKey;
412                         }
413                     }
414 
415                     out.print("<option value=\"" + Text.escapeHtml(valueText) + "\"" +
416                         (valueText.equals(value) ? " selected" : "") + ">" +
417                         Text.escapeHtml(displayText) + "</option>\n");
418                 }
419             }
420 
421             out.print("</select>\n");
422 
423             if (hasError) {
424                 out.print("</span>");
425             }
426         } catch (IOException ex) {
427             // ignore these errors, since they can occur when the user hits 'stop' in their browser
428 		} catch (Throwable t) {
429 			// WAS does not log exceptions that occur within tag libraries; log and rethrow
430 			t.printStackTrace();
431 			throw (JspException) new JspException("Exception occurred in SelectTag").initCause(t);
432 		}
433 
434         // selectTag element contents are not evaluated 
435         return SKIP_BODY;
436     }
437 
438     /** End of custom tag library processing. This method performs no work.
439      *
440      *  @return This tag always returns TagSupport.SKIP_BODY .
441      **/
442     public int doEndTag()
443     {
444         // reset attributes
445         id = null;
446         name = null;
447         value = null;
448         dataString = null;
449         data = null;
450         valueColumn = null;
451         displayColumn = null;
452         bundle = null;
453         bundleFormat = null;
454         formatDate = null;
455         clearAttributes();
456 
457         return SKIP_BODY;
458     }
459     
460 	/** Return a localised list for use in a mm:select tag. This function returns a list of 
461 	 * Maps, each element of which is a single option in the select tag. The map contains
462 	 * two attributes: 'text' and 'value', containing the internationalised and 
463 	 * non-internationalised versions of each option. Note that some select boxes are
464 	 * not internationalised (e.g. 'selectTop').   
465 	 * 
466 	 * @param bundle The bundle to retrieve localisation information form.
467 	 * @param prefix A prefix used to return information from the bundle
468 	 * @param options A comma-separated list of option values
469 	 * 
470 	 * @return A List as documented above
471 	 */  
472 	public static List getSelectOptions(ResourceBundle bundle, String prefix, String options) {
473 		List optionsList;
474 		try {
475 			optionsList = Text.parseCsv(options);
476 		} catch (ParseException pe) {
477 			throw new IllegalArgumentException("Invalid options list '" + options + "'");
478 		}
479 		List list = new ArrayList();
480 		for (Iterator i = optionsList.iterator(); i.hasNext(); ) {
481 			String option = (String) i.next();
482 			Map map = new HashMap();
483 			map.put("value", option);
484 			if (bundle!=null) {
485 				map.put("text", bundle.getString(prefix + option));  // localisation
486 			} else {
487 				map.put("text", option);  // no localisation                
488 			}
489 			list.add(map);
490 		}
491 		return list;
492 	}
493     
494     
495 }