View Javadoc
1   package org.metricshub.jawk.jrt;
2   
3   import java.util.Collection;
4   
5   /*-
6    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
7    * Jawk
8    * ჻჻჻჻჻჻
9    * Copyright (C) 2006 - 2025 MetricsHub
10   * ჻჻჻჻჻჻
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU Lesser General Public License as
13   * published by the Free Software Foundation, either version 3 of the
14   * License, or (at your option) any later version.
15   *
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Lesser Public License for more details.
20   *
21   * You should have received a copy of the GNU General Lesser Public
22   * License along with this program.  If not, see
23   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
24   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
25   */
26  
27  import java.util.Comparator;
28  import java.util.HashMap;
29  import java.util.LinkedHashMap;
30  import java.util.Map;
31  import java.util.Set;
32  import java.util.TreeMap;
33  import org.metricshub.jawk.intermediate.UninitializedObject;
34  
35  /**
36   * An AWK associative array.
37   * <p>
38   * The implementation requires the ability to choose,
39   * at runtime, whether the keys are to be maintained in
40   * sorted order or not. Therefore, the implementation
41   * contains a reference to a Map (either TreeMap or
42   * HashMap, depending on whether to maintain keys in
43   * sorted order or not) and delegates calls to it
44   * accordingly.
45   *
46   * @author Danny Daglas
47   */
48  public class AssocArray implements Comparator<Object>, Map<Object, Object> {
49  
50  	private Map<Object, Object> map;
51  
52  	/**
53  	 * <p>
54  	 * Constructor for AssocArray.
55  	 * </p>
56  	 *
57  	 * @param sortedArrayKeys Whether keys must be kept sorted
58  	 */
59  	public AssocArray(boolean sortedArrayKeys) {
60  		if (sortedArrayKeys) {
61  			map = new TreeMap<Object, Object>((Comparator<Object>) this);
62  		} else {
63  			map = new HashMap<Object, Object>();
64  		}
65  	}
66  
67  	/**
68  	 * The parameter to useMapType to convert
69  	 * this associative array to a HashMap.
70  	 */
71  	public static final int MT_HASH = 2;
72  	/**
73  	 * The parameter to useMapType to convert
74  	 * this associative array to a LinkedHashMap.
75  	 */
76  	public static final int MT_LINKED = 2 << 1;
77  	/**
78  	 * The parameter to useMapType to convert
79  	 * this associative array to a TreeMap.
80  	 */
81  	public static final int MT_TREE = 2 << 2;
82  
83  	/**
84  	 * Convert the map which backs this associative array
85  	 * into one of HashMap, LinkedHashMap, or TreeMap.
86  	 *
87  	 * @param mapType Can be one of MT_HASH, MT_LINKED,
88  	 *        or MT_TREE.
89  	 */
90  	public void useMapType(int mapType) {
91  		assert map.isEmpty();
92  		switch (mapType) {
93  		case MT_HASH:
94  			map = new HashMap<Object, Object>();
95  			break;
96  		case MT_LINKED:
97  			map = new LinkedHashMap<Object, Object>();
98  			break;
99  		case MT_TREE:
100 			map = new TreeMap<Object, Object>((Comparator<Object>) this);
101 			break;
102 		default:
103 			throw new Error("Invalid map type : " + mapType);
104 		}
105 	}
106 
107 	/**
108 	 * Provide a string representation of the delegated
109 	 * map object.
110 	 *
111 	 * @return string representing the map/array
112 	 */
113 	public String mapString() {
114 		// was:
115 		// return map.toString();
116 		// but since the extensions, assoc arrays can become keys as well
117 		StringBuilder sb = new StringBuilder().append('{');
118 		int cnt = 0;
119 		for (Map.Entry<Object, Object> entry : map.entrySet()) {
120 			if (cnt > 0) {
121 				sb.append(", ");
122 			}
123 			Object key = entry.getKey();
124 			if (key instanceof AssocArray) {
125 				sb.append(((AssocArray) key).mapString());
126 			} else {
127 				sb.append(key.toString());
128 			}
129 			sb.append('=');
130 			Object value = entry.getValue();
131 			if (value instanceof AssocArray) {
132 				sb.append(((AssocArray) value).mapString());
133 			} else {
134 				sb.append(value.toString());
135 			}
136 			++cnt;
137 		}
138 		return sb.append('}').toString();
139 	}
140 
141 	/** a "null" value in Awk */
142 	private static final UninitializedObject BLANK = new UninitializedObject();
143 
144 	/**
145 	 * <p>
146 	 * isIn.
147 	 * </p>
148 	 *
149 	 * @param key Key to be checked
150 	 * @return whether a particular key is
151 	 *         contained within the associative array.
152 	 *         Unlike get(), which adds a blank (null)
153 	 *         reference to the associative array if the
154 	 *         element is not found, isIn will not.
155 	 *         It exists to support the IN keyword.
156 	 */
157 	public boolean isIn(Object key) {
158 		if (key == null || key instanceof UninitializedObject) {
159 			// According to AWK semantics, an uninitialized index
160 			// evaluates to the empty string, not numeric zero
161 			key = "";
162 		}
163 
164 		if (map.containsKey(key)) {
165 			return true;
166 		}
167 
168 		try {
169 			long iKey = Long.parseLong(key.toString());
170 			if (map.containsKey(iKey)) {
171 				return true;
172 			}
173 		} catch (Exception e) {// NOPMD - EmptyCatchBlock: intentionally ignored
174 		}
175 
176 		return false;
177 	}
178 
179 	/**
180 	 * <p>
181 	 * get.
182 	 * </p>
183 	 *
184 	 * @param key Key to retrieve in the array
185 	 * @return the value of an associative array
186 	 *         element given a particular key.
187 	 *         If the key does not exist, a null value
188 	 *         (blank string) is inserted into the array
189 	 *         with this key, and the null value is returned.
190 	 */
191 	public Object get(Object key) {
192 		if (key == null || key instanceof UninitializedObject) {
193 			// AWK evaluates an uninitialized subscript to the empty string
194 			key = "";
195 		}
196 		Object result = map.get(key);
197 		if (result != null) {
198 			return result;
199 		}
200 
201 		// Did not find it?
202 		try {
203 			// try a integer version key
204 			key = Long.parseLong(key.toString());
205 			result = map.get(key);
206 			if (result != null) {
207 				return result;
208 			}
209 		} catch (Exception e) {// NOPMD - EmptyCatchBlock: intentionally ignored
210 		}
211 
212 		// based on the AWK specification:
213 		// Any reference (except for IN expressions) to a non-existent
214 		// array element will automatically create it.
215 		result = BLANK;
216 		map.put(key, result);
217 
218 		return result;
219 	}
220 
221 	/**
222 	 * Added to support insertion of primitive key types.
223 	 *
224 	 * @param key Key of the entry to put in the array
225 	 * @param value Value of the key
226 	 * @return the previous value of the specified key, or null if key didn't exist
227 	 */
228 	public Object put(Object key, Object value) {
229 		if (key == null || key instanceof UninitializedObject) {
230 			key = "";
231 		}
232 		try {
233 			// Save a primitive version
234 			long iKey = Long.parseLong(key.toString());
235 			return map.put(iKey, value);
236 		} catch (Exception e) {// NOPMD - EmptyCatchBlock: intentionally ignored
237 		}
238 
239 		return map.put(key, value);
240 	}
241 
242 	/**
243 	 * Added to support insertion of primitive key types.
244 	 *
245 	 * @param key Index of the entry to put in the array
246 	 * @param value Value of the key
247 	 * @return the previous value of the specified key, or null if key didn't exist
248 	 */
249 	public Object put(long key, Object value) {
250 		return map.put(key, value);
251 	}
252 
253 	/**
254 	 * <p>
255 	 * keySet.
256 	 * </p>
257 	 *
258 	 * @return the set of keys
259 	 */
260 	public Set<Object> keySet() {
261 		return map.keySet();
262 	}
263 
264 	/**
265 	 * Clear the array
266 	 */
267 	public void clear() {
268 		map.clear();
269 	}
270 
271 	/**
272 	 * Delete the specified entry
273 	 *
274 	 * @param key Key of the entry to remove from the array
275 	 * @return the value of the entry before it was removed
276 	 */
277 	public Object remove(Object key) {
278 		if (key == null || key instanceof UninitializedObject) {
279 			key = "";
280 		}
281 		Object result = map.remove(key);
282 		if (result != null) {
283 			return result;
284 		}
285 
286 		try {
287 			long iKey = Long.parseLong(key.toString());
288 			return map.remove(iKey);
289 		} catch (Exception e) {// NOPMD - EmptyCatchBlock: intentionally ignored
290 		}
291 
292 		return null;
293 	}
294 
295 	/**
296 	 * {@inheritDoc}
297 	 * Do nothing. Should not be called in this state.
298 	 */
299 	@Override
300 	public String toString() {
301 		throw new AwkRuntimeException("Cannot evaluate an unindexed array.");
302 	}
303 
304 	/**
305 	 * {@inheritDoc}
306 	 * Comparator implementation used by the TreeMap
307 	 * when keys are to be maintained in sorted order.
308 	 */
309 	@Override
310 	public int compare(Object o1, Object o2) {
311 		if (o1 instanceof String
312 				|| o2 instanceof String
313 				|| !(o1 instanceof Number && o2 instanceof Number)) {
314 			// Fall back to string comparison if any of the keys is a
315 			// String or not a Number
316 			String s1 = o1.toString();
317 			String s2 = o2.toString();
318 			return s1.compareTo(s2);
319 		}
320 
321 		// Both keys are numbers
322 		if (o1 instanceof Double
323 				|| o2 instanceof Double
324 				|| o1 instanceof Float
325 				|| o2 instanceof Float) {
326 			double d1 = ((Number) o1).doubleValue();
327 			double d2 = ((Number) o2).doubleValue();
328 			return Double.compare(d1, d2);
329 		}
330 
331 		long l1 = ((Number) o1).longValue();
332 		long l2 = ((Number) o2).longValue();
333 		return Long.compare(l1, l2);
334 	}
335 
336 	/**
337 	 * <p>
338 	 * getMapVersion.
339 	 * </p>
340 	 *
341 	 * @return the specification version of this class
342 	 */
343 	public String getMapVersion() {
344 		return map.getClass().getPackage().getSpecificationVersion();
345 	}
346 
347 	@Override
348 	public int size() {
349 		return map.size();
350 	}
351 
352 	@Override
353 	public boolean isEmpty() {
354 		return map.isEmpty();
355 	}
356 
357 	@Override
358 	public boolean containsKey(Object key) {
359 		return map.containsKey(key);
360 	}
361 
362 	@Override
363 	public boolean containsValue(Object value) {
364 		return map.containsValue(value);
365 	}
366 
367 	@Override
368 	public void putAll(Map<? extends Object, ? extends Object> m) {
369 		for (Map.Entry<? extends Object, ? extends Object> entry : m.entrySet()) {
370 			map.put(entry.getKey(), entry.getValue());
371 		}
372 	}
373 
374 	@Override
375 	public Collection<Object> values() {
376 		return map.values();
377 	}
378 
379 	@Override
380 	public Set<Entry<Object, Object>> entrySet() {
381 		return map.entrySet();
382 	}
383 }