View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration2.beanutils;
18  
19  import java.util.ArrayList;
20  import java.util.Collection;
21  import java.util.HashMap;
22  import java.util.LinkedList;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Set;
26  import java.util.function.Function;
27  import java.util.stream.Collectors;
28  
29  import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
30  import org.apache.commons.configuration2.HierarchicalConfiguration;
31  import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
32  import org.apache.commons.configuration2.interpol.ConfigurationInterpolator;
33  import org.apache.commons.configuration2.tree.NodeHandler;
34  import org.apache.commons.lang3.StringUtils;
35  
36  /**
37   * <p>
38   * An implementation of the {@code BeanDeclaration} interface that is suitable for XML configuration files.
39   * </p>
40   * <p>
41   * This class defines the standard layout of a bean declaration in an XML configuration file. Such a declaration must
42   * look like the following example fragment:
43   * </p>
44   *
45   * <pre>
46   *   ...
47   *   &lt;personBean config-class=&quot;my.model.PersonBean&quot;
48   *       lastName=&quot;Doe&quot; firstName=&quot;John&quot;&gt;
49   *       &lt;config-constrarg config-value=&quot;ID03493&quot; config-type=&quot;java.lang.String&quot;/&gt;
50   *       &lt;address config-class=&quot;my.model.AddressBean&quot;
51   *           street=&quot;21st street 11&quot; zip=&quot;1234&quot;
52   *           city=&quot;TestCity&quot;/&gt;
53   *   &lt;/personBean&gt;
54   * </pre>
55   *
56   * <p>
57   * The bean declaration can be contained in an arbitrary element. Here it is the {@code personBean} element. In the
58   * attributes of this element there can occur some reserved attributes, which have the following meaning:
59   * </p>
60   * <dl>
61   * <dt>{@code config-class}</dt>
62   * <dd>Here the full qualified name of the bean's class can be specified. An instance of this class will be created. If
63   * this attribute is not specified, the bean class must be provided in another way, e.g. as the {@code defaultClass}
64   * passed to the {@code BeanHelper} class.</dd>
65   * <dt>{@code config-factory}</dt>
66   * <dd>This attribute can contain the name of the {@link BeanFactory} that should be used for creating the bean. If it
67   * is defined, a factory with this name must have been registered at the {@code BeanHelper} class. If this attribute is
68   * missing, the default bean factory will be used.</dd>
69   * <dt>{@code config-factoryParam}</dt>
70   * <dd>With this attribute a parameter can be specified that will be passed to the bean factory. This may be useful for
71   * custom bean factories.</dd>
72   * </dl>
73   * <p>
74   * All further attributes starting with the {@code config-} prefix are considered as meta data and will be ignored. All
75   * other attributes are treated as properties of the bean to be created, i.e. corresponding setter methods of the bean
76   * will be invoked with the values specified here.
77   * </p>
78   * <p>
79   * If the bean to be created has also some complex properties (which are itself beans), their values cannot be
80   * initialized from attributes. For this purpose nested elements can be used. The example listing shows how an address
81   * bean can be initialized. This is done in a nested element whose name must match the name of a property of the
82   * enclosing bean declaration. The format of this nested element is exactly the same as for the bean declaration itself,
83   * i.e. it can have attributes defining meta data or bean properties and even further nested elements for complex bean
84   * properties.
85   * </p>
86   * <p>
87   * If the bean should be created using a specific constructor, the constructor arguments have to be specified. This is
88   * done by an arbitrary number of nested {@code <config-constrarg>} elements. Each element can either have the
89   * {@code config-value} attribute - then it defines a simple value - or must be again a bean declaration (conforming to
90   * the format defined here) defining the complex value of this constructor argument.
91   * </p>
92   * <p>
93   * A {@code XMLBeanDeclaration} object is usually created from a {@code HierarchicalConfiguration}. From this it will
94   * derive a {@code SubnodeConfiguration}, which is used to access the needed properties. This subnode configuration can
95   * be obtained using the {@link #getConfiguration()} method. All of its properties can be accessed in the usual way. To
96   * ensure that the property keys used by this class are understood by the configuration, the default expression engine
97   * will be set.
98   * </p>
99   *
100  * @since 1.3
101  */
102 public class XMLBeanDeclaration implements BeanDeclaration {
103 
104     /** Constant for the prefix of reserved attributes. */
105     public static final String RESERVED_PREFIX = "config-";
106 
107     /** Constant for the prefix for reserved attributes. */
108     public static final String ATTR_PREFIX = "[@" + RESERVED_PREFIX;
109 
110     /** Constant for the bean class attribute. */
111     public static final String ATTR_BEAN_CLASS = ATTR_PREFIX + "class]";
112 
113     /** Constant for the bean factory attribute. */
114     public static final String ATTR_BEAN_FACTORY = ATTR_PREFIX + "factory]";
115 
116     /** Constant for the bean factory parameter attribute. */
117     public static final String ATTR_FACTORY_PARAM = ATTR_PREFIX + "factoryParam]";
118 
119     /** Constant for the name of the bean class attribute. */
120     private static final String ATTR_BEAN_CLASS_NAME = RESERVED_PREFIX + "class";
121 
122     /** Constant for the name of the element for constructor arguments. */
123     private static final String ELEM_CTOR_ARG = RESERVED_PREFIX + "constrarg";
124 
125     /**
126      * Constant for the name of the attribute with the value of a constructor argument.
127      */
128     private static final String ATTR_CTOR_VALUE = RESERVED_PREFIX + "value";
129 
130     /**
131      * Constant for the name of the attribute with the data type of a constructor argument.
132      */
133     private static final String ATTR_CTOR_TYPE = RESERVED_PREFIX + "type";
134 
135     /** Stores the associated configuration. */
136     private final HierarchicalConfiguration<?> configuration;
137 
138     /** Stores the configuration node that contains the bean declaration. */
139     private final NodeData<?> nodeData;
140 
141     /** The name of the default bean class. */
142     private final String defaultBeanClassName;
143 
144     /**
145      * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration. The passed in
146      * key points to the bean declaration.
147      *
148      * @param config the configuration (must not be <b>null</b>)
149      * @param key the key to the bean declaration (this key must point to exactly one bean declaration or a
150      *        {@code IllegalArgumentException} exception will be thrown)
151      * @param <T> the node type of the configuration
152      * @throws IllegalArgumentException if required information is missing to construct the bean declaration
153      */
154     public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config, final String key) {
155         this(config, key, false);
156     }
157 
158     /**
159      * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration supporting
160      * optional declarations.
161      *
162      * @param config the configuration (must not be <b>null</b>)
163      * @param key the key to the bean declaration
164      * @param optional a flag whether this declaration is optional; if set to <b>true</b>, no exception will be thrown if
165      *        the passed in key is undefined
166      * @param <T> the node type of the configuration
167      * @throws IllegalArgumentException if required information is missing to construct the bean declaration
168      */
169     public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config, final String key, final boolean optional) {
170         this(config, key, optional, null);
171     }
172 
173     /**
174      * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration supporting
175      * optional declarations and a default bean class name. The passed in key points to the bean declaration. If the key
176      * does not exist and the boolean argument is <b>true</b>, the declaration is initialized with an empty configuration.
177      * It is possible to create objects from such an empty declaration if a default class is provided. If the key on the
178      * other hand has multiple values or is undefined and the boolean argument is <b>false</b>, a
179      * {@code IllegalArgumentException} exception will be thrown. It is possible to set a default bean class name; this name
180      * is used if the configuration does not contain a bean class.
181      *
182      * @param config the configuration (must not be <b>null</b>)
183      * @param key the key to the bean declaration
184      * @param optional a flag whether this declaration is optional; if set to <b>true</b>, no exception will be thrown if
185      *        the passed in key is undefined
186      * @param defBeanClsName a default bean class name
187      * @param <T> the node type of the configuration
188      * @throws IllegalArgumentException if required information is missing to construct the bean declaration
189      * @since 2.0
190      */
191     public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config, final String key, final boolean optional, final String defBeanClsName) {
192         if (config == null) {
193             throw new IllegalArgumentException("Configuration must not be null!");
194         }
195 
196         HierarchicalConfiguration<?> tmpconfiguration;
197         try {
198             tmpconfiguration = config.configurationAt(key);
199         } catch (final ConfigurationRuntimeException iex) {
200             // If we reach this block, the key does not have exactly one value
201             if (!optional || config.getMaxIndex(key) > 0) {
202                 throw iex;
203             }
204             tmpconfiguration = new BaseHierarchicalConfiguration();
205         }
206         this.nodeData = createNodeDataFromConfiguration(tmpconfiguration);
207         this.configuration = tmpconfiguration;
208         defaultBeanClassName = defBeanClsName;
209         initSubnodeConfiguration(getConfiguration());
210     }
211 
212     /**
213      * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration. The
214      * configuration's root node must contain the bean declaration.
215      *
216      * @param config the configuration with the bean declaration
217      * @param <T> the node type of the configuration
218      */
219     public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config) {
220         this(config, (String) null);
221     }
222 
223     /**
224      * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it with the configuration node that contains the
225      * bean declaration. This constructor is used internally.
226      *
227      * @param config the configuration
228      * @param node the node with the bean declaration.
229      */
230     XMLBeanDeclaration(final HierarchicalConfiguration<?> config, final NodeData<?> node) {
231         this.nodeData = node;
232         configuration = config;
233         defaultBeanClassName = null;
234         initSubnodeConfiguration(config);
235     }
236 
237     /**
238      * Gets the configuration object this bean declaration is based on.
239      *
240      * @return the associated configuration
241      */
242     public HierarchicalConfiguration<?> getConfiguration() {
243         return configuration;
244     }
245 
246     /**
247      * Gets the name of the default bean class. This class is used if no bean class is specified in the configuration. It
248      * may be <b>null</b> if no default class was set.
249      *
250      * @return the default bean class name
251      * @since 2.0
252      */
253     public String getDefaultBeanClassName() {
254         return defaultBeanClassName;
255     }
256 
257     /**
258      * Gets the name of the bean factory. This information is fetched from the {@code config-factory} attribute.
259      *
260      * @return the name of the bean factory
261      */
262     @Override
263     public String getBeanFactoryName() {
264         return getConfiguration().getString(ATTR_BEAN_FACTORY, null);
265     }
266 
267     /**
268      * Gets a parameter for the bean factory. This information is fetched from the {@code config-factoryParam} attribute.
269      *
270      * @return the parameter for the bean factory
271      */
272     @Override
273     public Object getBeanFactoryParameter() {
274         return getConfiguration().getProperty(ATTR_FACTORY_PARAM);
275     }
276 
277     /**
278      * Gets the name of the class of the bean to be created. This information is obtained from the {@code config-class}
279      * attribute.
280      *
281      * @return the name of the bean's class
282      */
283     @Override
284     public String getBeanClassName() {
285         return getConfiguration().getString(ATTR_BEAN_CLASS, getDefaultBeanClassName());
286     }
287 
288     /**
289      * Gets a map with the bean's (simple) properties. The properties are collected from all attribute nodes, which are
290      * not reserved.
291      *
292      * @return a map with the bean's properties
293      */
294     @Override
295     public Map<String, Object> getBeanProperties() {
296         return getAttributeNames().stream().filter(e -> !isReservedAttributeName(e))
297             .collect(Collectors.toMap(Function.identity(), e -> interpolate(getNode().getAttribute(e))));
298     }
299 
300     /**
301      * Gets a map with bean declarations for the complex properties of the bean to be created. These declarations are
302      * obtained from the child nodes of this declaration's root node.
303      *
304      * @return a map with bean declarations for complex properties
305      */
306     @Override
307     public Map<String, Object> getNestedBeanDeclarations() {
308         final Map<String, Object> nested = new HashMap<>();
309         getNode().getChildren().forEach(child -> {
310             if (!isReservedChildName(child.nodeName())) {
311                 final Object obj = nested.get(child.nodeName());
312                 if (obj != null) {
313                     final List<BeanDeclaration> list;
314                     if (obj instanceof List) {
315                         // Safe because we created the lists ourselves.
316                         @SuppressWarnings("unchecked")
317                         final List<BeanDeclaration> tmpList = (List<BeanDeclaration>) obj;
318                         list = tmpList;
319                     } else {
320                         list = new ArrayList<>();
321                         list.add((BeanDeclaration) obj);
322                         nested.put(child.nodeName(), list);
323                     }
324                     list.add(createBeanDeclaration(child));
325                 } else {
326                     nested.put(child.nodeName(), createBeanDeclaration(child));
327                 }
328             }
329         });
330         return nested;
331     }
332 
333     /**
334      * {@inheritDoc} This implementation processes all child nodes with the name {@code config-constrarg}. If such a node
335      * has a {@code config-class} attribute, it is considered a nested bean declaration; otherwise it is interpreted as a
336      * simple value. If no nested constructor argument declarations are found, result is an empty collection.
337      */
338     @Override
339     public Collection<ConstructorArg> getConstructorArgs() {
340         return getNode().getChildren(ELEM_CTOR_ARG).stream().map(this::createConstructorArg).collect(Collectors.toCollection(LinkedList::new));
341     }
342 
343     /**
344      * Performs interpolation for the specified value. This implementation will interpolate against the current subnode
345      * configuration's parent. If sub classes need a different interpolation mechanism, they should override this method.
346      *
347      * @param value the value that is to be interpolated
348      * @return the interpolated value
349      */
350     protected Object interpolate(final Object value) {
351         final ConfigurationInterpolator interpolator = getConfiguration().getInterpolator();
352         return interpolator != null ? interpolator.interpolate(value) : value;
353     }
354 
355     /**
356      * Tests if the specified child node name is reserved and thus should be ignored. This method is called when processing
357      * child nodes of this bean declaration. It is then possible to ignore some nodes with a specific meaning. This
358      * implementation delegates to {@link #isReservedName(String)} .
359      *
360      * @param name the name of the child node to be checked
361      * @return a flag whether this name is reserved
362      * @since 2.0
363      */
364     protected boolean isReservedChildName(final String name) {
365         return isReservedName(name);
366     }
367 
368     /**
369      * Tests if the specified attribute name is reserved and thus does not point to a property of the bean to be created.
370      * This method is called when processing the attributes of this bean declaration. It is then possible to ignore some
371      * attributes with a specific meaning. This implementation delegates to {@link #isReservedName(String)}.
372      *
373      * @param name the name of the attribute to be checked
374      * @return a flag whether this name is reserved
375      * @since 2.0
376      */
377     protected boolean isReservedAttributeName(final String name) {
378         return isReservedName(name);
379     }
380 
381     /**
382      * Tests if the specified name of a node or attribute is reserved and thus should be ignored. This method is called per
383      * default by the methods for checking attribute and child node names. It checks whether the passed in name starts with
384      * the reserved prefix.
385      *
386      * @param name the name to be checked
387      * @return a flag whether this name is reserved
388      */
389     protected boolean isReservedName(final String name) {
390         return name == null || name.startsWith(RESERVED_PREFIX);
391     }
392 
393     /**
394      * Gets a set with the names of the attributes of the configuration node holding the data of this bean declaration.
395      *
396      * @return the attribute names of the underlying configuration node
397      */
398     protected Set<String> getAttributeNames() {
399         return getNode().getAttributes();
400     }
401 
402     /**
403      * Gets the data about the associated node.
404      *
405      * @return the node with the bean declaration
406      */
407     NodeData<?> getNode() {
408         return nodeData;
409     }
410 
411     /**
412      * Creates a new {@code BeanDeclaration} for a child node of the current configuration node. This method is called by
413      * {@code getNestedBeanDeclarations()} for all complex sub properties detected by this method. Derived classes can hook
414      * in if they need a specific initialization. This base implementation creates a {@code XMLBeanDeclaration} that is
415      * properly initialized from the passed in node.
416      *
417      * @param nodeData the child node, for which a {@code BeanDeclaration} is to be created
418      * @return the {@code BeanDeclaration} for this child node
419      */
420     BeanDeclaration createBeanDeclaration(final NodeData<?> nodeData) {
421         for (final HierarchicalConfiguration<?> config : getConfiguration().configurationsAt(nodeData.escapedNodeName(getConfiguration()))) {
422             if (nodeData.matchesConfigRootNode(config)) {
423                 return new XMLBeanDeclaration(config, nodeData);
424             }
425         }
426         throw new ConfigurationRuntimeException("Unable to match node for " + nodeData.nodeName());
427     }
428 
429     /**
430      * Initializes the internally managed sub configuration. This method will set some default values for some properties.
431      *
432      * @param conf the configuration to initialize
433      */
434     private void initSubnodeConfiguration(final HierarchicalConfiguration<?> conf) {
435         conf.setExpressionEngine(null);
436     }
437 
438     /**
439      * Creates a {@code ConstructorArg} object for the specified configuration node.
440      *
441      * @param child the configuration node
442      * @return the corresponding {@code ConstructorArg} object
443      */
444     private ConstructorArg createConstructorArg(final NodeData<?> child) {
445         final String type = getAttribute(child, ATTR_CTOR_TYPE);
446         if (isBeanDeclarationArgument(child)) {
447             return ConstructorArg.forValue(getAttribute(child, ATTR_CTOR_VALUE), type);
448         }
449         return ConstructorArg.forBeanDeclaration(createBeanDeclaration(child), type);
450     }
451 
452     /**
453      * Gets an attribute of a configuration node. This method also takes interpolation into account.
454      *
455      * @param nodeData the node
456      * @param attribute the name of the attribute
457      * @return the string value of this attribute (can be <b>null</b>)
458      */
459     private String getAttribute(final NodeData<?> nodeData, final String attribute) {
460         final Object value = nodeData.getAttribute(attribute);
461         return value == null ? null : String.valueOf(interpolate(value));
462     }
463 
464     /**
465      * Tests whether the constructor argument represented by the given configuration node is a bean declaration.
466      *
467      * @param nodeData the configuration node in question
468      * @return a flag whether this constructor argument is a bean declaration
469      */
470     private static boolean isBeanDeclarationArgument(final NodeData<?> nodeData) {
471         return !nodeData.getAttributes().contains(ATTR_BEAN_CLASS_NAME);
472     }
473 
474     /**
475      * Creates a {@code NodeData} object from the root node of the given configuration.
476      *
477      * @param config the configuration
478      * @param <T> the type of the nodes
479      * @return the {@code NodeData} object
480      */
481     private static <T> NodeData<T> createNodeDataFromConfiguration(final HierarchicalConfiguration<T> config) {
482         final NodeHandler<T> handler = config.getNodeModel().getNodeHandler();
483         return new NodeData<>(handler.getRootNode(), handler);
484     }
485 
486     /**
487      * An internal helper class which wraps the node with the bean declaration and the corresponding node handler.
488      *
489      * @param <T> the type of the node
490      */
491     static class NodeData<T> {
492 
493         /** The wrapped node. */
494         private final T node;
495 
496         /** The node handler for interacting with this node. */
497         private final NodeHandler<T> nodeHandler;
498 
499         /**
500          * Constructs a new instance of {@code NodeData}.
501          *
502          * @param node the node
503          * @param nodeHandler the node handler
504          */
505         NodeData(final T node, final NodeHandler<T> nodeHandler) {
506             this.node = node;
507             this.nodeHandler = nodeHandler;
508         }
509 
510         /**
511          * Returns the name of the wrapped node.
512          *
513          * @return the node name
514          */
515         String nodeName() {
516             return nodeHandler.nodeName(node);
517         }
518 
519         /**
520          * Returns the unescaped name of the node stored in this data object. This method handles the case that the node name
521          * may contain reserved characters with a special meaning for the current expression engine. In this case, the
522          * characters affected have to be escaped accordingly.
523          *
524          * @param config the configuration
525          * @return the escaped node name
526          */
527         String escapedNodeName(final HierarchicalConfiguration<?> config) {
528             return config.getExpressionEngine().nodeKey(node, StringUtils.EMPTY, nodeHandler);
529         }
530 
531         /**
532          * Gets a list with the children of the wrapped node, again wrapped into {@code NodeData} objects.
533          *
534          * @return a list with the children
535          */
536         List<NodeData<T>> getChildren() {
537             return wrapInNodeData(nodeHandler.getChildren(node));
538         }
539 
540         /**
541          * Gets a list with the children of the wrapped node with the given name, again wrapped into {@code NodeData}
542          * objects.
543          *
544          * @param name the name of the desired child nodes
545          * @return a list with the children with this name
546          */
547         List<NodeData<T>> getChildren(final String name) {
548             return wrapInNodeData(nodeHandler.getChildren(node, name));
549         }
550 
551         /**
552          * Gets a set with the names of the attributes of the wrapped node.
553          *
554          * @return the attribute names of this node
555          */
556         Set<String> getAttributes() {
557             return nodeHandler.getAttributes(node);
558         }
559 
560         /**
561          * Gets the value of the attribute with the given name of the wrapped node.
562          *
563          * @param key the key of the attribute
564          * @return the value of this attribute
565          */
566         Object getAttribute(final String key) {
567             return nodeHandler.getAttributeValue(node, key);
568         }
569 
570         /**
571          * Returns a flag whether the wrapped node is the root node of the passed in configuration.
572          *
573          * @param config the configuration
574          * @return a flag whether this node is the configuration's root node
575          */
576         boolean matchesConfigRootNode(final HierarchicalConfiguration<?> config) {
577             return config.getNodeModel().getNodeHandler().getRootNode().equals(node);
578         }
579 
580         /**
581          * Wraps the passed in list of nodes in {@code NodeData} objects.
582          *
583          * @param nodes the list with nodes
584          * @return the wrapped nodes
585          */
586         List<NodeData<T>> wrapInNodeData(final List<T> nodes) {
587             return nodes.stream().map(n -> new NodeData<>(n, nodeHandler)).collect(Collectors.toList());
588         }
589     }
590 }