BasicConfigurationBuilder.java
- /*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package org.apache.commons.configuration2.builder;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.Map;
- import org.apache.commons.configuration2.ConfigurationUtils;
- import org.apache.commons.configuration2.ImmutableConfiguration;
- import org.apache.commons.configuration2.Initializable;
- import org.apache.commons.configuration2.beanutils.BeanDeclaration;
- import org.apache.commons.configuration2.beanutils.BeanHelper;
- import org.apache.commons.configuration2.beanutils.ConstructorArg;
- import org.apache.commons.configuration2.event.Event;
- import org.apache.commons.configuration2.event.EventListener;
- import org.apache.commons.configuration2.event.EventListenerList;
- import org.apache.commons.configuration2.event.EventListenerRegistrationData;
- import org.apache.commons.configuration2.event.EventSource;
- import org.apache.commons.configuration2.event.EventType;
- import org.apache.commons.configuration2.ex.ConfigurationException;
- import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
- import org.apache.commons.configuration2.reloading.ReloadingController;
- /**
- * <p>
- * An implementation of the {@code ConfigurationBuilder} interface which is able to create different concrete
- * {@code ImmutableConfiguration} implementations based on reflection.
- * </p>
- * <p>
- * When constructing an instance of this class the concrete {@code ImmutableConfiguration} implementation class has to
- * be provided. Then properties for the new {@code ImmutableConfiguration} instance can be set. The first call to
- * {@code getConfiguration()} creates and initializes the new {@code ImmutableConfiguration} object. It is cached and
- * returned by subsequent calls. This cache - and also the initialization properties set so far - can be flushed by
- * calling one of the {@code reset()} methods. That way other {@code ImmutableConfiguration} instances with different
- * properties can be created.
- * </p>
- * <p>
- * If the newly created {@code ImmutableConfiguration} object implements the {@code Initializable} interface, its
- * {@code initialize()} method is called after all initialization properties have been set. This way a concrete
- * implementation class can perform arbitrary initialization steps.
- * </p>
- * <p>
- * There are multiple options for setting up a {@code BasicConfigurationBuilder} instance:
- * </p>
- * <ul>
- * <li>All initialization properties can be set in one or multiple calls of the {@code configure()} method. In each call
- * an arbitrary number of {@link BuilderParameters} objects can be passed. The API allows method chaining and is
- * intended to be used from Java code.</li>
- * <li>If builder instances are created by other means - for example using a dependency injection framework -, the fluent API
- * approach may not be suitable. For those use cases it is also possible to pass in all initialization parameters as a
- * map. The keys of the map have to match initialization properties of the {@code ImmutableConfiguration} object to be
- * created, the values are the corresponding property values. For instance, the key <em>throwExceptionOnMissing</em> in
- * the map will cause the method {@code setThrowExceptionOnMissing()} on the {@code ImmutableConfiguration} object to be
- * called with the corresponding value as parameter.</li>
- * </ul>
- * <p>
- * A builder instance can be constructed with an <em>allowFailOnInit</em> flag. If set to <strong>true</strong>,
- * exceptions during initialization of the configuration are ignored; in such a case an empty configuration object is
- * returned. A use case for this flag is a scenario in which a configuration is optional and created on demand the first
- * time configuration data is to be stored. Consider an application that stores user-specific configuration data in the
- * user's home directory: When started for the first time by a new user there is no configuration file; so it makes
- * sense to start with an empty configuration object. On application exit, settings can be stored in this object and
- * written to the associated file. Then they are available on next application start.
- * </p>
- * <p>
- * This class is thread-safe. Multiple threads can modify initialization properties and call {@code getConfiguration()}.
- * However, the intended use case is that the builder is configured by a single thread first. Then
- * {@code getConfiguration()} can be called concurrently, and it is guaranteed that always the same
- * {@code ImmutableConfiguration} instance is returned until the builder is reset.
- * </p>
- *
- * @param <T> the concrete type of {@code ImmutableConfiguration} objects created by this builder
- * @since 2.0
- */
- public class BasicConfigurationBuilder<T extends ImmutableConfiguration> implements ConfigurationBuilder<T> {
- /**
- * Registers an event listener at an event source object.
- *
- * @param evSrc the event source
- * @param regData the registration data object
- * @param <E> the type of the event listener
- */
- private static <E extends Event> void registerListener(final EventSource evSrc, final EventListenerRegistrationData<E> regData) {
- evSrc.addEventListener(regData.getEventType(), regData.getListener());
- }
- /**
- * Removes an event listener from an event source object.
- *
- * @param evSrc the event source
- * @param regData the registration data object
- * @param <E> the type of the event listener
- */
- private static <E extends Event> void removeListener(final EventSource evSrc, final EventListenerRegistrationData<E> regData) {
- evSrc.removeEventListener(regData.getEventType(), regData.getListener());
- }
- /** The class of the objects produced by this builder instance. */
- private final Class<? extends T> resultClass;
- /** An object managing the event listeners registered at this builder. */
- private final EventListenerList eventListeners;
- /** A flag whether exceptions on initializing configurations are allowed. */
- private final boolean allowFailOnInit;
- /** The map with current initialization parameters. */
- private Map<String, Object> parameters;
- /** The current bean declaration. */
- private BeanDeclaration resultDeclaration;
- /** The result object of this builder. */
- private volatile T result;
- /**
- * Creates a new instance of {@code BasicConfigurationBuilder} and initializes it with the given result class. No
- * initialization properties are set.
- *
- * @param resCls the result class (must not be <strong>null</strong>)
- * @throws IllegalArgumentException if the result class is <strong>null</strong>
- */
- public BasicConfigurationBuilder(final Class<? extends T> resCls) {
- this(resCls, null);
- }
- /**
- * Creates a new instance of {@code BasicConfigurationBuilder} and initializes it with the given result class and an
- * initial set of builder parameters. The <em>allowFailOnInit</em> flag is set to <strong>false</strong>.
- *
- * @param resCls the result class (must not be <strong>null</strong>)
- * @param params a map with initialization parameters
- * @throws IllegalArgumentException if the result class is <strong>null</strong>
- */
- public BasicConfigurationBuilder(final Class<? extends T> resCls, final Map<String, Object> params) {
- this(resCls, params, false);
- }
- /**
- * Creates a new instance of {@code BasicConfigurationBuilder} and initializes it with the given result class, an
- * initial set of builder parameters, and the <em>allowFailOnInit</em> flag. The map with parameters may be <strong>null</strong>,
- * in this case no initialization parameters are set.
- *
- * @param resCls the result class (must not be <strong>null</strong>)
- * @param params a map with initialization parameters
- * @param allowFailOnInit a flag whether exceptions on initializing a newly created {@code ImmutableConfiguration}
- * object are allowed
- * @throws IllegalArgumentException if the result class is <strong>null</strong>
- */
- public BasicConfigurationBuilder(final Class<? extends T> resCls, final Map<String, Object> params, final boolean allowFailOnInit) {
- if (resCls == null) {
- throw new IllegalArgumentException("Result class must not be null!");
- }
- resultClass = resCls;
- this.allowFailOnInit = allowFailOnInit;
- eventListeners = new EventListenerList();
- updateParameters(params);
- }
- /**
- * {@inheritDoc} This implementation also takes care that the event listener is added to the managed configuration
- * object.
- *
- * @throws IllegalArgumentException if the event type or the listener is <strong>null</strong>
- */
- @Override
- public <E extends Event> void addEventListener(final EventType<E> eventType, final EventListener<? super E> listener) {
- installEventListener(eventType, listener);
- }
- /**
- * Adds the content of the given map to the already existing initialization parameters.
- *
- * @param params the map with additional initialization parameters; may be <strong>null</strong>, then this call has no effect
- * @return a reference to this builder for method chaining
- */
- public synchronized BasicConfigurationBuilder<T> addParameters(final Map<String, Object> params) {
- final Map<String, Object> newParams = new HashMap<>(getParameters());
- if (params != null) {
- newParams.putAll(params);
- }
- updateParameters(newParams);
- return this;
- }
- /**
- * Checks whether the class of the result configuration is compatible with this builder's result class. This is done to
- * ensure that only objects of the expected result class are created.
- *
- * @param inst the result instance to be checked
- * @throws ConfigurationRuntimeException if an invalid result class is detected
- */
- private void checkResultInstance(final Object inst) {
- if (!getResultClass().isInstance(inst)) {
- throw new ConfigurationRuntimeException("Incompatible result object: " + inst);
- }
- }
- /**
- * Appends the content of the specified {@code BuilderParameters} objects to the current initialization parameters.
- * Calling this method multiple times will create a union of the parameters provided.
- *
- * @param params an arbitrary number of objects with builder parameters
- * @return a reference to this builder for method chaining
- * @throws NullPointerException if a <strong>null</strong> array is passed
- */
- public BasicConfigurationBuilder<T> configure(final BuilderParameters... params) {
- final Map<String, Object> newParams = new HashMap<>();
- for (final BuilderParameters p : params) {
- newParams.putAll(p.getParameters());
- handleEventListenerProviders(p);
- }
- return setParameters(newParams);
- }
- /**
- * Connects this builder with a {@code ReloadingController}. With this method support for reloading can be added to an
- * arbitrary builder object. Event listeners are registered at the reloading controller and this builder with connect
- * both objects:
- * <ul>
- * <li>When the reloading controller detects that a reload is required, the builder's {@link #resetResult()} method is
- * called; so the managed result object is invalidated.</li>
- * <li>When a new result object has been created the controller's reloading state is reset, so that new changes can be
- * detected again.</li>
- * </ul>
- *
- * @param controller the {@code ReloadingController} to connect to (must not be <strong>null</strong>)
- * @throws IllegalArgumentException if the controller is <strong>null</strong>
- */
- public final void connectToReloadingController(final ReloadingController controller) {
- if (controller == null) {
- throw new IllegalArgumentException("ReloadingController must not be null!");
- }
- ReloadingBuilderSupportListener.connect(this, controller);
- }
- /**
- * Copies all {@code EventListener} objects registered at this builder to the specified target configuration builder.
- * This method is intended to be used by derived classes which support inheritance of their properties to other builder
- * objects.
- *
- * @param target the target configuration builder (must not be <strong>null</strong>)
- * @throws NullPointerException if the target builder is <strong>null</strong>
- */
- protected synchronized void copyEventListeners(final BasicConfigurationBuilder<?> target) {
- copyEventListeners(target, eventListeners);
- }
- /**
- * Copies all event listeners in the specified list to the specified target configuration builder. This method is
- * intended to be used by derived classes which have to deal with managed configuration builders that need to be
- * initialized with event listeners.
- *
- * @param target the target configuration builder (must not be <strong>null</strong>)
- * @param listeners the event listeners to be copied over
- * @throws NullPointerException if the target builder is <strong>null</strong>
- */
- protected void copyEventListeners(final BasicConfigurationBuilder<?> target, final EventListenerList listeners) {
- target.eventListeners.addAll(listeners);
- }
- /**
- * Creates a new, initialized result object. This method is called by {@code getConfiguration()} if no valid result
- * object exists. This base implementation performs two steps:
- * <ul>
- * <li>{@code createResultInstance()} is called to create a new, uninitialized result object.</li>
- * <li>{@code initResultInstance()} is called to process all initialization parameters.</li>
- * </ul>
- * It also evaluates the <em>allowFailOnInit</em> flag, i.e. if initialization causes an exception and this flag is set,
- * the exception is ignored, and the newly created, uninitialized configuration is returned. Note that this method is
- * called in a synchronized block.
- *
- * @return the newly created result object
- * @throws ConfigurationException if an error occurs
- */
- protected T createResult() throws ConfigurationException {
- final T resObj = createResultInstance();
- try {
- initResultInstance(resObj);
- } catch (final ConfigurationException cex) {
- if (!isAllowFailOnInit()) {
- throw cex;
- }
- }
- return resObj;
- }
- /**
- * Creates a new {@code BeanDeclaration} which is used for creating new result objects dynamically. This implementation
- * creates a specialized {@code BeanDeclaration} object that is initialized from the given map of initialization
- * parameters. The {@code BeanDeclaration} must be initialized with the result class of this builder, otherwise
- * exceptions will be thrown when the result object is created. Note: This method is invoked in a synchronized block.
- *
- * @param params a snapshot of the current initialization parameters
- * @return the {@code BeanDeclaration} for creating result objects
- * @throws ConfigurationException if an error occurs
- */
- protected BeanDeclaration createResultDeclaration(final Map<String, Object> params) throws ConfigurationException {
- return new BeanDeclaration() {
- @Override
- public String getBeanClassName() {
- return getResultClass().getName();
- }
- @Override
- public String getBeanFactoryName() {
- return null;
- }
- @Override
- public Object getBeanFactoryParameter() {
- return null;
- }
- @Override
- public Map<String, Object> getBeanProperties() {
- // the properties are equivalent to the parameters
- return params;
- }
- @Override
- public Collection<ConstructorArg> getConstructorArgs() {
- // no constructor arguments
- return Collections.emptySet();
- }
- @Override
- public Map<String, Object> getNestedBeanDeclarations() {
- // no nested beans
- return Collections.emptyMap();
- }
- };
- }
- /**
- * Creates the new, uninitialized result object. This is the first step of the process of producing a result object for
- * this builder. This implementation uses the {@link BeanHelper} class to create a new object based on the
- * {@link BeanDeclaration} returned by {@link #getResultDeclaration()}. Note: This method is invoked in a synchronized
- * block.
- *
- * @return the newly created, yet uninitialized result object
- * @throws ConfigurationException if an exception occurs
- */
- protected T createResultInstance() throws ConfigurationException {
- final Object bean = fetchBeanHelper().createBean(getResultDeclaration());
- checkResultInstance(bean);
- return getResultClass().cast(bean);
- }
- /**
- * Obtains the {@code BeanHelper} object to be used when dealing with bean declarations. This method checks whether this
- * builder was configured with a specific {@code BeanHelper} instance. If so, this instance is used. Otherwise, the
- * default {@code BeanHelper} is returned.
- *
- * @return the {@code BeanHelper} to be used
- */
- protected final BeanHelper fetchBeanHelper() {
- final BeanHelper helper = BasicBuilderParameters.fetchBeanHelper(getParameters());
- return helper != null ? helper : BeanHelper.INSTANCE;
- }
- /**
- * Returns an {@code EventSource} for the current result object. If there is no current result or if it does not extend
- * {@code EventSource}, a dummy event source is returned.
- *
- * @return the {@code EventSource} for the current result object
- */
- private EventSource fetchEventSource() {
- return ConfigurationUtils.asEventSource(result, true);
- }
- /**
- * Sends the specified builder event to all registered listeners.
- *
- * @param event the event to be fired
- */
- protected void fireBuilderEvent(final ConfigurationBuilderEvent event) {
- eventListeners.fire(event);
- }
- /**
- * {@inheritDoc} This implementation creates the result configuration on first access. Later invocations return the same
- * object until this builder is reset. The double-check idiom for lazy initialization is used (Bloch, Effective Java,
- * item 71).
- */
- @Override
- public T getConfiguration() throws ConfigurationException {
- fireBuilderEvent(new ConfigurationBuilderEvent(this, ConfigurationBuilderEvent.CONFIGURATION_REQUEST));
- T resObj = result;
- boolean created = false;
- if (resObj == null) {
- synchronized (this) {
- resObj = result;
- if (resObj == null) {
- result = resObj = createResult();
- created = true;
- }
- }
- }
- if (created) {
- fireBuilderEvent(new ConfigurationBuilderResultCreatedEvent(this, ConfigurationBuilderResultCreatedEvent.RESULT_CREATED, resObj));
- }
- return resObj;
- }
- /**
- * Gets a map with initialization parameters where all parameters starting with the reserved prefix have been
- * filtered out.
- *
- * @return the filtered parameters map
- */
- private Map<String, Object> getFilteredParameters() {
- final Map<String, Object> filteredMap = new HashMap<>(getParameters());
- filteredMap.keySet().removeIf(key -> key.startsWith(BuilderParameters.RESERVED_PARAMETER_PREFIX));
- return filteredMap;
- }
- /**
- * Gets a (unmodifiable) map with the current initialization parameters set for this builder. The map is populated
- * with the parameters set using the various configuration options.
- *
- * @return a map with the current set of initialization parameters
- */
- protected final synchronized Map<String, Object> getParameters() {
- if (parameters != null) {
- return parameters;
- }
- return Collections.emptyMap();
- }
- /**
- * Gets the result class of this builder. The objects produced by this builder have the class returned here.
- *
- * @return the result class of this builder
- */
- public Class<? extends T> getResultClass() {
- return resultClass;
- }
- /**
- * Gets the {@code BeanDeclaration} that is used to create and initialize result objects. The declaration is created
- * on first access (by invoking {@link #createResultDeclaration(Map)}) based on the current initialization parameters.
- *
- * @return the {@code BeanDeclaration} for dynamically creating a result object
- * @throws ConfigurationException if an error occurs
- */
- protected final synchronized BeanDeclaration getResultDeclaration() throws ConfigurationException {
- if (resultDeclaration == null) {
- resultDeclaration = createResultDeclaration(getFilteredParameters());
- }
- return resultDeclaration;
- }
- /**
- * Checks whether the specified parameters object implements the {@code EventListenerProvider} interface. If so, the
- * event listeners it provides are added to this builder.
- *
- * @param params the parameters object
- */
- private void handleEventListenerProviders(final BuilderParameters params) {
- if (params instanceof EventListenerProvider) {
- eventListeners.addAll(((EventListenerProvider) params).getListeners());
- }
- }
- /**
- * Performs special initialization of the result object. This method is called after parameters have been set on a newly
- * created result instance. If supported by the result class, the {@code initialize()} method is now called.
- *
- * @param obj the newly created result object
- */
- private void handleInitializable(final T obj) {
- if (obj instanceof Initializable) {
- ((Initializable) obj).initialize();
- }
- }
- /**
- * Initializes a newly created result object. This is the second step of the process of producing a result object for
- * this builder. This implementation uses the {@link BeanHelper} class to initialize the object's property based on the
- * {@link BeanDeclaration} returned by {@link #getResultDeclaration()}. Note: This method is invoked in a synchronized
- * block. This is required because internal state is accessed. Sub classes must not call this method without proper
- * synchronization.
- *
- * @param obj the object to be initialized
- * @throws ConfigurationException if an error occurs
- */
- protected void initResultInstance(final T obj) throws ConfigurationException {
- fetchBeanHelper().initBean(obj, getResultDeclaration());
- registerEventListeners(obj);
- handleInitializable(obj);
- }
- /**
- * Adds the specified event listener to this object. This method is called by {@code addEventListener()}, it does the
- * actual listener registration. Because it is final it can be called by sub classes in the constructor if there is
- * already the need to register an event listener.
- *
- * @param eventType the event type object
- * @param listener the listener to be registered
- * @param <E> the event type
- */
- protected final <E extends Event> void installEventListener(final EventType<E> eventType, final EventListener<? super E> listener) {
- fetchEventSource().addEventListener(eventType, listener);
- eventListeners.addEventListener(eventType, listener);
- }
- /**
- * Returns the <em>allowFailOnInit</em> flag. See the header comment for information about this flag.
- *
- * @return the <em>allowFailOnInit</em> flag
- */
- public boolean isAllowFailOnInit() {
- return allowFailOnInit;
- }
- /**
- * Registers the available event listeners at the given object. This method is called for each result object created by
- * the builder.
- *
- * @param obj the object to initialize
- */
- private void registerEventListeners(final T obj) {
- final EventSource evSrc = ConfigurationUtils.asEventSource(obj, true);
- eventListeners.getRegistrations().forEach(regData -> registerListener(evSrc, regData));
- }
- /**
- * {@inheritDoc} This implementation also takes care that the event listener is removed from the managed configuration
- * object.
- */
- @Override
- public <E extends Event> boolean removeEventListener(final EventType<E> eventType, final EventListener<? super E> listener) {
- fetchEventSource().removeEventListener(eventType, listener);
- return eventListeners.removeEventListener(eventType, listener);
- }
- /**
- * Removes all available event listeners from the given result object. This method is called when the result of this
- * builder is reset. Then the old managed configuration should no longer generate events.
- *
- * @param obj the affected result object
- */
- private void removeEventListeners(final T obj) {
- final EventSource evSrc = ConfigurationUtils.asEventSource(obj, true);
- eventListeners.getRegistrations().forEach(regData -> removeListener(evSrc, regData));
- }
- /**
- * Resets this builder. This is a convenience method which combines calls to {@link #resetResult()} and
- * {@link #resetParameters()}.
- */
- public synchronized void reset() {
- resetParameters();
- resetResult();
- }
- /**
- * Removes all initialization parameters of this builder. This method can be called if this builder is to be reused for
- * creating result objects with a different configuration.
- */
- public void resetParameters() {
- setParameters(null);
- }
- /**
- * Clears an existing result object. An invocation of this method causes a new {@code ImmutableConfiguration} object to
- * be created the next time {@link #getConfiguration()} is called.
- */
- public void resetResult() {
- final T oldResult;
- synchronized (this) {
- oldResult = result;
- result = null;
- resultDeclaration = null;
- }
- if (oldResult != null) {
- removeEventListeners(oldResult);
- }
- fireBuilderEvent(new ConfigurationBuilderEvent(this, ConfigurationBuilderEvent.RESET));
- }
- /**
- * Sets the initialization parameters of this builder. Already existing parameters are replaced by the content of the
- * given map.
- *
- * @param params the new initialization parameters of this builder; can be <strong>null</strong>, then all initialization
- * parameters are removed
- * @return a reference to this builder for method chaining
- */
- public synchronized BasicConfigurationBuilder<T> setParameters(final Map<String, Object> params) {
- updateParameters(params);
- return this;
- }
- /**
- * Replaces the current map with parameters by a new one.
- *
- * @param newParams the map with new parameters (may be <strong>null</strong>)
- */
- private void updateParameters(final Map<String, Object> newParams) {
- final Map<String, Object> map = new HashMap<>();
- if (newParams != null) {
- map.putAll(newParams);
- }
- parameters = Collections.unmodifiableMap(map);
- }
- }