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 * <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, 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 }