View Javadoc
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  package org.apache.commons.collections4.properties;
18  
19  import java.util.AbstractCollection;
20  import java.util.AbstractMap.SimpleEntry;
21  import java.util.AbstractSet;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.Enumeration;
25  import java.util.Iterator;
26  import java.util.LinkedHashSet;
27  import java.util.Map;
28  import java.util.Objects;
29  import java.util.Properties;
30  import java.util.Set;
31  import java.util.function.BiConsumer;
32  import java.util.function.BiFunction;
33  import java.util.function.Function;
34  import java.util.stream.Collectors;
35  
36  /**
37   * A drop-in replacement for {@link Properties} for ordered keys.
38   * <p>
39   * Overrides methods to keep keys in insertion order. Allows other methods in the superclass to work with ordered keys.
40   * </p>
41   *
42   * @see OrderedPropertiesFactory#INSTANCE
43   * @since 4.5.0-M1
44   */
45  public class OrderedProperties extends Properties {
46  
47      /**
48       * A key set view in insertion order.
49       */
50      private final class KeySet extends AbstractSet<Object> {
51  
52          @Override
53          public void clear() {
54              OrderedProperties.this.clear();
55          }
56  
57          @Override
58          public boolean contains(final Object key) {
59              return containsKey(key);
60          }
61  
62          @Override
63          public Iterator<Object> iterator() {
64              return orderedKeysIterator();
65          }
66  
67          @Override
68          public boolean remove(final Object key) {
69              return OrderedProperties.this.remove(key) != null;
70          }
71  
72          @Override
73          public int size() {
74              return OrderedProperties.this.size();
75          }
76      }
77  
78      /**
79       * A values view in key insertion order.
80       */
81      private final class Values extends AbstractCollection<Object> {
82  
83          @Override
84          public void clear() {
85              OrderedProperties.this.clear();
86          }
87  
88          @Override
89          public boolean contains(final Object value) {
90              return containsValue(value);
91          }
92  
93          @Override
94          public Iterator<Object> iterator() {
95              final Iterator<Object> keys = orderedKeysIterator();
96              return new Iterator<Object>() {
97  
98                  @Override
99                  public boolean hasNext() {
100                     return keys.hasNext();
101                 }
102 
103                 @Override
104                 public Object next() {
105                     return get(keys.next());
106                 }
107 
108                 @Override
109                 public void remove() {
110                     keys.remove();
111                 }
112             };
113         }
114 
115         @Override
116         public int size() {
117             return OrderedProperties.this.size();
118         }
119     }
120 
121     private static final long serialVersionUID = 1L;
122 
123     /**
124      * Preserves the insertion order.
125      */
126     private final LinkedHashSet<Object> orderedKeys = new LinkedHashSet<>();
127 
128     /**
129      * Constructs a new instance.
130      */
131     public OrderedProperties() {
132         // empty
133     }
134 
135     @Override
136     public synchronized void clear() {
137         orderedKeys.clear();
138         super.clear();
139     }
140 
141     @Override
142     public synchronized Object compute(final Object key, final BiFunction<? super Object, ? super Object, ? extends Object> remappingFunction) {
143         final Object compute = super.compute(key, remappingFunction);
144         if (compute != null) {
145             orderedKeys.add(key);
146         } else {
147             orderedKeys.remove(key);
148         }
149         return compute;
150     }
151 
152     @Override
153     public synchronized Object computeIfAbsent(final Object key, final Function<? super Object, ? extends Object> mappingFunction) {
154         final Object computeIfAbsent = super.computeIfAbsent(key, mappingFunction);
155         if (computeIfAbsent != null) {
156             orderedKeys.add(key);
157         }
158         return computeIfAbsent;
159     }
160 
161     @Override
162     public Set<Map.Entry<Object, Object>> entrySet() {
163         return orderedKeys.stream().map(k -> new SimpleEntry<>(k, get(k))).collect(Collectors.toCollection(LinkedHashSet::new));
164     }
165 
166     /**
167      * Enumerates all key/value pairs in the specified LinkedHashSet and omits the property if the key or value is not a string.
168      *
169      * @param result The result set to populate.
170      * @return The given set.
171      */
172     private synchronized LinkedHashSet<String> enumerateStringProperties(final LinkedHashSet<String> result) {
173         if (defaults != null) {
174             result.addAll(defaults.stringPropertyNames());
175         }
176         for (final Enumeration<?> e = keys(); e.hasMoreElements();) {
177             final Object k = e.nextElement();
178             final Object v = get(k);
179             if (k instanceof String && v instanceof String) {
180                 result.add((String) k);
181             }
182         }
183         return result;
184     }
185 
186     @Override
187     public synchronized void forEach(final BiConsumer<? super Object, ? super Object> action) {
188         Objects.requireNonNull(action, "action");
189         orderedKeys.forEach(k -> action.accept(k, get(k)));
190     }
191 
192     @Override
193     public synchronized Enumeration<Object> keys() {
194         return Collections.enumeration(orderedKeys);
195     }
196 
197     @Override
198     public Set<Object> keySet() {
199         return new KeySet();
200     }
201 
202     @Override
203     public synchronized Object merge(final Object key, final Object value,
204             final BiFunction<? super Object, ? super Object, ? extends Object> remappingFunction) {
205         final Object merge = super.merge(key, value, remappingFunction);
206         if (merge != null) {
207             orderedKeys.add(key);
208         } else {
209             orderedKeys.remove(key);
210         }
211         return merge;
212     }
213 
214     /**
215      * Creates an iterator over the keys in insertion order whose {@link Iterator#remove()} also removes the mapping.
216      *
217      * @return A new iterator.
218      */
219     private Iterator<Object> orderedKeysIterator() {
220         final Iterator<Object> iterator = orderedKeys.iterator();
221         return new Iterator<Object>() {
222 
223             private Object last;
224 
225             @Override
226             public boolean hasNext() {
227                 return iterator.hasNext();
228             }
229 
230             @Override
231             public Object next() {
232                 last = iterator.next();
233                 return last;
234             }
235 
236             @Override
237             public void remove() {
238                 // All orderedKeys writes happen under the OrderedProperties monitor.
239                 synchronized (OrderedProperties.this) {
240                     // Not remove(Object), which would edit orderedKeys while this iterator walks it.
241                     iterator.remove();
242                     OrderedProperties.super.remove(last);
243                 }
244             }
245         };
246     }
247 
248     @Override
249     public Enumeration<?> propertyNames() {
250         return Collections.enumeration(stringPropertyNames());
251     }
252 
253     @Override
254     public synchronized Object put(final Object key, final Object value) {
255         final Object put = super.put(key, value);
256         if (put == null) {
257             orderedKeys.add(key);
258         }
259         return put;
260     }
261 
262     @Override
263     public synchronized void putAll(final Map<? extends Object, ? extends Object> t) {
264         orderedKeys.addAll(t.keySet());
265         super.putAll(t);
266     }
267 
268     @Override
269     public synchronized Object putIfAbsent(final Object key, final Object value) {
270         final Object putIfAbsent = super.putIfAbsent(key, value);
271         if (putIfAbsent == null) {
272             orderedKeys.add(key);
273         }
274         return putIfAbsent;
275     }
276 
277     @Override
278     public synchronized Object remove(final Object key) {
279         final Object remove = super.remove(key);
280         if (remove != null) {
281             orderedKeys.remove(key);
282         }
283         return remove;
284     }
285 
286     @Override
287     public synchronized boolean remove(final Object key, final Object value) {
288         final boolean remove = super.remove(key, value);
289         if (remove) {
290             orderedKeys.remove(key);
291         }
292         return remove;
293     }
294 
295     @Override
296     public Set<String> stringPropertyNames() {
297         return enumerateStringProperties(new LinkedHashSet<>());
298     }
299 
300     @Override
301     public synchronized String toString() {
302         // Must override for Java 17 to maintain order since the implementation is based on a map
303         final int max = size() - 1;
304         if (max == -1) {
305             return "{}";
306         }
307         final StringBuilder sb = new StringBuilder();
308         final Iterator<Map.Entry<Object, Object>> it = entrySet().iterator();
309         sb.append('{');
310         for (int i = 0;; i++) {
311             final Map.Entry<Object, Object> e = it.next();
312             final Object key = e.getKey();
313             final Object value = e.getValue();
314             sb.append(key == this ? "(this Map)" : key.toString());
315             sb.append('=');
316             sb.append(value == this ? "(this Map)" : value.toString());
317             if (i == max) {
318                 return sb.append('}').toString();
319             }
320             sb.append(", ");
321         }
322     }
323 
324     @Override
325     public Collection<Object> values() {
326         return new Values();
327     }
328 }