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