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