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.juneau.commons.collections; 018 019import static java.util.Collections.*; 020import static java.util.stream.Collectors.*; 021import static org.apache.juneau.commons.utils.AssertionUtils.*; 022import static org.apache.juneau.commons.utils.Utils.*; 023 024import java.util.*; 025 026/** 027 * A bidirectional map with reverse key lookup by value. 028 * 029 * <p> 030 * This implementation provides efficient bidirectional lookups by maintaining two internal maps: 031 * a forward map for key-to-value lookups and a reverse map for value-to-key lookups. 032 * 033 * <h5 class='section'>Features:</h5> 034 * <ul> 035 * <li>Implements the standard {@link Map} interface for forward key→value lookups 036 * <li>Provides {@link #getKey(Object)} method for reverse value→key lookups 037 * <li>Maintains insertion order using {@link LinkedHashMap} internally 038 * <li>Automatically filters out null keys and values 039 * <li>Supports both mutable and unmodifiable instances via the builder 040 * <li>Thread-safety: Not thread-safe by default; external synchronization required if accessed by multiple threads 041 * </ul> 042 * 043 * <h5 class='section'>Usage:</h5> 044 * <p class='bjava'> 045 * <jc>// Create a bidirectional map</jc> 046 * BidiMap<String,Integer> <jv>map</jv> = BidiMap.<jsm>create</jsm>() 047 * .add(<js>"one"</js>, 1) 048 * .add(<js>"two"</js>, 2) 049 * .add(<js>"three"</js>, 3) 050 * .build(); 051 * 052 * <jc>// Forward lookup: key → value</jc> 053 * Integer <jv>value</jv> = <jv>map</jv>.get(<js>"two"</js>); <jc>// Returns 2</jc> 054 * 055 * <jc>// Reverse lookup: value → key</jc> 056 * String <jv>key</jv> = <jv>map</jv>.getKey(2); <jc>// Returns "two"</jc> 057 * </p> 058 * 059 * <h5 class='section'>Null Handling:</h5> 060 * <p> 061 * This map automatically filters out entries with null keys or values during construction. 062 * Attempting to add null keys or values via {@link #put(Object, Object)} or {@link #putAll(Map)} 063 * after construction will result in them being stored in the forward map but not the reverse map. 064 * 065 * <h5 class='section'>Unmodifiable Instances:</h5> 066 * <p class='bjava'> 067 * <jc>// Create an unmodifiable bidirectional map</jc> 068 * BidiMap<String,Integer> <jv>map</jv> = BidiMap.<jsm>create</jsm>() 069 * .add(<js>"one"</js>, 1) 070 * .add(<js>"two"</js>, 2) 071 * .unmodifiable() 072 * .build(); 073 * </p> 074 * 075 * <h5 class='section'>See Also:</h5> 076 * <ul> 077 * <li class='link'><a class="doclink" href="../../../../../index.html#juneau-commons">Overview > juneau-commons</a> 078 * </ul> 079 * 080 * @param <K> The key type. 081 * @param <V> The value type. 082 */ 083public class BidiMap<K,V> implements Map<K,V> { 084 085 /** 086 * Builder class for {@link BidiMap}. 087 * 088 * <p> 089 * Provides a fluent API for constructing bidirectional maps. 090 * 091 * <h5 class='section'>Example:</h5> 092 * <p class='bjava'> 093 * BidiMap<String,Integer> <jv>map</jv> = BidiMap.<jsm>create</jsm>() 094 * .add(<js>"one"</js>, 1) 095 * .add(<js>"two"</js>, 2) 096 * .unmodifiable() 097 * .build(); 098 * </p> 099 * 100 * @param <K> The key type. 101 * @param <V> The value type. 102 */ 103 public static class Builder<K,V> { 104 final HashMap<K,V> map = new LinkedHashMap<>(); 105 final Set<V> values = new HashSet<>(); 106 boolean unmodifiable; 107 108 /** 109 * Adds a key-value pair to this map. 110 * 111 * <p> 112 * Null keys and values are allowed in the builder but will be filtered out 113 * during the {@link #build()} operation. 114 * 115 * @param key The key. Can be <jk>null</jk>. 116 * @param value The value. Can be <jk>null</jk>. 117 * @return This object. 118 * @throws IllegalArgumentException if the value already exists mapped to a different key. 119 */ 120 public Builder<K,V> add(K key, V value) { 121 if (nn(key) && nn(value)) { 122 var existingValue = map.get(key); 123 if (nn(existingValue) && ! existingValue.equals(value)) { 124 // Key is being overwritten with a different value, remove old value from tracking 125 values.remove(existingValue); 126 } 127 assertArg(! (values.contains(value) && ! value.equals(existingValue)), "Value ''{0}'' is already mapped to a different key in this BidiMap.", value); 128 values.add(value); 129 } 130 map.put(key, value); 131 return this; 132 } 133 134 /** 135 * Builds a new {@link BidiMap} from the entries added to this builder. 136 * 137 * <p> 138 * Null keys and values are automatically filtered out during construction. 139 * 140 * @return A new {@link BidiMap} instance. 141 */ 142 public BidiMap<K,V> build() { 143 return new BidiMap<>(this); 144 } 145 146 /** 147 * Makes the resulting map unmodifiable. 148 * 149 * <p> 150 * When set, the built map will be wrapped with {@link Collections#unmodifiableMap(Map)}, 151 * preventing any modifications after construction. 152 * 153 * @return This object. 154 */ 155 public Builder<K,V> unmodifiable() { 156 unmodifiable = true; 157 return this; 158 } 159 } 160 161 /** 162 * Creates a new builder for this class. 163 * 164 * @param <K> The key type. 165 * @param <V> The value type. 166 * @return A new {@link Builder} instance. 167 */ 168 public static <K,V> Builder<K,V> create() { 169 return new Builder<>(); 170 } 171 172 private final Map<K,V> forward; 173 private final Map<V,K> reverse; 174 175 /** 176 * Constructor. 177 * 178 * <p> 179 * Constructs a bidirectional map from the provided builder, automatically filtering 180 * out any entries with null keys or values. 181 * 182 * @param builder The builder containing the initial entries. 183 */ 184 public BidiMap(Builder<K,V> builder) { 185 var forward = builder.map.entrySet().stream().filter(x -> nn(x.getKey()) && nn(x.getValue())).collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> b, LinkedHashMap::new)); 186 var reverse = builder.map.entrySet().stream().filter(x -> nn(x.getKey()) && nn(x.getValue())).collect(toMap(Map.Entry::getValue, Map.Entry::getKey, (a, b) -> b, LinkedHashMap::new)); 187 this.forward = builder.unmodifiable ? unmodifiableMap(forward) : forward; 188 this.reverse = builder.unmodifiable ? unmodifiableMap(reverse) : reverse; 189 } 190 191 /** 192 * Removes all key-value mappings from this map. 193 * 194 * <p> 195 * Clears both the forward and reverse maps. 196 * 197 * @throws UnsupportedOperationException if the map is unmodifiable. 198 */ 199 @Override /* Map */ 200 public void clear() { 201 forward.clear(); 202 reverse.clear(); 203 } 204 205 /** 206 * Returns <jk>true</jk> if this map contains a mapping for the specified key. 207 * 208 * @param key The key to check for. 209 * @return <jk>true</jk> if this map contains a mapping for the specified key. 210 */ 211 @Override /* Map */ 212 public boolean containsKey(Object key) { 213 return forward.containsKey(key); 214 } 215 216 /** 217 * Returns <jk>true</jk> if this map maps one or more keys to the specified value. 218 * 219 * <p> 220 * This implementation uses the reverse map for efficient lookup. 221 * 222 * @param value The value to check for. 223 * @return <jk>true</jk> if this map maps one or more keys to the specified value. 224 */ 225 @Override /* Map */ 226 public boolean containsValue(Object value) { 227 return reverse.containsKey(value); 228 } 229 230 /** 231 * Returns a {@link Set} view of the mappings contained in this map. 232 * 233 * @return A set view of the mappings contained in this map. 234 */ 235 @Override /* Map */ 236 public Set<Entry<K,V>> entrySet() { 237 return forward.entrySet(); 238 } 239 240 /** 241 * Returns the value to which the specified key is mapped, or <jk>null</jk> if this map contains no mapping for the key. 242 * 243 * @param key The key whose associated value is to be returned. 244 * @return The value to which the specified key is mapped, or <jk>null</jk> if this map contains no mapping for the key. 245 */ 246 @Override /* Map */ 247 public V get(Object key) { 248 return forward.get(key); 249 } 250 251 /** 252 * Returns the key that is currently mapped to the specified value. 253 * 254 * <p> 255 * This is the reverse lookup operation that makes this a bidirectional map. 256 * 257 * <h5 class='section'>Example:</h5> 258 * <p class='bjava'> 259 * BidiMap<String,Integer> <jv>map</jv> = BidiMap.<jsm>create</jsm>().add(<js>"two"</js>, 2).build(); 260 * String <jv>key</jv> = <jv>map</jv>.getKey(2); <jc>// Returns "two"</jc> 261 * </p> 262 * 263 * @param value The value whose associated key is to be returned. 264 * @return The key to which the specified value is mapped, or <jk>null</jk> if this map contains no mapping for the value. 265 */ 266 public K getKey(V value) { 267 return reverse.get(value); 268 } 269 270 /** 271 * Returns <jk>true</jk> if this map contains no key-value mappings. 272 * 273 * @return <jk>true</jk> if this map contains no key-value mappings. 274 */ 275 @Override /* Map */ 276 public boolean isEmpty() { return forward.isEmpty(); } 277 278 /** 279 * Returns a {@link Set} view of the keys contained in this map. 280 * 281 * @return A set view of the keys contained in this map. 282 */ 283 @Override /* Map */ 284 public Set<K> keySet() { 285 return forward.keySet(); 286 } 287 288 /** 289 * Associates the specified value with the specified key in this map. 290 * 291 * <p> 292 * This operation updates both the forward map (key→value) and the reverse map (value→key). 293 * 294 * @param key The key with which the specified value is to be associated. 295 * @param value The value to be associated with the specified key. 296 * @return The previous value associated with the key, or <jk>null</jk> if there was no mapping for the key. 297 * @throws UnsupportedOperationException if the map is unmodifiable. 298 * @throws IllegalArgumentException if the value already exists mapped to a different key. 299 */ 300 @Override /* Map */ 301 public V put(K key, V value) { 302 var existingKeyForValue = reverse.get(value); 303 assertArg(! (nn(existingKeyForValue) && ! existingKeyForValue.equals(key)), "Value ''{0}'' is already mapped to key ''{1}'' in this BidiMap.", value, existingKeyForValue); 304 var oldValue = forward.put(key, value); 305 if (nn(oldValue)) { 306 reverse.remove(oldValue); 307 } 308 reverse.put(value, key); 309 return oldValue; 310 } 311 312 /** 313 * Copies all mappings from the specified map to this map. 314 * 315 * <p> 316 * This operation updates both the forward and reverse maps. 317 * 318 * @param m Mappings to be stored in this map. 319 * @throws UnsupportedOperationException if the map is unmodifiable. 320 * @throws IllegalArgumentException if any value in the map already exists mapped to a different key. 321 */ 322 @Override /* Map */ 323 public void putAll(Map<? extends K,? extends V> m) { 324 // Check for duplicate values before making any changes 325 for (var entry : m.entrySet()) { 326 var key = entry.getKey(); 327 var value = entry.getValue(); 328 var existingKeyForValue = reverse.get(value); 329 assertArg(! (nn(existingKeyForValue) && ! existingKeyForValue.equals(key)), "Value ''{0}'' is already mapped to key ''{1}'' in this BidiMap.", value, existingKeyForValue); 330 } 331 // All checks passed, now perform the updates 332 for (var entry : m.entrySet()) { 333 put(entry.getKey(), entry.getValue()); 334 } 335 } 336 337 /** 338 * Removes the mapping for a key from this map if it is present. 339 * 340 * <p> 341 * This operation removes the entry from both the forward and reverse maps. 342 * 343 * @param key The key whose mapping is to be removed from the map. 344 * @return The previous value associated with the key, or <jk>null</jk> if there was no mapping for the key. 345 * @throws UnsupportedOperationException if the map is unmodifiable. 346 */ 347 @Override /* Map */ 348 public V remove(Object key) { 349 var value = forward.remove(key); 350 reverse.remove(value); 351 return value; 352 } 353 354 /** 355 * Returns the number of key-value mappings in this map. 356 * 357 * @return The number of key-value mappings in this map. 358 */ 359 @Override /* Map */ 360 public int size() { 361 return forward.size(); 362 } 363 364 /** 365 * Returns a {@link Collection} view of the values contained in this map. 366 * 367 * @return A collection view of the values contained in this map. 368 */ 369 @Override /* Map */ 370 public Collection<V> values() { 371 return forward.values(); 372 } 373 374 /** 375 * Returns a string representation of this map. 376 * 377 * <p> 378 * The format follows the standard Java map convention: <c>"{key1=value1, key2=value2, ...}"</c> 379 * 380 * @return A string representation of this map. 381 */ 382 @Override 383 public String toString() { 384 return forward.toString(); 385 } 386 387 /** 388 * Compares the specified object with this map for equality. 389 * 390 * <p> 391 * Returns <jk>true</jk> if the given object is also a map and the two maps represent the same 392 * mappings. More formally, two maps <c>m1</c> and <c>m2</c> represent the same mappings if 393 * <c>m1.entrySet().equals(m2.entrySet())</c>. 394 * 395 * <p> 396 * This implementation compares the entry sets of the two maps. 397 * 398 * @param o Object to be compared for equality with this map. 399 * @return <jk>true</jk> if the specified object is equal to this map. 400 */ 401 @Override 402 public boolean equals(Object o) { 403 return (o instanceof Map o2) && eq(this, o2, (x, y) -> x.entrySet().equals(y.entrySet())); 404 } 405 406 /** 407 * Returns the hash code value for this map. 408 * 409 * <p> 410 * The hash code of a map is defined to be the sum of the hash codes of each entry in the map's 411 * <c>entrySet()</c> view. This ensures that <c>m1.equals(m2)</c> implies that 412 * <c>m1.hashCode()==m2.hashCode()</c> for any two maps <c>m1</c> and <c>m2</c>, as required 413 * by the general contract of {@link Object#hashCode()}. 414 * 415 * <p> 416 * This implementation computes the hash code from the entry set. 417 * 418 * @return The hash code value for this map. 419 */ 420 @Override 421 public int hashCode() { 422 return entrySet().hashCode(); 423 } 424}