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