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.configuration;
019
020import java.util.AbstractMap;
021import java.util.ArrayList;
022import java.util.HashSet;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Map;
026import java.util.Properties;
027import java.util.Set;
028
029/**
030 * <p>
031 * A Map based Configuration.
032 * </p>
033 * <p>
034 * This implementation of the {@code Configuration} interface is
035 * initialized with a {@code java.util.Map}. The methods of the
036 * {@code Configuration} interface are implemented on top of the content of
037 * this map. The following storage scheme is used:
038 * </p>
039 * <p>
040 * Property keys are directly mapped to map keys, i.e. the
041 * {@code getProperty()} method directly performs a {@code get()} on
042 * the map. Analogously, {@code setProperty()} or
043 * {@code addProperty()} operations write new data into the map. If a value
044 * is added to an existing property, a {@code java.util.List} is created,
045 * which stores the values of this property.
046 * </p>
047 * <p>
048 * An important use case of this class is to treat a map as a
049 * {@code Configuration} allowing access to its data through the richer
050 * interface. This can be a bit problematic in some cases because the map may
051 * contain values that need not adhere to the default storage scheme used by
052 * typical configuration implementations, e.g. regarding lists. In such cases
053 * care must be taken when manipulating the data through the
054 * {@code Configuration} interface, e.g. by calling
055 * {@code addProperty()}; results may be different than expected.
056 * </p>
057 * <p>
058 * An important point is the handling of list delimiters: If delimiter parsing
059 * is enabled (which it is per default), {@code getProperty()} checks
060 * whether the value of a property is a string and whether it contains the list
061 * delimiter character. If this is the case, the value is split at the delimiter
062 * resulting in a list. This split operation typically also involves trimming
063 * the single values as the list delimiter character may be surrounded by
064 * whitespace. Trimming can be disabled with the
065 * {@link #setTrimmingDisabled(boolean)} method. The whole list splitting
066 * behavior can be disabled using the
067 * {@link #setDelimiterParsingDisabled(boolean)} method.
068 * </p>
069 * <p>
070 * Notice that list splitting is only performed for single string values. If a
071 * property has multiple values, the single values are not split even if they
072 * contain the list delimiter character.
073 * </p>
074 * <p>
075 * As the underlying {@code Map} is directly used as store of the property
076 * values, the thread-safety of this {@code Configuration} implementation
077 * depends on the map passed to the constructor.
078 * </p>
079 * <p>
080 * Notes about type safety: For properties with multiple values this implementation
081 * creates lists of type {@code Object} and stores them. If a property is assigned
082 * another value, the value is added to the list. This can cause problems if the
083 * map passed to the constructor already contains lists of other types. This
084 * should be avoided, otherwise it cannot be guaranteed that the application
085 * might throw {@code ClassCastException} exceptions later.
086 * </p>
087 *
088 * @author Emmanuel Bourg
089 * @version $Id: MapConfiguration.java 1534429 2013-10-22 00:45:36Z henning $
090 * @since 1.1
091 */
092public class MapConfiguration extends AbstractConfiguration implements Cloneable
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
102     * used to store the configuration properties, any change will also affect
103     * the Map.
104     *
105     * @param map the map
106     */
107    public MapConfiguration(Map<String, ?> map)
108    {
109        this.map = (Map<String, Object>) map;
110    }
111
112    /**
113     * Creates a new instance of {@code MapConfiguration} and initializes its
114     * content from the specified {@code Properties} object. The resulting
115     * configuration is not connected to the {@code Properties} object, but all
116     * keys which are strings are copied (keys of other types are ignored).
117     *
118     * @param props the {@code Properties} object defining the content of this
119     *        configuration
120     * @throws NullPointerException if the {@code Properties} object is
121     *         <b>null</b>
122     * @since 1.8
123     */
124    public MapConfiguration(Properties props)
125    {
126        map = convertPropertiesToMap(props);
127    }
128
129    /**
130     * Return the Map decorated by this configuration.
131     *
132     * @return the map this configuration is based onto
133     */
134    public Map<String, Object> getMap()
135    {
136        return map;
137    }
138
139    /**
140     * Returns the flag whether trimming of property values is disabled.
141     *
142     * @return <b>true</b> if trimming of property values is disabled;
143     *         <b>false</b> otherwise
144     * @since 1.7
145     */
146    public boolean isTrimmingDisabled()
147    {
148        return trimmingDisabled;
149    }
150
151    /**
152     * Sets a flag whether trimming of property values is disabled. This flag is
153     * only evaluated if list splitting is enabled. Refer to the header comment
154     * for more information about list splitting and trimming.
155     *
156     * @param trimmingDisabled a flag whether trimming of property values should
157     *        be disabled
158     * @since 1.7
159     */
160    public void setTrimmingDisabled(boolean trimmingDisabled)
161    {
162        this.trimmingDisabled = trimmingDisabled;
163    }
164
165    public Object getProperty(String key)
166    {
167        Object value = map.get(key);
168        if ((value instanceof String) && (!isDelimiterParsingDisabled()))
169        {
170            List<String> list = PropertyConverter.split((String) value, getListDelimiter(), !isTrimmingDisabled());
171            return list.size() > 1 ? list : list.get(0);
172        }
173        else
174        {
175            return value;
176        }
177    }
178
179    @Override
180    protected void addPropertyDirect(String key, Object value)
181    {
182        Object previousValue = getProperty(key);
183
184        if (previousValue == null)
185        {
186            map.put(key, value);
187        }
188        else if (previousValue instanceof List)
189        {
190            // the value is added to the existing list
191            // Note: This is problematic. See header comment!
192            ((List<Object>) previousValue).add(value);
193        }
194        else
195        {
196            // the previous value is replaced by a list containing the previous value and the new value
197            List<Object> list = new ArrayList<Object>();
198            list.add(previousValue);
199            list.add(value);
200
201            map.put(key, list);
202        }
203    }
204
205    public boolean isEmpty()
206    {
207        return map.isEmpty();
208    }
209
210    public boolean containsKey(String key)
211    {
212        return map.containsKey(key);
213    }
214
215    @Override
216    protected void clearPropertyDirect(String key)
217    {
218        map.remove(key);
219    }
220
221    public Iterator<String> getKeys()
222    {
223        return map.keySet().iterator();
224    }
225
226    /**
227     * Returns a copy of this object. The returned configuration will contain
228     * the same properties as the original. Event listeners are not cloned.
229     *
230     * @return the copy
231     * @since 1.3
232     */
233    @Override
234    public Object clone()
235    {
236        try
237        {
238            MapConfiguration copy = (MapConfiguration) super.clone();
239            copy.clearConfigurationListeners();
240            // Safe because ConfigurationUtils returns a map of the same types.
241            @SuppressWarnings("unchecked")
242            Map<String, Object> clonedMap = (Map<String, Object>) ConfigurationUtils.clone(map);
243            copy.map = clonedMap;
244            return copy;
245        }
246        catch (CloneNotSupportedException cex)
247        {
248            // cannot happen
249            throw new ConfigurationRuntimeException(cex);
250        }
251    }
252
253    /**
254     * Helper method for copying all string keys from the given
255     * {@code Properties} object to a newly created map.
256     *
257     * @param props the {@code Properties} to be copied
258     * @return a newly created map with all string keys of the properties
259     */
260    private static Map<String, Object> convertPropertiesToMap(final Properties props)
261    {
262        return new AbstractMap<String, Object>() {
263
264            @Override
265            public Set<Map.Entry<String, Object>> entrySet()
266            {
267                Set<Map.Entry<String, Object>> entries = new HashSet<Map.Entry<String, Object>>();
268                for (final Map.Entry<Object, Object> propertyEntry : props.entrySet())
269                {
270                    if (propertyEntry.getKey() instanceof String)
271                    {
272                        entries.add(new Map.Entry<String, Object>() {
273
274                            public String getKey()
275                            {
276                                return propertyEntry.getKey().toString();
277                            }
278
279                            public Object getValue()
280                            {
281                                return propertyEntry.getValue();
282                            }
283
284                            public Object setValue(Object value)
285                            {
286                                throw new UnsupportedOperationException();
287                            }
288                        });
289                    }
290                }
291                return entries;
292            }
293        };
294    }
295}