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    
006    package fc.web.forms;
007    
008    import java.io.*;
009    import java.util.*;
010    import java.sql.*;
011    import javax.servlet.*;
012    import javax.servlet.http.*;
013    
014    import fc.jdbc.*;
015    import fc.io.*;
016    import fc.util.*;
017    
018    /** 
019    An HTML Select field 
020    <p>
021    There 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    **/
047    public 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
053    LinkedHashMap   options = new LinkedHashMap();       
054    //orig selected options when form was constructed
055    Map       origSelectedMap = new HashMap(); 
056    int       size = 0;
057    boolean     multiple;
058    
059    static 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    /** 
071    Creates a new select element with no initial values. Multiple
072    selections are not initially allowed.
073    
074    @param  name    the field name
075    **/
076    public Select(String name)
077      {
078      super(name);
079      }
080      
081    /** 
082    Creates a new select element with the specified initial values
083    and no multiple selections allowed. There is no explicit default
084    selection which typically means the browser will show the first
085    item 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    **/
097    public Select(String name, List values)
098      {
099      super(name);
100      Argcheck.notnull(values, "values param was null");
101      initOptions(values);
102      }
103    
104    void 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    /**
117    The 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
120    first option returned by the query is selected for single-value selects.
121    <p>
122    Typically, this method may be invoked like:
123    <blockquote>
124    <tt>
125    useQuery(con, "select id, name from my_lookup_table", new Option("--choose--"));
126    </tt>
127    </blockquote>
128    If the lookup table has more than 2 columns, the query can look like (in
129    standard SQL syntax):
130    <blockquote>
131    <tt>
132    useQuery(con, "select id, <font color=blue>name1 || ' ' || name2</font> from my_lookup_table", new Option("--choose--"));
133    </tt>
134    </blockquote>
135    This 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    */
143    public Select useQuery(Connection con, String query, Select.Option defaultOpt) 
144    throws 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    /**
158    Convenience method that calls {@link useQuery(Connection, String, String)
159    useQuery(con, query, null)}.
160    */
161    public Select useQuery(Connection con, String query) throws SQLException
162      {
163      useQuery(con, query, null);
164      return this;
165      }
166    
167    /**
168    Uses the first column as the option text and if there is a second column
169    uses it as the corresponding option value.
170    */
171    public static List makeOptionsFromQuery(Connection con, String query) 
172    throws SQLException
173      {
174      return makeOptionsFromQuery(con, query, null);
175      }
176    
177    /**
178    Uses the first column as the option text and if there is a second column
179    uses it as the corresponding option value. Uses an additional default
180    option.
181    */
182    public static List makeOptionsFromQuery(
183      Connection con, String query, Select.Option defaultOption)
184    throws 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    
207    public Field.Type getType() {
208      return Field.Type.SELECT;
209      }
210    
211    /**
212    Sets the values for this select. Any previously set values are
213    first cleared before new values are set.
214    <p>
215    Note, to show user specific options via FormData, use a {@link RefreshableSelect}
216    instead.
217    
218    @param  values  a list of {@link Select.Option} objects
219    */
220    public 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    /** 
230    Returns a List containing the selected options. Each object in
231    the collection will be of type {@link Select.Option}. If there
232    are no selected options, the returned list will be an
233    empty list.
234    
235    @param  fd  the submited form data. This object should not be null
236          otherwise a NPE will be thrown.
237    **/
238    public 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    /** 
248    Convenience method that returns the selected option as a
249    String. <u>No guarantees are made as to which selection is
250    returned when more than one selection is selected (this
251    method is really meant for when the select only allows
252    single selections as a dropdown).</u>
253    <p>
254    The returned value is of type String obtained by called the
255    selected Option's {@link Select.Option#getValue} method.
256    <p>
257    If there is no selection, returns <tt>null</tt>
258    **/
259    public 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    /**
280    Convenience method that returns the single value of this field
281    as a Integer.
282    <p>
283    All the caveats of {@link #getSingleValueAsString()} apply.
284    
285    @throws NumberFormatException if the value could not be
286                    returned as in integer. 
287    */
288    public int getIntValue(FormData fd) {
289      return Integer.parseInt(getStringValue(fd));
290      }
291    
292    /**
293    Convenience method that returns the single value of this field
294    as a boolean. 
295    <p>
296    All the caveats of {@link #getSingleValueAsString()} apply.
297    In particular, the formdata should contain non-null data
298    with at least one selection.
299    */
300    public boolean getBooleanValue(FormData fd) {
301      return Boolean.valueOf(getStringValue(fd)).booleanValue();
302      }
303    
304    /**
305    Returns <tt>true</tt> if some option has been selected
306    by the user, <tt>false</tt> otherwise. (also returns
307    <tt>false</tt> is the specified form data is <tt>null</tt>).
308    */
309    public 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    */
353    public void setValueFromSubmit(FormData fd, HttpServletRequest req) 
354    throws 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    
382    private void addSelectedOpt(
383     HttpServletRequest req, Select.Data data, String submitValue)
384    throws 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    
402    public 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    /** 
481    Adds a new option to the selection list. Replaces any previous
482    option that was added previously with the same value.
483    
484    @param  opt the option to be added
485    **/
486    public 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    /** 
497    Adds a new option to the selection list. Replaces any previous
498    option that was added previously with the same value. This method
499    will have the same effect as if the {@link #add(Option) 
500    add(new Select.Option(item))} method was invoked.
501    
502    @param  item the option to be added
503    **/
504    public 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    /**
512    Clears all data in this select.
513    */
514    public void reset()
515      {
516      options.clear(); 
517      origSelectedMap.clear();
518      }
519      
520    /** 
521    This value (if set) is rendered as the html <tt>SIZE</tt> tag. 
522    If the list contains more options than specified by size, the browser 
523    will display the selection list with scrollbars.
524    
525    @return this object for method chaining convenience
526    **/
527    public 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. 
534    This value (if set) is rendered as the html <tt>MULTIPLE</tt> tag.
535    
536    @return this object for method chaining convenience
537    **/
538    public Select allowMultiple(boolean allow) {
539      this.multiple = allow;
540      return this;
541      } 
542    
543    /** Represents an option in the selection list **/
544    public 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