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
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    @Override
052    public synchronized void clear() {
053        orderedKeys.clear();
054        super.clear();
055    }
056
057    @Override
058    public synchronized Object compute(final Object key, final BiFunction<? super Object, ? super Object, ? extends Object> remappingFunction) {
059        final Object compute = super.compute(key, remappingFunction);
060        if (compute != null) {
061            orderedKeys.add(key);
062        }
063        return compute;
064    }
065
066    @Override
067    public synchronized Object computeIfAbsent(final Object key, final Function<? super Object, ? extends Object> mappingFunction) {
068        final Object computeIfAbsent = super.computeIfAbsent(key, mappingFunction);
069        if (computeIfAbsent != null) {
070            orderedKeys.add(key);
071        }
072        return computeIfAbsent;
073    }
074
075    @Override
076    public Set<Map.Entry<Object, Object>> entrySet() {
077        return orderedKeys.stream().map(k -> new SimpleEntry<>(k, get(k))).collect(Collectors.toCollection(LinkedHashSet::new));
078    }
079
080    @Override
081    public synchronized void forEach(final BiConsumer<? super Object, ? super Object> action) {
082        Objects.requireNonNull(action);
083        orderedKeys.forEach(k -> action.accept(k, get(k)));
084    }
085
086    @Override
087    public synchronized Enumeration<Object> keys() {
088        return Collections.enumeration(orderedKeys);
089    }
090
091    @Override
092    public Set<Object> keySet() {
093        return orderedKeys;
094    }
095
096    @Override
097    public synchronized Object merge(final Object key, final Object value,
098            final BiFunction<? super Object, ? super Object, ? extends Object> remappingFunction) {
099        orderedKeys.add(key);
100        return super.merge(key, value, remappingFunction);
101    }
102
103    @Override
104    public Enumeration<?> propertyNames() {
105        return Collections.enumeration(orderedKeys);
106    }
107
108    @Override
109    public synchronized Object put(final Object key, final Object value) {
110        final Object put = super.put(key, value);
111        if (put == null) {
112            orderedKeys.add(key);
113        }
114        return put;
115    }
116
117    @Override
118    public synchronized void putAll(final Map<? extends Object, ? extends Object> t) {
119        orderedKeys.addAll(t.keySet());
120        super.putAll(t);
121    }
122
123    @Override
124    public synchronized Object putIfAbsent(final Object key, final Object value) {
125        final Object putIfAbsent = super.putIfAbsent(key, value);
126        if (putIfAbsent == null) {
127            orderedKeys.add(key);
128        }
129        return putIfAbsent;
130    }
131
132    @Override
133    public synchronized Object remove(final Object key) {
134        final Object remove = super.remove(key);
135        if (remove != null) {
136            orderedKeys.remove(key);
137        }
138        return remove;
139    }
140
141    @Override
142    public synchronized boolean remove(final Object key, final Object value) {
143        final boolean remove = super.remove(key, value);
144        if (remove) {
145            orderedKeys.remove(key);
146        }
147        return remove;
148    }
149
150    @Override
151    public synchronized String toString() {
152        // Must override for Java 17 to maintain order since the implementation is based on a map
153        final int max = size() - 1;
154        if (max == -1) {
155            return "{}";
156        }
157        final StringBuilder sb = new StringBuilder();
158        final Iterator<Map.Entry<Object, Object>> it = entrySet().iterator();
159        sb.append('{');
160        for (int i = 0;; i++) {
161            final Map.Entry<Object, Object> e = it.next();
162            final Object key = e.getKey();
163            final Object value = e.getValue();
164            sb.append(key == this ? "(this Map)" : key.toString());
165            sb.append('=');
166            sb.append(value == this ? "(this Map)" : value.toString());
167            if (i == max) {
168                return sb.append('}').toString();
169            }
170            sb.append(", ");
171        }
172    }
173}