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 javax.servlet.*;
009    import javax.servlet.http.*;
010    import java.io.*;
011    import java.util.*;
012    import java.util.regex.*;
013    import java.sql.*;
014    
015    import fc.jdbc.*;
016    import fc.io.*;
017    import fc.util.*;
018    
019    /** 
020    A dependent HTML DependentSelect field. The values of this field change based
021    on the chosen value of some other field in the form. These new values are
022    calculated/set on the server side. 
023    <p>
024    Another 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
026    use AJAX on the client side.
027    
028    @author hursh jain
029    **/
030    public final class DependentSelect extends DependentField
031    {
032    public 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    /** 
072    Creates a new select element with no initial values. Multiple
073    selections are not initially allowed.
074    
075    @param  name    the field name
076    **/
077    public DependentSelect(String name)
078      {
079      super(name);
080      }
081      
082    public Field.Type getType() {
083      return Field.Type.SELECT;
084      }
085    
086    /** 
087    Returns a List containing the selected options. Each object in
088    the collection will be of type {@link Select.Option}. If there
089    are no selected options, the returned list will be an empty list.
090    
091    @param  fd  the submited form data
092    **/
093    public 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    /** 
103    Convenience method that returns the selected option as a String. <u>No
104    guarantees are made as to which selection is returned when more than one
105    selection is selected (this method is really meant for when the select only
106    allows single selections as a dropdown).</u>
107    <p>
108    The returned value is of type String obtained by called the selected
109    Select.Option's {@link Select.Option#getValue} method.
110    <p>
111    If there is no selection, returns <tt>null</tt>
112    **/
113    public 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    /**
132    Convenience method that returns the single value of this field
133    as a Integer.
134    <p>
135    All the caveats of {@link #getSingleValueAsString()} apply.
136    
137    @throws NumberFormatException if the value could not be
138                    returned as in integer. 
139    */
140    public int getIntValue(FormData fd) {
141      return Integer.parseInt(getStringValue(fd));
142      }
143    
144    /**
145    Convenience method that returns the single value of this field
146    as a boolean. 
147    <p>
148    All the caveats of {@link #getSingleValueAsString()} apply.
149    In particular, the formdata should contain non-null data
150    with at least one selection.
151    */
152    public boolean getBooleanValue(FormData fd) {
153      return Boolean.valueOf(getStringValue(fd)).booleanValue();
154      }
155    
156    /**
157    Returns <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
159    form data is <tt>null</tt>).
160    */
161    public 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    */
178    public 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    
202    public void renderImpl(FormData fd, Writer writer) 
203    throws 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. 
295    This value (if set) is rendered as the html <tt>MULTIPLE</tt> tag.
296    
297    @return this object for method chaining convenience
298    **/
299    public DependentSelect allowMultiple(FormData fd, boolean allow) 
300      {
301      Data d = getDataObject(fd);
302      d.multiple = allow;
303      return this;
304      } 
305    
306    /** 
307    This value (if set) is rendered as the html <tt>SIZE</tt> tag. 
308    If the list contains more options than specified by size, the browser 
309    will display the selection list with scrollbars.
310    
311    @return this object for method chaining convenience
312    **/
313    public DependentSelect setSize(FormData fd, int size) 
314      {
315      Data d = getDataObject(fd);
316      d.size = size;
317      return this;
318      } 
319      
320    /**
321    Sets the selected values for this select in the specified form data. This
322    is 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
324    a newly created {@link DependentSelect.Data} object which is then stored
325    inside the form data. Since these are set in the FormData object, these
326    values are request specific and can differ per user.
327    <p>
328    If the form has not been submitted, there is no form data object. A form
329    data 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    */
334    public 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    /**
344    Convenience method to set values in the specified {@link
345    DependentSelect.Data} object. 
346    The 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>
355    and
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    */
365    public 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    /**
379    Returns the data object for this field from the formdata. returns
380    the initial data (if applicable) if the form is being rendered for
381    the first time or if no dependent data has been created.
382    */
383    Data getDataObject(FormData fd)
384      {
385      Data d = (Data) fd.getData(name);
386      return d;
387      }
388        
389    }          //~class DependentSelect