1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.configuration2;
19
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Objects;
26 import java.util.Properties;
27
28 import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
29
30 /**
31 * <p>
32 * A Map based Configuration.
33 * </p>
34 * <p>
35 * This implementation of the {@code Configuration} interface is initialized with a {@link java.util.Map}. The methods
36 * of the {@code Configuration} interface are implemented on top of the content of this map. The following storage
37 * scheme is used:
38 * </p>
39 * <p>
40 * Property keys are directly mapped to map keys, i.e. the {@code getProperty()} method directly performs a
41 * {@code get()} on the map. Analogously, {@code setProperty()} or {@code addProperty()} operations write new data into
42 * the map. If a value is added to an existing property, a {@link java.util.List} is created, which stores the values of
43 * this property.
44 * </p>
45 * <p>
46 * An important use case of this class is to treat a map as a {@code Configuration} allowing access to its data through
47 * the richer interface. This can be a bit problematic in some cases because the map may contain values that need not
48 * adhere to the default storage scheme used by typical configuration implementations, for example regarding lists. In such
49 * cases care must be taken when manipulating the data through the {@code Configuration} interface, for example by calling
50 * {@code addProperty()}; results may be different than expected.
51 * </p>
52 * <p>
53 * The handling of list delimiters is a bit different for this configuration implementation: When a property of type
54 * String is queried, it is passed to the current {@link org.apache.commons.configuration2.convert.ListDelimiterHandler
55 * ListDelimiterHandler} which may generate multiple values. Note that per default a list delimiter handler is set which
56 * does not do any list splitting, so this feature is disabled. It can be enabled by setting a properly configured
57 * {@code ListDelimiterHandler} implementation, for example a
58 * {@link org.apache.commons.configuration2.convert.DefaultListDelimiterHandler DefaultListDelimiterHandler} object.
59 * </p>
60 * <p>
61 * Notice that list splitting is only performed for single string values. If a property has multiple values, the single
62 * values are not split even if they contain the list delimiter character.
63 * </p>
64 * <p>
65 * As the underlying {@code Map} is directly used as store of the property values, the thread-safety of this
66 * {@code Configuration} implementation depends on the map passed to the constructor.
67 * </p>
68 * <p>
69 * Notes about type safety: For properties with multiple values this implementation creates lists of type {@code Object}
70 * and stores them. If a property is assigned another value, the value is added to the list. This can cause problems if
71 * the map passed to the constructor already contains lists of other types. This should be avoided, otherwise it cannot
72 * be guaranteed that the application might throw {@code ClassCastException} exceptions later.
73 * </p>
74 *
75 * @since 1.1
76 */
77 public class MapConfiguration extends AbstractConfiguration implements Cloneable {
78
79 /**
80 * Helper method for converting the type of the {@code Properties} object to a supported map type. As stated by the
81 * comment of the constructor, we expect the {@code Properties} object to contain only String key; therefore, it is safe
82 * to do this cast.
83 *
84 * @param props the {@code Properties} to be copied.
85 * @return a newly created map with all string keys of the properties.
86 */
87 @SuppressWarnings("unchecked")
88 private static Map<String, Object> toMap(final Properties props) {
89 @SuppressWarnings("rawtypes")
90 final Map map = props;
91 return map;
92 }
93
94 /** The Map decorated by this configuration. */
95 protected Map<String, Object> map;
96
97 /** A flag whether trimming of property values should be disabled. */
98 private boolean trimmingDisabled;
99
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 }