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