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