    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}