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 */
017package org.apache.commons.configuration.beanutils;
018
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Map;
024
025import org.apache.commons.configuration.ConfigurationRuntimeException;
026import org.apache.commons.configuration.HierarchicalConfiguration;
027import org.apache.commons.configuration.PropertyConverter;
028import org.apache.commons.configuration.SubnodeConfiguration;
029import org.apache.commons.configuration.tree.ConfigurationNode;
030import 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 */
109public 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}