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 1208761 2011-11-30 20:39:47Z 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 SubnodeConfiguration configuration;
129    
130        /** Stores the configuration node that contains the bean declaration. */
131        private 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            try
174            {
175                configuration = config.configurationAt(key);
176                node = configuration.getRootNode();
177            }
178            catch (IllegalArgumentException iex)
179            {
180                // If we reach this block, the key does not have exactly one value
181                if (!optional || config.getMaxIndex(key) > 0)
182                {
183                    throw iex;
184                }
185                configuration = config.configurationAt(null);
186                node = new DefaultConfigurationNode();
187            }
188            initSubnodeConfiguration(getConfiguration());
189        }
190    
191        /**
192         * Creates a new instance of {@code XMLBeanDeclaration} and
193         * initializes it from the given configuration. The configuration's root
194         * node must contain the bean declaration.
195         *
196         * @param config the configuration with the bean declaration
197         */
198        public XMLBeanDeclaration(HierarchicalConfiguration config)
199        {
200            this(config, (String) null);
201        }
202    
203        /**
204         * Creates a new instance of {@code XMLBeanDeclaration} and
205         * initializes it with the configuration node that contains the bean
206         * declaration.
207         *
208         * @param config the configuration
209         * @param node the node with the bean declaration.
210         */
211        public XMLBeanDeclaration(SubnodeConfiguration config,
212                ConfigurationNode node)
213        {
214            if (config == null)
215            {
216                throw new IllegalArgumentException(
217                        "Configuration must not be null!");
218            }
219            if (node == null)
220            {
221                throw new IllegalArgumentException("Node must not be null!");
222            }
223    
224            this.node = node;
225            configuration = config;
226            initSubnodeConfiguration(config);
227        }
228    
229        /**
230         * Returns the configuration object this bean declaration is based on.
231         *
232         * @return the associated configuration
233         */
234        public SubnodeConfiguration getConfiguration()
235        {
236            return configuration;
237        }
238    
239        /**
240         * Returns the node that contains the bean declaration.
241         *
242         * @return the configuration node this bean declaration is based on
243         */
244        public ConfigurationNode getNode()
245        {
246            return node;
247        }
248    
249        /**
250         * Returns the name of the bean factory. This information is fetched from
251         * the {@code config-factory} attribute.
252         *
253         * @return the name of the bean factory
254         */
255        public String getBeanFactoryName()
256        {
257            return getConfiguration().getString(ATTR_BEAN_FACTORY);
258        }
259    
260        /**
261         * Returns a parameter for the bean factory. This information is fetched
262         * from the {@code config-factoryParam} attribute.
263         *
264         * @return the parameter for the bean factory
265         */
266        public Object getBeanFactoryParameter()
267        {
268            return getConfiguration().getProperty(ATTR_FACTORY_PARAM);
269        }
270    
271        /**
272         * Returns the name of the class of the bean to be created. This information
273         * is obtained from the {@code config-class} attribute.
274         *
275         * @return the name of the bean's class
276         */
277        public String getBeanClassName()
278        {
279            return getConfiguration().getString(ATTR_BEAN_CLASS);
280        }
281    
282        /**
283         * Returns a map with the bean's (simple) properties. The properties are
284         * collected from all attribute nodes, which are not reserved.
285         *
286         * @return a map with the bean's properties
287         */
288        public Map<String, Object> getBeanProperties()
289        {
290            Map<String, Object> props = new HashMap<String, Object>();
291            for (ConfigurationNode attr : getNode().getAttributes())
292            {
293                if (!isReservedNode(attr))
294                {
295                    props.put(attr.getName(), interpolate(attr .getValue()));
296                }
297            }
298    
299            return props;
300        }
301    
302        /**
303         * Returns a map with bean declarations for the complex properties of the
304         * bean to be created. These declarations are obtained from the child nodes
305         * of this declaration's root node.
306         *
307         * @return a map with bean declarations for complex properties
308         */
309        public Map<String, Object> getNestedBeanDeclarations()
310        {
311            Map<String, Object> nested = new HashMap<String, Object>();
312            for (ConfigurationNode child : getNode().getChildren())
313            {
314                if (!isReservedNode(child))
315                {
316                    if (nested.containsKey(child.getName()))
317                    {
318                        Object obj = nested.get(child.getName());
319                        List<BeanDeclaration> list;
320                        if (obj instanceof List)
321                        {
322                            // Safe because we created the lists ourselves.
323                            @SuppressWarnings("unchecked")
324                            List<BeanDeclaration> tmpList = (List<BeanDeclaration>) obj;
325                            list = tmpList;
326                        }
327                        else
328                        {
329                            list = new ArrayList<BeanDeclaration>();
330                            list.add((BeanDeclaration) obj);
331                            nested.put(child.getName(), list);
332                        }
333                        list.add(createBeanDeclaration(child));
334                    }
335                    else
336                    {
337                        nested.put(child.getName(), createBeanDeclaration(child));
338                    }
339                }
340            }
341    
342            return nested;
343        }
344    
345        /**
346         * Performs interpolation for the specified value. This implementation will
347         * interpolate against the current subnode configuration's parent. If sub
348         * classes need a different interpolation mechanism, they should override
349         * this method.
350         *
351         * @param value the value that is to be interpolated
352         * @return the interpolated value
353         */
354        protected Object interpolate(Object value)
355        {
356            return PropertyConverter.interpolate(value, getConfiguration()
357                    .getParent());
358        }
359    
360        /**
361         * Checks if the specified node is reserved and thus should be ignored. This
362         * method is called when the maps for the bean's properties and complex
363         * properties are collected. It checks whether the given node is an
364         * attribute node and if its name starts with the reserved prefix.
365         *
366         * @param nd the node to be checked
367         * @return a flag whether this node is reserved (and does not point to a
368         * property)
369         */
370        protected boolean isReservedNode(ConfigurationNode nd)
371        {
372            return nd.isAttribute()
373                    && (nd.getName() == null || nd.getName().startsWith(
374                            RESERVED_PREFIX));
375        }
376    
377        /**
378         * Creates a new {@code BeanDeclaration} for a child node of the
379         * current configuration node. This method is called by
380         * {@code getNestedBeanDeclarations()} for all complex sub properties
381         * detected by this method. Derived classes can hook in if they need a
382         * specific initialization. This base implementation creates a
383         * {@code XMLBeanDeclaration} that is properly initialized from the
384         * passed in node.
385         *
386         * @param node the child node, for which a {@code BeanDeclaration} is
387         *        to be created
388         * @return the {@code BeanDeclaration} for this child node
389         * @since 1.6
390         */
391        protected BeanDeclaration createBeanDeclaration(ConfigurationNode node)
392        {
393            List<HierarchicalConfiguration> list = getConfiguration().configurationsAt(node.getName());
394            if (list.size() == 1)
395            {
396                return new XMLBeanDeclaration((SubnodeConfiguration) list.get(0), node);
397            }
398            else
399            {
400                Iterator<HierarchicalConfiguration> iter = list.iterator();
401                while (iter.hasNext())
402                {
403                    SubnodeConfiguration config = (SubnodeConfiguration) iter.next();
404                    if (config.getRootNode().equals(node))
405                    {
406                        return new XMLBeanDeclaration(config, node);
407                    }
408                }
409                throw new ConfigurationRuntimeException("Unable to match node for " + node.getName());
410            }
411        }
412    
413        /**
414         * Initializes the internally managed subnode configuration. This method
415         * will set some default values for some properties.
416         *
417         * @param conf the configuration to initialize
418         */
419        private void initSubnodeConfiguration(SubnodeConfiguration conf)
420        {
421            conf.setThrowExceptionOnMissing(false);
422            conf.setExpressionEngine(null);
423        }
424    }