001: /*
002: * $Id: DynaActionForm.java 471754 2006-11-06 14:55:09Z husted $
003: *
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021: package org.apache.struts.action;
022:
023: import org.apache.commons.beanutils.ConversionException;
024: import org.apache.commons.beanutils.DynaBean;
025: import org.apache.commons.beanutils.DynaClass;
026: import org.apache.commons.beanutils.DynaProperty;
027: import org.apache.struts.config.FormBeanConfig;
028: import org.apache.struts.config.FormPropertyConfig;
029:
030: import javax.servlet.ServletRequest;
031: import javax.servlet.http.HttpServletRequest;
032:
033: import java.lang.reflect.Array;
034:
035: import java.util.HashMap;
036: import java.util.Iterator;
037: import java.util.List;
038: import java.util.Map;
039: import java.util.StringTokenizer;
040:
041: /**
042: * <p>Specialized subclass of <code>ActionForm</code> that allows the creation
043: * of form beans with dynamic sets of properties, without requiring the
044: * developer to create a Java class for each type of form bean.</p>
045: *
046: * <p><strong>USAGE NOTE</strong> - Since Struts 1.1, the <code>reset</code>
047: * method no longer initializes property values to those specified in
048: * <code><form-property></code> elements in the Struts module
049: * configuration file. If you wish to utilize that behavior, the simplest
050: * solution is to subclass <code>DynaActionForm</code> and call the
051: * <code>initialize</code> method inside it.</p>
052: *
053: * @version $Rev: 471754 $ $Date: 2005-11-12 11:52:08 -0500 (Sat, 12 Nov 2005)
054: * $
055: * @since Struts 1.1
056: */
057: public class DynaActionForm extends ActionForm implements DynaBean {
058: // ----------------------------------------------------- Instance Variables
059:
060: /**
061: * <p>The <code>DynaActionFormClass</code> with which we are associated.
062: * </p>
063: */
064: protected DynaActionFormClass dynaClass = null;
065:
066: /**
067: * <p>The set of property values for this <code>DynaActionForm</code>,
068: * keyed by property name.</p>
069: */
070: protected HashMap dynaValues = new HashMap();
071:
072: // ----------------------------------------------------- ActionForm Methods
073:
074: /**
075: * <p>Initialize all bean properties to their initial values, as specified
076: * in the {@link FormPropertyConfig} elements associated with the
077: * definition of this <code>DynaActionForm</code>.</p>
078: *
079: * @param mapping The mapping used to select this instance
080: */
081: public void initialize(ActionMapping mapping) {
082: String name = mapping.getName();
083:
084: if (name == null) {
085: return;
086: }
087:
088: FormBeanConfig config = mapping.getModuleConfig()
089: .findFormBeanConfig(name);
090:
091: if (config == null) {
092: return;
093: }
094:
095: initialize(config);
096: }
097:
098: /**
099: * <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 = mapping.getModuleConfig()
145: .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 = new StringTokenizer(resetValue,
168: ", \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 '"
208: + name + "(" + key + ")'");
209: } else if (value instanceof Map) {
210: return (((Map) value).containsKey(key));
211: } else {
212: throw new IllegalArgumentException(
213: "Non-mapped property for '" + name + "(" + key
214: + ")'");
215: }
216: }
217:
218: /**
219: * <p>Return the value of a simple property with the specified name.</p>
220: *
221: * @param name Name of the property whose value is to be retrieved
222: * @return The value of a simple property with the specified name.
223: * @throws IllegalArgumentException if there is no property of the
224: * specified name
225: * @throws NullPointerException if the type specified for the property
226: * is invalid
227: */
228: public Object get(String name) {
229: // Return any non-null value for the specified property
230: Object value = dynaValues.get(name);
231:
232: if (value != null) {
233: return (value);
234: }
235:
236: // Return a null value for a non-primitive property
237: Class type = getDynaProperty(name).getType();
238:
239: if (type == null) {
240: throw new NullPointerException("The type for property "
241: + name + " is invalid");
242: }
243:
244: if (!type.isPrimitive()) {
245: return (value);
246: }
247:
248: // Manufacture default values for primitive properties
249: if (type == Boolean.TYPE) {
250: return (Boolean.FALSE);
251: } else if (type == Byte.TYPE) {
252: return (new Byte((byte) 0));
253: } else if (type == Character.TYPE) {
254: return (new Character((char) 0));
255: } else if (type == Double.TYPE) {
256: return (new Double(0.0));
257: } else if (type == Float.TYPE) {
258: return (new Float((float) 0.0));
259: } else if (type == Integer.TYPE) {
260: return (new Integer(0));
261: } else if (type == Long.TYPE) {
262: return (new Long(0));
263: } else if (type == Short.TYPE) {
264: return (new Short((short) 0));
265: } else {
266: return (null);
267: }
268: }
269:
270: /**
271: * <p>Return the value of an indexed property with the specified name.
272: * </p>
273: *
274: * @param name Name of the property whose value is to be retrieved
275: * @param index Index of the value to be retrieved
276: * @return The value of an indexed property with the specified name.
277: * @throws IllegalArgumentException if there is no property of the
278: * specified name
279: * @throws IllegalArgumentException if the specified property exists, but
280: * is not indexed
281: * @throws NullPointerException if no array or List has been
282: * initialized for this property
283: */
284: public Object get(String name, int index) {
285: Object value = dynaValues.get(name);
286:
287: if (value == null) {
288: throw new NullPointerException("No indexed value for '"
289: + name + "[" + index + "]'");
290: } else if (value.getClass().isArray()) {
291: return (Array.get(value, index));
292: } else if (value instanceof List) {
293: return ((List) value).get(index);
294: } else {
295: throw new IllegalArgumentException(
296: "Non-indexed property for '" + name + "[" + index
297: + "]'");
298: }
299: }
300:
301: /**
302: * <p>Return the value of a mapped property with the specified name, or
303: * <code>null</code> if there is no value for the specified key. </p>
304: *
305: * @param name Name of the property whose value is to be retrieved
306: * @param key Key of the value to be retrieved
307: * @return The value of a mapped property with the specified name, or
308: * <code>null</code> if there is no value for the specified key.
309: * @throws NullPointerException if there is no property of the
310: * specified name
311: * @throws IllegalArgumentException if the specified property exists, but
312: * is not mapped
313: */
314: public Object get(String name, String key) {
315: Object value = dynaValues.get(name);
316:
317: if (value == null) {
318: throw new NullPointerException("No mapped value for '"
319: + name + "(" + key + ")'");
320: } else if (value instanceof Map) {
321: return (((Map) value).get(key));
322: } else {
323: throw new IllegalArgumentException(
324: "Non-mapped property for '" + name + "(" + key
325: + ")'");
326: }
327: }
328:
329: /**
330: * <p>Return the value of a <code>String</code> property with the
331: * specified name. This is equivalent to calling <code>(String)
332: * dynaForm.get(name)</code>.</p>
333: *
334: * @param name Name of the property whose value is to be retrieved.
335: * @return The value of a <code>String</code> property with the specified
336: * name.
337: * @throws IllegalArgumentException if there is no property of the
338: * specified name
339: * @throws NullPointerException if the type specified for the property
340: * is invalid
341: * @throws ClassCastException if the property is not a String.
342: * @since Struts 1.2
343: */
344: public String getString(String name) {
345: return (String) this .get(name);
346: }
347:
348: /**
349: * <p>Return the value of a <code>String[]</code> property with the
350: * specified name. This is equivalent to calling <code>(String[])
351: * dynaForm.get(name)</code>.</p>
352: *
353: * @param name Name of the property whose value is to be retrieved.
354: * @return The value of a <code>String[]</code> property with the
355: * specified name.
356: * @throws IllegalArgumentException if there is no property of the
357: * specified name
358: * @throws NullPointerException if the type specified for the property
359: * is invalid
360: * @throws ClassCastException if the property is not a String[].
361: * @since Struts 1.2
362: */
363: public String[] getStrings(String name) {
364: return (String[]) this .get(name);
365: }
366:
367: /**
368: * <p>Return the <code>DynaClass</code> instance that describes the set of
369: * properties available for this <code>DynaBean</code>.</p>
370: *
371: * @return The <code>DynaClass</code> instance that describes the set of
372: * properties available for this <code>DynaBean</code>.
373: */
374: public DynaClass getDynaClass() {
375: return (this .dynaClass);
376: }
377:
378: /**
379: * <p>Returns the <code>Map</code> containing the property values. This is
380: * done mostly to facilitate accessing the <code>DynaActionForm</code>
381: * through JavaBeans accessors, in order to use the JavaServer Pages
382: * Standard Tag Library (JSTL).</p>
383: *
384: * <p>For instance, the normal JSTL EL syntax for accessing an
385: * <code>ActionForm</code> would be something like this:
386: * <pre>
387: * ${formbean.prop}</pre>
388: * The JSTL EL syntax for accessing a <code>DynaActionForm</code> looks
389: * something like this (because of the presence of this
390: * <code>getMap()</code> method):
391: * <pre>
392: * ${dynabean.map.prop}</pre>
393: * </p>
394: *
395: * @return The <code>Map</code> containing the property values.
396: */
397: public Map getMap() {
398: return (dynaValues);
399: }
400:
401: /**
402: * <p>Remove any existing value for the specified key on the specified
403: * mapped property.</p>
404: *
405: * @param name Name of the property for which a value is to be removed
406: * @param key Key of the value to be removed
407: * @throws NullPointerException if there is no property of the
408: * specified name
409: * @throws IllegalArgumentException if there is no mapped property of the
410: * specified name
411: */
412: public void remove(String name, String key) {
413: Object value = dynaValues.get(name);
414:
415: if (value == null) {
416: throw new NullPointerException("No mapped value for '"
417: + name + "(" + key + ")'");
418: } else if (value instanceof Map) {
419: ((Map) value).remove(key);
420: } else {
421: throw new IllegalArgumentException(
422: "Non-mapped property for '" + name + "(" + key
423: + ")'");
424: }
425: }
426:
427: /**
428: * <p>Set the value of a simple property with the specified name.</p>
429: *
430: * @param name Name of the property whose value is to be set
431: * @param value Value to which this property is to be set
432: * @throws ConversionException if the specified value cannot be
433: * converted to the type required for
434: * this property
435: * @throws IllegalArgumentException if there is no property of the
436: * specified name
437: * @throws NullPointerException if the type specified for the property
438: * is invalid
439: * @throws NullPointerException if an attempt is made to set a
440: * primitive property to null
441: */
442: public void set(String name, Object value) {
443: DynaProperty descriptor = getDynaProperty(name);
444:
445: if (descriptor.getType() == null) {
446: throw new NullPointerException("The type for property "
447: + name + " is invalid");
448: }
449:
450: if (value == null) {
451: if (descriptor.getType().isPrimitive()) {
452: throw new NullPointerException("Primitive value for '"
453: + name + "'");
454: }
455: } else if (!isDynaAssignable(descriptor.getType(), value
456: .getClass())) {
457: throw new ConversionException(
458: "Cannot assign value of type '"
459: + value.getClass().getName()
460: + "' to property '" + name + "' of type '"
461: + descriptor.getType().getName() + "'");
462: }
463:
464: dynaValues.put(name, value);
465: }
466:
467: /**
468: * <p>Set the value of an indexed property with the specified name.</p>
469: *
470: * @param name Name of the property whose value is to be set
471: * @param index Index of the property to be set
472: * @param value Value to which this property is to be set
473: * @throws ConversionException if the specified value cannot be
474: * converted to the type required for
475: * this property
476: * @throws NullPointerException if there is no property of the
477: * specified name
478: * @throws IllegalArgumentException if the specified property exists, but
479: * is not indexed
480: * @throws IndexOutOfBoundsException if the specified index is outside the
481: * range of the underlying property
482: */
483: public void set(String name, int index, Object value) {
484: Object prop = dynaValues.get(name);
485:
486: if (prop == null) {
487: throw new NullPointerException("No indexed value for '"
488: + name + "[" + index + "]'");
489: } else if (prop.getClass().isArray()) {
490: Array.set(prop, index, value);
491: } else if (prop instanceof List) {
492: try {
493: ((List) prop).set(index, value);
494: } catch (ClassCastException e) {
495: throw new ConversionException(e.getMessage());
496: }
497: } else {
498: throw new IllegalArgumentException(
499: "Non-indexed property for '" + name + "[" + index
500: + "]'");
501: }
502: }
503:
504: /**
505: * <p>Set the value of a mapped property with the specified name.</p>
506: *
507: * @param name Name of the property whose value is to be set
508: * @param key Key of the property to be set
509: * @param value Value to which this property is to be set
510: * @throws NullPointerException if there is no property of the
511: * specified name
512: * @throws IllegalArgumentException if the specified property exists, but
513: * is not mapped
514: */
515: public void set(String name, String key, Object value) {
516: Object prop = dynaValues.get(name);
517:
518: if (prop == null) {
519: throw new NullPointerException("No mapped value for '"
520: + name + "(" + key + ")'");
521: } else if (prop instanceof Map) {
522: ((Map) prop).put(key, value);
523: } else {
524: throw new IllegalArgumentException(
525: "Non-mapped property for '" + name + "(" + key
526: + ")'");
527: }
528: }
529:
530: // --------------------------------------------------------- Public Methods
531:
532: /**
533: * <p>Render a String representation of this object.</p>
534: *
535: * @return A string representation of this object.
536: */
537: public String toString() {
538: StringBuffer sb = new StringBuffer("DynaActionForm[dynaClass=");
539: DynaClass dynaClass = getDynaClass();
540:
541: if (dynaClass == null) {
542: return sb.append("null]").toString();
543: }
544:
545: sb.append(dynaClass.getName());
546:
547: DynaProperty[] props = dynaClass.getDynaProperties();
548:
549: if (props == null) {
550: props = new DynaProperty[0];
551: }
552:
553: for (int i = 0; i < props.length; i++) {
554: sb.append(',');
555: sb.append(props[i].getName());
556: sb.append('=');
557:
558: Object value = get(props[i].getName());
559:
560: if (value == null) {
561: sb.append("<NULL>");
562: } else if (value.getClass().isArray()) {
563: int n = Array.getLength(value);
564:
565: sb.append("{");
566:
567: for (int j = 0; j < n; j++) {
568: if (j > 0) {
569: sb.append(',');
570: }
571:
572: sb.append(Array.get(value, j));
573: }
574:
575: sb.append("}");
576: } else if (value instanceof List) {
577: int n = ((List) value).size();
578:
579: sb.append("{");
580:
581: for (int j = 0; j < n; j++) {
582: if (j > 0) {
583: sb.append(',');
584: }
585:
586: sb.append(((List) value).get(j));
587: }
588:
589: sb.append("}");
590: } else if (value instanceof Map) {
591: int n = 0;
592: Iterator keys = ((Map) value).keySet().iterator();
593:
594: sb.append("{");
595:
596: while (keys.hasNext()) {
597: if (n > 0) {
598: sb.append(',');
599: }
600:
601: n++;
602:
603: Object key = keys.next();
604:
605: sb.append(key);
606: sb.append('=');
607: sb.append(((Map) value).get(key));
608: }
609:
610: sb.append("}");
611: } else {
612: sb.append(value);
613: }
614: }
615:
616: sb.append("]");
617:
618: return (sb.toString());
619: }
620:
621: // -------------------------------------------------------- Package Methods
622:
623: /**
624: * <p>Set the <code>DynaActionFormClass</code> instance with which we are
625: * associated.</p>
626: *
627: * @param dynaClass The DynaActionFormClass instance for this bean
628: */
629: void setDynaActionFormClass(DynaActionFormClass dynaClass) {
630: this .dynaClass = dynaClass;
631: }
632:
633: // ------------------------------------------------------ Protected Methods
634:
635: /**
636: * <p>Return the property descriptor for the specified property name.</p>
637: *
638: * @param name Name of the property for which to retrieve the descriptor
639: * @return The property descriptor for the specified property name.
640: * @throws IllegalArgumentException if this is not a valid property name
641: * for our DynaClass
642: */
643: protected DynaProperty getDynaProperty(String name) {
644: DynaProperty descriptor = getDynaClass().getDynaProperty(name);
645:
646: if (descriptor == null) {
647: throw new IllegalArgumentException(
648: "Invalid property name '" + name + "'");
649: }
650:
651: return (descriptor);
652: }
653:
654: /**
655: * <p>Indicates if an object of the source class is assignable to the
656: * destination class.</p>
657: *
658: * @param dest Destination class
659: * @param source Source class
660: * @return <code>true</code> if the source is assignable to the
661: * destination; <code>false</code> otherwise.
662: */
663: protected boolean isDynaAssignable(Class dest, Class source) {
664: if (dest.isAssignableFrom(source)
665: || ((dest == Boolean.TYPE) && (source == Boolean.class))
666: || ((dest == Byte.TYPE) && (source == Byte.class))
667: || ((dest == Character.TYPE) && (source == Character.class))
668: || ((dest == Double.TYPE) && (source == Double.class))
669: || ((dest == Float.TYPE) && (source == Float.class))
670: || ((dest == Integer.TYPE) && (source == Integer.class))
671: || ((dest == Long.TYPE) && (source == Long.class))
672: || ((dest == Short.TYPE) && (source == Short.class))) {
673: return (true);
674: } else {
675: return (false);
676: }
677: }
678: }
|