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}