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    
013    import fc.jdbc.*;
014    import fc.io.*;
015    import fc.util.*;
016    
017    /** 
018    Abstracts a HTML grouped choice element type such as choicebox or radio.
019    <p>
020    This class allows for <i>grouped fields</i> i.e., more than 1 form fields
021    having the <i>same name</i>. The main difference between a choicebox and
022    a radio group is that only 1 item can be selected within a radio group
023    whereas choiceboxes allow multiple selections within the group.
024    <p>
025    There are 3 different kinds of choice groups (and their concrete subclasses).
026    <dl>
027    <dt>ChoiceGroup</dt>
028      <dd>A normal choice group that is created/instantiated and added to the
029      form. This select displays the same options to all users and these
030      options cannot be changed per request/user (since it does not
031      implement the <tt>setValue(FormData fd, ...)</tt> method. This field
032      will always track that the submitted data is a legal value and was
033      not hacked/modified by the client.</dd>
034    <dt>RefreshableChoiceGroup</dt>
035      <dd>This select starts out by displaying the same options to all
036      users. However, it allows options to be thereafter set/displayed per
037      user/request. If per user/request options are shown, then the user
038      can modify/hack/send any option. The field itself won't track this
039      and applicaiton logic is responsible (if applicable) for tracking if
040      the submitted data is valid.</dd>
041    <dt>DependentChoiceGroup</dt>
042      <dd>This select is similar to a RefreshableSelect but uses an
043      external dependency class to set both it's initial values and
044      per user/request subsequent values. The dependency typically
045      looks at other fields in the form to generate this data.
046    </dl>
047    
048    @author hursh jain
049    **/
050    public abstract class ChoiceGroup extends Field
051    {
052    //choices in this group, linked hash map maintains insertion order
053    private Map   options  = new LinkedHashMap();     
054    
055    //we need this to retrieve options by index (0..n) etc
056    //(the options Map being a linked hash map gives us an
057    //iteration in insertion order but not by any index.
058    private List  optionsList = new ArrayList();      
059    
060    //orig selected options
061    private Map   origSelectedMap =  new HashMap();  
062    //if true, options will be rendered in reverse
063    private boolean reverseRender = false;
064    
065    private Map   reverseOptions;  //lazy creation    
066    
067    static class Data {
068      //options submitted by user [Choice.getValue()->Choice]
069      Map   selectedMap = new HashMap();   
070      }
071        
072    /** 
073    Creates a new grouped choice object that intitially contains no choice
074    fields
075    
076    @param  name    the field name
077    **/
078    public ChoiceGroup(String name)
079      {
080      super(name);
081      }
082    
083    /** 
084    Creates a new grouped choice object that intitially contains the specified
085    choices.
086    
087    @param  name    the field name
088    @param  choices   a list of {@link ChoiceGroup.Choice} objects.
089    **/
090    public ChoiceGroup(String name, List choices)
091      {
092      super(name);
093      try {
094        for (int n = 0; n < choices.size(); n++) {
095          ChoiceGroup.Choice choice = (ChoiceGroup.Choice) choices.get(n);
096          add(choice);
097          choice.parent = this;
098          }
099        }
100      catch (ClassCastException e) {
101        log.warn("You can only add choices of type: ChoiceGroup.Choice. This is different than just the Choice type.");
102        throw e;
103        }
104      }
105      
106    public abstract Field.Type getType();
107    
108    public void add(ChoiceGroup.Choice choice) 
109      {
110      String val = choice.getValue();
111      options.put(val, choice);
112      optionsList.add(choice);
113      if (choice.isOrigSelected()) {
114        origSelectedMap.put(val, choice); 
115        } 
116      //so this will be recreated with the new option when
117      //reverseRender is called again
118      reverseOptions = null;
119      choice.parent = this;
120      }
121    
122    /**
123    Adds all the elements of the specified choice group to this choice group.
124    
125    @throws IllegalArgumentException if the specified choicegrop 
126        was null or is the same choicegroup as the source.
127                    
128    */
129    public void addAll(ChoiceGroup cg) 
130      {
131      Argcheck.notnull(cg, "choicegroup parameter was null");
132      Argcheck.istrue(cg != this, "source and target choicegroups are the same");
133      
134      /*  
135      this.options.putAll(cg.options);
136      this.origSelectedMap.putAll(cg.origSelectedMap);
137      */
138      /*
139      we need to make a deep copy since if we just copy the maps, the pointed
140      to options would still be the same and a change to an option in one
141      group ( say writehtml(false) would affect options in other groups that
142      used addAll previously).
143      */
144      Iterator it = cg.options.values().iterator();
145      while (it.hasNext()) {
146        ChoiceGroup.Choice item = (ChoiceGroup.Choice) it.next();
147        item.copyTo(this);
148        }   
149      }
150    
151    /** 
152    Returns a Collection containing the choices selected by the user. Each
153    item in this collection is of type {@link ChoiceGroup.Choice}. If there
154    are no selected options, returns an empty unmodifiable collection.
155    
156    @param  fd  the submited form data
157    **/
158    public Collection getValue(FormData fd) 
159      {
160      ChoiceGroup.Data data = (ChoiceGroup.Data) fd.getData(name);
161      if (data == null)
162        return Form.empty_list;
163      return data.selectedMap.values();
164      }
165    
166    /**
167    Sets the selected values for this choicegroup in the specified form data.
168    <u>The choices in the choicegroup are not set by this method, only which of
169    the choices that will be displayed as selected/not-selected for the request
170    associated with the form data.</u>
171    <p>
172    To set the choices themselves, use the appropriate constructor or call
173    the {@link setValue(Collection)} method. <b>.The specified form data must
174    not be null.</b>
175    
176    @param  fd    the non-null form data object for rendering the form
177    @param  values  a collection of {@link ChoiceGroup.Choice} objects
178    */
179    public void setValue(FormData fd, Collection values)
180      {
181      Argcheck.notnull(fd, "specified fd param was null");
182      Argcheck.notnull(values, "specified values param was null");
183        
184      ChoiceGroup.Data data = new ChoiceGroup.Data();
185      fd.putData(name, data);
186    
187      Iterator it = values.iterator();
188      while (it.hasNext()) {
189        ChoiceGroup.Choice choice = (Choice) it.next();
190        if (choice.isOrigSelected()) {
191          data.selectedMap.put(choice.getValue(), choice);  
192          }
193        }
194      }
195    
196    /**
197    Sets the values for this choice group. Any previously set values are
198    first cleared before new values are set.
199    
200    @param  values  a collection of {@link ChoiceGroup.Choice} objects
201    */
202    public void setValue(Collection values)
203      {
204      Argcheck.notnull(values, "specified values param was null");
205      
206      Iterator it = values.iterator();
207      while (it.hasNext()) {
208        ChoiceGroup.Choice choice = (Choice) it.next();
209        add(choice);
210        }
211      }
212    
213    /**
214    Returns the choice specified by the index n. The choices are indexed in
215    the order that they were <i>added</i> to the form. (regardless of whether
216    this field is being reverse rendered).
217    
218    @throws IndexOutOfBoundsException   if the specified index is out
219                      of range.
220    */
221    public ChoiceGroup.Choice getChoice(int n)
222      {
223      return (ChoiceGroup.Choice) optionsList.get(n); 
224      }
225    
226    /**
227    Returns <tt>true</tt> if some option has been selected by the user,
228    <tt>false</tt> otherwise. (also returns <tt>false</tt> is the specified
229    form data is <tt>null</tt>).
230    */
231    public boolean isFilled(FormData fd) 
232      {
233      if (fd == null)
234        return false;
235        
236      ChoiceGroup.Data data = (ChoiceGroup.Data) fd.getData(name);
237      return (data != null &&
238          data.selectedMap.size() != 0);
239      }
240    
241    public void setValueFromSubmit(FormData fd, HttpServletRequest req) 
242    throws SubmitHackedException  
243    { 
244      String[] values = req.getParameterValues(name);
245      
246      /*
247      can be null if a) not present in the html form or b) present
248      in the form but not selected at all by the user.
249      */
250      if (values == null) {
251        return;
252        }
253      
254      //instantiate only when needed
255      ChoiceGroup.Data data = new ChoiceGroup.Data(); 
256      fd.putData(name, data);
257    
258      for (int n = 0; n < values.length; n++) 
259        {
260        //our options were stored with options' value as the key
261          Choice opt = (Choice) options.get(values[n]);
262      
263      // this can happen if option values were hacked by the client
264          if (opt == null) {
265            StringBuffer sb = new StringBuffer(512);
266          sb.append(".setSubmittedValue(): could not match/retrieve a submitted choice from the optionMap")
267              .append("fieldname=").append(name)
268              .append("; submited value='") 
269              .append(values[n])
270              .append("'; choices=").append(options); 
271          hacklert(req, sb.toString());
272            }       
273        else {
274          data.selectedMap.put(opt.getValue(), opt);
275          }
276        }
277      }   //~setValueFromSubmit
278    
279    
280    /**
281    If called with <tt>true</tt>, renders the choices contained in this group
282    in the reverse order in which they were added (by default, choices are
283    rendered in the order they were added)
284    */
285    public void reverseRender(boolean val)
286      {
287      synchronized (this) 
288        { //for mem vis.
289        reverseRender = val;
290        
291        if (reverseRender && reverseOptions == null) 
292          {
293          reverseOptions = new LinkedHashMap();
294          List list = new ArrayList();
295          Iterator it = options.entrySet().iterator();
296          while (it.hasNext()) {
297            list.add(it.next());
298            }
299          int count = list.size() - 1;
300          for (int n = count; n >= 0 ; n--) {
301            Map.Entry e = (Map.Entry) list.get(n);
302            reverseOptions.put(e.getKey(), e.getValue());
303            }
304          }
305        }
306      }
307    
308    /**
309    Renders all elements of this group one after onether, seperated by a
310    whitespace. For more control over rendering spacing and direction, use the
311    {@link #render(FormData, Writer, String, String)} method. It's also
312    possible to render each item in a group individually by <b>not</b> calling
313    this method but getting a Map/List of elements in this group and rendering
314    them in whichever location desired. (by calling render on each specific
315    choice itself).
316    <p>
317    Specify <tt>null</tt> for the formdata if rendering this element for the
318    first time (before it has been submitted to by the user).
319    */
320    public void renderImpl(FormData fd, Writer writer) throws IOException
321      {
322      renderImpl(fd, writer, null, "&nbsp;");
323      }
324    
325    
326    /**
327    Renders the elements of this form by prefixing and suffixing each element
328    with the specified arguments.
329    
330    @param  writer  the output destination
331    @param  prefix  prefix value for each element. specify
332            <tt>null</tt> or an empty string if not
333            needed.
334    @param  suffix  suffix value for each element. specify
335            <tt>null</tt> or an empty string if not
336            needed.
337    */
338    public void renderImpl(FormData fd, Writer writer, String prefix, String suffix) 
339    throws IOException
340      {
341      ChoiceGroup.Data data = null;
342    
343      if (fd != null) {
344        data = (ChoiceGroup.Data) fd.getData(name);
345        }
346    
347      Iterator it = null;
348      if (reverseRender)
349        it = reverseOptions.values().iterator();  
350      else  
351        it = options.values().iterator(); 
352      
353      while (it.hasNext()) 
354        {
355        Choice item = (Choice) it.next();
356        String itemval = item.getValue();
357    
358        boolean selected = false;
359        
360        if (fd != null)   /* maintain submit state */
361          {         
362          if (data != null) {  
363            selected = data.selectedMap.containsKey(itemval);
364            }
365          else {  //form submitted but option not selected
366            selected = false;
367            }
368          }
369        else {    
370          /* show original state */
371            selected = origSelectedMap.containsKey(itemval);
372            }
373      
374        boolean disabled = ! isEnabled(fd);
375      
376        if (prefix != null)
377          writer.write(prefix);
378        item.render(writer, selected, disabled);
379        if (suffix != null)
380          writer.write(suffix);
381        } 
382      }
383    
384    /**
385    Clears all values in this group.
386    */
387    public void reset()
388      {
389      options.clear();
390      optionsList.clear();      
391      origSelectedMap.clear();  
392      reverseRender = false;
393      if (reverseOptions != null)
394        reverseOptions.clear(); 
395      }
396    
397    /**
398    Returns a map containing {@link Choice} all elements contained within this
399    group. This is useful if the elements need to be rendered individually for
400    custom positioning on an html page. 
401    <p> 
402    Each elements in the map will reflect the original selection state when it
403    was created. Each element can optionally be checked to see if it was
404    selected by the user by calling the {@link #isSelected} method.
405    */
406    public Map getAllElements() 
407      {
408      return options;
409      }
410    
411    /**
412    Returns <tt>true</tt> is the specified choice was selected by the user
413    (the user's submission is provided by the formdata argument).
414    
415    @param  fd    form data submitted by user (can be null)
416    @param  choice  a choice option belonging to this group
417    @param  default_val 
418            the default value to return in case the form
419            data was null or did not contain this
420            choicegroup. This will typically be
421            <tt>false</tt> or
422            <tt>choice.isOrigSelected()</tt>
423    */
424    public boolean isSelected(
425      FormData fd, ChoiceGroup.Choice choice, boolean default_val)
426      {
427      if (fd == null)
428        return default_val;
429        
430      Argcheck.notnull(choice, "choice param was null");
431      String itemval = choice.getValue();
432      
433      ChoiceGroup.Data data = 
434          (ChoiceGroup.Data) fd.getData(name);
435    
436      if (data == null)
437        return default_val;
438        
439      boolean selected = data.selectedMap.containsKey(itemval);
440      return selected;
441      }
442      
443    /**
444    Utility methods that calls the {@link Choice#writeLabel} method
445    for each choice contained in this group
446    */
447    public void writeLabel(boolean val)  
448      {
449      Iterator it = options.values().iterator();
450      while (it.hasNext()) {
451        ChoiceGroup.Choice item = (ChoiceGroup.Choice) it.next();
452        item.writeLabel(val);
453        }
454      }
455    
456    public String toString() 
457      { 
458      String linesep = IOUtil.LINE_SEP;
459      StringBuffer buf = new StringBuffer(super.toString());
460      buf.append("; Orig. values: ");
461      buf.append(linesep);    
462    
463      Iterator it = null;
464      if (reverseRender)
465        it = reverseOptions.values().iterator();  
466      else  
467        it = options.values().iterator(); 
468      
469      while(it.hasNext()) {
470        buf.append("\t");
471        buf.append(it.next());
472        buf.append(linesep);    
473        }
474      return buf.toString();  
475      } 
476    
477    /* 
478    No longer this way:
479    
480    Creates a new choice in this choice group. This inner class is <b>not</b>
481    a static class but is a <b>member</b> class. Therefore to create new
482    instances of this class, the following syntax is used:
483    <blockquote>
484    <pre>
485      mychoicegroup.new Choice(...);
486    </pre>
487    where <tt>mychoicegroup</tt> is some reference to a <tt>ChoiceGroup</tt>
488    object.
489    </blockquote>
490    Creating a new choice instance automatically adds it to the choice group
491    object.
492    */
493    
494    //WE DONT USE A NON-STATIC INNER CLASS ANYMORE (WHICH WE SHOULD BECAUSE
495    //BECAUSE EACH CHOICE NEEDS A REFERENCE TO IT'S PARENT) -- BECAUSE IT
496    //MAKES ADDING NEW CHOICES HARDER.
497    
498    /**
499    Creates a new choice for this choice group. 
500    
501    Also note that unlike other fields, the HTML label/text for this choice
502    has to be provided via the constructor (as opposed to being written at the
503    jsp level). See {@link #htmlBeforeField}.
504    **/
505    public static class Choice
506      {
507      private ChoiceGroup parent; //set by choicegroup when added to it
508      private String    value;
509      private String    label;
510      private String    labelsep = " ";
511      private boolean   orig_selected;
512      private boolean   writeLabel = true;
513      private boolean   writeLabelAfter = true;
514      private boolean   writeLabelBefore = false;
515    
516      /** 
517      Creates a new choice object.
518    
519      @param  value   the value of this choice item
520      @param  label   the label (any html text) for this choice. 
521      @param  selected  <tt>true</tt> is this choice is
522                originally selected
523      **/
524      public Choice(String label, String value, boolean selected)
525        {
526        this.label = label;
527        this.value = value;
528        this.orig_selected = selected;
529        //add(this);
530        }
531    
532      /** 
533      Constructs a new unselected choice with the specified value 
534      and HTML text. 
535    
536      @param  value   the value of this choice item
537      @param  label the label (any html text) for this choice
538      **/
539      public Choice(String label, String value) {
540        this(label, value, false);
541        }
542    
543      /** 
544      Constructs a new unselected choice with the specified label (and no
545      separate value attribute)
546    
547      @param  label the label (any html) text for this choice
548      @param  selected  <tt>true</tt> is this choice is
549                originally selected
550      **/
551      public Choice(String label, boolean selected) {
552        this(label, null, selected);
553        }
554    
555      //makes a new copy and adds it to the specified 
556      //radioGroup
557      private void copyTo(ChoiceGroup target) {
558        Choice c = new Choice(value, label, orig_selected);
559        c.writeLabel = writeLabel;
560        c.writeLabelAfter = writeLabelAfter;
561        c.writeLabelBefore = writeLabelBefore;
562        target.add(c);
563        }
564      
565      /**  
566      By default, the HTML label (if any) is written after the input element
567      tag but calling this method reverses this order.
568      **/
569      public void labelBeforeField() {
570        writeLabelAfter = false;
571        writeLabelBefore = true;
572        }
573    
574      /**
575      Specify <tt>true</tt> to write the label for this choice,
576      <tt>false</tt> to skip the label. By default, this is <tt>true</tt>.
577      (false is useful when just the radio button need to be shown, say in a
578      table row with the label shown in a seperate header row).
579      */
580      public void writeLabel(boolean val) {
581        this.writeLabel = val;
582        }
583    
584      /**
585      Renders this choice maintaining it's selected state by using the
586      specified form data.
587      <p> Each choice can be rendered separately which helps in arbitrary
588      html layout. Choices can also be rendered together via the parent
589      {@link ChoiceGroup#render(FormData, Writer} method. 
590      */
591      public void render(FormData fd, Writer writer, boolean disabled)
592      throws IOException  
593        {
594        ChoiceGroup.Data data = null;
595    
596        if (fd != null) {
597          data = (ChoiceGroup.Data) fd.getData(parent.name);
598          }
599    
600        boolean selected = false;
601        
602        if (fd != null)   /* maintain submit state */
603          {         
604          if (data != null) {  
605            selected = data.selectedMap.containsKey(getValue());
606            }
607          else {  //form submitted but option not selected
608            selected = false;
609            }
610          }
611        else {    
612          /* show original state */
613            selected = isOrigSelected();
614            }
615      
616        render(writer, selected, disabled);   
617        }
618        
619      /**
620      Renders this choice with the select state specified by the
621      <tt>selected</tt> parameter.
622      <p> Each choice can be rendered separately which helps in arbitrary
623      html layout. Choices can also be rendered together via the parent
624      {@link ChoiceGroup#render(FormData, Writer} method.
625      */  
626      public void render(Writer writer, boolean selected, boolean disabled) 
627      throws IOException
628        {
629        if (writeLabel && writeLabelBefore) {
630          writer.write(label);
631          writer.write(labelsep);
632          }
633      
634        writer.write("<input type='");
635        writer.write(parent.getType().toString()); //type of choice (outer class)
636        writer.write("' name='");
637        writer.write(parent.name);
638        writer.write("'");
639        
640        if (value != null) {  //value tag present
641          writer.write(" value='");
642          writer.write(value);
643          writer.write("'"); 
644          }
645      
646        if (selected) {
647          writer.write(" checked");
648          }
649        
650        if (parent.renderStyleTag) {
651          writer.write(" style='");
652          writer.write(parent.styleTag);
653          writer.write("'");
654          }
655    
656        final int arlen = parent.arbitraryString.size();
657        for (int n = 0; n < arlen; n++) {
658          writer.write(" ");
659          writer.write(parent.arbitraryString.get(n).toString());
660          }
661          
662        writer.write(">");
663          
664        if (writeLabel && writeLabelAfter) {
665          writer.write(labelsep);
666          writer.write(label);
667          }
668        writer.write("</input>");
669        }
670    
671      /** 
672      Returns the value of this choice. If no value is set, returns the html
673      text value for this choice tag.
674      **/
675      public String getValue() 
676        {
677        if (value != null)
678          return value;
679        else
680          return label;
681        } 
682      
683      /**
684      Convenience method that returns the value of this 
685      choice as a Integer. 
686      
687      @throws NumberFormatException if the value could not be
688                      returned as an integer. 
689      */
690      public int getIntValue() {
691        return Integer.parseInt(getValue());
692        }
693      
694      /**
695      Convenience method that returns the value of this choice as a Short.
696      
697      @throws NumberFormatException if the value could not be
698                      returned as a short.  
699      */
700      public short getShortValue(FormData fd) {
701        return Short.parseShort(getValue());
702        }
703      
704      /**
705      Convenience method that returns the value of this choice as a boolean.
706      The value is converted into a boolean as per the {@link
707      Boolean.valueOf(String)} method.
708      */
709      public boolean getBooleanValue(FormData fd) {
710        return Boolean.valueOf(getValue()).booleanValue();
711        }
712        
713      /**
714      Returns the label for this choice. 
715      */
716      public String getLabel()
717        {
718        return label;
719        }
720      
721      /**
722      Sets the seperator between labels and the choice. Defaults to a space
723      if not set.
724      */
725      public void setLabelSeperator(String sep)
726        {
727        labelsep = sep;
728        }
729      
730      /** 
731      @return <tt>true</tt> if this field was originally set to 
732      selected, <tt>false</tt> otherwise
733      **/
734      public boolean isOrigSelected() {
735        return orig_selected;
736        }
737    
738      public String toString() 
739        {
740        return "ChoiceGroup.Choice: [value=" + value + 
741            "; label=" + label + 
742        /*
743        note, we don't want to call form.toString() because that would
744        result in a recursive loop if form.toString() is ever changed to
745        call toString() of all it's constituent fields
746        */  
747            "]"; 
748        //ok but don't need this: form= + form.getName();
749        }
750      
751      }   //~innerclass Choice        
752    
753    }