    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 multiple file-based configurations
    045 * based on a file name pattern.
    046 * </p>
    047 * <p>
    048 * This builder class is initialized with a pattern string and a {@link ConfigurationInterpolator} object. Each time a
    049 * configuration is requested, the pattern is evaluated against the {@code ConfigurationInterpolator} (so all variables
    050 * are replaced by their current values). The resulting string is interpreted as a file name for a configuration file to
    051 * be loaded. For example, providing a pattern of <em>file:///opt/config/${product}/${client}/config.xml</em> will
    052 * result in <em>product</em> and <em>client</em> being resolved on every call. By storing configuration files in a
    053 * corresponding directory structure, specialized configuration files associated with a specific product and client can
    054 * be loaded. Thus an application can be made multi-tenant in a transparent way.
    055 * </p>
    056 * <p>
    057 * This builder class keeps a map with configuration builders for configurations already loaded. The
    058 * {@code getConfiguration()} method first evaluates the pattern string and checks whether a builder for the resulting
    059 * file name is available. If yes, it is queried for its configuration. Otherwise, a new file-based configuration
    060 * builder is created now and initialized.
    061 * </p>
    062 * <p>
    063 * Configuration of an instance happens in the usual way for configuration builders. A
    064 * {@link MultiFileBuilderParametersImpl} parameters object is expected which must contain a file name pattern string
    065 * and a {@code ConfigurationInterpolator}. Other properties of this parameters object are used to initialize the
    066 * builders for managed configurations.
    067 * </p>
    068 *
    069 * @since 2.0
    070 * @param <T> the concrete type of {@code Configuration} objects created by this builder
    071 */
    072public class MultiFileConfigurationBuilder<T extends FileBasedConfiguration> extends BasicConfigurationBuilder<T> {
    073    /**
    074     * Constant for the name of the key referencing the {@code ConfigurationInterpolator} in this builder's parameters.
    075     */
    076    private static final String KEY_INTERPOLATOR = "interpolator";
    077
    078    /** A cache for already created managed builders. */
    079    private final ConcurrentMap<String, FileBasedConfigurationBuilder<T>> managedBuilders = new ConcurrentHashMap<>();
    080
    081    /** Stores the {@code ConfigurationInterpolator} object. */
    082    private final AtomicReference<ConfigurationInterpolator> interpolator = new AtomicReference<>();
    083
    084    /**
    085     * A flag for preventing reentrant access to managed builders on interpolation of the file name pattern.
    086     */
    087    private final ThreadLocal<Boolean> inInterpolation = new ThreadLocal<>();
    088
    089    /** A list for the event listeners to be passed to managed builders. */
    090    private final EventListenerList configurationListeners = new EventListenerList();
    091
    092    /**
    093     * A specialized event listener which gets registered at all managed builders. This listener just propagates
    094     * notifications from managed builders to the listeners registered at this {@code MultiFileConfigurationBuilder}.
    095     */
    096    private final EventListener<ConfigurationBuilderEvent> managedBuilderDelegationListener = this::handleManagedBuilderEvent;
    097
    098    /**
    099     * Creates a new instance of {@code MultiFileConfigurationBuilder} and sets initialization parameters and a flag whether
    100     * initialization failures should be ignored.
    101     *
    102     * @param resCls the result configuration class
    103     * @param params a map with initialization parameters
    104     * @param allowFailOnInit a flag whether initialization errors should be ignored
    105     * @throws IllegalArgumentException if the result class is <b>null</b>
    106     */
    107    public MultiFileConfigurationBuilder(final Class<? extends T> resCls, final Map<String, Object> params, final boolean allowFailOnInit) {
    108        super(resCls, params, allowFailOnInit);
    109    }
    110
    111    /**
    112     * Creates a new instance of {@code MultiFileConfigurationBuilder} and sets initialization parameters.
    113     *
    114     * @param resCls the result configuration class
    115     * @param params a map with initialization parameters
    116     * @throws IllegalArgumentException if the result class is <b>null</b>
    117     */
    118    public MultiFileConfigurationBuilder(final Class<? extends T> resCls, final Map<String, Object> params) {
    119        super(resCls, params);
    120    }
    121
    122    /**
    123     * Creates a new instance of {@code MultiFileConfigurationBuilder} without setting initialization parameters.
    124     *
    125     * @param resCls the result configuration class
    126     * @throws IllegalArgumentException if the result class is <b>null</b>
    127     */
    128    public MultiFileConfigurationBuilder(final Class<? extends T> resCls) {
    129        super(resCls);
    130    }
    131
    132    /**
    133     * {@inheritDoc} This method is overridden to adapt the return type.
    134     */
    135    @Override
    136    public MultiFileConfigurationBuilder<T> configure(final BuilderParameters... params) {
    137        super.configure(params);
    138        return this;
    139    }
    140
    141    /**
    142     * {@inheritDoc} This implementation evaluates the file name pattern using the configured
    143     * {@code ConfigurationInterpolator}. If this file has already been loaded, the corresponding builder is accessed.
    144     * Otherwise, a new builder is created for loading this configuration file.
    145     */
    146    @Override
    147    public T getConfiguration() throws ConfigurationException {
    148        return getManagedBuilder().getConfiguration();
    149    }
    150
    151    /**
    152     * Returns the managed {@code FileBasedConfigurationBuilder} for the current file name pattern. It is determined based
    153     * on the evaluation of the file name pattern using the configured {@code ConfigurationInterpolator}. If this is the
    154     * first access to this configuration file, the builder is created.
    155     *
    156     * @return the configuration builder for the configuration corresponding to the current evaluation of the file name
    157     *         pattern
    158     * @throws ConfigurationException if the builder cannot be determined (e.g. due to missing initialization parameters)
    159     */
    160    public FileBasedConfigurationBuilder<T> getManagedBuilder() throws ConfigurationException {
    161        final Map<String, Object> params = getParameters();
    162        final MultiFileBuilderParametersImpl multiParams = MultiFileBuilderParametersImpl.fromParameters(params, true);
    163        if (multiParams.getFilePattern() == null) {
    164            throw new ConfigurationException("No file name pattern is set!");
    165        }
    166        final String fileName = fetchFileName(multiParams);
    167
    168        FileBasedConfigurationBuilder<T> builder = getManagedBuilders().get(fileName);
    169        if (builder == null) {
    170            builder = createInitializedManagedBuilder(fileName, createManagedBuilderParameters(params, multiParams));
    171            final FileBasedConfigurationBuilder<T> newBuilder = ConcurrentUtils.putIfAbsent(getManagedBuilders(), fileName, builder);
    172            if (newBuilder == builder) {
    173                initListeners(newBuilder);
    174            } else {
    175                builder = newBuilder;
    176            }
    177        }
    178        return builder;
    179    }
    180
    181    /**
    182     * {@inheritDoc} This implementation ensures that the listener is also added to managed configuration builders if
    183     * necessary. Listeners for the builder-related event types are excluded because otherwise they would be triggered by
    184     * the internally used configuration builders.
    185     */
    186    @Override
    187    public synchronized <E extends Event> void addEventListener(final EventType<E> eventType, final EventListener<? super E> l) {
    188        super.addEventListener(eventType, l);
    189        if (isEventTypeForManagedBuilders(eventType)) {
    190            for (final FileBasedConfigurationBuilder<T> b : getManagedBuilders().values()) {
    191                b.addEventListener(eventType, l);
    192            }
    193            configurationListeners.addEventListener(eventType, l);
    194        }
    195    }
    196
    197    /**
    198     * {@inheritDoc} This implementation ensures that the listener is also removed from managed configuration builders if
    199     * necessary.
    200     */
    201    @Override
    202    public synchronized <E extends Event> boolean removeEventListener(final EventType<E> eventType, final EventListener<? super E> l) {
    203        final boolean result = super.removeEventListener(eventType, l);
    204        if (isEventTypeForManagedBuilders(eventType)) {
    205            for (final FileBasedConfigurationBuilder<T> b : getManagedBuilders().values()) {
    206                b.removeEventListener(eventType, l);
    207            }
    208            configurationListeners.removeEventListener(eventType, l);
    209        }
    210        return result;
    211    }
    212
    213    /**
    214     * {@inheritDoc} This implementation clears the cache with all managed builders.
    215     */
    216    @Override
    217    public synchronized void resetParameters() {
    218        for (final FileBasedConfigurationBuilder<T> b : getManagedBuilders().values()) {
    219            b.removeEventListener(ConfigurationBuilderEvent.ANY, managedBuilderDelegationListener);
    220        }
    221        getManagedBuilders().clear();
    222        interpolator.set(null);
    223        super.resetParameters();
    224    }
    225
    226    /**
    227     * Returns the {@code ConfigurationInterpolator} used by this instance. This is the object used for evaluating the file
    228     * name pattern. It is created on demand.
    229     *
    230     * @return the {@code ConfigurationInterpolator}
    231     */
    232    protected ConfigurationInterpolator getInterpolator() {
    233        ConfigurationInterpolator result;
    234        boolean done;
    235
    236        // This might create multiple instances under high load,
    237        // however, always the same instance is returned.
    238        do {
    239            result = interpolator.get();
    240            if (result != null) {
    241                done = true;
    242            } else {
    243                result = createInterpolator();
    244                done = interpolator.compareAndSet(null, result);
    245            }
    246        } while (!done);
    247
    248        return result;
    249    }
    250
    251    /**
    252     * Creates the {@code ConfigurationInterpolator} to be used by this instance. This method is called when a file name is
    253     * to be constructed, but no current {@code ConfigurationInterpolator} instance is available. It obtains an instance
    254     * from this builder's parameters. If no properties of the {@code ConfigurationInterpolator} are specified in the
    255     * parameters, a default instance without lookups is returned (which is probably not very helpful).
    256     *
    257     * @return the {@code ConfigurationInterpolator} to be used
    258     */
    259    protected ConfigurationInterpolator createInterpolator() {
    260        final InterpolatorSpecification spec = BasicBuilderParameters.fetchInterpolatorSpecification(getParameters());
    261        return ConfigurationInterpolator.fromSpecification(spec);
    262    }
    263
    264    /**
    265     * Determines the file name of a configuration based on the file name pattern. This method is called on every access to
    266     * this builder's configuration. It obtains the {@link ConfigurationInterpolator} from this builder's parameters and
    267     * uses it to interpolate the file name pattern.
    268     *
    269     * @param multiParams the parameters object for this builder
    270     * @return the name of the configuration file to be loaded
    271     */
    272    protected String constructFileName(final MultiFileBuilderParametersImpl multiParams) {
    273        final ConfigurationInterpolator ci = getInterpolator();
    274        return String.valueOf(ci.interpolate(multiParams.getFilePattern()));
    275    }
    276
    277    /**
    278     * Creates a builder for a managed configuration. This method is called whenever a configuration for a file name is
    279     * requested which has not yet been loaded. The passed in map with parameters is populated from this builder's
    280     * configuration (i.e. the basic parameters plus the optional parameters for managed builders). This base implementation
    281     * creates a standard builder for file-based configurations. Derived classes may override it to create special purpose
    282     * builders.
    283     *
    284     * @param fileName the name of the file to be loaded
    285     * @param params a map with initialization parameters for the new builder
    286     * @return the newly created builder instance
    287     * @throws ConfigurationException if an error occurs
    288     */
    289    protected FileBasedConfigurationBuilder<T> createManagedBuilder(final String fileName, final Map<String, Object> params) throws ConfigurationException {
    290        return new FileBasedConfigurationBuilder<>(getResultClass(), params, isAllowFailOnInit());
    291    }
    292
    293    /**
    294     * Creates a fully initialized builder for a managed configuration. This method is called by {@code getConfiguration()}
    295     * whenever a configuration file is requested which has not yet been loaded. This implementation delegates to
    296     * {@code createManagedBuilder()} for actually creating the builder object. Then it sets the location to the
    297     * configuration file.
    298     *
    299     * @param fileName the name of the file to be loaded
    300     * @param params a map with initialization parameters for the new builder
    301     * @return the newly created and initialized builder instance
    302     * @throws ConfigurationException if an error occurs
    303     */
    304    protected FileBasedConfigurationBuilder<T> createInitializedManagedBuilder(final String fileName, final Map<String, Object> params)
    305        throws ConfigurationException {
    306        final FileBasedConfigurationBuilder<T> managedBuilder = createManagedBuilder(fileName, params);
    307        managedBuilder.getFileHandler().setFileName(fileName);
    308        return managedBuilder;
    309    }
    310
    311    /**
    312     * Returns the map with the managed builders created so far by this {@code MultiFileConfigurationBuilder}. This map is
    313     * exposed to derived classes so they can access managed builders directly. However, derived classes are not expected to
    314     * manipulate this map.
    315     *
    316     * @return the map with the managed builders
    317     */
    318    protected ConcurrentMap<String, FileBasedConfigurationBuilder<T>> getManagedBuilders() {
    319        return managedBuilders;
    320    }
    321
    322    /**
    323     * Registers event listeners at the passed in newly created managed builder. This method registers a special
    324     * {@code EventListener} which propagates builder events to listeners registered at this builder. In addition,
    325     * {@code ConfigurationListener} and {@code ConfigurationErrorListener} objects are registered at the new builder.
    326     *
    327     * @param newBuilder the builder to be initialized
    328     */
    329    private void initListeners(final FileBasedConfigurationBuilder<T> newBuilder) {
    330        copyEventListeners(newBuilder, configurationListeners);
    331        newBuilder.addEventListener(ConfigurationBuilderEvent.ANY, managedBuilderDelegationListener);
    332    }
    333
    334    /**
    335     * Generates a file name for a managed builder based on the file name pattern. This method prevents infinite loops which
    336     * could happen if the file name pattern cannot be resolved and the {@code ConfigurationInterpolator} used by this
    337     * object causes a recursive lookup to this builder's configuration.
    338     *
    339     * @param multiParams the current builder parameters
    340     * @return the file name for a managed builder
    341     */
    342    private String fetchFileName(final MultiFileBuilderParametersImpl multiParams) {
    343        String fileName;
    344        final Boolean reentrant = inInterpolation.get();
    345        if (reentrant != null && reentrant.booleanValue()) {
    346            fileName = multiParams.getFilePattern();
    347        } else {
    348            inInterpolation.set(Boolean.TRUE);
    349            try {
    350                fileName = constructFileName(multiParams);
    351            } finally {
    352                inInterpolation.set(Boolean.FALSE);
    353            }
    354        }
    355        return fileName;
    356    }
    357
    358    /**
    359     * Handles events received from managed configuration builders. This method creates a new event with a source pointing
    360     * to this builder and propagates it to all registered listeners.
    361     *
    362     * @param event the event received from a managed builder
    363     */
    364    private void handleManagedBuilderEvent(final ConfigurationBuilderEvent event) {
    365        if (ConfigurationBuilderEvent.RESET.equals(event.getEventType())) {
    366            resetResult();
    367        } else {
    368            fireBuilderEvent(createEventWithChangedSource(event));
    369        }
    370    }
    371
    372    /**
    373     * Creates a new {@code ConfigurationBuilderEvent} based on the passed in event, but with the source changed to this
    374     * builder. This method is called when an event was received from a managed builder. In this case, the event has to be
    375     * passed to the builder listeners registered at this object, but with the correct source property.
    376     *
    377     * @param event the event received from a managed builder
    378     * @return the event to be propagated
    379     */
    380    private ConfigurationBuilderEvent createEventWithChangedSource(final ConfigurationBuilderEvent event) {
    381        if (ConfigurationBuilderResultCreatedEvent.RESULT_CREATED.equals(event.getEventType())) {
    382            return new ConfigurationBuilderResultCreatedEvent(this, ConfigurationBuilderResultCreatedEvent.RESULT_CREATED,
    383                ((ConfigurationBuilderResultCreatedEvent) event).getConfiguration());
    384        }
    385        @SuppressWarnings("unchecked")
    386        final
    387        // This is safe due to the constructor of ConfigurationBuilderEvent
    388        EventType<? extends ConfigurationBuilderEvent> type = (EventType<? extends ConfigurationBuilderEvent>) event.getEventType();
    389        return new ConfigurationBuilderEvent(this, type);
    390    }
    391
    392    /**
    393     * Creates a map with parameters for a new managed configuration builder. This method merges the basic parameters set
    394     * for this builder with the specific parameters object for managed builders (if provided).
    395     *
    396     * @param params the parameters of this builder
    397     * @param multiParams the parameters object for this builder
    398     * @return the parameters for a new managed builder
    399     */
    400    private static Map<String, Object> createManagedBuilderParameters(final Map<String, Object> params, final MultiFileBuilderParametersImpl multiParams) {
    401        final Map<String, Object> newParams = new HashMap<>(params);
    402        newParams.remove(KEY_INTERPOLATOR);
    403        final BuilderParameters managedBuilderParameters = multiParams.getManagedBuilderParameters();
    404        if (managedBuilderParameters != null) {
    405            // clone parameters as they are applied to multiple builders
    406            final BuilderParameters copy = (BuilderParameters) ConfigurationUtils.cloneIfPossible(managedBuilderParameters);
    407            newParams.putAll(copy.getParameters());
    408        }
    409        return newParams;
    410    }
    411
    412    /**
    413     * Checks whether the given event type is of interest for the managed configuration builders. This method is called by
    414     * the methods for managing event listeners to find out whether a listener should be passed to the managed builders,
    415     * too.
    416     *
    417     * @param eventType the event type object
    418     * @return a flag whether this event type is of interest for managed builders
    419     */
    420    private static boolean isEventTypeForManagedBuilders(final EventType<?> eventType) {
    421        return !EventType.isInstanceOf(eventType, ConfigurationBuilderEvent.ANY);
    422    }
    423}