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