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