View Javadoc
1   package io.jawk.util;
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.HashMap;
26  import java.util.Locale;
27  import java.util.Map;
28  import java.util.Objects;
29  import java.util.concurrent.atomic.AtomicLong;
30  import io.jawk.Awk;
31  
32  /**
33   * Reusable behavioral configuration for the Jawk engine.
34   * <p>
35   * Instances hold settings that control how the AWK interpreter behaves
36   * (field separator, locale, sorted keys, initial variables, and default
37   * record separator) but do <em>not</em> carry per-execution state such as
38   * input sources, filename arguments, or output destinations. This
39   * separation allows a single {@code AwkSettings} object to be shared
40   * across many invocations of {@link io.jawk.Awk#eval},
41   * {@link io.jawk.Awk#script}, or {@link io.jawk.Awk#createAvm()}.
42   * </p>
43   * <p>
44   * Output is configured on the execution builder returned by
45   * {@link io.jawk.Awk#script(String)} or {@link io.jawk.Awk#script(io.jawk.AwkProgram)}
46   * and is therefore always per-execution.
47   * </p>
48   *
49   * @author Danny Daglas
50   */
51  public class AwkSettings {
52  
53  	/**
54  	 * Shared immutable settings instance representing the default configuration.
55  	 */
56  	public static final AwkSettings DEFAULT_SETTINGS = new ImmutableAwkSettings();
57  
58  	/**
59  	 * Contains variable assignments which are applied prior to
60  	 * executing the script (-v assignments).
61  	 * The values may be of type <code>Integer</code>,
62  	 * <code>Double</code>, <code>String</code>,
63  	 * {@link io.jawk.jrt.AssocArray} (for array variables),
64  	 * any {@link java.util.Map} that Jawk exposes directly to the script,
65  	 * or any {@link java.util.List} that Jawk materializes as an array with
66  	 * zero-based {@link java.lang.Long} keys.
67  	 * <p>
68  	 * When a {@link java.util.Map} is provided, the Jawk runtime may mutate it
69  	 * during execution. Callers must therefore supply a mutable map
70  	 * implementation. Numeric indices written by the runtime into such maps and
71  	 * indices derived from {@link java.util.List} values use
72  	 * {@link java.lang.Long} keys (for example, <code>0L</code>,
73  	 * <code>1L</code>, ...).
74  	 * </p>
75  	 */
76  	private final Map<String, Object> variables = new HashMap<String, Object>();
77  
78  	/**
79  	 * Initial Field Separator (FS) value.
80  	 * <code>null</code> means the default FS value.
81  	 */
82  	private volatile String fieldSeparator = null;
83  
84  	/**
85  	 * Whether to maintain array keys in sorted order;
86  	 * <code>false</code> by default.
87  	 */
88  	private volatile boolean useSortedArrayKeys = false;
89  
90  	/**
91  	 * Whether to accept gawk-style arrays of arrays syntax such as {@code a[i][j]}
92  	 * and subarray operands in array-only positions such as {@code split(..., a[i])}.
93  	 * <code>true</code> by default.
94  	 */
95  	private volatile boolean posix;
96  
97  	/**
98  	 * Locale for the output of numbers
99  	 * <code>US-English</code> by default.
100 	 */
101 	private volatile Locale locale = Locale.US;
102 
103 	/**
104 	 * Default value for RS, when not set specifically by the AWK script.
105 	 * Defaults to {@link Awk#DEFAULT_RS} per POSIX. Platform-specific
106 	 * end-of-line handling is the responsibility of the input source.
107 	 */
108 	private volatile String defaultRS = Awk.DEFAULT_RS;
109 
110 	/**
111 	 * Monotonically increasing counter incremented whenever the settings change.
112 	 * It allows callers that cache derived runtime state to detect when a new
113 	 * snapshot must be built.
114 	 */
115 	private final AtomicLong modificationCount = new AtomicLong();
116 
117 	/**
118 	 * Creates a settings instance holding Jawk's default configuration, each
119 	 * field starting at the default documented on its getter.
120 	 * <p>
121 	 * Use the setters to depart from it, or the shared {@link #DEFAULT_SETTINGS}
122 	 * instance when the defaults are enough.
123 	 * </p>
124 	 */
125 	public AwkSettings() {
126 		// Every field is initialized with its documented default.
127 	}
128 
129 	/**
130 	 * <p>
131 	 * toDescriptionString.
132 	 * </p>
133 	 *
134 	 * @return a human readable representation of the parameters values.
135 	 */
136 	public String toDescriptionString() {
137 		StringBuilder desc = new StringBuilder();
138 
139 		final char newLine = '\n';
140 
141 		desc.append("variables = ").append(getVariables()).append(newLine);
142 		desc.append("fieldSeparator = ").append(getFieldSeparator()).append(newLine);
143 		desc.append("useSortedArrayKeys = ").append(isUseSortedArrayKeys()).append(newLine);
144 		desc.append("posix = ").append(isPosix()).append(newLine);
145 		return desc.toString();
146 	}
147 
148 	/**
149 	 * Provides a description of extensions that are enabled/disabled.
150 	 * The default compiler implementation uses this method
151 	 * to describe extensions which are compiled into the script.
152 	 * The description is then provided to the user within the usage.
153 	 *
154 	 * @return A description of the extensions which are enabled/disabled.
155 	 */
156 	public String toExtensionDescription() {
157 		StringBuilder extensions = new StringBuilder();
158 
159 		if (isUseSortedArrayKeys()) {
160 			extensions.append(", associative array keys are sorted");
161 		}
162 		if (!isPosix()) {
163 			extensions.append(", arrays of arrays");
164 		}
165 		if (extensions.length() > 0) {
166 			return "{extensions: " + extensions.substring(2) + "}";
167 		} else {
168 			return "{no compiled extensions utilized}";
169 		}
170 	}
171 
172 	@SuppressWarnings("unused")
173 	private void addInitialVariable(String keyValue) {
174 		int equalsIdx = keyValue.indexOf('=');
175 		String name = keyValue.substring(0, equalsIdx);
176 		String valueString = keyValue.substring(equalsIdx + 1);
177 		// note: can overwrite previously defined variables
178 		putVariable(name, valueString);
179 	}
180 
181 	/**
182 	 * Contains variable assignments which are applied prior to
183 	 * executing the script (-v assignments).
184 	 * The values may be of type <code>Integer</code>,
185 	 * <code>Double</code>, <code>String</code>,
186 	 * {@link io.jawk.jrt.AssocArray} (for array variables),
187 	 * any {@link java.util.Map} that Jawk exposes directly to the script,
188 	 * or any {@link java.util.List} that Jawk materializes as an array with
189 	 * zero-based {@link java.lang.Long} keys.
190 	 *
191 	 * @return the variables
192 	 */
193 	public Map<String, Object> getVariables() {
194 		synchronized (variables) {
195 			return new HashMap<String, Object>(variables);
196 		}
197 	}
198 
199 	/**
200 	 * Returns the number of explicit mutations applied to this settings
201 	 * instance.
202 	 * <p>
203 	 * The value is intended for cache invalidation only; it has no behavioral
204 	 * meaning other than changing whenever one of the configuration mutators is
205 	 * called.
206 	 * </p>
207 	 *
208 	 * @return the current modification counter
209 	 */
210 	public long getModificationCount() {
211 		return modificationCount.get();
212 	}
213 
214 	/**
215 	 * Contains variable assignments which are applied prior to
216 	 * executing the script (-v assignments).
217 	 * The values may be of type <code>Integer</code>,
218 	 * <code>Double</code>, <code>String</code>,
219 	 * {@link io.jawk.jrt.AssocArray} (for array variables),
220 	 * any {@link java.util.Map} that Jawk exposes directly to the script,
221 	 * or any {@link java.util.List} that Jawk materializes as an array with
222 	 * zero-based {@link java.lang.Long} keys.
223 	 *
224 	 * @param variables the variables to set
225 	 */
226 	public void setVariables(Map<String, Object> variables) {
227 		synchronized (this.variables) {
228 			this.variables.clear();
229 			this.variables.putAll(variables);
230 		}
231 		markModified();
232 	}
233 
234 	/**
235 	 * Put or replace a variable entry.
236 	 *
237 	 * @param name Variable name
238 	 * @param value Variable value
239 	 */
240 	public void putVariable(String name, Object value) {
241 		synchronized (variables) {
242 			variables.put(name, value);
243 		}
244 		markModified();
245 	}
246 
247 	/**
248 	 * Initial Field Separator (FS) value.
249 	 * <code>null</code> means the default FS value.
250 	 *
251 	 * @return the fieldSeparator
252 	 */
253 	public String getFieldSeparator() {
254 		return fieldSeparator;
255 	}
256 
257 	/**
258 	 * Initial Field Separator (FS) value.
259 	 * <code>null</code> means the default FS value.
260 	 *
261 	 * @param fieldSeparator the fieldSeparator to set
262 	 */
263 	public void setFieldSeparator(String fieldSeparator) {
264 		this.fieldSeparator = fieldSeparator;
265 		markModified();
266 	}
267 
268 	/**
269 	 * Whether to maintain array keys in sorted order;
270 	 * <code>false</code> by default.
271 	 *
272 	 * @return the useSortedArrayKeys
273 	 */
274 	public boolean isUseSortedArrayKeys() {
275 		return useSortedArrayKeys;
276 	}
277 
278 	/**
279 	 * Whether to maintain array keys in sorted order;
280 	 * <code>false</code> by default.
281 	 *
282 	 * @param useSortedArrayKeys the useSortedArrayKeys to set
283 	 */
284 	public void setUseSortedArrayKeys(boolean useSortedArrayKeys) {
285 		this.useSortedArrayKeys = useSortedArrayKeys;
286 		markModified();
287 	}
288 
289 	/**
290 	 * Whether POSIX compile-time behavior is enforced, rejecting gawk syntax
291 	 * such as arrays of arrays ({@code a[i][j]}, subarray operands like
292 	 * {@code split(..., a[i])}) and typed regexp literals ({@code @/re/}),
293 	 * and treating the gawk-specific {@code BEGINFILE} / {@code ENDFILE}
294 	 * patterns as ordinary identifiers.
295 	 *
296 	 * @return {@code true} when POSIX compile-time behavior is enforced
297 	 */
298 	public boolean isPosix() {
299 		return posix;
300 	}
301 
302 	/**
303 	 * Enables or disables POSIX compile-time behavior. When enabled, gawk
304 	 * syntax such as arrays of arrays ({@code a[i][j]}, subarray operands like
305 	 * {@code split(..., a[i])} or {@code for (k in a[i])}) and typed regexp
306 	 * literals ({@code @/re/}) is rejected, and the gawk-specific
307 	 * {@code BEGINFILE} / {@code ENDFILE} patterns are not special.
308 	 *
309 	 * @param posix {@code true} to enforce POSIX compile-time behavior
310 	 */
311 	public void setPosix(boolean posix) {
312 		this.posix = posix;
313 		markModified();
314 	}
315 
316 	/**
317 	 * <p>
318 	 * Getter for the field <code>locale</code>.
319 	 * </p>
320 	 *
321 	 * @return the Locale that will be used for outputting numbers
322 	 */
323 	public Locale getLocale() {
324 		return locale;
325 	}
326 
327 	/**
328 	 * Sets the Locale for outputting numbers.
329 	 *
330 	 * @param pLocale The locale to be used (e.g.: <code>Locale.US</code>)
331 	 */
332 	public void setLocale(Locale pLocale) {
333 		locale = pLocale == null ? Locale.US : pLocale;
334 		markModified();
335 	}
336 
337 	/**
338 	 * <p>
339 	 * Getter for the field <code>defaultRS</code>.
340 	 * </p>
341 	 *
342 	 * @return the default RS, when not set by the AWK script
343 	 */
344 	public String getDefaultRS() {
345 		return defaultRS;
346 	}
347 
348 	/**
349 	 * Sets the default RS, when not set by the AWK script
350 	 *
351 	 * @param rs The regular expression that separates records
352 	 */
353 	public void setDefaultRS(String rs) {
354 		defaultRS = Objects.requireNonNull(rs, "defaultRS");
355 		markModified();
356 	}
357 
358 	/**
359 	 * Records that this settings instance has been mutated, so that callers
360 	 * caching derived runtime state can detect it through
361 	 * {@link #getModificationCount()}.
362 	 * <p>
363 	 * Every mutator of this class calls it; subclasses adding their own
364 	 * configuration must do the same.
365 	 * </p>
366 	 */
367 	protected final void markModified() {
368 		modificationCount.incrementAndGet();
369 	}
370 
371 	private static final class ImmutableAwkSettings extends AwkSettings {
372 
373 		private ImmutableAwkSettings() {
374 			super();
375 		}
376 
377 		@Override
378 		public void setVariables(Map<String, Object> variables) {
379 			throw unsupported();
380 		}
381 
382 		@Override
383 		public void putVariable(String name, Object value) {
384 			throw unsupported();
385 		}
386 
387 		@Override
388 		public void setFieldSeparator(String fieldSeparator) {
389 			throw unsupported();
390 		}
391 
392 		@Override
393 		public void setUseSortedArrayKeys(boolean useSortedArrayKeys) {
394 			throw unsupported();
395 		}
396 
397 		@Override
398 		public void setPosix(boolean posix) {
399 			throw unsupported();
400 		}
401 
402 		@Override
403 		public void setLocale(Locale pLocale) {
404 			throw unsupported();
405 		}
406 
407 		@Override
408 		public void setDefaultRS(String rs) {
409 			throw unsupported();
410 		}
411 
412 		private UnsupportedOperationException unsupported() {
413 			return new UnsupportedOperationException("DEFAULT_SETTINGS is immutable");
414 		}
415 	}
416 }