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.ArrayList;
020    import java.util.HashMap;
021    import java.util.Iterator;
022    import java.util.List;
023    import java.util.Map;
024    
025    import org.apache.commons.configuration.ConfigurationRuntimeException;
026    import org.apache.commons.configuration.HierarchicalConfiguration;
027    import org.apache.commons.configuration.PropertyConverter;
028    import org.apache.commons.configuration.SubnodeConfiguration;
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} 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     * fragment:
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 personBean} 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}</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} passed to the {@code BeanHelper} class.</dd>
065     * <dt>{@code config-factory}</dt>
066     * <dd>This attribute can contain the name of the
067     * {@link BeanFactory} 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} class. If this attribute is missing, the default
070     * bean factory will be used.</dd>
071     * <dt>{@code config-factoryParam}</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-} 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} object is usually created from a
094     * {@code HierarchicalConfiguration}. From this it will derive a
095     * {@code SubnodeConfiguration}, which is used to access the needed
096     * properties. This subnode configuration can be obtained using the
097     * {@link #getConfiguration()} 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 <a
105     * href="http://commons.apache.org/configuration/team-list.html">Commons
106     * Configuration team</a>
107     * @version $Id: XMLBeanDeclaration.java 1301959 2012-03-17 16:43:18Z oheger $
108     */
109    public class XMLBeanDeclaration implements BeanDeclaration
110    {
111        /** Constant for the prefix of reserved attributes. */
112        public static final String RESERVED_PREFIX = "config-";
113    
114        /** Constant for the prefix for reserved attributes.*/
115        public static final String ATTR_PREFIX = "[@" + RESERVED_PREFIX;
116    
117        /** Constant for the bean class attribute. */
118        public static final String ATTR_BEAN_CLASS = ATTR_PREFIX + "class]";
119    
120        /** Constant for the bean factory attribute. */
121        public static final String ATTR_BEAN_FACTORY = ATTR_PREFIX + "factory]";
122    
123        /** Constant for the bean factory parameter attribute. */
124        public static final String ATTR_FACTORY_PARAM = ATTR_PREFIX
125                + "factoryParam]";
126    
127        /** Stores the associated configuration. */
128        private final SubnodeConfiguration configuration;
129    
130        /** Stores the configuration node that contains the bean declaration. */
131        private final ConfigurationNode node;
132    
133        /**
134         * Creates a new instance of {@code XMLBeanDeclaration} and
135         * initializes it from the given configuration. The passed in key points to
136         * the bean declaration.
137         *
138         * @param config the configuration
139         * @param key the key to the bean declaration (this key must point to
140         * exactly one bean declaration or a {@code IllegalArgumentException}
141         * exception will be thrown)
142         */
143        public XMLBeanDeclaration(HierarchicalConfiguration config, String key)
144        {
145            this(config, key, false);
146        }
147    
148        /**
149         * Creates a new instance of {@code XMLBeanDeclaration} and
150         * initializes it from the given configuration. The passed in key points to
151         * the bean declaration. If the key does not exist and the boolean argument
152         * is <b>true</b>, the declaration is initialized with an empty
153         * configuration. It is possible to create objects from such an empty
154         * declaration if a default class is provided. If the key on the other hand
155         * has multiple values or is undefined and the boolean argument is <b>false</b>,
156         * a {@code IllegalArgumentException} exception will be thrown.
157         *
158         * @param config the configuration
159         * @param key the key to the bean declaration
160         * @param optional a flag whether this declaration is optional; if set to
161         * <b>true</b>, no exception will be thrown if the passed in key is
162         * undefined
163         */
164        public XMLBeanDeclaration(HierarchicalConfiguration config, String key,
165                boolean optional)
166        {
167            if (config == null)
168            {
169                throw new IllegalArgumentException(
170                        "Configuration must not be null!");
171            }
172    
173            SubnodeConfiguration tmpconfiguration = null;
174            ConfigurationNode tmpnode = null;
175            try
176            {
177                tmpconfiguration = config.configurationAt(key);
178                tmpnode = tmpconfiguration.getRootNode();
179            }
180            catch (IllegalArgumentException iex)
181            {
182                // If we reach this block, the key does not have exactly one value
183                if (!optional || config.getMaxIndex(key) > 0)
184                {
185                    throw iex;
186                }
187                tmpconfiguration = config.configurationAt(null);
188                tmpnode = new DefaultConfigurationNode();
189            }
190            this.node = tmpnode;
191            this.configuration = tmpconfiguration;
192            initSubnodeConfiguration(getConfiguration());
193        }
194    
195        /**
196         * Creates a new instance of {@code XMLBeanDeclaration} and
197         * initializes it from the given configuration. The configuration's root
198         * node must contain the bean declaration.
199         *
200         * @param config the configuration with the bean declaration
201         */
202        public XMLBeanDeclaration(HierarchicalConfiguration config)
203        {
204            this(config, (String) null);
205        }
206    
207        /**
208         * Creates a new instance of {@code XMLBeanDeclaration} and
209         * initializes it with the configuration node that contains the bean
210         * declaration.
211         *
212         * @param config the configuration
213         * @param node the node with the bean declaration.
214         */
215        public XMLBeanDeclaration(SubnodeConfiguration config,
216                ConfigurationNode node)
217        {
218            if (config == null)
219            {
220                throw new IllegalArgumentException(
221                        "Configuration must not be null!");
222            }
223            if (node == null)
224            {
225                throw new IllegalArgumentException("Node must not be null!");
226            }
227    
228            this.node = node;
229            configuration = config;
230            initSubnodeConfiguration(config);
231        }
232    
233        /**
234         * Returns the configuration object this bean declaration is based on.
235         *
236         * @return the associated configuration
237         */
238        public SubnodeConfiguration getConfiguration()
239        {
240            return configuration;
241        }
242    
243        /**
244         * Returns the node that contains the bean declaration.
245         *
246         * @return the configuration node this bean declaration is based on
247         */
248        public ConfigurationNode getNode()
249        {
250            return node;
251        }
252    
253        /**
254         * Returns the name of the bean factory. This information is fetched from
255         * the {@code config-factory} attribute.
256         *
257         * @return the name of the bean factory
258         */
259        public String getBeanFactoryName()
260        {
261            return getConfiguration().getString(ATTR_BEAN_FACTORY);
262        }
263    
264        /**
265         * Returns a parameter for the bean factory. This information is fetched
266         * from the {@code config-factoryParam} attribute.
267         *
268         * @return the parameter for the bean factory
269         */
270        public Object getBeanFactoryParameter()
271        {
272            return getConfiguration().getProperty(ATTR_FACTORY_PARAM);
273        }
274    
275        /**
276         * Returns the name of the class of the bean to be created. This information
277         * is obtained from the {@code config-class} attribute.
278         *
279         * @return the name of the bean's class
280         */
281        public String getBeanClassName()
282        {
283            return getConfiguration().getString(ATTR_BEAN_CLASS);
284        }
285    
286        /**
287         * Returns a map with the bean's (simple) properties. The properties are
288         * collected from all attribute nodes, which are not reserved.
289         *
290         * @return a map with the bean's properties
291         */
292        public Map<String, Object> getBeanProperties()
293        {
294            Map<String, Object> props = new HashMap<String, Object>();
295            for (ConfigurationNode attr : getNode().getAttributes())
296            {
297                if (!isReservedNode(attr))
298                {
299                    props.put(attr.getName(), interpolate(attr .getValue()));
300                }
301            }
302    
303            return props;
304        }
305    
306        /**
307         * Returns a map with bean declarations for the complex properties of the
308         * bean to be created. These declarations are obtained from the child nodes
309         * of this declaration's root node.
310         *
311         * @return a map with bean declarations for complex properties
312         */
313        public Map<String, Object> getNestedBeanDeclarations()
314        {
315            Map<String, Object> nested = new HashMap<String, Object>();
316            for (ConfigurationNode child : getNode().getChildren())
317            {
318                if (!isReservedNode(child))
319                {
320                    if (nested.containsKey(child.getName()))
321                    {
322                        Object obj = nested.get(child.getName());
323                        List<BeanDeclaration> list;
324                        if (obj instanceof List)
325                        {
326                            // Safe because we created the lists ourselves.
327                            @SuppressWarnings("unchecked")
328                            List<BeanDeclaration> tmpList = (List<BeanDeclaration>) obj;
329                            list = tmpList;
330                        }
331                        else
332                        {
333                            list = new ArrayList<BeanDeclaration>();
334                            list.add((BeanDeclaration) obj);
335                            nested.put(child.getName(), list);
336                        }
337                        list.add(createBeanDeclaration(child));
338                    }
339                    else
340                    {
341                        nested.put(child.getName(), createBeanDeclaration(child));
342                    }
343                }
344            }
345    
346            return nested;
347        }
348    
349        /**
350         * Performs interpolation for the specified value. This implementation will
351         * interpolate against the current subnode configuration's parent. If sub
352         * classes need a different interpolation mechanism, they should override
353         * this method.
354         *
355         * @param value the value that is to be interpolated
356         * @return the interpolated value
357         */
358        protected Object interpolate(Object value)
359        {
360            return PropertyConverter.interpolate(value, getConfiguration()
361                    .getParent());
362        }
363    
364        /**
365         * Checks if the specified node is reserved and thus should be ignored. This
366         * method is called when the maps for the bean's properties and complex
367         * properties are collected. It checks whether the given node is an
368         * attribute node and if its name starts with the reserved prefix.
369         *
370         * @param nd the node to be checked
371         * @return a flag whether this node is reserved (and does not point to a
372         * property)
373         */
374        protected boolean isReservedNode(ConfigurationNode nd)
375        {
376            return nd.isAttribute()
377                    && (nd.getName() == null || nd.getName().startsWith(
378                            RESERVED_PREFIX));
379        }
380    
381        /**
382         * Creates a new {@code BeanDeclaration} for a child node of the
383         * current configuration node. This method is called by
384         * {@code getNestedBeanDeclarations()} for all complex sub properties
385         * detected by this method. Derived classes can hook in if they need a
386         * specific initialization. This base implementation creates a
387         * {@code XMLBeanDeclaration} that is properly initialized from the
388         * passed in node.
389         *
390         * @param node the child node, for which a {@code BeanDeclaration} is
391         *        to be created
392         * @return the {@code BeanDeclaration} for this child node
393         * @since 1.6
394         */
395        protected BeanDeclaration createBeanDeclaration(ConfigurationNode node)
396        {
397            List<HierarchicalConfiguration> list = getConfiguration().configurationsAt(node.getName());
398            if (list.size() == 1)
399            {
400                return new XMLBeanDeclaration((SubnodeConfiguration) list.get(0), node);
401            }
402            else
403            {
404                Iterator<HierarchicalConfiguration> iter = list.iterator();
405                while (iter.hasNext())
406                {
407                    SubnodeConfiguration config = (SubnodeConfiguration) iter.next();
408                    if (config.getRootNode().equals(node))
409                    {
410                        return new XMLBeanDeclaration(config, node);
411                    }
412                }
413                throw new ConfigurationRuntimeException("Unable to match node for " + node.getName());
414            }
415        }
416    
417        /**
418         * Initializes the internally managed subnode configuration. This method
419         * will set some default values for some properties.
420         *
421         * @param conf the configuration to initialize
422         */
423        private void initSubnodeConfiguration(SubnodeConfiguration conf)
424        {
425            conf.setThrowExceptionOnMissing(false);
426            conf.setExpressionEngine(null);
427        }
428    }