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