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