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 */
017package org.apache.commons.collections4.properties;
018
019import java.util.AbstractCollection;
020import java.util.AbstractMap.SimpleEntry;
021import java.util.AbstractSet;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Enumeration;
025import java.util.Iterator;
026import java.util.LinkedHashSet;
027import java.util.Map;
028import java.util.Objects;
029import java.util.Properties;
030import java.util.Set;
031import java.util.function.BiConsumer;
032import java.util.function.BiFunction;
033import java.util.function.Function;
034import java.util.stream.Collectors;
035
036/**
037 * A drop-in replacement for {@link Properties} for ordered keys.
038 * <p>
039 * Overrides methods to keep keys in insertion order. Allows other methods in the superclass to work with ordered keys.
040 * </p>
041 *
042 * @see OrderedPropertiesFactory#INSTANCE
043 * @since 4.5.0-M1
044 */
045public class OrderedProperties extends Properties {
046
047    /**
048     * A key set view in insertion order.
049     */
050    private final class KeySet extends AbstractSet<Object> {
051
052        @Override
053        public void clear() {
054            OrderedProperties.this.clear();
055        }
056
057        @Override
058        public boolean contains(final Object key) {
059            return containsKey(key);
060        }
061
062        @Override
063        public Iterator<Object> iterator() {
064            return orderedKeysIterator();
065        }
066
067        @Override
068        public boolean remove(final Object key) {
069            return OrderedProperties.this.remove(key) != null;
070        }
071
072        @Override
073        public int size() {
074            return OrderedProperties.this.size();
075        }
076    }
077
078    /**
079     * A values view in key insertion order.
080     */
081    private final class Values extends AbstractCollection<Object> {
082
083        @Override
084        public void clear() {
085            OrderedProperties.this.clear();
086        }
087
088        @Override
089        public boolean contains(final Object value) {
090            return containsValue(value);
091        }
092
093        @Override
094        public Iterator<Object> iterator() {
095            final Iterator<Object> keys = orderedKeysIterator();
096            return new Iterator<Object>() {
097
098                @Override
099                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}