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