    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 1842194 2018-09-27 22:24:23Z 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(final 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(final Class<? extends T> resCls,
    135            final Map<String, Object> params, final 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(final Class<? extends T> resCls,
    149            final 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(final 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(final 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        final Map<String, Object> params = getParameters();
    204        final 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        final 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            final 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            final EventType<E> eventType, final EventListener<? super E> l)
    243    {
    244        super.addEventListener(eventType, l);
    245        if (isEventTypeForManagedBuilders(eventType))
    246        {
    247            for (final 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            final EventType<E> eventType, final EventListener<? super E> l)
    263    {
    264        final boolean result = super.removeEventListener(eventType, l);
    265        if (isEventTypeForManagedBuilders(eventType))
    266        {
    267            for (final 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 (final 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        final 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            final MultiFileBuilderParametersImpl multiParams)
    355    {
    356        final 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            final String fileName, final 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            final String fileName, final Map<String, Object> params)
    396            throws ConfigurationException
    397    {
    398        final 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(final 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(final MultiFileBuilderParametersImpl multiParams)
    444    {
    445        String fileName;
    446        final 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(final 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            final 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        final
    508        // This is safe due to the constructor of ConfigurationBuilderEvent
    509        EventType<? extends ConfigurationBuilderEvent> type =
    510                (EventType<? extends ConfigurationBuilderEvent>) event
    511                        .getEventType();
    512        return new ConfigurationBuilderEvent(this, type);
    513    }
    514
    515    /**
    516     * Creates a map with parameters for a new managed configuration builder.
    517     * This method merges the basic parameters set for this builder with the
    518     * specific parameters object for managed builders (if provided).
    519     *
    520     * @param params the parameters of this builder
    521     * @param multiParams the parameters object for this builder
    522     * @return the parameters for a new managed builder
    523     */
    524    private static Map<String, Object> createManagedBuilderParameters(
    525            final Map<String, Object> params,
    526            final MultiFileBuilderParametersImpl multiParams)
    527    {
    528        final Map<String, Object> newParams = new HashMap<>(params);
    529        newParams.remove(KEY_INTERPOLATOR);
    530        final BuilderParameters managedBuilderParameters =
    531                multiParams.getManagedBuilderParameters();
    532        if (managedBuilderParameters != null)
    533        {
    534            // clone parameters as they are applied to multiple builders
    535            final BuilderParameters copy =
    536                    (BuilderParameters) ConfigurationUtils
    537                            .cloneIfPossible(managedBuilderParameters);
    538            newParams.putAll(copy.getParameters());
    539        }
    540        return newParams;
    541    }
    542
    543    /**
    544     * Checks whether the given event type is of interest for the managed
    545     * configuration builders. This method is called by the methods for managing
    546     * event listeners to find out whether a listener should be passed to the
    547     * managed builders, too.
    548     *
    549     * @param eventType the event type object
    550     * @return a flag whether this event type is of interest for managed
    551     *         builders
    552     */
    553    private static boolean isEventTypeForManagedBuilders(final EventType<?> eventType)
    554    {
    555        return !EventType
    556                .isInstanceOf(eventType, ConfigurationBuilderEvent.ANY);
    557    }
    558}