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