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 */
017package org.apache.commons.collections4.properties;
018
019import java.util.AbstractMap.SimpleEntry;
020import java.util.Collections;
021import java.util.Enumeration;
022import java.util.Iterator;
023import java.util.LinkedHashSet;
024import java.util.Map;
025import java.util.Objects;
026import java.util.Properties;
027import java.util.Set;
028import java.util.function.BiConsumer;
029import java.util.function.BiFunction;
030import java.util.function.Function;
031import java.util.stream.Collectors;
032
033/**
034 * A drop-in replacement for {@link Properties} for ordered keys.
035 * <p>
036 * Overrides methods to keep keys in insertion order. Allows other methods in the superclass to work with ordered keys.
037 * </p>
038 *
039 * @see OrderedPropertiesFactory#INSTANCE
040 * @since 4.5.0-M1
041 */
042public class OrderedProperties extends Properties {
043
044    private static final long serialVersionUID = 1L;
045
046    /**
047     * Preserves the insertion order.
048     */
049    private final LinkedHashSet<Object> orderedKeys = new LinkedHashSet<>();
050
051    /**
052     * Constructs a new instance.
053     */
054    public OrderedProperties() {
055        // empty
056    }
057
058    @Override
059    public synchronized void clear() {
060        orderedKeys.clear();
061        super.clear();
062    }
063
064    @Override
065    public synchronized Object compute(final Object key, final BiFunction<? super Object, ? super Object, ? extends Object> remappingFunction) {
066        final Object compute = super.compute(key, remappingFunction);
067        if (compute != null) {
068            orderedKeys.add(key);
069        }
070        return compute;
071    }
072
073    @Override
074    public synchronized Object computeIfAbsent(final Object key, final Function<? super Object, ? extends Object> mappingFunction) {
075        final Object computeIfAbsent = super.computeIfAbsent(key, mappingFunction);
076        if (computeIfAbsent != null) {
077            orderedKeys.add(key);
078        }
079        return computeIfAbsent;
080    }
081
082    @Override
083    public Set<Map.Entry<Object, Object>> entrySet() {
084        return orderedKeys.stream().map(k -> new SimpleEntry<>(k, get(k))).collect(Collectors.toCollection(LinkedHashSet::new));
085    }
086
087    @Override
088    public synchronized void forEach(final BiConsumer<? super Object, ? super Object> action) {
089        Objects.requireNonNull(action);
090        orderedKeys.forEach(k -> action.accept(k, get(k)));
091    }
092
093    @Override
094    public synchronized Enumeration<Object> keys() {
095        return Collections.enumeration(orderedKeys);
096    }
097
098    @Override
099    public Set<Object> keySet() {
100        return orderedKeys;
101    }
102
103    @Override
104    public synchronized Object merge(final Object key, final Object value,
105            final BiFunction<? super Object, ? super Object, ? extends Object> remappingFunction) {
106        orderedKeys.add(key);
107        return super.merge(key, value, remappingFunction);
108    }
109
110    @Override
111    public Enumeration<?> propertyNames() {
112        return Collections.enumeration(orderedKeys);
113    }
114
115    @Override
116    public synchronized Object put(final Object key, final Object value) {
117        final Object put = super.put(key, value);
118        if (put == null) {
119            orderedKeys.add(key);
120        }
121        return put;
122    }
123
124    @Override
125    public synchronized void putAll(final Map<? extends Object, ? extends Object> t) {
126        orderedKeys.addAll(t.keySet());
127        super.putAll(t);
128    }
129
130    @Override
131    public synchronized Object putIfAbsent(final Object key, final Object value) {
132        final Object putIfAbsent = super.putIfAbsent(key, value);
133        if (putIfAbsent == null) {
134            orderedKeys.add(key);
135        }
136        return putIfAbsent;
137    }
138
139    @Override
140    public synchronized Object remove(final Object key) {
141        final Object remove = super.remove(key);
142        if (remove != null) {
143            orderedKeys.remove(key);
144        }
145        return remove;
146    }
147
148    @Override
149    public synchronized boolean remove(final Object key, final Object value) {
150        final boolean remove = super.remove(key, value);
151        if (remove) {
152            orderedKeys.remove(key);
153        }
154        return remove;
155    }
156
157    @Override
158    public synchronized String toString() {
159        // Must override for Java 17 to maintain order since the implementation is based on a map
160        final int max = size() - 1;
161        if (max == -1) {
162            return "{}";
163        }
164        final StringBuilder sb = new StringBuilder();
165        final Iterator<Map.Entry<Object, Object>> it = entrySet().iterator();
166        sb.append('{');
167        for (int i = 0;; i++) {
168            final Map.Entry<Object, Object> e = it.next();
169            final Object key = e.getKey();
170            final Object value = e.getValue();
171            sb.append(key == this ? "(this Map)" : key.toString());
172            sb.append('=');
173            sb.append(value == this ? "(this Map)" : value.toString());
174            if (i == max) {
175                return sb.append('}').toString();
176            }
177            sb.append(", ");
178        }
179    }
180}