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    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration2.builder;
18  
19  import java.util.ArrayList;
20  import java.util.Collection;
21  import java.util.HashMap;
22  import java.util.Map;
23  
24  import org.apache.commons.configuration2.ConfigurationDecoder;
25  import org.apache.commons.configuration2.beanutils.BeanHelper;
26  import org.apache.commons.configuration2.convert.ConversionHandler;
27  import org.apache.commons.configuration2.convert.ListDelimiterHandler;
28  import org.apache.commons.configuration2.interpol.ConfigurationInterpolator;
29  import org.apache.commons.configuration2.interpol.InterpolatorSpecification;
30  import org.apache.commons.configuration2.interpol.Lookup;
31  import org.apache.commons.configuration2.io.ConfigurationLogger;
32  import org.apache.commons.configuration2.sync.Synchronizer;
33  
34  /**
35   * <p>
36   * An implementation of {@code BuilderParameters} which handles the parameters of a {@link ConfigurationBuilder} common
37   * to all concrete {@code Configuration} implementations.
38   * </p>
39   * <p>
40   * This class provides methods for setting standard properties supported by the {@code AbstractConfiguration} base
41   * class. A fluent interface can be used to set property values.
42   * </p>
43   * <p>
44   * This class is not thread-safe. It is intended that an instance is constructed and initialized by a single thread
45   * during configuration of a {@code ConfigurationBuilder}.
46   * </p>
47   *
48   * @since 2.0
49   */
50  public class BasicBuilderParameters implements Cloneable, BuilderParameters, BasicBuilderProperties<BasicBuilderParameters> {
51      /** The key of the <em>throwExceptionOnMissing</em> property. */
52      private static final String PROP_THROW_EXCEPTION_ON_MISSING = "throwExceptionOnMissing";
53  
54      /** The key of the <em>listDelimiterHandler</em> property. */
55      private static final String PROP_LIST_DELIMITER_HANDLER = "listDelimiterHandler";
56  
57      /** The key of the <em>logger</em> property. */
58      private static final String PROP_LOGGER = "logger";
59  
60      /** The key for the <em>interpolator</em> property. */
61      private static final String PROP_INTERPOLATOR = "interpolator";
62  
63      /** The key for the <em>prefixLookups</em> property. */
64      private static final String PROP_PREFIX_LOOKUPS = "prefixLookups";
65  
66      /** The key for the <em>defaultLookups</em> property. */
67      private static final String PROP_DEFAULT_LOOKUPS = "defaultLookups";
68  
69      /** The key for the <em>parentInterpolator</em> property. */
70      private static final String PROP_PARENT_INTERPOLATOR = "parentInterpolator";
71  
72      /** The key for the <em>synchronizer</em> property. */
73      private static final String PROP_SYNCHRONIZER = "synchronizer";
74  
75      /** The key for the <em>conversionHandler</em> property. */
76      private static final String PROP_CONVERSION_HANDLER = "conversionHandler";
77  
78      /** The key for the <em>configurationDecoder</em> property. */
79      private static final String PROP_CONFIGURATION_DECODER = "configurationDecoder";
80  
81      /** The key for the {@code BeanHelper}. */
82      private static final String PROP_BEAN_HELPER = RESERVED_PARAMETER_PREFIX + "BeanHelper";
83  
84      /** The map for storing the current property values. */
85      private Map<String, Object> properties;
86  
87      /**
88       * Creates a new instance of {@code BasicBuilderParameters}.
89       */
90      public BasicBuilderParameters() {
91          properties = new HashMap<>();
92      }
93  
94      /**
95       * {@inheritDoc} This implementation returns a copy of the internal parameters map with the values set so far.
96       * Collection structures (e.g. for lookup objects) are stored as defensive copies, so the original data cannot be
97       * modified.
98       */
99      @Override
100     public Map<String, Object> getParameters() {
101         final HashMap<String, Object> result = new HashMap<>(properties);
102         if (result.containsKey(PROP_INTERPOLATOR)) {
103             // A custom ConfigurationInterpolator overrides lookups
104             result.remove(PROP_PREFIX_LOOKUPS);
105             result.remove(PROP_DEFAULT_LOOKUPS);
106             result.remove(PROP_PARENT_INTERPOLATOR);
107         }
108 
109         createDefensiveCopies(result);
110         return result;
111     }
112 
113     /**
114      * Sets the <em>logger</em> property. With this property a concrete {@code Log} object can be set for the configuration.
115      * Thus logging behavior can be controlled.
116      *
117      * @param log the {@code Log} for the configuration produced by this builder
118      * @return a reference to this object for method chaining
119      */
120     @Override
121     public BasicBuilderParameters setLogger(final ConfigurationLogger log) {
122         return setProperty(PROP_LOGGER, log);
123     }
124 
125     /**
126      * Sets the value of the <em>throwExceptionOnMissing</em> property. This property controls the configuration's behavior
127      * if missing properties are queried: a value of <b>true</b> causes the configuration to throw an exception, for a value
128      * of <b>false</b> it will return <b>null</b> values. (Note: Methods returning a primitive data type will always throw
129      * an exception if the property is not defined.)
130      *
131      * @param b the value of the property
132      * @return a reference to this object for method chaining
133      */
134     @Override
135     public BasicBuilderParameters setThrowExceptionOnMissing(final boolean b) {
136         return setProperty(PROP_THROW_EXCEPTION_ON_MISSING, Boolean.valueOf(b));
137     }
138 
139     /**
140      * Sets the value of the <em>listDelimiterHandler</em> property. This property defines the object responsible for
141      * dealing with list delimiter and escaping characters. Note:
142      * {@link org.apache.commons.configuration2.AbstractConfiguration AbstractConfiguration} does not allow setting this
143      * property to <b>null</b>. If the default {@code ListDelimiterHandler} is to be used, do not call this method.
144      *
145      * @param handler the {@code ListDelimiterHandler}
146      * @return a reference to this object for method chaining
147      */
148     @Override
149     public BasicBuilderParameters setListDelimiterHandler(final ListDelimiterHandler handler) {
150         return setProperty(PROP_LIST_DELIMITER_HANDLER, handler);
151     }
152 
153     /**
154      * {@inheritDoc} The passed in {@code ConfigurationInterpolator} is set without modifications.
155      */
156     @Override
157     public BasicBuilderParameters setInterpolator(final ConfigurationInterpolator ci) {
158         return setProperty(PROP_INTERPOLATOR, ci);
159     }
160 
161     /**
162      * {@inheritDoc} A defensive copy of the passed in map is created. A <b>null</b> argument causes all prefix lookups to
163      * be removed from the internal parameters map.
164      */
165     @Override
166     public BasicBuilderParameters setPrefixLookups(final Map<String, ? extends Lookup> lookups) {
167         if (lookups == null) {
168             properties.remove(PROP_PREFIX_LOOKUPS);
169             return this;
170         }
171         return setProperty(PROP_PREFIX_LOOKUPS, new HashMap<>(lookups));
172     }
173 
174     /**
175      * {@inheritDoc} A defensive copy of the passed in collection is created. A <b>null</b> argument causes all default
176      * lookups to be removed from the internal parameters map.
177      */
178     @Override
179     public BasicBuilderParameters setDefaultLookups(final Collection<? extends Lookup> lookups) {
180         if (lookups == null) {
181             properties.remove(PROP_DEFAULT_LOOKUPS);
182             return this;
183         }
184         return setProperty(PROP_DEFAULT_LOOKUPS, new ArrayList<>(lookups));
185     }
186 
187     /**
188      * {@inheritDoc} This implementation stores the passed in {@code ConfigurationInterpolator} object in the internal
189      * parameters map.
190      */
191     @Override
192     public BasicBuilderParameters setParentInterpolator(final ConfigurationInterpolator parent) {
193         return setProperty(PROP_PARENT_INTERPOLATOR, parent);
194     }
195 
196     /**
197      * {@inheritDoc} This implementation stores the passed in {@code Synchronizer} object in the internal parameters map.
198      */
199     @Override
200     public BasicBuilderParameters setSynchronizer(final Synchronizer sync) {
201         return setProperty(PROP_SYNCHRONIZER, sync);
202     }
203 
204     /**
205      * {@inheritDoc} This implementation stores the passed in {@code ConversionHandler} object in the internal parameters
206      * map.
207      */
208     @Override
209     public BasicBuilderParameters setConversionHandler(final ConversionHandler handler) {
210         return setProperty(PROP_CONVERSION_HANDLER, handler);
211     }
212 
213     /**
214      * {@inheritDoc} This implementation stores the passed in {@code BeanHelper} object in the internal parameters map, but
215      * uses a reserved key, so that it is not used for the initialization of properties of the managed configuration object.
216      * The {@code fetchBeanHelper()} method can be used to obtain the {@code BeanHelper} instance from a parameters map.
217      */
218     @Override
219     public BasicBuilderParameters setBeanHelper(final BeanHelper beanHelper) {
220         return setProperty(PROP_BEAN_HELPER, beanHelper);
221     }
222 
223     /**
224      * {@inheritDoc} This implementation stores the passed in {@code ConfigurationDecoder} object in the internal parameters
225      * map.
226      */
227     @Override
228     public BasicBuilderParameters setConfigurationDecoder(final ConfigurationDecoder decoder) {
229         return setProperty(PROP_CONFIGURATION_DECODER, decoder);
230     }
231 
232     /**
233      * Merges this object with the given parameters object. This method adds all property values defined by the passed in
234      * parameters object to the internal storage which are not already in. So properties already defined in this object take
235      * precedence. Property names starting with the reserved parameter prefix are ignored.
236      *
237      * @param p the object whose properties should be merged (must not be <b>null</b>)
238      * @throws IllegalArgumentException if the passed in object is <b>null</b>
239      */
240     public void merge(final BuilderParameters p) {
241         if (p == null) {
242             throw new IllegalArgumentException("Parameters to merge must not be null!");
243         }
244         p.getParameters().forEach((k, v) -> {
245             if (!properties.containsKey(k) && !k.startsWith(RESERVED_PARAMETER_PREFIX)) {
246                 storeProperty(k, v);
247             }
248         });
249     }
250 
251     /**
252      * Inherits properties from the specified map. This can be used for instance to reuse parameters from one builder in
253      * another builder - also in parent-child relations in which a parent builder creates child builders. The purpose of
254      * this method is to let a concrete implementation decide which properties can be inherited. Because parameters are
255      * basically organized as a map it would be possible to simply copy over all properties from the source object. However,
256      * this is not appropriate in all cases. For instance, some properties - like a {@code ConfigurationInterpolator} - are
257      * tightly connected to a configuration and cannot be reused in a different context. For other properties, e.g. a file
258      * name, it does not make sense to copy it. Therefore, an implementation has to be explicit in the properties it wants
259      * to take over.
260      *
261      * @param source the source properties to inherit from
262      * @throws IllegalArgumentException if the source map is <b>null</b>
263      */
264     public void inheritFrom(final Map<String, ?> source) {
265         if (source == null) {
266             throw new IllegalArgumentException("Source properties must not be null!");
267         }
268         copyPropertiesFrom(source, PROP_BEAN_HELPER, PROP_CONFIGURATION_DECODER, PROP_CONVERSION_HANDLER, PROP_LIST_DELIMITER_HANDLER, PROP_LOGGER,
269             PROP_SYNCHRONIZER, PROP_THROW_EXCEPTION_ON_MISSING);
270     }
271 
272     /**
273      * Obtains a specification for a {@link ConfigurationInterpolator} from the specified map with parameters. All
274      * properties related to interpolation are evaluated and added to the specification object.
275      *
276      * @param params the map with parameters (must not be <b>null</b>)
277      * @return an {@code InterpolatorSpecification} object constructed with data from the map
278      * @throws IllegalArgumentException if the map is <b>null</b> or contains invalid data
279      */
280     public static InterpolatorSpecification fetchInterpolatorSpecification(final Map<String, Object> params) {
281         checkParameters(params);
282         return new InterpolatorSpecification.Builder().withInterpolator(fetchParameter(params, PROP_INTERPOLATOR, ConfigurationInterpolator.class))
283             .withParentInterpolator(fetchParameter(params, PROP_PARENT_INTERPOLATOR, ConfigurationInterpolator.class))
284             .withPrefixLookups(fetchAndCheckPrefixLookups(params)).withDefaultLookups(fetchAndCheckDefaultLookups(params)).create();
285     }
286 
287     /**
288      * Obtains the {@code BeanHelper} object from the specified map with parameters. This method can be used to obtain an
289      * instance from a parameters map that has been set via the {@code setBeanHelper()} method. If no such instance is
290      * found, result is <b>null</b>.
291      *
292      * @param params the map with parameters (must not be <b>null</b>)
293      * @return the {@code BeanHelper} stored in this map or <b>null</b>
294      * @throws IllegalArgumentException if the map is <b>null</b>
295      */
296     public static BeanHelper fetchBeanHelper(final Map<String, Object> params) {
297         checkParameters(params);
298         return (BeanHelper) params.get(PROP_BEAN_HELPER);
299     }
300 
301     /**
302      * Clones this object. This is useful because multiple builder instances may use a similar set of parameters. However,
303      * single instances of parameter objects must not assigned to multiple builders. Therefore, cloning a parameters object
304      * provides a solution for this use case. This method creates a new parameters object with the same content as this one.
305      * The internal map storing the parameter values is cloned, too, also collection structures contained in this map.
306      * However, no a full deep clone operation is performed. Objects like a {@code ConfigurationInterpolator} or
307      * {@code Lookup}s are shared between this and the newly created instance.
308      *
309      * @return a clone of this object
310      */
311     @Override
312     public BasicBuilderParameters clone() {
313         try {
314             final BasicBuilderParameters copy = (BasicBuilderParameters) super.clone();
315             copy.properties = getParameters();
316             return copy;
317         } catch (final CloneNotSupportedException cnex) {
318             // should not happen
319             throw new AssertionError(cnex);
320         }
321     }
322 
323     /**
324      * Sets a property for this parameters object. Properties are stored in an internal map. With this method a new entry
325      * can be added to this map. If the value is <b>null</b>, the key is removed from the internal map. This method can be
326      * used by sub classes which also store properties in a map.
327      *
328      * @param key the key of the property
329      * @param value the value of the property
330      */
331     protected void storeProperty(final String key, final Object value) {
332         if (value == null) {
333             properties.remove(key);
334         } else {
335             properties.put(key, value);
336         }
337     }
338 
339     /**
340      * Obtains the value of the specified property from the internal map. This method can be used by derived classes if a
341      * specific property is to be accessed. If the given key is not found, result is <b>null</b>.
342      *
343      * @param key the key of the property in question
344      * @return the value of the property with this key or <b>null</b>
345      */
346     protected Object fetchProperty(final String key) {
347         return properties.get(key);
348     }
349 
350     /**
351      * Copies a number of properties from the given map into this object. Properties are only copied if they are defined in
352      * the source map.
353      *
354      * @param source the source map
355      * @param keys the keys to be copied
356      */
357     protected void copyPropertiesFrom(final Map<String, ?> source, final String... keys) {
358         for (final String key : keys) {
359             final Object value = source.get(key);
360             if (value != null) {
361                 storeProperty(key, value);
362             }
363         }
364     }
365 
366     /**
367      * Helper method for setting a property value.
368      *
369      * @param key the key of the property
370      * @param value the value of the property
371      * @return a reference to this object
372      */
373     private BasicBuilderParameters setProperty(final String key, final Object value) {
374         storeProperty(key, value);
375         return this;
376     }
377 
378     /**
379      * Creates defensive copies for collection structures when constructing the map with parameters. It should not be
380      * possible to modify this object's internal state when having access to the parameters map.
381      *
382      * @param params the map with parameters to be passed to the caller
383      */
384     private static void createDefensiveCopies(final HashMap<String, Object> params) {
385         final Map<String, ? extends Lookup> prefixLookups = fetchPrefixLookups(params);
386         if (prefixLookups != null) {
387             params.put(PROP_PREFIX_LOOKUPS, new HashMap<>(prefixLookups));
388         }
389         final Collection<? extends Lookup> defLookups = fetchDefaultLookups(params);
390         if (defLookups != null) {
391             params.put(PROP_DEFAULT_LOOKUPS, new ArrayList<>(defLookups));
392         }
393     }
394 
395     /**
396      * Obtains the map with prefix lookups from the parameters map.
397      *
398      * @param params the map with parameters
399      * @return the map with prefix lookups (may be <b>null</b>)
400      */
401     private static Map<String, ? extends Lookup> fetchPrefixLookups(final Map<String, Object> params) {
402         // This is safe to cast because we either have full control over the map
403         // and thus know the types of the contained values or have checked
404         // the content before
405         @SuppressWarnings("unchecked")
406         final Map<String, ? extends Lookup> prefixLookups = (Map<String, ? extends Lookup>) params.get(PROP_PREFIX_LOOKUPS);
407         return prefixLookups;
408     }
409 
410     /**
411      * Tests whether the passed in map with parameters contains a map with prefix lookups. This method is used if the
412      * parameters map is from an insecure source and we cannot be sure that it contains valid data. Therefore, we have to
413      * map that the key for the prefix lookups actually points to a map containing keys and values of expected data types.
414      *
415      * @param params the parameters map
416      * @return the obtained map with prefix lookups
417      * @throws IllegalArgumentException if the map contains invalid data
418      */
419     private static Map<String, ? extends Lookup> fetchAndCheckPrefixLookups(final Map<String, Object> params) {
420         final Map<?, ?> prefixes = fetchParameter(params, PROP_PREFIX_LOOKUPS, Map.class);
421         if (prefixes == null) {
422             return null;
423         }
424         prefixes.forEach((k, v) -> {
425             if (!(k instanceof String) || !(v instanceof Lookup)) {
426                 throw new IllegalArgumentException("Map with prefix lookups contains invalid data: " + prefixes);
427             }
428         });
429         return fetchPrefixLookups(params);
430     }
431 
432     /**
433      * Obtains the collection with default lookups from the parameters map.
434      *
435      * @param params the map with parameters
436      * @return the collection with default lookups (may be <b>null</b>)
437      */
438     private static Collection<? extends Lookup> fetchDefaultLookups(final Map<String, Object> params) {
439         // This is safe to cast because we either have full control over the map
440         // and thus know the types of the contained values or have checked
441         // the content before
442         @SuppressWarnings("unchecked")
443         final Collection<? extends Lookup> defLookups = (Collection<? extends Lookup>) params.get(PROP_DEFAULT_LOOKUPS);
444         return defLookups;
445     }
446 
447     /**
448      * Tests whether the passed in map with parameters contains a valid collection with default lookups. This method works
449      * like {@link #fetchAndCheckPrefixLookups(Map)}, but tests the default lookups collection.
450      *
451      * @param params the map with parameters
452      * @return the collection with default lookups (may be <b>null</b>)
453      * @throws IllegalArgumentException if invalid data is found
454      */
455     private static Collection<? extends Lookup> fetchAndCheckDefaultLookups(final Map<String, Object> params) {
456         final Collection<?> col = fetchParameter(params, PROP_DEFAULT_LOOKUPS, Collection.class);
457         if (col == null) {
458             return null;
459         }
460 
461         if (col.stream().noneMatch(Lookup.class::isInstance)) {
462             throw new IllegalArgumentException("Collection with default lookups contains invalid data: " + col);
463         }
464         return fetchDefaultLookups(params);
465     }
466 
467     /**
468      * Obtains a parameter from a map and performs a type check.
469      *
470      * @param params the map with parameters
471      * @param key the key of the parameter
472      * @param expClass the expected class of the parameter value
473      * @param <T> the parameter type
474      * @return the value of the parameter in the correct data type
475      * @throws IllegalArgumentException if the parameter is not of the expected type
476      */
477     private static <T> T fetchParameter(final Map<String, Object> params, final String key, final Class<T> expClass) {
478         final Object value = params.get(key);
479         if (value == null) {
480             return null;
481         }
482         if (!expClass.isInstance(value)) {
483             throw new IllegalArgumentException(String.format("Parameter %s is not of type %s!", key, expClass.getSimpleName()));
484         }
485         return expClass.cast(value);
486     }
487 
488     /**
489      * Checks whether a map with parameters is present. Throws an exception if not.
490      *
491      * @param params the map with parameters to check
492      * @throws IllegalArgumentException if the map is <b>null</b>
493      */
494     private static void checkParameters(final Map<String, Object> params) {
495         if (params == null) {
496             throw new IllegalArgumentException("Parameters map must not be null!");
497         }
498     }
499 }