001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.configuration2;
018
019import java.io.IOException;
020import java.io.Reader;
021import java.io.Writer;
022import java.math.BigDecimal;
023import java.math.BigInteger;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Objects;
029import java.util.Properties;
030
031import org.apache.commons.configuration2.event.Event;
032import org.apache.commons.configuration2.event.EventListener;
033import org.apache.commons.configuration2.event.EventType;
034import org.apache.commons.configuration2.ex.ConfigurationException;
035import org.apache.commons.configuration2.io.FileBased;
036import org.apache.commons.configuration2.tree.ExpressionEngine;
037import org.apache.commons.configuration2.tree.ImmutableNode;
038
039/**
040 * Wraps a BaseHierarchicalConfiguration and allows subtrees to be accessed via a configured path with replaceable
041 * tokens derived from the ConfigurationInterpolator. When used with injection frameworks such as Spring it allows
042 * components to be injected with subtrees of the configuration.
043 *
044 * @since 1.6
045 */
046public class PatternSubtreeConfigurationWrapper extends BaseHierarchicalConfiguration implements FileBasedConfiguration {
047
048    /** The wrapped configuration */
049    private final HierarchicalConfiguration<ImmutableNode> config;
050
051    /** The path to the subtree */
052    private final String path;
053
054    /** True if the path ends with '/', false otherwise */
055    private final boolean trailing;
056
057    /** True if the constructor has finished */
058    private final boolean init;
059
060    /**
061     * Constructs a new instance.
062     *
063     * @param config The Configuration to be wrapped.
064     * @param path The base path pattern.
065     */
066    public PatternSubtreeConfigurationWrapper(final HierarchicalConfiguration<ImmutableNode> config, final String path) {
067        this.config = Objects.requireNonNull(config, "config");
068        this.path = path;
069        this.trailing = path.endsWith("/");
070        this.init = true;
071    }
072
073    @Override
074    public <T extends Event> void addEventListener(final EventType<T> eventType, final EventListener<? super T> listener) {
075        getConfig().addEventListener(eventType, listener);
076    }
077
078    @Override
079    protected void addNodesInternal(final String key, final Collection<? extends ImmutableNode> nodes) {
080        getConfig().addNodes(key, nodes);
081    }
082
083    @Override
084    protected void addPropertyInternal(final String key, final Object value) {
085        config.addProperty(makePath(key), value);
086    }
087
088    @Override
089    public void clearErrorListeners() {
090        getConfig().clearErrorListeners();
091    }
092
093    @Override
094    public void clearEventListeners() {
095        getConfig().clearEventListeners();
096    }
097
098    @Override
099    protected void clearInternal() {
100        getConfig().clear();
101    }
102
103    @Override
104    protected void clearPropertyDirect(final String key) {
105        config.clearProperty(makePath(key));
106    }
107
108    @Override
109    protected Object clearTreeInternal(final String key) {
110        config.clearTree(makePath(key));
111        return Collections.emptyList();
112    }
113
114    @Override
115    public HierarchicalConfiguration<ImmutableNode> configurationAt(final String key) {
116        return config.configurationAt(makePath(key));
117    }
118
119    @Override
120    public HierarchicalConfiguration<ImmutableNode> configurationAt(final String key, final boolean supportUpdates) {
121        return config.configurationAt(makePath(key), supportUpdates);
122    }
123
124    @Override
125    public List<HierarchicalConfiguration<ImmutableNode>> configurationsAt(final String key) {
126        return config.configurationsAt(makePath(key));
127    }
128
129    @Override
130    protected boolean containsKeyInternal(final String key) {
131        return config.containsKey(makePath(key));
132    }
133
134    /**
135     * Tests whether this configuration contains one or more matches to this value. This operation stops at first
136     * match but may be more expensive than the containsKey method.
137     *
138     * @since 2.11.0
139     */
140    @Override
141    protected boolean containsValueInternal(final Object value) {
142        return config.containsValue(value);
143    }
144
145    /**
146     * Returns the wrapped configuration as a {@code FileBased} object. If this cast is not possible, an exception is
147     * thrown.
148     *
149     * @return the wrapped configuration as {@code FileBased}
150     * @throws ConfigurationException if the wrapped configuration does not implement {@code FileBased}
151     */
152    private FileBased fetchFileBased() throws ConfigurationException {
153        if (!(config instanceof FileBased)) {
154            throw new ConfigurationException("Wrapped configuration does not implement FileBased! No I/O operations are supported.");
155        }
156        return (FileBased) config;
157    }
158
159    @Override
160    public BigDecimal getBigDecimal(final String key) {
161        return config.getBigDecimal(makePath(key));
162    }
163
164    @Override
165    public BigDecimal getBigDecimal(final String key, final BigDecimal defaultValue) {
166        return config.getBigDecimal(makePath(key), defaultValue);
167    }
168
169    @Override
170    public BigInteger getBigInteger(final String key) {
171        return config.getBigInteger(makePath(key));
172    }
173
174    @Override
175    public BigInteger getBigInteger(final String key, final BigInteger defaultValue) {
176        return config.getBigInteger(makePath(key), defaultValue);
177    }
178
179    @Override
180    public boolean getBoolean(final String key) {
181        return config.getBoolean(makePath(key));
182    }
183
184    @Override
185    public boolean getBoolean(final String key, final boolean defaultValue) {
186        return config.getBoolean(makePath(key), defaultValue);
187    }
188
189    @Override
190    public Boolean getBoolean(final String key, final Boolean defaultValue) {
191        return config.getBoolean(makePath(key), defaultValue);
192    }
193
194    @Override
195    public byte getByte(final String key) {
196        return config.getByte(makePath(key));
197    }
198
199    @Override
200    public byte getByte(final String key, final byte defaultValue) {
201        return config.getByte(makePath(key), defaultValue);
202    }
203
204    @Override
205    public Byte getByte(final String key, final Byte defaultValue) {
206        return config.getByte(makePath(key), defaultValue);
207    }
208
209    private BaseHierarchicalConfiguration getConfig() {
210        return (BaseHierarchicalConfiguration) config.configurationAt(makePath());
211    }
212
213    @Override
214    public double getDouble(final String key) {
215        return config.getDouble(makePath(key));
216    }
217
218    @Override
219    public double getDouble(final String key, final double defaultValue) {
220        return config.getDouble(makePath(key), defaultValue);
221    }
222
223    @Override
224    public Double getDouble(final String key, final Double defaultValue) {
225        return config.getDouble(makePath(key), defaultValue);
226    }
227
228    @Override
229    public <T extends Event> Collection<EventListener<? super T>> getEventListeners(final EventType<T> eventType) {
230        return getConfig().getEventListeners(eventType);
231    }
232
233    @Override
234    public ExpressionEngine getExpressionEngine() {
235        return config.getExpressionEngine();
236    }
237
238    @Override
239    public float getFloat(final String key) {
240        return config.getFloat(makePath(key));
241    }
242
243    @Override
244    public float getFloat(final String key, final float defaultValue) {
245        return config.getFloat(makePath(key), defaultValue);
246    }
247
248    @Override
249    public Float getFloat(final String key, final Float defaultValue) {
250        return config.getFloat(makePath(key), defaultValue);
251    }
252
253    @Override
254    public int getInt(final String key) {
255        return config.getInt(makePath(key));
256    }
257
258    @Override
259    public int getInt(final String key, final int defaultValue) {
260        return config.getInt(makePath(key), defaultValue);
261    }
262
263    @Override
264    public Integer getInteger(final String key, final Integer defaultValue) {
265        return config.getInteger(makePath(key), defaultValue);
266    }
267
268    @Override
269    protected Iterator<String> getKeysInternal() {
270        return config.getKeys(makePath());
271    }
272
273    @Override
274    protected Iterator<String> getKeysInternal(final String prefix) {
275        return config.getKeys(makePath(prefix));
276    }
277
278    @Override
279    public List<Object> getList(final String key) {
280        return config.getList(makePath(key));
281    }
282
283    @Override
284    public List<Object> getList(final String key, final List<?> defaultValue) {
285        return config.getList(makePath(key), defaultValue);
286    }
287
288    @Override
289    public long getLong(final String key) {
290        return config.getLong(makePath(key));
291    }
292
293    @Override
294    public long getLong(final String key, final long defaultValue) {
295        return config.getLong(makePath(key), defaultValue);
296    }
297
298    @Override
299    public Long getLong(final String key, final Long defaultValue) {
300        return config.getLong(makePath(key), defaultValue);
301    }
302
303    @Override
304    protected int getMaxIndexInternal(final String key) {
305        return config.getMaxIndex(makePath(key));
306    }
307
308    @Override
309    public Properties getProperties(final String key) {
310        return config.getProperties(makePath(key));
311    }
312
313    @Override
314    protected Object getPropertyInternal(final String key) {
315        return config.getProperty(makePath(key));
316    }
317
318    @Override
319    public short getShort(final String key) {
320        return config.getShort(makePath(key));
321    }
322
323    @Override
324    public short getShort(final String key, final short defaultValue) {
325        return config.getShort(makePath(key), defaultValue);
326    }
327
328    @Override
329    public Short getShort(final String key, final Short defaultValue) {
330        return config.getShort(makePath(key), defaultValue);
331    }
332
333    @Override
334    public String getString(final String key) {
335        return config.getString(makePath(key));
336    }
337
338    @Override
339    public String getString(final String key, final String defaultValue) {
340        return config.getString(makePath(key), defaultValue);
341    }
342
343    @Override
344    public String[] getStringArray(final String key) {
345        return config.getStringArray(makePath(key));
346    }
347
348    @Override
349    public Configuration interpolatedConfiguration() {
350        return getConfig().interpolatedConfiguration();
351    }
352
353    @Override
354    protected boolean isEmptyInternal() {
355        return getConfig().isEmpty();
356    }
357
358    private String makePath() {
359        final String pathPattern = trailing ? path.substring(0, path.length() - 1) : path;
360        return substitute(pathPattern);
361    }
362
363    /*
364     * Resolve the root expression and then add the item being retrieved. Insert a separator character as required.
365     */
366    private String makePath(final String item) {
367        final String pathPattern;
368        if ((item.isEmpty() || item.startsWith("/")) && trailing) {
369            pathPattern = path.substring(0, path.length() - 1);
370        } else if (!item.startsWith("/") || !trailing) {
371            pathPattern = path + "/";
372        } else {
373            pathPattern = path;
374        }
375        return substitute(pathPattern) + item;
376    }
377
378    @Override
379    public void read(final Reader reader) throws ConfigurationException, IOException {
380        fetchFileBased().read(reader);
381    }
382
383    @Override
384    public <T extends Event> boolean removeEventListener(final EventType<T> eventType, final EventListener<? super T> listener) {
385        return getConfig().removeEventListener(eventType, listener);
386    }
387
388    @Override
389    public void setExpressionEngine(final ExpressionEngine expressionEngine) {
390        if (init) {
391            config.setExpressionEngine(expressionEngine);
392        } else {
393            super.setExpressionEngine(expressionEngine);
394        }
395    }
396
397    @Override
398    protected void setPropertyInternal(final String key, final Object value) {
399        getConfig().setProperty(key, value);
400    }
401
402    @Override
403    public Configuration subset(final String prefix) {
404        return getConfig().subset(prefix);
405    }
406
407    /**
408     * Uses this configuration's {@code ConfigurationInterpolator} to perform variable substitution on the given pattern
409     * string.
410     *
411     * @param pattern the pattern string
412     * @return the string with variables replaced
413     */
414    private String substitute(final String pattern) {
415        return Objects.toString(getInterpolator().interpolate(pattern), null);
416    }
417
418    @Override
419    public void write(final Writer writer) throws ConfigurationException, IOException {
420        fetchFileBased().write(writer);
421    }
422}