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 }