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 	 * Numeric values are rendered with {@code OFMT} (or as integers when they
243 	 * hold an integral value). String values — including numeric strings that
244 	 * originate from input — are printed verbatim, as required by POSIX.
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(value, ofmt, locale);
253 	}
254 
255 	/**
256 	 * Converts a {@code print} operand that renders as a numeric string into a
257 	 * numeric form.
258 	 * <p>
259 	 * Historical versions of Jawk applied this conversion in plain {@code print},
260 	 * which is not what POSIX AWK does: {@code print} outputs string values
261 	 * verbatim, and only actual numbers are formatted with {@code OFMT}. The
262 	 * built-in sinks therefore no longer call this helper; it is preserved only
263 	 * for compatibility with custom sinks compiled against earlier releases.
264 	 * </p>
265 	 *
266 	 * @param value operand to normalize
267 	 * @return the normalized value, either unchanged or converted to a numeric form
268 	 * @deprecated Plain {@code print} does not numerically coerce string values;
269 	 *             use the operand as-is, or {@link #formatPrintArgument(Object, String)}
270 	 *             to render it the way {@code print} would.
271 	 */
272 	@Deprecated
273 	protected final Object normalizePrintArgument(Object value) {
274 		if (value == null || value instanceof Number) {
275 			return value;
276 		}
277 		try {
278 			return Double.valueOf(new BigDecimal(value.toString()).doubleValue());
279 		} catch (NumberFormatException e) {
280 			return value;
281 		}
282 	}
283 
284 	/**
285 	 * Formats a string in the same way as AWK's {@code sprintf()} built-in.
286 	 * <p>
287 	 * Subclasses may override this method to customize formatting. The default
288 	 * implementation delegates to {@link Printf4J#sprintf(Locale, String, Object...)}.
289 	 * Because {@link #printf(String, String, String, String, Object...)} uses this
290 	 * method internally, overriding it ensures that both {@code printf} and
291 	 * {@code sprintf} produce consistent output.
292 	 * </p>
293 	 *
294 	 * @param format format string
295 	 * @param values arguments supplied after the format string
296 	 * @return formatted text
297 	 */
298 	public String sprintf(String format, Object... values) {
299 		Object[] safeValues = values == null ? new Object[0] : values;
300 		return Printf4J.sprintf(locale, format, safeValues);
301 	}
302 
303 	/**
304 	 * Formats one {@code printf} result string using this sink's locale.
305 	 *
306 	 * @param format format string passed to {@code printf}
307 	 * @param values arguments supplied after the format string
308 	 * @return formatted text
309 	 */
310 	protected final String formatPrintfResult(String format, Object... values) {
311 		return sprintf(format, values);
312 	}
313 
314 	/**
315 	 * Formats one already-normalized AWK output value.
316 	 *
317 	 * @param value value to format
318 	 * @param ofmt numeric output format
319 	 * @param locale locale used for numeric formatting
320 	 * @return textual output for {@code value}
321 	 */
322 	public static String formatOutputValue(Object value, String ofmt, Locale locale) {
323 		if (value == null) {
324 			return "";
325 		}
326 		if (!(value instanceof Number)) {
327 			return value.toString();
328 		}
329 
330 		double number = ((Number) value).doubleValue();
331 		if (JRT.isActuallyLong(number)) {
332 			return Long.toString((long) Math.rint(number));
333 		}
334 
335 		try {
336 			String rendered = String.format(locale, ofmt, number);
337 			if ((rendered.indexOf('.') > -1 || rendered.indexOf(',') > -1)
338 					&& rendered.indexOf('e') == -1
339 					&& rendered.indexOf('E') == -1) {
340 				while (rendered.endsWith("0")) {
341 					rendered = rendered.substring(0, rendered.length() - 1);
342 				}
343 				if (rendered.endsWith(".") || rendered.endsWith(",")) {
344 					rendered = rendered.substring(0, rendered.length() - 1);
345 				}
346 			}
347 			return rendered;
348 		} catch (java.util.UnknownFormatConversionException e) {
349 			return "";
350 		}
351 	}
352 }