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><form-bean></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 }