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 */
017
018package org.apache.commons.configuration2;
019
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Map;
025import java.util.Objects;
026import java.util.Properties;
027
028import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
029
030/**
031 * <p>
032 * A Map based Configuration.
033 * </p>
034 * <p>
035 * This implementation of the {@code Configuration} interface is initialized with a {@link java.util.Map}. The methods
036 * of the {@code Configuration} interface are implemented on top of the content of this map. The following storage
037 * scheme is used:
038 * </p>
039 * <p>
040 * Property keys are directly mapped to map keys, i.e. the {@code getProperty()} method directly performs a
041 * {@code get()} on the map. Analogously, {@code setProperty()} or {@code addProperty()} operations write new data into
042 * the map. If a value is added to an existing property, a {@link java.util.List} is created, which stores the values of
043 * this property.
044 * </p>
045 * <p>
046 * An important use case of this class is to treat a map as a {@code Configuration} allowing access to its data through
047 * the richer interface. This can be a bit problematic in some cases because the map may contain values that need not
048 * adhere to the default storage scheme used by typical configuration implementations, for example regarding lists. In such
049 * cases care must be taken when manipulating the data through the {@code Configuration} interface, for example by calling
050 * {@code addProperty()}; results may be different than expected.
051 * </p>
052 * <p>
053 * The handling of list delimiters is a bit different for this configuration implementation: When a property of type
054 * String is queried, it is passed to the current {@link org.apache.commons.configuration2.convert.ListDelimiterHandler
055 * ListDelimiterHandler} which may generate multiple values. Note that per default a list delimiter handler is set which
056 * does not do any list splitting, so this feature is disabled. It can be enabled by setting a properly configured
057 * {@code ListDelimiterHandler} implementation, for example a
058 * {@link org.apache.commons.configuration2.convert.DefaultListDelimiterHandler DefaultListDelimiterHandler} object.
059 * </p>
060 * <p>
061 * Notice that list splitting is only performed for single string values. If a property has multiple values, the single
062 * values are not split even if they contain the list delimiter character.
063 * </p>
064 * <p>
065 * As the underlying {@code Map} is directly used as store of the property values, the thread-safety of this
066 * {@code Configuration} implementation depends on the map passed to the constructor.
067 * </p>
068 * <p>
069 * Notes about type safety: For properties with multiple values this implementation creates lists of type {@code Object}
070 * and stores them. If a property is assigned another value, the value is added to the list. This can cause problems if
071 * the map passed to the constructor already contains lists of other types. This should be avoided, otherwise it cannot
072 * be guaranteed that the application might throw {@code ClassCastException} exceptions later.
073 * </p>
074 *
075 * @since 1.1
076 */
077public class MapConfiguration extends AbstractConfiguration implements Cloneable {
078
079    /**
080     * Helper method for converting the type of the {@code Properties} object to a supported map type. As stated by the
081     * comment of the constructor, we expect the {@code Properties} object to contain only String key; therefore, it is safe
082     * to do this cast.
083     *
084     * @param props the {@code Properties} to be copied.
085     * @return a newly created map with all string keys of the properties.
086     */
087    @SuppressWarnings("unchecked")
088    private static Map<String, Object> toMap(final Properties props) {
089        @SuppressWarnings("rawtypes")
090        final Map map = props;
091        return map;
092    }
093
094    /** The Map decorated by this configuration. */
095    protected Map<String, Object> map;
096
097    /** A flag whether trimming of property values should be disabled. */
098    private boolean trimmingDisabled;
099
100    /**
101     * Create a Configuration decorator around the specified Map. The map is used to store the configuration properties, any
102     * change will also affect the Map.
103     *
104     * @param map the map.
105     */
106    public MapConfiguration(final Map<String, ?> map) {
107        this.map = (Map<String, Object>) Objects.requireNonNull(map, "map");
108    }
109
110    /**
111     * Creates a new instance of {@code MapConfiguration} which uses the specified {@code Properties} object as its data
112     * store. All changes of this configuration affect the given {@code Properties} object and vice versa. Note that while
113     * {@code Properties} actually implements {@code Map<Object, Object>}, we expect it to contain only string keys. Other
114     * key types will lead to {@code ClassCastException} exceptions on certain methods.
115     *
116     * @param props the {@code Properties} object defining the content of this configuration.
117     * @since 1.8
118     */
119    public MapConfiguration(final Properties props) {
120        map = toMap(Objects.requireNonNull(props));
121    }
122
123    @Override
124    protected void addPropertyDirect(final String key, final Object value) {
125        final Object previousValue = getProperty(key);
126
127        if (previousValue == null) {
128            map.put(key, value);
129        } else if (previousValue instanceof List) {
130            // the value is added to the existing list
131            // Note: This is problematic. See header comment!
132            ((List<Object>) previousValue).add(value);
133        } else {
134            // the previous value is replaced by a list containing the previous value and the new value
135            final List<Object> list = new ArrayList<>();
136            list.add(previousValue);
137            list.add(value);
138
139            map.put(key, list);
140        }
141    }
142
143    @Override
144    protected void clearPropertyDirect(final String key) {
145        map.remove(key);
146    }
147
148    /**
149     * Returns a copy of this object. The returned configuration will contain the same properties as the original. Event listeners are not cloned.
150     *
151     * @return the copy.
152     * @since 1.3
153     */
154    @Override
155    public Object clone() {
156        try {
157            final MapConfiguration copy = (MapConfiguration) super.clone();
158            copy.map = ConfigurationUtils.clone(map);
159            copy.cloneInterpolator(this);
160            return copy;
161        } catch (final CloneNotSupportedException cex) {
162            // cannot happen
163            throw new ConfigurationRuntimeException(cex);
164        }
165    }
166
167    @Override
168    protected boolean containsKeyInternal(final String key) {
169        return map.containsKey(key);
170    }
171
172    /**
173     * Tests whether this configuration contains one or more matches to this value. This operation stops at first match
174     * but may be more expensive than the containsKey method.
175     *
176     * @since 2.11.0
177     */
178    @Override
179    protected boolean containsValueInternal(final Object value) {
180        return value != null && map.containsValue(value);
181    }
182
183    @Override
184    protected Iterator<String> getKeysInternal() {
185        return map.keySet().iterator();
186    }
187
188    /**
189     * Gets the Map decorated by this configuration.
190     *
191     * @return the map this configuration is based onto.
192     */
193    public Map<String, Object> getMap() {
194        return map;
195    }
196
197    @Override
198    protected Object getPropertyInternal(final String key) {
199        final Object value = map.get(key);
200        if (value instanceof String) {
201            final Collection<String> list = getListDelimiterHandler().split((String) value, !isTrimmingDisabled());
202            return list.size() > 1 ? list : list.iterator().next();
203        }
204        return value;
205    }
206
207    @Override
208    protected boolean isEmptyInternal() {
209        return map.isEmpty();
210    }
211
212    /**
213     * Tests whether the flag whether trimming of property values is disabled.
214     *
215     * @return <strong>true</strong> if trimming of property values is disabled; <strong>false</strong> otherwise.
216     * @since 1.7
217     */
218    public boolean isTrimmingDisabled() {
219        return trimmingDisabled;
220    }
221
222    /**
223     * Sets a flag whether trimming of property values is disabled. This flag is only evaluated if list splitting is
224     * enabled. Refer to the header comment for more information about list splitting and trimming.
225     *
226     * @param trimmingDisabled a flag whether trimming of property values should be disabled.
227     * @since 1.7
228     */
229    public void setTrimmingDisabled(final boolean trimmingDisabled) {
230        this.trimmingDisabled = trimmingDisabled;
231    }
232
233    @Override
234    protected int sizeInternal() {
235        return map.size();
236    }
237
238    /**
239     * Converts this object to a String suitable for debugging and logging.
240     *
241     * @since 2.3
242     */
243    @Override
244    public String toString() {
245        return getClass().getSimpleName() + " [map=" + map + ", trimmingDisabled=" + trimmingDisabled + "]";
246    }
247}