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