diff --git a/http-api/src/main/java/io/avaje/http/api/StreamingOutput.java b/http-api/src/main/java/io/avaje/http/api/StreamingOutput.java new file mode 100644 index 00000000..31cf16a7 --- /dev/null +++ b/http-api/src/main/java/io/avaje/http/api/StreamingOutput.java @@ -0,0 +1,24 @@ +package io.avaje.http.api; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * An avaje {@link Controller} endpoint is able to use an instance of this interface + * as a return type. + * + *
{@code
+ * @Post("some_endpoint")
+ * @Produces("application/octet-stream")
+ * public StreamingOutput thumbnail(
+ *   InputStream inputStream,
+ *   @QueryParam(Constants.KEY_SIZE) @Min(1) @Max(Constants.MAX_SIZE) Integer size
+ * ) throws IOException {
+ *   return (os) -> os.write(new byte[] { 0x01, 0x02, 0x03 });
+ * }
+ * }
+ */ + +public interface StreamingOutput { + void write(OutputStream outputStream) throws IOException; +} diff --git a/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java b/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java index 1a12143a..803a1dcf 100644 --- a/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java +++ b/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java @@ -107,6 +107,7 @@ enum ResponseMode { Jstachio, Templating, InputStream, + StreamingOutput, Other } @@ -117,6 +118,9 @@ ResponseMode responseMode() { if (isInputStream(method.returnType())) { return ResponseMode.InputStream; } + if (isStreamingOutput(method.returnType())) { + return ResponseMode.StreamingOutput; + } if (producesJson()) { return ResponseMode.Json; } @@ -136,6 +140,10 @@ private boolean isInputStream(TypeMirror type) { return isAssignable2Interface(type.toString(), "java.io.InputStream"); } + private boolean isStreamingOutput(TypeMirror type) { + return isAssignable2Interface(type.toString(), "io.avaje.http.api.StreamingOutput"); + } + private boolean producesJson() { return !"byte[]".equals(method.returnType().toString()) && !useJstachio @@ -294,11 +302,19 @@ private void writeContextReturn(ResponseMode responseMode, String resultVariable case Json -> writeJsonReturn(produces, indent); case Text -> writer.append("ctx.text(%s);", resultVariable); case Templating -> writer.append("ctx.html(%s);", resultVariable); + case StreamingOutput -> writeStreamingOutputReturn(produces, resultVariable, indent); default -> writer.append("ctx.contentType(\"%s\").write(%s);", produces, resultVariable); } writer.eol(); } + private void writeStreamingOutputReturn(String produces, String resultVariable, String indent) { + writer.append("ctx.contentType(\"%s\");", produces).eol(); + writer.append(indent).append("try (java.io.OutputStream ctxOutputStream = ctx.outputStream()) {").eol(); + writer.append(indent).append(indent).append("%s.write(ctxOutputStream);", resultVariable).eol(); + writer.append(indent).append("}", resultVariable); + } + private void writeJsonReturn(String produces, String indent) { var uType = UType.parse(method.returnType()); boolean streaming = useJsonB && streamingContent(uType); diff --git a/tests/test-javalin-jsonb/src/main/resources/public/openapi.json b/tests/test-javalin-jsonb/src/main/resources/public/openapi.json index 11c8d142..34818240 100644 --- a/tests/test-javalin-jsonb/src/main/resources/public/openapi.json +++ b/tests/test-javalin-jsonb/src/main/resources/public/openapi.json @@ -520,7 +520,23 @@ "content" : { "application/x-www-form-urlencoded" : { "schema" : { - "$ref" : "#/components/schemas/HelloForm" + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "nullable" : false + }, + "email" : { + "type" : "string" + }, + "url" : { + "type" : "string" + }, + "startDate" : { + "type" : "string", + "format" : "date" + } + } } } }, @@ -579,7 +595,23 @@ "content" : { "application/x-www-form-urlencoded" : { "schema" : { - "$ref" : "#/components/schemas/HelloForm" + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "nullable" : false + }, + "email" : { + "type" : "string" + }, + "url" : { + "type" : "string" + }, + "startDate" : { + "type" : "string", + "format" : "date" + } + } } } }, @@ -1029,6 +1061,57 @@ } } }, + "/openapi/form" : { + "post" : { + "tags" : [ + + ], + "summary" : "", + "description" : "", + "parameters" : [ + { + "name" : "Head-String", + "in" : "header", + "schema" : { + "type" : "string" + } + } + ], + "requestBody" : { + "content" : { + "application/x-www-form-urlencoded" : { + "schema" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + }, + "email" : { + "type" : "string" + }, + "url" : { + "type" : "string" + } + } + } + } + }, + "required" : true + }, + "responses" : { + "201" : { + "description" : "", + "content" : { + "application/json" : { + "schema" : { + "type" : "string" + } + } + } + } + } + } + }, "/openapi/get" : { "get" : { "tags" : [ @@ -1653,11 +1736,31 @@ ], "summary" : "", "description" : "", + "parameters" : [ + { + "name" : "Head-String", + "in" : "header", + "schema" : { + "type" : "string" + } + } + ], "requestBody" : { "content" : { "application/x-www-form-urlencoded" : { "schema" : { - "$ref" : "#/components/schemas/MyForm" + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + }, + "email" : { + "type" : "string" + }, + "url" : { + "type" : "string" + } + } } } }, @@ -2241,42 +2344,6 @@ } } }, - "HelloForm" : { - "required" : [ - "name" - ], - "type" : "object", - "properties" : { - "name" : { - "type" : "string", - "nullable" : false - }, - "email" : { - "type" : "string" - }, - "url" : { - "type" : "string" - }, - "startDate" : { - "type" : "string", - "format" : "date" - } - } - }, - "MyForm" : { - "type" : "object", - "properties" : { - "name" : { - "type" : "string" - }, - "email" : { - "type" : "string" - }, - "url" : { - "type" : "string" - } - } - }, "Person" : { "type" : "object", "properties" : { diff --git a/tests/test-javalin/src/main/resources/public/openapi.json b/tests/test-javalin/src/main/resources/public/openapi.json index cf36d6a3..28081a57 100644 --- a/tests/test-javalin/src/main/resources/public/openapi.json +++ b/tests/test-javalin/src/main/resources/public/openapi.json @@ -490,7 +490,23 @@ "content" : { "application/x-www-form-urlencoded" : { "schema" : { - "$ref" : "#/components/schemas/HelloForm" + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "nullable" : false + }, + "email" : { + "type" : "string" + }, + "url" : { + "type" : "string" + }, + "startDate" : { + "type" : "string", + "format" : "date" + } + } } } }, @@ -549,7 +565,23 @@ "content" : { "application/x-www-form-urlencoded" : { "schema" : { - "$ref" : "#/components/schemas/HelloForm" + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "nullable" : false + }, + "email" : { + "type" : "string" + }, + "url" : { + "type" : "string" + }, + "startDate" : { + "type" : "string", + "format" : "date" + } + } } } }, @@ -874,28 +906,6 @@ "format" : "date-time" } } - }, - "HelloForm" : { - "required" : [ - "name" - ], - "type" : "object", - "properties" : { - "name" : { - "type" : "string", - "nullable" : false - }, - "email" : { - "type" : "string" - }, - "url" : { - "type" : "string" - }, - "startDate" : { - "type" : "string", - "format" : "date" - } - } } } } diff --git a/tests/test-jex/src/main/java/org/example/web/HelloController.java b/tests/test-jex/src/main/java/org/example/web/HelloController.java index f3b892ed..fdb3c65e 100644 --- a/tests/test-jex/src/main/java/org/example/web/HelloController.java +++ b/tests/test-jex/src/main/java/org/example/web/HelloController.java @@ -10,6 +10,7 @@ import io.avaje.http.api.Produces; import io.avaje.http.api.Put; import io.avaje.http.api.Valid; +import io.avaje.http.api.StreamingOutput; import io.avaje.jex.http.Context; // @Roles(AppRoles.BASIC_USER) @@ -77,4 +78,12 @@ String testBigInt(BigInteger val) { String rawJsonString() { return "{\"key\": 42 }"; } + + @Get("streamBytes") + @Produces(value = "text/html", statusCode = 200) + StreamingOutput streamBytes() { + return outputStream -> outputStream.write(new byte[]{ + 0x41, 0x76, 0x61, 0x6a, 0x65 + }); + } } diff --git a/tests/test-jex/src/main/resources/public/openapi.json b/tests/test-jex/src/main/resources/public/openapi.json index 8017f8b3..3789d95d 100644 --- a/tests/test-jex/src/main/resources/public/openapi.json +++ b/tests/test-jex/src/main/resources/public/openapi.json @@ -590,7 +590,23 @@ "content" : { "application/x-www-form-urlencoded" : { "schema" : { - "$ref" : "#/components/schemas/HelloForm" + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "nullable" : false + }, + "email" : { + "type" : "string" + }, + "url" : { + "type" : "string" + }, + "startDate" : { + "type" : "string", + "format" : "date" + } + } } } }, @@ -649,7 +665,23 @@ "content" : { "application/x-www-form-urlencoded" : { "schema" : { - "$ref" : "#/components/schemas/HelloForm" + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "nullable" : false + }, + "email" : { + "type" : "string" + }, + "url" : { + "type" : "string" + }, + "startDate" : { + "type" : "string", + "format" : "date" + } + } } } }, @@ -1345,6 +1377,27 @@ } } }, + "/streamBytes" : { + "get" : { + "tags" : [ + + ], + "summary" : "", + "description" : "", + "responses" : { + "200" : { + "description" : "", + "content" : { + "text/html" : { + "schema" : { + "$ref" : "#/components/schemas/StreamingOutput" + } + } + } + } + } + } + }, "/test/enumQuery" : { "get" : { "tags" : [ @@ -1615,28 +1668,6 @@ "HelloDto>" : { "type" : "object" }, - "HelloForm" : { - "required" : [ - "name" - ], - "type" : "object", - "properties" : { - "name" : { - "type" : "string", - "nullable" : false - }, - "email" : { - "type" : "string" - }, - "url" : { - "type" : "string" - }, - "startDate" : { - "type" : "string", - "format" : "date" - } - } - }, "HelloWorld" : { "type" : "object", "properties" : { @@ -1683,6 +1714,9 @@ } } }, + "StreamingOutput" : { + "type" : "object" + }, "String>" : { "type" : "object" }, diff --git a/tests/test-jex/src/test/java/org/example/web/HelloControllerTest.java b/tests/test-jex/src/test/java/org/example/web/HelloControllerTest.java index ab52ad3b..f4b13785 100644 --- a/tests/test-jex/src/test/java/org/example/web/HelloControllerTest.java +++ b/tests/test-jex/src/test/java/org/example/web/HelloControllerTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.net.http.HttpResponse; +import java.util.Optional; import org.junit.jupiter.api.Test; @@ -205,4 +206,19 @@ void optionalQueryParamString_withValue() { assertThat(res.body()).isEqualTo("takesOptionalString-Optional[foo]"); } + @Test + void streamBytesTest() { + HttpResponse res = client.request() + .path("streamBytes") + .GET() + .asString(); + + Optional contentTypeHeaderValueOptional = res.headers().firstValue("Content-Type"); + + assertThat(contentTypeHeaderValueOptional.isPresent()).isEqualTo(true); + assertThat(contentTypeHeaderValueOptional.get()).isEqualTo("text/html"); + assertThat(res.body()).isEqualTo("Avaje"); + assertThat(res.statusCode()).isEqualTo(200); + } + } diff --git a/tests/test-sigma/src/main/resources/public/openapi.json b/tests/test-sigma/src/main/resources/public/openapi.json index 01e736b6..c94620ea 100644 --- a/tests/test-sigma/src/main/resources/public/openapi.json +++ b/tests/test-sigma/src/main/resources/public/openapi.json @@ -520,7 +520,23 @@ "content" : { "application/x-www-form-urlencoded" : { "schema" : { - "$ref" : "#/components/schemas/HelloForm" + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "nullable" : false + }, + "email" : { + "type" : "string" + }, + "url" : { + "type" : "string" + }, + "startDate" : { + "type" : "string", + "format" : "date" + } + } } } }, @@ -579,7 +595,23 @@ "content" : { "application/x-www-form-urlencoded" : { "schema" : { - "$ref" : "#/components/schemas/HelloForm" + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "nullable" : false + }, + "email" : { + "type" : "string" + }, + "url" : { + "type" : "string" + }, + "startDate" : { + "type" : "string", + "format" : "date" + } + } } } }, @@ -1516,7 +1548,18 @@ "content" : { "application/x-www-form-urlencoded" : { "schema" : { - "$ref" : "#/components/schemas/MyForm" + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + }, + "email" : { + "type" : "string" + }, + "url" : { + "type" : "string" + } + } } } }, @@ -2026,28 +2069,6 @@ } } }, - "HelloForm" : { - "required" : [ - "name" - ], - "type" : "object", - "properties" : { - "name" : { - "type" : "string", - "nullable" : false - }, - "email" : { - "type" : "string" - }, - "url" : { - "type" : "string" - }, - "startDate" : { - "type" : "string", - "format" : "date" - } - } - }, "HelloWorld" : { "type" : "object", "properties" : { @@ -2076,20 +2097,6 @@ } } }, - "MyForm" : { - "type" : "object", - "properties" : { - "name" : { - "type" : "string" - }, - "email" : { - "type" : "string" - }, - "url" : { - "type" : "string" - } - } - }, "Person" : { "type" : "object", "properties" : {