MultiFileConfigurationBuilder.java

  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. import java.util.HashMap;
  19. import java.util.Map;
  20. import java.util.concurrent.ConcurrentHashMap;
  21. import java.util.concurrent.ConcurrentMap;
  22. import java.util.concurrent.atomic.AtomicReference;

  23. import org.apache.commons.configuration2.ConfigurationUtils;
  24. import org.apache.commons.configuration2.FileBasedConfiguration;
  25. import org.apache.commons.configuration2.builder.BasicBuilderParameters;
  26. import org.apache.commons.configuration2.builder.BasicConfigurationBuilder;
  27. import org.apache.commons.configuration2.builder.BuilderParameters;
  28. import org.apache.commons.configuration2.builder.ConfigurationBuilderEvent;
  29. import org.apache.commons.configuration2.builder.ConfigurationBuilderResultCreatedEvent;
  30. import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
  31. import org.apache.commons.configuration2.event.Event;
  32. import org.apache.commons.configuration2.event.EventListener;
  33. import org.apache.commons.configuration2.event.EventListenerList;
  34. import org.apache.commons.configuration2.event.EventType;
  35. import org.apache.commons.configuration2.ex.ConfigurationException;
  36. import org.apache.commons.configuration2.interpol.ConfigurationInterpolator;
  37. import org.apache.commons.configuration2.interpol.InterpolatorSpecification;
  38. import org.apache.commons.lang3.concurrent.ConcurrentUtils;

  39. /**
  40.  * <p>
  41.  * A specialized {@code ConfigurationBuilder} implementation providing access to multiple file-based configurations
  42.  * based on a file name pattern.
  43.  * </p>
  44.  * <p>
  45.  * This builder class is initialized with a pattern string and a {@link ConfigurationInterpolator} object. Each time a
  46.  * configuration is requested, the pattern is evaluated against the {@code ConfigurationInterpolator} (so all variables
  47.  * are replaced by their current values). The resulting string is interpreted as a file name for a configuration file to
  48.  * be loaded. For example, providing a pattern of <em>file:///opt/config/${product}/${client}/config.xml</em> will
  49.  * result in <em>product</em> and <em>client</em> being resolved on every call. By storing configuration files in a
  50.  * corresponding directory structure, specialized configuration files associated with a specific product and client can
  51.  * be loaded. Thus an application can be made multi-tenant in a transparent way.
  52.  * </p>
  53.  * <p>
  54.  * This builder class keeps a map with configuration builders for configurations already loaded. The
  55.  * {@code getConfiguration()} method first evaluates the pattern string and checks whether a builder for the resulting
  56.  * file name is available. If yes, it is queried for its configuration. Otherwise, a new file-based configuration
  57.  * builder is created now and initialized.
  58.  * </p>
  59.  * <p>
  60.  * Configuration of an instance happens in the usual way for configuration builders. A
  61.  * {@link MultiFileBuilderParametersImpl} parameters object is expected which must contain a file name pattern string
  62.  * and a {@code ConfigurationInterpolator}. Other properties of this parameters object are used to initialize the
  63.  * builders for managed configurations.
  64.  * </p>
  65.  *
  66.  * @param <T> the concrete type of {@code Configuration} objects created by this builder
  67.  * @since 2.0
  68.  */
  69. public class MultiFileConfigurationBuilder<T extends FileBasedConfiguration> extends BasicConfigurationBuilder<T> {
  70.     /**
  71.      * Constant for the name of the key referencing the {@code ConfigurationInterpolator} in this builder's parameters.
  72.      */
  73.     private static final String KEY_INTERPOLATOR = "interpolator";

  74.     /**
  75.      * Creates a map with parameters for a new managed configuration builder. This method merges the basic parameters set
  76.      * for this builder with the specific parameters object for managed builders (if provided).
  77.      *
  78.      * @param params the parameters of this builder
  79.      * @param multiParams the parameters object for this builder
  80.      * @return the parameters for a new managed builder
  81.      */
  82.     private static Map<String, Object> createManagedBuilderParameters(final Map<String, Object> params, final MultiFileBuilderParametersImpl multiParams) {
  83.         final Map<String, Object> newParams = new HashMap<>(params);
  84.         newParams.remove(KEY_INTERPOLATOR);
  85.         final BuilderParameters managedBuilderParameters = multiParams.getManagedBuilderParameters();
  86.         if (managedBuilderParameters != null) {
  87.             // clone parameters as they are applied to multiple builders
  88.             final BuilderParameters copy = (BuilderParameters) ConfigurationUtils.cloneIfPossible(managedBuilderParameters);
  89.             newParams.putAll(copy.getParameters());
  90.         }
  91.         return newParams;
  92.     }

  93.     /**
  94.      * Checks whether the given event type is of interest for the managed configuration builders. This method is called by
  95.      * the methods for managing event listeners to find out whether a listener should be passed to the managed builders,
  96.      * too.
  97.      *
  98.      * @param eventType the event type object
  99.      * @return a flag whether this event type is of interest for managed builders
  100.      */
  101.     private static boolean isEventTypeForManagedBuilders(final EventType<?> eventType) {
  102.         return !EventType.isInstanceOf(eventType, ConfigurationBuilderEvent.ANY);
  103.     }

  104.     /** A cache for already created managed builders. */
  105.     private final ConcurrentMap<String, FileBasedConfigurationBuilder<T>> managedBuilders = new ConcurrentHashMap<>();

  106.     /** Stores the {@code ConfigurationInterpolator} object. */
  107.     private final AtomicReference<ConfigurationInterpolator> interpolator = new AtomicReference<>();

  108.     /**
  109.      * A flag for preventing reentrant access to managed builders on interpolation of the file name pattern.
  110.      */
  111.     private final ThreadLocal<Boolean> inInterpolation = new ThreadLocal<>();

  112.     /** A list for the event listeners to be passed to managed builders. */
  113.     private final EventListenerList configurationListeners = new EventListenerList();

  114.     /**
  115.      * A specialized event listener which gets registered at all managed builders. This listener just propagates
  116.      * notifications from managed builders to the listeners registered at this {@code MultiFileConfigurationBuilder}.
  117.      */
  118.     private final EventListener<ConfigurationBuilderEvent> managedBuilderDelegationListener = this::handleManagedBuilderEvent;

  119.     /**
  120.      * Creates a new instance of {@code MultiFileConfigurationBuilder} without setting initialization parameters.
  121.      *
  122.      * @param resCls the result configuration class
  123.      * @throws IllegalArgumentException if the result class is <strong>null</strong>
  124.      */
  125.     public MultiFileConfigurationBuilder(final Class<? extends T> resCls) {
  126.         super(resCls);
  127.     }

  128.     /**
  129.      * Creates a new instance of {@code MultiFileConfigurationBuilder} and sets initialization parameters.
  130.      *
  131.      * @param resCls the result configuration class
  132.      * @param params a map with initialization parameters
  133.      * @throws IllegalArgumentException if the result class is <strong>null</strong>
  134.      */
  135.     public MultiFileConfigurationBuilder(final Class<? extends T> resCls, final Map<String, Object> params) {
  136.         super(resCls, params);
  137.     }

  138.     /**
  139.      * Creates a new instance of {@code MultiFileConfigurationBuilder} and sets initialization parameters and a flag whether
  140.      * initialization failures should be ignored.
  141.      *
  142.      * @param resCls the result configuration class
  143.      * @param params a map with initialization parameters
  144.      * @param allowFailOnInit a flag whether initialization errors should be ignored
  145.      * @throws IllegalArgumentException if the result class is <strong>null</strong>
  146.      */
  147.     public MultiFileConfigurationBuilder(final Class<? extends T> resCls, final Map<String, Object> params, final boolean allowFailOnInit) {
  148.         super(resCls, params, allowFailOnInit);
  149.     }

  150.     /**
  151.      * {@inheritDoc} This implementation ensures that the listener is also added to managed configuration builders if
  152.      * necessary. Listeners for the builder-related event types are excluded because otherwise they would be triggered by
  153.      * the internally used configuration builders.
  154.      */
  155.     @Override
  156.     public synchronized <E extends Event> void addEventListener(final EventType<E> eventType, final EventListener<? super E> l) {
  157.         super.addEventListener(eventType, l);
  158.         if (isEventTypeForManagedBuilders(eventType)) {
  159.             getManagedBuilders().values().forEach(b -> b.addEventListener(eventType, l));
  160.             configurationListeners.addEventListener(eventType, l);
  161.         }
  162.     }

  163.     /**
  164.      * {@inheritDoc} This method is overridden to adapt the return type.
  165.      */
  166.     @Override
  167.     public MultiFileConfigurationBuilder<T> configure(final BuilderParameters... params) {
  168.         super.configure(params);
  169.         return this;
  170.     }

  171.     /**
  172.      * Determines the file name of a configuration based on the file name pattern. This method is called on every access to
  173.      * this builder's configuration. It obtains the {@link ConfigurationInterpolator} from this builder's parameters and
  174.      * uses it to interpolate the file name pattern.
  175.      *
  176.      * @param multiParams the parameters object for this builder
  177.      * @return the name of the configuration file to be loaded
  178.      */
  179.     protected String constructFileName(final MultiFileBuilderParametersImpl multiParams) {
  180.         final ConfigurationInterpolator ci = getInterpolator();
  181.         return String.valueOf(ci.interpolate(multiParams.getFilePattern()));
  182.     }

  183.     /**
  184.      * Creates a new {@code ConfigurationBuilderEvent} based on the passed in event, but with the source changed to this
  185.      * builder. This method is called when an event was received from a managed builder. In this case, the event has to be
  186.      * passed to the builder listeners registered at this object, but with the correct source property.
  187.      *
  188.      * @param event the event received from a managed builder
  189.      * @return the event to be propagated
  190.      */
  191.     private ConfigurationBuilderEvent createEventWithChangedSource(final ConfigurationBuilderEvent event) {
  192.         if (ConfigurationBuilderResultCreatedEvent.RESULT_CREATED.equals(event.getEventType())) {
  193.             return new ConfigurationBuilderResultCreatedEvent(this, ConfigurationBuilderResultCreatedEvent.RESULT_CREATED,
  194.                 ((ConfigurationBuilderResultCreatedEvent) event).getConfiguration());
  195.         }
  196.         @SuppressWarnings("unchecked")
  197.         final
  198.         // This is safe due to the constructor of ConfigurationBuilderEvent
  199.         EventType<? extends ConfigurationBuilderEvent> type = (EventType<? extends ConfigurationBuilderEvent>) event.getEventType();
  200.         return new ConfigurationBuilderEvent(this, type);
  201.     }

  202.     /**
  203.      * Creates a fully initialized builder for a managed configuration. This method is called by {@code getConfiguration()}
  204.      * whenever a configuration file is requested which has not yet been loaded. This implementation delegates to
  205.      * {@code createManagedBuilder()} for actually creating the builder object. Then it sets the location to the
  206.      * configuration file.
  207.      *
  208.      * @param fileName the name of the file to be loaded
  209.      * @param params a map with initialization parameters for the new builder
  210.      * @return the newly created and initialized builder instance
  211.      * @throws ConfigurationException if an error occurs
  212.      */
  213.     protected FileBasedConfigurationBuilder<T> createInitializedManagedBuilder(final String fileName, final Map<String, Object> params)
  214.         throws ConfigurationException {
  215.         final FileBasedConfigurationBuilder<T> managedBuilder = createManagedBuilder(fileName, params);
  216.         managedBuilder.getFileHandler().setFileName(fileName);
  217.         return managedBuilder;
  218.     }

  219.     /**
  220.      * Creates the {@code ConfigurationInterpolator} to be used by this instance. This method is called when a file name is
  221.      * to be constructed, but no current {@code ConfigurationInterpolator} instance is available. It obtains an instance
  222.      * from this builder's parameters. If no properties of the {@code ConfigurationInterpolator} are specified in the
  223.      * parameters, a default instance without lookups is returned (which is probably not very helpful).
  224.      *
  225.      * @return the {@code ConfigurationInterpolator} to be used
  226.      */
  227.     protected ConfigurationInterpolator createInterpolator() {
  228.         final InterpolatorSpecification spec = BasicBuilderParameters.fetchInterpolatorSpecification(getParameters());
  229.         return ConfigurationInterpolator.fromSpecification(spec);
  230.     }

  231.     /**
  232.      * Creates a builder for a managed configuration. This method is called whenever a configuration for a file name is
  233.      * requested which has not yet been loaded. The passed in map with parameters is populated from this builder's
  234.      * configuration (i.e. the basic parameters plus the optional parameters for managed builders). This base implementation
  235.      * creates a standard builder for file-based configurations. Derived classes may override it to create special purpose
  236.      * builders.
  237.      *
  238.      * @param fileName the name of the file to be loaded
  239.      * @param params a map with initialization parameters for the new builder
  240.      * @return the newly created builder instance
  241.      * @throws ConfigurationException if an error occurs
  242.      */
  243.     protected FileBasedConfigurationBuilder<T> createManagedBuilder(final String fileName, final Map<String, Object> params) throws ConfigurationException {
  244.         return new FileBasedConfigurationBuilder<>(getResultClass(), params, isAllowFailOnInit());
  245.     }

  246.     /**
  247.      * Generates a file name for a managed builder based on the file name pattern. This method prevents infinite loops which
  248.      * could happen if the file name pattern cannot be resolved and the {@code ConfigurationInterpolator} used by this
  249.      * object causes a recursive lookup to this builder's configuration.
  250.      *
  251.      * @param multiParams the current builder parameters
  252.      * @return the file name for a managed builder
  253.      */
  254.     private String fetchFileName(final MultiFileBuilderParametersImpl multiParams) {
  255.         String fileName;
  256.         final Boolean reentrant = inInterpolation.get();
  257.         if (reentrant != null && reentrant.booleanValue()) {
  258.             fileName = multiParams.getFilePattern();
  259.         } else {
  260.             inInterpolation.set(Boolean.TRUE);
  261.             try {
  262.                 fileName = constructFileName(multiParams);
  263.             } finally {
  264.                 inInterpolation.set(Boolean.FALSE);
  265.             }
  266.         }
  267.         return fileName;
  268.     }

  269.     /**
  270.      * {@inheritDoc} This implementation evaluates the file name pattern using the configured
  271.      * {@code ConfigurationInterpolator}. If this file has already been loaded, the corresponding builder is accessed.
  272.      * Otherwise, a new builder is created for loading this configuration file.
  273.      */
  274.     @Override
  275.     public T getConfiguration() throws ConfigurationException {
  276.         return getManagedBuilder().getConfiguration();
  277.     }

  278.     /**
  279.      * Gets the {@code ConfigurationInterpolator} used by this instance. This is the object used for evaluating the file
  280.      * name pattern. It is created on demand.
  281.      *
  282.      * @return the {@code ConfigurationInterpolator}
  283.      */
  284.     protected ConfigurationInterpolator getInterpolator() {
  285.         ConfigurationInterpolator result;
  286.         boolean done;

  287.         // This might create multiple instances under high load,
  288.         // however, always the same instance is returned.
  289.         do {
  290.             result = interpolator.get();
  291.             if (result != null) {
  292.                 done = true;
  293.             } else {
  294.                 result = createInterpolator();
  295.                 done = interpolator.compareAndSet(null, result);
  296.             }
  297.         } while (!done);

  298.         return result;
  299.     }

  300.     /**
  301.      * Gets the managed {@code FileBasedConfigurationBuilder} for the current file name pattern. It is determined based
  302.      * on the evaluation of the file name pattern using the configured {@code ConfigurationInterpolator}. If this is the
  303.      * first access to this configuration file, the builder is created.
  304.      *
  305.      * @return the configuration builder for the configuration corresponding to the current evaluation of the file name
  306.      *         pattern
  307.      * @throws ConfigurationException if the builder cannot be determined (for example due to missing initialization parameters)
  308.      */
  309.     public FileBasedConfigurationBuilder<T> getManagedBuilder() throws ConfigurationException {
  310.         final Map<String, Object> params = getParameters();
  311.         final MultiFileBuilderParametersImpl multiParams = MultiFileBuilderParametersImpl.fromParameters(params, true);
  312.         if (multiParams.getFilePattern() == null) {
  313.             throw new ConfigurationException("No file name pattern is set!");
  314.         }
  315.         final String fileName = fetchFileName(multiParams);

  316.         FileBasedConfigurationBuilder<T> builder = getManagedBuilders().get(fileName);
  317.         if (builder == null) {
  318.             builder = createInitializedManagedBuilder(fileName, createManagedBuilderParameters(params, multiParams));
  319.             final FileBasedConfigurationBuilder<T> newBuilder = ConcurrentUtils.putIfAbsent(getManagedBuilders(), fileName, builder);
  320.             if (newBuilder == builder) {
  321.                 initListeners(newBuilder);
  322.             } else {
  323.                 builder = newBuilder;
  324.             }
  325.         }
  326.         return builder;
  327.     }

  328.     /**
  329.      * Gets the map with the managed builders created so far by this {@code MultiFileConfigurationBuilder}. This map is
  330.      * exposed to derived classes so they can access managed builders directly. However, derived classes are not expected to
  331.      * manipulate this map.
  332.      *
  333.      * @return the map with the managed builders
  334.      */
  335.     protected ConcurrentMap<String, FileBasedConfigurationBuilder<T>> getManagedBuilders() {
  336.         return managedBuilders;
  337.     }

  338.     /**
  339.      * Handles events received from managed configuration builders. This method creates a new event with a source pointing
  340.      * to this builder and propagates it to all registered listeners.
  341.      *
  342.      * @param event the event received from a managed builder
  343.      */
  344.     private void handleManagedBuilderEvent(final ConfigurationBuilderEvent event) {
  345.         if (ConfigurationBuilderEvent.RESET.equals(event.getEventType())) {
  346.             resetResult();
  347.         } else {
  348.             fireBuilderEvent(createEventWithChangedSource(event));
  349.         }
  350.     }

  351.     /**
  352.      * Registers event listeners at the passed in newly created managed builder. This method registers a special
  353.      * {@code EventListener} which propagates builder events to listeners registered at this builder. In addition,
  354.      * {@code ConfigurationListener} and {@code ConfigurationErrorListener} objects are registered at the new builder.
  355.      *
  356.      * @param newBuilder the builder to be initialized
  357.      */
  358.     private void initListeners(final FileBasedConfigurationBuilder<T> newBuilder) {
  359.         copyEventListeners(newBuilder, configurationListeners);
  360.         newBuilder.addEventListener(ConfigurationBuilderEvent.ANY, managedBuilderDelegationListener);
  361.     }

  362.     /**
  363.      * {@inheritDoc} This implementation ensures that the listener is also removed from managed configuration builders if
  364.      * necessary.
  365.      */
  366.     @Override
  367.     public synchronized <E extends Event> boolean removeEventListener(final EventType<E> eventType, final EventListener<? super E> l) {
  368.         final boolean result = super.removeEventListener(eventType, l);
  369.         if (isEventTypeForManagedBuilders(eventType)) {
  370.             getManagedBuilders().values().forEach(b -> b.removeEventListener(eventType, l));
  371.             configurationListeners.removeEventListener(eventType, l);
  372.         }
  373.         return result;
  374.     }

  375.     /**
  376.      * {@inheritDoc} This implementation clears the cache with all managed builders.
  377.      */
  378.     @Override
  379.     public synchronized void resetParameters() {
  380.         getManagedBuilders().values().forEach(b -> b.removeEventListener(ConfigurationBuilderEvent.ANY, managedBuilderDelegationListener));
  381.         getManagedBuilders().clear();
  382.         interpolator.set(null);
  383.         super.resetParameters();
  384.     }
  385. }