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 */
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, e.g. regarding lists. In such
049 * cases care must be taken when manipulating the data through the {@code Configuration} interface, e.g. 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, e.g. 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     * Helper method for converting the type of the {@code Properties} object to a supported map type. As stated by the
080     * comment of the constructor, we expect the {@code Properties} object to contain only String key; therefore, it is safe
081     * to do this cast.
082     *
083     * @param props the {@code Properties} to be copied
084     * @return a newly created map with all string keys of the properties
085     */
086    @SuppressWarnings("unchecked")
087    private static Map<String, Object> toMap(final Properties props) {
088        @SuppressWarnings("rawtypes")
089        final Map map = props;
090        return map;
091    }
092
093    /** The Map decorated by this configuration. */
094    protected Map<String, Object> map;
095
096    /** A flag whether trimming of property values should be disabled. */
097    private boolean trimmingDisabled;
098
099    /**
100     * Create a Configuration decorator around the specified Map. The map is used to store the configuration properties, any
101     * change will also affect the Map.
102     *
103     * @param map the map
104     */
105    public MapConfiguration(final Map<String, ?> map) {
106        this.map = (Map<String, Object>) Objects.requireNonNull(map, "map");
107    }
108
109    /**
110     * Creates a new instance of {@code MapConfiguration} which uses the specified {@code Properties} object as its data
111     * store. All changes of this configuration affect the given {@code Properties} object and vice versa. Note that while
112     * {@code Properties} actually implements {@code Map<Object, Object>}, we expect it to contain only string keys. Other
113     * key types will lead to {@code ClassCastException} exceptions on certain methods.
114     *
115     * @param props the {@code Properties} object defining the content of this configuration
116     * @since 1.8
117     */
118    public MapConfiguration(final Properties props) {
119        map = toMap(Objects.requireNonNull(props));
120    }
121
122    @Override
123    protected void addPropertyDirect(final String key, final Object value) {
124        final Object previousValue = getProperty(key);
125
126        if (previousValue == null) {
127            map.put(key, value);
128        } else if (previousValue instanceof List) {
129            // the value is added to the existing list
130            // Note: This is problematic. See header comment!
131            ((List<Object>) previousValue).add(value);
132        } else {
133            // the previous value is replaced by a list containing the previous value and the new value
134            final List<Object> list = new ArrayList<>();
135            list.add(previousValue);
136            list.add(value);
137
138            map.put(key, list);
139        }
140    }
141
142    @Override
143    protected void clearPropertyDirect(final String key) {
144        map.remove(key);
145    }
146
147    /**
148     * Returns a copy of this object. The returned configuration will contain the same properties as the original. Event
149     * 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            // Safe because ConfigurationUtils returns a map of the same types.
159            @SuppressWarnings("unchecked")
160            final Map<String, Object> clonedMap = (Map<String, Object>) ConfigurationUtils.clone(map);
161            copy.map = clonedMap;
162            copy.cloneInterpolator(this);
163            return copy;
164        } catch (final CloneNotSupportedException cex) {
165            // cannot happen
166            throw new ConfigurationRuntimeException(cex);
167        }
168    }
169
170    @Override
171    protected boolean containsKeyInternal(final String key) {
172        return map.containsKey(key);
173    }
174
175    /**
176     * Tests whether this configuration contains one or more matches to this value. This operation stops at first match
177     * but may be more expensive than the containsKey method.
178     * @since 2.11.0
179     */
180    @Override
181    protected boolean containsValueInternal(final Object value) {
182        return value != null && map.containsValue(value);
183    }
184
185    @Override
186    protected Iterator<String> getKeysInternal() {
187        return map.keySet().iterator();
188    }
189
190    /**
191     * Gets the Map decorated by this configuration.
192     *
193     * @return the map this configuration is based onto
194     */
195    public Map<String, Object> getMap() {
196        return map;
197    }
198
199    @Override
200    protected Object getPropertyInternal(final String key) {
201        final Object value = map.get(key);
202        if (value instanceof String) {
203            final Collection<String> list = getListDelimiterHandler().split((String) value, !isTrimmingDisabled());
204            return list.size() > 1 ? list : list.iterator().next();
205        }
206        return value;
207    }
208
209    @Override
210    protected boolean isEmptyInternal() {
211        return map.isEmpty();
212    }
213
214    /**
215     * Returns the flag whether trimming of property values is disabled.
216     *
217     * @return <b>true</b> if trimming of property values is disabled; <b>false</b> otherwise
218     * @since 1.7
219     */
220    public boolean isTrimmingDisabled() {
221        return trimmingDisabled;
222    }
223
224    /**
225     * Sets a flag whether trimming of property values is disabled. This flag is only evaluated if list splitting is
226     * enabled. Refer to the header comment for more information about list splitting and trimming.
227     *
228     * @param trimmingDisabled a flag whether trimming of property values should be disabled
229     * @since 1.7
230     */
231    public void setTrimmingDisabled(final boolean trimmingDisabled) {
232        this.trimmingDisabled = trimmingDisabled;
233    }
234
235    @Override
236    protected int sizeInternal() {
237        return map.size();
238    }
239
240    /**
241     * Converts this object to a String suitable for debugging and logging.
242     *
243     * @since 2.3
244     */
245    @Override
246    public String toString() {
247        return getClass().getSimpleName() + " [map=" + map + ", trimmingDisabled=" + trimmingDisabled + "]";
248    }
249}