View Javadoc
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.config;
22  
23  import java.lang.reflect.InvocationTargetException;
24  import java.util.HashMap;
25  
26  import org.apache.commons.beanutils.BeanUtils;
27  import org.apache.commons.beanutils.DynaBean;
28  import org.apache.commons.beanutils.MutableDynaClass;
29  import org.apache.struts.action.ActionForm;
30  import org.apache.struts.action.ActionServlet;
31  import org.apache.struts.action.DynaActionForm;
32  import org.apache.struts.action.DynaActionFormClass;
33  import org.apache.struts.chain.commands.util.ClassUtils;
34  import org.apache.struts.chain.contexts.ActionContext;
35  import org.apache.struts.chain.contexts.ServletActionContext;
36  import org.apache.struts.util.RequestUtils;
37  import org.apache.struts.validator.BeanValidatorForm;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  /**
42   * <p>A JavaBean representing the configuration information of a
43   * <code>&lt;form-bean&gt;</code> element in a Struts configuration file.<p>
44   *
45   * @version $Rev$ $Date: 2006-01-17 07:26:20 -0500 (Tue, 17 Jan 2006)
46   *          $
47   * @since Struts 1.1
48   */
49  public class FormBeanConfig extends BaseConfig {
50      private static final long serialVersionUID = -2606605006051449892L;
51  
52      /**
53       * The {@code Log} instance for this class.
54       */
55      private transient final Logger log =
56          LoggerFactory.getLogger(FormBeanConfig.class);
57  
58      // ----------------------------------------------------- Instance Variables
59  
60      /**
61       * The set of FormProperty elements defining dynamic form properties for
62       * this form bean, keyed by property name.
63       */
64      protected HashMap<String, FormPropertyConfig> formProperties = new HashMap<>();
65  
66      /**
67       * <p>The lockable object we can synchronize on when creating
68       * DynaActionFormClass.</p>
69       */
70      protected String lock = "";
71  
72      // ------------------------------------------------------------- Properties
73  
74      /**
75       * The DynaActionFormClass associated with a DynaActionForm.
76       */
77      protected transient DynaActionFormClass dynaActionFormClass;
78  
79      /**
80       * Is the form bean class an instance of DynaActionForm with dynamic
81       * properties?
82       */
83      protected boolean dynamic = false;
84  
85      /**
86       * The name of the FormBeanConfig that this config inherits configuration
87       * information from.
88       */
89      protected String inherit = null;
90  
91      /**
92       * Have the inheritance values for this class been applied?
93       */
94      protected boolean extensionProcessed = false;
95  
96      /**
97       * The unique identifier of this form bean, which is used to reference
98       * this bean in <code>ActionMapping</code> instances as well as for the
99       * name of the request or session attribute under which the corresponding
100      * form bean instance is created or accessed.
101      */
102     protected String name = null;
103 
104     /**
105      * The fully qualified Java class name of the implementation class to be
106      * used or generated.
107      */
108     protected String type = null;
109 
110     /**
111      * Is this DynaClass currently restricted (for DynaBeans with a
112      * MutableDynaClass).
113      */
114     protected boolean restricted = false;
115 
116     /**
117      * <p>Return the DynaActionFormClass associated with a
118      * DynaActionForm.</p>
119      *
120      * @throws IllegalArgumentException if the ActionForm is not dynamic
121      */
122     public DynaActionFormClass getDynaActionFormClass() {
123         if (dynamic == false) {
124             throw new IllegalArgumentException("ActionForm is not dynamic");
125         }
126 
127         synchronized (lock) {
128             if (dynaActionFormClass == null) {
129                 dynaActionFormClass = new DynaActionFormClass(this);
130             }
131         }
132 
133         return dynaActionFormClass;
134     }
135 
136     public boolean getDynamic() {
137         return (this.dynamic);
138     }
139 
140     public String getExtends() {
141         return (this.inherit);
142     }
143 
144     public void setExtends(String extend) {
145         throwIfConfigured();
146         this.inherit = extend;
147     }
148 
149     public boolean isExtensionProcessed() {
150         return extensionProcessed;
151     }
152 
153     public String getName() {
154         return (this.name);
155     }
156 
157     public void setName(String name) {
158         throwIfConfigured();
159         this.name = name;
160     }
161 
162     public String getType() {
163         return (this.type);
164     }
165 
166     public void setType(String type) {
167         throwIfConfigured();
168         this.type = type;
169 
170         Class<DynaActionForm> dynaBeanClass = DynaActionForm.class;
171         Class<?> formBeanClass = formBeanClass();
172 
173         if (formBeanClass != null) {
174             if (dynaBeanClass.isAssignableFrom(formBeanClass)) {
175                 this.dynamic = true;
176             } else {
177                 this.dynamic = false;
178             }
179         } else {
180             this.dynamic = false;
181         }
182     }
183 
184     /**
185      * <p>Indicates whether a MutableDynaClass is currently restricted.</p>
186      * <p>If so, no changes to the existing registration of property names,
187      * data types, readability, or writeability are allowed.</p>
188      */
189     public boolean isRestricted() {
190         return restricted;
191     }
192 
193     /**
194      * <p>Set whether a MutableDynaClass is currently restricted.</p> <p>If
195      * so, no changes to the existing registration of property names, data
196      * types, readability, or writeability are allowed.</p>
197      */
198     public void setRestricted(boolean restricted) {
199         this.restricted = restricted;
200     }
201 
202     // ------------------------------------------------------ Protected Methods
203 
204     /**
205      * <p>Traces the hierarchy of this object to check if any of the ancestors
206      * is extending this instance.</p>
207      *
208      * @param moduleConfig The configuration for the module being configured.
209      * @return true if circular inheritance was detected.
210      */
211     protected boolean checkCircularInheritance(ModuleConfig moduleConfig) {
212         String ancestorName = getExtends();
213 
214         while (ancestorName != null) {
215             // check if we have the same name as an ancestor
216             if (getName().equals(ancestorName)) {
217                 return true;
218             }
219 
220             // get our ancestor's ancestor
221             FormBeanConfig ancestor =
222                 moduleConfig.findFormBeanConfig(ancestorName);
223 
224             ancestorName = ancestor.getExtends();
225         }
226 
227         return false;
228     }
229 
230     /**
231      * <p>Compare the form properties of this bean with that of the given and
232      * copy those that are not present.</p>
233      *
234      * @param config The form bean config to copy properties from.
235      * @see #inheritFrom(FormBeanConfig)
236      */
237     protected void inheritFormProperties(FormBeanConfig config)
238         throws ClassNotFoundException, IllegalAccessException,
239             InstantiationException, InvocationTargetException {
240         throwIfConfigured();
241 
242         // Inherit form property configs
243         FormPropertyConfig[] baseFpcs = config.findFormPropertyConfigs();
244 
245         for (int i = 0; i < baseFpcs.length; i++) {
246             FormPropertyConfig baseFpc = baseFpcs[i];
247 
248             // Do we have this prop?
249             FormPropertyConfig prop =
250                 this.findFormPropertyConfig(baseFpc.getName());
251 
252             if (prop == null) {
253                 // We don't have this, so let's copy it
254                 prop =
255                     (FormPropertyConfig) RequestUtils.applicationInstance(baseFpc.getClass()
256                                                                                  .getName());
257 
258                 BeanUtils.copyProperties(prop, baseFpc);
259                 this.addFormPropertyConfig(prop);
260                 prop.setProperties(baseFpc.copyProperties());
261             }
262         }
263     }
264 
265     // --------------------------------------------------------- Public Methods
266 
267     /**
268      * Create and return an {@code ActionForm} instance appropriate to
269      * the information in this {@code FormBeanConfig}.
270      *
271      * <p>Although this method is not formally deprecated yet, where possible,
272      * the form which accepts an {@code ActionContext} as an argument is
273      * preferred, to help sever direct dependencies on the Servlet API.  As
274      * the ActionContext becomes more familiar in Struts, this method will
275      * almost certainly be deprecated.</p>
276      *
277      * @param servlet The action servlet
278      *
279      * @return ActionForm instance
280      *
281      * @throws InstantiationException if this Class represents an abstract
282      *                                class, an array class, a primitive type,
283      *                                or void; or if instantiation fails for
284      *                                some other reason
285      * @throws IllegalAccessException if the Class or the appropriate
286      *                                constructor is not accessible
287      * @throws IllegalArgumentException if the number of actual and formal
288      *                                  parameters differ
289      * @throws InvocationTargetException if the underlying constructor
290      *                                   throws an exception
291      * @throws NoSuchMethodException if a matching method is not found
292      * @throws SecurityException if there is a security-exception
293      * @throws ClassNotFoundException if the specified class cannot be loaded
294      */
295     public ActionForm createActionForm(ActionServlet servlet)
296         throws InstantiationException, IllegalAccessException,
297             IllegalArgumentException, InvocationTargetException,
298             NoSuchMethodException, SecurityException, ClassNotFoundException {
299 
300         Object obj = null;
301 
302         // Create a new form bean instance
303         if (getDynamic()) {
304             obj = getDynaActionFormClass().newInstance();
305         } else {
306             obj = formBeanClass().getDeclaredConstructor().newInstance();
307         }
308 
309         ActionForm form = null;
310 
311         if (obj instanceof ActionForm) {
312             form = (ActionForm) obj;
313         } else {
314             form = new BeanValidatorForm(obj);
315         }
316 
317         form.setServlet(servlet);
318 
319         if (form instanceof DynaBean
320             && ((DynaBean) form).getDynaClass() instanceof MutableDynaClass) {
321             DynaBean dynaBean = (DynaBean) form;
322             MutableDynaClass dynaClass =
323                 (MutableDynaClass) dynaBean.getDynaClass();
324 
325             // Add properties
326             dynaClass.setRestricted(false);
327 
328             FormPropertyConfig[] props = findFormPropertyConfigs();
329 
330             for (int i = 0; i < props.length; i++) {
331                 dynaClass.add(props[i].getName(), props[i].getTypeClass());
332                 dynaBean.set(props[i].getName(), props[i].initial());
333             }
334 
335             dynaClass.setRestricted(isRestricted());
336         }
337 
338         if (form instanceof BeanValidatorForm) {
339             ((BeanValidatorForm)form).initialize(this);
340         }
341 
342         return form;
343     }
344 
345     /**
346      * Create and return an {@code ActionForm} instance appropriate to
347      * the information in this {@code FormBeanConfig}.
348      *
349      * <p><b>NOTE:</b> If the given {@code ActionContext} is not of type
350      * {@code ServletActionContext} (or a subclass), then the form which
351      * is returned will have a null {@code servlet} property.  Some of
352      * the subclasses of {@code ActionForm} included in Struts will later
353      * throw a {@code NullPointerException} in this case.</p>
354      *
355      * <p>TODO: Find a way to control this direct dependency on the
356      * Servlet API.</p>
357      *
358      * @param context The ActionContext.
359      *
360      * @return ActionForm instance
361      *
362      * @throws InstantiationException if this Class represents an abstract
363      *                                class, an array class, a primitive type,
364      *                                or void; or if instantiation fails for
365      *                                some other reason
366      * @throws IllegalAccessException if the Class or the appropriate
367      *                                constructor is not accessible
368      * @throws IllegalArgumentException if the number of actual and formal
369      *                                  parameters differ
370      * @throws InvocationTargetException if the underlying constructor
371      *                                   throws an exception
372      * @throws NoSuchMethodException if a matching method is not found
373      * @throws SecurityException if there is a security-exception
374      * @throws ClassNotFoundException if the specified class cannot be loaded
375      */
376     public ActionForm createActionForm(ActionContext context)
377         throws InstantiationException, IllegalAccessException,
378             IllegalArgumentException, InvocationTargetException,
379             NoSuchMethodException, SecurityException, ClassNotFoundException {
380 
381         ActionServlet actionServlet = null;
382 
383         if (context instanceof ServletActionContext) {
384             ServletActionContext saContext = (ServletActionContext) context;
385 
386             actionServlet = saContext.getActionServlet();
387         }
388 
389         return createActionForm(actionServlet);
390     }
391 
392     /**
393      * <p>Checks if the given <code>ActionForm</code> instance is suitable for
394      * use as an alternative to calling this <code>FormBeanConfig</code>
395      * instance's <code>createActionForm</code> method.</p>
396      *
397      * @param form an existing form instance that may be reused.
398      * @return true if the given form can be reused as the form for this
399      *         config.
400      */
401     public boolean canReuse(ActionForm form) {
402         if (form != null) {
403             if (this.getDynamic()) {
404                 String className = ((DynaBean) form).getDynaClass().getName();
405 
406                 if (className.equals(this.getName())) {
407                     log.debug("Can reuse existing instance (dynamic)");
408 
409                     return (true);
410                 }
411             } else {
412                 try {
413                     // check if the form's class is compatible with the class
414                     //      we're configured for
415                     Class<?> formClass = form.getClass();
416 
417                     if (form instanceof BeanValidatorForm) {
418                         BeanValidatorForm beanValidatorForm =
419                             (BeanValidatorForm) form;
420 
421                         if (beanValidatorForm.getInstance() instanceof DynaBean) {
422                             String formName = beanValidatorForm.getStrutsConfigFormName();
423                             if (getName().equals(formName)) {
424                                 log.debug("Can reuse existing instance (BeanValidatorForm)");
425                                 return true;
426                             } else {
427                                 return false;
428                             }
429                         }
430                         formClass = beanValidatorForm.getInstance().getClass();
431                     }
432 
433                     Class<?> configClass =
434                         ClassUtils.getApplicationClass(this.getType());
435 
436                     if (configClass.isAssignableFrom(formClass)) {
437                         log.debug("Can reuse existing instance (non-dynamic)");
438 
439                         return (true);
440                     }
441                 } catch (Exception e) {
442                     log.debug("Error testing existing instance for reusability; just create a new instance",
443                         e);
444                 }
445             }
446         }
447 
448         return false;
449     }
450 
451     /**
452      * Add a new <code>FormPropertyConfig</code> instance to the set
453      * associated with this module.
454      *
455      * @param config The new configuration instance to be added
456      * @throws IllegalArgumentException if this property name has already been
457      *                                  defined
458      */
459     public void addFormPropertyConfig(FormPropertyConfig config) {
460         throwIfConfigured();
461 
462         if (formProperties.containsKey(config.getName())) {
463             throw new IllegalArgumentException("Property " + config.getName()
464                 + " already defined");
465         }
466 
467         formProperties.put(config.getName(), config);
468     }
469 
470     /**
471      * Return the form property configuration for the specified property name,
472      * if any; otherwise return <code>null</code>.
473      *
474      * @param name Form property name to find a configuration for
475      */
476     public FormPropertyConfig findFormPropertyConfig(String name) {
477         return (formProperties.get(name));
478     }
479 
480     /**
481      * Return the form property configurations for this module.  If there are
482      * none, a zero-length array is returned.
483      */
484     public FormPropertyConfig[] findFormPropertyConfigs() {
485         FormPropertyConfig[] results =
486             new FormPropertyConfig[formProperties.size()];
487 
488         return (formProperties.values().toArray(results));
489     }
490 
491     /**
492      * Freeze the configuration of this component.
493      */
494     public void freeze() {
495         super.freeze();
496 
497         FormPropertyConfig[] fpconfigs = findFormPropertyConfigs();
498 
499         for (int i = 0; i < fpconfigs.length; i++) {
500             fpconfigs[i].freeze();
501         }
502     }
503 
504     /**
505      * <p>Inherit values that have not been overridden from the provided
506      * config object.  Subclasses overriding this method should verify that
507      * the given parameter is of a class that contains a property it is trying
508      * to inherit:</p>
509      *
510      * <pre>
511      * if (config instanceof MyCustomConfig) {
512      *     MyCustomConfig myConfig =
513      *         (MyCustomConfig) config;
514      *
515      *     if (getMyCustomProp() == null) {
516      *         setMyCustomProp(myConfig.getMyCustomProp());
517      *     }
518      * }
519      * </pre>
520      *
521      * <p>If the given <code>config</code> is extending another object, those
522      * extensions should be resolved before it's used as a parameter to this
523      * method.</p>
524      *
525      * @param config The object that this instance will be inheriting its
526      *               values from.
527      * @see #processExtends(ModuleConfig)
528      */
529     public void inheritFrom(FormBeanConfig config)
530         throws ClassNotFoundException, IllegalAccessException,
531             InstantiationException, InvocationTargetException {
532         throwIfConfigured();
533 
534         // Inherit values that have not been overridden
535         if (getName() == null) {
536             setName(config.getName());
537         }
538 
539         if (!isRestricted()) {
540             setRestricted(config.isRestricted());
541         }
542 
543         if (getType() == null) {
544             setType(config.getType());
545         }
546 
547         inheritFormProperties(config);
548         inheritProperties(config);
549     }
550 
551     /**
552      * <p>Inherit configuration information from the FormBeanConfig that this
553      * instance is extending.  This method verifies that any form bean config
554      * object that it inherits from has also had its processExtends() method
555      * called.</p>
556      *
557      * @param moduleConfig The {@link ModuleConfig} that this bean is from.
558      * @see #inheritFrom(FormBeanConfig)
559      */
560     public void processExtends(ModuleConfig moduleConfig)
561         throws ClassNotFoundException, IllegalAccessException,
562             InstantiationException, InvocationTargetException {
563         if (configured) {
564             throw new IllegalStateException("Configuration is frozen");
565         }
566 
567         String ancestor = getExtends();
568 
569         if ((!extensionProcessed) && (ancestor != null)) {
570             FormBeanConfig baseConfig =
571                 moduleConfig.findFormBeanConfig(ancestor);
572 
573             if (baseConfig == null) {
574                 throw new NullPointerException("Unable to find "
575                     + "form bean '" + ancestor + "' to extend.");
576             }
577 
578             // Check against circule inheritance and make sure the base config's
579             //  own extends have been processed already
580             if (checkCircularInheritance(moduleConfig)) {
581                 throw new IllegalArgumentException(
582                     "Circular inheritance detected for form bean " + getName());
583             }
584 
585             // Make sure the ancestor's own extension has been processed.
586             if (!baseConfig.isExtensionProcessed()) {
587                 baseConfig.processExtends(moduleConfig);
588             }
589 
590             // Copy values from the base config
591             inheritFrom(baseConfig);
592         }
593 
594         extensionProcessed = true;
595     }
596 
597     /**
598      * Remove the specified form property configuration instance.
599      *
600      * @param config FormPropertyConfig instance to be removed
601      */
602     public void removeFormPropertyConfig(FormPropertyConfig config) {
603         if (configured) {
604             throw new IllegalStateException("Configuration is frozen");
605         }
606 
607         formProperties.remove(config.getName());
608     }
609 
610     /**
611      * Return a String representation of this object.
612      */
613     public String toString() {
614         StringBuilder sb = new StringBuilder("FormBeanConfig[");
615 
616         sb.append("name=");
617         sb.append(this.name);
618         sb.append(",type=");
619         sb.append(this.type);
620         sb.append(",extends=");
621         sb.append(this.inherit);
622         sb.append("]");
623 
624         return (sb.toString());
625     }
626 
627     // ------------------------------------------------------ Protected Methods
628 
629     /**
630      * Return the <code>Class</code> instance for the form bean implementation
631      * configured by this <code>FormBeanConfig</code> instance.  This method
632      * uses the same algorithm as <code>RequestUtils.applicationClass()</code>
633      * but is reproduced to avoid a runtime dependence.
634      */
635     protected Class<?> formBeanClass() {
636         ClassLoader classLoader =
637             Thread.currentThread().getContextClassLoader();
638 
639         if (classLoader == null) {
640             classLoader = this.getClass().getClassLoader();
641         }
642 
643         try {
644             return (classLoader.loadClass(getType()));
645         } catch (Exception e) {
646             return (null);
647         }
648     }
649 }