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 }