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;
018
019import java.io.ByteArrayOutputStream;
020import java.io.PrintStream;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029
030import org.apache.commons.configuration2.event.ConfigurationEvent;
031import org.apache.commons.configuration2.event.EventListener;
032import org.apache.commons.configuration2.event.EventSource;
033import org.apache.commons.configuration2.event.EventType;
034import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
035import org.apache.commons.configuration2.sync.LockMode;
036import org.apache.commons.configuration2.tree.DefaultConfigurationKey;
037import org.apache.commons.configuration2.tree.DefaultExpressionEngine;
038import org.apache.commons.configuration2.tree.ExpressionEngine;
039import org.apache.commons.configuration2.tree.ImmutableNode;
040import org.apache.commons.configuration2.tree.NodeCombiner;
041import org.apache.commons.configuration2.tree.NodeTreeWalker;
042import org.apache.commons.configuration2.tree.QueryResult;
043import org.apache.commons.configuration2.tree.TreeUtils;
044import org.apache.commons.configuration2.tree.UnionCombiner;
045
046/**
047 * <p>
048 * A hierarchical composite configuration class.
049 * </p>
050 * <p>
051 * This class maintains a list of configuration objects, which can be added
052 * using the diverse {@code addConfiguration()} methods. After that the
053 * configurations can be accessed either by name (if one was provided when the
054 * configuration was added) or by index. For the whole set of managed
055 * configurations a logical node structure is constructed. For this purpose a
056 * {@link org.apache.commons.configuration2.tree.NodeCombiner NodeCombiner}
057 * object can be set. This makes it possible to specify different algorithms for
058 * the combination process.
059 * </p>
060 * <p>
061 * The big advantage of this class is that it creates a truly hierarchical
062 * structure of all the properties stored in the contained configurations - even
063 * if some of them are no hierarchical configurations per se. So all enhanced
064 * features provided by a hierarchical configuration (e.g. choosing an
065 * expression engine) are applicable.
066 * </p>
067 * <p>
068 * The class works by registering itself as an event listener at all added
069 * configurations. So it gets notified whenever one of these configurations is
070 * changed and can invalidate its internal node structure. The next time a
071 * property is accessed the node structure will be re-constructed using the
072 * current state of the managed configurations. Note that, depending on the used
073 * {@code NodeCombiner}, this may be a complex operation.
074 * </p>
075 * <p>
076 * Because of the way a {@code CombinedConfiguration} is working it has more or
077 * less view character: it provides a logic view on the configurations it
078 * contains. In this constellation not all methods defined for hierarchical
079 * configurations - especially methods that update the stored properties - can
080 * be implemented in a consistent manner. Using such methods (like
081 * {@code addProperty()}, or {@code clearProperty()} on a
082 * {@code CombinedConfiguration} is not strictly forbidden, however, depending
083 * on the current {@link NodeCombiner} and the involved properties, the results
084 * may be different than expected. Some examples may illustrate this:
085 * </p>
086 * <ul>
087 * <li>Imagine a {@code CombinedConfiguration} <em>cc</em> containing two child
088 * configurations with the following content:
089 * <dl>
090 * <dt>user.properties</dt>
091 * <dd>
092 *
093 * <pre>
094 * gui.background = blue
095 * gui.position = (10, 10, 400, 200)
096 * </pre>
097 *
098 * </dd>
099 * <dt>default.properties</dt>
100 * <dd>
101 *
102 * <pre>
103 * gui.background = black
104 * gui.foreground = white
105 * home.dir = /data
106 * </pre>
107 *
108 * </dd>
109 * </dl>
110 * As a {@code NodeCombiner} a
111 * {@link org.apache.commons.configuration2.tree.OverrideCombiner
112 * OverrideCombiner} is used. This combiner will ensure that defined user
113 * settings take precedence over the default values. If the resulting
114 * {@code CombinedConfiguration} is queried for the background color,
115 * {@code blue} will be returned because this value is defined in
116 * {@code user.properties}. Now consider what happens if the key
117 * {@code gui.background} is removed from the {@code CombinedConfiguration}:
118 *
119 * <pre>
120 * cc.clearProperty(&quot;gui.background&quot;);
121 * </pre>
122 *
123 * Will a {@code cc.containsKey("gui.background")} now return <b>false</b>? No,
124 * it won't! The {@code clearProperty()} operation is executed on the node set
125 * of the combined configuration, which was constructed from the nodes of the
126 * two child configurations. It causes the value of the <em>background</em> node
127 * to be cleared, which is also part of the first child configuration. This
128 * modification of one of its child configurations causes the
129 * {@code CombinedConfiguration} to be re-constructed. This time the
130 * {@code OverrideCombiner} cannot find a {@code gui.background} property in the
131 * first child configuration, but it finds one in the second, and adds it to the
132 * resulting combined configuration. So the property is still present (with a
133 * different value now).</li>
134 * <li>{@code addProperty()} can also be problematic: Most node combiners use
135 * special view nodes for linking parts of the original configurations' data
136 * together. If new properties are added to such a special node, they do not
137 * belong to any of the managed configurations and thus hang in the air. Using
138 * the same configurations as in the last example, the statement
139 *
140 * <pre>
141 * addProperty(&quot;database.user&quot;, &quot;scott&quot;);
142 * </pre>
143 *
144 * would cause such a hanging property. If now one of the child configurations
145 * is changed and the {@code CombinedConfiguration} is re-constructed, this
146 * property will disappear! (Add operations are not problematic if they result
147 * in a child configuration being updated. For instance an
148 * {@code addProperty("home.url", "localhost");} will alter the second child
149 * configuration - because the prefix <em>home</em> is here already present;
150 * when the {@code CombinedConfiguration} is re-constructed, this change is
151 * taken into account.)</li>
152 * </ul>
153 * <p>
154 * Because of such problems it is recommended to perform updates only on the
155 * managed child configurations.
156 * </p>
157 * <p>
158 * Whenever the node structure of a {@code CombinedConfiguration} becomes
159 * invalid (either because one of the contained configurations was modified or
160 * because the {@code invalidate()} method was directly called) an event is
161 * generated. So this can be detected by interested event listeners. This also
162 * makes it possible to add a combined configuration into another one.
163 * </p>
164 * <p>
165 * Notes about thread-safety: This configuration implementation uses a
166 * {@code Synchronizer} object to protect instances against concurrent access.
167 * The concrete {@code Synchronizer} implementation used determines whether an
168 * instance of this class is thread-safe or not. In contrast to other
169 * implementations derived from {@link BaseHierarchicalConfiguration},
170 * thread-safety is an issue here because the nodes structure used by this
171 * configuration has to be constructed dynamically when a child configuration is
172 * changed. Therefore, when multiple threads are involved which also manipulate
173 * one of the child configurations, a proper {@code Synchronizer} object should
174 * be set. Note that the {@code Synchronizer} objects used by the child
175 * configurations do not really matter. Because immutable in-memory nodes
176 * structures are used for them there is no danger that updates on child
177 * configurations could interfere with read operations on the combined
178 * configuration.
179 * </p>
180 *
181 * @since 1.3
182 * @version $Id: CombinedConfiguration.java 1790899 2017-04-10 21:56:46Z ggregory $
183 */
184public class CombinedConfiguration extends BaseHierarchicalConfiguration implements
185        EventListener<ConfigurationEvent>, Cloneable
186{
187    /**
188     * Constant for the event type fired when the internal node structure of a
189     * combined configuration becomes invalid.
190     *
191     * @since 2.0
192     */
193    public static final EventType<ConfigurationEvent> COMBINED_INVALIDATE =
194            new EventType<>(ConfigurationEvent.ANY,
195                    "COMBINED_INVALIDATE");
196
197    /** Constant for the expression engine for parsing the at path. */
198    private static final DefaultExpressionEngine AT_ENGINE = DefaultExpressionEngine.INSTANCE;
199
200    /** Constant for the default node combiner. */
201    private static final NodeCombiner DEFAULT_COMBINER = new UnionCombiner();
202
203    /** Constant for a root node for an empty configuration. */
204    private static final ImmutableNode EMPTY_ROOT = new ImmutableNode.Builder()
205            .create();
206
207    /** Stores the combiner. */
208    private NodeCombiner nodeCombiner;
209
210    /** Stores a list with the contained configurations. */
211    private List<ConfigData> configurations;
212
213    /** Stores a map with the named configurations. */
214    private Map<String, Configuration> namedConfigurations;
215
216    /**
217     * An expression engine used for converting child configurations to
218     * hierarchical ones.
219     */
220    private ExpressionEngine conversionExpressionEngine;
221
222    /** A flag whether this configuration is up-to-date. */
223    private boolean upToDate;
224
225    /**
226     * Creates a new instance of {@code CombinedConfiguration} and
227     * initializes the combiner to be used.
228     *
229     * @param comb the node combiner (can be <b>null</b>, then a union combiner
230     * is used as default)
231     */
232    public CombinedConfiguration(NodeCombiner comb)
233    {
234        nodeCombiner = (comb != null) ? comb : DEFAULT_COMBINER;
235        initChildCollections();
236    }
237
238    /**
239     * Creates a new instance of {@code CombinedConfiguration} that uses
240     * a union combiner.
241     *
242     * @see org.apache.commons.configuration2.tree.UnionCombiner
243     */
244    public CombinedConfiguration()
245    {
246        this(null);
247    }
248
249    /**
250     * Returns the node combiner that is used for creating the combined node
251     * structure.
252     *
253     * @return the node combiner
254     */
255    public NodeCombiner getNodeCombiner()
256    {
257        beginRead(true);
258        try
259        {
260            return nodeCombiner;
261        }
262        finally
263        {
264            endRead();
265        }
266    }
267
268    /**
269     * Sets the node combiner. This object will be used when the combined node
270     * structure is to be constructed. It must not be <b>null</b>, otherwise an
271     * {@code IllegalArgumentException} exception is thrown. Changing the
272     * node combiner causes an invalidation of this combined configuration, so
273     * that the new combiner immediately takes effect.
274     *
275     * @param nodeCombiner the node combiner
276     */
277    public void setNodeCombiner(NodeCombiner nodeCombiner)
278    {
279        if (nodeCombiner == null)
280        {
281            throw new IllegalArgumentException(
282                    "Node combiner must not be null!");
283        }
284
285        beginWrite(true);
286        try
287        {
288            this.nodeCombiner = nodeCombiner;
289            invalidateInternal();
290        }
291        finally
292        {
293            endWrite();
294        }
295    }
296
297    /**
298     * Returns the {@code ExpressionEngine} for converting flat child
299     * configurations to hierarchical ones.
300     *
301     * @return the conversion expression engine
302     * @since 1.6
303     */
304    public ExpressionEngine getConversionExpressionEngine()
305    {
306        beginRead(true);
307        try
308        {
309            return conversionExpressionEngine;
310        }
311        finally
312        {
313            endRead();
314        }
315    }
316
317    /**
318     * Sets the {@code ExpressionEngine} for converting flat child
319     * configurations to hierarchical ones. When constructing the root node for
320     * this combined configuration the properties of all child configurations
321     * must be combined to a single hierarchical node structure. In this
322     * process, non hierarchical configurations are converted to hierarchical
323     * ones first. This can be problematic if a child configuration contains
324     * keys that are no compatible with the default expression engine used by
325     * hierarchical configurations. Therefore it is possible to specify a
326     * specific expression engine to be used for this purpose.
327     *
328     * @param conversionExpressionEngine the conversion expression engine
329     * @see ConfigurationUtils#convertToHierarchical(Configuration, ExpressionEngine)
330     * @since 1.6
331     */
332    public void setConversionExpressionEngine(
333            ExpressionEngine conversionExpressionEngine)
334    {
335        beginWrite(true);
336        try
337        {
338            this.conversionExpressionEngine = conversionExpressionEngine;
339        }
340        finally
341        {
342            endWrite();
343        }
344    }
345
346    /**
347     * Adds a new configuration to this combined configuration. It is possible
348     * (but not mandatory) to give the new configuration a name. This name must
349     * be unique, otherwise a {@code ConfigurationRuntimeException} will
350     * be thrown. With the optional {@code at} argument you can specify
351     * where in the resulting node structure the content of the added
352     * configuration should appear. This is a string that uses dots as property
353     * delimiters (independent on the current expression engine). For instance
354     * if you pass in the string {@code "database.tables"},
355     * all properties of the added configuration will occur in this branch.
356     *
357     * @param config the configuration to add (must not be <b>null</b>)
358     * @param name the name of this configuration (can be <b>null</b>)
359     * @param at the position of this configuration in the combined tree (can be
360     * <b>null</b>)
361     */
362    public void addConfiguration(Configuration config, String name,
363            String at)
364    {
365        if (config == null)
366        {
367            throw new IllegalArgumentException(
368                    "Added configuration must not be null!");
369        }
370
371        beginWrite(true);
372        try
373        {
374            if (name != null && namedConfigurations.containsKey(name))
375            {
376                throw new ConfigurationRuntimeException(
377                        "A configuration with the name '"
378                                + name
379                                + "' already exists in this combined configuration!");
380            }
381
382            ConfigData cd = new ConfigData(config, name, at);
383            if (getLogger().isDebugEnabled())
384            {
385                getLogger()
386                        .debug("Adding configuration " + config + " with name "
387                                + name);
388            }
389            configurations.add(cd);
390            if (name != null)
391            {
392                namedConfigurations.put(name, config);
393            }
394
395            invalidateInternal();
396        }
397        finally
398        {
399            endWrite();
400        }
401        registerListenerAt(config);
402    }
403
404    /**
405     * Adds a new configuration to this combined configuration with an optional
406     * name. The new configuration's properties will be added under the root of
407     * the combined node structure.
408     *
409     * @param config the configuration to add (must not be <b>null</b>)
410     * @param name the name of this configuration (can be <b>null</b>)
411     */
412    public void addConfiguration(Configuration config, String name)
413    {
414        addConfiguration(config, name, null);
415    }
416
417    /**
418     * Adds a new configuration to this combined configuration. The new
419     * configuration is not given a name. Its properties will be added under the
420     * root of the combined node structure.
421     *
422     * @param config the configuration to add (must not be <b>null</b>)
423     */
424    public void addConfiguration(Configuration config)
425    {
426        addConfiguration(config, null, null);
427    }
428
429    /**
430     * Returns the number of configurations that are contained in this combined
431     * configuration.
432     *
433     * @return the number of contained configurations
434     */
435    public int getNumberOfConfigurations()
436    {
437        beginRead(true);
438        try
439        {
440            return getNumberOfConfigurationsInternal();
441        }
442        finally
443        {
444            endRead();
445        }
446    }
447
448    /**
449     * Returns the configuration at the specified index. The contained
450     * configurations are numbered in the order they were added to this combined
451     * configuration. The index of the first configuration is 0.
452     *
453     * @param index the index
454     * @return the configuration at this index
455     */
456    public Configuration getConfiguration(int index)
457    {
458        beginRead(true);
459        try
460        {
461            ConfigData cd = configurations.get(index);
462            return cd.getConfiguration();
463        }
464        finally
465        {
466            endRead();
467        }
468    }
469
470    /**
471     * Returns the configuration with the given name. This can be <b>null</b>
472     * if no such configuration exists.
473     *
474     * @param name the name of the configuration
475     * @return the configuration with this name
476     */
477    public Configuration getConfiguration(String name)
478    {
479        beginRead(true);
480        try
481        {
482            return namedConfigurations.get(name);
483        }
484        finally
485        {
486            endRead();
487        }
488    }
489
490    /**
491     * Returns a List of all the configurations that have been added.
492     * @return A List of all the configurations.
493     * @since 1.7
494     */
495    public List<Configuration> getConfigurations()
496    {
497        beginRead(true);
498        try
499        {
500            List<Configuration> list =
501                    new ArrayList<>(getNumberOfConfigurationsInternal());
502            for (ConfigData cd : configurations)
503            {
504                list.add(cd.getConfiguration());
505            }
506            return list;
507        }
508        finally
509        {
510            endRead();
511        }
512    }
513
514    /**
515     * Returns a List of the names of all the configurations that have been
516     * added in the order they were added. A NULL value will be present in
517     * the list for each configuration that was added without a name.
518     * @return A List of all the configuration names.
519     * @since 1.7
520     */
521    public List<String> getConfigurationNameList()
522    {
523        beginRead(true);
524        try
525        {
526            List<String> list = new ArrayList<>(getNumberOfConfigurationsInternal());
527            for (ConfigData cd : configurations)
528            {
529                list.add(cd.getName());
530            }
531            return list;
532        }
533        finally
534        {
535            endRead();
536        }
537    }
538
539    /**
540     * Removes the specified configuration from this combined configuration.
541     *
542     * @param config the configuration to be removed
543     * @return a flag whether this configuration was found and could be removed
544     */
545    public boolean removeConfiguration(Configuration config)
546    {
547        for (int index = 0; index < getNumberOfConfigurations(); index++)
548        {
549            if (configurations.get(index).getConfiguration() == config)
550            {
551                removeConfigurationAt(index);
552                return true;
553            }
554        }
555
556        return false;
557    }
558
559    /**
560     * Removes the configuration at the specified index.
561     *
562     * @param index the index
563     * @return the removed configuration
564     */
565    public Configuration removeConfigurationAt(int index)
566    {
567        ConfigData cd = configurations.remove(index);
568        if (cd.getName() != null)
569        {
570            namedConfigurations.remove(cd.getName());
571        }
572        unregisterListenerAt(cd.getConfiguration());
573        invalidateInternal();
574        return cd.getConfiguration();
575    }
576
577    /**
578     * Removes the configuration with the specified name.
579     *
580     * @param name the name of the configuration to be removed
581     * @return the removed configuration (<b>null</b> if this configuration
582     * was not found)
583     */
584    public Configuration removeConfiguration(String name)
585    {
586        Configuration conf = getConfiguration(name);
587        if (conf != null)
588        {
589            removeConfiguration(conf);
590        }
591        return conf;
592    }
593
594    /**
595     * Returns a set with the names of all configurations contained in this
596     * combined configuration. Of course here are only these configurations
597     * listed, for which a name was specified when they were added.
598     *
599     * @return a set with the names of the contained configurations (never
600     * <b>null</b>)
601     */
602    public Set<String> getConfigurationNames()
603    {
604        beginRead(true);
605        try
606        {
607            return namedConfigurations.keySet();
608        }
609        finally
610        {
611            endRead();
612        }
613    }
614
615    /**
616     * Invalidates this combined configuration. This means that the next time a
617     * property is accessed the combined node structure must be re-constructed.
618     * Invalidation of a combined configuration also means that an event of type
619     * {@code EVENT_COMBINED_INVALIDATE} is fired. Note that while other
620     * events most times appear twice (once before and once after an update),
621     * this event is only fired once (after update).
622     */
623    public void invalidate()
624    {
625        beginWrite(true);
626        try
627        {
628            invalidateInternal();
629        }
630        finally
631        {
632            endWrite();
633        }
634    }
635
636    /**
637     * Event listener call back for configuration update events. This method is
638     * called whenever one of the contained configurations was modified. It
639     * invalidates this combined configuration.
640     *
641     * @param event the update event
642     */
643    @Override
644    public void onEvent(ConfigurationEvent event)
645    {
646        if (event.isBeforeUpdate())
647        {
648            invalidate();
649        }
650    }
651
652    /**
653     * Clears this configuration. All contained configurations will be removed.
654     */
655    @Override
656    protected void clearInternal()
657    {
658        unregisterListenerAtChildren();
659        initChildCollections();
660        invalidateInternal();
661    }
662
663    /**
664     * Returns a copy of this object. This implementation performs a deep clone,
665     * i.e. all contained configurations will be cloned, too. For this to work,
666     * all contained configurations must be cloneable. Registered event
667     * listeners won't be cloned. The clone will use the same node combiner than
668     * the original.
669     *
670     * @return the copied object
671     */
672    @Override
673    public Object clone()
674    {
675        beginRead(false);
676        try
677        {
678            CombinedConfiguration copy = (CombinedConfiguration) super.clone();
679            copy.initChildCollections();
680            for (ConfigData cd : configurations)
681            {
682                copy.addConfiguration(ConfigurationUtils.cloneConfiguration(cd
683                        .getConfiguration()), cd.getName(), cd.getAt());
684            }
685
686            return copy;
687        }
688        finally
689        {
690            endRead();
691        }
692    }
693
694    /**
695     * Returns the configuration source, in which the specified key is defined.
696     * This method will determine the configuration node that is identified by
697     * the given key. The following constellations are possible:
698     * <ul>
699     * <li>If no node object is found for this key, <b>null</b> is returned.</li>
700     * <li>If the key maps to multiple nodes belonging to different
701     * configuration sources, a {@code IllegalArgumentException} is
702     * thrown (in this case no unique source can be determined).</li>
703     * <li>If exactly one node is found for the key, the (child) configuration
704     * object, to which the node belongs is determined and returned.</li>
705     * <li>For keys that have been added directly to this combined
706     * configuration and that do not belong to the namespaces defined by
707     * existing child configurations this configuration will be returned.</li>
708     * </ul>
709     *
710     * @param key the key of a configuration property
711     * @return the configuration, to which this property belongs or <b>null</b>
712     * if the key cannot be resolved
713     * @throws IllegalArgumentException if the key maps to multiple properties
714     * and the source cannot be determined, or if the key is <b>null</b>
715     * @since 1.5
716     */
717    public Configuration getSource(String key)
718    {
719        if (key == null)
720        {
721            throw new IllegalArgumentException("Key must not be null!");
722        }
723
724        Set<Configuration> sources = getSources(key);
725        if (sources.isEmpty())
726        {
727            return null;
728        }
729        Iterator<Configuration> iterator = sources.iterator();
730        Configuration source = iterator.next();
731        if (iterator.hasNext())
732        {
733            throw new IllegalArgumentException("The key " + key
734                    + " is defined by multiple sources!");
735        }
736        return source;
737    }
738
739    /**
740     * Returns a set with the configuration sources, in which the specified key
741     * is defined. This method determines the configuration nodes that are
742     * identified by the given key. It then determines the configuration sources
743     * to which these nodes belong and adds them to the result set. Note the
744     * following points:
745     * <ul>
746     * <li>If no node object is found for this key, an empty set is returned.</li>
747     * <li>For keys that have been added directly to this combined configuration
748     * and that do not belong to the namespaces defined by existing child
749     * configurations this combined configuration is contained in the result
750     * set.</li>
751     * </ul>
752     *
753     * @param key the key of a configuration property
754     * @return a set with the configuration sources, which contain this property
755     * @since 2.0
756     */
757    public Set<Configuration> getSources(String key)
758    {
759        beginRead(false);
760        try
761        {
762            List<QueryResult<ImmutableNode>> results = fetchNodeList(key);
763            Set<Configuration> sources = new HashSet<>();
764
765            for (QueryResult<ImmutableNode> result : results)
766            {
767                Set<Configuration> resultSources =
768                        findSourceConfigurations(result.getNode());
769                if (resultSources.isEmpty())
770                {
771                    // key must be defined in combined configuration
772                    sources.add(this);
773                }
774                else
775                {
776                    sources.addAll(resultSources);
777                }
778            }
779
780            return sources;
781        }
782        finally
783        {
784            endRead();
785        }
786    }
787
788    /**
789     * {@inheritDoc} This implementation checks whether a combined root node
790     * is available. If not, it is constructed by requesting a write lock.
791     */
792    @Override
793    protected void beginRead(boolean optimize)
794    {
795        if (optimize)
796        {
797            // just need a lock, don't construct configuration
798            super.beginRead(true);
799            return;
800        }
801
802        boolean lockObtained = false;
803        do
804        {
805            super.beginRead(false);
806            if (isUpToDate())
807            {
808                lockObtained = true;
809            }
810            else
811            {
812                // release read lock and try to obtain a write lock
813                endRead();
814                beginWrite(false); // this constructs the root node
815                endWrite();
816            }
817        } while (!lockObtained);
818    }
819
820    /**
821     * {@inheritDoc} This implementation checks whether a combined root node
822     * is available. If not, it is constructed now.
823     */
824    @Override
825    protected void beginWrite(boolean optimize)
826    {
827        super.beginWrite(true);
828        if (optimize)
829        {
830            // just need a lock, don't construct configuration
831            return;
832        }
833
834        try
835        {
836            if (!isUpToDate())
837            {
838                getSubConfigurationParentModel().replaceRoot(
839                        constructCombinedNode(), this);
840                upToDate = true;
841            }
842        }
843        catch (RuntimeException rex)
844        {
845            endWrite();
846            throw rex;
847        }
848    }
849
850    /**
851     * Returns a flag whether this configuration has been invalidated. This
852     * means that the combined nodes structure has to be rebuilt before the
853     * configuration can be accessed.
854     *
855     * @return a flag whether this configuration is invalid
856     */
857    private boolean isUpToDate()
858    {
859        return upToDate;
860    }
861
862    /**
863     * Marks this configuration as invalid. This means that the next access
864     * re-creates the root node. An invalidate event is also fired. Note:
865     * This implementation expects that an exclusive (write) lock is held on
866     * this instance.
867     */
868    private void invalidateInternal()
869    {
870        upToDate = false;
871        fireEvent(COMBINED_INVALIDATE, null, null, false);
872    }
873
874    /**
875     * Initializes internal data structures for storing information about
876     * child configurations.
877     */
878    private void initChildCollections()
879    {
880        configurations = new ArrayList<>();
881        namedConfigurations = new HashMap<>();
882    }
883
884    /**
885     * Creates the root node of this combined configuration.
886     *
887     * @return the combined root node
888     */
889    private ImmutableNode constructCombinedNode()
890    {
891        if (getNumberOfConfigurationsInternal() < 1)
892        {
893            if (getLogger().isDebugEnabled())
894            {
895                getLogger().debug("No configurations defined for " + this);
896            }
897            return EMPTY_ROOT;
898        }
899
900        else
901        {
902            Iterator<ConfigData> it = configurations.iterator();
903            ImmutableNode node = it.next().getTransformedRoot();
904            while (it.hasNext())
905            {
906                node = nodeCombiner.combine(node,
907                        it.next().getTransformedRoot());
908            }
909            if (getLogger().isDebugEnabled())
910            {
911                ByteArrayOutputStream os = new ByteArrayOutputStream();
912                PrintStream stream = new PrintStream(os);
913                TreeUtils.printTree(stream, node);
914                getLogger().debug(os.toString());
915            }
916            return node;
917        }
918    }
919
920    /**
921     * Determines the configurations to which the specified node belongs. This
922     * is done by inspecting the nodes structures of all child configurations.
923     *
924     * @param node the node
925     * @return a set with the owning configurations
926     */
927    private Set<Configuration> findSourceConfigurations(ImmutableNode node)
928    {
929        Set<Configuration> result = new HashSet<>();
930        FindNodeVisitor<ImmutableNode> visitor =
931                new FindNodeVisitor<>(node);
932
933        for (ConfigData cd : configurations)
934        {
935            NodeTreeWalker.INSTANCE.walkBFS(cd.getRootNode(), visitor,
936                    getModel().getNodeHandler());
937            if (visitor.isFound())
938            {
939                result.add(cd.getConfiguration());
940                visitor.reset();
941            }
942        }
943
944        return result;
945    }
946
947    /**
948     * Registers this combined configuration as listener at the given child
949     * configuration.
950     *
951     * @param configuration the child configuration
952     */
953    private void registerListenerAt(Configuration configuration)
954    {
955        if (configuration instanceof EventSource)
956        {
957            ((EventSource) configuration).addEventListener(
958                    ConfigurationEvent.ANY, this);
959        }
960    }
961
962    /**
963     * Removes this combined configuration as listener from the given child
964     * configuration.
965     *
966     * @param configuration the child configuration
967     */
968    private void unregisterListenerAt(Configuration configuration)
969    {
970        if (configuration instanceof EventSource)
971        {
972            ((EventSource) configuration).removeEventListener(
973                    ConfigurationEvent.ANY, this);
974        }
975    }
976
977    /**
978     * Removes this combined configuration as listener from all child
979     * configurations. This method is called on a clear() operation.
980     */
981    private void unregisterListenerAtChildren()
982    {
983        if (configurations != null)
984        {
985            for (ConfigData child : configurations)
986            {
987                unregisterListenerAt(child.getConfiguration());
988            }
989        }
990    }
991
992    /**
993     * Returns the number of child configurations in this combined
994     * configuration. The internal list of child configurations is accessed
995     * without synchronization.
996     *
997     * @return the number of child configurations
998     */
999    private int getNumberOfConfigurationsInternal()
1000    {
1001        return configurations.size();
1002    }
1003
1004    /**
1005     * An internal helper class for storing information about contained
1006     * configurations.
1007     */
1008    private class ConfigData
1009    {
1010        /** Stores a reference to the configuration. */
1011        private final Configuration configuration;
1012
1013        /** Stores the name under which the configuration is stored. */
1014        private final String name;
1015
1016        /** Stores the at information as path of nodes. */
1017        private final Collection<String> atPath;
1018
1019        /** Stores the at string.*/
1020        private final String at;
1021
1022        /** Stores the root node for this child configuration.*/
1023        private ImmutableNode rootNode;
1024
1025        /**
1026         * Creates a new instance of {@code ConfigData} and initializes
1027         * it.
1028         *
1029         * @param config the configuration
1030         * @param n the name
1031         * @param at the at position
1032         */
1033        public ConfigData(Configuration config, String n, String at)
1034        {
1035            configuration = config;
1036            name = n;
1037            atPath = parseAt(at);
1038            this.at = at;
1039        }
1040
1041        /**
1042         * Returns the stored configuration.
1043         *
1044         * @return the configuration
1045         */
1046        public Configuration getConfiguration()
1047        {
1048            return configuration;
1049        }
1050
1051        /**
1052         * Returns the configuration's name.
1053         *
1054         * @return the name
1055         */
1056        public String getName()
1057        {
1058            return name;
1059        }
1060
1061        /**
1062         * Returns the at position of this configuration.
1063         *
1064         * @return the at position
1065         */
1066        public String getAt()
1067        {
1068            return at;
1069        }
1070
1071        /**
1072         * Returns the root node for this child configuration.
1073         *
1074         * @return the root node of this child configuration
1075         * @since 1.5
1076         */
1077        public ImmutableNode getRootNode()
1078        {
1079            return rootNode;
1080        }
1081
1082        /**
1083         * Returns the transformed root node of the stored configuration. The
1084         * term &quot;transformed&quot; means that an eventually defined at path
1085         * has been applied.
1086         *
1087         * @return the transformed root node
1088         */
1089        public ImmutableNode getTransformedRoot()
1090        {
1091            ImmutableNode configRoot = getRootNodeOfConfiguration();
1092            return (atPath == null) ? configRoot : prependAtPath(configRoot);
1093        }
1094
1095        /**
1096         * Prepends the at path to the given node.
1097         *
1098         * @param node the root node of the represented configuration
1099         * @return the new root node including the at path
1100         */
1101        private ImmutableNode prependAtPath(ImmutableNode node)
1102        {
1103            ImmutableNode.Builder pathBuilder = new ImmutableNode.Builder();
1104            Iterator<String> pathIterator = atPath.iterator();
1105            prependAtPathComponent(pathBuilder, pathIterator.next(),
1106                    pathIterator, node);
1107            return new ImmutableNode.Builder(1).addChild(pathBuilder.create())
1108                    .create();
1109        }
1110
1111        /**
1112         * Handles a single component of the at path. A corresponding node is
1113         * created and added to the hierarchical path to the original root node
1114         * of the configuration.
1115         *
1116         * @param builder the current node builder object
1117         * @param currentComponent the name of the current path component
1118         * @param components an iterator with all components of the at path
1119         * @param orgRoot the original root node of the wrapped configuration
1120         */
1121        private void prependAtPathComponent(ImmutableNode.Builder builder,
1122                String currentComponent, Iterator<String> components,
1123                ImmutableNode orgRoot)
1124        {
1125            builder.name(currentComponent);
1126            if (components.hasNext())
1127            {
1128                ImmutableNode.Builder childBuilder =
1129                        new ImmutableNode.Builder();
1130                prependAtPathComponent(childBuilder, components.next(),
1131                        components, orgRoot);
1132                builder.addChild(childBuilder.create());
1133            }
1134            else
1135            {
1136                builder.addChildren(orgRoot.getChildren());
1137                builder.addAttributes(orgRoot.getAttributes());
1138                builder.value(orgRoot.getValue());
1139            }
1140        }
1141
1142        /**
1143         * Obtains the root node of the wrapped configuration. If necessary, a
1144         * hierarchical representation of the configuration has to be created
1145         * first.
1146         *
1147         * @return the root node of the associated configuration
1148         */
1149        private ImmutableNode getRootNodeOfConfiguration()
1150        {
1151            getConfiguration().lock(LockMode.READ);
1152            try
1153            {
1154                ImmutableNode root =
1155                        ConfigurationUtils
1156                                .convertToHierarchical(getConfiguration(),
1157                                        conversionExpressionEngine).getNodeModel()
1158                                .getInMemoryRepresentation();
1159                rootNode = root;
1160                return root;
1161            }
1162            finally
1163            {
1164                getConfiguration().unlock(LockMode.READ);
1165            }
1166        }
1167
1168        /**
1169         * Splits the at path into its components.
1170         *
1171         * @param at the at string
1172         * @return a collection with the names of the single components
1173         */
1174        private Collection<String> parseAt(String at)
1175        {
1176            if (at == null)
1177            {
1178                return null;
1179            }
1180
1181            Collection<String> result = new ArrayList<>();
1182            DefaultConfigurationKey.KeyIterator it = new DefaultConfigurationKey(
1183                    AT_ENGINE, at).iterator();
1184            while (it.hasNext())
1185            {
1186                result.add(it.nextKey());
1187            }
1188            return result;
1189        }
1190    }
1191}