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 }