1 /*
2 * $Id$
3 *
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
19 * under the License.
20 */
21 package org.apache.struts.action;
22
23 import java.lang.reflect.Array;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.StringTokenizer;
28
29 import jakarta.servlet.ServletRequest;
30 import jakarta.servlet.http.HttpServletRequest;
31
32 import org.apache.commons.beanutils.ConversionException;
33 import org.apache.commons.beanutils.DynaBean;
34 import org.apache.commons.beanutils.DynaClass;
35 import org.apache.commons.beanutils.DynaProperty;
36 import org.apache.struts.config.FormBeanConfig;
37 import org.apache.struts.config.FormPropertyConfig;
38
39 /**
40 * <p>Specialized subclass of <code>ActionForm</code> that allows the creation
41 * of form beans with dynamic sets of properties, without requiring the
42 * developer to create a Java class for each type of form bean.</p>
43 *
44 * <p><strong>USAGE NOTE</strong> - Since Struts 1.1, the <code>reset</code>
45 * method no longer initializes property values to those specified in
46 * <code><form-property></code> elements in the Struts module
47 * configuration file. If you wish to utilize that behavior, the simplest
48 * solution is to subclass <code>DynaActionForm</code> and call the
49 * <code>initialize</code> method inside it.</p>
50 *
51 * @version $Rev$ $Date: 2005-11-12 11:52:08 -0500 (Sat, 12 Nov 2005)
52 * $
53 * @since Struts 1.1
54 */
55 public class DynaActionForm extends ActionForm implements DynaBean {
56 private static final long serialVersionUID = 1726775935544475430L;
57
58 // ----------------------------------------------------- Instance Variables
59
60 /**
61 * <p>The <code>DynaActionFormClass</code> with which we are associated.
62 * </p>
63 */
64 protected DynaActionFormClass dynaClass = null;
65
66 /**
67 * <p>The set of property values for this <code>DynaActionForm</code>,
68 * keyed by property name.</p>
69 */
70 protected HashMap<String, Object> dynaValues = new HashMap<>();
71
72 // ----------------------------------------------------- ActionForm Methods
73
74 /**
75 * <p>Initialize all bean properties to their initial values, as specified
76 * in the {@link FormPropertyConfig} elements associated with the
77 * definition of this <code>DynaActionForm</code>.</p>
78 *
79 * @param mapping The mapping used to select this instance
80 */
81 public void initialize(ActionMapping mapping) {
82 String name = mapping.getName();
83
84 if (name == null) {
85 return;
86 }
87
88 FormBeanConfig config =
89 mapping.getModuleConfig().findFormBeanConfig(name);
90
91 if (config == null) {
92 return;
93 }
94
95 initialize(config);
96 }
97
98 /**
99 * <p>Initialize the specified form bean.</p>
100 *
101 * @param config The configuration for the form bean to initialize.
102 */
103 public void initialize(FormBeanConfig config) {
104 FormPropertyConfig[] props = config.findFormPropertyConfigs();
105
106 for (int i = 0; i < props.length; i++) {
107 set(props[i].getName(), props[i].initial());
108 }
109 }
110
111 // :FIXME: Is there any point in retaining these reset methods
112 // since they now simply replicate the superclass behavior?
113
114 /**
115 * <p>Reset bean properties to their default state, as needed. This method
116 * is called before the properties are repopulated by the controller.</p>
117 *
118 * <p>The default implementation attempts to forward to the HTTP version
119 * of this method.</p>
120 *
121 * @param mapping The mapping used to select this instance
122 * @param request The servlet request we are processing
123 */
124 public void reset(ActionMapping mapping, ServletRequest request) {
125 super.reset(mapping, request);
126 }
127
128 /**
129 * <p>Reset the properties to their <code>initial</code> value if their
130 * <code>reset</code> configuration is set to true or if
131 * <code>reset</code> is set to a list of HTTP request methods that
132 * includes the method of given <code>request</code> object.</p>
133 *
134 * @param mapping The mapping used to select this instance
135 * @param request The servlet request we are processing
136 */
137 public void reset(ActionMapping mapping, HttpServletRequest request) {
138 String name = getDynaClass().getName();
139
140 if (name == null) {
141 return;
142 }
143
144 FormBeanConfig config =
145 mapping.getModuleConfig().findFormBeanConfig(name);
146
147 if (config == null) {
148 return;
149 }
150
151 // look for properties we should reset
152 FormPropertyConfig[] props = config.findFormPropertyConfigs();
153
154 for (int i = 0; i < props.length; i++) {
155 String resetValue = props[i].getReset();
156
157 // skip this property if there's no reset value
158 if ((resetValue == null) || (resetValue.length() <= 0)) {
159 continue;
160 }
161
162 boolean reset = Boolean.valueOf(resetValue).booleanValue();
163
164 if (!reset) {
165 // check for the request method
166 // use a StringTokenizer with the default delimiters + a comma
167 StringTokenizer st =
168 new StringTokenizer(resetValue, ", \t\n\r\f");
169
170 while (st.hasMoreTokens()) {
171 String token = st.nextToken();
172
173 if (token.equalsIgnoreCase(request.getMethod())) {
174 reset = true;
175
176 break;
177 }
178 }
179 }
180
181 if (reset) {
182 set(props[i].getName(), props[i].initial());
183 }
184 }
185 }
186
187 // ------------------------------------------------------- DynaBean Methods
188
189 /**
190 * <p>Indicates if the specified mapped property contain a value for the
191 * specified key value.</p>
192 *
193 * @param name Name of the property to check
194 * @param key Name of the key to check
195 * @return <code>true</code> if the specified mapped property contains a
196 * value for the specified key value; <code>true</code>
197 * otherwise.
198 * @throws NullPointerException if there is no property of the
199 * specified name
200 * @throws IllegalArgumentException if there is no mapped property of the
201 * specified name
202 */
203 public boolean contains(String name, String key) {
204 Object value = dynaValues.get(name);
205
206 if (value == null) {
207 throw new NullPointerException("No mapped value for '" + name + "("
208 + key + ")'");
209 } else if (value instanceof Map) {
210 return (((Map<?, ?>) value).containsKey(key));
211 } else {
212 throw new IllegalArgumentException("Non-mapped property for '"
213 + name + "(" + key + ")'");
214 }
215 }
216
217 /**
218 * <p>Return the value of a simple property with the specified name.</p>
219 *
220 * @param name Name of the property whose value is to be retrieved
221 * @return The value of a simple property with the specified name.
222 * @throws IllegalArgumentException if there is no property of the
223 * specified name
224 * @throws NullPointerException if the type specified for the property
225 * is invalid
226 */
227 public Object get(String name) {
228 // Return any non-null value for the specified property
229 Object value = dynaValues.get(name);
230
231 if (value != null) {
232 return (value);
233 }
234
235 // Return a null value for a non-primitive property
236 Class<?> type = getDynaProperty(name).getType();
237
238 if (type == null) {
239 throw new NullPointerException("The type for property " + name
240 + " is invalid");
241 }
242
243 if (!type.isPrimitive()) {
244 return (value);
245 }
246
247 // Manufacture default values for primitive properties
248 if (type == Boolean.TYPE) {
249 return (Boolean.FALSE);
250 } else if (type == Byte.TYPE) {
251 return (Byte.valueOf((byte) 0));
252 } else if (type == Character.TYPE) {
253 return (Character.valueOf((char) 0));
254 } else if (type == Double.TYPE) {
255 return (Double.valueOf(0.0));
256 } else if (type == Float.TYPE) {
257 return (Float.valueOf((float) 0.0));
258 } else if (type == Integer.TYPE) {
259 return (Integer.valueOf(0));
260 } else if (type == Long.TYPE) {
261 return (Long.valueOf(0));
262 } else if (type == Short.TYPE) {
263 return (Short.valueOf((short) 0));
264 } else {
265 return (null);
266 }
267 }
268
269 /**
270 * <p>Return the value of an indexed property with the specified name.
271 * </p>
272 *
273 * @param name Name of the property whose value is to be retrieved
274 * @param index Index of the value to be retrieved
275 * @return The value of an indexed property with the specified name.
276 * @throws IllegalArgumentException if there is no property of the
277 * specified name
278 * @throws IllegalArgumentException if the specified property exists, but
279 * is not indexed
280 * @throws NullPointerException if no array or List has been
281 * initialized for this property
282 */
283 public Object get(String name, int index) {
284 Object value = dynaValues.get(name);
285
286 if (value == null) {
287 throw new NullPointerException("No indexed value for '" + name
288 + "[" + index + "]'");
289 } else if (value.getClass().isArray()) {
290 return (Array.get(value, index));
291 } else if (value instanceof List) {
292 return ((List<?>) value).get(index);
293 } else {
294 throw new IllegalArgumentException("Non-indexed property for '"
295 + name + "[" + index + "]'");
296 }
297 }
298
299 /**
300 * <p>Return the value of a mapped property with the specified name, or
301 * <code>null</code> if there is no value for the specified key. </p>
302 *
303 * @param name Name of the property whose value is to be retrieved
304 * @param key Key of the value to be retrieved
305 * @return The value of a mapped property with the specified name, or
306 * <code>null</code> if there is no value for the specified key.
307 * @throws NullPointerException if there is no property of the
308 * specified name
309 * @throws IllegalArgumentException if the specified property exists, but
310 * is not mapped
311 */
312 public Object get(String name, String key) {
313 Object value = dynaValues.get(name);
314
315 if (value == null) {
316 throw new NullPointerException("No mapped value for '" + name + "("
317 + key + ")'");
318 } else if (value instanceof Map) {
319 return (((Map<?, ?>) value).get(key));
320 } else {
321 throw new IllegalArgumentException("Non-mapped property for '"
322 + name + "(" + key + ")'");
323 }
324 }
325
326 /**
327 * <p>Return the value of a <code>String</code> property with the
328 * specified name. This is equivalent to calling <code>(String)
329 * dynaForm.get(name)</code>.</p>
330 *
331 * @param name Name of the property whose value is to be retrieved.
332 * @return The value of a <code>String</code> property with the specified
333 * name.
334 * @throws IllegalArgumentException if there is no property of the
335 * specified name
336 * @throws NullPointerException if the type specified for the property
337 * is invalid
338 * @throws ClassCastException if the property is not a String.
339 * @since Struts 1.2
340 */
341 public String getString(String name) {
342 return (String) this.get(name);
343 }
344
345 /**
346 * <p>Return the value of a <code>String[]</code> property with the
347 * specified name. This is equivalent to calling <code>(String[])
348 * dynaForm.get(name)</code>.</p>
349 *
350 * @param name Name of the property whose value is to be retrieved.
351 * @return The value of a <code>String[]</code> property with the
352 * specified name.
353 * @throws IllegalArgumentException if there is no property of the
354 * specified name
355 * @throws NullPointerException if the type specified for the property
356 * is invalid
357 * @throws ClassCastException if the property is not a String[].
358 * @since Struts 1.2
359 */
360 public String[] getStrings(String name) {
361 return (String[]) this.get(name);
362 }
363
364 /**
365 * <p>Return the <code>DynaClass</code> instance that describes the set of
366 * properties available for this <code>DynaBean</code>.</p>
367 *
368 * @return The <code>DynaClass</code> instance that describes the set of
369 * properties available for this <code>DynaBean</code>.
370 */
371 public DynaClass getDynaClass() {
372 return (this.dynaClass);
373 }
374
375 /**
376 * <p>Returns the <code>Map</code> containing the property values. This is
377 * done mostly to facilitate accessing the <code>DynaActionForm</code>
378 * through JavaBeans accessors, in order to use the JavaServer Pages
379 * Standard Tag Library (JSTL).</p>
380 *
381 * <p>For instance, the normal JSTL EL syntax for accessing an
382 * <code>ActionForm</code> would be something like this:
383 * <pre>
384 * ${formbean.prop}</pre>
385 * The JSTL EL syntax for accessing a <code>DynaActionForm</code> looks
386 * something like this (because of the presence of this
387 * <code>getMap()</code> method):
388 * <pre>
389 * ${dynabean.map.prop}</pre>
390 * </p>
391 *
392 * @return The <code>Map</code> containing the property values.
393 */
394 public Map<String, Object> getMap() {
395 return (dynaValues);
396 }
397
398 /**
399 * <p>Remove any existing value for the specified key on the specified
400 * mapped property.</p>
401 *
402 * @param name Name of the property for which a value is to be removed
403 * @param key Key of the value to be removed
404 * @throws NullPointerException if there is no property of the
405 * specified name
406 * @throws IllegalArgumentException if there is no mapped property of the
407 * specified name
408 */
409 public void remove(String name, String key) {
410 Object value = dynaValues.get(name);
411
412 if (value == null) {
413 throw new NullPointerException("No mapped value for '" + name + "("
414 + key + ")'");
415 } else if (value instanceof Map) {
416 ((Map<?, ?>) value).remove(key);
417 } else {
418 throw new IllegalArgumentException("Non-mapped property for '"
419 + name + "(" + key + ")'");
420 }
421 }
422
423 /**
424 * <p>Set the value of a simple property with the specified name.</p>
425 *
426 * @param name Name of the property whose value is to be set
427 * @param value Value to which this property is to be set
428 * @throws ConversionException if the specified value cannot be
429 * converted to the type required for
430 * this property
431 * @throws IllegalArgumentException if there is no property of the
432 * specified name
433 * @throws NullPointerException if the type specified for the property
434 * is invalid
435 * @throws NullPointerException if an attempt is made to set a
436 * primitive property to null
437 */
438 public void set(String name, Object value) {
439 DynaProperty descriptor = getDynaProperty(name);
440
441 if (descriptor.getType() == null) {
442 throw new NullPointerException("The type for property " + name
443 + " is invalid");
444 }
445
446 if (value == null) {
447 if (descriptor.getType().isPrimitive()) {
448 throw new NullPointerException("Primitive value for '" + name
449 + "'");
450 }
451 } else if (!isDynaAssignable(descriptor.getType(), value.getClass())) {
452 throw new ConversionException("Cannot assign value of type '"
453 + value.getClass().getName() + "' to property '" + name
454 + "' of type '" + descriptor.getType().getName() + "'");
455 }
456
457 dynaValues.put(name, value);
458 }
459
460 /**
461 * <p>Set the value of an indexed property with the specified name.</p>
462 *
463 * @param name Name of the property whose value is to be set
464 * @param index Index of the property to be set
465 * @param value Value to which this property is to be set
466 * @throws ConversionException if the specified value cannot be
467 * converted to the type required for
468 * this property
469 * @throws NullPointerException if there is no property of the
470 * specified name
471 * @throws IllegalArgumentException if the specified property exists, but
472 * is not indexed
473 * @throws IndexOutOfBoundsException if the specified index is outside the
474 * range of the underlying property
475 */
476 public void set(String name, int index, Object value) {
477 Object prop = dynaValues.get(name);
478
479 if (prop == null) {
480 throw new NullPointerException("No indexed value for '" + name
481 + "[" + index + "]'");
482 } else if (prop.getClass().isArray()) {
483 Array.set(prop, index, value);
484 } else if (prop instanceof List) {
485 try {
486 @SuppressWarnings("unchecked")
487 List<Object> list = (List<Object>)prop;
488 list.set(index, value);
489 } catch (ClassCastException e) {
490 throw new ConversionException(e.getMessage(), e);
491 }
492 } else {
493 throw new IllegalArgumentException("Non-indexed property for '"
494 + name + "[" + index + "]'");
495 }
496 }
497
498 /**
499 * <p>Set the value of a mapped property with the specified name.</p>
500 *
501 * @param name Name of the property whose value is to be set
502 * @param key Key of the property to be set
503 * @param value Value to which this property is to be set
504 * @throws NullPointerException if there is no property of the
505 * specified name
506 * @throws IllegalArgumentException if the specified property exists, but
507 * is not mapped
508 */
509 public void set(String name, String key, Object value) {
510 Object prop = dynaValues.get(name);
511
512 if (prop == null) {
513 throw new NullPointerException("No mapped value for '" + name + "("
514 + key + ")'");
515 } else if (prop instanceof Map) {
516 @SuppressWarnings("unchecked")
517 Map<String, Object> map = (Map<String, Object>)prop;
518 map.put(key, value);
519 } else {
520 throw new IllegalArgumentException("Non-mapped property for '"
521 + name + "(" + key + ")'");
522 }
523 }
524
525 // --------------------------------------------------------- Public Methods
526
527 /**
528 * <p>Render a String representation of this object.</p>
529 *
530 * @return A string representation of this object.
531 */
532 public String toString() {
533 StringBuilder sb = new StringBuilder("DynaActionForm[dynaClass=");
534 DynaClass dynaClass = getDynaClass();
535
536 if (dynaClass == null) {
537 return sb.append("null]").toString();
538 }
539
540 sb.append(dynaClass.getName());
541
542 DynaProperty[] props = dynaClass.getDynaProperties();
543
544 if (props == null) {
545 props = new DynaProperty[0];
546 }
547
548 for (int i = 0; i < props.length; i++) {
549 sb.append(',');
550 sb.append(props[i].getName());
551 sb.append('=');
552
553 Object value = get(props[i].getName());
554
555 if (value == null) {
556 sb.append("<NULL>");
557 } else if (value.getClass().isArray()) {
558 int n = Array.getLength(value);
559
560 sb.append("{");
561
562 for (int j = 0; j < n; j++) {
563 if (j > 0) {
564 sb.append(',');
565 }
566
567 sb.append(Array.get(value, j));
568 }
569
570 sb.append("}");
571 } else if (value instanceof List) {
572 int n = ((List<?>) value).size();
573
574 sb.append("{");
575
576 for (int j = 0; j < n; j++) {
577 if (j > 0) {
578 sb.append(',');
579 }
580
581 sb.append(((List<?>) value).get(j));
582 }
583
584 sb.append("}");
585 } else if (value instanceof Map) {
586 boolean first = true;
587
588 sb.append("{");
589
590 for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
591 if (first) {
592 first = false;
593 } else {
594 sb.append(',');
595 }
596
597 sb.append(entry.getKey());
598 sb.append('=');
599 sb.append(entry.getValue());
600 }
601
602 sb.append("}");
603 } else {
604 sb.append(value);
605 }
606 }
607
608 sb.append("]");
609
610 return (sb.toString());
611 }
612
613 // -------------------------------------------------------- Package Methods
614
615 /**
616 * <p>Set the <code>DynaActionFormClass</code> instance with which we are
617 * associated.</p>
618 *
619 * @param dynaClass The DynaActionFormClass instance for this bean
620 */
621 void setDynaActionFormClass(DynaActionFormClass dynaClass) {
622 this.dynaClass = dynaClass;
623 }
624
625 // ------------------------------------------------------ Protected Methods
626
627 /**
628 * <p>Return the property descriptor for the specified property name.</p>
629 *
630 * @param name Name of the property for which to retrieve the descriptor
631 * @return The property descriptor for the specified property name.
632 * @throws IllegalArgumentException if this is not a valid property name
633 * for our DynaClass
634 */
635 protected DynaProperty getDynaProperty(String name) {
636 DynaProperty descriptor = getDynaClass().getDynaProperty(name);
637
638 if (descriptor == null) {
639 throw new IllegalArgumentException("Invalid property name '" + name
640 + "'");
641 }
642
643 return (descriptor);
644 }
645
646 /**
647 * <p>Indicates if an object of the source class is assignable to the
648 * destination class.</p>
649 *
650 * @param dest Destination class
651 * @param source Source class
652 * @return <code>true</code> if the source is assignable to the
653 * destination; <code>false</code> otherwise.
654 */
655 protected boolean isDynaAssignable(Class<?> dest, Class<?> source) {
656 if (dest.isAssignableFrom(source)
657 || ((dest == Boolean.TYPE) && (source == Boolean.class))
658 || ((dest == Byte.TYPE) && (source == Byte.class))
659 || ((dest == Character.TYPE) && (source == Character.class))
660 || ((dest == Double.TYPE) && (source == Double.class))
661 || ((dest == Float.TYPE) && (source == Float.class))
662 || ((dest == Integer.TYPE) && (source == Integer.class))
663 || ((dest == Long.TYPE) && (source == Long.class))
664 || ((dest == Short.TYPE) && (source == Short.class))) {
665 return (true);
666 } else {
667 return (false);
668 }
669 }
670 }