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;
018:
019: import java.util.ArrayList;
020: import java.util.Collection;
021: import java.util.HashMap;
022: import java.util.Iterator;
023: import java.util.List;
024: import java.util.Map;
025: import java.util.Set;
026:
027: import org.apache.commons.configuration.event.ConfigurationEvent;
028: import org.apache.commons.configuration.event.ConfigurationListener;
029: import org.apache.commons.configuration.tree.ConfigurationNode;
030: import org.apache.commons.configuration.tree.DefaultConfigurationKey;
031: import org.apache.commons.configuration.tree.DefaultConfigurationNode;
032: import org.apache.commons.configuration.tree.DefaultExpressionEngine;
033: import org.apache.commons.configuration.tree.NodeCombiner;
034: import org.apache.commons.configuration.tree.UnionCombiner;
035: import org.apache.commons.configuration.tree.ViewNode;
036:
037: /**
038: * <p>
039: * A hierarchical composite configuration class.
040: * </p>
041: * <p>
042: * This class maintains a list of configuration objects, which can be added
043: * using the divers <code>addConfiguration()</code> methods. After that the
044: * configurations can be accessed either by name (if one was provided when the
045: * configuration was added) or by index. For the whole set of managed
046: * configurations a logical node structure is constructed. For this purpose a
047: * <code>{@link org.apache.commons.configuration.tree.NodeCombiner NodeCombiner}</code>
048: * object can be set. This makes it possible to specify different algorithms for
049: * the combination process.
050: * </p>
051: * <p>
052: * The big advantage of this class is that it creates a truely hierarchical
053: * structure of all the properties stored in the contained configurations - even
054: * if some of them are no hierarchical configurations per se. So all enhanced
055: * features provided by a hierarchical configuration (e.g. choosing an
056: * expression engine) are applicable.
057: * </p>
058: * <p>
059: * The class works by registering itself as an event listener add all added
060: * configurations. So it gets notified whenever one of these configurations is
061: * changed and can invalidate its internal node structure. The next time a
062: * property is accessed the node structure will be re-constructed using the
063: * current state of the managed configurations. Node that, depending on the used
064: * <code>NodeCombiner</code>, this may be a complex operation.
065: * </p>
066: * <p>
067: * It is not strictly forbidden to manipulate a
068: * <code>CombinedConfiguration</code> directly, but the results may be
069: * unpredictable. For instance some node combiners use special view nodes for
070: * linking parts of the original configurations' data together. If new
071: * properties are added to such a special node, they do not belong to any of the
072: * managed configurations and thus hang in the air. It is also possible that
073: * direct updates on a <code>CombinedConfiguration</code> are incompatible
074: * with the used node combiner (e.g. if the
075: * <code>{@link org.apache.commons.configuration.tree.OverrideCombiner OverrideCombiner}</code>
076: * is used and properties are removed the resulting node structure may be
077: * incorrect because some properties that were hidden by the removed properties
078: * are not visible). So it is recommended to perform updates only on the managed
079: * configurations.
080: * </p>
081: * <p>
082: * Whenever the node structure of a <code>CombinedConfiguration</code> becomes
083: * invalid (either because one of the contained configurations was modified or
084: * because the <code>invalidate()</code> method was directly called) an event
085: * is generated. So this can be detected by interested event listeners. This
086: * also makes it possible to add a combined configuration into another one.
087: * </p>
088: * <p>
089: * Implementation note: Adding and removing configurations to and from a
090: * combined configuration is not thread-safe. If a combined configuration is
091: * manipulated by multiple threads, the developer has to take care about
092: * properly synchronization.
093: * </p>
094: *
095: * @author <a
096: * href="http://jakarta.apache.org/commons/configuration/team-list.html">Commons
097: * Configuration team</a>
098: * @since 1.3
099: * @version $Id: CombinedConfiguration.java 484692 2006-12-08 18:30:15Z oheger $
100: */
101: public class CombinedConfiguration extends HierarchicalConfiguration
102: implements ConfigurationListener, Cloneable {
103: /**
104: * Constant for the invalidate event that is fired when the internal node
105: * structure becomes invalid.
106: */
107: public static final int EVENT_COMBINED_INVALIDATE = 40;
108:
109: /**
110: * The serial version ID.
111: */
112: private static final long serialVersionUID = 8338574525528692307L;
113:
114: /** Constant for the expression engine for parsing the at path. */
115: private static final DefaultExpressionEngine AT_ENGINE = new DefaultExpressionEngine();
116:
117: /** Constant for the default node combiner. */
118: private static final NodeCombiner DEFAULT_COMBINER = new UnionCombiner();
119:
120: /** Constant for the name of the property used for the reload check.*/
121: private static final String PROP_RELOAD_CHECK = "CombinedConfigurationReloadCheck";
122:
123: /** Stores the combiner. */
124: private NodeCombiner nodeCombiner;
125:
126: /** Stores the combined root node. */
127: private ConfigurationNode combinedRoot;
128:
129: /** Stores a list with the contained configurations. */
130: private List configurations;
131:
132: /** Stores a map with the named configurations. */
133: private Map namedConfigurations;
134:
135: /** A flag whether an enhanced reload check is to be performed.*/
136: private boolean forceReloadCheck;
137:
138: /**
139: * Creates a new instance of <code>CombinedConfiguration</code> and
140: * initializes the combiner to be used.
141: *
142: * @param comb the node combiner (can be <b>null</b>, then a union combiner
143: * is used as default)
144: */
145: public CombinedConfiguration(NodeCombiner comb) {
146: setNodeCombiner((comb != null) ? comb : DEFAULT_COMBINER);
147: clear();
148: }
149:
150: /**
151: * Creates a new instance of <code>CombinedConfiguration</code> that uses
152: * a union combiner.
153: *
154: * @see org.apache.commons.configuration.tree.UnionCombiner
155: */
156: public CombinedConfiguration() {
157: this (null);
158: }
159:
160: /**
161: * Returns the node combiner that is used for creating the combined node
162: * structure.
163: *
164: * @return the node combiner
165: */
166: public NodeCombiner getNodeCombiner() {
167: return nodeCombiner;
168: }
169:
170: /**
171: * Sets the node combiner. This object will be used when the combined node
172: * structure is to be constructed. It must not be <b>null</b>, otherwise an
173: * <code>IllegalArgumentException</code> exception is thrown. Changing the
174: * node combiner causes an invalidation of this combined configuration, so
175: * that the new combiner immediately takes effect.
176: *
177: * @param nodeCombiner the node combiner
178: */
179: public void setNodeCombiner(NodeCombiner nodeCombiner) {
180: if (nodeCombiner == null) {
181: throw new IllegalArgumentException(
182: "Node combiner must not be null!");
183: }
184: this .nodeCombiner = nodeCombiner;
185: invalidate();
186: }
187:
188: /**
189: * Returns a flag whether an enhanced reload check must be performed.
190: *
191: * @return the force reload check flag
192: * @since 1.4
193: */
194: public boolean isForceReloadCheck() {
195: return forceReloadCheck;
196: }
197:
198: /**
199: * Sets the force reload check flag. If this flag is set, each property
200: * access on this configuration will cause a reload check on the contained
201: * configurations. This is a workaround for a problem with some reload
202: * implementations that only check if a reload is required when they are
203: * triggered. Per default this mode is disabled. If the force reload check
204: * flag is set to <b>true</b>, accessing properties will be less
205: * performant, but reloads on contained configurations will be detected.
206: *
207: * @param forceReloadCheck the value of the flag
208: * @since 1.4
209: */
210: public void setForceReloadCheck(boolean forceReloadCheck) {
211: this .forceReloadCheck = forceReloadCheck;
212: }
213:
214: /**
215: * Adds a new configuration to this combined configuration. It is possible
216: * (but not mandatory) to give the new configuration a name. This name must
217: * be unique, otherwise a <code>ConfigurationRuntimeException</code> will
218: * be thrown. With the optional <code>at</code> argument you can specify
219: * where in the resulting node structure the content of the added
220: * configuration should appear. This is a string that uses dots as property
221: * delimiters (independent on the current expression engine). For instance
222: * if you pass in the string <code>"database.tables"</code>,
223: * all properties of the added configuration will occur in this branch.
224: *
225: * @param config the configuration to add (must not be <b>null</b>)
226: * @param name the name of this configuration (can be <b>null</b>)
227: * @param at the position of this configuration in the combined tree (can be
228: * <b>null</b>)
229: */
230: public void addConfiguration(AbstractConfiguration config,
231: String name, String at) {
232: if (config == null) {
233: throw new IllegalArgumentException(
234: "Added configuration must not be null!");
235: }
236: if (name != null && namedConfigurations.containsKey(name)) {
237: throw new ConfigurationRuntimeException(
238: "A configuration with the name '"
239: + name
240: + "' already exists in this combined configuration!");
241: }
242:
243: ConfigData cd = new ConfigData(config, name, at);
244: configurations.add(cd);
245: if (name != null) {
246: namedConfigurations.put(name, config);
247: }
248:
249: config.addConfigurationListener(this );
250: invalidate();
251: }
252:
253: /**
254: * Adds a new configuration to this combined configuration with an optional
255: * name. The new configuration's properties will be added under the root of
256: * the combined node structure.
257: *
258: * @param config the configuration to add (must not be <b>null</b>)
259: * @param name the name of this configuration (can be <b>null</b>)
260: */
261: public void addConfiguration(AbstractConfiguration config,
262: String name) {
263: addConfiguration(config, name, null);
264: }
265:
266: /**
267: * Adds a new configuration to this combined configuration. The new
268: * configuration is not given a name. Its properties will be added under the
269: * root of the combined node structure.
270: *
271: * @param config the configuration to add (must not be <b>null</b>)
272: */
273: public void addConfiguration(AbstractConfiguration config) {
274: addConfiguration(config, null, null);
275: }
276:
277: /**
278: * Returns the number of configurations that are contained in this combined
279: * configuration.
280: *
281: * @return the number of contained configurations
282: */
283: public int getNumberOfConfigurations() {
284: return configurations.size();
285: }
286:
287: /**
288: * Returns the configuration at the specified index. The contained
289: * configurations are numbered in the order they were added to this combined
290: * configuration. The index of the first configuration is 0.
291: *
292: * @param index the index
293: * @return the configuration at this index
294: */
295: public Configuration getConfiguration(int index) {
296: ConfigData cd = (ConfigData) configurations.get(index);
297: return cd.getConfiguration();
298: }
299:
300: /**
301: * Returns the configuration with the given name. This can be <b>null</b>
302: * if no such configuration exists.
303: *
304: * @param name the name of the configuration
305: * @return the configuration with this name
306: */
307: public Configuration getConfiguration(String name) {
308: return (Configuration) namedConfigurations.get(name);
309: }
310:
311: /**
312: * Removes the specified configuration from this combined configuration.
313: *
314: * @param config the configuration to be removed
315: * @return a flag whether this configuration was found and could be removed
316: */
317: public boolean removeConfiguration(Configuration config) {
318: for (int index = 0; index < getNumberOfConfigurations(); index++) {
319: if (((ConfigData) configurations.get(index))
320: .getConfiguration() == config) {
321: removeConfigurationAt(index);
322: return true;
323: }
324: }
325:
326: return false;
327: }
328:
329: /**
330: * Removes the configuration at the specified index.
331: *
332: * @param index the index
333: * @return the removed configuration
334: */
335: public Configuration removeConfigurationAt(int index) {
336: ConfigData cd = (ConfigData) configurations.remove(index);
337: if (cd.getName() != null) {
338: namedConfigurations.remove(cd.getName());
339: }
340: cd.getConfiguration().removeConfigurationListener(this );
341: invalidate();
342: return cd.getConfiguration();
343: }
344:
345: /**
346: * Removes the configuration with the specified name.
347: *
348: * @param name the name of the configuration to be removed
349: * @return the removed configuration (<b>null</b> if this configuration
350: * was not found)
351: */
352: public Configuration removeConfiguration(String name) {
353: Configuration conf = getConfiguration(name);
354: if (conf != null) {
355: removeConfiguration(conf);
356: }
357: return conf;
358: }
359:
360: /**
361: * Returns a set with the names of all configurations contained in this
362: * combined configuration. Of course here are only these configurations
363: * listed, for which a name was specified when they were added.
364: *
365: * @return a set with the names of the contained configurations (never
366: * <b>null</b>)
367: */
368: public Set getConfigurationNames() {
369: return namedConfigurations.keySet();
370: }
371:
372: /**
373: * Invalidates this combined configuration. This means that the next time a
374: * property is accessed the combined node structure must be re-constructed.
375: * Invalidation of a combined configuration also means that an event of type
376: * <code>EVENT_COMBINED_INVALIDATE</code> is fired. Note that while other
377: * events most times appear twice (once before and once after an update),
378: * this event is only fired once (after update).
379: */
380: public void invalidate() {
381: synchronized (getNodeCombiner()) // use combiner as lock
382: {
383: combinedRoot = null;
384: }
385: fireEvent(EVENT_COMBINED_INVALIDATE, null, null, false);
386: }
387:
388: /**
389: * Event listener call back for configuration update events. This method is
390: * called whenever one of the contained configurations was modified. It
391: * invalidates this combined configuration.
392: *
393: * @param event the update event
394: */
395: public void configurationChanged(ConfigurationEvent event) {
396: invalidate();
397: }
398:
399: /**
400: * Returns the configuration root node of this combined configuration. This
401: * method will construct a combined node structure using the current node
402: * combiner if necessary.
403: *
404: * @return the combined root node
405: */
406: public ConfigurationNode getRootNode() {
407: synchronized (getNodeCombiner()) {
408: if (combinedRoot == null) {
409: combinedRoot = constructCombinedNode();
410: }
411: return combinedRoot;
412: }
413: }
414:
415: /**
416: * Clears this configuration. All contained configurations will be removed.
417: */
418: public void clear() {
419: fireEvent(EVENT_CLEAR, null, null, true);
420: configurations = new ArrayList();
421: namedConfigurations = new HashMap();
422: fireEvent(EVENT_CLEAR, null, null, false);
423: invalidate();
424: }
425:
426: /**
427: * Returns a copy of this object. This implementation performs a deep clone,
428: * i.e. all contained configurations will be cloned, too. For this to work,
429: * all contained configurations must be cloneable. Registered event
430: * listeners won't be cloned. The clone will use the same node combiner than
431: * the original.
432: *
433: * @return the copied object
434: */
435: public Object clone() {
436: CombinedConfiguration copy = (CombinedConfiguration) super
437: .clone();
438: copy.clear();
439: for (Iterator it = configurations.iterator(); it.hasNext();) {
440: ConfigData cd = (ConfigData) it.next();
441: copy.addConfiguration(
442: (AbstractConfiguration) ConfigurationUtils
443: .cloneConfiguration(cd.getConfiguration()),
444: cd.getName(), cd.getAt());
445: }
446:
447: copy.setRootNode(new DefaultConfigurationNode());
448: return copy;
449: }
450:
451: /**
452: * Returns the value of the specified property. This implementation
453: * evaluates the <em>force reload check</em> flag. If it is set, all
454: * contained configurations will be triggered before the value of the
455: * requested property is retrieved.
456: *
457: * @param key the key of the desired property
458: * @return the value of this property
459: * @since 1.4
460: */
461: public Object getProperty(String key) {
462: if (isForceReloadCheck()) {
463: for (Iterator it = configurations.iterator(); it.hasNext();) {
464: try {
465: // simply retrieve a property; this is enough for
466: // triggering a reload
467: ((ConfigData) it.next()).getConfiguration()
468: .getProperty(PROP_RELOAD_CHECK);
469: } catch (Exception ex) {
470: // ignore all exceptions, e.g. missing property exceptions
471: ;
472: }
473: }
474: }
475:
476: return super .getProperty(key);
477: }
478:
479: /**
480: * Creates the root node of this combined configuration.
481: *
482: * @return the combined root node
483: */
484: private ConfigurationNode constructCombinedNode() {
485: if (getNumberOfConfigurations() < 1) {
486: return new ViewNode();
487: }
488:
489: else {
490: Iterator it = configurations.iterator();
491: ConfigurationNode node = ((ConfigData) it.next())
492: .getTransformedRoot();
493: while (it.hasNext()) {
494: node = getNodeCombiner().combine(node,
495: ((ConfigData) it.next()).getTransformedRoot());
496: }
497: return node;
498: }
499: }
500:
501: /**
502: * An internal helper class for storing information about contained
503: * configurations.
504: */
505: static class ConfigData {
506: /** Stores a reference to the configuration. */
507: private AbstractConfiguration configuration;
508:
509: /** Stores the name under which the configuration is stored. */
510: private String name;
511:
512: /** Stores the at information as path of nodes. */
513: private Collection atPath;
514:
515: /** Stores the at string.*/
516: private String at;
517:
518: /**
519: * Creates a new instance of <code>ConfigData</code> and initializes
520: * it.
521: *
522: * @param config the configuration
523: * @param n the name
524: * @param at the at position
525: */
526: public ConfigData(AbstractConfiguration config, String n,
527: String at) {
528: configuration = config;
529: name = n;
530: atPath = parseAt(at);
531: this .at = at;
532: }
533:
534: /**
535: * Returns the stored configuration.
536: *
537: * @return the configuration
538: */
539: public AbstractConfiguration getConfiguration() {
540: return configuration;
541: }
542:
543: /**
544: * Returns the configuration's name.
545: *
546: * @return the name
547: */
548: public String getName() {
549: return name;
550: }
551:
552: /**
553: * Returns the at position of this configuration.
554: *
555: * @return the at position
556: */
557: public String getAt() {
558: return at;
559: }
560:
561: /**
562: * Returns the transformed root node of the stored configuration. The
563: * term "transformed" means that an eventually defined at path
564: * has been applied.
565: *
566: * @return the transformed root node
567: */
568: public ConfigurationNode getTransformedRoot() {
569: ViewNode result = new ViewNode();
570: ViewNode atParent = result;
571:
572: if (atPath != null) {
573: // Build the complete path
574: for (Iterator it = atPath.iterator(); it.hasNext();) {
575: ViewNode node = new ViewNode();
576: node.setName((String) it.next());
577: atParent.addChild(node);
578: atParent = node;
579: }
580: }
581:
582: // Copy data of the root node to the new path
583: HierarchicalConfiguration hc = ConfigurationUtils
584: .convertToHierarchical(getConfiguration());
585: atParent.appendChildren(hc.getRootNode());
586: atParent.appendAttributes(hc.getRootNode());
587:
588: return result;
589: }
590:
591: /**
592: * Splits the at path into its components.
593: *
594: * @param at the at string
595: * @return a collection with the names of the single components
596: */
597: private Collection parseAt(String at) {
598: if (at == null) {
599: return null;
600: }
601:
602: Collection result = new ArrayList();
603: DefaultConfigurationKey.KeyIterator it = new DefaultConfigurationKey(
604: AT_ENGINE, at).iterator();
605: while (it.hasNext()) {
606: result.add(it.nextKey());
607: }
608: return result;
609: }
610: }
611: }
|