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