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