View Javadoc
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    *     https://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;
18  
19  import java.util.Collection;
20  import java.util.Collections;
21  import java.util.HashMap;
22  import java.util.Map;
23  
24  import org.apache.commons.configuration2.ConfigurationUtils;
25  import org.apache.commons.configuration2.ImmutableConfiguration;
26  import org.apache.commons.configuration2.Initializable;
27  import org.apache.commons.configuration2.beanutils.BeanDeclaration;
28  import org.apache.commons.configuration2.beanutils.BeanHelper;
29  import org.apache.commons.configuration2.beanutils.ConstructorArg;
30  import org.apache.commons.configuration2.event.Event;
31  import org.apache.commons.configuration2.event.EventListener;
32  import org.apache.commons.configuration2.event.EventListenerList;
33  import org.apache.commons.configuration2.event.EventListenerRegistrationData;
34  import org.apache.commons.configuration2.event.EventSource;
35  import org.apache.commons.configuration2.event.EventType;
36  import org.apache.commons.configuration2.ex.ConfigurationException;
37  import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
38  import org.apache.commons.configuration2.reloading.ReloadingController;
39  
40  /**
41   * <p>
42   * An implementation of the {@code ConfigurationBuilder} interface which is able to create different concrete
43   * {@code ImmutableConfiguration} implementations based on reflection.
44   * </p>
45   * <p>
46   * When constructing an instance of this class the concrete {@code ImmutableConfiguration} implementation class has to
47   * be provided. Then properties for the new {@code ImmutableConfiguration} instance can be set. The first call to
48   * {@code getConfiguration()} creates and initializes the new {@code ImmutableConfiguration} object. It is cached and
49   * returned by subsequent calls. This cache - and also the initialization properties set so far - can be flushed by
50   * calling one of the {@code reset()} methods. That way other {@code ImmutableConfiguration} instances with different
51   * properties can be created.
52   * </p>
53   * <p>
54   * If the newly created {@code ImmutableConfiguration} object implements the {@code Initializable} interface, its
55   * {@code initialize()} method is called after all initialization properties have been set. This way a concrete
56   * implementation class can perform arbitrary initialization steps.
57   * </p>
58   * <p>
59   * There are multiple options for setting up a {@code BasicConfigurationBuilder} instance:
60   * </p>
61   * <ul>
62   * <li>All initialization properties can be set in one or multiple calls of the {@code configure()} method. In each call
63   * an arbitrary number of {@link BuilderParameters} objects can be passed. The API allows method chaining and is
64   * intended to be used from Java code.</li>
65   * <li>If builder instances are created by other means - for example using a dependency injection framework -, the fluent API
66   * approach may not be suitable. For those use cases it is also possible to pass in all initialization parameters as a
67   * map. The keys of the map have to match initialization properties of the {@code ImmutableConfiguration} object to be
68   * created, the values are the corresponding property values. For instance, the key <em>throwExceptionOnMissing</em> in
69   * the map will cause the method {@code setThrowExceptionOnMissing()} on the {@code ImmutableConfiguration} object to be
70   * called with the corresponding value as parameter.</li>
71   * </ul>
72   * <p>
73   * A builder instance can be constructed with an <em>allowFailOnInit</em> flag. If set to <strong>true</strong>,
74   * exceptions during initialization of the configuration are ignored; in such a case an empty configuration object is
75   * returned. A use case for this flag is a scenario in which a configuration is optional and created on demand the first
76   * time configuration data is to be stored. Consider an application that stores user-specific configuration data in the
77   * user's home directory: When started for the first time by a new user there is no configuration file; so it makes
78   * sense to start with an empty configuration object. On application exit, settings can be stored in this object and
79   * written to the associated file. Then they are available on next application start.
80   * </p>
81   * <p>
82   * This class is thread-safe. Multiple threads can modify initialization properties and call {@code getConfiguration()}.
83   * However, the intended use case is that the builder is configured by a single thread first. Then
84   * {@code getConfiguration()} can be called concurrently, and it is guaranteed that always the same
85   * {@code ImmutableConfiguration} instance is returned until the builder is reset.
86   * </p>
87   *
88   * @param <T> the concrete type of {@code ImmutableConfiguration} objects created by this builder
89   * @since 2.0
90   */
91  public class BasicConfigurationBuilder<T extends ImmutableConfiguration> implements ConfigurationBuilder<T> {
92  
93      /**
94       * Registers an event listener at an event source object.
95       *
96       * @param evSrc the event source
97       * @param regData the registration data object
98       * @param <E> the type of the event listener
99       */
100     private static <E extends Event> void registerListener(final EventSource evSrc, final EventListenerRegistrationData<E> regData) {
101         evSrc.addEventListener(regData.getEventType(), regData.getListener());
102     }
103 
104     /**
105      * Removes an event listener from an event source object.
106      *
107      * @param evSrc the event source
108      * @param regData the registration data object
109      * @param <E> the type of the event listener
110      */
111     private static <E extends Event> void removeListener(final EventSource evSrc, final EventListenerRegistrationData<E> regData) {
112         evSrc.removeEventListener(regData.getEventType(), regData.getListener());
113     }
114 
115     /** The class of the objects produced by this builder instance. */
116     private final Class<? extends T> resultClass;
117 
118     /** An object managing the event listeners registered at this builder. */
119     private final EventListenerList eventListeners;
120 
121     /** A flag whether exceptions on initializing configurations are allowed. */
122     private final boolean allowFailOnInit;
123 
124     /** The map with current initialization parameters. */
125     private Map<String, Object> parameters;
126 
127     /** The current bean declaration. */
128     private BeanDeclaration resultDeclaration;
129 
130     /** The result object of this builder. */
131     private volatile T result;
132 
133     /**
134      * Creates a new instance of {@code BasicConfigurationBuilder} and initializes it with the given result class. No
135      * initialization properties are set.
136      *
137      * @param resCls the result class (must not be <strong>null</strong>)
138      * @throws IllegalArgumentException if the result class is <strong>null</strong>
139      */
140     public BasicConfigurationBuilder(final Class<? extends T> resCls) {
141         this(resCls, null);
142     }
143 
144     /**
145      * Creates a new instance of {@code BasicConfigurationBuilder} and initializes it with the given result class and an
146      * initial set of builder parameters. The <em>allowFailOnInit</em> flag is set to <strong>false</strong>.
147      *
148      * @param resCls the result class (must not be <strong>null</strong>)
149      * @param params a map with initialization parameters
150      * @throws IllegalArgumentException if the result class is <strong>null</strong>
151      */
152     public BasicConfigurationBuilder(final Class<? extends T> resCls, final Map<String, Object> params) {
153         this(resCls, params, false);
154     }
155 
156     /**
157      * Creates a new instance of {@code BasicConfigurationBuilder} and initializes it with the given result class, an
158      * initial set of builder parameters, and the <em>allowFailOnInit</em> flag. The map with parameters may be <strong>null</strong>,
159      * in this case no initialization parameters are set.
160      *
161      * @param resCls the result class (must not be <strong>null</strong>)
162      * @param params a map with initialization parameters
163      * @param allowFailOnInit a flag whether exceptions on initializing a newly created {@code ImmutableConfiguration}
164      *        object are allowed
165      * @throws IllegalArgumentException if the result class is <strong>null</strong>
166      */
167     public BasicConfigurationBuilder(final Class<? extends T> resCls, final Map<String, Object> params, final boolean allowFailOnInit) {
168         if (resCls == null) {
169             throw new IllegalArgumentException("Result class must not be null.");
170         }
171 
172         resultClass = resCls;
173         this.allowFailOnInit = allowFailOnInit;
174         eventListeners = new EventListenerList();
175         updateParameters(params);
176     }
177 
178     /**
179      * {@inheritDoc} This implementation also takes care that the event listener is added to the managed configuration
180      * object.
181      *
182      * @throws IllegalArgumentException if the event type or the listener is <strong>null</strong>
183      */
184     @Override
185     public <E extends Event> void addEventListener(final EventType<E> eventType, final EventListener<? super E> listener) {
186         installEventListener(eventType, listener);
187     }
188 
189     /**
190      * Adds the content of the given map to the already existing initialization parameters.
191      *
192      * @param params the map with additional initialization parameters; may be <strong>null</strong>, then this call has no effect
193      * @return a reference to this builder for method chaining
194      */
195     public synchronized BasicConfigurationBuilder<T> addParameters(final Map<String, Object> params) {
196         final Map<String, Object> newParams = new HashMap<>(getParameters());
197         if (params != null) {
198             newParams.putAll(params);
199         }
200         updateParameters(newParams);
201         return this;
202     }
203 
204     /**
205      * Checks whether the class of the result configuration is compatible with this builder's result class. This is done to
206      * ensure that only objects of the expected result class are created.
207      *
208      * @param inst the result instance to be checked
209      * @throws ConfigurationRuntimeException if an invalid result class is detected
210      */
211     private void checkResultInstance(final Object inst) {
212         if (!getResultClass().isInstance(inst)) {
213             throw new ConfigurationRuntimeException("Incompatible result object: " + inst);
214         }
215     }
216 
217     /**
218      * Appends the content of the specified {@code BuilderParameters} objects to the current initialization parameters.
219      * Calling this method multiple times will create a union of the parameters provided.
220      *
221      * @param params an arbitrary number of objects with builder parameters
222      * @return a reference to this builder for method chaining
223      * @throws NullPointerException if a <strong>null</strong> array is passed
224      */
225     public BasicConfigurationBuilder<T> configure(final BuilderParameters... params) {
226         final Map<String, Object> newParams = new HashMap<>();
227         for (final BuilderParameters p : params) {
228             newParams.putAll(p.getParameters());
229             handleEventListenerProviders(p);
230         }
231         return setParameters(newParams);
232     }
233 
234     /**
235      * Connects this builder with a {@code ReloadingController}. With this method support for reloading can be added to an
236      * arbitrary builder object. Event listeners are registered at the reloading controller and this builder with connect
237      * both objects:
238      * <ul>
239      * <li>When the reloading controller detects that a reload is required, the builder's {@link #resetResult()} method is
240      * called; so the managed result object is invalidated.</li>
241      * <li>When a new result object has been created the controller's reloading state is reset, so that new changes can be
242      * detected again.</li>
243      * </ul>
244      *
245      * @param controller the {@code ReloadingController} to connect to (must not be <strong>null</strong>)
246      * @throws IllegalArgumentException if the controller is <strong>null</strong>
247      */
248     public final void connectToReloadingController(final ReloadingController controller) {
249         if (controller == null) {
250             throw new IllegalArgumentException("ReloadingController must not be null.");
251         }
252         ReloadingBuilderSupportListener.connect(this, controller);
253     }
254 
255     /**
256      * Copies all {@code EventListener} objects registered at this builder to the specified target configuration builder.
257      * This method is intended to be used by derived classes which support inheritance of their properties to other builder
258      * objects.
259      *
260      * @param target the target configuration builder (must not be <strong>null</strong>)
261      * @throws NullPointerException if the target builder is <strong>null</strong>
262      */
263     protected synchronized void copyEventListeners(final BasicConfigurationBuilder<?> target) {
264         copyEventListeners(target, eventListeners);
265     }
266 
267     /**
268      * Copies all event listeners in the specified list to the specified target configuration builder. This method is
269      * intended to be used by derived classes which have to deal with managed configuration builders that need to be
270      * initialized with event listeners.
271      *
272      * @param target the target configuration builder (must not be <strong>null</strong>)
273      * @param listeners the event listeners to be copied over
274      * @throws NullPointerException if the target builder is <strong>null</strong>
275      */
276     protected void copyEventListeners(final BasicConfigurationBuilder<?> target, final EventListenerList listeners) {
277         target.eventListeners.addAll(listeners);
278     }
279 
280     /**
281      * Creates a new, initialized result object. This method is called by {@code getConfiguration()} if no valid result
282      * object exists. This base implementation performs two steps:
283      * <ul>
284      * <li>{@code createResultInstance()} is called to create a new, uninitialized result object.</li>
285      * <li>{@code initResultInstance()} is called to process all initialization parameters.</li>
286      * </ul>
287      * It also evaluates the <em>allowFailOnInit</em> flag, i.e. if initialization causes an exception and this flag is set,
288      * the exception is ignored, and the newly created, uninitialized configuration is returned. Note that this method is
289      * called in a synchronized block.
290      *
291      * @return the newly created result object
292      * @throws ConfigurationException if an error occurs
293      */
294     protected T createResult() throws ConfigurationException {
295         final T resObj = createResultInstance();
296 
297         try {
298             initResultInstance(resObj);
299         } catch (final ConfigurationException cex) {
300             if (!isAllowFailOnInit()) {
301                 throw cex;
302             }
303         }
304 
305         return resObj;
306     }
307 
308     /**
309      * Creates a new {@code BeanDeclaration} which is used for creating new result objects dynamically. This implementation
310      * creates a specialized {@code BeanDeclaration} object that is initialized from the given map of initialization
311      * parameters. The {@code BeanDeclaration} must be initialized with the result class of this builder, otherwise
312      * exceptions will be thrown when the result object is created. Note: This method is invoked in a synchronized block.
313      *
314      * @param params a snapshot of the current initialization parameters
315      * @return the {@code BeanDeclaration} for creating result objects
316      * @throws ConfigurationException if an error occurs
317      */
318     protected BeanDeclaration createResultDeclaration(final Map<String, Object> params) throws ConfigurationException {
319         return new BeanDeclaration() {
320             @Override
321             public String getBeanClassName() {
322                 return getResultClass().getName();
323             }
324 
325             @Override
326             public String getBeanFactoryName() {
327                 return null;
328             }
329 
330             @Override
331             public Object getBeanFactoryParameter() {
332                 return null;
333             }
334 
335             @Override
336             public Map<String, Object> getBeanProperties() {
337                 // the properties are equivalent to the parameters
338                 return params;
339             }
340 
341             @Override
342             public Collection<ConstructorArg> getConstructorArgs() {
343                 // no constructor arguments
344                 return Collections.emptySet();
345             }
346 
347             @Override
348             public Map<String, Object> getNestedBeanDeclarations() {
349                 // no nested beans
350                 return Collections.emptyMap();
351             }
352         };
353     }
354 
355     /**
356      * Creates the new, uninitialized result object. This is the first step of the process of producing a result object for
357      * this builder. This implementation uses the {@link BeanHelper} class to create a new object based on the
358      * {@link BeanDeclaration} returned by {@link #getResultDeclaration()}. Note: This method is invoked in a synchronized
359      * block.
360      *
361      * @return the newly created, yet uninitialized result object
362      * @throws ConfigurationException if an exception occurs
363      */
364     protected T createResultInstance() throws ConfigurationException {
365         final Object bean = fetchBeanHelper().createBean(getResultDeclaration());
366         checkResultInstance(bean);
367         return getResultClass().cast(bean);
368     }
369 
370     /**
371      * Obtains the {@code BeanHelper} object to be used when dealing with bean declarations. This method checks whether this
372      * builder was configured with a specific {@code BeanHelper} instance. If so, this instance is used. Otherwise, the
373      * default {@code BeanHelper} is returned.
374      *
375      * @return the {@code BeanHelper} to be used
376      */
377     protected final BeanHelper fetchBeanHelper() {
378         final BeanHelper helper = BasicBuilderParameters.fetchBeanHelper(getParameters());
379         return helper != null ? helper : BeanHelper.INSTANCE;
380     }
381 
382     /**
383      * Returns an {@code EventSource} for the current result object. If there is no current result or if it does not extend
384      * {@code EventSource}, a dummy event source is returned.
385      *
386      * @return the {@code EventSource} for the current result object
387      */
388     private EventSource fetchEventSource() {
389         return ConfigurationUtils.asEventSource(result, true);
390     }
391 
392     /**
393      * Sends the specified builder event to all registered listeners.
394      *
395      * @param event the event to be fired
396      */
397     protected void fireBuilderEvent(final ConfigurationBuilderEvent event) {
398         eventListeners.fire(event);
399     }
400 
401     /**
402      * {@inheritDoc} This implementation creates the result configuration on first access. Later invocations return the same
403      * object until this builder is reset. The double-check idiom for lazy initialization is used (Bloch, Effective Java,
404      * item 71).
405      */
406     @Override
407     public T getConfiguration() throws ConfigurationException {
408         fireBuilderEvent(new ConfigurationBuilderEvent(this, ConfigurationBuilderEvent.CONFIGURATION_REQUEST));
409 
410         T resObj = result;
411         boolean created = false;
412         if (resObj == null) {
413             synchronized (this) {
414                 resObj = result;
415                 if (resObj == null) {
416                     result = resObj = createResult();
417                     created = true;
418                 }
419             }
420         }
421 
422         if (created) {
423             fireBuilderEvent(new ConfigurationBuilderResultCreatedEvent(this, ConfigurationBuilderResultCreatedEvent.RESULT_CREATED, resObj));
424         }
425         return resObj;
426     }
427 
428     /**
429      * Gets a map with initialization parameters where all parameters starting with the reserved prefix have been
430      * filtered out.
431      *
432      * @return the filtered parameters map
433      */
434     private Map<String, Object> getFilteredParameters() {
435         final Map<String, Object> filteredMap = new HashMap<>(getParameters());
436         filteredMap.keySet().removeIf(key -> key.startsWith(BuilderParameters.RESERVED_PARAMETER_PREFIX));
437         return filteredMap;
438     }
439 
440     /**
441      * Gets a (unmodifiable) map with the current initialization parameters set for this builder. The map is populated
442      * with the parameters set using the various configuration options.
443      *
444      * @return a map with the current set of initialization parameters
445      */
446     protected final synchronized Map<String, Object> getParameters() {
447         if (parameters != null) {
448             return parameters;
449         }
450         return Collections.emptyMap();
451     }
452 
453     /**
454      * Gets the result class of this builder. The objects produced by this builder have the class returned here.
455      *
456      * @return the result class of this builder
457      */
458     public Class<? extends T> getResultClass() {
459         return resultClass;
460     }
461 
462     /**
463      * Gets the {@code BeanDeclaration} that is used to create and initialize result objects. The declaration is created
464      * on first access (by invoking {@link #createResultDeclaration(Map)}) based on the current initialization parameters.
465      *
466      * @return the {@code BeanDeclaration} for dynamically creating a result object
467      * @throws ConfigurationException if an error occurs
468      */
469     protected final synchronized BeanDeclaration getResultDeclaration() throws ConfigurationException {
470         if (resultDeclaration == null) {
471             resultDeclaration = createResultDeclaration(getFilteredParameters());
472         }
473         return resultDeclaration;
474     }
475 
476     /**
477      * Checks whether the specified parameters object implements the {@code EventListenerProvider} interface. If so, the
478      * event listeners it provides are added to this builder.
479      *
480      * @param params the parameters object
481      */
482     private void handleEventListenerProviders(final BuilderParameters params) {
483         if (params instanceof EventListenerProvider) {
484             eventListeners.addAll(((EventListenerProvider) params).getListeners());
485         }
486     }
487 
488     /**
489      * Performs special initialization of the result object. This method is called after parameters have been set on a newly
490      * created result instance. If supported by the result class, the {@code initialize()} method is now called.
491      *
492      * @param obj the newly created result object
493      */
494     private void handleInitializable(final T obj) {
495         if (obj instanceof Initializable) {
496             ((Initializable) obj).initialize();
497         }
498     }
499 
500     /**
501      * Initializes a newly created result object. This is the second step of the process of producing a result object for
502      * this builder. This implementation uses the {@link BeanHelper} class to initialize the object's property based on the
503      * {@link BeanDeclaration} returned by {@link #getResultDeclaration()}. Note: This method is invoked in a synchronized
504      * block. This is required because internal state is accessed. Sub classes must not call this method without proper
505      * synchronization.
506      *
507      * @param obj the object to be initialized
508      * @throws ConfigurationException if an error occurs
509      */
510     protected void initResultInstance(final T obj) throws ConfigurationException {
511         fetchBeanHelper().initBean(obj, getResultDeclaration());
512         registerEventListeners(obj);
513         handleInitializable(obj);
514     }
515 
516     /**
517      * Adds the specified event listener to this object. This method is called by {@code addEventListener()}, it does the
518      * actual listener registration. Because it is final it can be called by sub classes in the constructor if there is
519      * already the need to register an event listener.
520      *
521      * @param eventType the event type object
522      * @param listener the listener to be registered
523      * @param <E> the event type
524      */
525     protected final <E extends Event> void installEventListener(final EventType<E> eventType, final EventListener<? super E> listener) {
526         fetchEventSource().addEventListener(eventType, listener);
527         eventListeners.addEventListener(eventType, listener);
528     }
529 
530     /**
531      * Returns the <em>allowFailOnInit</em> flag. See the header comment for information about this flag.
532      *
533      * @return the <em>allowFailOnInit</em> flag
534      */
535     public boolean isAllowFailOnInit() {
536         return allowFailOnInit;
537     }
538 
539     /**
540      * Registers the available event listeners at the given object. This method is called for each result object created by
541      * the builder.
542      *
543      * @param obj the object to initialize
544      */
545     private void registerEventListeners(final T obj) {
546         final EventSource evSrc = ConfigurationUtils.asEventSource(obj, true);
547         eventListeners.getRegistrations().forEach(regData -> registerListener(evSrc, regData));
548     }
549 
550     /**
551      * {@inheritDoc} This implementation also takes care that the event listener is removed from the managed configuration
552      * object.
553      */
554     @Override
555     public <E extends Event> boolean removeEventListener(final EventType<E> eventType, final EventListener<? super E> listener) {
556         fetchEventSource().removeEventListener(eventType, listener);
557         return eventListeners.removeEventListener(eventType, listener);
558     }
559 
560     /**
561      * Removes all available event listeners from the given result object. This method is called when the result of this
562      * builder is reset. Then the old managed configuration should no longer generate events.
563      *
564      * @param obj the affected result object
565      */
566     private void removeEventListeners(final T obj) {
567         final EventSource evSrc = ConfigurationUtils.asEventSource(obj, true);
568         eventListeners.getRegistrations().forEach(regData -> removeListener(evSrc, regData));
569     }
570 
571     /**
572      * Resets this builder. This is a convenience method which combines calls to {@link #resetResult()} and
573      * {@link #resetParameters()}.
574      */
575     public synchronized void reset() {
576         resetParameters();
577         resetResult();
578     }
579 
580     /**
581      * Removes all initialization parameters of this builder. This method can be called if this builder is to be reused for
582      * creating result objects with a different configuration.
583      */
584     public void resetParameters() {
585         setParameters(null);
586     }
587 
588     /**
589      * Clears an existing result object. An invocation of this method causes a new {@code ImmutableConfiguration} object to
590      * be created the next time {@link #getConfiguration()} is called.
591      */
592     public void resetResult() {
593         final T oldResult;
594         synchronized (this) {
595             oldResult = result;
596             result = null;
597             resultDeclaration = null;
598         }
599 
600         if (oldResult != null) {
601             removeEventListeners(oldResult);
602         }
603         fireBuilderEvent(new ConfigurationBuilderEvent(this, ConfigurationBuilderEvent.RESET));
604     }
605 
606     /**
607      * Sets the initialization parameters of this builder. Already existing parameters are replaced by the content of the
608      * given map.
609      *
610      * @param params the new initialization parameters of this builder; can be <strong>null</strong>, then all initialization
611      *        parameters are removed
612      * @return a reference to this builder for method chaining
613      */
614     public synchronized BasicConfigurationBuilder<T> setParameters(final Map<String, Object> params) {
615         updateParameters(params);
616         return this;
617     }
618 
619     /**
620      * Replaces the current map with parameters by a new one.
621      *
622      * @param newParams the map with new parameters (may be <strong>null</strong>)
623      */
624     private void updateParameters(final Map<String, Object> newParams) {
625         final Map<String, Object> map = new HashMap<>();
626         if (newParams != null) {
627             map.putAll(newParams);
628         }
629         parameters = Collections.unmodifiableMap(map);
630     }
631 }