001package io.jstach.jstachio;
002
003import java.io.DataOutput;
004import java.io.IOException;
005import java.io.OutputStream;
006import java.nio.charset.Charset;
007
008import org.eclipse.jdt.annotation.Nullable;
009
010import io.jstach.jstachio.Output.CloseableEncodedOutput;
011
012/**
013 * A low level abstraction and implementation detail analogous to {@link Appendable} and
014 * {@link DataOutput}.
015 *
016 * @author agentgt
017 * @param <E> the exception type that can happen on output
018 */
019public interface Output<E extends Exception> {
020
021        /**
022         * Analogous to {@link Appendable#append(CharSequence)}.
023         * @param s unlike appendable always non null.
024         * @throws E if an error happens while writting to the appendable
025         * @apiNote Implementations are required to implement this method.
026         */
027        public void append(CharSequence s) throws E;
028
029        /**
030         * Analogous to {@link Appendable#append(CharSequence)} which by default treats the
031         * String as a CharSequence.
032         * @param s unlike appendable always non null.
033         * @throws E if an error happens while writting to the appendable
034         * @apiNote Implementations are required to implement this method.
035         */
036        default void append(String s) throws E {
037                append((CharSequence) s);
038        }
039
040        /**
041         * Analogous to {@link Appendable#append(CharSequence, int, int)}.
042         * @param csq Unlike appendable never null.
043         * @param start start inclusive
044         * @param end end exclusive
045         * @throws E if an error happens while writting to the appendable
046         * @apiNote Implementations are required to implement this method.
047         */
048        public void append(CharSequence csq, int start, int end) throws E;
049
050        /**
051         * Appends a character to an appendable.
052         * @param c character
053         * @throws E if an error happens while writting to the appendable
054         * @apiNote Implementations are required to implement this method.
055         */
056
057        public void append(char c) throws E;
058
059        /**
060         * Write a short by using {@link String#valueOf(int)}
061         * @param s short
062         * @throws E if an error happens while writting to the appendable
063         */
064        default void append(short s) throws E {
065                append(String.valueOf(s));
066        }
067
068        /**
069         * Write a int by using {@link String#valueOf(int)}.
070         * <p>
071         * Implementations should override if they want different behavior or able to support
072         * appendables that can write the native type.
073         * @param i int
074         * @throws E if an error happens while writting to the appendable
075         */
076        default void append(int i) throws E {
077                append(String.valueOf(i));
078        }
079
080        /**
081         * Write a long by using {@link String#valueOf(long)}.
082         * <p>
083         * Implementations should override if they want different behavior or able to support
084         * appendables that can write the native type.
085         * @param l long
086         * @throws E if an error happens while writting to the appendable
087         */
088        default void append(long l) throws E {
089                append(String.valueOf(l));
090        }
091
092        /**
093         * Write a long by using {@link String#valueOf(long)}.
094         * <p>
095         * Implementations should override if they want different behavior or able to support
096         * appendables that can write the native type.
097         * @param d double
098         * @throws E if an error happens while writting to the appendable
099         */
100        default void append(double d) throws E {
101                append(String.valueOf(d));
102        }
103
104        /**
105         * Write a long by using {@link String#valueOf(long)}.
106         * <p>
107         * Implementations should override if they want different behavior or able to support
108         * appendables that can write the native type.
109         * @param b boolean
110         * @throws E if an error happens while writting to the appendable
111         */
112        default void append(boolean b) throws E {
113                append(String.valueOf(b));
114        }
115
116        /**
117         * Adapts an {@link OutputStream} as an {@link Output}.
118         * @param a the OutputStream to be wrapped.
119         * @param charset the encoding to use
120         * @return outputstream output
121         */
122        public static EncodedOutput<IOException> of(OutputStream a, Charset charset) {
123                return new OutputStreamOutput(a, charset);
124        }
125
126        /**
127         * Adapts an {@link Appendable} as an {@link Output}.
128         * @param a the appendable to be wrapped.
129         * @return string based output
130         */
131        public static Output<IOException> of(Appendable a) {
132                return new AppendableOutput(a);
133        }
134
135        /**
136         * Adapts a {@link StringBuilder} as an {@link Output}.
137         * @param a the StringBuilder to be wrapped.
138         * @return string based output
139         */
140        public static StringOutput of(StringBuilder a) {
141                return new StringOutput(a);
142        }
143
144        /**
145         * Converts the output to an appendable unless it already is one.
146         * @return adapted appendable of this output.
147         */
148        default Appendable toAppendable() {
149                if (this instanceof Appendable a) {
150                        return a;
151                }
152                return new OutputAppendable(this);
153        }
154
155        /**
156         * A specialized Output designed for pre-encoded templates that have already encoded
157         * byte arrays to be used directly.
158         * <p>
159         * Most template engines traditionally write to an Appendable and as they write to the
160         * appendable in a web framework the output is eventually converted to bytes in almost
161         * always <code>UTF-8</code> format. Given that a large majority of templates is
162         * static text this encoding can be done apriori which saves some processing time
163         * especially if the text contains any non latin1 characters.
164         *
165         * @author agentgt
166         * @param <E> the exception type
167         */
168        public interface EncodedOutput<E extends Exception> extends Output<E> {
169
170                /**
171                 * Analogous to {@link OutputStream#write(byte[])}. Implementations should not
172                 * alter the byte array.
173                 * @param bytes already encoded bytes
174                 * @throws E if an error happens
175                 */
176                public void write(byte[] bytes) throws E;
177
178                /**
179                 * Analogous to {@link OutputStream#write(byte[], int, int)}. Generated templates
180                 * do not call this method as great care as to be taken to preserve the encoding.
181                 * It is only provided in the case of future found optimizations and is not
182                 * currently required.
183                 * <p>
184                 * The default implementation creates an array copies the data and then calls
185                 * {@link #write(byte[])}.
186                 * @param bytes already encoded bytes
187                 * @param off offset
188                 * @param len length to copy
189                 * @throws E if an error happens
190                 */
191                default void write(byte[] bytes, int off, int len) throws E {
192                        byte[] dest = new byte[len];
193                        System.arraycopy(bytes, off, dest, 0, len);
194                        write(dest);
195                }
196
197                @Override
198                default void append(char c) throws E {
199                        append(String.valueOf(c));
200                }
201
202                @Override
203                default void append(String s) throws E {
204                        write(s.getBytes(charset()));
205                }
206
207                @Override
208                default void append(CharSequence csq, int start, int end) throws E {
209                        append(csq.subSequence(start, end).toString());
210                }
211
212                /**
213                 * The charset that the encoded output <em>should</em> be.
214                 * @return expected charset
215                 */
216                Charset charset();
217
218                /**
219                 * Adapts an {@link OutputStream} as an {@link EncodedOutput}. The resulting
220                 * output can be closed and will close the passed in OutputStream.
221                 * @param a the OutputStream to be wrapped.
222                 * @param charset the encoding to use
223                 * @return outputstream output
224                 */
225                public static CloseableEncodedOutput<IOException> of(OutputStream a, Charset charset) {
226                        return new OutputStreamOutput(a, charset);
227                }
228
229        }
230
231        /**
232         * An encoded output that can be closed. This maybe to close downstream outputstreams
233         * or to signify ready for reuse or to clear buffers.
234         *
235         * @author agent
236         * @param <E> error on close
237         */
238        public interface CloseableEncodedOutput<E extends Exception> extends EncodedOutput<E>, AutoCloseable {
239
240                @Override
241                public void close() throws E;
242
243        }
244
245        /**
246         * String Builder based output.
247         *
248         * @author agentgt
249         *
250         */
251        public class StringOutput implements Output<RuntimeException> {
252
253                private final StringBuilder buffer;
254
255                /**
256                 * Create using supplied StringBuilder.
257                 * @param buffer never null.
258                 */
259                public StringOutput(StringBuilder buffer) {
260                        super();
261                        this.buffer = buffer;
262                }
263
264                @Override
265                public void append(CharSequence s) {
266                        buffer.append(s);
267                }
268
269                @Override
270                public void append(String s) {
271                        buffer.append(s);
272                }
273
274                @Override
275                public void append(CharSequence csq, int start, int end) {
276                        buffer.append(csq, start, end);
277
278                }
279
280                @Override
281                public void append(char c) {
282                        buffer.append(c);
283
284                }
285
286                @Override
287                public String toString() {
288                        return buffer.toString();
289                }
290
291                @Override
292                public void append(boolean b) throws RuntimeException {
293                        buffer.append(b);
294                }
295
296                @Override
297                public void append(double d) throws RuntimeException {
298                        buffer.append(d);
299                }
300
301                @Override
302                public void append(int i) throws RuntimeException {
303                        buffer.append(i);
304                }
305
306                @Override
307                public void append(long l) throws RuntimeException {
308                        buffer.append(l);
309                }
310
311                @Override
312                public void append(short s) throws RuntimeException {
313                        buffer.append(s);
314                }
315
316                /**
317                 * The buffer that has been wrapped.
318                 * @return the wrapped builder
319                 */
320                public StringBuilder getBuffer() {
321                        return buffer;
322                }
323
324        }
325
326}
327
328class OutputAppendable implements Appendable {
329
330        private final Output<?> output;
331
332        public OutputAppendable(Output<?> output) {
333                super();
334                this.output = output;
335        }
336
337        @Override
338        public Appendable append(@Nullable CharSequence csq) throws IOException {
339                try {
340                        output.append(csq == null ? "null" : csq);
341                        return this;
342                }
343                catch (Exception e) {
344                        if (e instanceof IOException ioe) {
345                                throw ioe;
346                        }
347                        throw new IOException(e);
348                }
349        }
350
351        @Override
352        public Appendable append(@Nullable CharSequence csq, int start, int end) throws IOException {
353                try {
354                        output.append(csq == null ? " null " : csq, start, end);
355                        return this;
356                }
357                catch (Exception e) {
358                        if (e instanceof IOException ioe) {
359                                throw ioe;
360                        }
361                        throw new IOException(e);
362                }
363        }
364
365        @Override
366        public Appendable append(char c) throws IOException {
367                try {
368                        output.append(c);
369                        return this;
370                }
371                catch (Exception e) {
372                        if (e instanceof IOException ioe) {
373                                throw ioe;
374                        }
375                        throw new IOException(e);
376                }
377        }
378
379}
380
381class AppendableOutput implements Output<IOException> {
382
383        private final Appendable appendable;
384
385        public AppendableOutput(Appendable appendable) {
386                super();
387                this.appendable = appendable;
388        }
389
390        @Override
391        public void append(CharSequence s) throws IOException {
392                appendable.append(s);
393        }
394
395        @Override
396        public void append(CharSequence csq, int start, int end) throws IOException {
397                appendable.append(csq, start, end);
398
399        }
400
401        @Override
402        public void append(char c) throws IOException {
403                appendable.append(c);
404
405        }
406
407}
408
409class OutputStreamOutput implements CloseableEncodedOutput<IOException> {
410
411        private final OutputStream outputStream;
412
413        private final Charset charset;
414
415        public OutputStreamOutput(OutputStream outputStream, Charset charset) {
416                super();
417                this.outputStream = outputStream;
418                this.charset = charset;
419        }
420
421        @Override
422        public void write(byte[] b) throws IOException {
423                outputStream.write(b);
424        }
425
426        @Override
427        public void write(byte[] bytes, int off, int len) throws IOException {
428                outputStream.write(bytes, off, len);
429        }
430
431        @Override
432        public void append(CharSequence csq) throws IOException {
433                append(csq.toString());
434        }
435
436        @Override
437        public void append(String s) throws IOException {
438                write(s.getBytes(charset));
439        }
440
441        @Override
442        public Charset charset() {
443                return charset;
444        }
445
446        @Override
447        public void close() throws IOException {
448                outputStream.close();
449        }
450
451}