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 */
017 package org.apache.commons.configuration.beanutils;
018
019 import java.util.HashMap;
020 import java.util.Iterator;
021 import java.util.Map;
022 import java.util.List;
023 import java.util.ArrayList;
024
025 import org.apache.commons.configuration.HierarchicalConfiguration;
026 import org.apache.commons.configuration.PropertyConverter;
027 import org.apache.commons.configuration.SubnodeConfiguration;
028 import org.apache.commons.configuration.ConfigurationRuntimeException;
029 import org.apache.commons.configuration.tree.ConfigurationNode;
030 import org.apache.commons.configuration.tree.DefaultConfigurationNode;
031
032 /**
033 * <p>
034 * An implementation of the <code>BeanDeclaration</code> interface that is
035 * suitable for XML configuration files.
036 * </p>
037 * <p>
038 * This class defines the standard layout of a bean declaration in an XML
039 * configuration file. Such a declaration must look like the following example
040 * fragement:
041 * </p>
042 * <p>
043 *
044 * <pre>
045 * ...
046 * <personBean config-class="my.model.PersonBean"
047 * lastName="Doe" firstName="John">
048 * <address config-class="my.model.AddressBean"
049 * street="21st street 11" zip="1234"
050 * city="TestCity"/>
051 * </personBean>
052 * </pre>
053 *
054 * </p>
055 * <p>
056 * The bean declaration can be contained in an arbitrary element. Here it is the
057 * <code><personBean></code> element. In the attributes of this element
058 * there can occur some reserved attributes, which have the following meaning:
059 * <dl>
060 * <dt><code>config-class</code></dt>
061 * <dd>Here the full qualified name of the bean's class can be specified. An
062 * instance of this class will be created. If this attribute is not specified,
063 * the bean class must be provided in another way, e.g. as the
064 * <code>defaultClass</code> passed to the <code>BeanHelper</code> class.</dd>
065 * <dt><code>config-factory</code></dt>
066 * <dd>This attribute can contain the name of the
067 * <code>{@link BeanFactory}</code> that should be used for creating the bean.
068 * If it is defined, a factory with this name must have been registered at the
069 * <code>BeanHelper</code> class. If this attribute is missing, the default
070 * bean factory will be used.</dd>
071 * <dt><code>config-factoryParam</code></dt>
072 * <dd>With this attribute a parameter can be specified that will be passed to
073 * the bean factory. This may be useful for custom bean factories.</dd>
074 * </dl>
075 * </p>
076 * <p>
077 * All further attributes starting with the <code>config-</code> prefix are
078 * considered as meta data and will be ignored. All other attributes are treated
079 * as properties of the bean to be created, i.e. corresponding setter methods of
080 * the bean will be invoked with the values specified here.
081 * </p>
082 * <p>
083 * If the bean to be created has also some complex properties (which are itself
084 * beans), their values cannot be initialized from attributes. For this purpose
085 * nested elements can be used. The example listing shows how an address bean
086 * can be initialized. This is done in a nested element whose name must match
087 * the name of a property of the enclosing bean declaration. The format of this
088 * nested element is exactly the same as for the bean declaration itself, i.e.
089 * it can have attributes defining meta data or bean properties and even further
090 * nested elements for complex bean properties.
091 * </p>
092 * <p>
093 * A <code>XMLBeanDeclaration</code> object is usually created from a
094 * <code>HierarchicalConfiguration</code>. From this it will derive a
095 * <code>SubnodeConfiguration</code>, which is used to access the needed
096 * properties. This subnode configuration can be obtained using the
097 * <code>{@link #getConfiguration()}</code> method. All of its properties can
098 * be accessed in the usual way. To ensure that the property keys used by this
099 * class are understood by the configuration, the default expression engine will
100 * be set.
101 * </p>
102 *
103 * @since 1.3
104 * @author Oliver Heger
105 * @version $Id: XMLBeanDeclaration.java 766914 2009-04-20 23:38:49Z rgoers $
106 */
107 public class XMLBeanDeclaration implements BeanDeclaration
108 {
109 /** Constant for the prefix of reserved attributes. */
110 public static final String RESERVED_PREFIX = "config-";
111
112 /** Constant for the prefix for reserved attributes.*/
113 public static final String ATTR_PREFIX = "[@" + RESERVED_PREFIX;
114
115 /** Constant for the bean class attribute. */
116 public static final String ATTR_BEAN_CLASS = ATTR_PREFIX + "class]";
117
118 /** Constant for the bean factory attribute. */
119 public static final String ATTR_BEAN_FACTORY = ATTR_PREFIX + "factory]";
120
121 /** Constant for the bean factory parameter attribute. */
122 public static final String ATTR_FACTORY_PARAM = ATTR_PREFIX
123 + "factoryParam]";
124
125 /** Stores the associated configuration. */
126 private SubnodeConfiguration configuration;
127
128 /** Stores the configuration node that contains the bean declaration. */
129 private ConfigurationNode node;
130
131 /**
132 * Creates a new instance of <code>XMLBeanDeclaration</code> and
133 * initializes it from the given configuration. The passed in key points to
134 * the bean declaration.
135 *
136 * @param config the configuration
137 * @param key the key to the bean declaration (this key must point to
138 * exactly one bean declaration or a <code>IllegalArgumentException</code>
139 * exception will be thrown)
140 */
141 public XMLBeanDeclaration(HierarchicalConfiguration config, String key)
142 {
143 this(config, key, false);
144 }
145
146 /**
147 * Creates a new instance of <code>XMLBeanDeclaration</code> and
148 * initializes it from the given configuration. The passed in key points to
149 * the bean declaration. If the key does not exist and the boolean argument
150 * is <b>true</b>, the declaration is initialized with an empty
151 * configuration. It is possible to create objects from such an empty
152 * declaration if a default class is provided. If the key on the other hand
153 * has multiple values or is undefined and the boolean argument is <b>false</b>,
154 * a <code>IllegalArgumentException</code> exception will be thrown.
155 *
156 * @param config the configuration
157 * @param key the key to the bean declaration
158 * @param optional a flag whether this declaration is optional; if set to
159 * <b>true</b>, no exception will be thrown if the passed in key is
160 * undefined
161 */
162 public XMLBeanDeclaration(HierarchicalConfiguration config, String key,
163 boolean optional)
164 {
165 if (config == null)
166 {
167 throw new IllegalArgumentException(
168 "Configuration must not be null!");
169 }
170
171 try
172 {
173 configuration = config.configurationAt(key);
174 node = configuration.getRootNode();
175 }
176 catch (IllegalArgumentException iex)
177 {
178 // If we reach this block, the key does not have exactly one value
179 if (!optional || config.getMaxIndex(key) > 0)
180 {
181 throw iex;
182 }
183 configuration = config.configurationAt(null);
184 node = new DefaultConfigurationNode();
185 }
186 initSubnodeConfiguration(getConfiguration());
187 }
188
189 /**
190 * Creates a new instance of <code>XMLBeanDeclaration</code> and
191 * initializes it from the given configuration. The configuration's root
192 * node must contain the bean declaration.
193 *
194 * @param config the configuration with the bean declaration
195 */
196 public XMLBeanDeclaration(HierarchicalConfiguration config)
197 {
198 this(config, (String) null);
199 }
200
201 /**
202 * Creates a new instance of <code>XMLBeanDeclaration</code> and
203 * initializes it with the configuration node that contains the bean
204 * declaration.
205 *
206 * @param config the configuration
207 * @param node the node with the bean declaration.
208 */
209 public XMLBeanDeclaration(SubnodeConfiguration config,
210 ConfigurationNode node)
211 {
212 if (config == null)
213 {
214 throw new IllegalArgumentException(
215 "Configuration must not be null!");
216 }
217 if (node == null)
218 {
219 throw new IllegalArgumentException("Node must not be null!");
220 }
221
222 this.node = node;
223 configuration = config;
224 initSubnodeConfiguration(config);
225 }
226
227 /**
228 * Returns the configuration object this bean declaration is based on.
229 *
230 * @return the associated configuration
231 */
232 public SubnodeConfiguration getConfiguration()
233 {
234 return configuration;
235 }
236
237 /**
238 * Returns the node that contains the bean declaration.
239 *
240 * @return the configuration node this bean declaration is based on
241 */
242 public ConfigurationNode getNode()
243 {
244 return node;
245 }
246
247 /**
248 * Returns the name of the bean factory. This information is fetched from
249 * the <code>config-factory</code> attribute.
250 *
251 * @return the name of the bean factory
252 */
253 public String getBeanFactoryName()
254 {
255 return getConfiguration().getString(ATTR_BEAN_FACTORY);
256 }
257
258 /**
259 * Returns a parameter for the bean factory. This information is fetched
260 * from the <code>config-factoryParam</code> attribute.
261 *
262 * @return the parameter for the bean factory
263 */
264 public Object getBeanFactoryParameter()
265 {
266 return getConfiguration().getProperty(ATTR_FACTORY_PARAM);
267 }
268
269 /**
270 * Returns the name of the class of the bean to be created. This information
271 * is obtained from the <code>config-class</code> attribute.
272 *
273 * @return the name of the bean's class
274 */
275 public String getBeanClassName()
276 {
277 return getConfiguration().getString(ATTR_BEAN_CLASS);
278 }
279
280 /**
281 * Returns a map with the bean's (simple) properties. The properties are
282 * collected from all attribute nodes, which are not reserved.
283 *
284 * @return a map with the bean's properties
285 */
286 public Map getBeanProperties()
287 {
288 Map props = new HashMap();
289 for (Iterator it = getNode().getAttributes().iterator(); it.hasNext();)
290 {
291 ConfigurationNode attr = (ConfigurationNode) it.next();
292 if (!isReservedNode(attr))
293 {
294 props.put(attr.getName(), interpolate(attr .getValue()));
295 }
296 }
297
298 return props;
299 }
300
301 /**
302 * Returns a map with bean declarations for the complex properties of the
303 * bean to be created. These declarations are obtained from the child nodes
304 * of this declaration's root node.
305 *
306 * @return a map with bean declarations for complex properties
307 */
308 public Map getNestedBeanDeclarations()
309 {
310 Map nested = new HashMap();
311 for (Iterator it = getNode().getChildren().iterator(); it.hasNext();)
312 {
313 ConfigurationNode child = (ConfigurationNode) it.next();
314 if (!isReservedNode(child))
315 {
316 if (nested.containsKey(child.getName()))
317 {
318 Object obj = nested.get(child.getName());
319 List list;
320 if (obj instanceof List)
321 {
322 list = (List) obj;
323 }
324 else
325 {
326 list = new ArrayList();
327 list.add(obj);
328 nested.put(child.getName(), list);
329 }
330 list.add(createBeanDeclaration(child));
331 }
332 else
333 {
334 nested.put(child.getName(), createBeanDeclaration(child));
335 }
336 }
337 }
338
339 return nested;
340 }
341
342 /**
343 * Performs interpolation for the specified value. This implementation will
344 * interpolate against the current subnode configuration's parent. If sub
345 * classes need a different interpolation mechanism, they should override
346 * this method.
347 *
348 * @param value the value that is to be interpolated
349 * @return the interpolated value
350 */
351 protected Object interpolate(Object value)
352 {
353 return PropertyConverter.interpolate(value, getConfiguration()
354 .getParent());
355 }
356
357 /**
358 * Checks if the specified node is reserved and thus should be ignored. This
359 * method is called when the maps for the bean's properties and complex
360 * properties are collected. It checks whether the given node is an
361 * attribute node and if its name starts with the reserved prefix.
362 *
363 * @param nd the node to be checked
364 * @return a flag whether this node is reserved (and does not point to a
365 * property)
366 */
367 protected boolean isReservedNode(ConfigurationNode nd)
368 {
369 return nd.isAttribute()
370 && (nd.getName() == null || nd.getName().startsWith(
371 RESERVED_PREFIX));
372 }
373
374 /**
375 * Creates a new <code>BeanDeclaration</code> for a child node of the
376 * current configuration node. This method is called by
377 * <code>getNestedBeanDeclarations()</code> for all complex sub properties
378 * detected by this method. Derived classes can hook in if they need a
379 * specific initialization. This base implementation creates a
380 * <code>XMLBeanDeclaration</code> that is properly initialized from the
381 * passed in node.
382 *
383 * @param node the child node, for which a <code>BeanDeclaration</code> is
384 * to be created
385 * @return the <code>BeanDeclaration</code> for this child node
386 * @since 1.6
387 */
388 protected BeanDeclaration createBeanDeclaration(ConfigurationNode node)
389 {
390 List list = getConfiguration().configurationsAt(node.getName());
391 if (list.size() == 1)
392 {
393 return new XMLBeanDeclaration((SubnodeConfiguration) list.get(0), node);
394 }
395 else
396 {
397 Iterator iter = list.iterator();
398 while (iter.hasNext())
399 {
400 SubnodeConfiguration config = (SubnodeConfiguration) iter.next();
401 if (config.getRootNode().equals(node))
402 {
403 return new XMLBeanDeclaration(config, node);
404 }
405 }
406 throw new ConfigurationRuntimeException("Unable to match node for " + node.getName());
407 }
408 }
409
410 /**
411 * Initializes the internally managed subnode configuration. This method
412 * will set some default values for some properties.
413 *
414 * @param conf the configuration to initialize
415 */
416 private void initSubnodeConfiguration(SubnodeConfiguration conf)
417 {
418 conf.setThrowExceptionOnMissing(false);
419 conf.setExpressionEngine(null);
420 }
421 }