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 java.io.*; 009import java.util.*; 010import java.sql.*; 011import javax.servlet.*; 012import javax.servlet.http.*; 013 014import fc.jdbc.*; 015import fc.io.*; 016import fc.util.*; 017 018/** 019An HTML Select field 020<p> 021There 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**/ 047public 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 053LinkedHashMap options = new LinkedHashMap(); 054//orig selected options when form was constructed 055Map origSelectedMap = new HashMap(); 056int size = 0; 057boolean multiple; 058 059static 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/** 071Creates a new select element with no initial values. Multiple 072selections are not initially allowed. 073 074@param name the field name 075**/ 076public Select(String name) 077 { 078 super(name); 079 } 080 081/** 082Creates a new select element with the specified initial values 083and no multiple selections allowed. There is no explicit default 084selection which typically means the browser will show the first 085item 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**/ 097public Select(String name, List values) 098 { 099 super(name); 100 Argcheck.notnull(values, "values param was null"); 101 initOptions(values); 102 } 103 104void 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/** 117The 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 120first option returned by the query is selected for single-value selects. 121<p> 122Typically, this method may be invoked like: 123<blockquote> 124<tt> 125useQuery(con, "select id, name from my_lookup_table", new Option("--choose--")); 126</tt> 127</blockquote> 128If the lookup table has more than 2 columns, the query can look like (in 129standard SQL syntax): 130<blockquote> 131<tt> 132useQuery(con, "select id, <font color=blue>name1 || ' ' || name2</font> from my_lookup_table", new Option("--choose--")); 133</tt> 134</blockquote> 135This 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*/ 143public Select useQuery(Connection con, String query, Select.Option defaultOpt) 144throws 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/** 158Convenience method that calls {@link useQuery(Connection, String, String) 159useQuery(con, query, null)}. 160*/ 161public Select useQuery(Connection con, String query) throws SQLException 162 { 163 useQuery(con, query, null); 164 return this; 165 } 166 167/** 168Uses the first column as the option text and if there is a second column 169uses it as the corresponding option value. 170*/ 171public static List makeOptionsFromQuery(Connection con, String query) 172throws SQLException 173 { 174 return makeOptionsFromQuery(con, query, null); 175 } 176 177/** 178Uses the first column as the option text and if there is a second column 179uses it as the corresponding option value. Uses an additional default 180option. 181*/ 182public static List makeOptionsFromQuery( 183 Connection con, String query, Select.Option defaultOption) 184throws 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 207public Field.Type getType() { 208 return Field.Type.SELECT; 209 } 210 211/** 212Sets the values for this select. Any previously set values are 213first cleared before new values are set. 214<p> 215Note, to show user specific options via FormData, use a {@link RefreshableSelect} 216instead. 217 218@param values a list of {@link Select.Option} objects 219*/ 220public 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/** 230Returns a List containing the selected options. Each object in 231the collection will be of type {@link Select.Option}. If there 232are no selected options, the returned list will be an 233empty list. 234 235@param fd the submited form data. This object should not be null 236 otherwise a NPE will be thrown. 237**/ 238public 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/** 248Convenience method that returns the selected option as a 249String. <u>No guarantees are made as to which selection is 250returned when more than one selection is selected (this 251method is really meant for when the select only allows 252single selections as a dropdown).</u> 253<p> 254The returned value is of type String obtained by called the 255selected Option's {@link Select.Option#getValue} method. 256<p> 257If there is no selection, returns <tt>null</tt> 258**/ 259public 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/** 280Convenience method that returns the single value of this field 281as a Integer. 282<p> 283All the caveats of {@link #getSingleValueAsString()} apply. 284 285@throws NumberFormatException if the value could not be 286 returned as in integer. 287*/ 288public int getIntValue(FormData fd) { 289 return Integer.parseInt(getStringValue(fd)); 290 } 291 292/** 293Convenience method that returns the single value of this field 294as a boolean. 295<p> 296All the caveats of {@link #getSingleValueAsString()} apply. 297In particular, the formdata should contain non-null data 298with at least one selection. 299*/ 300public boolean getBooleanValue(FormData fd) { 301 return Boolean.valueOf(getStringValue(fd)).booleanValue(); 302 } 303 304/** 305Returns <tt>true</tt> if some option has been selected 306by the user, <tt>false</tt> otherwise. (also returns 307<tt>false</tt> is the specified form data is <tt>null</tt>). 308*/ 309public 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*/ 353public void setValueFromSubmit(FormData fd, HttpServletRequest req) 354throws 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 382private void addSelectedOpt( 383 HttpServletRequest req, Select.Data data, String submitValue) 384throws 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 402public 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/** 481Adds a new option to the selection list. Replaces any previous 482option that was added previously with the same value. 483 484@param opt the option to be added 485**/ 486public 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/** 497Adds a new option to the selection list. Replaces any previous 498option that was added previously with the same value. This method 499will have the same effect as if the {@link #add(Option) 500add(new Select.Option(item))} method was invoked. 501 502@param item the option to be added 503**/ 504public 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/** 512Clears all data in this select. 513*/ 514public void reset() 515 { 516 options.clear(); 517 origSelectedMap.clear(); 518 } 519 520/** 521This value (if set) is rendered as the html <tt>SIZE</tt> tag. 522If the list contains more options than specified by size, the browser 523will display the selection list with scrollbars. 524 525@return this object for method chaining convenience 526**/ 527public 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. 534This value (if set) is rendered as the html <tt>MULTIPLE</tt> tag. 535 536@return this object for method chaining convenience 537**/ 538public Select allowMultiple(boolean allow) { 539 this.multiple = allow; 540 return this; 541 } 542 543/** Represents an option in the selection list **/ 544public 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