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 javax.servlet.*;
009 import javax.servlet.http.*;
010 import java.io.*;
011 import java.util.*;
012 import java.util.regex.*;
013 import java.sql.*;
014
015 import fc.jdbc.*;
016 import fc.io.*;
017 import fc.util.*;
018
019 /**
020 A dependent HTML DependentSelect field. The values of this field change based
021 on the chosen value of some other field in the form. These new values are
022 calculated/set on the server side.
023 <p>
024 Another good (and equally valid) approach is to instead use javascript arrays
025 (with different sets of values) and swap out values on the client and/or
026 use AJAX on the client side.
027
028 @author hursh jain
029 **/
030 public final class DependentSelect extends DependentField
031 {
032 public static class Data
033 {
034 //---------- Select Data -------------
035 private LinkedHashMap options = new LinkedHashMap();
036 //the initially selected options when form was constructed
037 private Map origSelectedMap = new HashMap();
038 private int size = 0;
039 private boolean multiple;
040 //---------- Client Submit Data -------
041 //[option1.getValue() -> option1] ...
042 private Map selectedMap = null; //created if there is data
043 //[option1, option2....]
044 private List selectedList = null; //created if there is data
045 //-------------------------------------
046 /**
047 Convienence method for use by the {@link Dependency#getInitialValues}
048 method.
049 */
050 public void addOption(Select.Option item) {
051 String itemval = item.getValue();
052 options.put(itemval, item);
053 if (item.isOrigSelected()) {
054 origSelectedMap.put(itemval, item);
055 }
056 }
057
058 public void clearSubmittedData() {
059 if (selectedMap != null)
060 selectedMap.clear();
061 if (selectedList != null)
062 selectedList.clear();
063 }
064
065 public String toString() {
066 return new ToString(this, ToString.Style.VisibleLevel.PRIVATE)
067 .reflect().render();
068 }
069 }
070
071 /**
072 Creates a new select element with no initial values. Multiple
073 selections are not initially allowed.
074
075 @param name the field name
076 **/
077 public DependentSelect(String name)
078 {
079 super(name);
080 }
081
082 public Field.Type getType() {
083 return Field.Type.SELECT;
084 }
085
086 /**
087 Returns a List containing the selected options. Each object in
088 the collection will be of type {@link Select.Option}. If there
089 are no selected options, the returned list will be an empty list.
090
091 @param fd the submited form data
092 **/
093 public List getValue(FormData fd)
094 {
095 Data data = (Data) fd.getData(name);
096 if (data == null || data.selectedList == null) {
097 return Form.empty_list;
098 }
099 return data.selectedList;
100 }
101
102 /**
103 Convenience method that returns the selected option as a String. <u>No
104 guarantees are made as to which selection is returned when more than one
105 selection is selected (this method is really meant for when the select only
106 allows single selections as a dropdown).</u>
107 <p>
108 The returned value is of type String obtained by called the selected
109 Select.Option's {@link Select.Option#getValue} method.
110 <p>
111 If there is no selection, returns <tt>null</tt>
112 **/
113 public String getStringValue(FormData fd)
114 {
115 Data data = (Data) fd.getData(name);
116 if (data == null) {
117 return null;
118 }
119
120 List list = data.selectedList;
121 if (list == null || list.size() == 0) {
122 return null;
123 }
124 else {
125 Select.Option opt = (Select.Option) list.get(0);
126 return opt.getValue();
127 }
128 }
129
130
131 /**
132 Convenience method that returns the single value of this field
133 as a Integer.
134 <p>
135 All the caveats of {@link #getSingleValueAsString()} apply.
136
137 @throws NumberFormatException if the value could not be
138 returned as in integer.
139 */
140 public int getIntValue(FormData fd) {
141 return Integer.parseInt(getStringValue(fd));
142 }
143
144 /**
145 Convenience method that returns the single value of this field
146 as a boolean.
147 <p>
148 All the caveats of {@link #getSingleValueAsString()} apply.
149 In particular, the formdata should contain non-null data
150 with at least one selection.
151 */
152 public boolean getBooleanValue(FormData fd) {
153 return Boolean.valueOf(getStringValue(fd)).booleanValue();
154 }
155
156 /**
157 Returns <tt>true</tt> if some option has been selected by the user,
158 <tt>false</tt> otherwise. (also returns <tt>false</tt> is the specified
159 form data is <tt>null</tt>).
160 */
161 public boolean isFilled(FormData fd)
162 {
163 if (fd == null)
164 return false;
165
166 Data data = (Data) fd.getData(name);
167
168 if (data == null)
169 return false;
170
171 List list = data.selectedList;
172 return (list != null && list.size() != 0);
173 }
174
175 /**
176 @see Select#setValueFromSubmit
177 */
178 public void setValueFromSubmit(FormData fd, HttpServletRequest req)
179 {
180 dependency.setDependencyDataFromSubmit(fd, req);
181
182 //value(s) associated with the selection field name
183 String[] values = req.getParameterValues(name);
184
185 Data data = new Data();
186 fd.putData(name, data);
187
188 if (values == null) {
189 return;
190 }
191
192 data.selectedMap = new HashMap();
193 data.selectedList = new ArrayList();
194
195 for (int n = 0; n < values.length; n++) {
196 Select.Option opt = new Select.Option(values[n]);
197 data.selectedMap.put(opt.getValue(), opt);
198 data.selectedList.add(opt);
199 }
200 }
201
202 public void renderImpl(FormData fd, Writer writer)
203 throws SQLException, IOException
204 {
205 Data data = null;
206
207 if (fd == null) {
208 data = (Data) dependency.getInitialValues(this);
209 }
210 else{
211 //Case 1. Form submitted
212 // dependency updates the data object for this field in the formdata.
213 // _after_ the form is submitted. The dependency will set render data
214 // in a non-null data object (creating it if necessary).
215 // (even though the submitted data parts in the data object can be null)
216 //
217 //Case 2. Form not submitted but FormData created for initial values.
218 // The dependency will not be invoked by the form handling
219 // mechanism so we still need to get initial values manually.
220 //
221 data = (Data) fd.getData(name);
222 if (data == null) {
223 data = (Data) dependency.getInitialValues(this);
224 }
225 }
226
227 Argcheck.notnull(data, "Internal error: unexpected state, data was null");
228
229 writer.write("<select");
230 writer.write(" name='");
231 writer.write(name);
232 writer.write("'");
233
234 if (data.size > 0) {
235 writer.write(" size='");
236 writer.write(String.valueOf(data.size));
237 writer.write("'");
238 }
239
240 if (data.multiple) {
241 writer.write(" multiple");
242 }
243
244 if (! enabled || ! isEnabled(fd)) {
245 writer.write(" disabled");
246 }
247
248 if (renderStyleTag) {
249 writer.write(" style='");
250 writer.write(styleTag);
251 writer.write("'");
252 }
253
254 final int arlen = arbitraryString.size();
255 for (int n = 0; n < arlen; n++) {
256 writer.write(" ");
257 writer.write(arbitraryString.get(n).toString());
258 }
259
260 writer.write(">\n");
261
262 Iterator it = data.options.values().iterator();
263 while (it.hasNext())
264 {
265 Select.Option item = (Select.Option) it.next();
266 String itemval = item.getValue();
267
268 boolean selected = false;
269
270 if (fd != null) /* maintain submit state */
271 {
272 if (data.selectedMap != null){
273 selected = data.selectedMap.containsKey(itemval);
274 }
275 else {
276 //form submitted but no submit data for this field
277 //hence selecteMap == null
278 }
279 }
280 else{ /* form not submitted, show original state */
281 selected = data.origSelectedMap.containsKey(itemval);
282 }
283
284 writer.write(item.render(selected));
285 writer.write("\n"); //sufficient for easy view source in browser
286 }
287
288 writer.write("</select>\n");
289
290 dependency.renderDependencyData(fd, writer);
291 }
292
293 /**
294 <tt>true</tt> if multiple selections are allowed, <tt>false</tt> otherwise.
295 This value (if set) is rendered as the html <tt>MULTIPLE</tt> tag.
296
297 @return this object for method chaining convenience
298 **/
299 public DependentSelect allowMultiple(FormData fd, boolean allow)
300 {
301 Data d = getDataObject(fd);
302 d.multiple = allow;
303 return this;
304 }
305
306 /**
307 This value (if set) is rendered as the html <tt>SIZE</tt> tag.
308 If the list contains more options than specified by size, the browser
309 will display the selection list with scrollbars.
310
311 @return this object for method chaining convenience
312 **/
313 public DependentSelect setSize(FormData fd, int size)
314 {
315 Data d = getDataObject(fd);
316 d.size = size;
317 return this;
318 }
319
320 /**
321 Sets the selected values for this select in the specified form data. This
322 is useful for showing different <i>initial</i> values to each user
323 (before the form has been submitted by that user). The values are set in
324 a newly created {@link DependentSelect.Data} object which is then stored
325 inside the form data. Since these are set in the FormData object, these
326 values are request specific and can differ per user.
327 <p>
328 If the form has not been submitted, there is no form data object. A form
329 data object should be manually created if needed for storing the value.
330
331 @param data an existing form data object.
332 @param values a list of {@link Select.Option} objects
333 */
334 public void setValue(FormData fd, List values)
335 {
336 Argcheck.notnull(fd, "specified fd param was null");
337
338 DependentSelect.Data data = new DependentSelect.Data();
339 fd.putData(name, data);
340 setValue(data, values);
341 }
342
343 /**
344 Convenience method to set values in the specified {@link
345 DependentSelect.Data} object.
346 The following two are equivalent:
347 <blockquote>
348 <pre>
349 List values .....
350 DependentSelect.Data data = new DependentSelect.Data();
351 fd.putData(name, data);
352 setValue(<i>data</i>, values);
353 </blockquote>
354 </pre>
355 and
356 <blockquote>
357 <pre>
358 List values .....
359 setValue(<i>fd</i>, values);
360 </blockquote>
361 </pre>
362 @param data an existing DependentSelect.Data object.
363 @param values a list of {@link Select.Option} objects
364 */
365 public void setValue(DependentSelect.Data data, List values)
366 {
367 Iterator it = values.iterator();
368 while (it.hasNext()) {
369 Select.Option item = (Select.Option) it.next();
370 String itemval = item.getValue();
371 data.options.put(itemval, item);
372 if (item.isOrigSelected()) {
373 data.origSelectedMap.put(itemval, item);
374 }
375 }
376 }
377
378 /**
379 Returns the data object for this field from the formdata. returns
380 the initial data (if applicable) if the form is being rendered for
381 the first time or if no dependent data has been created.
382 */
383 Data getDataObject(FormData fd)
384 {
385 Data d = (Data) fd.getData(name);
386 return d;
387 }
388
389 } //~class DependentSelect