org.apache.commons.configuration2.builder.BasicConfigurationBuilder Maven / Gradle / Ivy
Show all versions of commons-configuration2 Show documentation
/*
* 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;
/**
*
* An implementation of the {@code ConfigurationBuilder} interface which is able to create different concrete
* {@code ImmutableConfiguration} implementations based on reflection.
*
*
* 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.
*
*
* 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.
*
*
* There are multiple options for setting up a {@code BasicConfigurationBuilder} instance:
*
*
* - 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.
* - If builder instances are created by other means - e.g. 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 throwExceptionOnMissing in
* the map will cause the method {@code setThrowExceptionOnMissing()} on the {@code ImmutableConfiguration} object to be
* called with the corresponding value as parameter.
*
*
* A builder instance can be constructed with an allowFailOnInit flag. If set to true,
* 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.
*
*
* 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.
*
*
* @since 2.0
* @param the concrete type of {@code ImmutableConfiguration} objects created by this builder
*/
public class BasicConfigurationBuilder implements ConfigurationBuilder {
/**
* Registers an event listener at an event source object.
*
* @param evSrc the event source
* @param regData the registration data object
* @param the type of the event listener
*/
private static void registerListener(final EventSource evSrc, final EventListenerRegistrationData 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 the type of the event listener
*/
private static void removeListener(final EventSource evSrc, final EventListenerRegistrationData 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 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 null)
* @throws IllegalArgumentException if the result class is null
*/
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 allowFailOnInit flag is set to false.
*
* @param resCls the result class (must not be null)
* @param params a map with initialization parameters
* @throws IllegalArgumentException if the result class is null
*/
public BasicConfigurationBuilder(final Class extends T> resCls, final Map 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 allowFailOnInit flag. The map with parameters may be null,
* in this case no initialization parameters are set.
*
* @param resCls the result class (must not be null)
* @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 null
*/
public BasicConfigurationBuilder(final Class extends T> resCls, final Map 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 null
*/
@Override
public void addEventListener(final EventType 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 null, then this call has no effect
* @return a reference to this builder for method chaining
*/
public synchronized BasicConfigurationBuilder addParameters(final Map params) {
final Map 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 null array is passed
*/
public BasicConfigurationBuilder configure(final BuilderParameters... params) {
final Map 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:
*
* - 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.
* - When a new result object has been created the controller's reloading state is reset, so that new changes can be
* detected again.
*
*
* @param controller the {@code ReloadingController} to connect to (must not be null)
* @throws IllegalArgumentException if the controller is null
*/
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 null)
* @throws NullPointerException if the target builder is null
*/
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 null)
* @param listeners the event listeners to be copied over
* @throws NullPointerException if the target builder is null
*/
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:
*
* - {@code createResultInstance()} is called to create a new, uninitialized result object.
* - {@code initResultInstance()} is called to process all initialization parameters.
*
* It also evaluates the allowFailOnInit 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 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 getBeanProperties() {
// the properties are equivalent to the parameters
return params;
}
@Override
public Collection getConstructorArgs() {
// no constructor arguments
return Collections.emptySet();
}
@Override
public Map 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 getFilteredParameters() {
final Map 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 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 the event type
*/
protected final void installEventListener(final EventType eventType, final EventListener super E> listener) {
fetchEventSource().addEventListener(eventType, listener);
eventListeners.addEventListener(eventType, listener);
}
/**
* Returns the allowFailOnInit flag. See the header comment for information about this flag.
*
* @return the allowFailOnInit 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 boolean removeEventListener(final EventType 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 null, then all initialization
* parameters are removed
* @return a reference to this builder for method chaining
*/
public synchronized BasicConfigurationBuilder setParameters(final Map params) {
updateParameters(params);
return this;
}
/**
* Replaces the current map with parameters by a new one.
*
* @param newParams the map with new parameters (may be null)
*/
private void updateParameters(final Map newParams) {
final Map map = new HashMap<>();
if (newParams != null) {
map.putAll(newParams);
}
parameters = Collections.unmodifiableMap(map);
}
}