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.configuration2.beanutils; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.HashMap; 022import java.util.LinkedList; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.function.Function; 027import java.util.stream.Collectors; 028 029import org.apache.commons.configuration2.BaseHierarchicalConfiguration; 030import org.apache.commons.configuration2.HierarchicalConfiguration; 031import org.apache.commons.configuration2.ex.ConfigurationRuntimeException; 032import org.apache.commons.configuration2.interpol.ConfigurationInterpolator; 033import org.apache.commons.configuration2.tree.NodeHandler; 034import org.apache.commons.lang3.StringUtils; 035 036/** 037 * <p> 038 * An implementation of the {@code BeanDeclaration} interface that is suitable for XML configuration files. 039 * </p> 040 * <p> 041 * This class defines the standard layout of a bean declaration in an XML configuration file. Such a declaration must 042 * look like the following example fragment: 043 * </p> 044 * 045 * <pre> 046 * ... 047 * <personBean config-class="my.model.PersonBean" 048 * lastName="Doe" firstName="John"> 049 * <config-constrarg config-value="ID03493" config-type="java.lang.String"/> 050 * <address config-class="my.model.AddressBean" 051 * street="21st street 11" zip="1234" 052 * city="TestCity"/> 053 * </personBean> 054 * </pre> 055 * 056 * <p> 057 * The bean declaration can be contained in an arbitrary element. Here it is the {@code personBean} element. In the 058 * attributes of this element there can occur some reserved attributes, which have the following meaning: 059 * </p> 060 * <dl> 061 * <dt>{@code config-class}</dt> 062 * <dd>Here the full qualified name of the bean's class can be specified. An instance of this class will be created. If 063 * this attribute is not specified, the bean class must be provided in another way, e.g. as the {@code defaultClass} 064 * passed to the {@code BeanHelper} class.</dd> 065 * <dt>{@code config-factory}</dt> 066 * <dd>This attribute can contain the name of the {@link BeanFactory} that should be used for creating the bean. If it 067 * is defined, a factory with this name must have been registered at the {@code BeanHelper} class. If this attribute is 068 * missing, the default bean factory will be used.</dd> 069 * <dt>{@code config-factoryParam}</dt> 070 * <dd>With this attribute a parameter can be specified that will be passed to the bean factory. This may be useful for 071 * custom bean factories.</dd> 072 * </dl> 073 * <p> 074 * All further attributes starting with the {@code config-} prefix are considered as meta data and will be ignored. All 075 * other attributes are treated as properties of the bean to be created, i.e. corresponding setter methods of the bean 076 * will be invoked with the values specified here. 077 * </p> 078 * <p> 079 * If the bean to be created has also some complex properties (which are itself beans), their values cannot be 080 * initialized from attributes. For this purpose nested elements can be used. The example listing shows how an address 081 * bean can be initialized. This is done in a nested element whose name must match the name of a property of the 082 * enclosing bean declaration. The format of this nested element is exactly the same as for the bean declaration itself, 083 * i.e. it can have attributes defining meta data or bean properties and even further nested elements for complex bean 084 * properties. 085 * </p> 086 * <p> 087 * If the bean should be created using a specific constructor, the constructor arguments have to be specified. This is 088 * done by an arbitrary number of nested {@code <config-constrarg>} elements. Each element can either have the 089 * {@code config-value} attribute - then it defines a simple value - or must be again a bean declaration (conforming to 090 * the format defined here) defining the complex value of this constructor argument. 091 * </p> 092 * <p> 093 * A {@code XMLBeanDeclaration} object is usually created from a {@code HierarchicalConfiguration}. From this it will 094 * derive a {@code SubnodeConfiguration}, which is used to access the needed properties. This subnode configuration can 095 * be obtained using the {@link #getConfiguration()} method. All of its properties can be accessed in the usual way. To 096 * ensure that the property keys used by this class are understood by the configuration, the default expression engine 097 * will be set. 098 * </p> 099 * 100 * @since 1.3 101 */ 102public 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 <b>null</b>) 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 <b>null</b>) 315 * @param key the key to the bean declaration 316 * @param optional a flag whether this declaration is optional; if set to <b>true</b>, 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 <b>true</b>, 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 <b>false</b>, 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 <b>null</b>) 335 * @param key the key to the bean declaration 336 * @param optional a flag whether this declaration is optional; if set to <b>true</b>, 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 <b>null</b>) 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 <b>null</b> 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}