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 java.io.*; 009 import java.util.*; 010 import java.sql.*; 011 import javax.servlet.*; 012 import javax.servlet.http.*; 013 014 import fc.jdbc.*; 015 import fc.io.*; 016 import fc.util.*; 017 018 /** 019 An HTML Select field 020 <p> 021 There are 3 different kinds of selects: 022 <dl> 023 <dt>Select</dt> 024 <dd>A normal select that is created/instantiated and added to the 025 form. This select displays the same options to all users and these 026 options cannot be changed per request/user (since it does not 027 implement the <tt>setValue(FormData fd, ...)</tt> method. This field 028 will always track that the submitted data is a legal value and was 029 not hacked/modified by the client.</dd> 030 <dt>RefreshableSelect</dt> 031 <dd>This select starts out by displaying the same options to all 032 users. However, it allows options to be thereafter set/displayed per 033 user/request. If per user/request options are shown, then the browser 034 can modify/hack/send any option. This field (in contrast to most 035 other fields in the form) itself won't track this and application 036 logic is responsible (if applicable) for tracking if the submitted 037 data is valid.</dd>. 038 <dt>DependentSelect</dt> 039 <dd>This select is similar to a RefreshableSelect but uses an 040 external dependency class to set both it's initial values and per 041 user/request subsequent values. The dependency typically looks at 042 other fields in the form to generate this data. 043 </dl> 044 045 @author hursh jain 046 **/ 047 public class Select extends Field 048 { 049 //option[text='foo',value='1'], getValue()==1 050 //option[text='foo'], getValue()==foo 051 052 //all options shown to user 053 LinkedHashMap options = new LinkedHashMap(); 054 //orig selected options when form was constructed 055 Map origSelectedMap = new HashMap(); 056 int size = 0; 057 boolean multiple; 058 059 static final class Data 060 { 061 //To store selected options submitted by the user 062 //acts like a set, used via containsKey() to see if an options 063 //is selected (so we can reselect it) during rendering. 064 private Map selectedMap = new HashMap(); 065 066 //to store all options submitted by user 067 private List selectedList = new ArrayList(); 068 } 069 070 /** 071 Creates a new select element with no initial values. Multiple 072 selections are not initially allowed. 073 074 @param name the field name 075 **/ 076 public Select(String name) 077 { 078 super(name); 079 } 080 081 /** 082 Creates a new select element with the specified initial values 083 and no multiple selections allowed. There is no explicit default 084 selection which typically means the browser will show the first 085 item in the list as the selection item. 086 087 @param name the field name 088 @param values the initial option values for this class. The 089 objects contained in the list must be of type 090 {@link Option} otherwise a 091 <tt>ClassCastException</tt> may be thrown in 092 future operations on this object. 093 094 @throws IllegalArgumentException if the values parameter was null 095 (note: an empty but non-null List is ok) 096 **/ 097 public Select(String name, List values) 098 { 099 super(name); 100 Argcheck.notnull(values, "values param was null"); 101 initOptions(values); 102 } 103 104 void initOptions(final List values) 105 { 106 for (int n = 0; n < values.size(); n++) { 107 final Option item = (Option) values.get(n); 108 String itemval = item.getValue(); 109 this.options.put(itemval, item); 110 if (item.isOrigSelected()) { 111 this.origSelectedMap.put(itemval, item); 112 } 113 } 114 } 115 116 /** 117 The specified query is used to populate this select. The default parameter 118 (if not null) is the default selected option. If the default option is 119 <tt>null</tt>, no option is selected for multiple-value selects and the 120 first option returned by the query is selected for single-value selects. 121 <p> 122 Typically, this method may be invoked like: 123 <blockquote> 124 <tt> 125 useQuery(con, "select id, name from my_lookup_table", new Option("--choose--")); 126 </tt> 127 </blockquote> 128 If the lookup table has more than 2 columns, the query can look like (in 129 standard SQL syntax): 130 <blockquote> 131 <tt> 132 useQuery(con, "select id, <font color=blue>name1 || ' ' || name2</font> from my_lookup_table", new Option("--choose--")); 133 </tt> 134 </blockquote> 135 This shows columns name1 and name2 concatenated with each other. 136 137 @param con the connection to use for the query. This connection is 138 <b>not</b> closed by this method. <u>Close this connection 139 from the calling code to prevent connection leaks.</u> 140 141 @return this select for method chaining convenience 142 */ 143 public Select useQuery(Connection con, String query, Select.Option defaultOpt) 144 throws SQLException 145 { 146 reset(); 147 148 if (defaultOpt != null) 149 add(defaultOpt); 150 151 final List list = makeOptionsFromQuery(con, query); 152 initOptions(list); 153 154 return this; 155 } 156 157 /** 158 Convenience method that calls {@link useQuery(Connection, String, String) 159 useQuery(con, query, null)}. 160 */ 161 public Select useQuery(Connection con, String query) throws SQLException 162 { 163 useQuery(con, query, null); 164 return this; 165 } 166 167 /** 168 Uses the first column as the option text and if there is a second column 169 uses it as the corresponding option value. 170 */ 171 public static List makeOptionsFromQuery(Connection con, String query) 172 throws SQLException 173 { 174 return makeOptionsFromQuery(con, query, null); 175 } 176 177 /** 178 Uses the first column as the option text and if there is a second column 179 uses it as the corresponding option value. Uses an additional default 180 option. 181 */ 182 public static List makeOptionsFromQuery( 183 Connection con, String query, Select.Option defaultOption) 184 throws SQLException 185 { 186 final List list = new ArrayList(); 187 if (defaultOption != null) 188 list.add(defaultOption); 189 190 final Statement stmt = con.createStatement(); 191 final ResultSet rs = stmt.executeQuery(query); 192 final ResultSetMetaData rsmd = rs.getMetaData(); 193 final int numberOfColumns = rsmd.getColumnCount(); 194 if (numberOfColumns == 0) { 195 log.warn("Query [",query, "] returned no columns"); 196 } 197 while (rs.next()) { 198 String text = rs.getString(1); 199 String value = (numberOfColumns == 1) ? text : rs.getString(2); 200 Select.Option opt = new Select.Option(text, value); 201 list.add(opt); 202 } 203 stmt.close(); 204 return list; 205 } 206 207 public Field.Type getType() { 208 return Field.Type.SELECT; 209 } 210 211 /** 212 Sets the values for this select. Any previously set values are 213 first cleared before new values are set. 214 <p> 215 Note, to show user specific options via FormData, use a {@link RefreshableSelect} 216 instead. 217 218 @param values a list of {@link Select.Option} objects 219 */ 220 public void setValue(List values) 221 { 222 Argcheck.notnull(values, "specified values param was null"); 223 options.clear(); 224 origSelectedMap.clear(); 225 initOptions(values); 226 } 227 228 229 /** 230 Returns a List containing the selected options. Each object in 231 the collection will be of type {@link Select.Option}. If there 232 are no selected options, the returned list will be an 233 empty list. 234 235 @param fd the submited form data. This object should not be null 236 otherwise a NPE will be thrown. 237 **/ 238 public List getValue(FormData fd) 239 { 240 Select.Data data = (Select.Data) fd.getData(name); 241 if (data == null) { 242 return Form.empty_list; 243 } 244 return data.selectedList; 245 } 246 247 /** 248 Convenience method that returns the selected option as a 249 String. <u>No guarantees are made as to which selection is 250 returned when more than one selection is selected (this 251 method is really meant for when the select only allows 252 single selections as a dropdown).</u> 253 <p> 254 The returned value is of type String obtained by called the 255 selected Option's {@link Select.Option#getValue} method. 256 <p> 257 If there is no selection, returns <tt>null</tt> 258 **/ 259 public String getStringValue(FormData fd) 260 { 261 String result = null; 262 263 Select.Data data = (Select.Data) fd.getData(name); 264 if (data == null) { 265 return null; 266 } 267 268 List list = data.selectedList; 269 if (list.size() == 0) { 270 return result; 271 } 272 else { 273 Option opt = (Option) list.get(0); 274 return opt.getValue(); 275 } 276 } 277 278 279 /** 280 Convenience method that returns the single value of this field 281 as a Integer. 282 <p> 283 All the caveats of {@link #getSingleValueAsString()} apply. 284 285 @throws NumberFormatException if the value could not be 286 returned as in integer. 287 */ 288 public int getIntValue(FormData fd) { 289 return Integer.parseInt(getStringValue(fd)); 290 } 291 292 /** 293 Convenience method that returns the single value of this field 294 as a boolean. 295 <p> 296 All the caveats of {@link #getSingleValueAsString()} apply. 297 In particular, the formdata should contain non-null data 298 with at least one selection. 299 */ 300 public boolean getBooleanValue(FormData fd) { 301 return Boolean.valueOf(getStringValue(fd)).booleanValue(); 302 } 303 304 /** 305 Returns <tt>true</tt> if some option has been selected 306 by the user, <tt>false</tt> otherwise. (also returns 307 <tt>false</tt> is the specified form data is <tt>null</tt>). 308 */ 309 public boolean isFilled(FormData fd) 310 { 311 if (fd == null) 312 return false; 313 314 Select.Data data = (Select.Data) fd.getData(name); 315 if (data == null) { 316 return false; 317 } 318 List list = data.selectedList; 319 //if data is present, then list always will be non-null 320 return (/*list != null &&*/ list.size() != 0); 321 } 322 323 /* 324 Select values can be present as: 325 selectname=value 326 327 or for multiple selections (for say a select called "sel1") 328 sel1=value1&sel1=val2 329 330 The values sent are those in the value tag in the option 331 and if missing those of the corresponding html for the option. 332 333 If a select allows multiple and none are selected, then 334 nothing at all is sent (note this is different than for 335 single (and not multiple) selects, where some value will always 336 be sent (since something is always selected). 337 338 We can track submitted/selected options in 2 ways: 339 1. go thru the option list and set a select/unselect on each option 340 element and then the option element renders itself appropriately. 341 2. keep a separate list of selected elements and at render time display 342 the item as selected only if it's also in the select list. 343 344 (w) implies that we have to specify select/no select at 345 render time to each option as opposed to setting that flag 346 in the option before telling the option to render itself. 347 (2) is chosen here. 348 349 the client can send hacked 2 or more options with the same 350 value this method will simply add any such duplicate values 351 at the end of the list. this is ok. 352 */ 353 public void setValueFromSubmit(FormData fd, HttpServletRequest req) 354 throws SubmitHackedException 355 { 356 //value(s) associated with the selection field name 357 String[] values = req.getParameterValues(name); 358 359 //todo: if ! multiple && enabled && values == null, hacklert 360 if (values == null) { 361 return; 362 } 363 364 //lazy instantiation 365 Select.Data data = new Select.Data(); 366 fd.putData(name, data); 367 368 if (multiple && values.length > 1) 369 hacklert(req, "recieved multiple values for a single value select"); 370 371 if (multiple) 372 { 373 for (int n = 0; n < values.length; n++) { 374 addSelectedOpt(req, data, values[n]); 375 } 376 } 377 else{ 378 addSelectedOpt(req, data, values[0]); 379 } 380 } 381 382 private void addSelectedOpt( 383 HttpServletRequest req, Select.Data data, String submitValue) 384 throws SubmitHackedException 385 { 386 // our options were stored with option's value as the key 387 Option opt = (Option) options.get(submitValue); 388 389 // this can happen if option values were hacked by the client 390 if (opt == null) 391 { 392 hacklert(req, 393 "could not match/retrieve a submitted option from the options." + 394 "; Submited value=[" + submitValue +"]; options=" + options); 395 } 396 else{ 397 data.selectedMap.put(opt.getValue(), opt); 398 data.selectedList.add(opt); 399 } 400 } 401 402 public void renderImpl(FormData fd, Writer writer) throws IOException 403 { 404 Select.Data data = null; 405 406 if (fd != null) { //we have submit or initial data in the FD 407 //can be null, no submit/initial data for this particular field 408 data = (Select.Data) fd.getData(name); 409 } 410 411 //data can be null here. 412 413 writer.write("<select"); 414 writer.write(" name='"); 415 writer.write(name); 416 writer.write("'"); 417 418 if (size > 0) { 419 writer.write(" size='"); 420 writer.write(String.valueOf(size)); 421 writer.write("'"); 422 } 423 424 if (! enabled || ! isEnabled(fd)) { 425 writer.write(" disabled"); 426 } 427 428 if (multiple) { 429 writer.write(" multiple"); 430 } 431 432 if (renderStyleTag) { 433 writer.write(" style='"); 434 writer.write(styleTag); 435 writer.write("'"); 436 } 437 438 final int arlen = arbitraryString.size(); 439 for (int n = 0; n < arlen; n++) { 440 writer.write(" "); 441 writer.write(arbitraryString.get(n).toString()); 442 } 443 444 writer.write(">\n"); 445 446 Iterator it = options.values().iterator(); 447 while (it.hasNext()) 448 { 449 Option item = (Option) it.next(); 450 String itemval = item.getValue(); 451 452 boolean selected = false; 453 454 //NOTE: DO NOT COMBINE and say: 455 //if (fd != null && data != null) 456 // that's _the_ classic bug, because if: 457 // fd != null but data == null, we will drop down 458 // to (3) but we need to go to (2). 459 if (fd != null) /* maintain submit state */ 460 { 461 /*1*/ if (data != null) { 462 selected = data.selectedMap.containsKey(itemval); 463 } 464 else { 465 /*2*/ //form submitted but option not selected 466 //selected should be false here [which it is by default] 467 } 468 } 469 else{ /* form not submitted, show original state */ 470 /*3*/ selected = origSelectedMap.containsKey(itemval); 471 } 472 473 writer.write(item.render(selected)); 474 writer.write("\n"); //sufficient for easy view source in browser 475 } 476 477 writer.write("</select>"); 478 } 479 480 /** 481 Adds a new option to the selection list. Replaces any previous 482 option that was added previously with the same value. 483 484 @param opt the option to be added 485 **/ 486 public void add(Option opt) 487 { 488 Argcheck.notnull(opt, "opt param was null"); 489 String optval = opt.getValue(); 490 options.put(optval, opt); 491 if (opt.isOrigSelected()) { 492 origSelectedMap.put(optval, opt); 493 } 494 } 495 496 /** 497 Adds a new option to the selection list. Replaces any previous 498 option that was added previously with the same value. This method 499 will have the same effect as if the {@link #add(Option) 500 add(new Select.Option(item))} method was invoked. 501 502 @param item the option to be added 503 **/ 504 public void add(String item) 505 { 506 Argcheck.notnull(item, "item param was null"); 507 Select.Option opt = new Select.Option(item); 508 add(opt); 509 } 510 511 /** 512 Clears all data in this select. 513 */ 514 public void reset() 515 { 516 options.clear(); 517 origSelectedMap.clear(); 518 } 519 520 /** 521 This value (if set) is rendered as the html <tt>SIZE</tt> tag. 522 If the list contains more options than specified by size, the browser 523 will display the selection list with scrollbars. 524 525 @return this object for method chaining convenience 526 **/ 527 public Select setSize(int size) { 528 this.size = size; 529 return this; 530 } 531 532 /** 533 <tt>true</tt> is multiple selections are allowed, <tt>false</tt> otherwise. 534 This value (if set) is rendered as the html <tt>MULTIPLE</tt> tag. 535 536 @return this object for method chaining convenience 537 **/ 538 public Select allowMultiple(boolean allow) { 539 this.multiple = allow; 540 return this; 541 } 542 543 /** Represents an option in the selection list **/ 544 public static final class Option 545 { 546 private String value; 547 private String text; 548 private boolean orig_selected; 549 550 /** 551 Constructs a new option with the specified text and 552 value of the option tag. 553 554 @param text the html text of the option tag 555 @param value the value of the option tag 556 @param selected <tt>true</tt> if this option is selected 557 by default. If more than one selected option 558 is added to a select field and that select 559 field does <b>not</b> have it's multiple attribute 560 set, then the option displayed as selected is 561 browser dependent (Moz1, IE6 show 562 the last selected, N4 the first). More than one 563 selected option should not be shown for non multiple 564 select fields anyway. 565 **/ 566 public Option(String text, String value, boolean selected) 567 { 568 this.text = text; 569 this.value = value; 570 this.orig_selected = selected; 571 } 572 573 /** 574 Constructs a new unselected option with the specified 575 text and value of the option tag. 576 577 @param text the html text of the option tag 578 @param value the value of the option tag 579 **/ 580 public Option(String text, String value) { 581 this(text, value, false); 582 } 583 584 /** 585 Constructs a new option with the specified text (and no 586 separate value tag). 587 588 @param text the html text of the option tag 589 @param selected <tt>true</tt> to select this option 590 <tt>false</tt> otherwise 591 **/ 592 public Option(String text, boolean selected) { 593 this(text, null, selected); 594 } 595 596 /** 597 Constructs a new unselected option with the specified 598 html text and no separate value. 599 600 @param text the html text of the option tag 601 **/ 602 public Option(String text) { 603 this(text, null, false); 604 } 605 606 boolean isOrigSelected() { 607 return orig_selected; 608 } 609 610 /** 611 Returns the value of this option tag. If no value is set, 612 returns the html text value for this option tag 613 **/ 614 public String getValue() 615 { 616 if (value != null) 617 return value; 618 else 619 return text; 620 } 621 622 public String render(final boolean selected) { 623 StringBuffer buf = new StringBuffer(32); 624 buf.append("<option"); 625 if (value != null) { 626 buf.append(" value='"); 627 buf.append(value); 628 buf.append("'"); 629 } 630 if (selected) { 631 buf.append(" SELECTED"); 632 } 633 buf.append(">"); 634 buf.append(text); 635 buf.append("</option>"); 636 return buf.toString(); 637 } 638 639 public String toString() { 640 return render(false); 641 } 642 } //~class Option 643 644 } //~class Select