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 045 * multiple file-based configurations based on a file name pattern. 046 * </p> 047 * <p> 048 * This builder class is initialized with a pattern string and a 049 * {@link ConfigurationInterpolator} object. Each time a configuration is 050 * requested, the pattern is evaluated against the 051 * {@code ConfigurationInterpolator} (so all variables are replaced by their 052 * current values). The resulting string is interpreted as a file name for a 053 * configuration file to be loaded. For example, providing a pattern of 054 * <em>file:///opt/config/${product}/${client}/config.xml</em> will result in 055 * <em>product</em> and <em>client</em> being resolved on every call. By storing 056 * configuration files in a corresponding directory structure, specialized 057 * configuration files associated with a specific product and client can be 058 * loaded. Thus an application can be made multi-tenant in a transparent way. 059 * </p> 060 * <p> 061 * This builder class keeps a map with configuration builders for configurations 062 * already loaded. The {@code getConfiguration()} method first evaluates the 063 * pattern string and checks whether a builder for the resulting file name is 064 * available. If yes, it is queried for its configuration. Otherwise, a new 065 * file-based configuration builder is created now and initialized. 066 * </p> 067 * <p> 068 * Configuration of an instance happens in the usual way for configuration 069 * builders. A {@link MultiFileBuilderParametersImpl} parameters object is 070 * expected which must contain a file name pattern string and a 071 * {@code ConfigurationInterpolator}. Other properties of this parameters object 072 * are used to initialize the builders for managed configurations. 073 * </p> 074 * 075 * @since 2.0 076 * @param <T> the concrete type of {@code Configuration} objects created by this 077 * builder 078 */ 079public class MultiFileConfigurationBuilder<T extends FileBasedConfiguration> 080 extends BasicConfigurationBuilder<T> 081{ 082 /** 083 * Constant for the name of the key referencing the 084 * {@code ConfigurationInterpolator} in this builder's parameters. 085 */ 086 private static final String KEY_INTERPOLATOR = "interpolator"; 087 088 /** A cache for already created managed builders. */ 089 private final ConcurrentMap<String, FileBasedConfigurationBuilder<T>> managedBuilders = 090 new ConcurrentHashMap<>(); 091 092 /** Stores the {@code ConfigurationInterpolator} object. */ 093 private final AtomicReference<ConfigurationInterpolator> interpolator = 094 new AtomicReference<>(); 095 096 /** 097 * A flag for preventing reentrant access to managed builders on 098 * interpolation of the file name pattern. 099 */ 100 private final ThreadLocal<Boolean> inInterpolation = 101 new ThreadLocal<>(); 102 103 /** A list for the event listeners to be passed to managed builders. */ 104 private final EventListenerList configurationListeners = new EventListenerList(); 105 106 /** 107 * A specialized event listener which gets registered at all managed 108 * builders. This listener just propagates notifications from managed 109 * builders to the listeners registered at this 110 * {@code MultiFileConfigurationBuilder}. 111 */ 112 private final EventListener<ConfigurationBuilderEvent> managedBuilderDelegationListener = 113 event -> handleManagedBuilderEvent(event); 114 115 /** 116 * Creates a new instance of {@code MultiFileConfigurationBuilder} and sets 117 * initialization parameters and a flag whether initialization failures 118 * should be ignored. 119 * 120 * @param resCls the result configuration class 121 * @param params a map with initialization parameters 122 * @param allowFailOnInit a flag whether initialization errors should be 123 * ignored 124 * @throws IllegalArgumentException if the result class is <b>null</b> 125 */ 126 public MultiFileConfigurationBuilder(final Class<? extends T> resCls, 127 final Map<String, Object> params, final boolean allowFailOnInit) 128 { 129 super(resCls, params, allowFailOnInit); 130 } 131 132 /** 133 * Creates a new instance of {@code MultiFileConfigurationBuilder} and sets 134 * initialization parameters. 135 * 136 * @param resCls the result configuration class 137 * @param params a map with initialization parameters 138 * @throws IllegalArgumentException if the result class is <b>null</b> 139 */ 140 public MultiFileConfigurationBuilder(final Class<? extends T> resCls, 141 final Map<String, Object> params) 142 { 143 super(resCls, params); 144 } 145 146 /** 147 * Creates a new instance of {@code MultiFileConfigurationBuilder} without 148 * setting initialization parameters. 149 * 150 * @param resCls the result configuration class 151 * @throws IllegalArgumentException if the result class is <b>null</b> 152 */ 153 public MultiFileConfigurationBuilder(final Class<? extends T> resCls) 154 { 155 super(resCls); 156 } 157 158 /** 159 * {@inheritDoc} This method is overridden to adapt the return type. 160 */ 161 @Override 162 public MultiFileConfigurationBuilder<T> configure(final BuilderParameters... params) 163 { 164 super.configure(params); 165 return this; 166 } 167 168 /** 169 * {@inheritDoc} This implementation evaluates the file name pattern using 170 * the configured {@code ConfigurationInterpolator}. If this file has 171 * already been loaded, the corresponding builder is accessed. Otherwise, a 172 * new builder is created for loading this configuration file. 173 */ 174 @Override 175 public T getConfiguration() throws ConfigurationException 176 { 177 return getManagedBuilder().getConfiguration(); 178 } 179 180 /** 181 * Returns the managed {@code FileBasedConfigurationBuilder} for the current 182 * file name pattern. It is determined based on the evaluation of the file 183 * name pattern using the configured {@code ConfigurationInterpolator}. If 184 * this is the first access to this configuration file, the builder is 185 * created. 186 * 187 * @return the configuration builder for the configuration corresponding to 188 * the current evaluation of the file name pattern 189 * @throws ConfigurationException if the builder cannot be determined (e.g. 190 * due to missing initialization parameters) 191 */ 192 public FileBasedConfigurationBuilder<T> getManagedBuilder() 193 throws ConfigurationException 194 { 195 final Map<String, Object> params = getParameters(); 196 final MultiFileBuilderParametersImpl multiParams = 197 MultiFileBuilderParametersImpl.fromParameters(params, true); 198 if (multiParams.getFilePattern() == null) 199 { 200 throw new ConfigurationException("No file name pattern is set!"); 201 } 202 final String fileName = fetchFileName(multiParams); 203 204 FileBasedConfigurationBuilder<T> builder = 205 getManagedBuilders().get(fileName); 206 if (builder == null) 207 { 208 builder = 209 createInitializedManagedBuilder(fileName, 210 createManagedBuilderParameters(params, multiParams)); 211 final FileBasedConfigurationBuilder<T> newBuilder = 212 ConcurrentUtils.putIfAbsent(getManagedBuilders(), fileName, 213 builder); 214 if (newBuilder == builder) 215 { 216 initListeners(newBuilder); 217 } 218 else 219 { 220 builder = newBuilder; 221 } 222 } 223 return builder; 224 } 225 226 /** 227 * {@inheritDoc} This implementation ensures that the listener is also added 228 * to managed configuration builders if necessary. Listeners for the builder-related 229 * event types are excluded because otherwise they would be triggered by the 230 * internally used configuration builders. 231 */ 232 @Override 233 public synchronized <E extends Event> void addEventListener( 234 final EventType<E> eventType, final EventListener<? super E> l) 235 { 236 super.addEventListener(eventType, l); 237 if (isEventTypeForManagedBuilders(eventType)) 238 { 239 for (final FileBasedConfigurationBuilder<T> b : getManagedBuilders() 240 .values()) 241 { 242 b.addEventListener(eventType, l); 243 } 244 configurationListeners.addEventListener(eventType, l); 245 } 246 } 247 248 /** 249 * {@inheritDoc} This implementation ensures that the listener is also 250 * removed from managed configuration builders if necessary. 251 */ 252 @Override 253 public synchronized <E extends Event> boolean removeEventListener( 254 final EventType<E> eventType, final EventListener<? super E> l) 255 { 256 final boolean result = super.removeEventListener(eventType, l); 257 if (isEventTypeForManagedBuilders(eventType)) 258 { 259 for (final FileBasedConfigurationBuilder<T> b : getManagedBuilders() 260 .values()) 261 { 262 b.removeEventListener(eventType, l); 263 } 264 configurationListeners.removeEventListener(eventType, l); 265 } 266 return result; 267 } 268 269 /** 270 * {@inheritDoc} This implementation clears the cache with all managed 271 * builders. 272 */ 273 @Override 274 public synchronized void resetParameters() 275 { 276 for (final FileBasedConfigurationBuilder<T> b : getManagedBuilders().values()) 277 { 278 b.removeEventListener(ConfigurationBuilderEvent.ANY, 279 managedBuilderDelegationListener); 280 } 281 getManagedBuilders().clear(); 282 interpolator.set(null); 283 super.resetParameters(); 284 } 285 286 /** 287 * Returns the {@code ConfigurationInterpolator} used by this instance. This 288 * is the object used for evaluating the file name pattern. It is created on 289 * demand. 290 * 291 * @return the {@code ConfigurationInterpolator} 292 */ 293 protected ConfigurationInterpolator getInterpolator() 294 { 295 ConfigurationInterpolator result; 296 boolean done; 297 298 // This might create multiple instances under high load, 299 // however, always the same instance is returned. 300 do 301 { 302 result = interpolator.get(); 303 if (result != null) 304 { 305 done = true; 306 } 307 else 308 { 309 result = createInterpolator(); 310 done = interpolator.compareAndSet(null, result); 311 } 312 } while (!done); 313 314 return result; 315 } 316 317 /** 318 * Creates the {@code ConfigurationInterpolator} to be used by this 319 * instance. This method is called when a file name is to be constructed, 320 * but no current {@code ConfigurationInterpolator} instance is available. 321 * It obtains an instance from this builder's parameters. If no properties 322 * of the {@code ConfigurationInterpolator} are specified in the parameters, 323 * a default instance without lookups is returned (which is probably not 324 * very helpful). 325 * 326 * @return the {@code ConfigurationInterpolator} to be used 327 */ 328 protected ConfigurationInterpolator createInterpolator() 329 { 330 final InterpolatorSpecification spec = 331 BasicBuilderParameters 332 .fetchInterpolatorSpecification(getParameters()); 333 return ConfigurationInterpolator.fromSpecification(spec); 334 } 335 336 /** 337 * Determines the file name of a configuration based on the file name 338 * pattern. This method is called on every access to this builder's 339 * configuration. It obtains the {@link ConfigurationInterpolator} from this 340 * builder's parameters and uses it to interpolate the file name pattern. 341 * 342 * @param multiParams the parameters object for this builder 343 * @return the name of the configuration file to be loaded 344 */ 345 protected String constructFileName( 346 final MultiFileBuilderParametersImpl multiParams) 347 { 348 final ConfigurationInterpolator ci = getInterpolator(); 349 return String.valueOf(ci.interpolate(multiParams.getFilePattern())); 350 } 351 352 /** 353 * Creates a builder for a managed configuration. This method is called 354 * whenever a configuration for a file name is requested which has not yet 355 * been loaded. The passed in map with parameters is populated from this 356 * builder's configuration (i.e. the basic parameters plus the optional 357 * parameters for managed builders). This base implementation creates a 358 * standard builder for file-based configurations. Derived classes may 359 * override it to create special purpose builders. 360 * 361 * @param fileName the name of the file to be loaded 362 * @param params a map with initialization parameters for the new builder 363 * @return the newly created builder instance 364 * @throws ConfigurationException if an error occurs 365 */ 366 protected FileBasedConfigurationBuilder<T> createManagedBuilder( 367 final String fileName, final Map<String, Object> params) 368 throws ConfigurationException 369 { 370 return new FileBasedConfigurationBuilder<>(getResultClass(), params, 371 isAllowFailOnInit()); 372 } 373 374 /** 375 * Creates a fully initialized builder for a managed configuration. This 376 * method is called by {@code getConfiguration()} whenever a configuration 377 * file is requested which has not yet been loaded. This implementation 378 * delegates to {@code createManagedBuilder()} for actually creating the 379 * builder object. Then it sets the location to the configuration file. 380 * 381 * @param fileName the name of the file to be loaded 382 * @param params a map with initialization parameters for the new builder 383 * @return the newly created and initialized builder instance 384 * @throws ConfigurationException if an error occurs 385 */ 386 protected FileBasedConfigurationBuilder<T> createInitializedManagedBuilder( 387 final String fileName, final Map<String, Object> params) 388 throws ConfigurationException 389 { 390 final FileBasedConfigurationBuilder<T> managedBuilder = 391 createManagedBuilder(fileName, params); 392 managedBuilder.getFileHandler().setFileName(fileName); 393 return managedBuilder; 394 } 395 396 /** 397 * Returns the map with the managed builders created so far by this 398 * {@code MultiFileConfigurationBuilder}. This map is exposed to derived 399 * classes so they can access managed builders directly. However, derived 400 * classes are not expected to manipulate this map. 401 * 402 * @return the map with the managed builders 403 */ 404 protected ConcurrentMap<String, FileBasedConfigurationBuilder<T>> getManagedBuilders() 405 { 406 return managedBuilders; 407 } 408 409 /** 410 * Registers event listeners at the passed in newly created managed builder. 411 * This method registers a special {@code EventListener} which propagates 412 * builder events to listeners registered at this builder. In addition, 413 * {@code ConfigurationListener} and {@code ConfigurationErrorListener} 414 * objects are registered at the new builder. 415 * 416 * @param newBuilder the builder to be initialized 417 */ 418 private void initListeners(final FileBasedConfigurationBuilder<T> newBuilder) 419 { 420 copyEventListeners(newBuilder, configurationListeners); 421 newBuilder.addEventListener(ConfigurationBuilderEvent.ANY, 422 managedBuilderDelegationListener); 423 } 424 425 /** 426 * Generates a file name for a managed builder based on the file name 427 * pattern. This method prevents infinite loops which could happen if the 428 * file name pattern cannot be resolved and the 429 * {@code ConfigurationInterpolator} used by this object causes a recursive 430 * lookup to this builder's configuration. 431 * 432 * @param multiParams the current builder parameters 433 * @return the file name for a managed builder 434 */ 435 private String fetchFileName(final MultiFileBuilderParametersImpl multiParams) 436 { 437 String fileName; 438 final Boolean reentrant = inInterpolation.get(); 439 if (reentrant != null && reentrant.booleanValue()) 440 { 441 fileName = multiParams.getFilePattern(); 442 } 443 else 444 { 445 inInterpolation.set(Boolean.TRUE); 446 try 447 { 448 fileName = constructFileName(multiParams); 449 } 450 finally 451 { 452 inInterpolation.set(Boolean.FALSE); 453 } 454 } 455 return fileName; 456 } 457 458 /** 459 * Handles events received from managed configuration builders. This method 460 * creates a new event with a source pointing to this builder and propagates 461 * it to all registered listeners. 462 * 463 * @param event the event received from a managed builder 464 */ 465 private void handleManagedBuilderEvent(final ConfigurationBuilderEvent event) 466 { 467 if (ConfigurationBuilderEvent.RESET.equals(event.getEventType())) 468 { 469 resetResult(); 470 } 471 else 472 { 473 fireBuilderEvent(createEventWithChangedSource(event)); 474 } 475 } 476 477 /** 478 * Creates a new {@code ConfigurationBuilderEvent} based on the passed in 479 * event, but with the source changed to this builder. This method is called 480 * when an event was received from a managed builder. In this case, the 481 * event has to be passed to the builder listeners registered at this 482 * object, but with the correct source property. 483 * 484 * @param event the event received from a managed builder 485 * @return the event to be propagated 486 */ 487 private ConfigurationBuilderEvent createEventWithChangedSource( 488 final ConfigurationBuilderEvent event) 489 { 490 if (ConfigurationBuilderResultCreatedEvent.RESULT_CREATED.equals(event 491 .getEventType())) 492 { 493 return new ConfigurationBuilderResultCreatedEvent(this, 494 ConfigurationBuilderResultCreatedEvent.RESULT_CREATED, 495 ((ConfigurationBuilderResultCreatedEvent) event) 496 .getConfiguration()); 497 } 498 @SuppressWarnings("unchecked") 499 final 500 // This is safe due to the constructor of ConfigurationBuilderEvent 501 EventType<? extends ConfigurationBuilderEvent> type = 502 (EventType<? extends ConfigurationBuilderEvent>) event 503 .getEventType(); 504 return new ConfigurationBuilderEvent(this, type); 505 } 506 507 /** 508 * Creates a map with parameters for a new managed configuration builder. 509 * This method merges the basic parameters set for this builder with the 510 * specific parameters object for managed builders (if provided). 511 * 512 * @param params the parameters of this builder 513 * @param multiParams the parameters object for this builder 514 * @return the parameters for a new managed builder 515 */ 516 private static Map<String, Object> createManagedBuilderParameters( 517 final Map<String, Object> params, 518 final MultiFileBuilderParametersImpl multiParams) 519 { 520 final Map<String, Object> newParams = new HashMap<>(params); 521 newParams.remove(KEY_INTERPOLATOR); 522 final BuilderParameters managedBuilderParameters = 523 multiParams.getManagedBuilderParameters(); 524 if (managedBuilderParameters != null) 525 { 526 // clone parameters as they are applied to multiple builders 527 final BuilderParameters copy = 528 (BuilderParameters) ConfigurationUtils 529 .cloneIfPossible(managedBuilderParameters); 530 newParams.putAll(copy.getParameters()); 531 } 532 return newParams; 533 } 534 535 /** 536 * Checks whether the given event type is of interest for the managed 537 * configuration builders. This method is called by the methods for managing 538 * event listeners to find out whether a listener should be passed to the 539 * managed builders, too. 540 * 541 * @param eventType the event type object 542 * @return a flag whether this event type is of interest for managed 543 * builders 544 */ 545 private static boolean isEventTypeForManagedBuilders(final EventType<?> eventType) 546 { 547 return !EventType 548 .isInstanceOf(eventType, ConfigurationBuilderEvent.ANY); 549 } 550}