View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration2.builder.combined;
18  
19  import java.util.HashMap;
20  import java.util.Map;
21  import java.util.concurrent.ConcurrentHashMap;
22  import java.util.concurrent.ConcurrentMap;
23  import java.util.concurrent.atomic.AtomicReference;
24  
25  import org.apache.commons.configuration2.ConfigurationUtils;
26  import org.apache.commons.configuration2.FileBasedConfiguration;
27  import org.apache.commons.configuration2.builder.BasicBuilderParameters;
28  import org.apache.commons.configuration2.builder.BasicConfigurationBuilder;
29  import org.apache.commons.configuration2.builder.BuilderParameters;
30  import org.apache.commons.configuration2.builder.ConfigurationBuilderEvent;
31  import org.apache.commons.configuration2.builder.ConfigurationBuilderResultCreatedEvent;
32  import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
33  import org.apache.commons.configuration2.event.Event;
34  import org.apache.commons.configuration2.event.EventListener;
35  import org.apache.commons.configuration2.event.EventListenerList;
36  import org.apache.commons.configuration2.event.EventType;
37  import org.apache.commons.configuration2.ex.ConfigurationException;
38  import org.apache.commons.configuration2.interpol.ConfigurationInterpolator;
39  import org.apache.commons.configuration2.interpol.InterpolatorSpecification;
40  import org.apache.commons.lang3.concurrent.ConcurrentUtils;
41  
42  /**
43   * <p>
44   * A specialized {@code ConfigurationBuilder} implementation providing access to multiple file-based configurations
45   * based on a file name pattern.
46   * </p>
47   * <p>
48   * This builder class is initialized with a pattern string and a {@link ConfigurationInterpolator} object. Each time a
49   * configuration is requested, the pattern is evaluated against the {@code ConfigurationInterpolator} (so all variables
50   * are replaced by their current values). The resulting string is interpreted as a file name for a configuration file to
51   * be loaded. For example, providing a pattern of <em>file:///opt/config/${product}/${client}/config.xml</em> will
52   * result in <em>product</em> and <em>client</em> being resolved on every call. By storing configuration files in a
53   * corresponding directory structure, specialized configuration files associated with a specific product and client can
54   * be loaded. Thus an application can be made multi-tenant in a transparent way.
55   * </p>
56   * <p>
57   * This builder class keeps a map with configuration builders for configurations already loaded. The
58   * {@code getConfiguration()} method first evaluates the pattern string and checks whether a builder for the resulting
59   * file name is available. If yes, it is queried for its configuration. Otherwise, a new file-based configuration
60   * builder is created now and initialized.
61   * </p>
62   * <p>
63   * Configuration of an instance happens in the usual way for configuration builders. A
64   * {@link MultiFileBuilderParametersImpl} parameters object is expected which must contain a file name pattern string
65   * and a {@code ConfigurationInterpolator}. Other properties of this parameters object are used to initialize the
66   * builders for managed configurations.
67   * </p>
68   *
69   * @since 2.0
70   * @param <T> the concrete type of {@code Configuration} objects created by this builder
71   */
72  public class MultiFileConfigurationBuilder<T extends FileBasedConfiguration> extends BasicConfigurationBuilder<T> {
73      /**
74       * Constant for the name of the key referencing the {@code ConfigurationInterpolator} in this builder's parameters.
75       */
76      private static final String KEY_INTERPOLATOR = "interpolator";
77  
78      /** A cache for already created managed builders. */
79      private final ConcurrentMap<String, FileBasedConfigurationBuilder<T>> managedBuilders = new ConcurrentHashMap<>();
80  
81      /** Stores the {@code ConfigurationInterpolator} object. */
82      private final AtomicReference<ConfigurationInterpolator> interpolator = new AtomicReference<>();
83  
84      /**
85       * A flag for preventing reentrant access to managed builders on interpolation of the file name pattern.
86       */
87      private final ThreadLocal<Boolean> inInterpolation = new ThreadLocal<>();
88  
89      /** A list for the event listeners to be passed to managed builders. */
90      private final EventListenerList configurationListeners = new EventListenerList();
91  
92      /**
93       * A specialized event listener which gets registered at all managed builders. This listener just propagates
94       * notifications from managed builders to the listeners registered at this {@code MultiFileConfigurationBuilder}.
95       */
96      private final EventListener<ConfigurationBuilderEvent> managedBuilderDelegationListener = this::handleManagedBuilderEvent;
97  
98      /**
99       * 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      * Gets 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             getManagedBuilders().values().forEach(b -> b.addEventListener(eventType, l));
191             configurationListeners.addEventListener(eventType, l);
192         }
193     }
194 
195     /**
196      * {@inheritDoc} This implementation ensures that the listener is also removed from managed configuration builders if
197      * necessary.
198      */
199     @Override
200     public synchronized <E extends Event> boolean removeEventListener(final EventType<E> eventType, final EventListener<? super E> l) {
201         final boolean result = super.removeEventListener(eventType, l);
202         if (isEventTypeForManagedBuilders(eventType)) {
203             getManagedBuilders().values().forEach(b -> b.removeEventListener(eventType, l));
204             configurationListeners.removeEventListener(eventType, l);
205         }
206         return result;
207     }
208 
209     /**
210      * {@inheritDoc} This implementation clears the cache with all managed builders.
211      */
212     @Override
213     public synchronized void resetParameters() {
214         getManagedBuilders().values().forEach(b -> b.removeEventListener(ConfigurationBuilderEvent.ANY, managedBuilderDelegationListener));
215         getManagedBuilders().clear();
216         interpolator.set(null);
217         super.resetParameters();
218     }
219 
220     /**
221      * Gets the {@code ConfigurationInterpolator} used by this instance. This is the object used for evaluating the file
222      * name pattern. It is created on demand.
223      *
224      * @return the {@code ConfigurationInterpolator}
225      */
226     protected ConfigurationInterpolator getInterpolator() {
227         ConfigurationInterpolator result;
228         boolean done;
229 
230         // This might create multiple instances under high load,
231         // however, always the same instance is returned.
232         do {
233             result = interpolator.get();
234             if (result != null) {
235                 done = true;
236             } else {
237                 result = createInterpolator();
238                 done = interpolator.compareAndSet(null, result);
239             }
240         } while (!done);
241 
242         return result;
243     }
244 
245     /**
246      * Creates the {@code ConfigurationInterpolator} to be used by this instance. This method is called when a file name is
247      * to be constructed, but no current {@code ConfigurationInterpolator} instance is available. It obtains an instance
248      * from this builder's parameters. If no properties of the {@code ConfigurationInterpolator} are specified in the
249      * parameters, a default instance without lookups is returned (which is probably not very helpful).
250      *
251      * @return the {@code ConfigurationInterpolator} to be used
252      */
253     protected ConfigurationInterpolator createInterpolator() {
254         final InterpolatorSpecification spec = BasicBuilderParameters.fetchInterpolatorSpecification(getParameters());
255         return ConfigurationInterpolator.fromSpecification(spec);
256     }
257 
258     /**
259      * Determines the file name of a configuration based on the file name pattern. This method is called on every access to
260      * this builder's configuration. It obtains the {@link ConfigurationInterpolator} from this builder's parameters and
261      * uses it to interpolate the file name pattern.
262      *
263      * @param multiParams the parameters object for this builder
264      * @return the name of the configuration file to be loaded
265      */
266     protected String constructFileName(final MultiFileBuilderParametersImpl multiParams) {
267         final ConfigurationInterpolator ci = getInterpolator();
268         return String.valueOf(ci.interpolate(multiParams.getFilePattern()));
269     }
270 
271     /**
272      * Creates a builder for a managed configuration. This method is called whenever a configuration for a file name is
273      * requested which has not yet been loaded. The passed in map with parameters is populated from this builder's
274      * configuration (i.e. the basic parameters plus the optional parameters for managed builders). This base implementation
275      * creates a standard builder for file-based configurations. Derived classes may override it to create special purpose
276      * builders.
277      *
278      * @param fileName the name of the file to be loaded
279      * @param params a map with initialization parameters for the new builder
280      * @return the newly created builder instance
281      * @throws ConfigurationException if an error occurs
282      */
283     protected FileBasedConfigurationBuilder<T> createManagedBuilder(final String fileName, final Map<String, Object> params) throws ConfigurationException {
284         return new FileBasedConfigurationBuilder<>(getResultClass(), params, isAllowFailOnInit());
285     }
286 
287     /**
288      * Creates a fully initialized builder for a managed configuration. This method is called by {@code getConfiguration()}
289      * whenever a configuration file is requested which has not yet been loaded. This implementation delegates to
290      * {@code createManagedBuilder()} for actually creating the builder object. Then it sets the location to the
291      * configuration file.
292      *
293      * @param fileName the name of the file to be loaded
294      * @param params a map with initialization parameters for the new builder
295      * @return the newly created and initialized builder instance
296      * @throws ConfigurationException if an error occurs
297      */
298     protected FileBasedConfigurationBuilder<T> createInitializedManagedBuilder(final String fileName, final Map<String, Object> params)
299         throws ConfigurationException {
300         final FileBasedConfigurationBuilder<T> managedBuilder = createManagedBuilder(fileName, params);
301         managedBuilder.getFileHandler().setFileName(fileName);
302         return managedBuilder;
303     }
304 
305     /**
306      * Gets the map with the managed builders created so far by this {@code MultiFileConfigurationBuilder}. This map is
307      * exposed to derived classes so they can access managed builders directly. However, derived classes are not expected to
308      * manipulate this map.
309      *
310      * @return the map with the managed builders
311      */
312     protected ConcurrentMap<String, FileBasedConfigurationBuilder<T>> getManagedBuilders() {
313         return managedBuilders;
314     }
315 
316     /**
317      * Registers event listeners at the passed in newly created managed builder. This method registers a special
318      * {@code EventListener} which propagates builder events to listeners registered at this builder. In addition,
319      * {@code ConfigurationListener} and {@code ConfigurationErrorListener} objects are registered at the new builder.
320      *
321      * @param newBuilder the builder to be initialized
322      */
323     private void initListeners(final FileBasedConfigurationBuilder<T> newBuilder) {
324         copyEventListeners(newBuilder, configurationListeners);
325         newBuilder.addEventListener(ConfigurationBuilderEvent.ANY, managedBuilderDelegationListener);
326     }
327 
328     /**
329      * Generates a file name for a managed builder based on the file name pattern. This method prevents infinite loops which
330      * could happen if the file name pattern cannot be resolved and the {@code ConfigurationInterpolator} used by this
331      * object causes a recursive lookup to this builder's configuration.
332      *
333      * @param multiParams the current builder parameters
334      * @return the file name for a managed builder
335      */
336     private String fetchFileName(final MultiFileBuilderParametersImpl multiParams) {
337         String fileName;
338         final Boolean reentrant = inInterpolation.get();
339         if (reentrant != null && reentrant.booleanValue()) {
340             fileName = multiParams.getFilePattern();
341         } else {
342             inInterpolation.set(Boolean.TRUE);
343             try {
344                 fileName = constructFileName(multiParams);
345             } finally {
346                 inInterpolation.set(Boolean.FALSE);
347             }
348         }
349         return fileName;
350     }
351 
352     /**
353      * Handles events received from managed configuration builders. This method creates a new event with a source pointing
354      * to this builder and propagates it to all registered listeners.
355      *
356      * @param event the event received from a managed builder
357      */
358     private void handleManagedBuilderEvent(final ConfigurationBuilderEvent event) {
359         if (ConfigurationBuilderEvent.RESET.equals(event.getEventType())) {
360             resetResult();
361         } else {
362             fireBuilderEvent(createEventWithChangedSource(event));
363         }
364     }
365 
366     /**
367      * Creates a new {@code ConfigurationBuilderEvent} based on the passed in event, but with the source changed to this
368      * builder. This method is called when an event was received from a managed builder. In this case, the event has to be
369      * passed to the builder listeners registered at this object, but with the correct source property.
370      *
371      * @param event the event received from a managed builder
372      * @return the event to be propagated
373      */
374     private ConfigurationBuilderEvent createEventWithChangedSource(final ConfigurationBuilderEvent event) {
375         if (ConfigurationBuilderResultCreatedEvent.RESULT_CREATED.equals(event.getEventType())) {
376             return new ConfigurationBuilderResultCreatedEvent(this, ConfigurationBuilderResultCreatedEvent.RESULT_CREATED,
377                 ((ConfigurationBuilderResultCreatedEvent) event).getConfiguration());
378         }
379         @SuppressWarnings("unchecked")
380         final
381         // This is safe due to the constructor of ConfigurationBuilderEvent
382         EventType<? extends ConfigurationBuilderEvent> type = (EventType<? extends ConfigurationBuilderEvent>) event.getEventType();
383         return new ConfigurationBuilderEvent(this, type);
384     }
385 
386     /**
387      * Creates a map with parameters for a new managed configuration builder. This method merges the basic parameters set
388      * for this builder with the specific parameters object for managed builders (if provided).
389      *
390      * @param params the parameters of this builder
391      * @param multiParams the parameters object for this builder
392      * @return the parameters for a new managed builder
393      */
394     private static Map<String, Object> createManagedBuilderParameters(final Map<String, Object> params, final MultiFileBuilderParametersImpl multiParams) {
395         final Map<String, Object> newParams = new HashMap<>(params);
396         newParams.remove(KEY_INTERPOLATOR);
397         final BuilderParameters managedBuilderParameters = multiParams.getManagedBuilderParameters();
398         if (managedBuilderParameters != null) {
399             // clone parameters as they are applied to multiple builders
400             final BuilderParameters copy = (BuilderParameters) ConfigurationUtils.cloneIfPossible(managedBuilderParameters);
401             newParams.putAll(copy.getParameters());
402         }
403         return newParams;
404     }
405 
406     /**
407      * Checks whether the given event type is of interest for the managed configuration builders. This method is called by
408      * the methods for managing event listeners to find out whether a listener should be passed to the managed builders,
409      * too.
410      *
411      * @param eventType the event type object
412      * @return a flag whether this event type is of interest for managed builders
413      */
414     private static boolean isEventTypeForManagedBuilders(final EventType<?> eventType) {
415         return !EventType.isInstanceOf(eventType, ConfigurationBuilderEvent.ANY);
416     }
417 }