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