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.io.IOException;
26  import java.io.OutputStream;
27  import java.io.PrintStream;
28  import java.math.BigDecimal;
29  import java.util.Locale;
30  import org.metricshub.printf4j.Printf4J;
31  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
32  
33  /**
34   * Output target used by AWK {@code print} and {@code printf} statements.
35   * <p>
36   * Implementations decide how to represent AWK output, whether as text written
37   * to a stream, appended characters, or structured values collected by the
38   * embedding application. Numeric rendering uses the sink's immutable
39   * construction-time locale.
40   * </p>
41   */
42  public abstract class AwkSink {
43  
44  	private final Locale locale;
45  
46  	/**
47  	 * Creates a sink using the default {@link Locale#US} formatting rules.
48  	 */
49  	protected AwkSink() {
50  		this(Locale.US);
51  	}
52  
53  	/**
54  	 * Creates a sink using the supplied locale for numeric formatting.
55  	 *
56  	 * @param localeParam locale to use for numeric formatting
57  	 */
58  	protected AwkSink(Locale localeParam) {
59  		this.locale = localeParam == null ? Locale.US : localeParam;
60  	}
61  
62  	/**
63  	 * Returns the locale used by this sink when it renders numeric values.
64  	 *
65  	 * @return sink locale
66  	 */
67  	public final Locale getLocale() {
68  		return locale;
69  	}
70  
71  	/**
72  	 * Writes one AWK {@code print} operation.
73  	 *
74  	 * @param ofs output field separator
75  	 * @param ors output record separator
76  	 * @param ofmt numeric output format used by plain {@code print}
77  	 * @param values values supplied to {@code print}
78  	 * @throws IOException if the sink cannot write the output
79  	 */
80  	public abstract void print(String ofs, String ors, String ofmt, Object... values) throws IOException;
81  
82  	/**
83  	 * Writes one AWK {@code printf} operation.
84  	 *
85  	 * @param ofs output field separator
86  	 * @param ors output record separator
87  	 * @param ofmt numeric output format available to the sink
88  	 * @param format format string passed to {@code printf}
89  	 * @param values arguments supplied after the format string
90  	 * @throws IOException if the sink cannot write the output
91  	 */
92  	public abstract void printf(String ofs, String ors, String ofmt, String format, Object... values)
93  			throws IOException;
94  
95  	/**
96  	 * Flushes any buffered output held by this sink.
97  	 *
98  	 * @throws IOException if the sink cannot be flushed
99  	 */
100 	public void flush() throws IOException {
101 		// Most sinks do not buffer explicitly.
102 	}
103 
104 	/**
105 	 * Returns a {@link PrintStream} view that receives raw process output written
106 	 * by spawned commands such as {@code system("...")}.
107 	 * <p>
108 	 * The default implementation returns a stream that silently discards all
109 	 * output. Override this method in sinks that need to capture process output.
110 	 * </p>
111 	 *
112 	 * @return print stream that should receive raw process output
113 	 */
114 	@SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "The shared discard stream is stateless and safe to expose.")
115 	public PrintStream getPrintStream() {
116 		return NULL_PRINT_STREAM;
117 	}
118 
119 	/** Shared discard stream returned by the default {@link #getPrintStream()}. */
120 	private static final PrintStream NULL_PRINT_STREAM = newNullPrintStream();
121 
122 	/**
123 	 * A shared no-op sink that silently discards all output.
124 	 * <p>
125 	 * This singleton is safe to share across all JRT/AVM instances because
126 	 * its {@link #print(String, String, String, Object...)},
127 	 * {@link #printf(String, String, String, String, Object...)}, and
128 	 * {@link #flush()} operations are all no-ops.
129 	 */
130 	public static final AwkSink NOP_SINK = new NoOpAwkSink();
131 
132 	private static final class NoOpAwkSink extends AwkSink {
133 
134 		NoOpAwkSink() {
135 			super();
136 		}
137 
138 		@Override
139 		public void print(String ofs, String ors, String ofmt, Object... values) {
140 			// discard
141 		}
142 
143 		@Override
144 		public void printf(String ofs, String ors, String ofmt, String format, Object... values) {
145 			// discard
146 		}
147 	}
148 
149 	private static PrintStream newNullPrintStream() {
150 		try {
151 			return new PrintStream(
152 					new OutputStream() {
153 						@Override
154 						public void write(int b) {
155 							// discard
156 						}
157 
158 						@Override
159 						public void write(byte[] b, int off, int len) {
160 							// discard
161 						}
162 					},
163 					false,
164 					"UTF-8") {
165 
166 				@Override
167 				public void close() {
168 					// Prevent closing; this stream is a shared singleton.
169 				}
170 			};
171 		} catch (java.io.UnsupportedEncodingException e) {
172 			throw new IllegalStateException(e);
173 		}
174 	}
175 
176 	/**
177 	 * Creates a sink backed by an {@link OutputStream}.
178 	 *
179 	 * @param outputStream stream that should receive AWK output
180 	 * @return sink writing to {@code outputStream}
181 	 */
182 	public static AwkSink from(OutputStream outputStream) {
183 		return from(outputStream, Locale.US);
184 	}
185 
186 	/**
187 	 * Creates a sink backed by an {@link OutputStream}.
188 	 *
189 	 * @param outputStream stream that should receive AWK output
190 	 * @param locale locale to use for numeric formatting
191 	 * @return sink writing to {@code outputStream}
192 	 */
193 	public static AwkSink from(OutputStream outputStream, Locale locale) {
194 		return new OutputStreamAwkSink(outputStream, locale);
195 	}
196 
197 	/**
198 	 * Creates a sink backed by a {@link PrintStream}.
199 	 *
200 	 * @param printStream stream that should receive AWK output
201 	 * @return sink writing to {@code printStream}
202 	 */
203 	public static AwkSink from(PrintStream printStream) {
204 		return from(printStream, Locale.US);
205 	}
206 
207 	/**
208 	 * Creates a sink backed by a {@link PrintStream}.
209 	 *
210 	 * @param printStream stream that should receive AWK output
211 	 * @param locale locale to use for numeric formatting
212 	 * @return sink writing to {@code printStream}
213 	 */
214 	public static AwkSink from(PrintStream printStream, Locale locale) {
215 		return new OutputStreamAwkSink(printStream, locale);
216 	}
217 
218 	/**
219 	 * Creates a sink backed by an {@link Appendable}.
220 	 *
221 	 * @param appendable appendable that should receive AWK output
222 	 * @return sink writing to {@code appendable}
223 	 */
224 	public static AwkSink from(Appendable appendable) {
225 		return from(appendable, Locale.US);
226 	}
227 
228 	/**
229 	 * Creates a sink backed by an {@link Appendable}.
230 	 *
231 	 * @param appendable appendable that should receive AWK output
232 	 * @param locale locale to use for numeric formatting
233 	 * @return sink writing to {@code appendable}
234 	 */
235 	public static AwkSink from(Appendable appendable, Locale locale) {
236 		return new AppendableAwkSink(appendable, locale);
237 	}
238 
239 	/**
240 	 * Formats one operand of a plain AWK {@code print} statement.
241 	 * <p>
242 	 * AWK applies {@code OFMT} not only to numeric values, but also to strings
243 	 * that can be interpreted numerically. This helper preserves that behaviour
244 	 * for text-based sinks.
245 	 * </p>
246 	 *
247 	 * @param value operand to format
248 	 * @param ofmt numeric output format
249 	 * @return the textual representation AWK would print for this operand
250 	 */
251 	protected final String formatPrintArgument(Object value, String ofmt) {
252 		return formatOutputValue(normalizePrintArgument(value), ofmt, locale);
253 	}
254 
255 	/**
256 	 * Formats a string in the same way as AWK's {@code sprintf()} built-in.
257 	 * <p>
258 	 * Subclasses may override this method to customize formatting. The default
259 	 * implementation delegates to {@link Printf4J#sprintf(Locale, String, Object...)}.
260 	 * Because {@link #printf(String, String, String, String, Object...)} uses this
261 	 * method internally, overriding it ensures that both {@code printf} and
262 	 * {@code sprintf} produce consistent output.
263 	 * </p>
264 	 *
265 	 * @param format format string
266 	 * @param values arguments supplied after the format string
267 	 * @return formatted text
268 	 */
269 	public String sprintf(String format, Object... values) {
270 		Object[] safeValues = values == null ? new Object[0] : values;
271 		return Printf4J.sprintf(locale, format, safeValues);
272 	}
273 
274 	/**
275 	 * Formats one {@code printf} result string using this sink's locale.
276 	 *
277 	 * @param format format string passed to {@code printf}
278 	 * @param values arguments supplied after the format string
279 	 * @return formatted text
280 	 */
281 	protected final String formatPrintfResult(String format, Object... values) {
282 		return sprintf(format, values);
283 	}
284 
285 	/**
286 	 * Converts a {@code print} operand into the value shape AWK uses before it
287 	 * applies {@code OFMT}.
288 	 * <p>
289 	 * When a non-numeric object renders as a numeric string, AWK treats it as a
290 	 * number for plain {@code print}. Structured sinks can reuse this helper when
291 	 * they want text output compatible with standard AWK behaviour.
292 	 * </p>
293 	 *
294 	 * @param value operand to normalize
295 	 * @return the normalized value, either unchanged or converted to a numeric form
296 	 */
297 	protected final Object normalizePrintArgument(Object value) {
298 		if (value == null || value instanceof Number) {
299 			return value;
300 		}
301 		try {
302 			return Double.valueOf(new BigDecimal(value.toString()).doubleValue());
303 		} catch (NumberFormatException e) {
304 			return value;
305 		}
306 	}
307 
308 	/**
309 	 * Formats one already-normalized AWK output value.
310 	 *
311 	 * @param value value to format
312 	 * @param ofmt numeric output format
313 	 * @param locale locale used for numeric formatting
314 	 * @return textual output for {@code value}
315 	 */
316 	public static String formatOutputValue(Object value, String ofmt, Locale locale) {
317 		if (value == null) {
318 			return "";
319 		}
320 		if (!(value instanceof Number)) {
321 			return value.toString();
322 		}
323 
324 		double number = ((Number) value).doubleValue();
325 		if (JRT.isActuallyLong(number)) {
326 			return Long.toString((long) Math.rint(number));
327 		}
328 
329 		try {
330 			String rendered = String.format(locale, ofmt, number);
331 			if ((rendered.indexOf('.') > -1 || rendered.indexOf(',') > -1)
332 					&& rendered.indexOf('e') == -1
333 					&& rendered.indexOf('E') == -1) {
334 				while (rendered.endsWith("0")) {
335 					rendered = rendered.substring(0, rendered.length() - 1);
336 				}
337 				if (rendered.endsWith(".") || rendered.endsWith(",")) {
338 					rendered = rendered.substring(0, rendered.length() - 1);
339 				}
340 			}
341 			return rendered;
342 		} catch (java.util.UnknownFormatConversionException e) {
343 			return "";
344 		}
345 	}
346 }