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 refreshable HTML Select field. This is different than {@link Select}
020 in that is allows the values/options displayed by this select to be
021 changed dynamically/per-user via the {@link #setValue(FormData, List)}
022 method.
023 <p>
024 There is no easy way to persistently keep track of these values which may
025 be different for each user. Therefore, unlike other field classes, this
026 class does (by default) no check to see if the returned values by the
027 user match at least one of the values displayed to the user in the first
028 place. This can happen if values are hacked on the client side.
029 <p>
030 Database constraints (that only accept valid values) should be used to
031 possibly handle bad data returned by this field.
032
033 @author hursh jain
034 **/
035
036 public final class RefreshableSelect extends Select
037 {
038 static 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 /**
058 Creates a new select element with no initial values. Multiple selections
059 are not initially allowed.
060
061 @param name the field name
062 **/
063 public RefreshableSelect(String name)
064 {
065 super(name);
066 }
067
068 /**
069 Creates a new select element with the specified initial values
070 and no multiple selections allowed. There is no explicit default
071 selection which typically means the browser will show the first
072 item 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 **/
084 public RefreshableSelect(String name, List values)
085 {
086 super(name);
087 Argcheck.notnull(values, "values param was null");
088 initOptions(values);
089 }
090
091 public Field.Type getType() {
092 return Field.Type.SELECT;
093 }
094
095 /**
096 Sets the options for this select in the specified form data. This is
097 useful for showing different <i>initial</i> values to each user (before
098 the form has been submitted by that user).
099 <p>
100 If the form has not been submitted, there is no form data object. A form
101 data 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 */
106 public 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
130 specified form data. All of the initial options are used and option
131 corresponding to the specified value is selected. There must be some
132 initial options set for this field and the specified value should match
133 one of the initial options.
134 <p>
135 If the form has not been submitted, there is no form data object. A form
136 data object should be manually created if needed for storing the
137 selected 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 */
142 public 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 /**
164 Convenience method that invokes {@link #setSelectedValue(fd, String)}
165 after converting the specified integer value to a string.
166 */
167 public void setSelectedValue(FormData fd, int value) {
168 setSelectedValue(fd, String.valueOf(value));
169 }
170
171 /**
172 Returns a List containing the selected options. Each object in
173 the collection will be of type {@link Select.Option}. If there
174 are no selected options, the returned list will be an
175 empty list.
176
177 @param fd the submited form data. This object should not be null
178 otherwise a NPE will be thrown.
179 **/
180 public 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 /**
190 Convenience method that returns the selected option as a
191 String. <u>No guarantees are made as to which selection is
192 returned when more than one selection is selected (this
193 method is really meant for when the select only allows
194 single selections as a dropdown).</u>
195 <p>
196 The returned value is of type String obtained by called the
197 selected Option's {@link Select.Option#getValue} method.
198 <p>
199 If there is no selection, returns <tt>null</tt>
200 **/
201 public 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 /**
222 Convenience method that returns the single value of this field
223 as a Integer.
224 <p>
225 All the caveats of {@link #getSingleValueAsString()} apply.
226
227 @throws NumberFormatException if the value could not be
228 returned as in integer.
229 */
230 public int getIntValue(FormData fd) {
231 return Integer.parseInt(getStringValue(fd));
232 }
233
234 /**
235 Convenience method that returns the single value of this field
236 as a boolean.
237 <p>
238 All the caveats of {@link #getSingleValueAsString()} apply.
239 In particular, the formdata should contain non-null data
240 with at least one selection.
241 */
242 public boolean getBooleanValue(FormData fd) {
243 return Boolean.valueOf(getStringValue(fd)).booleanValue();
244 }
245
246 /**
247 Returns <tt>true</tt> if some option has been selected
248 by the user, <tt>false</tt> otherwise. (also returns
249 <tt>false</tt> is the specified form data is <tt>null</tt>).
250 */
251 public 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 */
294 public void setValueFromSubmit(FormData fd, HttpServletRequest req)
295 throws 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
325 private 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
333 public 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 /**
413 Adds a new option to the selection list. Replaces any previous
414 option that was added previously with the same value.
415
416 @param opt the option to be added
417 **/
418 public 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 /**
429 Adds a new option to the selection list. Replaces any previous
430 option that was added previously with the same value. This method
431 will have the same effect as if the {@link #add(Select.Option)
432 add(new Select.Option(item))} method was invoked.
433
434 @param item the option to be added
435 **/
436 public 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 /**
444 Clears all data in this select.
445 */
446 public void reset()
447 {
448 options.clear();
449 origSelectedMap.clear();
450 }
451
452 /**
453 This value (if set) is rendered as the html <tt>SIZE</tt> tag.
454 If the list contains more options than specified by size, the browser
455 will display the selection list with scrollbars.
456
457 @return this object for method chaining convenience
458 **/
459 public 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.
466 This value (if set) is rendered as the html <tt>MULTIPLE</tt> tag.
467
468 @return this object for method chaining convenience
469 **/
470 public Select allowMultiple(boolean allow) {
471 this.multiple = allow;
472 return this;
473 }
474 } //~class Select