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 org.apache.juneau.commons.utils.AssertionUtils.*;
020import static org.apache.juneau.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.ThrowableUtils.*;
022
023import java.lang.reflect.*;
024import java.util.*;
025import java.util.stream.Collectors;
026
027import org.apache.juneau.commons.utils.Utils;
028
029/**
030 * An unmodifiable, fixed-size map implementation backed by parallel key and value arrays.
031 *
032 * <p>
033 * This class provides a simple, efficient, immutable map implementation for scenarios where the
034 * set of keys and values is known in advance and doesn't need to change. It's particularly useful
035 * for small maps (typically less than 10 entries) where the overhead of a {@link HashMap} is not
036 * justified.
037 *
038 * <h5 class='section'>Features:</h5>
039 * <ul class='spaced-list'>
040 *    <li><b>Unmodifiable:</b> All mutation operations throw {@link UnsupportedOperationException}
041 *    <li><b>Fixed Keys and Values:</b> Keys and values are set at construction time
042 *    <li><b>Insertion Order:</b> Preserves the order of keys as provided in the constructor
043 *    <li><b>Null Support:</b> Supports null keys and values (unlike {@link Map#of()})
044 *    <li><b>Array-Backed:</b> Uses simple arrays for storage, avoiding hash computation overhead
045 *    <li><b>Memory Efficient:</b> Minimal memory footprint compared to {@link HashMap}
046 *    <li><b>Predictable Performance:</b> O(n) lookup time, but faster than {@link HashMap} for small n
047 * </ul>
048 *
049 * <h5 class='section'>Use Cases:</h5>
050 * <ul class='spaced-list'>
051 *    <li>Immutable configuration maps with a fixed set of known keys
052 *    <li>Small lookup tables that should not be modified
053 *    <li>Method return values where immutability is desired
054 *    <li>Situations where map size is small and keys are known at compile time
055 * </ul>
056 *
057 * <h5 class='section'>Usage:</h5>
058 * <p class='bjava'>
059 *    <jc>// Create an unmodifiable map with fixed keys and values</jc>
060 *    String[] <jv>keys</jv> = {<js>"host"</js>, <js>"port"</js>, <js>"timeout"</js>};
061 *    Object[] <jv>values</jv> = {<js>"localhost"</js>, 8080, 30000};
062 *
063 *    SimpleMap&lt;String,Object&gt; <jv>config</jv> = <jk>new</jk> SimpleMap&lt;&gt;(<jv>keys</jv>, <jv>values</jv>);
064 *
065 *    <jc>// Get values</jc>
066 *    String <jv>host</jv> = (String)<jv>config</jv>.get(<js>"host"</js>);     <jc>// Returns: "localhost"</jc>
067 *    Integer <jv>port</jv> = (Integer)<jv>config</jv>.get(<js>"port"</js>);   <jc>// Returns: 8080</jc>
068 *
069 *    <jc>// Cannot modify</jc>
070 *    <jv>config</jv>.put(<js>"port"</js>, 9090);  <jc>// Throws UnsupportedOperationException</jc>
071 * </p>
072 *
073 * <h5 class='section'>Restrictions:</h5>
074 * <ul class='spaced-list'>
075 *    <li><b>Immutable:</b> Cannot add, remove, or modify entries after construction
076 *    <li><b>Fixed Size:</b> Size is determined at construction time
077 *    <li><b>Unique Keys:</b> Keys must be unique (no duplicates)
078 *    <li><b>Array Length:</b> Keys and values arrays must have the same length
079 * </ul>
080 *
081 * <h5 class='section'>Performance Characteristics:</h5>
082 * <ul class='spaced-list'>
083 *    <li><b>get(key):</b> O(n) - Linear search through keys array
084 *    <li><b>size():</b> O(1) - Direct array length access
085 *    <li><b>Memory:</b> Lower overhead than {@link HashMap} for small maps
086 * </ul>
087 *
088 * <h5 class='section'>Thread Safety:</h5>
089 * <p>
090 * This class is thread-safe since it's immutable after construction. Multiple threads can safely
091 * read from a SimpleMap concurrently without synchronization.
092 *
093 * <h5 class='section'>Example - Configuration Map:</h5>
094 * <p class='bjava'>
095 *    <jc>// Create immutable configuration</jc>
096 *    SimpleMap&lt;String,Object&gt; <jv>defaults</jv> = <jk>new</jk> SimpleMap&lt;&gt;(
097 *       <jk>new</jk> String[]{<js>"maxConnections"</js>, <js>"timeout"</js>, <js>"retries"</js>},
098 *       <jk>new</jk> Object[]{100, 5000, 3}
099 *    );
100 *
101 *    <jc>// Safe to share across threads</jc>
102 *    <jk>return</jk> <jv>defaults</jv>;
103 * </p>
104 *
105 * <h5 class='section'>Comparison with Map.of():</h5>
106 * <ul class='spaced-list'>
107 *    <li><b>Null Support:</b> SimpleMap supports null keys/values, Map.of() does not
108 *    <li><b>Insertion Order:</b> SimpleMap preserves insertion order, Map.of() does not
109 *    <li><b>Size Limit:</b> Map.of() limited to 10 entries, SimpleMap has no limit
110 * </ul>
111 *
112 * <h5 class='section'>See Also:</h5>
113 * <ul>
114 *    <li class='jc'>{@link SimpleMap} - The modifiable version of this class
115 *    <li class='link'><a class="doclink" href="../../../../../index.html#juneau-commons">Overview &gt; juneau-commons</a>
116 * </ul>
117 *
118 * @param <K> The key type.
119 * @param <V> The value type.
120 */
121public class SimpleMap<K,V> extends AbstractMap<K,V> {
122
123   /**
124    * Inner class representing a single key-value entry in this map.
125    * <p>
126    * This entry is unmodifiable - calling {@link #setValue(Object)} will throw
127    * {@link UnsupportedOperationException}.
128    */
129   class SimpleUnmodifiableMapEntry implements Map.Entry<K,V> {
130
131      /** The index into the keys/values arrays for this entry. */
132      private final int index;
133
134      /**
135       * Constructor.
136       * @param index The array index for this entry.
137       */
138      SimpleUnmodifiableMapEntry(int index) {
139         this.index = index;
140      }
141
142      @Override /* Map.Entry */
143      public K getKey() { return keys[index]; }
144
145      @Override /* Map.Entry */
146      public V getValue() { return values[index]; }
147
148      @Override /* Map.Entry */
149      public V setValue(V val) {
150         throw unsupportedOp("Map is unmodifiable");
151      }
152
153      @Override
154      public String toString() {
155         return getKey() + "=" + getValue();
156      }
157
158      @Override
159      public boolean equals(Object o) {
160         return (o instanceof Map.Entry<?,?> o2) && Utils.eq(this, o2, (x,y) -> Utils.eq(x.getKey(), y.getKey()) && Utils.eq(x.getValue(), y.getValue()));
161      }
162
163      @Override
164      public int hashCode() {
165         K key = getKey();
166         V value = getValue();
167         return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode());
168      }
169   }
170
171   /** The array of keys. Keys are immutable after construction. */
172   final K[] keys;
173
174   /** The array of values. Values are immutable after construction. */
175   final V[] values;
176
177   /** Pre-constructed entries array for {@link #entrySet()}. */
178   final SimpleUnmodifiableMapEntry[] entries;
179
180   /**
181    * Constructs a new SimpleMap with the specified keys and values.
182    *
183    * <p>
184    * The keys and values arrays are stored by reference (not copied), so external
185    * modifications to the arrays after construction will affect the map's behavior.
186    * For true immutability, ensure the arrays are not modified after passing them
187    * to this constructor.
188    *
189    * <h5 class='section'>Example:</h5>
190    * <p class='bjava'>
191    *    String[] <jv>keys</jv> = {<js>"name"</js>, <js>"age"</js>, <js>"city"</js>};
192    *    Object[] <jv>values</jv> = {<js>"John"</js>, 30, <js>"NYC"</js>};
193    *
194    *    SimpleMap&lt;String,Object&gt; <jv>person</jv> = <jk>new</jk> SimpleMap&lt;&gt;(<jv>keys</jv>, <jv>values</jv>);
195    * </p>
196    *
197    * @param keys The map keys. Must not be <jk>null</jk>. Individual keys can be <jk>null</jk>.
198    * @param values The map values. Must not be <jk>null</jk> and must have the same length as keys.
199    *               Individual values can be <jk>null</jk>.
200    * @throws IllegalArgumentException if:
201    *         <ul>
202    *          <li>The keys array is <jk>null</jk>
203    *          <li>The values array is <jk>null</jk>
204    *          <li>The keys and values arrays have different lengths
205    *          <li>Any key appears more than once (duplicate keys)
206    *         </ul>
207    */
208   @SuppressWarnings("unchecked")
209   public SimpleMap(K[] keys, V[] values) {
210      assertArgsNotNull("keys", keys, "values", values);
211      assertArg(keys.length == values.length, "keys ''{0}'' and values ''{1}'' array lengths differ", keys.length, values.length);
212
213      // Check for duplicate keys
214      for (var i = 0; i < keys.length; i++) {
215         for (var j = i + 1; j < keys.length; j++) {
216            if (Utils.eq(keys[i], keys[j])) {
217               throw illegalArg("Duplicate key found: {0}", keys[i]);
218            }
219         }
220      }
221
222      this.keys = keys;
223      this.values = values;
224      entries = (SimpleUnmodifiableMapEntry[])Array.newInstance(SimpleUnmodifiableMapEntry.class, keys.length);
225      for (var i = 0; i < keys.length; i++) {
226         entries[i] = new SimpleUnmodifiableMapEntry(i);
227      }
228   }
229
230   /**
231    * Returns a {@link Set} view of the mappings contained in this map.
232    *
233    * <p>
234    * The returned set is unmodifiable and backed by the underlying entries array.
235    *
236    * <h5 class='section'>Example:</h5>
237    * <p class='bjava'>
238    *    SimpleMap&lt;String,Integer&gt; <jv>map</jv> = <jk>new</jk> SimpleMap&lt;&gt;(
239    *       <jk>new</jk> String[]{<js>"a"</js>, <js>"b"</js>},
240    *       <jk>new</jk> Integer[]{1, 2}
241    *    );
242    *
243    *    <jk>for</jk> (Map.Entry&lt;String,Integer&gt; <jv>entry</jv> : <jv>map</jv>.entrySet()) {
244    *       System.<jsf>out</jsf>.println(<jv>entry</jv>.getKey() + <js>"="</js> + <jv>entry</jv>.getValue());
245    *    }
246    * </p>
247    *
248    * @return An unmodifiable set view of the mappings in this map.
249    */
250   @Override /* Map */
251   public Set<Map.Entry<K,V>> entrySet() {
252      return Collections.unmodifiableSet(toSet(entries));
253   }
254
255   /**
256    * Returns the value associated with the specified key.
257    *
258    * <p>
259    * This method performs a linear search through the keys array, using {@link Object#equals(Object)}
260    * for comparison. For small maps (typically less than 10 entries), this is often faster than
261    * the hash lookup in {@link HashMap}.
262    *
263    * <h5 class='section'>Example:</h5>
264    * <p class='bjava'>
265    *    SimpleMap&lt;String,Integer&gt; <jv>map</jv> = <jk>new</jk> SimpleMap&lt;&gt;(
266    *       <jk>new</jk> String[]{<js>"age"</js>, <js>"score"</js>},
267    *       <jk>new</jk> Integer[]{25, 100}
268    *    );
269    *
270    *    Integer <jv>age</jv> = <jv>map</jv>.get(<js>"age"</js>);     <jc>// Returns: 25</jc>
271    *    Integer <jv>height</jv> = <jv>map</jv>.get(<js>"height"</js>); <jc>// Returns: null (key not found)</jc>
272    * </p>
273    *
274    * @param key The key whose associated value is to be returned.
275    * @return The value associated with the specified key, or <jk>null</jk> if the key is not found
276    *         (or if the key is mapped to <jk>null</jk>).
277    */
278   @Override /* Map */
279   public V get(Object key) {
280      for (var i = 0; i < keys.length; i++)
281         if (Utils.eq(keys[i], key))
282            return values[i];
283      return null;
284   }
285
286   /**
287    * Returns a {@link Set} view of the keys contained in this map.
288    *
289    * <p>
290    * The returned set is unmodifiable and backed by the underlying keys array.
291    *
292    * <h5 class='section'>Example:</h5>
293    * <p class='bjava'>
294    *    SimpleMap&lt;String,Integer&gt; <jv>map</jv> = <jk>new</jk> SimpleMap&lt;&gt;(
295    *       <jk>new</jk> String[]{<js>"x"</js>, <js>"y"</js>, <js>"z"</js>},
296    *       <jk>new</jk> Integer[]{1, 2, 3}
297    *    );
298    *
299    *    Set&lt;String&gt; <jv>keys</jv> = <jv>map</jv>.keySet();  <jc>// Returns: [x, y, z]</jc>
300    * </p>
301    *
302    * @return An unmodifiable set view of the keys in this map.
303    */
304   @Override /* Map */
305   public Set<K> keySet() {
306      return Collections.unmodifiableSet(toSet(keys));
307   }
308
309   /**
310    * Throws {@link UnsupportedOperationException} as this map is unmodifiable.
311    *
312    * @param key Ignored.
313    * @param value Ignored.
314    * @return Never returns normally.
315    * @throws UnsupportedOperationException Always thrown, as this map cannot be modified.
316    */
317   @Override /* Map */
318   public V put(K key, V value) {
319      throw unsupportedOp("Map is unmodifiable");
320   }
321
322   /**
323    * Returns a string representation of this map.
324    *
325    * <p>
326    * The format follows the standard Java map convention: <c>"{key1=value1, key2=value2, ...}"</c>
327    *
328    * @return A string representation of this map.
329    */
330   @Override
331   public String toString() {
332      return entrySet().stream().map(Object::toString).collect(Collectors.joining(", ", "{", "}"));
333   }
334
335   /**
336    * Compares the specified object with this map for equality.
337    *
338    * <p>
339    * Returns <jk>true</jk> if the given object is also a map and the two maps represent the same
340    * mappings. More formally, two maps <c>m1</c> and <c>m2</c> represent the same mappings if
341    * <c>m1.entrySet().equals(m2.entrySet())</c>.
342    *
343    * <p>
344    * This implementation compares the entry sets of the two maps.
345    *
346    * @param o Object to be compared for equality with this map.
347    * @return <jk>true</jk> if the specified object is equal to this map.
348    */
349   @Override
350   public boolean equals(Object o) {
351      return (o instanceof Map o2) && Utils.eq(this, o2, (x, y) -> x.entrySet().equals(y.entrySet()));
352   }
353
354   /**
355    * Returns the hash code value for this map.
356    *
357    * <p>
358    * The hash code of a map is defined to be the sum of the hash codes of each entry in the map's
359    * <c>entrySet()</c> view. This ensures that <c>m1.equals(m2)</c> implies that
360    * <c>m1.hashCode()==m2.hashCode()</c> for any two maps <c>m1</c> and <c>m2</c>, as required
361    * by the general contract of {@link Object#hashCode()}.
362    *
363    * <p>
364    * This implementation computes the hash code from the entry set.
365    *
366    * @return The hash code value for this map.
367    */
368   @Override
369   public int hashCode() {
370      return entrySet().hashCode();
371   }
372}