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