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}