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