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 refreshable HTML Select field. This is different than {@link Select} 020in that is allows the values/options displayed by this select to be 021changed dynamically/per-user via the {@link #setValue(FormData, List)} 022method. 023<p> 024There is no easy way to persistently keep track of these values which may 025be different for each user. Therefore, unlike other field classes, this 026class does (by default) no check to see if the returned values by the 027user match at least one of the values displayed to the user in the first 028place. This can happen if values are hacked on the client side. 029<p> 030Database constraints (that only accept valid values) should be used to 031possibly handle bad data returned by this field. 032 033@author hursh jain 034**/ 035 036public final class RefreshableSelect extends Select 037{ 038static final class Data 039 { 040 //These are null by default but can override the options for the 041 //select instance when set per user.This is useful when values are refreshed 042 //every submit by re-querying the database. (we don't want to set new 043 //values in the instance fields because that would effectively cause 044 //all threads to synchronize rendering this field) 045 private LinkedHashMap options; 046 private Map origSelectedMap; 047 048 //To store selected options submitted by the user 049 //acts like a set, used via containsKey() to see if an options 050 //is selected (so we can reselect it) during rendering. 051 private Map selectedMap = null; //null implies no submitted data 052 053 //to store all options submitted by user 054 private List selectedList = null; 055 } 056 057/** 058Creates a new select element with no initial values. Multiple selections 059are not initially allowed. 060 061@param name the field name 062**/ 063public RefreshableSelect(String name) 064 { 065 super(name); 066 } 067 068/** 069Creates a new select element with the specified initial values 070and no multiple selections allowed. There is no explicit default 071selection which typically means the browser will show the first 072item in the list as the selection item. 073 074@param name the field name 075@param values the initial option values for this class. The 076 objects contained in the list must be of type 077 {@link Select.Option} otherwise a 078 <tt>ClassCastException</tt> may be thrown in 079 future operations on this object. 080 081@throws IllegalArgumentException if the values parameter was null 082 (note: an empty but non-null List is ok) 083**/ 084public RefreshableSelect(String name, List values) 085 { 086 super(name); 087 Argcheck.notnull(values, "values param was null"); 088 initOptions(values); 089 } 090 091public Field.Type getType() { 092 return Field.Type.SELECT; 093 } 094 095/** 096Sets the options for this select in the specified form data. This is 097useful for showing different <i>initial</i> values to each user (before 098the form has been submitted by that user). 099<p> 100If the form has not been submitted, there is no form data object. A form 101data object should be manually created if needed for storing the value. 102 103@param fd the non-null form data used for rendering the form 104@param values a list of {@link Select.Option} objects 105*/ 106public void setValue(FormData fd, final List values) 107 { 108 Argcheck.notnull(fd, "specified fd param was null"); 109 Argcheck.notnull(values, "specified values param was null"); 110 111 Data data = new Data(); 112 data.options = new LinkedHashMap(); 113 data.origSelectedMap = new HashMap(); 114 fd.putData(name, data); 115 116 for (int n = 0; n < values.size(); n++) { 117 Select.Option item = (Select.Option) values.get(n); 118 String itemval = item.getValue(); 119 data.options.put(itemval, item); 120 if (item.isOrigSelected()) { 121 data.origSelectedMap.put(itemval, item); 122 } 123 } 124 //System.out.println("data.options ==> " + data.options); 125 } 126 127 128/** 129<i>Convenience</i> method that sets the options for this select in the 130specified form data. All of the initial options are used and option 131corresponding to the specified value is selected. There must be some 132initial options set for this field and the specified value should match 133one of the initial options. 134<p> 135If the form has not been submitted, there is no form data object. A form 136data object should be manually created if needed for storing the 137selected value. 138 139@param fd the non-null form data used for rendering the form 140@param value the option with this value is selected 141*/ 142public void setSelectedValue(FormData fd, String value) 143 { 144 Argcheck.notnull(fd, "specified fd param was null"); 145 Argcheck.notnull(value, "specified selected param was null"); 146 147 Data data = new Data(); 148 data.options = new LinkedHashMap(options); 149 data.origSelectedMap = new HashMap(); 150 fd.putData(name, data); 151 152 Iterator it = data.options.values().iterator(); 153 while (it.hasNext()) 154 { 155 Select.Option opt = (Select.Option) it.next(); 156 String optval = opt.getValue(); 157 if (optval.equals(value)) { 158 data.origSelectedMap.put(optval, opt); 159 } 160 } 161 } 162 163/** 164Convenience method that invokes {@link #setSelectedValue(fd, String)} 165after converting the specified integer value to a string. 166*/ 167public void setSelectedValue(FormData fd, int value) { 168 setSelectedValue(fd, String.valueOf(value)); 169 } 170 171/** 172Returns a List containing the selected options. Each object in 173the collection will be of type {@link Select.Option}. If there 174are no selected options, the returned list will be an 175empty list. 176 177@param fd the submited form data. This object should not be null 178 otherwise a NPE will be thrown. 179**/ 180public List getValue(FormData fd) 181 { 182 RefreshableSelect.Data data = (RefreshableSelect.Data) fd.getData(name); 183 if (data == null) { 184 return Form.empty_list; 185 } 186 return data.selectedList; 187 } 188 189/** 190Convenience method that returns the selected option as a 191String. <u>No guarantees are made as to which selection is 192returned when more than one selection is selected (this 193method is really meant for when the select only allows 194single selections as a dropdown).</u> 195<p> 196The returned value is of type String obtained by called the 197selected Option's {@link Select.Option#getValue} method. 198<p> 199If there is no selection, returns <tt>null</tt> 200**/ 201public String getStringValue(FormData fd) 202 { 203 String result = null; 204 205 RefreshableSelect.Data data = (RefreshableSelect.Data) fd.getData(name); 206 if (data == null) { 207 return null; 208 } 209 210 List list = data.selectedList; 211 if (list == null || list.size() == 0) { 212 return result; 213 } 214 else { 215 Select.Option opt = (Select.Option) list.get(0); 216 return opt.getValue(); 217 } 218 } 219 220 221/** 222Convenience method that returns the single value of this field 223as a Integer. 224<p> 225All the caveats of {@link #getSingleValueAsString()} apply. 226 227@throws NumberFormatException if the value could not be 228 returned as in integer. 229*/ 230public int getIntValue(FormData fd) { 231 return Integer.parseInt(getStringValue(fd)); 232 } 233 234/** 235Convenience method that returns the single value of this field 236as a boolean. 237<p> 238All the caveats of {@link #getSingleValueAsString()} apply. 239In particular, the formdata should contain non-null data 240with at least one selection. 241*/ 242public boolean getBooleanValue(FormData fd) { 243 return Boolean.valueOf(getStringValue(fd)).booleanValue(); 244 } 245 246/** 247Returns <tt>true</tt> if some option has been selected 248by the user, <tt>false</tt> otherwise. (also returns 249<tt>false</tt> is the specified form data is <tt>null</tt>). 250*/ 251public boolean isFilled(FormData fd) 252 { 253 if (fd == null) 254 return false; 255 256 RefreshableSelect.Data data = (RefreshableSelect.Data) fd.getData(name); 257 if (data == null) { 258 return false; 259 } 260 List list = data.selectedList; 261 return (list != null && list.size() != 0); 262 } 263 264/* 265 Select values can be present as: 266 selectname=value 267 268 or for multiple selections (for say a select called "sel1") 269 sel1=value1&sel1=val2 270 271 The values sent are those in the value tag in the option 272 and if missing those of the corresponding html for the option. 273 274 If a select allows multiple and none are selected, then 275 nothing at all is sent (note this is different than for 276 single (and not multiple) selects, where some value will always 277 be sent (since something is always selected). 278 279 We can track submitted/selected options in 2 ways: 280 1. go thru the option list and set a select/unselect on each option 281 element and then the option element renders itself appropriately. 282 2. keep a separate list of selected elements and at render time display 283 the item as selected only if it's also in the select list. 284 285 (w) implies that we have to specify select/no select at 286 render time to each option as opposed to setting that flag 287 in the option before telling the option to render itself. 288 (2) is chosen here. 289 290 the client can send hacked 2 or more options with the same 291 value this method will simply add any such duplicate values 292 at the end of the list. this is ok. 293*/ 294public void setValueFromSubmit(FormData fd, HttpServletRequest req) 295throws SubmitHackedException 296 { 297 //value(s) associated with the selection field name 298 String[] values = req.getParameterValues(name); 299 300 if (values == null) { 301 return; 302 } 303 304 //lazy instantiation 305 RefreshableSelect.Data data = new RefreshableSelect.Data(); 306 data.selectedMap = new HashMap(); 307 data.selectedList = new ArrayList(); 308 309 fd.putData(name, data); 310 311 if (multiple && values.length > 1) 312 hacklert(req, "recieved multiple values for a single value select"); 313 314 if (multiple) 315 { 316 for (int n = 0; n < values.length; n++) { 317 addSelectedOpt(req, data, values[n]); 318 } 319 } 320 else{ 321 addSelectedOpt(req, data, values[0]); 322 } 323 } 324 325private void addSelectedOpt( 326 HttpServletRequest req, RefreshableSelect.Data data, String submitValue) 327 { 328 Select.Option opt = new Select.Option(submitValue); 329 data.selectedMap.put(opt.getValue(), opt); 330 data.selectedList.add(opt); 331 } 332 333public void renderImpl(FormData fd, Writer writer) throws IOException 334 { 335 RefreshableSelect.Data data = null; 336 337 Map options = this.options; 338 Map origSelectedMap = this.origSelectedMap; 339 340 if (fd != null) { //we have submit or initial data 341 data = (RefreshableSelect.Data) fd.getData(name); //can be null 342 if (data != null) { 343 if (data.options != null) { 344 /*per user options & origMap were set */ 345 options = data.options; 346 origSelectedMap = data.origSelectedMap; 347 } 348 } 349 } 350 351 writer.write("<select"); 352 writer.write(" name='"); 353 writer.write(name); 354 writer.write("'"); 355 356 if (size > 0) { 357 writer.write(" size='"); 358 writer.write(String.valueOf(size)); 359 writer.write("'"); 360 } 361 362 if (! enabled || ! isEnabled(fd)) { 363 writer.write(" disabled"); 364 } 365 366 if (multiple) { 367 writer.write(" multiple"); 368 } 369 370 if (renderStyleTag) { 371 writer.write(" style='"); 372 writer.write(styleTag); 373 writer.write("'"); 374 } 375 376 final int arlen = arbitraryString.size(); 377 for (int n = 0; n < arlen; n++) { 378 writer.write(" "); 379 writer.write(arbitraryString.get(n).toString()); 380 } 381 382 writer.write(">\n"); 383 384 Iterator it = options.values().iterator(); 385 while (it.hasNext()) 386 { 387 Select.Option item = (Select.Option) it.next(); 388 String itemval = item.getValue(); 389 390 boolean selected = false; 391 392 if (fd != null) /* maintain submit state */ 393 { 394 if (data.selectedMap != null) { /*there was a submit*/ 395 selected = data.selectedMap.containsKey(itemval); 396 } 397 else { /*initial data*/ 398 selected = origSelectedMap.containsKey(itemval); 399 } 400 } 401 else{ /* form not submitted, show original state */ 402 selected = origSelectedMap.containsKey(itemval); 403 } 404 405 writer.write(item.render(selected)); 406 writer.write("\n"); //sufficient for easy view source in browser 407 } 408 409 writer.write("</select>"); 410 } 411 412/** 413Adds a new option to the selection list. Replaces any previous 414option that was added previously with the same value. 415 416@param opt the option to be added 417**/ 418public void add(Select.Option opt) 419 { 420 Argcheck.notnull(opt, "opt param was null"); 421 String optval = opt.getValue(); 422 options.put(optval, opt); 423 if (opt.isOrigSelected()) { 424 origSelectedMap.put(optval, opt); 425 } 426 } 427 428/** 429Adds a new option to the selection list. Replaces any previous 430option that was added previously with the same value. This method 431will have the same effect as if the {@link #add(Select.Option) 432add(new Select.Option(item))} method was invoked. 433 434@param item the option to be added 435**/ 436public void add(String item) 437 { 438 Argcheck.notnull(item, "item param was null"); 439 Select.Option opt = new Select.Option(item); 440 add(opt); 441 } 442 443/** 444Clears all data in this select. 445*/ 446public void reset() 447 { 448 options.clear(); 449 origSelectedMap.clear(); 450 } 451 452/** 453This value (if set) is rendered as the html <tt>SIZE</tt> tag. 454If the list contains more options than specified by size, the browser 455will display the selection list with scrollbars. 456 457@return this object for method chaining convenience 458**/ 459public Select setSize(int size) { 460 this.size = size; 461 return this; 462 } 463 464/** 465<tt>true</tt> is multiple selections are allowed, <tt>false</tt> otherwise. 466This value (if set) is rendered as the html <tt>MULTIPLE</tt> tag. 467 468@return this object for method chaining convenience 469**/ 470public Select allowMultiple(boolean allow) { 471 this.multiple = allow; 472 return this; 473 } 474} //~class Select