    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}