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.builder.combined;
018
019import java.util.HashMap;
020import java.util.Map;
021import java.util.concurrent.ConcurrentHashMap;
022import java.util.concurrent.ConcurrentMap;
023import java.util.concurrent.atomic.AtomicReference;
024
025import org.apache.commons.configuration2.ConfigurationUtils;
026import org.apache.commons.configuration2.FileBasedConfiguration;
027import org.apache.commons.configuration2.builder.BasicBuilderParameters;
028import org.apache.commons.configuration2.builder.BasicConfigurationBuilder;
029import org.apache.commons.configuration2.builder.BuilderParameters;
030import org.apache.commons.configuration2.builder.ConfigurationBuilderEvent;
031import org.apache.commons.configuration2.builder.ConfigurationBuilderResultCreatedEvent;
032import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
033import org.apache.commons.configuration2.event.Event;
034import org.apache.commons.configuration2.event.EventListener;
035import org.apache.commons.configuration2.event.EventListenerList;
036import org.apache.commons.configuration2.event.EventType;
037import org.apache.commons.configuration2.ex.ConfigurationException;
038import org.apache.commons.configuration2.interpol.ConfigurationInterpolator;
039import org.apache.commons.configuration2.interpol.InterpolatorSpecification;
040import org.apache.commons.lang3.concurrent.ConcurrentUtils;
041
042/**
043 * <p>
044 * A specialized {@code ConfigurationBuilder} implementation providing access to
045 * multiple file-based configurations based on a file name pattern.
046 * </p>
047 * <p>
048 * This builder class is initialized with a pattern string and a
049 * {@link ConfigurationInterpolator} object. Each time a configuration is
050 * requested, the pattern is evaluated against the
051 * {@code ConfigurationInterpolator} (so all variables are replaced by their
052 * current values). The resulting string is interpreted as a file name for a
053 * configuration file to be loaded. For example, providing a pattern of
054 * <em>file:///opt/config/${product}/${client}/config.xml</em> will result in
055 * <em>product</em> and <em>client</em> being resolved on every call. By storing
056 * configuration files in a corresponding directory structure, specialized
057 * configuration files associated with a specific product and client can be
058 * loaded. Thus an application can be made multi-tenant in a transparent way.
059 * </p>
060 * <p>
061 * This builder class keeps a map with configuration builders for configurations
062 * already loaded. The {@code getConfiguration()} method first evaluates the
063 * pattern string and checks whether a builder for the resulting file name is
064 * available. If yes, it is queried for its configuration. Otherwise, a new
065 * file-based configuration builder is created now and initialized.
066 * </p>
067 * <p>
068 * Configuration of an instance happens in the usual way for configuration
069 * builders. A {@link MultiFileBuilderParametersImpl} parameters object is
070 * expected which must contain a file name pattern string and a
071 * {@code ConfigurationInterpolator}. Other properties of this parameters object
072 * are used to initialize the builders for managed configurations.
073 * </p>
074 *
075 * @since 2.0
076 * @param <T> the concrete type of {@code Configuration} objects created by this
077 *        builder
078 */
079public class MultiFileConfigurationBuilder<T extends FileBasedConfiguration>
080        extends BasicConfigurationBuilder<T>
081{
082    /**
083     * Constant for the name of the key referencing the
084     * {@code ConfigurationInterpolator} in this builder's parameters.
085     */
086    private static final String KEY_INTERPOLATOR = "interpolator";
087
088    /** A cache for already created managed builders. */
089    private final ConcurrentMap<String, FileBasedConfigurationBuilder<T>> managedBuilders =
090            new ConcurrentHashMap<>();
091
092    /** Stores the {@code ConfigurationInterpolator} object. */
093    private final AtomicReference<ConfigurationInterpolator> interpolator =
094            new AtomicReference<>();
095
096    /**
097     * A flag for preventing reentrant access to managed builders on
098     * interpolation of the file name pattern.
099     */
100    private final ThreadLocal<Boolean> inInterpolation =
101            new ThreadLocal<>();
102
103    /** A list for the event listeners to be passed to managed builders. */
104    private final EventListenerList configurationListeners = new EventListenerList();
105
106    /**
107     * A specialized event listener which gets registered at all managed
108     * builders. This listener just propagates notifications from managed
109     * builders to the listeners registered at this
110     * {@code MultiFileConfigurationBuilder}.
111     */
112    private final EventListener<ConfigurationBuilderEvent> managedBuilderDelegationListener =
113            event -> handleManagedBuilderEvent(event);
114
115    /**
116     * Creates a new instance of {@code MultiFileConfigurationBuilder} and sets
117     * initialization parameters and a flag whether initialization failures
118     * should be ignored.
119     *
120     * @param resCls the result configuration class
121     * @param params a map with initialization parameters
122     * @param allowFailOnInit a flag whether initialization errors should be
123     *        ignored
124     * @throws IllegalArgumentException if the result class is <b>null</b>
125     */
126    public MultiFileConfigurationBuilder(final Class<? extends T> resCls,
127            final Map<String, Object> params, final boolean allowFailOnInit)
128    {
129        super(resCls, params, allowFailOnInit);
130    }
131
132    /**
133     * Creates a new instance of {@code MultiFileConfigurationBuilder} and sets
134     * initialization parameters.
135     *
136     * @param resCls the result configuration class
137     * @param params a map with initialization parameters
138     * @throws IllegalArgumentException if the result class is <b>null</b>
139     */
140    public MultiFileConfigurationBuilder(final Class<? extends T> resCls,
141            final Map<String, Object> params)
142    {
143        super(resCls, params);
144    }
145
146    /**
147     * Creates a new instance of {@code MultiFileConfigurationBuilder} without
148     * setting initialization parameters.
149     *
150     * @param resCls the result configuration class
151     * @throws IllegalArgumentException if the result class is <b>null</b>
152     */
153    public MultiFileConfigurationBuilder(final Class<? extends T> resCls)
154    {
155        super(resCls);
156    }
157
158    /**
159     * {@inheritDoc} This method is overridden to adapt the return type.
160     */
161    @Override
162    public MultiFileConfigurationBuilder<T> configure(final BuilderParameters... params)
163    {
164        super.configure(params);
165        return this;
166    }
167
168    /**
169     * {@inheritDoc} This implementation evaluates the file name pattern using
170     * the configured {@code ConfigurationInterpolator}. If this file has
171     * already been loaded, the corresponding builder is accessed. Otherwise, a
172     * new builder is created for loading this configuration file.
173     */
174    @Override
175    public T getConfiguration() throws ConfigurationException
176    {
177        return getManagedBuilder().getConfiguration();
178    }
179
180    /**
181     * Returns the managed {@code FileBasedConfigurationBuilder} for the current
182     * file name pattern. It is determined based on the evaluation of the file
183     * name pattern using the configured {@code ConfigurationInterpolator}. If
184     * this is the first access to this configuration file, the builder is
185     * created.
186     *
187     * @return the configuration builder for the configuration corresponding to
188     *         the current evaluation of the file name pattern
189     * @throws ConfigurationException if the builder cannot be determined (e.g.
190     *         due to missing initialization parameters)
191     */
192    public FileBasedConfigurationBuilder<T> getManagedBuilder()
193            throws ConfigurationException
194    {
195        final Map<String, Object> params = getParameters();
196        final MultiFileBuilderParametersImpl multiParams =
197                MultiFileBuilderParametersImpl.fromParameters(params, true);
198        if (multiParams.getFilePattern() == null)
199        {
200            throw new ConfigurationException("No file name pattern is set!");
201        }
202        final String fileName = fetchFileName(multiParams);
203
204        FileBasedConfigurationBuilder<T> builder =
205                getManagedBuilders().get(fileName);
206        if (builder == null)
207        {
208            builder =
209                    createInitializedManagedBuilder(fileName,
210                            createManagedBuilderParameters(params, multiParams));
211            final FileBasedConfigurationBuilder<T> newBuilder =
212                    ConcurrentUtils.putIfAbsent(getManagedBuilders(), fileName,
213                            builder);
214            if (newBuilder == builder)
215            {
216                initListeners(newBuilder);
217            }
218            else
219            {
220                builder = newBuilder;
221            }
222        }
223        return builder;
224    }
225
226    /**
227     * {@inheritDoc} This implementation ensures that the listener is also added
228     * to managed configuration builders if necessary. Listeners for the builder-related
229     * event types are excluded because otherwise they would be triggered by the
230     * internally used configuration builders.
231     */
232    @Override
233    public synchronized <E extends Event> void addEventListener(
234            final EventType<E> eventType, final EventListener<? super E> l)
235    {
236        super.addEventListener(eventType, l);
237        if (isEventTypeForManagedBuilders(eventType))
238        {
239            for (final FileBasedConfigurationBuilder<T> b : getManagedBuilders()
240                    .values())
241            {
242                b.addEventListener(eventType, l);
243            }
244            configurationListeners.addEventListener(eventType, l);
245        }
246    }
247
248    /**
249     * {@inheritDoc} This implementation ensures that the listener is also
250     * removed from managed configuration builders if necessary.
251     */
252    @Override
253    public synchronized <E extends Event> boolean removeEventListener(
254            final EventType<E> eventType, final EventListener<? super E> l)
255    {
256        final boolean result = super.removeEventListener(eventType, l);
257        if (isEventTypeForManagedBuilders(eventType))
258        {
259            for (final FileBasedConfigurationBuilder<T> b : getManagedBuilders()
260                    .values())
261            {
262                b.removeEventListener(eventType, l);
263            }
264            configurationListeners.removeEventListener(eventType, l);
265        }
266        return result;
267    }
268
269    /**
270     * {@inheritDoc} This implementation clears the cache with all managed
271     * builders.
272     */
273    @Override
274    public synchronized void resetParameters()
275    {
276        for (final FileBasedConfigurationBuilder<T> b : getManagedBuilders().values())
277        {
278            b.removeEventListener(ConfigurationBuilderEvent.ANY,
279                    managedBuilderDelegationListener);
280        }
281        getManagedBuilders().clear();
282        interpolator.set(null);
283        super.resetParameters();
284    }
285
286    /**
287     * Returns the {@code ConfigurationInterpolator} used by this instance. This
288     * is the object used for evaluating the file name pattern. It is created on
289     * demand.
290     *
291     * @return the {@code ConfigurationInterpolator}
292     */
293    protected ConfigurationInterpolator getInterpolator()
294    {
295        ConfigurationInterpolator result;
296        boolean done;
297
298        // This might create multiple instances under high load,
299        // however, always the same instance is returned.
300        do
301        {
302            result = interpolator.get();
303            if (result != null)
304            {
305                done = true;
306            }
307            else
308            {
309                result = createInterpolator();
310                done = interpolator.compareAndSet(null, result);
311            }
312        } while (!done);
313
314        return result;
315    }
316
317    /**
318     * Creates the {@code ConfigurationInterpolator} to be used by this
319     * instance. This method is called when a file name is to be constructed,
320     * but no current {@code ConfigurationInterpolator} instance is available.
321     * It obtains an instance from this builder's parameters. If no properties
322     * of the {@code ConfigurationInterpolator} are specified in the parameters,
323     * a default instance without lookups is returned (which is probably not
324     * very helpful).
325     *
326     * @return the {@code ConfigurationInterpolator} to be used
327     */
328    protected ConfigurationInterpolator createInterpolator()
329    {
330        final InterpolatorSpecification spec =
331                BasicBuilderParameters
332                        .fetchInterpolatorSpecification(getParameters());
333        return ConfigurationInterpolator.fromSpecification(spec);
334    }
335
336    /**
337     * Determines the file name of a configuration based on the file name
338     * pattern. This method is called on every access to this builder's
339     * configuration. It obtains the {@link ConfigurationInterpolator} from this
340     * builder's parameters and uses it to interpolate the file name pattern.
341     *
342     * @param multiParams the parameters object for this builder
343     * @return the name of the configuration file to be loaded
344     */
345    protected String constructFileName(
346            final MultiFileBuilderParametersImpl multiParams)
347    {
348        final ConfigurationInterpolator ci = getInterpolator();
349        return String.valueOf(ci.interpolate(multiParams.getFilePattern()));
350    }
351
352    /**
353     * Creates a builder for a managed configuration. This method is called
354     * whenever a configuration for a file name is requested which has not yet
355     * been loaded. The passed in map with parameters is populated from this
356     * builder's configuration (i.e. the basic parameters plus the optional
357     * parameters for managed builders). This base implementation creates a
358     * standard builder for file-based configurations. Derived classes may
359     * override it to create special purpose builders.
360     *
361     * @param fileName the name of the file to be loaded
362     * @param params a map with initialization parameters for the new builder
363     * @return the newly created builder instance
364     * @throws ConfigurationException if an error occurs
365     */
366    protected FileBasedConfigurationBuilder<T> createManagedBuilder(
367            final String fileName, final Map<String, Object> params)
368            throws ConfigurationException
369    {
370        return new FileBasedConfigurationBuilder<>(getResultClass(), params,
371                isAllowFailOnInit());
372    }
373
374    /**
375     * Creates a fully initialized builder for a managed configuration. This
376     * method is called by {@code getConfiguration()} whenever a configuration
377     * file is requested which has not yet been loaded. This implementation
378     * delegates to {@code createManagedBuilder()} for actually creating the
379     * builder object. Then it sets the location to the configuration file.
380     *
381     * @param fileName the name of the file to be loaded
382     * @param params a map with initialization parameters for the new builder
383     * @return the newly created and initialized builder instance
384     * @throws ConfigurationException if an error occurs
385     */
386    protected FileBasedConfigurationBuilder<T> createInitializedManagedBuilder(
387            final String fileName, final Map<String, Object> params)
388            throws ConfigurationException
389    {
390        final FileBasedConfigurationBuilder<T> managedBuilder =
391                createManagedBuilder(fileName, params);
392        managedBuilder.getFileHandler().setFileName(fileName);
393        return managedBuilder;
394    }
395
396    /**
397     * Returns the map with the managed builders created so far by this
398     * {@code MultiFileConfigurationBuilder}. This map is exposed to derived
399     * classes so they can access managed builders directly. However, derived
400     * classes are not expected to manipulate this map.
401     *
402     * @return the map with the managed builders
403     */
404    protected ConcurrentMap<String, FileBasedConfigurationBuilder<T>> getManagedBuilders()
405    {
406        return managedBuilders;
407    }
408
409    /**
410     * Registers event listeners at the passed in newly created managed builder.
411     * This method registers a special {@code EventListener} which propagates
412     * builder events to listeners registered at this builder. In addition,
413     * {@code ConfigurationListener} and {@code ConfigurationErrorListener}
414     * objects are registered at the new builder.
415     *
416     * @param newBuilder the builder to be initialized
417     */
418    private void initListeners(final FileBasedConfigurationBuilder<T> newBuilder)
419    {
420        copyEventListeners(newBuilder, configurationListeners);
421        newBuilder.addEventListener(ConfigurationBuilderEvent.ANY,
422                managedBuilderDelegationListener);
423    }
424
425    /**
426     * Generates a file name for a managed builder based on the file name
427     * pattern. This method prevents infinite loops which could happen if the
428     * file name pattern cannot be resolved and the
429     * {@code ConfigurationInterpolator} used by this object causes a recursive
430     * lookup to this builder's configuration.
431     *
432     * @param multiParams the current builder parameters
433     * @return the file name for a managed builder
434     */
435    private String fetchFileName(final MultiFileBuilderParametersImpl multiParams)
436    {
437        String fileName;
438        final Boolean reentrant = inInterpolation.get();
439        if (reentrant != null && reentrant.booleanValue())
440        {
441            fileName = multiParams.getFilePattern();
442        }
443        else
444        {
445            inInterpolation.set(Boolean.TRUE);
446            try
447            {
448                fileName = constructFileName(multiParams);
449            }
450            finally
451            {
452                inInterpolation.set(Boolean.FALSE);
453            }
454        }
455        return fileName;
456    }
457
458    /**
459     * Handles events received from managed configuration builders. This method
460     * creates a new event with a source pointing to this builder and propagates
461     * it to all registered listeners.
462     *
463     * @param event the event received from a managed builder
464     */
465    private void handleManagedBuilderEvent(final ConfigurationBuilderEvent event)
466    {
467        if (ConfigurationBuilderEvent.RESET.equals(event.getEventType()))
468        {
469            resetResult();
470        }
471        else
472        {
473            fireBuilderEvent(createEventWithChangedSource(event));
474        }
475    }
476
477    /**
478     * Creates a new {@code ConfigurationBuilderEvent} based on the passed in
479     * event, but with the source changed to this builder. This method is called
480     * when an event was received from a managed builder. In this case, the
481     * event has to be passed to the builder listeners registered at this
482     * object, but with the correct source property.
483     *
484     * @param event the event received from a managed builder
485     * @return the event to be propagated
486     */
487    private ConfigurationBuilderEvent createEventWithChangedSource(
488            final ConfigurationBuilderEvent event)
489    {
490        if (ConfigurationBuilderResultCreatedEvent.RESULT_CREATED.equals(event
491                .getEventType()))
492        {
493            return new ConfigurationBuilderResultCreatedEvent(this,
494                    ConfigurationBuilderResultCreatedEvent.RESULT_CREATED,
495                    ((ConfigurationBuilderResultCreatedEvent) event)
496                            .getConfiguration());
497        }
498        @SuppressWarnings("unchecked")
499        final
500        // This is safe due to the constructor of ConfigurationBuilderEvent
501        EventType<? extends ConfigurationBuilderEvent> type =
502                (EventType<? extends ConfigurationBuilderEvent>) event
503                        .getEventType();
504        return new ConfigurationBuilderEvent(this, type);
505    }
506
507    /**
508     * Creates a map with parameters for a new managed configuration builder.
509     * This method merges the basic parameters set for this builder with the
510     * specific parameters object for managed builders (if provided).
511     *
512     * @param params the parameters of this builder
513     * @param multiParams the parameters object for this builder
514     * @return the parameters for a new managed builder
515     */
516    private static Map<String, Object> createManagedBuilderParameters(
517            final Map<String, Object> params,
518            final MultiFileBuilderParametersImpl multiParams)
519    {
520        final Map<String, Object> newParams = new HashMap<>(params);
521        newParams.remove(KEY_INTERPOLATOR);
522        final BuilderParameters managedBuilderParameters =
523                multiParams.getManagedBuilderParameters();
524        if (managedBuilderParameters != null)
525        {
526            // clone parameters as they are applied to multiple builders
527            final BuilderParameters copy =
528                    (BuilderParameters) ConfigurationUtils
529                            .cloneIfPossible(managedBuilderParameters);
530            newParams.putAll(copy.getParameters());
531        }
532        return newParams;
533    }
534
535    /**
536     * Checks whether the given event type is of interest for the managed
537     * configuration builders. This method is called by the methods for managing
538     * event listeners to find out whether a listener should be passed to the
539     * managed builders, too.
540     *
541     * @param eventType the event type object
542     * @return a flag whether this event type is of interest for managed
543     *         builders
544     */
545    private static boolean isEventTypeForManagedBuilders(final EventType<?> eventType)
546    {
547        return !EventType
548                .isInstanceOf(eventType, ConfigurationBuilderEvent.ANY);
549    }
550}