View Javadoc
1   package io.jawk.jrt;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * Jawk
6    * ჻჻჻჻჻჻
7    * Copyright (C) 2006 - 2026 MetricsHub
8    * ჻჻჻჻჻჻
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   *
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Lesser Public License for more details.
18   *
19   * You should have received a copy of the GNU General Lesser Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
22   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
23   */
24  
25  import java.util.List;
26  import java.util.Map;
27  import io.jawk.intermediate.UninitializedObject;
28  
29  /**
30   * An AWK associative array.
31   * <p>
32   * This interface extends {@link Map} and provides AWK-specific behaviour:
33   * automatic key normalization (null and uninitialized values map to {@code ""}),
34   * numeric key coercion ({@code "1"} and {@code 1L} address the same slot), and
35   * auto-creation of blank entries on first access.
36   * </p>
37   * <p>
38   * Concrete implementations directly extend a JDK {@link Map} class to avoid
39   * delegation overhead:
40   * </p>
41   * <ul>
42   * <li>{@link HashAssocArray} &mdash; backed by {@link java.util.HashMap}</li>
43   * <li>{@link ListAssocArray} &mdash; materialized from a {@link java.util.List}
44   * and backed by {@link java.util.HashMap}</li>
45   * <li>{@link SortedAssocArray} &mdash; backed by {@link java.util.TreeMap} with
46   * AWK key ordering</li>
47   * </ul>
48   * <p>
49   * Use the factory methods to create instances:
50   * </p>
51   *
52   * <pre>
53   * AssocArray hash = AssocArray.createHash();
54   * AssocArray sorted = AssocArray.createSorted();
55   * AssocArray list = AssocArray.createFromList(values, sortedArrayKeys);
56   * AssocArray aa = AssocArray.create(sortedArrayKeys);
57   * </pre>
58   *
59   * @author Danny Daglas
60   */
61  public interface AssocArray extends Map<Object, Object> {
62  
63  	/** A blank (uninitialized) value shared across all AWK array accesses. */
64  	UninitializedObject BLANK = new UninitializedObject();
65  
66  // -------------------------------------------------------------------------
67  // Key-normalization helpers (used by concrete implementations)
68  // -------------------------------------------------------------------------
69  
70  	/**
71  	 * Converts a key to the canonical form expected by AWK: {@code null} and
72  	 * {@link UninitializedObject} map to the empty string, and internal input
73  	 * strings map to their string value.
74  	 *
75  	 * @param key the raw key
76  	 * @return the normalized key, never {@code null}
77  	 */
78  	static Object normalizeKey(Object key) {
79  		if (key == null || key instanceof UninitializedObject) {
80  			return "";
81  		}
82  		if (key instanceof StrNum) {
83  			return key.toString();
84  		}
85  		if (key instanceof Double || key instanceof Float) {
86  			double numericKey = ((Number) key).doubleValue();
87  			if (JRT.isActuallyLong(numericKey)) {
88  				return Long.valueOf((long) Math.rint(numericKey));
89  			}
90  		}
91  		return key;
92  	}
93  
94  	/**
95  	 * Attempts to parse the key as a {@code Long}.
96  	 *
97  	 * @param key the key to parse (must not be {@code null})
98  	 * @return the {@code Long} value, or {@code null} if the key cannot be parsed
99  	 *         as a long integer
100 	 */
101 	static Long toLongKey(Object key) {
102 		try {
103 			return Long.parseLong(key.toString());
104 		} catch (Exception e) { // NOPMD - EmptyCatchBlock: intentionally ignored
105 			return null;
106 		}
107 	}
108 
109 // -------------------------------------------------------------------------
110 // AWK-specific default methods
111 // -------------------------------------------------------------------------
112 
113 	/**
114 	 * Returns whether a particular key is contained within the associative array.
115 	 * <p>
116 	 * Unlike {@link #get(Object)}, which auto-creates a blank entry when the key
117 	 * is absent, this method does not modify the array. It exists to support the
118 	 * AWK {@code IN} keyword.
119 	 * </p>
120 	 *
121 	 * @param key Key to be checked
122 	 * @return {@code true} if the key (or its numeric equivalent) is present
123 	 */
124 	default boolean isIn(Object key) {
125 		key = normalizeKey(key);
126 		if (containsKey(key)) {
127 			return true;
128 		}
129 		try {
130 			long iKey = Long.parseLong(key.toString());
131 			return containsKey(iKey);
132 		} catch (Exception e) { // NOPMD - EmptyCatchBlock: intentionally ignored
133 		}
134 		return false;
135 	}
136 
137 	/**
138 	 * Provides a string representation of this associative array, recursively
139 	 * rendering nested arrays.
140 	 *
141 	 * @return a human-readable map string of the form {@code {key=value, ...}}
142 	 */
143 	default String mapString() {
144 // Since extensions allow assoc arrays to become keys as well,
145 // we render nested arrays recursively rather than using toString().
146 		StringBuilder sb = new StringBuilder().append('{');
147 		int cnt = 0;
148 		for (Map.Entry<Object, Object> entry : entrySet()) {
149 			if (cnt > 0) {
150 				sb.append(", ");
151 			}
152 			Object key = entry.getKey();
153 			if (key instanceof AssocArray) {
154 				sb.append(((AssocArray) key).mapString());
155 			} else {
156 				sb.append(key.toString());
157 			}
158 			sb.append('=');
159 			Object value = entry.getValue();
160 			if (value instanceof AssocArray) {
161 				sb.append(((AssocArray) value).mapString());
162 			} else {
163 				sb.append(value.toString());
164 			}
165 			++cnt;
166 		}
167 		return sb.append('}').toString();
168 	}
169 
170 	/**
171 	 * Stores a value using a primitive {@code long} key, bypassing string parsing.
172 	 * <p>
173 	 * This is a convenience overload for callers that already hold a {@code long}
174 	 * key. The default implementation boxes the key and delegates to
175 	 * {@link #put(Object, Object)}.
176 	 * </p>
177 	 *
178 	 * @param key the long key
179 	 * @param value the value to associate with the key
180 	 * @return the previous value associated with the key, or {@code null}
181 	 */
182 	default Object put(long key, Object value) {
183 		return put(Long.valueOf(key), value);
184 	}
185 
186 	/**
187 	 * Returns the specification version of the underlying JDK {@link Map} class
188 	 * that backs this implementation.
189 	 *
190 	 * @return the specification version string, or {@code null} if unavailable
191 	 */
192 	default String getMapVersion() {
193 		return getClass().getSuperclass().getPackage().getSpecificationVersion();
194 	}
195 
196 // -------------------------------------------------------------------------
197 // Factory methods
198 // -------------------------------------------------------------------------
199 
200 	/**
201 	 * Creates a new hash-based associative array (backed by {@link java.util.HashMap}).
202 	 *
203 	 * @return a new {@link HashAssocArray}
204 	 */
205 	static AssocArray createHash() {
206 		return new HashAssocArray();
207 	}
208 
209 	/**
210 	 * Creates a new sorted associative array (backed by {@link java.util.TreeMap}
211 	 * with AWK key ordering).
212 	 *
213 	 * @return a new {@link SortedAssocArray}
214 	 */
215 	static AssocArray createSorted() {
216 		return new SortedAssocArray();
217 	}
218 
219 	/**
220 	 * Creates a new associative array of the appropriate type.
221 	 *
222 	 * @param sortedArrayKeys {@code true} to create a sorted (tree-backed) array,
223 	 *        {@code false} for a hash-backed array
224 	 * @return a new {@link AssocArray} instance
225 	 */
226 	static AssocArray create(boolean sortedArrayKeys) {
227 		return sortedArrayKeys ? createSorted() : createHash();
228 	}
229 
230 	/**
231 	 * Creates a new associative array materialized from a Java {@link List}.
232 	 * <p>
233 	 * List elements are stored under zero-based {@link Long} keys. Nested
234 	 * {@link List} values are recursively materialized as associative arrays so
235 	 * JSON-like object trees can be traversed with AWK array syntax.
236 	 * </p>
237 	 *
238 	 * @param values list values to expose as an AWK array
239 	 * @param sortedArrayKeys {@code true} to create a sorted array, {@code false}
240 	 *        for a hash-backed array
241 	 * @return a new {@link AssocArray} containing the list values
242 	 */
243 	static AssocArray createFromList(List<?> values, boolean sortedArrayKeys) {
244 		return ListAssocArray.createFromList(values, sortedArrayKeys);
245 	}
246 
247 	/**
248 	 * Normalizes an externally supplied structured value before the AVM stores it.
249 	 * <p>
250 	 * {@link List} values are converted to {@link AssocArray} instances.
251 	 * {@link Map} values are kept in place to preserve direct-map performance, but
252 	 * their nested list values are recursively converted.
253 	 * </p>
254 	 *
255 	 * @param value value to normalize
256 	 * @param sortedArrayKeys {@code true} when converted lists should use sorted
257 	 *        array keys
258 	 * @return the normalized value
259 	 */
260 	static Object normalizeValue(Object value, boolean sortedArrayKeys) {
261 		return ListAssocArray.normalizeValue(value, sortedArrayKeys);
262 	}
263 
264 }