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 refreshable HTML Select field. This is different than {@link Select}
020in that is allows the values/options displayed by this select to be
021changed dynamically/per-user via the {@link #setValue(FormData, List)}
022method.
023<p>
024There is no easy way to persistently keep track of these values which may
025be different for each user. Therefore, unlike other field classes, this
026class does (by default) no check to see if the returned values by the
027user match at least one of the values displayed to the user in the first
028place. This can happen if values are hacked on the client side.
029<p>
030Database constraints (that only accept valid values) should be used to
031possibly handle bad data returned by this field.
032
033@author hursh jain
034**/
035
036public final class RefreshableSelect extends Select
037{
038static 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/** 
058Creates a new select element with no initial values. Multiple selections
059are not initially allowed.
060
061@param  name    the field name
062**/
063public RefreshableSelect(String name)
064  {
065  super(name);
066  }
067  
068/** 
069Creates a new select element with the specified initial values
070and no multiple selections allowed. There is no explicit default
071selection which typically means the browser will show the first
072item 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**/
084public RefreshableSelect(String name, List values)
085  {
086  super(name);
087  Argcheck.notnull(values, "values param was null");
088  initOptions(values);
089  }
090
091public Field.Type getType() {
092  return Field.Type.SELECT;
093  }
094
095/**
096Sets the options for this select in the specified form data. This is
097useful for showing different <i>initial</i> values to each user (before
098the form has been submitted by that user).
099<p>
100If the form has not been submitted, there is no form data object. A form
101data 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*/
106public 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
130specified form data. All of the initial options are used and option
131corresponding to the specified value is selected. There must be some
132initial options set for this field and the specified value should match
133one of the initial options.
134<p>
135If the form has not been submitted, there is no form data object. A form
136data object should be manually created if needed for storing the 
137selected 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*/
142public 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/**
164Convenience method that invokes {@link #setSelectedValue(fd, String)}
165after converting the specified integer value to a string.
166*/
167public void setSelectedValue(FormData fd, int value) {
168  setSelectedValue(fd, String.valueOf(value));
169  }
170
171/** 
172Returns a List containing the selected options. Each object in
173the collection will be of type {@link Select.Option}. If there
174are no selected options, the returned list will be an
175empty list.
176
177@param  fd  the submited form data. This object should not be null
178      otherwise a NPE will be thrown.
179**/
180public 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/** 
190Convenience method that returns the selected option as a
191String. <u>No guarantees are made as to which selection is
192returned when more than one selection is selected (this
193method is really meant for when the select only allows
194single selections as a dropdown).</u>
195<p>
196The returned value is of type String obtained by called the
197selected Option's {@link Select.Option#getValue} method.
198<p>
199If there is no selection, returns <tt>null</tt>
200**/
201public 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/**
222Convenience method that returns the single value of this field
223as a Integer.
224<p>
225All the caveats of {@link #getSingleValueAsString()} apply.
226
227@throws NumberFormatException if the value could not be
228                returned as in integer. 
229*/
230public int getIntValue(FormData fd) {
231  return Integer.parseInt(getStringValue(fd));
232  }
233
234/**
235Convenience method that returns the single value of this field
236as a boolean. 
237<p>
238All the caveats of {@link #getSingleValueAsString()} apply.
239In particular, the formdata should contain non-null data
240with at least one selection.
241*/
242public boolean getBooleanValue(FormData fd) {
243  return Boolean.valueOf(getStringValue(fd)).booleanValue();
244  }
245
246/**
247Returns <tt>true</tt> if some option has been selected
248by the user, <tt>false</tt> otherwise. (also returns
249<tt>false</tt> is the specified form data is <tt>null</tt>).
250*/
251public 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*/
294public void setValueFromSubmit(FormData fd, HttpServletRequest req) 
295throws 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
325private 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
333public 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/** 
413Adds a new option to the selection list. Replaces any previous
414option that was added previously with the same value.
415
416@param  opt the option to be added
417**/
418public 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/** 
429Adds a new option to the selection list. Replaces any previous
430option that was added previously with the same value. This method
431will have the same effect as if the {@link #add(Select.Option) 
432add(new Select.Option(item))} method was invoked.
433
434@param  item the option to be added
435**/
436public 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/**
444Clears all data in this select.
445*/
446public void reset()
447  {
448  options.clear(); 
449  origSelectedMap.clear();
450  }
451  
452/** 
453This value (if set) is rendered as the html <tt>SIZE</tt> tag. 
454If the list contains more options than specified by size, the browser 
455will display the selection list with scrollbars.
456
457@return this object for method chaining convenience
458**/
459public 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. 
466This value (if set) is rendered as the html <tt>MULTIPLE</tt> tag.
467
468@return this object for method chaining convenience
469**/
470public Select allowMultiple(boolean allow) {
471  this.multiple = allow;
472  return this;
473  } 
474}          //~class Select