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 javax.servlet.*;
009import javax.servlet.http.*;
010import java.io.*;
011import java.util.*;
012import java.util.regex.*;
013import java.sql.*;
014
015import fc.jdbc.*;
016import fc.io.*;
017import fc.util.*;
018
019/** 
020A dependent HTML DependentSelect field. The values of this field change based
021on the chosen value of some other field in the form. These new values are
022calculated/set on the server side. 
023<p>
024Another good (and equally valid) approach is to instead use javascript arrays
025(with different sets of values) and swap out values on the client and/or
026use AJAX on the client side.
027
028@author hursh jain
029**/
030public final class DependentSelect extends DependentField
031{
032public static class Data
033  {
034  //---------- Select Data -------------
035  private LinkedHashMap   options  = new LinkedHashMap();
036  //the initially selected options when form was constructed
037  private Map       origSelectedMap = new HashMap();
038  private int       size = 0;
039  private boolean     multiple;
040  //---------- Client Submit Data -------
041  //[option1.getValue() -> option1] ...
042  private Map  selectedMap = null;  //created if there is data  
043  //[option1, option2....]
044  private List selectedList = null; //created if there is data
045  //-------------------------------------
046  /**
047  Convienence method for use by the {@link Dependency#getInitialValues}
048  method. 
049  */
050  public void addOption(Select.Option item) {
051    String itemval = item.getValue();
052    options.put(itemval, item);
053    if (item.isOrigSelected()) {
054      origSelectedMap.put(itemval, item); 
055      } 
056    }
057  
058  public void clearSubmittedData() {
059    if (selectedMap != null)
060      selectedMap.clear();
061    if (selectedList != null)
062      selectedList.clear();
063    }
064  
065  public String toString() { 
066    return new ToString(this, ToString.Style.VisibleLevel.PRIVATE)
067    .reflect().render(); 
068    }
069  }
070
071/** 
072Creates a new select element with no initial values. Multiple
073selections are not initially allowed.
074
075@param  name    the field name
076**/
077public DependentSelect(String name)
078  {
079  super(name);
080  }
081  
082public Field.Type getType() {
083  return Field.Type.SELECT;
084  }
085
086/** 
087Returns a List containing the selected options. Each object in
088the collection will be of type {@link Select.Option}. If there
089are no selected options, the returned list will be an empty list.
090
091@param  fd  the submited form data
092**/
093public List getValue(FormData fd) 
094  {
095  Data data = (Data) fd.getData(name);
096  if (data == null || data.selectedList == null) {
097    return Form.empty_list;
098    }
099  return data.selectedList;
100  }
101
102/** 
103Convenience method that returns the selected option as a String. <u>No
104guarantees are made as to which selection is returned when more than one
105selection is selected (this method is really meant for when the select only
106allows single selections as a dropdown).</u>
107<p>
108The returned value is of type String obtained by called the selected
109Select.Option's {@link Select.Option#getValue} method.
110<p>
111If there is no selection, returns <tt>null</tt>
112**/
113public String getStringValue(FormData fd) 
114  {
115  Data data = (Data) fd.getData(name);
116  if (data == null) {
117    return null;
118    }
119
120  List list = data.selectedList;
121  if (list == null || list.size() == 0) {
122    return null;
123    }
124  else {
125    Select.Option opt = (Select.Option) list.get(0);
126    return opt.getValue();
127    }
128  }
129
130
131/**
132Convenience method that returns the single value of this field
133as a Integer.
134<p>
135All the caveats of {@link #getSingleValueAsString()} apply.
136
137@throws NumberFormatException if the value could not be
138                returned as in integer. 
139*/
140public int getIntValue(FormData fd) {
141  return Integer.parseInt(getStringValue(fd));
142  }
143
144/**
145Convenience method that returns the single value of this field
146as a boolean. 
147<p>
148All the caveats of {@link #getSingleValueAsString()} apply.
149In particular, the formdata should contain non-null data
150with at least one selection.
151*/
152public boolean getBooleanValue(FormData fd) {
153  return Boolean.valueOf(getStringValue(fd)).booleanValue();
154  }
155
156/**
157Returns <tt>true</tt> if some option has been selected by the user,
158<tt>false</tt> otherwise. (also returns <tt>false</tt> is the specified
159form data is <tt>null</tt>).
160*/
161public boolean isFilled(FormData fd) 
162  {
163  if (fd == null)
164    return false;
165    
166  Data data = (Data) fd.getData(name);
167  
168  if (data == null) 
169    return false;
170  
171  List list = data.selectedList;
172  return (list != null && list.size() != 0);
173  }
174
175/**
176@see Select#setValueFromSubmit
177*/
178public void setValueFromSubmit(FormData fd, HttpServletRequest req) 
179  {
180  dependency.setDependencyDataFromSubmit(fd, req);
181  
182  //value(s) associated with the selection field name
183  String[] values = req.getParameterValues(name);
184  
185  Data data = new Data();
186  fd.putData(name, data);
187
188  if (values == null) {
189    return;
190    }
191
192  data.selectedMap = new HashMap();
193  data.selectedList = new ArrayList();
194        
195  for (int n = 0; n < values.length; n++) {
196    Select.Option opt = new Select.Option(values[n]);
197    data.selectedMap.put(opt.getValue(), opt);
198    data.selectedList.add(opt);
199    }
200  }
201
202public void renderImpl(FormData fd, Writer writer) 
203throws SQLException, IOException
204  {
205  Data data = null;
206  
207  if (fd == null) {
208    data = (Data) dependency.getInitialValues(this);
209    }
210  else{
211  //Case 1. Form submitted
212  //  dependency updates the data object for this field in the formdata.
213  //  _after_ the form is submitted. The dependency will set render data
214  //  in a non-null data object (creating it if necessary).
215  //  (even though the submitted data parts in the data object can be null)
216  //
217  //Case 2. Form not submitted but FormData created for initial values.
218  // The dependency will not be invoked by the form handling
219  //  mechanism so we still need to get initial values manually. 
220  //
221    data = (Data) fd.getData(name);
222    if (data == null) {
223      data = (Data) dependency.getInitialValues(this);
224      }
225    }
226  
227  Argcheck.notnull(data, "Internal error: unexpected state, data was null");
228    
229  writer.write("<select");  
230  writer.write(" name='");
231  writer.write(name);
232  writer.write("'");
233  
234  if (data.size > 0) {
235    writer.write(" size='");
236    writer.write(String.valueOf(data.size));
237    writer.write("'"); 
238    }
239    
240  if (data.multiple) {
241    writer.write(" multiple"); 
242    } 
243  
244  if (! enabled || ! isEnabled(fd)) {
245    writer.write(" disabled");
246    }
247    
248  if (renderStyleTag) {
249    writer.write(" style='");
250    writer.write(styleTag);
251    writer.write("'");
252    }
253    
254  final int arlen = arbitraryString.size();
255  for (int n = 0; n < arlen; n++) {
256    writer.write(" ");
257    writer.write(arbitraryString.get(n).toString());
258    }
259    
260  writer.write(">\n");
261  
262  Iterator it = data.options.values().iterator(); 
263  while (it.hasNext()) 
264    {
265    Select.Option item = (Select.Option) it.next();
266    String itemval = item.getValue();
267    
268    boolean selected = false;
269  
270    if (fd != null)   /* maintain submit state */
271      {         
272      if (data.selectedMap != null){
273        selected = data.selectedMap.containsKey(itemval);
274        }
275      else {  
276        //form submitted but no submit data for this field
277        //hence selecteMap == null
278        }
279      }
280    else{    /* form not submitted, show original state */
281        selected = data.origSelectedMap.containsKey(itemval);
282        }
283  
284    writer.write(item.render(selected));
285    writer.write("\n"); //sufficient for easy view source in browser    
286    } 
287
288  writer.write("</select>\n");
289
290  dependency.renderDependencyData(fd, writer);
291  }
292            
293/** 
294<tt>true</tt> if multiple selections are allowed, <tt>false</tt> otherwise. 
295This value (if set) is rendered as the html <tt>MULTIPLE</tt> tag.
296
297@return this object for method chaining convenience
298**/
299public DependentSelect allowMultiple(FormData fd, boolean allow) 
300  {
301  Data d = getDataObject(fd);
302  d.multiple = allow;
303  return this;
304  } 
305
306/** 
307This value (if set) is rendered as the html <tt>SIZE</tt> tag. 
308If the list contains more options than specified by size, the browser 
309will display the selection list with scrollbars.
310
311@return this object for method chaining convenience
312**/
313public DependentSelect setSize(FormData fd, int size) 
314  {
315  Data d = getDataObject(fd);
316  d.size = size;
317  return this;
318  } 
319  
320/**
321Sets the selected values for this select in the specified form data. This
322is useful for showing different <i>initial</i> values to each user
323(before the form has been submitted by that user). The values are set in
324a newly created {@link DependentSelect.Data} object which is then stored
325inside the form data. Since these are set in the FormData object, these
326values are request specific and can differ per user.
327<p>
328If the form has not been submitted, there is no form data object. A form
329data object should be manually created if needed for storing the value.
330
331@param  data  an existing form data object.
332@param  values  a list of {@link Select.Option} objects
333*/
334public void setValue(FormData fd, List values)
335  {
336  Argcheck.notnull(fd, "specified fd param was null");
337
338  DependentSelect.Data data = new DependentSelect.Data();
339  fd.putData(name, data);
340  setValue(data, values);
341  }
342  
343/**
344Convenience method to set values in the specified {@link
345DependentSelect.Data} object. 
346The following two are equivalent:
347<blockquote>
348<pre>
349  List values .....
350  DependentSelect.Data data = new DependentSelect.Data();
351  fd.putData(name, data);
352  setValue(<i>data</i>, values);
353</blockquote>
354</pre>
355and
356<blockquote>
357<pre>
358  List values .....
359  setValue(<i>fd</i>, values);
360</blockquote>
361</pre>
362@param  data  an existing DependentSelect.Data object.
363@param  values  a list of {@link Select.Option} objects
364*/
365public void setValue(DependentSelect.Data data, List values)
366  {
367  Iterator it = values.iterator();
368  while (it.hasNext()) {
369    Select.Option item = (Select.Option) it.next();
370    String itemval = item.getValue();
371    data.options.put(itemval, item);
372    if (item.isOrigSelected()) {
373      data.origSelectedMap.put(itemval, item);  
374      }
375    }   
376  }
377  
378/**
379Returns the data object for this field from the formdata. returns
380the initial data (if applicable) if the form is being rendered for
381the first time or if no dependent data has been created.
382*/
383Data getDataObject(FormData fd)
384  {
385  Data d = (Data) fd.getData(name);
386  return d;
387  }
388    
389}          //~class DependentSelect