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