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.*;
012
013import fc.jdbc.*;
014import fc.io.*;
015import fc.util.*;
016
017/** 
018Abstracts a HTML grouped choice element type such as choicebox or radio.
019<p>
020This class allows for <i>grouped fields</i> i.e., more than 1 form fields
021having the <i>same name</i>. The main difference between a choicebox and
022a radio group is that only 1 item can be selected within a radio group
023whereas choiceboxes allow multiple selections within the group.
024<p>
025There 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**/
050public abstract class ChoiceGroup extends Field
051{
052//choices in this group, linked hash map maintains insertion order
053private 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.
058private List  optionsList = new ArrayList();      
059
060//orig selected options
061private Map   origSelectedMap =  new HashMap();  
062//if true, options will be rendered in reverse
063private boolean reverseRender = false;
064
065private Map   reverseOptions;  //lazy creation    
066
067static class Data {
068  //options submitted by user [Choice.getValue()->Choice]
069  Map   selectedMap = new HashMap();   
070  }
071    
072/** 
073Creates a new grouped choice object that intitially contains no choice
074fields
075
076@param  name    the field name
077**/
078public ChoiceGroup(String name)
079  {
080  super(name);
081  }
082
083/** 
084Creates a new grouped choice object that intitially contains the specified
085choices.
086
087@param  name    the field name
088@param  choices   a list of {@link ChoiceGroup.Choice} objects.
089**/
090public 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  
106public abstract Field.Type getType();
107
108public 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/**
123Adds 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*/
129public 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/** 
152Returns a Collection containing the choices selected by the user. Each
153item in this collection is of type {@link ChoiceGroup.Choice}. If there
154are no selected options, returns an empty unmodifiable collection.
155
156@param  fd  the submited form data
157**/
158public 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/**
167Sets 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
169the choices that will be displayed as selected/not-selected for the request
170associated with the form data.</u>
171<p>
172To set the choices themselves, use the appropriate constructor or call
173the {@link setValue(Collection)} method. <b>.The specified form data must
174not 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*/
179public 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/**
197Sets the values for this choice group. Any previously set values are
198first cleared before new values are set.
199
200@param  values  a collection of {@link ChoiceGroup.Choice} objects
201*/
202public 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/**
214Returns the choice specified by the index n. The choices are indexed in
215the order that they were <i>added</i> to the form. (regardless of whether
216this field is being reverse rendered).
217
218@throws IndexOutOfBoundsException   if the specified index is out
219                  of range.
220*/
221public ChoiceGroup.Choice getChoice(int n)
222  {
223  return (ChoiceGroup.Choice) optionsList.get(n); 
224  }
225
226/**
227Returns <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
229form data is <tt>null</tt>).
230*/
231public 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
241public void setValueFromSubmit(FormData fd, HttpServletRequest req) 
242throws 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/**
281If called with <tt>true</tt>, renders the choices contained in this group
282in the reverse order in which they were added (by default, choices are
283rendered in the order they were added)
284*/
285public 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/**
309Renders all elements of this group one after onether, seperated by a
310whitespace. For more control over rendering spacing and direction, use the
311{@link #render(FormData, Writer, String, String)} method. It's also
312possible to render each item in a group individually by <b>not</b> calling
313this method but getting a Map/List of elements in this group and rendering
314them in whichever location desired. (by calling render on each specific
315choice itself).
316<p>
317Specify <tt>null</tt> for the formdata if rendering this element for the
318first time (before it has been submitted to by the user).
319*/
320public void renderImpl(FormData fd, Writer writer) throws IOException
321  {
322  renderImpl(fd, writer, null, "&nbsp;");
323  }
324
325
326/**
327Renders the elements of this form by prefixing and suffixing each element
328with 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*/
338public void renderImpl(FormData fd, Writer writer, String prefix, String suffix) 
339throws 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/**
385Clears all values in this group.
386*/
387public 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/**
398Returns a map containing {@link Choice} all elements contained within this
399group. This is useful if the elements need to be rendered individually for
400custom positioning on an html page. 
401<p> 
402Each elements in the map will reflect the original selection state when it
403was created. Each element can optionally be checked to see if it was
404selected by the user by calling the {@link #isSelected} method.
405*/
406public Map getAllElements() 
407  {
408  return options;
409  }
410
411/**
412Returns <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*/
424public 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/**
444Utility methods that calls the {@link Choice#writeLabel} method
445for each choice contained in this group
446*/
447public 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
456public 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/* 
478No longer this way:
479
480Creates a new choice in this choice group. This inner class is <b>not</b>
481a static class but is a <b>member</b> class. Therefore to create new
482instances of this class, the following syntax is used:
483<blockquote>
484<pre>
485  mychoicegroup.new Choice(...);
486</pre>
487where <tt>mychoicegroup</tt> is some reference to a <tt>ChoiceGroup</tt>
488object.
489</blockquote>
490Creating a new choice instance automatically adds it to the choice group
491object.
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/**
499Creates a new choice for this choice group. 
500
501Also note that unlike other fields, the HTML label/text for this choice
502has to be provided via the constructor (as opposed to being written at the
503jsp level). See {@link #htmlBeforeField}.
504**/
505public 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}