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 * 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 /**
79 * Creates a map with parameters for a new managed configuration builder. This method merges the basic parameters set
80 * for this builder with the specific parameters object for managed builders (if provided).
81 *
82 * @param params the parameters of this builder
83 * @param multiParams the parameters object for this builder
84 * @return the parameters for a new managed builder
85 */
86 private static Map<String, Object> createManagedBuilderParameters(final Map<String, Object> params, final MultiFileBuilderParametersImpl multiParams) {
87 final Map<String, Object> newParams = new HashMap<>(params);
88 newParams.remove(KEY_INTERPOLATOR);
89 final BuilderParameters managedBuilderParameters = multiParams.getManagedBuilderParameters();
90 if (managedBuilderParameters != null) {
91 // clone parameters as they are applied to multiple builders
92 final BuilderParameters copy = (BuilderParameters) ConfigurationUtils.cloneIfPossible(managedBuilderParameters);
93 newParams.putAll(copy.getParameters());
94 }
95 return newParams;
96 }
97
98 /**
99 * 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 <strong>null</strong>
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 <strong>null</strong>
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 <strong>null</strong>
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 (for example 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 }