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, " ");
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 }