Skip to content

Commit 33c1c29

Browse files
Support for Jax-RS style "StreamingOutput" for JEX (#674)
* Support for Jax-RS style "StreamingOutput" This commit only implements for the JEX server option. * Support for Jax-RS style "StreamingOutput" Add a test # Conflicts: # tests/test-jex/src/main/resources/public/openapi.json * Support for Jax-RS style "StreamingOutput" Swich back to the 3.3 of the avaje-jex and closing the `OutputStream` instead of flushing. --------- Co-authored-by: Rob Bygrave <130515035+rob-bygrave@users.noreply.github.com>
1 parent 23e0c53 commit 33c1c29

File tree

8 files changed

+309
-126
lines changed

8 files changed

+309
-126
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package io.avaje.http.api;
2+
3+
import java.io.IOException;
4+
import java.io.OutputStream;
5+
6+
/**
7+
* An avaje {@link Controller} endpoint is able to use an instance of this interface
8+
* as a return type.
9+
*
10+
* <pre>{@code
11+
* @Post("some_endpoint")
12+
* @Produces("application/octet-stream")
13+
* public StreamingOutput thumbnail(
14+
* InputStream inputStream,
15+
* @QueryParam(Constants.KEY_SIZE) @Min(1) @Max(Constants.MAX_SIZE) Integer size
16+
* ) throws IOException {
17+
* return (os) -> os.write(new byte[] { 0x01, 0x02, 0x03 });
18+
* }
19+
* }</pre>
20+
*/
21+
22+
public interface StreamingOutput {
23+
void write(OutputStream outputStream) throws IOException;
24+
}

http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ enum ResponseMode {
107107
Jstachio,
108108
Templating,
109109
InputStream,
110+
StreamingOutput,
110111
Other
111112
}
112113

@@ -117,6 +118,9 @@ ResponseMode responseMode() {
117118
if (isInputStream(method.returnType())) {
118119
return ResponseMode.InputStream;
119120
}
121+
if (isStreamingOutput(method.returnType())) {
122+
return ResponseMode.StreamingOutput;
123+
}
120124
if (producesJson()) {
121125
return ResponseMode.Json;
122126
}
@@ -136,6 +140,10 @@ private boolean isInputStream(TypeMirror type) {
136140
return isAssignable2Interface(type.toString(), "java.io.InputStream");
137141
}
138142

143+
private boolean isStreamingOutput(TypeMirror type) {
144+
return isAssignable2Interface(type.toString(), "io.avaje.http.api.StreamingOutput");
145+
}
146+
139147
private boolean producesJson() {
140148
return !"byte[]".equals(method.returnType().toString())
141149
&& !useJstachio
@@ -294,11 +302,19 @@ private void writeContextReturn(ResponseMode responseMode, String resultVariable
294302
case Json -> writeJsonReturn(produces, indent);
295303
case Text -> writer.append("ctx.text(%s);", resultVariable);
296304
case Templating -> writer.append("ctx.html(%s);", resultVariable);
305+
case StreamingOutput -> writeStreamingOutputReturn(produces, resultVariable, indent);
297306
default -> writer.append("ctx.contentType(\"%s\").write(%s);", produces, resultVariable);
298307
}
299308
writer.eol();
300309
}
301310

311+
private void writeStreamingOutputReturn(String produces, String resultVariable, String indent) {
312+
writer.append("ctx.contentType(\"%s\");", produces).eol();
313+
writer.append(indent).append("try (java.io.OutputStream ctxOutputStream = ctx.outputStream()) {").eol();
314+
writer.append(indent).append(indent).append("%s.write(ctxOutputStream);", resultVariable).eol();
315+
writer.append(indent).append("}", resultVariable);
316+
}
317+
302318
private void writeJsonReturn(String produces, String indent) {
303319
var uType = UType.parse(method.returnType());
304320
boolean streaming = useJsonB && streamingContent(uType);

tests/test-javalin-jsonb/src/main/resources/public/openapi.json

Lines changed: 106 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,23 @@
520520
"content" : {
521521
"application/x-www-form-urlencoded" : {
522522
"schema" : {
523-
"$ref" : "#/components/schemas/HelloForm"
523+
"type" : "object",
524+
"properties" : {
525+
"name" : {
526+
"type" : "string",
527+
"nullable" : false
528+
},
529+
"email" : {
530+
"type" : "string"
531+
},
532+
"url" : {
533+
"type" : "string"
534+
},
535+
"startDate" : {
536+
"type" : "string",
537+
"format" : "date"
538+
}
539+
}
524540
}
525541
}
526542
},
@@ -579,7 +595,23 @@
579595
"content" : {
580596
"application/x-www-form-urlencoded" : {
581597
"schema" : {
582-
"$ref" : "#/components/schemas/HelloForm"
598+
"type" : "object",
599+
"properties" : {
600+
"name" : {
601+
"type" : "string",
602+
"nullable" : false
603+
},
604+
"email" : {
605+
"type" : "string"
606+
},
607+
"url" : {
608+
"type" : "string"
609+
},
610+
"startDate" : {
611+
"type" : "string",
612+
"format" : "date"
613+
}
614+
}
583615
}
584616
}
585617
},
@@ -1029,6 +1061,57 @@
10291061
}
10301062
}
10311063
},
1064+
"/openapi/form" : {
1065+
"post" : {
1066+
"tags" : [
1067+
1068+
],
1069+
"summary" : "",
1070+
"description" : "",
1071+
"parameters" : [
1072+
{
1073+
"name" : "Head-String",
1074+
"in" : "header",
1075+
"schema" : {
1076+
"type" : "string"
1077+
}
1078+
}
1079+
],
1080+
"requestBody" : {
1081+
"content" : {
1082+
"application/x-www-form-urlencoded" : {
1083+
"schema" : {
1084+
"type" : "object",
1085+
"properties" : {
1086+
"name" : {
1087+
"type" : "string"
1088+
},
1089+
"email" : {
1090+
"type" : "string"
1091+
},
1092+
"url" : {
1093+
"type" : "string"
1094+
}
1095+
}
1096+
}
1097+
}
1098+
},
1099+
"required" : true
1100+
},
1101+
"responses" : {
1102+
"201" : {
1103+
"description" : "",
1104+
"content" : {
1105+
"application/json" : {
1106+
"schema" : {
1107+
"type" : "string"
1108+
}
1109+
}
1110+
}
1111+
}
1112+
}
1113+
}
1114+
},
10321115
"/openapi/get" : {
10331116
"get" : {
10341117
"tags" : [
@@ -1653,11 +1736,31 @@
16531736
],
16541737
"summary" : "",
16551738
"description" : "",
1739+
"parameters" : [
1740+
{
1741+
"name" : "Head-String",
1742+
"in" : "header",
1743+
"schema" : {
1744+
"type" : "string"
1745+
}
1746+
}
1747+
],
16561748
"requestBody" : {
16571749
"content" : {
16581750
"application/x-www-form-urlencoded" : {
16591751
"schema" : {
1660-
"$ref" : "#/components/schemas/MyForm"
1752+
"type" : "object",
1753+
"properties" : {
1754+
"name" : {
1755+
"type" : "string"
1756+
},
1757+
"email" : {
1758+
"type" : "string"
1759+
},
1760+
"url" : {
1761+
"type" : "string"
1762+
}
1763+
}
16611764
}
16621765
}
16631766
},
@@ -2241,42 +2344,6 @@
22412344
}
22422345
}
22432346
},
2244-
"HelloForm" : {
2245-
"required" : [
2246-
"name"
2247-
],
2248-
"type" : "object",
2249-
"properties" : {
2250-
"name" : {
2251-
"type" : "string",
2252-
"nullable" : false
2253-
},
2254-
"email" : {
2255-
"type" : "string"
2256-
},
2257-
"url" : {
2258-
"type" : "string"
2259-
},
2260-
"startDate" : {
2261-
"type" : "string",
2262-
"format" : "date"
2263-
}
2264-
}
2265-
},
2266-
"MyForm" : {
2267-
"type" : "object",
2268-
"properties" : {
2269-
"name" : {
2270-
"type" : "string"
2271-
},
2272-
"email" : {
2273-
"type" : "string"
2274-
},
2275-
"url" : {
2276-
"type" : "string"
2277-
}
2278-
}
2279-
},
22802347
"Person" : {
22812348
"type" : "object",
22822349
"properties" : {

tests/test-javalin/src/main/resources/public/openapi.json

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,23 @@
490490
"content" : {
491491
"application/x-www-form-urlencoded" : {
492492
"schema" : {
493-
"$ref" : "#/components/schemas/HelloForm"
493+
"type" : "object",
494+
"properties" : {
495+
"name" : {
496+
"type" : "string",
497+
"nullable" : false
498+
},
499+
"email" : {
500+
"type" : "string"
501+
},
502+
"url" : {
503+
"type" : "string"
504+
},
505+
"startDate" : {
506+
"type" : "string",
507+
"format" : "date"
508+
}
509+
}
494510
}
495511
}
496512
},
@@ -549,7 +565,23 @@
549565
"content" : {
550566
"application/x-www-form-urlencoded" : {
551567
"schema" : {
552-
"$ref" : "#/components/schemas/HelloForm"
568+
"type" : "object",
569+
"properties" : {
570+
"name" : {
571+
"type" : "string",
572+
"nullable" : false
573+
},
574+
"email" : {
575+
"type" : "string"
576+
},
577+
"url" : {
578+
"type" : "string"
579+
},
580+
"startDate" : {
581+
"type" : "string",
582+
"format" : "date"
583+
}
584+
}
553585
}
554586
}
555587
},
@@ -874,28 +906,6 @@
874906
"format" : "date-time"
875907
}
876908
}
877-
},
878-
"HelloForm" : {
879-
"required" : [
880-
"name"
881-
],
882-
"type" : "object",
883-
"properties" : {
884-
"name" : {
885-
"type" : "string",
886-
"nullable" : false
887-
},
888-
"email" : {
889-
"type" : "string"
890-
},
891-
"url" : {
892-
"type" : "string"
893-
},
894-
"startDate" : {
895-
"type" : "string",
896-
"format" : "date"
897-
}
898-
}
899909
}
900910
}
901911
}

tests/test-jex/src/main/java/org/example/web/HelloController.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.avaje.http.api.Produces;
1111
import io.avaje.http.api.Put;
1212
import io.avaje.http.api.Valid;
13+
import io.avaje.http.api.StreamingOutput;
1314
import io.avaje.jex.http.Context;
1415

1516
// @Roles(AppRoles.BASIC_USER)
@@ -77,4 +78,12 @@ String testBigInt(BigInteger val) {
7778
String rawJsonString() {
7879
return "{\"key\": 42 }";
7980
}
81+
82+
@Get("streamBytes")
83+
@Produces(value = "text/html", statusCode = 200)
84+
StreamingOutput streamBytes() {
85+
return outputStream -> outputStream.write(new byte[]{
86+
0x41, 0x76, 0x61, 0x6a, 0x65
87+
});
88+
}
8089
}

0 commit comments

Comments
 (0)