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&lt;String,Integer&gt; <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&lt;String,Integer&gt; <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 &gt; 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&lt;String,Integer&gt; <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&lt;String,Integer&gt; <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}