001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.commons.configuration.beanutils;
018    
019    import java.util.HashMap;
020    import java.util.Iterator;
021    import java.util.Map;
022    
023    import org.apache.commons.configuration.HierarchicalConfiguration;
024    import org.apache.commons.configuration.PropertyConverter;
025    import org.apache.commons.configuration.SubnodeConfiguration;
026    import org.apache.commons.configuration.tree.ConfigurationNode;
027    import org.apache.commons.configuration.tree.DefaultConfigurationNode;
028    
029    /**
030     * <p>
031     * An implementation of the <code>BeanDeclaration</code> interface that is
032     * suitable for XML configuration files.
033     * </p>
034     * <p>
035     * This class defines the standard layout of a bean declaration in an XML
036     * configuration file. Such a declaration must look like the following example
037     * fragement:
038     * </p>
039     * <p>
040     *
041     * <pre>
042     *   ...
043     *   &lt;personBean config-class=&quot;my.model.PersonBean&quot;
044     *       lastName=&quot;Doe&quot; firstName=&quot;John&quot;&gt;
045     *       &lt;address config-class=&quot;my.model.AddressBean&quot;
046     *           street=&quot;21st street 11&quot; zip=&quot;1234&quot;
047     *           city=&quot;TestCity&quot;/&gt;
048     *   &lt;/personBean&gt;
049     * </pre>
050     *
051     * </p>
052     * <p>
053     * The bean declaration can be contained in an arbitrary element. Here it is the
054     * <code>&lt;personBean&gt;</code> element. In the attributes of this element
055     * there can occur some reserved attributes, which have the following meaning:
056     * <dl>
057     * <dt><code>config-class</code></dt>
058     * <dd>Here the full qualified name of the bean's class can be specified. An
059     * instance of this class will be created. If this attribute is not specified,
060     * the bean class must be provided in another way, e.g. as the
061     * <code>defaultClass</code> passed to the <code>BeanHelper</code> class.</dd>
062     * <dt><code>config-factory</code></dt>
063     * <dd>This attribute can contain the name of the
064     * <code>{@link BeanFactory}</code> that should be used for creating the bean.
065     * If it is defined, a factory with this name must have been registered at the
066     * <code>BeanHelper</code> class. If this attribute is missing, the default
067     * bean factory will be used.</dd>
068     * <dt><code>config-factoryParam</code></dt>
069     * <dd>With this attribute a parameter can be specified that will be passed to
070     * the bean factory. This may be useful for custom bean factories.</dd>
071     * </dl>
072     * </p>
073     * <p>
074     * All further attributes starting with the <code>config-</code> prefix are
075     * considered as meta data and will be ignored. All other attributes are treated
076     * as properties of the bean to be created, i.e. corresponding setter methods of
077     * the bean will be invoked with the values specified here.
078     * </p>
079     * <p>
080     * If the bean to be created has also some complex properties (which are itself
081     * beans), their values cannot be initialized from attributes. For this purpose
082     * nested elements can be used. The example listing shows how an address bean
083     * can be initialized. This is done in a nested element whose name must match
084     * the name of a property of the enclosing bean declaration. The format of this
085     * nested element is exactly the same as for the bean declaration itself, i.e.
086     * it can have attributes defining meta data or bean properties and even further
087     * nested elements for complex bean properties.
088     * </p>
089     * <p>
090     * A <code>XMLBeanDeclaration</code> object is usually created from a
091     * <code>HierarchicalConfiguration</code>. From this it will derive a
092     * <code>SubnodeConfiguration</code>, which is used to access the needed
093     * properties. This subnode configuration can be obtained using the
094     * <code>{@link #getConfiguration()}</code> method. All of its properties can
095     * be accessed in the usual way. To ensure that the property keys used by this
096     * class are understood by the configuration, the default expression engine will
097     * be set.
098     * </p>
099     *
100     * @since 1.3
101     * @author Oliver Heger
102     * @version $Id: XMLBeanDeclaration.java 670739 2008-06-23 20:36:37Z oheger $
103     */
104    public class XMLBeanDeclaration implements BeanDeclaration
105    {
106        /** Constant for the prefix of reserved attributes. */
107        public static final String RESERVED_PREFIX = "config-";
108    
109        /** Constant for the prefix for reserved attributes.*/
110        public static final String ATTR_PREFIX = "[@" + RESERVED_PREFIX;
111    
112        /** Constant for the bean class attribute. */
113        public static final String ATTR_BEAN_CLASS = ATTR_PREFIX + "class]";
114    
115        /** Constant for the bean factory attribute. */
116        public static final String ATTR_BEAN_FACTORY = ATTR_PREFIX + "factory]";
117    
118        /** Constant for the bean factory parameter attribute. */
119        public static final String ATTR_FACTORY_PARAM = ATTR_PREFIX
120                + "factoryParam]";
121    
122        /** Stores the associated configuration. */
123        private SubnodeConfiguration configuration;
124    
125        /** Stores the configuration node that contains the bean declaration. */
126        private ConfigurationNode node;
127    
128        /**
129         * Creates a new instance of <code>XMLBeanDeclaration</code> and
130         * initializes it from the given configuration. The passed in key points to
131         * the bean declaration.
132         *
133         * @param config the configuration
134         * @param key the key to the bean declaration (this key must point to
135         * exactly one bean declaration or a <code>IllegalArgumentException</code>
136         * exception will be thrown)
137         */
138        public XMLBeanDeclaration(HierarchicalConfiguration config, String key)
139        {
140            this(config, key, false);
141        }
142    
143        /**
144         * Creates a new instance of <code>XMLBeanDeclaration</code> and
145         * initializes it from the given configuration. The passed in key points to
146         * the bean declaration. If the key does not exist and the boolean argument
147         * is <b>true</b>, the declaration is initialized with an empty
148         * configuration. It is possible to create objects from such an empty
149         * declaration if a default class is provided. If the key on the other hand
150         * has multiple values or is undefined and the boolean argument is <b>false</b>,
151         * a <code>IllegalArgumentException</code> exception will be thrown.
152         *
153         * @param config the configuration
154         * @param key the key to the bean declaration
155         * @param optional a flag whether this declaration is optional; if set to
156         * <b>true</b>, no exception will be thrown if the passed in key is
157         * undefined
158         */
159        public XMLBeanDeclaration(HierarchicalConfiguration config, String key,
160                boolean optional)
161        {
162            if (config == null)
163            {
164                throw new IllegalArgumentException(
165                        "Configuration must not be null!");
166            }
167    
168            try
169            {
170                configuration = config.configurationAt(key);
171                node = configuration.getRootNode();
172            }
173            catch (IllegalArgumentException iex)
174            {
175                // If we reach this block, the key does not have exactly one value
176                if (!optional || config.getMaxIndex(key) > 0)
177                {
178                    throw iex;
179                }
180                configuration = config.configurationAt(null);
181                node = new DefaultConfigurationNode();
182            }
183            initSubnodeConfiguration(getConfiguration());
184        }
185    
186        /**
187         * Creates a new instance of <code>XMLBeanDeclaration</code> and
188         * initializes it from the given configuration. The configuration's root
189         * node must contain the bean declaration.
190         *
191         * @param config the configuration with the bean declaration
192         */
193        public XMLBeanDeclaration(HierarchicalConfiguration config)
194        {
195            this(config, (String) null);
196        }
197    
198        /**
199         * Creates a new instance of <code>XMLBeanDeclaration</code> and
200         * initializes it with the configuration node that contains the bean
201         * declaration.
202         *
203         * @param config the configuration
204         * @param node the node with the bean declaration.
205         */
206        public XMLBeanDeclaration(SubnodeConfiguration config,
207                ConfigurationNode node)
208        {
209            if (config == null)
210            {
211                throw new IllegalArgumentException(
212                        "Configuration must not be null!");
213            }
214            if (node == null)
215            {
216                throw new IllegalArgumentException("Node must not be null!");
217            }
218    
219            this.node = node;
220            configuration = config;
221            initSubnodeConfiguration(config);
222        }
223    
224        /**
225         * Returns the configuration object this bean declaration is based on.
226         *
227         * @return the associated configuration
228         */
229        public SubnodeConfiguration getConfiguration()
230        {
231            return configuration;
232        }
233    
234        /**
235         * Returns the node that contains the bean declaration.
236         *
237         * @return the configuration node this bean declaration is based on
238         */
239        public ConfigurationNode getNode()
240        {
241            return node;
242        }
243    
244        /**
245         * Returns the name of the bean factory. This information is fetched from
246         * the <code>config-factory</code> attribute.
247         *
248         * @return the name of the bean factory
249         */
250        public String getBeanFactoryName()
251        {
252            return getConfiguration().getString(ATTR_BEAN_FACTORY);
253        }
254    
255        /**
256         * Returns a parameter for the bean factory. This information is fetched
257         * from the <code>config-factoryParam</code> attribute.
258         *
259         * @return the parameter for the bean factory
260         */
261        public Object getBeanFactoryParameter()
262        {
263            return getConfiguration().getProperty(ATTR_FACTORY_PARAM);
264        }
265    
266        /**
267         * Returns the name of the class of the bean to be created. This information
268         * is obtained from the <code>config-class</code> attribute.
269         *
270         * @return the name of the bean's class
271         */
272        public String getBeanClassName()
273        {
274            return getConfiguration().getString(ATTR_BEAN_CLASS);
275        }
276    
277        /**
278         * Returns a map with the bean's (simple) properties. The properties are
279         * collected from all attribute nodes, which are not reserved.
280         *
281         * @return a map with the bean's properties
282         */
283        public Map getBeanProperties()
284        {
285            Map props = new HashMap();
286            for (Iterator it = getNode().getAttributes().iterator(); it.hasNext();)
287            {
288                ConfigurationNode attr = (ConfigurationNode) it.next();
289                if (!isReservedNode(attr))
290                {
291                    props.put(attr.getName(), interpolate(attr .getValue()));
292                }
293            }
294    
295            return props;
296        }
297    
298        /**
299         * Returns a map with bean declarations for the complex properties of the
300         * bean to be created. These declarations are obtained from the child nodes
301         * of this declaration's root node.
302         *
303         * @return a map with bean declarations for complex properties
304         */
305        public Map getNestedBeanDeclarations()
306        {
307            Map nested = new HashMap();
308            for (Iterator it = getNode().getChildren().iterator(); it.hasNext();)
309            {
310                ConfigurationNode child = (ConfigurationNode) it.next();
311                if (!isReservedNode(child))
312                {
313                    nested.put(child.getName(), createBeanDeclaration(child));
314                }
315            }
316    
317            return nested;
318        }
319    
320        /**
321         * Performs interpolation for the specified value. This implementation will
322         * interpolate against the current subnode configuration's parent. If sub
323         * classes need a different interpolation mechanism, they should override
324         * this method.
325         *
326         * @param value the value that is to be interpolated
327         * @return the interpolated value
328         */
329        protected Object interpolate(Object value)
330        {
331            return PropertyConverter.interpolate(value, getConfiguration()
332                    .getParent());
333        }
334    
335        /**
336         * Checks if the specified node is reserved and thus should be ignored. This
337         * method is called when the maps for the bean's properties and complex
338         * properties are collected. It checks whether the given node is an
339         * attribute node and if its name starts with the reserved prefix.
340         *
341         * @param nd the node to be checked
342         * @return a flag whether this node is reserved (and does not point to a
343         * property)
344         */
345        protected boolean isReservedNode(ConfigurationNode nd)
346        {
347            return nd.isAttribute()
348                    && (nd.getName() == null || nd.getName().startsWith(
349                            RESERVED_PREFIX));
350        }
351    
352        /**
353         * Creates a new <code>BeanDeclaration</code> for a child node of the
354         * current configuration node. This method is called by
355         * <code>getNestedBeanDeclarations()</code> for all complex sub properties
356         * detected by this method. Derived classes can hook in if they need a
357         * specific initialization. This base implementation creates a
358         * <code>XMLBeanDeclaration</code> that is properly initialized from the
359         * passed in node.
360         *
361         * @param node the child node, for which a <code>BeanDeclaration</code> is
362         *        to be created
363         * @return the <code>BeanDeclaration</code> for this child node
364         * @since 1.6
365         */
366        protected BeanDeclaration createBeanDeclaration(ConfigurationNode node)
367        {
368            return new XMLBeanDeclaration(getConfiguration().configurationAt(
369                    node.getName()), node);
370        }
371    
372        /**
373         * Initializes the internally managed subnode configuration. This method
374         * will set some default values for some properties.
375         *
376         * @param conf the configuration to initialize
377         */
378        private void initSubnodeConfiguration(SubnodeConfiguration conf)
379        {
380            conf.setThrowExceptionOnMissing(false);
381            conf.setExpressionEngine(null);
382        }
383    }