diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/BufferedOutStream.java b/avaje-jex/src/main/java/io/avaje/jex/core/BufferedOutStream.java index 48bbe9fd..43cffd30 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/BufferedOutStream.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/BufferedOutStream.java @@ -15,7 +15,6 @@ final class BufferedOutStream extends OutputStream { private long count; BufferedOutStream(JdkContext context, int initial, long max) { - this.context = context; this.max = max; this.buffer = new ByteArrayOutputStream(initial); diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/JdkContext.java b/avaje-jex/src/main/java/io/avaje/jex/core/JdkContext.java index 7e73622f..8f7d9d4c 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/JdkContext.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/JdkContext.java @@ -503,6 +503,16 @@ public void write(byte[] bytes) { } } + @Override + public void write(byte[] bufferBytes, int length) { + try (var os = exchange.getResponseBody()) { + exchange.sendResponseHeaders(statusCode(), length == 0 ? -1 : length); + os.write(bufferBytes, 0, length); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + @Override public void write(InputStream is) { try (is; var os = outputStream()) { diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/json/JsonbOutput.java b/avaje-jex/src/main/java/io/avaje/jex/core/json/JsonbOutput.java new file mode 100644 index 00000000..49623f15 --- /dev/null +++ b/avaje-jex/src/main/java/io/avaje/jex/core/json/JsonbOutput.java @@ -0,0 +1,64 @@ +package io.avaje.jex.core.json; + +import io.avaje.jex.http.Context; +import io.avaje.json.stream.JsonOutput; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * avaje-jsonb output that allows for writing fixed length content + * straight from the avaje-jsonb buffer, avoiding the jex side buffer. + */ +public final class JsonbOutput implements JsonOutput { + + private final Context context; + private OutputStream os; + + public static JsonOutput of(Context context) { + return new JsonbOutput(context); + } + + private JsonbOutput(Context context) { + this.context = context; + } + + @Override + public void write(byte[] content, int offset, int length) throws IOException { + if (os == null) { + // exceeds the avaje-jsonb buffer size + os = context.outputStream(); + } + os.write(content, offset, length); + } + + @Override + public void writeLast(byte[] content, int offset, int length) throws IOException { + if (os == null) { + // write as fixed length content straight from the avaje-jsonb buffer + context.write(content, length); + } else { + os.write(content, offset, length); + } + } + + @Override + public void flush() throws IOException { + if (os != null) { + os.flush(); + } + } + + @Override + public void close() throws IOException { + if (os != null) { + os.close(); + } + } + + @Override + public OutputStream unwrapOutputStream() { + return context.outputStream(); + } + +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/http/Context.java b/avaje-jex/src/main/java/io/avaje/jex/http/Context.java index e7af6a64..6ce09f8f 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/http/Context.java +++ b/avaje-jex/src/main/java/io/avaje/jex/http/Context.java @@ -449,6 +449,16 @@ default String userAgent() { */ void write(byte[] bytes); + /** + * Writes the first bytes from this buffer directly to the response. + * + *
The bytes written will be from position 0 to length. + * + * @param bufferBytes The byte array to write. + * @param length The number of bytes to write from the buffer. + */ + void write(byte[] bufferBytes, int length); + /** * Writes the content from the given InputStream directly to the response body. * diff --git a/avaje-jex/src/test/java/io/avaje/jex/core/JsonTest.java b/avaje-jex/src/test/java/io/avaje/jex/core/JsonTest.java index 99c20ede..66fc947c 100644 --- a/avaje-jex/src/test/java/io/avaje/jex/core/JsonTest.java +++ b/avaje-jex/src/test/java/io/avaje/jex/core/JsonTest.java @@ -11,6 +11,7 @@ import java.util.concurrent.locks.LockSupport; import java.util.stream.Stream; +import io.avaje.jex.core.json.JsonbOutput; import io.avaje.jsonb.Json; import io.avaje.jsonb.JsonType; import io.avaje.jsonb.Jsonb; @@ -58,6 +59,13 @@ static TestPair init() { var result = HelloDto.rob(); jsonTypeHelloDto.toJson(result, ctx.outputStream()); }) + .get( + "/usingJsonOutput", + ctx -> { + ctx.status(200).contentType("application/json"); + var result = HelloDto.fi(); + jsonTypeHelloDto.toJson(result, JsonbOutput.of(ctx)); + }) .get("/iterate", ctx -> ctx.jsonStream(ITERATOR)) .get("/stream", ctx -> ctx.jsonStream(HELLO_BEANS.stream())) .post("/", ctx -> ctx.text("bean[" + ctx.bodyAsClass(HelloDto.class) + "]")); @@ -118,6 +126,21 @@ void usingOutputStream() { assertThat(bean.name).isEqualTo("rob"); } + @Test + void usingJsonOutput() { + var hres = pair.request().path("usingJsonOutput") + .GET() + .as(HelloDto.class); + + assertThat(hres.statusCode()).isEqualTo(200); + final HttpHeaders headers = hres.headers(); + assertThat(headers.firstValue("Content-Type").orElseThrow()).isEqualTo("application/json"); + + var bean = hres.body(); + assertThat(bean.id).isEqualTo(45); + assertThat(bean.name).isEqualTo("fi"); + } + @Test void stream_viaIterator() {