001// Copyright (c) 2001 Hursh Jain (http://www.mollypages.org) 
002// The Molly framework is freely distributable under the terms of an
003// MIT-style license. For details, see the molly pages web site at:
004// http://www.mollypages.org/. Use, modify, have fun !
005
006package fc.web.forms;
007
008import java.io.*;
009import java.util.*;
010import java.sql.*;
011import javax.servlet.*;
012import javax.servlet.http.*;
013
014import fc.jdbc.*;
015import fc.io.*;
016import fc.util.*;
017
018/** 
019An HTML Select field 
020<p>
021There are 3 different kinds of selects:
022<dl>
023<dt>Select</dt>
024  <dd>A normal select that is created/instantiated and added to the
025  form. This select displays the same options to all users and these
026  options cannot be changed per request/user (since it does not
027  implement the <tt>setValue(FormData fd, ...)</tt> method. This field
028  will always track that the submitted data is a legal value and was
029  not hacked/modified by the client.</dd>
030<dt>RefreshableSelect</dt>
031  <dd>This select starts out by displaying the same options to all
032  users. However, it allows options to be thereafter set/displayed per
033  user/request. If per user/request options are shown, then the browser
034  can modify/hack/send any option. This field (in contrast to most
035  other fields in the form) itself won't track this and application
036  logic is responsible (if applicable) for tracking if the submitted
037  data is valid.</dd>.
038<dt>DependentSelect</dt>
039  <dd>This select is similar to a RefreshableSelect but uses an
040  external dependency class to set both it's initial values and per
041  user/request subsequent values. The dependency typically looks at
042  other fields in the form to generate this data.
043</dl>
044
045@author hursh jain
046**/
047public class Select extends Field
048{
049//option[text='foo',value='1'], getValue()==1
050//option[text='foo'], getValue()==foo
051
052//all options shown to user
053LinkedHashMap   options = new LinkedHashMap();       
054//orig selected options when form was constructed
055Map       origSelectedMap = new HashMap(); 
056int       size = 0;
057boolean     multiple;
058
059static final class Data 
060  {
061  //To store selected options submitted by the user
062  //acts like a set, used via containsKey() to see if an options
063  //is selected (so we can reselect it) during rendering. 
064  private Map  selectedMap = new HashMap(); 
065  
066  //to store all options submitted by user
067  private List selectedList  = new ArrayList();
068  }
069
070/** 
071Creates a new select element with no initial values. Multiple
072selections are not initially allowed.
073
074@param  name    the field name
075**/
076public Select(String name)
077  {
078  super(name);
079  }
080  
081/** 
082Creates a new select element with the specified initial values
083and no multiple selections allowed. There is no explicit default
084selection which typically means the browser will show the first
085item in the list as the selection item.
086
087@param  name    the field name
088@param  values    the initial option values for this class. The
089          objects contained in the list must be of type
090          {@link Option} otherwise a 
091          <tt>ClassCastException</tt> may be thrown in 
092          future operations on this object.
093
094@throws IllegalArgumentException  if the values parameter was null
095                  (note: an empty but non-null List is ok)
096**/
097public Select(String name, List values)
098  {
099  super(name);
100  Argcheck.notnull(values, "values param was null");
101  initOptions(values);
102  }
103
104void initOptions(final List values) 
105  { 
106  for (int n = 0; n < values.size(); n++) {
107    final Option item = (Option) values.get(n);
108    String itemval = item.getValue();
109    this.options.put(itemval, item);
110    if (item.isOrigSelected()) {
111      this.origSelectedMap.put(itemval, item);  
112      }
113    } 
114  }
115
116/**
117The specified query is used to populate this select. The default parameter
118(if not null) is the default selected option. If the default option is
119<tt>null</tt>, no option is selected for multiple-value selects and the
120first option returned by the query is selected for single-value selects.
121<p>
122Typically, this method may be invoked like:
123<blockquote>
124<tt>
125useQuery(con, "select id, name from my_lookup_table", new Option("--choose--"));
126</tt>
127</blockquote>
128If the lookup table has more than 2 columns, the query can look like (in
129standard SQL syntax):
130<blockquote>
131<tt>
132useQuery(con, "select id, <font color=blue>name1 || ' ' || name2</font> from my_lookup_table", new Option("--choose--"));
133</tt>
134</blockquote>
135This shows columns name1 and name2 concatenated with each other.
136
137@param  con   the connection to use for the query. This connection is
138        <b>not</b> closed by this method. <u>Close this connection
139        from the calling code to prevent connection leaks.</u>
140
141@return this select for method chaining convenience
142*/
143public Select useQuery(Connection con, String query, Select.Option defaultOpt) 
144throws SQLException
145  {
146  reset();
147  
148  if (defaultOpt != null)
149    add(defaultOpt);
150
151  final List list = makeOptionsFromQuery(con, query);
152  initOptions(list);
153
154  return this;
155  }
156
157/**
158Convenience method that calls {@link useQuery(Connection, String, String)
159useQuery(con, query, null)}.
160*/
161public Select useQuery(Connection con, String query) throws SQLException
162  {
163  useQuery(con, query, null);
164  return this;
165  }
166
167/**
168Uses the first column as the option text and if there is a second column
169uses it as the corresponding option value.
170*/
171public static List makeOptionsFromQuery(Connection con, String query) 
172throws SQLException
173  {
174  return makeOptionsFromQuery(con, query, null);
175  }
176
177/**
178Uses the first column as the option text and if there is a second column
179uses it as the corresponding option value. Uses an additional default
180option.
181*/
182public static List makeOptionsFromQuery(
183  Connection con, String query, Select.Option defaultOption)
184throws SQLException
185  {
186  final List list = new ArrayList();
187  if (defaultOption != null)
188    list.add(defaultOption);
189  
190  final Statement stmt = con.createStatement();
191  final ResultSet rs = stmt.executeQuery(query);
192  final ResultSetMetaData rsmd = rs.getMetaData();
193  final int numberOfColumns = rsmd.getColumnCount();  
194  if (numberOfColumns == 0) {
195    log.warn("Query [",query, "] returned no columns");
196    }
197  while (rs.next()) {
198    String text = rs.getString(1);
199    String value = (numberOfColumns == 1) ? text : rs.getString(2);
200    Select.Option opt = new Select.Option(text, value);
201    list.add(opt);
202    }
203  stmt.close();
204  return list;
205  }
206
207public Field.Type getType() {
208  return Field.Type.SELECT;
209  }
210
211/**
212Sets the values for this select. Any previously set values are
213first cleared before new values are set.
214<p>
215Note, to show user specific options via FormData, use a {@link RefreshableSelect}
216instead.
217
218@param  values  a list of {@link Select.Option} objects
219*/
220public void setValue(List values)
221  {
222  Argcheck.notnull(values, "specified values param was null");
223  options.clear();
224  origSelectedMap.clear();
225  initOptions(values);  
226  }
227
228
229/** 
230Returns a List containing the selected options. Each object in
231the collection will be of type {@link Select.Option}. If there
232are no selected options, the returned list will be an
233empty list.
234
235@param  fd  the submited form data. This object should not be null
236      otherwise a NPE will be thrown.
237**/
238public List getValue(FormData fd) 
239  {
240  Select.Data data = (Select.Data) fd.getData(name);
241  if (data == null) {
242    return Form.empty_list;
243    }
244  return data.selectedList;
245  }
246
247/** 
248Convenience method that returns the selected option as a
249String. <u>No guarantees are made as to which selection is
250returned when more than one selection is selected (this
251method is really meant for when the select only allows
252single selections as a dropdown).</u>
253<p>
254The returned value is of type String obtained by called the
255selected Option's {@link Select.Option#getValue} method.
256<p>
257If there is no selection, returns <tt>null</tt>
258**/
259public String getStringValue(FormData fd) 
260  {
261  String result = null;
262  
263  Select.Data data = (Select.Data) fd.getData(name);
264  if (data == null) {
265    return null;
266    }
267
268  List list = data.selectedList;
269  if (list.size() == 0) {
270    return result;
271    }
272  else {
273    Option opt = (Option) list.get(0);
274    return opt.getValue();
275    }
276  }
277
278
279/**
280Convenience method that returns the single value of this field
281as a Integer.
282<p>
283All the caveats of {@link #getSingleValueAsString()} apply.
284
285@throws NumberFormatException if the value could not be
286                returned as in integer. 
287*/
288public int getIntValue(FormData fd) {
289  return Integer.parseInt(getStringValue(fd));
290  }
291
292/**
293Convenience method that returns the single value of this field
294as a boolean. 
295<p>
296All the caveats of {@link #getSingleValueAsString()} apply.
297In particular, the formdata should contain non-null data
298with at least one selection.
299*/
300public boolean getBooleanValue(FormData fd) {
301  return Boolean.valueOf(getStringValue(fd)).booleanValue();
302  }
303
304/**
305Returns <tt>true</tt> if some option has been selected
306by the user, <tt>false</tt> otherwise. (also returns
307<tt>false</tt> is the specified form data is <tt>null</tt>).
308*/
309public boolean isFilled(FormData fd) 
310  {
311  if (fd == null)
312    return false;
313    
314  Select.Data data = (Select.Data) fd.getData(name);
315  if (data == null) {
316    return false;
317    }
318  List list = data.selectedList;
319  //if data is present, then list always will be non-null
320  return (/*list != null &&*/ list.size() != 0);
321  }
322
323/*
324 Select values can be present as:
325 selectname=value
326
327 or for multiple selections (for say a select called "sel1")
328 sel1=value1&sel1=val2
329 
330 The values sent are those in the value tag in the option
331 and if missing those of the corresponding html for the option.
332
333 If a select allows multiple and none are selected, then
334 nothing at all is sent (note this is different than for
335 single (and not multiple) selects, where some value will always
336 be sent (since something is always selected).
337
338 We can track submitted/selected options in 2 ways:
339 1. go thru the option list and set a select/unselect on each option
340 element and then the option element renders itself appropriately.
341 2. keep a separate list of selected elements and at render time display
342 the item as selected only if it's also in the select list.
343
344 (w) implies that we have to specify select/no select at
345 render time to each option as opposed to setting that flag
346 in the option before telling the option to render itself.
347 (2) is chosen here.
348
349 the client can send hacked 2 or more options with the same
350 value this method will simply add any such duplicate values
351 at the end of the list. this is ok.
352*/
353public void setValueFromSubmit(FormData fd, HttpServletRequest req) 
354throws SubmitHackedException
355  {
356  //value(s) associated with the selection field name
357  String[] values = req.getParameterValues(name);
358  
359  //todo: if ! multiple && enabled && values == null, hacklert
360  if (values == null) {
361    return;
362    }
363
364  //lazy instantiation
365  Select.Data data = new Select.Data();
366  fd.putData(name, data);
367  
368  if (multiple && values.length > 1)
369    hacklert(req, "recieved multiple values for a single value select");
370    
371  if (multiple) 
372    {
373    for (int n = 0; n < values.length; n++) {
374      addSelectedOpt(req, data, values[n]);
375      }
376    }
377  else{
378    addSelectedOpt(req, data, values[0]);
379    }
380  }
381
382private void addSelectedOpt(
383 HttpServletRequest req, Select.Data data, String submitValue)
384throws SubmitHackedException
385  {
386  // our options were stored with option's value as the key
387    Option opt = (Option) options.get(submitValue);
388
389  // this can happen if option values were hacked by the client
390    if (opt == null) 
391      {
392      hacklert(req, 
393        "could not match/retrieve a submitted option from the options." + 
394        "; Submited value=[" + submitValue +"]; options=" + options); 
395      }       
396  else{
397    data.selectedMap.put(opt.getValue(), opt);
398    data.selectedList.add(opt);
399    }
400  } 
401
402public void renderImpl(FormData fd, Writer writer) throws IOException
403  {
404  Select.Data data = null;
405
406  if (fd != null)  { //we have submit or initial data in the FD
407    //can be null, no submit/initial data for this particular field
408    data = (Select.Data) fd.getData(name); 
409    }
410  
411  //data can be null here.
412  
413  writer.write("<select");  
414  writer.write(" name='");
415  writer.write(name);
416  writer.write("'");
417  
418  if (size > 0) {
419    writer.write(" size='");
420    writer.write(String.valueOf(size));
421    writer.write("'"); 
422    }
423    
424  if (! enabled || ! isEnabled(fd)) {
425    writer.write(" disabled");
426    }
427
428  if (multiple) {
429    writer.write(" multiple"); 
430    } 
431    
432  if (renderStyleTag) {
433    writer.write(" style='");
434    writer.write(styleTag);
435    writer.write("'");
436    }
437    
438  final int arlen = arbitraryString.size();
439  for (int n = 0; n < arlen; n++) {
440    writer.write(" ");
441    writer.write(arbitraryString.get(n).toString());
442    }
443    
444  writer.write(">\n");
445  
446  Iterator it = options.values().iterator(); 
447  while (it.hasNext()) 
448    {
449    Option item = (Option) it.next();
450    String itemval = item.getValue();
451    
452    boolean selected = false;
453    
454    //NOTE: DO NOT COMBINE  and say:
455    //if (fd != null && data != null)
456    //  that's _the_ classic bug, because if:
457    //  fd != null but data == null, we will drop down
458    //  to (3) but we need to go to (2).
459    if (fd != null)   /* maintain submit state */
460      {         
461/*1*/   if (data != null) {  
462        selected = data.selectedMap.containsKey(itemval);
463        }
464      else {  
465/*2*/     //form submitted but option not selected        
466        //selected should be false here [which it is by default]
467        }
468      }
469    else{    /* form not submitted, show original state */
470/*3*/   selected = origSelectedMap.containsKey(itemval);
471        }
472       
473    writer.write(item.render(selected));
474    writer.write("\n"); //sufficient for easy view source in browser    
475    } 
476
477  writer.write("</select>");
478  }
479  
480/** 
481Adds a new option to the selection list. Replaces any previous
482option that was added previously with the same value.
483
484@param  opt the option to be added
485**/
486public void add(Option opt) 
487  {
488  Argcheck.notnull(opt, "opt param was null");
489  String optval = opt.getValue();
490  options.put(optval, opt);
491  if (opt.isOrigSelected()) {
492    origSelectedMap.put(optval, opt); 
493    }
494  } 
495
496/** 
497Adds a new option to the selection list. Replaces any previous
498option that was added previously with the same value. This method
499will have the same effect as if the {@link #add(Option) 
500add(new Select.Option(item))} method was invoked.
501
502@param  item the option to be added
503**/
504public void add(String item) 
505  {
506  Argcheck.notnull(item, "item param was null");
507  Select.Option opt = new Select.Option(item);
508  add(opt); 
509  } 
510  
511/**
512Clears all data in this select.
513*/
514public void reset()
515  {
516  options.clear(); 
517  origSelectedMap.clear();
518  }
519  
520/** 
521This value (if set) is rendered as the html <tt>SIZE</tt> tag. 
522If the list contains more options than specified by size, the browser 
523will display the selection list with scrollbars.
524
525@return this object for method chaining convenience
526**/
527public Select setSize(int size) {
528  this.size = size;
529  return this;
530  } 
531
532/** 
533<tt>true</tt> is multiple selections are allowed, <tt>false</tt> otherwise. 
534This value (if set) is rendered as the html <tt>MULTIPLE</tt> tag.
535
536@return this object for method chaining convenience
537**/
538public Select allowMultiple(boolean allow) {
539  this.multiple = allow;
540  return this;
541  } 
542
543/** Represents an option in the selection list **/
544public static final class Option 
545  {
546  private String  value;
547  private String  text;
548  private boolean orig_selected;
549  
550  /** 
551  Constructs a new option with the specified text and 
552  value of the option tag.
553  
554  @param  text    the html text of the option tag 
555  @param  value   the value of the option tag
556  @param  selected  <tt>true</tt> if this option is selected
557            by default. If more than one selected option
558            is added to a select field and that select
559            field does <b>not</b> have it's multiple attribute
560            set, then the option displayed as selected is 
561            browser dependent (Moz1, IE6 show
562            the last selected, N4 the first). More than one
563            selected option should not be shown for non multiple
564            select fields anyway.
565  **/
566  public Option(String text, String value, boolean selected) 
567    {
568    this.text = text;
569    this.value = value;
570    this.orig_selected = selected;
571    }
572
573  /** 
574  Constructs a new unselected option with the specified 
575  text and value of the option tag.
576  
577  @param  text  the html text of the option tag 
578  @param  value the value of the option tag
579  **/
580  public Option(String text, String value) {
581    this(text, value, false);
582    }
583  
584  /** 
585  Constructs a new option with the specified text (and no
586  separate value tag).
587  
588  @param  text    the html text of the option tag 
589  @param  selected  <tt>true</tt> to select this option
590            <tt>false</tt> otherwise
591  **/
592  public Option(String text, boolean selected) {
593    this(text, null, selected);
594    }
595
596  /** 
597  Constructs a new unselected option with the specified
598  html text and no separate value.
599
600  @param  text  the html text of the option tag 
601  **/
602  public Option(String text) {
603    this(text, null, false);
604    }
605    
606  boolean isOrigSelected() {
607    return orig_selected;
608    }
609        
610  /** 
611  Returns the value of this option tag. If no value is set, 
612  returns the html text value for this option tag 
613  **/
614  public String getValue() 
615    {
616    if (value != null)
617      return value;
618    else
619      return text;
620    } 
621        
622  public String render(final boolean selected) {
623    StringBuffer buf = new StringBuffer(32);
624    buf.append("<option");
625    if (value != null) {
626      buf.append(" value='");
627      buf.append(value);
628      buf.append("'");
629      }
630    if (selected) {
631      buf.append(" SELECTED");
632      }
633    buf.append(">");
634    buf.append(text);
635    buf.append("</option>");
636    return buf.toString();
637    } 
638  
639  public String toString() {
640    return render(false);
641    }
642  } //~class Option
643    
644}          //~class Select