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 refreshable HTML Select field. This is different than {@link Select}
020    in that is allows the values/options displayed by this select to be
021    changed dynamically/per-user via the {@link #setValue(FormData, List)}
022    method.
023    <p>
024    There is no easy way to persistently keep track of these values which may
025    be different for each user. Therefore, unlike other field classes, this
026    class does (by default) no check to see if the returned values by the
027    user match at least one of the values displayed to the user in the first
028    place. This can happen if values are hacked on the client side.
029    <p>
030    Database constraints (that only accept valid values) should be used to
031    possibly handle bad data returned by this field.
032    
033    @author hursh jain
034    **/
035    
036    public final class RefreshableSelect extends Select
037    {
038    static final class Data 
039      {
040      //These are null by default but can override the options for the
041      //select instance when set per user.This is useful when values are refreshed
042      //every submit by re-querying the database. (we don't want to set new
043      //values in the instance fields because that would effectively cause
044      //all threads to synchronize rendering this field)
045      private LinkedHashMap options; 
046      private Map       origSelectedMap; 
047      
048      //To store selected options submitted by the user
049      //acts like a set, used via containsKey() to see if an options
050      //is selected (so we can reselect it) during rendering. 
051      private Map  selectedMap = null;  //null implies no submitted data  
052      
053      //to store all options submitted by user
054      private List selectedList = null;
055      }
056    
057    /** 
058    Creates a new select element with no initial values. Multiple selections
059    are not initially allowed.
060    
061    @param  name    the field name
062    **/
063    public RefreshableSelect(String name)
064      {
065      super(name);
066      }
067      
068    /** 
069    Creates a new select element with the specified initial values
070    and no multiple selections allowed. There is no explicit default
071    selection which typically means the browser will show the first
072    item in the list as the selection item.
073    
074    @param  name    the field name
075    @param  values    the initial option values for this class. The
076              objects contained in the list must be of type
077              {@link Select.Option} otherwise a 
078              <tt>ClassCastException</tt> may be thrown in 
079              future operations on this object.
080    
081    @throws IllegalArgumentException  if the values parameter was null
082                      (note: an empty but non-null List is ok)
083    **/
084    public RefreshableSelect(String name, List values)
085      {
086      super(name);
087      Argcheck.notnull(values, "values param was null");
088      initOptions(values);
089      }
090    
091    public Field.Type getType() {
092      return Field.Type.SELECT;
093      }
094    
095    /**
096    Sets the options for this select in the specified form data. This is
097    useful for showing different <i>initial</i> values to each user (before
098    the form has been submitted by that user).
099    <p>
100    If the form has not been submitted, there is no form data object. A form
101    data object should be manually created if needed for storing the value.
102    
103    @param  fd    the non-null form data used for rendering the form
104    @param  values  a list of {@link Select.Option} objects
105    */
106    public void setValue(FormData fd, final List values)
107      {
108      Argcheck.notnull(fd, "specified fd param was null");
109      Argcheck.notnull(values, "specified values param was null");
110    
111      Data data = new Data();
112      data.options = new LinkedHashMap();
113      data.origSelectedMap = new HashMap();
114      fd.putData(name, data);
115      
116      for (int n = 0; n < values.size(); n++) {
117        Select.Option item = (Select.Option) values.get(n);
118        String itemval = item.getValue();
119        data.options.put(itemval, item);
120        if (item.isOrigSelected()) {
121          data.origSelectedMap.put(itemval, item);  
122          }
123        }   
124      //System.out.println("data.options ==> " + data.options);  
125      }
126    
127    
128    /**
129    <i>Convenience</i> method that sets the options for this select in the
130    specified form data. All of the initial options are used and option
131    corresponding to the specified value is selected. There must be some
132    initial options set for this field and the specified value should match
133    one of the initial options.
134    <p>
135    If the form has not been submitted, there is no form data object. A form
136    data object should be manually created if needed for storing the 
137    selected value.
138    
139    @param  fd    the non-null form data used for rendering the form
140    @param  value the option with this value is selected
141    */
142    public void setSelectedValue(FormData fd, String value)
143      {
144      Argcheck.notnull(fd, "specified fd param was null");
145      Argcheck.notnull(value, "specified selected param was null");
146    
147      Data data = new Data();
148      data.options = new LinkedHashMap(options);
149      data.origSelectedMap = new HashMap();
150      fd.putData(name, data);
151      
152      Iterator it = data.options.values().iterator();
153      while (it.hasNext())
154        {
155        Select.Option opt = (Select.Option) it.next();
156        String optval = opt.getValue();
157        if (optval.equals(value)) {
158          data.origSelectedMap.put(optval, opt);  
159          }
160        }   
161      }
162    
163    /**
164    Convenience method that invokes {@link #setSelectedValue(fd, String)}
165    after converting the specified integer value to a string.
166    */
167    public void setSelectedValue(FormData fd, int value) {
168      setSelectedValue(fd, String.valueOf(value));
169      }
170    
171    /** 
172    Returns a List containing the selected options. Each object in
173    the collection will be of type {@link Select.Option}. If there
174    are no selected options, the returned list will be an
175    empty list.
176    
177    @param  fd  the submited form data. This object should not be null
178          otherwise a NPE will be thrown.
179    **/
180    public List getValue(FormData fd) 
181      {
182      RefreshableSelect.Data data = (RefreshableSelect.Data) fd.getData(name);
183      if (data == null) {
184        return Form.empty_list;
185        }
186      return data.selectedList;
187      }
188    
189    /** 
190    Convenience method that returns the selected option as a
191    String. <u>No guarantees are made as to which selection is
192    returned when more than one selection is selected (this
193    method is really meant for when the select only allows
194    single selections as a dropdown).</u>
195    <p>
196    The returned value is of type String obtained by called the
197    selected Option's {@link Select.Option#getValue} method.
198    <p>
199    If there is no selection, returns <tt>null</tt>
200    **/
201    public String getStringValue(FormData fd) 
202      {
203      String result = null;
204      
205      RefreshableSelect.Data data = (RefreshableSelect.Data) fd.getData(name);
206      if (data == null) {
207        return null;
208        }
209    
210      List list = data.selectedList;
211      if (list == null || list.size() == 0) {
212        return result;
213        }
214      else {
215        Select.Option opt = (Select.Option) list.get(0);
216        return opt.getValue();
217        }
218      }
219    
220    
221    /**
222    Convenience method that returns the single value of this field
223    as a Integer.
224    <p>
225    All the caveats of {@link #getSingleValueAsString()} apply.
226    
227    @throws NumberFormatException if the value could not be
228                    returned as in integer. 
229    */
230    public int getIntValue(FormData fd) {
231      return Integer.parseInt(getStringValue(fd));
232      }
233    
234    /**
235    Convenience method that returns the single value of this field
236    as a boolean. 
237    <p>
238    All the caveats of {@link #getSingleValueAsString()} apply.
239    In particular, the formdata should contain non-null data
240    with at least one selection.
241    */
242    public boolean getBooleanValue(FormData fd) {
243      return Boolean.valueOf(getStringValue(fd)).booleanValue();
244      }
245    
246    /**
247    Returns <tt>true</tt> if some option has been selected
248    by the user, <tt>false</tt> otherwise. (also returns
249    <tt>false</tt> is the specified form data is <tt>null</tt>).
250    */
251    public boolean isFilled(FormData fd) 
252      {
253      if (fd == null)
254        return false;
255        
256      RefreshableSelect.Data data = (RefreshableSelect.Data) fd.getData(name);
257      if (data == null) {
258        return false;
259        }
260      List list = data.selectedList;
261      return (list != null && list.size() != 0);
262      }
263    
264    /*
265     Select values can be present as:
266     selectname=value
267    
268     or for multiple selections (for say a select called "sel1")
269     sel1=value1&sel1=val2
270     
271     The values sent are those in the value tag in the option
272     and if missing those of the corresponding html for the option.
273    
274     If a select allows multiple and none are selected, then
275     nothing at all is sent (note this is different than for
276     single (and not multiple) selects, where some value will always
277     be sent (since something is always selected).
278    
279     We can track submitted/selected options in 2 ways:
280     1. go thru the option list and set a select/unselect on each option
281     element and then the option element renders itself appropriately.
282     2. keep a separate list of selected elements and at render time display
283     the item as selected only if it's also in the select list.
284    
285     (w) implies that we have to specify select/no select at
286     render time to each option as opposed to setting that flag
287     in the option before telling the option to render itself.
288     (2) is chosen here.
289    
290     the client can send hacked 2 or more options with the same
291     value this method will simply add any such duplicate values
292     at the end of the list. this is ok.
293    */
294    public void setValueFromSubmit(FormData fd, HttpServletRequest req) 
295    throws SubmitHackedException
296      {
297      //value(s) associated with the selection field name
298      String[] values = req.getParameterValues(name);
299      
300      if (values == null) {
301        return;
302        }
303    
304      //lazy instantiation
305      RefreshableSelect.Data data = new RefreshableSelect.Data();
306      data.selectedMap = new HashMap();
307      data.selectedList = new ArrayList();
308      
309      fd.putData(name, data);
310      
311      if (multiple && values.length > 1)
312        hacklert(req, "recieved multiple values for a single value select");
313        
314      if (multiple) 
315        {
316        for (int n = 0; n < values.length; n++) {
317          addSelectedOpt(req, data, values[n]);
318          }
319        }
320      else{
321        addSelectedOpt(req, data, values[0]);
322        }
323      }
324    
325    private void addSelectedOpt(
326     HttpServletRequest req, RefreshableSelect.Data data, String submitValue)
327      {
328      Select.Option opt = new Select.Option(submitValue);
329      data.selectedMap.put(opt.getValue(), opt);
330      data.selectedList.add(opt);
331      } 
332    
333    public void renderImpl(FormData fd, Writer writer) throws IOException
334      {
335      RefreshableSelect.Data data = null;
336    
337      Map options =  this.options;
338      Map origSelectedMap = this.origSelectedMap;
339      
340      if (fd != null)  { //we have submit or initial data
341        data = (RefreshableSelect.Data) fd.getData(name); //can be null
342        if (data != null) {
343          if (data.options != null) { 
344            /*per user options & origMap were set */      
345            options = data.options;
346            origSelectedMap = data.origSelectedMap;
347            }
348          }
349        }
350      
351      writer.write("<select"); 
352      writer.write(" name='");
353      writer.write(name);
354      writer.write("'");
355      
356      if (size > 0) {
357        writer.write(" size='");
358        writer.write(String.valueOf(size));
359        writer.write("'"); 
360        }
361        
362      if (! enabled || ! isEnabled(fd)) {
363        writer.write(" disabled");
364        }
365    
366      if (multiple) {
367        writer.write(" multiple"); 
368        } 
369        
370      if (renderStyleTag) {
371        writer.write(" style='");
372        writer.write(styleTag);
373        writer.write("'");
374        }
375        
376      final int arlen = arbitraryString.size();
377      for (int n = 0; n < arlen; n++) {
378        writer.write(" ");
379        writer.write(arbitraryString.get(n).toString());
380        }
381        
382      writer.write(">\n");
383      
384      Iterator it = options.values().iterator(); 
385      while (it.hasNext()) 
386        {
387        Select.Option item = (Select.Option) it.next();
388        String itemval = item.getValue();
389        
390        boolean selected = false;
391        
392        if (fd != null)   /* maintain submit state */
393          {         
394          if (data.selectedMap != null) { /*there was a submit*/  
395            selected = data.selectedMap.containsKey(itemval);
396            }
397          else {            /*initial data*/
398            selected = origSelectedMap.containsKey(itemval);
399            }
400          }
401          else{    /* form not submitted, show original state */
402              selected = origSelectedMap.containsKey(itemval);
403            }
404           
405        writer.write(item.render(selected));
406        writer.write("\n"); //sufficient for easy view source in browser    
407        } 
408    
409      writer.write("</select>");
410      }
411      
412    /** 
413    Adds a new option to the selection list. Replaces any previous
414    option that was added previously with the same value.
415    
416    @param  opt the option to be added
417    **/
418    public void add(Select.Option opt) 
419      {
420      Argcheck.notnull(opt, "opt param was null");
421      String optval = opt.getValue();
422      options.put(optval, opt);
423      if (opt.isOrigSelected()) {
424        origSelectedMap.put(optval, opt); 
425        }
426      } 
427    
428    /** 
429    Adds a new option to the selection list. Replaces any previous
430    option that was added previously with the same value. This method
431    will have the same effect as if the {@link #add(Select.Option) 
432    add(new Select.Option(item))} method was invoked.
433    
434    @param  item the option to be added
435    **/
436    public void add(String item) 
437      {
438      Argcheck.notnull(item, "item param was null");
439      Select.Option opt = new Select.Option(item);
440      add(opt); 
441      } 
442      
443    /**
444    Clears all data in this select.
445    */
446    public void reset()
447      {
448      options.clear(); 
449      origSelectedMap.clear();
450      }
451      
452    /** 
453    This value (if set) is rendered as the html <tt>SIZE</tt> tag. 
454    If the list contains more options than specified by size, the browser 
455    will display the selection list with scrollbars.
456    
457    @return this object for method chaining convenience
458    **/
459    public Select setSize(int size) {
460      this.size = size;
461      return this;
462      } 
463    
464    /** 
465    <tt>true</tt> is multiple selections are allowed, <tt>false</tt> otherwise. 
466    This value (if set) is rendered as the html <tt>MULTIPLE</tt> tag.
467    
468    @return this object for method chaining convenience
469    **/
470    public Select allowMultiple(boolean allow) {
471      this.multiple = allow;
472      return this;
473      } 
474    }          //~class Select