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.sql.*;
013    
014    import fc.jdbc.*;
015    import fc.io.*;
016    import fc.util.*;
017    
018    /**
019    Represents an HTML form. It is possible to instantiate a different form
020    object per user and store each form in that users session scope. However,
021    more typically, instances of this object are instantiated once per unique
022    form and stored in web application scope.
023    <p>
024    Method in this class are thread safe since form processing is done
025    entirely within various methods and the form processing results in the
026    creation of seperate {@link FormData} objects for each request.
027    <p>
028    Various {@link Field Fields} can be added to the form at form creation
029    time. Some fields (like {@link Hidden} can also be added per user/request, 
030    dynamically, to the {@link FormData} object. This is useful while saving
031    some user or request specific information.
032    <p>
033    A form collects data from the front-end/web-client and often saves it to a
034    back end database entity. Form data validation can be done in 2 ways:
035    <ol>
036      <li>at the <i>form</i> level where the form collects data and validates
037      it via validators associated with the form object.
038      <li>at the <i>field</i> level where the form itself does no higher
039      level validation but populates a field and then asks that field to
040      validate itself.
041    </ol>
042    In both cases, any errors associated with the form can be shown back to the
043    user. This framework allows both types of validation methods to be used.
044    Using validation and/or showing validation errors is optional. This framework
045    can simply be used to maintain form state and validation can be done via
046    other ad-hoc code (outside of this framework) as needed.
047    <p>
048    An HTML form presented to the user can contain fields from more than 1
049    database table. For example, there could to 2 database tables, say
050    "person" and "office" both with a "other" column. Since <u>each field
051    contained in the form object must have a unique name</u>, there will be a
052    name clash between the fields/attributes of the entities when 2 fields
053    with the name "other" are added to the form.
054    <p>
055    There are 2 general approaches to solving this problem. 
056    <ol>
057    <li>Seperate database tables should be represented by seperate Form objects
058    (all of which can be rendered in the same HTML document shown to the user).
059    Different instances of form objects (say 1 per table) act as seperate
060    name-spaces and fields having the same name can exist in these seperate
061    instances. However, the dynamic server page where the form object is
062    rendered/submitted has to obtain these seperate instances from the application
063    object (or wherever all forms are stored) -- which adds some complexity.
064    <li>Create 1 instance of a form object containing fields from more than one
065    table. In this case, some or all field names can be prepended (or suffixed)
066    with the name of the database table. This removes any name clashes if done
067    only for clashing fields; if done for every field may help in documenting
068    which table that field belongs to. There is more typing involved with this
069    approach.
070    </ol>
071    Either of the above approaches is valid. 
072    <p>
073    Sometimes, other methods need to obtain a field (for a particular table) by
074    name. For example, the managers generated by {@link fc.jdbc.dbo.Generate}
075    have a <tt>addValidators(form)</tt> method that extract the fields for a
076    particular table and add validators to that field. This method in
077    particular need to be passed either the appropriate form object in which
078    that field exists (if there are multiple form objects) or in the case of
079    only 1 form object, passed the prefix for that particular field, if that
080    field was added with a prefix to it's name.
081    
082    @author hursh jain
083    **/
084    public class Form 
085    {
086    //useful for method that return empty lists/maps in lieu of nulls
087    static final public List empty_list = 
088      Collections.unmodifiableList(new ArrayList());
089    
090    static final public Map empty_map = 
091      Collections.unmodifiableMap(new HashMap());
092    
093    protected   Log       log;
094    //fields have field-level validators  
095    protected   LinkedHashMap   fields;
096    //collections of fields -- t  hese fields are directly added
097    //to the fields map also  
098    protected LinkedHashMap   fieldRef; 
099    protected   LinkedHashMap     formValidators; //form-level validators
100    protected List        dependencies;
101    protected   String        name;     //the form name
102    protected String        method;     
103    protected String        actionURL;
104    protected String        displayURL;
105    //protected String        fieldNamePrefix;
106    protected   java.util.Timer   timer;
107    protected SubmitHackedHandler hackHandler;
108    /** 
109    Creates a new form with the specified name, method (get/post) and URL
110    **/
111    public Form(String name, String method, String actionURL, Log log, 
112          SubmitHackedHandler handler) 
113      { 
114      this.name = name; 
115      this.method = method;
116      this.actionURL = actionURL;
117      fields = new LinkedHashMap();
118      fieldRef = new LinkedHashMap();
119      formValidators = new LinkedHashMap();
120      dependencies = new ArrayList();
121      this.log = log;
122    
123      this.hackHandler = handler;
124      if (handler == null) {
125        hackHandler = new SubmitHackedHandler(this);
126        }
127      }
128    
129    /*
130    Creates a new form with the specified name, method (get/post) and URL
131    and the default {@link SubmitHackedHandler} 
132    **/
133    public Form(String name, String method, String actionURL, Log log)     
134      {
135      this(name, method, actionURL, log, null);
136      }
137    
138    /** 
139    Creates a new form with logging to the default log retreived by
140    {@link Log#getDefault} and the default {@link SubmitHackedHandler}
141    **/
142    public Form(String name, String method, String actionURL) 
143      { 
144      this(name, method, actionURL, Log.getDefault(), null);
145      }
146    
147    /** 
148    Creates a new form with the specified name, method=POST and no action URL
149    **/
150    public Form(String name) 
151      { 
152      this(name, "post", null);
153      }
154    
155    /**
156    This method should be called when the form is removed from session or
157    application scope. It is very important to do this if there are background
158    update tasks because this method stops such background refresh timer thread
159    (which would otherwise continue to run).
160    */
161    public void destroy()
162      {
163      timer.cancel();
164      fields.clear();
165      }
166    
167    /** 
168    Removes the specified field from the form. See {@link
169    java.util.Map#remove(Object)} for the general contract.
170    
171    @param  field the field to remove from the form.
172    **/
173    public Object remove(Field field) { 
174      return fields.remove(field.getName());
175      }
176    
177    /** 
178    Adds the specified element to the form. The added field can
179    later be retrieved via it's name. The field iteration order
180    will be the same order in which fields are added via this
181    method.
182    <p>
183    Note, field names have to be unique. "Grouped" fields (such
184    as HTML form checkboxes or radio buttons) which require the
185    same field name are encapsulated by a single appropriate
186    grouped field object such as {@link RadioGroup} and {@link
187    CheckboxGroup}. This field object itself should have a
188    unique name.
189    
190    @return  A reference to this form as a convenience for method chaining
191    @throws  IllegalArgumentException if an element with the same name
192                      already exists in this form.
193    **/
194    public Form add(Field field) 
195      { 
196      String name = field.getName();
197      
198      //not needed,  prefix each field manually if needed.
199      /*
200      if (fieldNamePrefix != null) {
201        name = fieldNamePrefix + name;
202        }
203      */
204      if (fields.containsKey(name)) {
205        throw new IllegalArgumentException("A form element by that name [" + name + "] already exists");
206        }
207      fields.put(name, field); 
208      field.setParentForm(this);
209    
210      return this;
211      }
212    
213    /**
214    Adds the specified dependency to the form.
215    */
216    public Form add(Dependency d) 
217      { 
218      dependencies.add(d); 
219    
220      return this;
221      }
222    
223    
224    /**
225    Adds a {@link FieldRef} to this Form. This is useful when
226    to track a set of related fields in a form. (Note: those fields
227    <b>must</b> also directly be added to the form).
228    */
229    public Form add(FieldRef ref) 
230      {
231      String name = ref.getName();
232      if (fieldRef.containsKey(name)) {
233        throw new IllegalArgumentException("A fieldRef by that name [" + name+ "] already exists");
234        }
235      fieldRef.put(name, ref);
236      return this;
237      }
238    
239    /**
240    Adds a <b>form level</b> validation error message. This method is needed when
241    form level validation is done via classes that are <b>not</b> subclasses
242    of {@link FormValidator} (these classes are used outside of the
243    validation framework and their error message still needs to be added to
244    the form data).
245    <p> 
246    <b> This is a very useful method for adding the results of arbitrary code
247    to this form. </b>
248    
249    @param  fd      form data
250    @param  errorKey  an arbitrary/optional validation key which allows 
251              to later retrieve and display a particular 
252              form error. Specify <tt>null</tt> for no
253              key.
254    @param  errorMessage the form error message
255    
256    @see Field#addError
257    **/
258    public void addError(FormData fd, String errorKey, String errorMessage) 
259      {
260      fd.addFormError(errorKey, errorMessage);
261      }
262    
263    /**
264    Convenience method that invokes {@link addError(FormData, String, String) 
265    addError} with an empty validation <tt>errorKey</tt>. 
266    */
267    public void addError(FormData fd, String errorMessage) 
268      {
269      addError(fd, null, errorMessage);
270      }
271    
272    /**
273    Adds a {@link Hidden} field to this form. This field exists only for the
274    duration of this form data object and is useful for request specific 
275    data.
276    */
277    public void addDynamicField(FormData fd, Hidden f) {
278      fd.addDynamicField(f);
279      }
280    
281    /** 
282    Adds a form level validator to this form. Note, validator
283    names have to be unique and not clash with other form level
284    validator names.
285    
286    @throws  IllegalArgumentException if a validator with the same name
287                      already exists in this form.
288    **/
289    public void addValidator(FormValidator validator) {
290      String name = validator.getName();
291      if (formValidators.containsKey(name)) {
292        throw new IllegalArgumentException("A validator by that name [" + name + "] already exists in this form");
293        }
294      formValidators.put(name, validator);
295      }
296    
297    /**
298    Adds an arbitrary message for this request. Messages can include arbitrary
299    warnings, usage help etc. 
300    */
301    public void addMessage(FormData fd, String key, String value) 
302      {
303      fd.addMessage(key, value);
304      }
305    
306    /**
307    Returns true if the form contains the specified field.
308    */
309    public boolean containsField(String fieldName) 
310      {
311      return fields.containsKey(fieldName); 
312      }
313    
314    /**
315    Retrieves a previously added message or <tt>null</tt> if the speccified
316    form data was <tt>null</tt> or if the message was not found.
317    */
318    public String getMessage(FormData fd, String key) 
319      {
320      if (fd == null)
321        return null;
322        
323      return fd.getMessage(key);
324      }
325            
326    public void addSubmitHackedHandler(SubmitHackedHandler handler)
327      {
328      Argcheck.notnull(handler, "handler param was null");
329      this.hackHandler = handler;
330      }
331    
332    protected void handleSubmitHacked(HttpServletRequest req, String msg)
333    throws SubmitHackedException
334      {
335      hackHandler.handle(req, msg);
336      }
337            
338    /** 
339    Processes the submitted form. <b>This method must be called after each form
340    submission by the servlet or page code that handles form submits</b>. This
341    method does the following:
342    <ol>
343    <li>populates the fields of the form from the submitted request.
344    <li>if the form has any dependencies and if the dependencies need updating,
345    then those dependencies are updated. Validation is not carried out in this
346    case since the form has changed and the user needs to typically carry out
347    some other action before submitting the form. 
348    <li>if there are no updated dependencies, validates the form via any
349    validators attached to the form and it's fields.
350    </ol>
351    Note: To see if there were any validation errors, invoke the {@link
352    hasError(FormData)} method with the formdata returned by this method.
353    Seeing validation errors is optional of course, and so is using validators
354    in the first place. 
355    
356    @param    req   the HttpServletRequest corresponding to the request
357              being processed by this form.
358    
359    @return   a {@link FormData} object containing populated 
360          field data and the results of the validation.
361    **/ 
362    public FormData handleSubmit(HttpServletRequest req)  
363    throws ServletException, IOException
364      {
365      return handleSubmit(req, null);
366      }
367    
368    /**
369    This method is similar to {@link handleSubmit(HttpServletRequest)} except
370    it stores the specified connection in the form data. This connection can
371    be used by any attached dependency classes for database queries when 
372    updating form depedencies.  
373    <p>
374    Note, we can set arbitrary objects in the {@link HttpServletRequest} via
375    it's {@link HttpServletRequest#setattribute setAttribute} method.
376    However, specifying and storing the connection in the FormData is easier
377    to remember and use.
378    <p>
379    There are 2 cases:
380    <ol>
381    <li>The dependent fields (if any) will internally use {@link WebApp#getConnection}
382    to obtain a connection and no connection has to be set in the form data.</li>
383    The {@link handleSubmit(HttpServletRequest)} can then be used. 
384    <li>The dependent fields (if any) classes will expect a connection in the form data. In
385    that case, this method should be invoked.</li>
386    </ol>
387    If there are no dependent fields, then the {@link handleSubmit(HttpServletRequest)} 
388    is sufficient. 
389    
390    @param    req   the HttpServletRequest corresponding to the request
391              being processed by this form.
392    @param    con   a connection object. The connection is not closed and
393              it is the callers responsiblity to do so.
394    */ 
395    public FormData handleSubmit(HttpServletRequest req, Connection con)  
396    throws ServletException, IOException
397      {
398      FormData fd = populate(req);  //each field sets it's submitted value
399      if (con != null)
400        fd.setConnection(con);
401      
402      updateDependencies(fd, req);  //this can throw an exception.
403      
404      if (! fd.dependencyUpdated) 
405        validate(fd, req);
406      
407      return fd;
408      }
409    
410    
411    public FormData refresh(HttpServletRequest req) 
412    throws ServletException, IOException
413      {
414      return refresh(req, null);
415      }
416    
417    /**
418    Refreshes the form for this request by running all attached refreshers
419    and dependencies. Maintains form state. Does not run any validatators.
420    Useful for re-displaying a form when lookup fields have changed in the
421    database.
422    <p>
423    <u>To refresh the form for all users/requests from this point onwards,
424    create a new instance of the form and replace the existing instance
425    of the form with the newly created instance.</u>
426    */
427    public FormData refresh(HttpServletRequest req, Connection con) 
428    throws ServletException, IOException
429      {
430      FormData fd = populate(req);  //each field sets it's submitted value
431      if (con != null)
432        fd.setConnection(con);
433      
434      updateDependencies(fd, req);  //this can throw an exception.
435      
436      return fd;
437      }
438      
439    /**
440    Populates field's based on the parameters submitted via the specified
441    request and stores it into the specified FormData object. 
442    **/
443    protected FormData populate(HttpServletRequest req) 
444    throws SubmitHackedException
445      {
446      FormData fd = new FormData();
447      Iterator it = fields.entrySet().iterator();
448      while (it.hasNext()) {
449        Map.Entry entry = (Map.Entry) it.next();
450        Field f = (Field) entry.getValue();
451        f.setValueFromSubmit(fd, req);    
452        }
453      return fd;
454      }
455    
456    protected void updateDependencies(FormData fd, HttpServletRequest req)
457    throws DependencyException
458      {
459      for (int i = 0; i < dependencies.size(); i++)
460        {
461        Dependency d = (Dependency) dependencies.get(i);
462        d.updateValues(fd, req);
463        }
464      }
465      
466    /** 
467    Validates each field and also calls each form level validator.
468    <p>
469    Validation is a 2-step process. First, only enabled fields are validated.
470    Some validators may enable or disable fields as part of their validation
471    strategy (for example, if the user chooses a radio button marked 'detail',
472    then fields corresponding to the detail section need to be enabled). (other
473    fields might be disabled). Fields that are re-enabled in this fashion are
474    validated by this method.
475    <p>
476    After running through all the validators, returns <tt>false</tt> is there
477    is a validation error, <tt>true</tt> otherwise. The field and form
478    validation errors can then be retrieved via the {@link
479    FormData#getFieldErrors} and {@link FormData#getFormErrorsCollection},
480    {@link FormData#getFormErrorsMap} methods.
481    **/
482    protected boolean validate(FormData fd, HttpServletRequest req) 
483      {
484      boolean result = true;
485      Map errors = fd.getFieldErrors();
486      Field f = null;
487      Iterator it = null;
488      //List checkAgainList = new ArrayList();
489      
490      //for all form validators
491      it = formValidators.values().iterator();
492      while (it.hasNext()) {
493        FormValidator v = (FormValidator) it.next();
494        if (! v.validate(fd, req)) {
495          result = false; //latch
496          String tmp = v.getErrorMessage();
497          String vname = v.getName();
498          log.bug("Group validate error: ", vname, tmp);
499          fd.addFormError(vname, tmp);      
500          }
501        }
502      
503      //for all field validators, (some could actually be
504      //disabled by the form validators above).
505      it  = fields.values().iterator(); 
506      while (it.hasNext()) 
507        {
508        f = (Field) it.next();
509        
510        if (f.isEnabled(fd)) 
511          { 
512          if (! f.validate(fd, req)) {
513            result = false; //latch
514            }
515          } 
516        } //!while
517      
518      return result;  
519      }
520    
521    /**
522    Returns <tt>true</tt> if validation did not succeed.
523    */
524    public static boolean hasError(FormData fd) 
525      {
526      if (fd == null)
527        return false; //no data so could not validate
528      
529      //FieldErrors = map: fieldname->list of errors
530      //entries in the map (via field.validate) should
531      //only be created if there is a validation error
532      //an empty list of errors should not be created
533      //otherwise the size() below will not work
534      if (fd.getFormErrorsCollection().size() > 0 ||
535        fd.getFieldErrors().size() > 0) 
536        {
537        return true;
538        }
539      
540      return false;
541      }
542    
543    /**
544    Convenience method to render <i>form-level</i> validation errors. This method
545    writes validation error message for the specified form level validator.
546    The error messages are encapulated in a html &lt;div&gt; tag and the class
547    of this tag is set to <tt>form-errmsg</tt>. Each error message within this
548    div is assigned it's own div with class set to <tt>form-errmsg-item</tt>
549    
550    @param  writer      the Writer to render errors to
551    **/
552    public void renderError(FormData fd, Writer writer) 
553    throws IOException 
554      {
555      if (fd == null)
556        return;
557      
558      Collection errors = fd.getFormErrorsCollection();
559    
560      int size = errors.size();
561      if (size == 0)
562        return;
563      
564      writer.write("<div class='form-errmsg'>\n");
565      Iterator it = errors.iterator();
566      while (it.hasNext())
567        {
568        String msg = (String) it.next();
569        if (msg == null)
570          continue;
571        writer.write("<div class='form-errmsg-item'>");
572        writer.write(msg);
573        writer.write("</div>\n");
574        }
575      writer.write("</div>\n");
576      }
577    
578    
579    /**
580    Convenience method to render a validation error for a particluar form level
581    validator. The error messages are encapulated in a html &lt;div&gt; tag and
582    the class of this tag is set to <tt>form-errmsg</tt>.
583    
584    @param  writer        the Writer to render errors to
585    @param  formValidatorName the name of a form level validator
586    **/
587    public void renderErrorFor(
588      FormData fd, Writer writer, String formValidatorName) 
589    throws IOException 
590      {
591      if (fd == null)
592        return;
593      
594      List errors = (List) getFormErrorsFor(fd, formValidatorName);
595      
596      int size = errors.size();
597      if (size == 0)
598        return;
599      
600      writer.write("<div class='form-errmsg'>\n");
601      Iterator it = errors.iterator();
602      while (it.hasNext())
603        {
604        String msg = (String) it.next();
605        if (msg == null)
606          continue;
607        writer.write("<div class='form-errmsg-item'>");
608        writer.write(msg);
609        writer.write("</div>\n");
610        }
611      writer.write("</div>\n");
612      }
613    
614    /** 
615    @param   fd a non-null form data object
616    @return   a map containing field names as keys and the 
617          field's validation error as the value. The data
618          type of each value is that returned by the {@link
619          Field#getValidateErrors} method
620    **/
621    public Map getFieldErrors(FormData fd) {
622      return fd.getFieldErrors();
623      }
624    
625    /** 
626    @param   fd a non-null form data object
627    @return   a list containing form level errors. The data type
628          of each value is that returned by the {@link
629          FormValidator#getErrorMessage()} method
630    **/
631    public Collection getFormErrorsCollection(FormData fd) {
632      return fd.getFormErrorsCollection();
633      }
634    
635    /**
636    Returns a list containing all form-level errors for the specified
637    validator. If there are no errors for the specified validator, returns an
638    empty list.
639    
640    @param   fd         a non-null form data object
641    @param   formValidatorName  a form validator name
642    */
643    public List getFormErrorsFor(FormData fd, String formValidatorName) 
644      {
645      Map map = fd.getFormErrorsMap();
646      List list = (List) map.get(formValidatorName);
647      if (list == null)
648        return empty_list;
649        
650      return list;
651      }
652      
653    /**
654    Renders <b>all</b> dynamically added hidden fields of the form. Dynamic
655    fields are created/ added per user per submit to the {@link FormData}
656    if/as needed.
657    <p>
658    <b>Note</b>: Dynamic fields are <b>not</b> part of a form's fields and
659    are not available via 
660    */
661    public void renderDynamicFields(FormData fd, Writer out) 
662    throws SQLException, IOException
663      {
664      if (fd == null || fd.dynamicFields == null)
665        return;
666    
667      Iterator it = fd.dynamicFields.values().iterator();
668      while (it.hasNext()) {
669        Hidden h = (Hidden) it.next();
670        h.render(out);
671        }
672      }
673    
674    /** Returns the name of this form **/
675    public String getName() { 
676      return name; 
677      }
678    
679    /** 
680    Sets an arbitrary application specific string that can 
681    later be retrieved. Some applications may find 
682    this method useful.
683    **/
684    public void setActionURL(String url) {
685      this.actionURL = url;
686      }
687    
688    /** 
689    Returns the action URL string. Returns <tt>null</tt>
690    if no url has been set.
691    **/ 
692    public String getActionURL() {
693      return actionURL;
694      }
695    
696    
697    /** 
698    Sets an arbitrary application specific string. Intended
699    to store the URL to the jsp or front end page that will
700    render this form for the user.
701    **/
702    public void setDisplayURL(String url) {
703      this.displayURL = url;
704      }
705    
706    /** 
707    Returns the display URL string. Returns <tt>null</tt>
708    if no display url has been set.
709    **/ 
710    public String getDisplayURL() {
711      return displayURL;
712      }
713    
714    
715    /** 
716    Returns a collection view of all the fields contained in this
717    form.
718    **/
719    public Collection getAllFields() { 
720      return fields.values(); 
721      }
722    
723    
724    /*
725    *This is overkill, manually prefix fields as needed
726    *
727    *Automatically appends the specified prefix to the names of all fields
728    *added after this method is called. Suppose there are 2 tables contained in
729    *1 form and both tables contain a field called <tt>id</tt>. Then, since
730    *field names in the form must be unique, one can either add those fields
731    *under different names (say, "table1.id", and "table2.id") or can do the
732    *following:
733    *<blockquote>
734    *form.setFieldNamePrefix("table1");
735    *form.add(id);
736    *form.setFieldNamePrefix("table2");
737    *form.add(id);
738    *</blockquote>
739    *This latter way is useful when many field names clash.
740    *
741    *@param prefix  the prefix string. Specify <tt>null</tt>
742    *       or the empty string to have no prefix.
743    *       (<tt>null</tt> is more efficient)
744    *
745    *public void setFieldNamePrefix(String prefix) {
746    * if ( (prefix == null) || prefix.equals("") ) 
747    *   {
748    *   this.fieldNamePrefix = null;
749    *   return;
750    *   } 
751    * this.fieldNamePrefix = prefix + "_";
752    * }
753    */
754    
755    //get various Field types
756    /**
757    Returns the form field with the specified name.
758    
759    @param    name  the name of the field
760    
761    @return   the field or <tt>null</tt> if no element with that name 
762          was found in this form. 
763    **/
764    public Field get(String name) 
765      {
766      Field result = (Field) fields.get(name);  
767      
768      if (result == null) 
769        log.warn("Tried to obtain a non-existent field from the form. Did you add a field with this name to the form ? Fieldname: [", name, "]");
770        
771      return result;
772      }
773    
774    /**
775    Gets a {@link Hidden} field added to the form data object. Returns
776    <tt>null</tt> if the form data object is null or if the specified
777    field does not exist. <u>Dyanamic fields returned by this method are only 
778    available for the duration of the form data object and <u>can not
779    obtain a field from a submitted form.</u> (use {@link getDynamicField(FormData, HttpServletRequest)}
780    for that purpose.
781    */
782    public Hidden getDynamicField(FormData fd, String fieldName) 
783      {
784      if (fd == null)
785        return null;
786    
787      return fd.getDynamicField(fieldName);
788      }
789    
790    
791    /**
792    Once the form is displayed to the user (with, say, hidden fields that
793    were added dynamically for that request), then when that form is
794    submitted, those dynamic fields can be retrieved via any of: 
795    <ul> 
796    <li> the {@link HttpServletRequest#getParameter} method (often the easiest
797    way)
798    <li> the {@link #getSubmittedField(HttpServletRequest)} method (which will
799    attempt to read the request parameter with the specified name and return
800    is as a Hidden field. (this is slightly more convenient if there is more
801    than 1 hidden field with the same name, since all hidden fields with the
802    same name are encapsulated by one object of type {@link Hidden}).
803    </ul>
804    */
805    public Hidden getSubmittedField(HttpServletRequest req, String name) 
806      {
807      String str = req.getParameter(name);
808    
809      if (str == null)
810        str = "";
811    
812      Hidden hidden = new Hidden(name, str);
813      hidden.dynamicallyAdded = true;
814      return hidden;
815      }
816    
817    /**
818    Returns all the fields that <b>contains</b> the specified name. This is
819    useful when a bunch of fields are created via database lookup tables (and
820    each such field is prefixed/suffixed by say <tt>lookup_name + some
821    number</tt>).
822    
823    @param    name  the name of the field
824    
825    @return   a List containing fields with matching names or or an empty list
826          if no elements containing that name were found in this form.
827    **/
828    public List getContaining(String name) 
829      {
830      final List list = new ArrayList();
831      final Iterator it = fields.values().iterator();
832      while (it.hasNext())
833        {
834        final Field f = (Field) it.next();
835        if ( f.getName().indexOf(name) >= 0 )
836          list.add(f);
837        }
838      return list;
839      }
840    
841    /**
842    Returns the {@link FieldRef} with the specified key. This is
843    used to retrieve an arbitrary object added the via {@link
844    #addObject} method. Returns <tt>null</tt> if no object is
845    found for the specified key.
846    */
847    public FieldRef getFieldRef(String key) 
848      {
849      return (FieldRef) fieldRef.get(key); 
850      }
851    
852    public java.util.Timer getTimer() 
853      {
854      synchronized (this) {
855        if (timer == null) {
856          timer = new java.util.Timer();
857          }
858        }
859        
860      return timer;
861      }
862    
863    /** 
864    Returns the field with the specific name as a {@link Select}.
865    
866    @throws IllegalArgumentException if a field with the specified name does not exist or
867                     if the field exists but is not of type Select
868    **/
869    public Select getSelect(String name)
870      {
871      return (Select) get(name);
872      }
873    
874    /** 
875    Returns the field with the specific name as a {@link RefreshableSelect}.
876    
877    @throws IllegalArgumentException if a field with the specified name does not exist or
878                     if the field exists but is not of type RefreshableSelect
879    **/
880    public RefreshableSelect getUpdatableSelect(String name)
881      {
882      return (RefreshableSelect) get(name);
883      }
884    
885    
886    
887    /** 
888    Returns the field with the specific name as a {@link Checkbox}.
889    
890    @throws IllegalArgumentException if a field with the specified name does not exist or
891                     if the field exists but is not of type Checkbox
892    **/
893    public Checkbox getCheckbox(String name)
894      {
895      return (Checkbox) get(name);
896      }
897    
898    /** 
899    Returns the field with the specific name as a {@link
900    Checkbox}. 
901    
902    @throws IllegalArgumentException if a field with the specified name does not exist or
903                     if the field exists but is not of type Checkbox
904    **/
905    public CheckboxGroup getCheckboxGroup(String name)
906      {
907      return (CheckboxGroup) get(name);
908      }
909    
910    /** 
911    Returns the field with the specific name as a {@link
912    Hidden}.
913    
914    @throws IllegalArgumentException if a field with the specified name does not exist or
915                     if the field exists but is not of type Hidden
916    **/
917    public Hidden getHidden(String name)
918      {
919      return (Hidden) get(name);
920      }
921    
922    /** 
923    Returns the field with the specific name as a {@link Text}.
924    
925    @throws IllegalArgumentException if a field with the specified name does not exist or
926                     if the field exists but is not of type Text
927    **/
928    public Text getText(String name)
929      {
930      return (Text) get(name);
931      }
932    
933    /** 
934    Returns the field with the specific name as a {@link
935    TextArea}.
936    
937    @throws IllegalArgumentException if a field with the specified name does not exist or
938                     if the field exists but is not of type TextArea
939    **/
940    public TextArea getTextArea(String name)
941      {
942      return (TextArea) get(name);
943      }
944    
945    /** 
946    Returns the field with the specific name as a {@link
947    Password}.
948    
949    @throws IllegalArgumentException if a field with the specified name does not exist or
950                     if the field exists but is not of type Password
951    **/
952    public Password getPassword(String name)
953      {
954      return (Password) get(name);
955      }
956    
957    /** 
958    Returns the field with the specific name as a {@link Radio}.
959    
960    @throws IllegalArgumentException if a field with the specified name does not exist or
961                     if the field exists but is not of type Radio
962    **/
963    public Radio getRadio(String name)
964      {
965      return (Radio) get(name);
966      }
967    
968    /** 
969    Returns the field with the specific name as a {@link
970    Checkbox}.
971    
972    @throws IllegalArgumentException if a field with the specified name does not exist or
973                     if the field exists but is not of type Checkbox
974    **/
975    public RadioGroup getRadioGroup(String name)
976      {
977      return (RadioGroup) get(name);
978      }
979      
980    public String toString() 
981      { 
982      final int border_len = 70;
983      final String nl = fc.io.IOUtil.LINE_SEP;
984    
985      final int formval_size = formValidators.size();
986      final int fieldref_size = fieldRef.size();
987      StringBuffer buf = new StringBuffer(256);
988      StringBuffer tmp = null;
989      
990      buf.append(nl);
991      buf.append(StringUtil.repeat('-', border_len));
992      buf .append(nl)
993        .append("Form Name: ")
994        .append(name)
995        .append(" [")
996        .append(fields.size())
997        .append(" Fields]")
998        .append(" [")
999        .append(formval_size)
1000        .append(" FormValidators]")
1001        .append(" [")
1002        .append(fieldref_size)
1003        .append(" FieldRefs]");
1004    
1005         if (timer != null) {
1006          buf.append(" [Attached Timer: ")
1007             .append(timer)
1008             .append(" ]");
1009          }
1010          
1011        buf.append(nl);
1012        
1013        
1014      ByteArrayOutputStream out = null;
1015      TablePrinter.PrintConfig config = null;
1016      TablePrinter p = null;
1017      
1018      
1019      out = new ByteArrayOutputStream(2048);
1020      config = new TablePrinter.PrintConfig();
1021      config.setPrintBorders(false);
1022      config.setCellSpacing(1);
1023      config.setCellPadding(0);
1024      config.setAutoFit(true);
1025      p = new TablePrinter(3, new PrintStream(out), config);
1026      p.startTable();
1027    
1028      p.startRow();
1029      p.printCell("FIELD");
1030      p.printCell("TYPE");
1031      p.printCell("FIELD_VALIDATORS");
1032      p.endRow();
1033    
1034      Iterator it = fields.values().iterator();
1035      while(it.hasNext()) 
1036        {
1037        Field f = (Field) it.next();
1038        p.startRow();
1039        p.printCell(f.getName());
1040        p.printCell(f.getType().toString());
1041        List list = f.getValidators();
1042        int size = list.size();
1043        if (size == 0) {
1044          p.printCell("-");
1045          }
1046        else{
1047          tmp = new StringBuffer(256);
1048          for (int n = 0; n < size; n++) 
1049            {
1050            if (n != 0)
1051              tmp.append(", ");
1052            FieldValidator fv = (FieldValidator) list.get(n);
1053            if (fv instanceof VText) {
1054              tmp.append(fv); //prints a bit more info.
1055              }
1056            else{
1057              String s = fv.getClass().getName();
1058              tmp.append(s.substring((s.lastIndexOf(".") + 1), s.length()));
1059              }
1060            }
1061          p.printCell(tmp.toString());
1062          } 
1063        p.endRow();
1064        }
1065      
1066      p.endTable();
1067      buf.append(out.toString());
1068    
1069      if (formval_size > 0) 
1070        {
1071        buf.append(nl); 
1072        
1073        out.reset();
1074        config = new TablePrinter.PrintConfig();
1075        config.setPrintBorders(false);
1076        config.setCellSpacing(1);
1077        config.setCellPadding(0);
1078        config.setAutoFit(true);
1079        p = new TablePrinter(1, new PrintStream(out), config);
1080        p.startTable();
1081    
1082        p.startRow();
1083        p.printCell("Form-Validators....");
1084        p.endRow();
1085        
1086        it = formValidators.entrySet().iterator();
1087        while (it.hasNext())
1088          {
1089          p.startRow();
1090          Map.Entry me = (Map.Entry) it.next();
1091          String key = (String) me.getKey();
1092          tmp = new StringBuffer(tmp.length() + 2);
1093          tmp.append("\t");
1094          tmp.append(key);
1095          p.printCell(tmp.toString());
1096          p.endRow();
1097          }
1098        
1099        p.endTable();
1100        buf.append(out.toString());
1101        }
1102    
1103      if (fieldref_size > 0) 
1104        {
1105        buf.append(nl); 
1106        
1107        out.reset();
1108        config = new TablePrinter.PrintConfig();
1109        config.setPrintBorders(false);
1110        config.setCellSpacing(1);
1111        config.setCellPadding(0);
1112        config.setAutoFit(true);
1113        p = new TablePrinter(2, new PrintStream(out), config);
1114        p.startTable();
1115    
1116        p.startRow();
1117        p.printCell("Field References....");
1118        p.printCell("");
1119        p.endRow();
1120        
1121        it = fieldRef.values().iterator();
1122        while (it.hasNext())
1123          {
1124          p.startRow();
1125          FieldRef fr = (FieldRef) it.next();
1126          tmp = new StringBuffer(tmp.length() + 2);
1127          tmp.append("\t");
1128          tmp.append(fr.getName());
1129          p.printCell(tmp.toString());
1130          tmp = new StringBuffer(128);
1131          if (fr.getList() != null) {
1132            tmp.append(fr.getList().size());
1133            tmp.append(" fields");
1134            }
1135          else if (fr.getMap() != null) {
1136            tmp.append(fr.getMap().size());
1137            tmp.append(" fields");
1138            }
1139          else {
1140            tmp.append(fr.getObject());
1141            }
1142          p.printCell(tmp.toString());
1143          p.endRow();
1144          }
1145        
1146        p.endTable();
1147        buf.append(out.toString());
1148        }
1149    
1150      buf.append(StringUtil.repeat('-', border_len));
1151      return buf.toString();
1152      }
1153    
1154    }          //~class Form