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