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 * https://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 * <personBean config-class="my.model.PersonBean"
48 * lastName="Doe" firstName="John">
49 * <config-constrarg config-value="ID03493" config-type="java.lang.String"/>
50 * <address config-class="my.model.AddressBean"
51 * street="21st street 11" zip="1234"
52 * city="TestCity"/>
53 * </personBean>
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, for example 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 /**
105 * An internal helper class which wraps the node with the bean declaration and the corresponding node handler.
106 *
107 * @param <T> the type of the node
108 */
109 static class NodeData<T> {
110
111 /** The wrapped node. */
112 private final T node;
113
114 /** The node handler for interacting with this node. */
115 private final NodeHandler<T> nodeHandler;
116
117 /**
118 * Constructs a new instance of {@code NodeData}.
119 *
120 * @param node the node
121 * @param nodeHandler the node handler
122 */
123 NodeData(final T node, final NodeHandler<T> nodeHandler) {
124 this.node = node;
125 this.nodeHandler = nodeHandler;
126 }
127
128 /**
129 * Returns the unescaped name of the node stored in this data object. This method handles the case that the node name
130 * may contain reserved characters with a special meaning for the current expression engine. In this case, the
131 * characters affected have to be escaped accordingly.
132 *
133 * @param config the configuration
134 * @return the escaped node name
135 */
136 String escapedNodeName(final HierarchicalConfiguration<?> config) {
137 return config.getExpressionEngine().nodeKey(node, StringUtils.EMPTY, nodeHandler);
138 }
139
140 /**
141 * Gets the value of the attribute with the given name of the wrapped node.
142 *
143 * @param key the key of the attribute
144 * @return the value of this attribute
145 */
146 Object getAttribute(final String key) {
147 return nodeHandler.getAttributeValue(node, key);
148 }
149
150 /**
151 * Gets a set with the names of the attributes of the wrapped node.
152 *
153 * @return the attribute names of this node
154 */
155 Set<String> getAttributes() {
156 return nodeHandler.getAttributes(node);
157 }
158
159 /**
160 * Gets a list with the children of the wrapped node, again wrapped into {@code NodeData} objects.
161 *
162 * @return a list with the children
163 */
164 List<NodeData<T>> getChildren() {
165 return wrapInNodeData(nodeHandler.getChildren(node));
166 }
167
168 /**
169 * Gets a list with the children of the wrapped node with the given name, again wrapped into {@code NodeData}
170 * objects.
171 *
172 * @param name the name of the desired child nodes
173 * @return a list with the children with this name
174 */
175 List<NodeData<T>> getChildren(final String name) {
176 return wrapInNodeData(nodeHandler.getChildren(node, name));
177 }
178
179 /**
180 * Returns a flag whether the wrapped node is the root node of the passed in configuration.
181 *
182 * @param config the configuration
183 * @return a flag whether this node is the configuration's root node
184 */
185 boolean matchesConfigRootNode(final HierarchicalConfiguration<?> config) {
186 return config.getNodeModel().getNodeHandler().getRootNode().equals(node);
187 }
188
189 /**
190 * Returns the name of the wrapped node.
191 *
192 * @return the node name
193 */
194 String nodeName() {
195 return nodeHandler.nodeName(node);
196 }
197
198 /**
199 * Wraps the passed in list of nodes in {@code NodeData} objects.
200 *
201 * @param nodes the list with nodes
202 * @return the wrapped nodes
203 */
204 List<NodeData<T>> wrapInNodeData(final List<T> nodes) {
205 return nodes.stream().map(n -> new NodeData<>(n, nodeHandler)).collect(Collectors.toList());
206 }
207 }
208
209 /** Constant for the prefix of reserved attributes. */
210 public static final String RESERVED_PREFIX = "config-";
211
212 /** Constant for the prefix for reserved attributes. */
213 public static final String ATTR_PREFIX = "[@" + RESERVED_PREFIX;
214
215 /** Constant for the bean class attribute. */
216 public static final String ATTR_BEAN_CLASS = ATTR_PREFIX + "class]";
217
218 /** Constant for the bean factory attribute. */
219 public static final String ATTR_BEAN_FACTORY = ATTR_PREFIX + "factory]";
220
221 /** Constant for the bean factory parameter attribute. */
222 public static final String ATTR_FACTORY_PARAM = ATTR_PREFIX + "factoryParam]";
223
224 /** Constant for the name of the bean class attribute. */
225 private static final String ATTR_BEAN_CLASS_NAME = RESERVED_PREFIX + "class";
226
227 /** Constant for the name of the element for constructor arguments. */
228 private static final String ELEM_CTOR_ARG = RESERVED_PREFIX + "constrarg";
229
230 /**
231 * Constant for the name of the attribute with the value of a constructor argument.
232 */
233 private static final String ATTR_CTOR_VALUE = RESERVED_PREFIX + "value";
234
235 /**
236 * Constant for the name of the attribute with the data type of a constructor argument.
237 */
238 private static final String ATTR_CTOR_TYPE = RESERVED_PREFIX + "type";
239
240 /**
241 * Creates a {@code NodeData} object from the root node of the given configuration.
242 *
243 * @param config the configuration
244 * @param <T> the type of the nodes
245 * @return the {@code NodeData} object
246 */
247 private static <T> NodeData<T> createNodeDataFromConfiguration(final HierarchicalConfiguration<T> config) {
248 final NodeHandler<T> handler = config.getNodeModel().getNodeHandler();
249 return new NodeData<>(handler.getRootNode(), handler);
250 }
251
252 /**
253 * Tests whether the constructor argument represented by the given configuration node is a bean declaration.
254 *
255 * @param nodeData the configuration node in question
256 * @return a flag whether this constructor argument is a bean declaration
257 */
258 private static boolean isBeanDeclarationArgument(final NodeData<?> nodeData) {
259 return !nodeData.getAttributes().contains(ATTR_BEAN_CLASS_NAME);
260 }
261
262 /** Stores the associated configuration. */
263 private final HierarchicalConfiguration<?> configuration;
264
265 /** Stores the configuration node that contains the bean declaration. */
266 private final NodeData<?> nodeData;
267
268 /** The name of the default bean class. */
269 private final String defaultBeanClassName;
270
271 /**
272 * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it with the configuration node that contains the
273 * bean declaration. This constructor is used internally.
274 *
275 * @param config the configuration
276 * @param node the node with the bean declaration.
277 */
278 XMLBeanDeclaration(final HierarchicalConfiguration<?> config, final NodeData<?> node) {
279 this.nodeData = node;
280 configuration = config;
281 defaultBeanClassName = null;
282 initSubnodeConfiguration(config);
283 }
284
285 /**
286 * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration. The
287 * configuration's root node must contain the bean declaration.
288 *
289 * @param config the configuration with the bean declaration
290 * @param <T> the node type of the configuration
291 */
292 public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config) {
293 this(config, (String) null);
294 }
295
296 /**
297 * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration. The passed in
298 * key points to the bean declaration.
299 *
300 * @param config the configuration (must not be <strong>null</strong>)
301 * @param key the key to the bean declaration (this key must point to exactly one bean declaration or a
302 * {@code IllegalArgumentException} exception will be thrown)
303 * @param <T> the node type of the configuration
304 * @throws IllegalArgumentException if required information is missing to construct the bean declaration
305 */
306 public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config, final String key) {
307 this(config, key, false);
308 }
309
310 /**
311 * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration supporting
312 * optional declarations.
313 *
314 * @param config the configuration (must not be <strong>null</strong>)
315 * @param key the key to the bean declaration
316 * @param optional a flag whether this declaration is optional; if set to <strong>true</strong>, no exception will be thrown if
317 * the passed in key is undefined
318 * @param <T> the node type of the configuration
319 * @throws IllegalArgumentException if required information is missing to construct the bean declaration
320 */
321 public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config, final String key, final boolean optional) {
322 this(config, key, optional, null);
323 }
324
325 /**
326 * Constructs a new instance of {@code XMLBeanDeclaration} and initializes it from the given configuration supporting
327 * optional declarations and a default bean class name. The passed in key points to the bean declaration. If the key
328 * does not exist and the boolean argument is <strong>true</strong>, the declaration is initialized with an empty configuration.
329 * It is possible to create objects from such an empty declaration if a default class is provided. If the key on the
330 * other hand has multiple values or is undefined and the boolean argument is <strong>false</strong>, a
331 * {@code IllegalArgumentException} exception will be thrown. It is possible to set a default bean class name; this name
332 * is used if the configuration does not contain a bean class.
333 *
334 * @param config the configuration (must not be <strong>null</strong>)
335 * @param key the key to the bean declaration
336 * @param optional a flag whether this declaration is optional; if set to <strong>true</strong>, no exception will be thrown if
337 * the passed in key is undefined
338 * @param defBeanClsName a default bean class name
339 * @param <T> the node type of the configuration
340 * @throws IllegalArgumentException if required information is missing to construct the bean declaration
341 * @since 2.0
342 */
343 public <T> XMLBeanDeclaration(final HierarchicalConfiguration<T> config, final String key, final boolean optional, final String defBeanClsName) {
344 if (config == null) {
345 throw new IllegalArgumentException("Configuration must not be null!");
346 }
347
348 HierarchicalConfiguration<?> tmpconfiguration;
349 try {
350 tmpconfiguration = config.configurationAt(key);
351 } catch (final ConfigurationRuntimeException iex) {
352 // If we reach this block, the key does not have exactly one value
353 if (!optional || config.getMaxIndex(key) > 0) {
354 throw iex;
355 }
356 tmpconfiguration = new BaseHierarchicalConfiguration();
357 }
358 this.nodeData = createNodeDataFromConfiguration(tmpconfiguration);
359 this.configuration = tmpconfiguration;
360 defaultBeanClassName = defBeanClsName;
361 initSubnodeConfiguration(getConfiguration());
362 }
363
364 /**
365 * Creates a new {@code BeanDeclaration} for a child node of the current configuration node. This method is called by
366 * {@code getNestedBeanDeclarations()} for all complex sub properties detected by this method. Derived classes can hook
367 * in if they need a specific initialization. This base implementation creates a {@code XMLBeanDeclaration} that is
368 * properly initialized from the passed in node.
369 *
370 * @param nodeData the child node, for which a {@code BeanDeclaration} is to be created
371 * @return the {@code BeanDeclaration} for this child node
372 */
373 BeanDeclaration createBeanDeclaration(final NodeData<?> nodeData) {
374 for (final HierarchicalConfiguration<?> config : getConfiguration().configurationsAt(nodeData.escapedNodeName(getConfiguration()))) {
375 if (nodeData.matchesConfigRootNode(config)) {
376 return new XMLBeanDeclaration(config, nodeData);
377 }
378 }
379 throw new ConfigurationRuntimeException("Unable to match node for " + nodeData.nodeName());
380 }
381
382 /**
383 * Creates a {@code ConstructorArg} object for the specified configuration node.
384 *
385 * @param child the configuration node
386 * @return the corresponding {@code ConstructorArg} object
387 */
388 private ConstructorArg createConstructorArg(final NodeData<?> child) {
389 final String type = getAttribute(child, ATTR_CTOR_TYPE);
390 if (isBeanDeclarationArgument(child)) {
391 return ConstructorArg.forValue(getAttribute(child, ATTR_CTOR_VALUE), type);
392 }
393 return ConstructorArg.forBeanDeclaration(createBeanDeclaration(child), type);
394 }
395
396 /**
397 * Gets an attribute of a configuration node. This method also takes interpolation into account.
398 *
399 * @param nodeData the node
400 * @param attribute the name of the attribute
401 * @return the string value of this attribute (can be <strong>null</strong>)
402 */
403 private String getAttribute(final NodeData<?> nodeData, final String attribute) {
404 final Object value = nodeData.getAttribute(attribute);
405 return value == null ? null : String.valueOf(interpolate(value));
406 }
407
408 /**
409 * Gets a set with the names of the attributes of the configuration node holding the data of this bean declaration.
410 *
411 * @return the attribute names of the underlying configuration node
412 */
413 protected Set<String> getAttributeNames() {
414 return getNode().getAttributes();
415 }
416
417 /**
418 * Gets the name of the class of the bean to be created. This information is obtained from the {@code config-class}
419 * attribute.
420 *
421 * @return the name of the bean's class
422 */
423 @Override
424 public String getBeanClassName() {
425 return getConfiguration().getString(ATTR_BEAN_CLASS, getDefaultBeanClassName());
426 }
427
428 /**
429 * Gets the name of the bean factory. This information is fetched from the {@code config-factory} attribute.
430 *
431 * @return the name of the bean factory
432 */
433 @Override
434 public String getBeanFactoryName() {
435 return getConfiguration().getString(ATTR_BEAN_FACTORY, null);
436 }
437
438 /**
439 * Gets a parameter for the bean factory. This information is fetched from the {@code config-factoryParam} attribute.
440 *
441 * @return the parameter for the bean factory
442 */
443 @Override
444 public Object getBeanFactoryParameter() {
445 return getConfiguration().getProperty(ATTR_FACTORY_PARAM);
446 }
447
448 /**
449 * Gets a map with the bean's (simple) properties. The properties are collected from all attribute nodes, which are
450 * not reserved.
451 *
452 * @return a map with the bean's properties
453 */
454 @Override
455 public Map<String, Object> getBeanProperties() {
456 return getAttributeNames().stream().filter(e -> !isReservedAttributeName(e))
457 .collect(Collectors.toMap(Function.identity(), e -> interpolate(getNode().getAttribute(e))));
458 }
459
460 /**
461 * Gets the configuration object this bean declaration is based on.
462 *
463 * @return the associated configuration
464 */
465 public HierarchicalConfiguration<?> getConfiguration() {
466 return configuration;
467 }
468
469 /**
470 * {@inheritDoc} This implementation processes all child nodes with the name {@code config-constrarg}. If such a node
471 * has a {@code config-class} attribute, it is considered a nested bean declaration; otherwise it is interpreted as a
472 * simple value. If no nested constructor argument declarations are found, result is an empty collection.
473 */
474 @Override
475 public Collection<ConstructorArg> getConstructorArgs() {
476 return getNode().getChildren(ELEM_CTOR_ARG).stream().map(this::createConstructorArg).collect(Collectors.toCollection(LinkedList::new));
477 }
478
479 /**
480 * Gets the name of the default bean class. This class is used if no bean class is specified in the configuration. It
481 * may be <strong>null</strong> if no default class was set.
482 *
483 * @return the default bean class name
484 * @since 2.0
485 */
486 public String getDefaultBeanClassName() {
487 return defaultBeanClassName;
488 }
489
490 /**
491 * Gets a map with bean declarations for the complex properties of the bean to be created. These declarations are
492 * obtained from the child nodes of this declaration's root node.
493 *
494 * @return a map with bean declarations for complex properties
495 */
496 @Override
497 public Map<String, Object> getNestedBeanDeclarations() {
498 final Map<String, Object> nested = new HashMap<>();
499 getNode().getChildren().forEach(child -> {
500 if (!isReservedChildName(child.nodeName())) {
501 final Object obj = nested.get(child.nodeName());
502 if (obj != null) {
503 final List<BeanDeclaration> list;
504 if (obj instanceof List) {
505 // Safe because we created the lists ourselves.
506 @SuppressWarnings("unchecked")
507 final List<BeanDeclaration> tmpList = (List<BeanDeclaration>) obj;
508 list = tmpList;
509 } else {
510 list = new ArrayList<>();
511 list.add((BeanDeclaration) obj);
512 nested.put(child.nodeName(), list);
513 }
514 list.add(createBeanDeclaration(child));
515 } else {
516 nested.put(child.nodeName(), createBeanDeclaration(child));
517 }
518 }
519 });
520 return nested;
521 }
522
523 /**
524 * Gets the data about the associated node.
525 *
526 * @return the node with the bean declaration
527 */
528 NodeData<?> getNode() {
529 return nodeData;
530 }
531
532 /**
533 * Initializes the internally managed sub configuration. This method will set some default values for some properties.
534 *
535 * @param conf the configuration to initialize
536 */
537 private void initSubnodeConfiguration(final HierarchicalConfiguration<?> conf) {
538 conf.setExpressionEngine(null);
539 }
540
541 /**
542 * Performs interpolation for the specified value. This implementation will interpolate against the current subnode
543 * configuration's parent. If sub classes need a different interpolation mechanism, they should override this method.
544 *
545 * @param value the value that is to be interpolated
546 * @return the interpolated value
547 */
548 protected Object interpolate(final Object value) {
549 final ConfigurationInterpolator interpolator = getConfiguration().getInterpolator();
550 return interpolator != null ? interpolator.interpolate(value) : value;
551 }
552
553 /**
554 * Tests if the specified attribute name is reserved and thus does not point to a property of the bean to be created.
555 * This method is called when processing the attributes of this bean declaration. It is then possible to ignore some
556 * attributes with a specific meaning. This implementation delegates to {@link #isReservedName(String)}.
557 *
558 * @param name the name of the attribute to be checked
559 * @return a flag whether this name is reserved
560 * @since 2.0
561 */
562 protected boolean isReservedAttributeName(final String name) {
563 return isReservedName(name);
564 }
565
566 /**
567 * Tests if the specified child node name is reserved and thus should be ignored. This method is called when processing
568 * child nodes of this bean declaration. It is then possible to ignore some nodes with a specific meaning. This
569 * implementation delegates to {@link #isReservedName(String)}.
570 *
571 * @param name the name of the child node to be checked
572 * @return a flag whether this name is reserved
573 * @since 2.0
574 */
575 protected boolean isReservedChildName(final String name) {
576 return isReservedName(name);
577 }
578
579 /**
580 * Tests if the specified name of a node or attribute is reserved and thus should be ignored. This method is called per
581 * default by the methods for checking attribute and child node names. It checks whether the passed in name starts with
582 * the reserved prefix.
583 *
584 * @param name the name to be checked
585 * @return a flag whether this name is reserved
586 */
587 protected boolean isReservedName(final String name) {
588 return name == null || name.startsWith(RESERVED_PREFIX);
589 }
590 }