Skip to content

Commit f295cce

Browse files
authored
Merge branch 'master' into default-validator-handler
2 parents 9df68f1 + ab9ada9 commit f295cce

File tree

17 files changed

+493
-203
lines changed

17 files changed

+493
-203
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-client/src/main/java/io/avaje/http/client/DHttpApi.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ <T> T provideFor(Class<T> type, HttpClient httpClient) {
4242
+ type
4343
+ "\nPossible Causes: \n"
4444
+ "1. Missing @Client or @Client.Import annotation.\n"
45-
+ "2. The avaje-http-client-generator dependency was not available during compilation\n");
45+
+ "2. The avaje-http-client-generator dependency was not available during compilation\n"
46+
+ "3. Using JDK 23+, and need to set property <maven.compiler.proc>full</maven.compiler.proc>\n");
4647
}
4748
return apiProvider.provide(httpClient);
4849
}

http-client/src/main/java/io/avaje/http/client/DHttpClientBuilder.java

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -144,29 +144,30 @@ private static ExecutorService virtualThreadExecutor() {
144144
* Create a reasonable default BodyAdapter if avaje-jsonb or Jackson are present.
145145
*/
146146
private BodyAdapter defaultBodyAdapter() {
147-
if (detectJsonb()) {
148-
bodyAdapter = new JsonbBodyAdapter();
149-
} else if (detectJackson()) {
150-
bodyAdapter = new JacksonBodyAdapter();
151-
}
152-
return bodyAdapter;
153-
}
154-
155-
private boolean detectJsonb() {
156-
return detectTypeExists("io.avaje.jsonb.Jsonb");
157-
}
158-
159-
private boolean detectJackson() {
160-
return detectTypeExists("com.fasterxml.jackson.databind.ObjectMapper");
161-
}
162-
163-
private boolean detectTypeExists(String className) {
164-
try {
165-
Class.forName(className);
166-
return true;
167-
} catch (ClassNotFoundException | IllegalAccessError e) {
168-
return false;
169-
}
147+
ModuleLayer bootLayer = ModuleLayer.boot();
148+
return bootLayer
149+
.findModule("io.avaje.http.client")
150+
.map(m -> {
151+
if (bootLayer.findModule("io.avaje.jsonb").isPresent()) {
152+
return new JsonbBodyAdapter();
153+
}
154+
if (bootLayer.findModule("com.fasterxml.jackson.databind").isPresent()) {
155+
return new JacksonBodyAdapter();
156+
}
157+
return bodyAdapter;
158+
})
159+
.orElseGet(() -> {
160+
try {
161+
return new JsonbBodyAdapter();
162+
} catch (NoClassDefFoundError e) {
163+
// I guess it don't exist
164+
}
165+
try {
166+
return new JacksonBodyAdapter();
167+
} catch (NoClassDefFoundError e) {
168+
return bodyAdapter;
169+
}
170+
});
170171
}
171172

172173
private DHttpClientContext buildClient() {

http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818
final class ControllerMethodWriter {
1919

20+
private static final String FILTER_CHAIN = "FilterChain";
2021
private static final Map<Integer,String> statusMap = new HashMap<>();
2122
static {
2223
statusMap.put(200, "OK_200");
@@ -78,7 +79,7 @@ final class ControllerMethodWriter {
7879
}
7980

8081
private void validateMethod() {
81-
if (method.params().stream().map(MethodParam::shortType).noneMatch("FilterChain"::equals)) {
82+
if (method.params().stream().map(MethodParam::shortType).noneMatch(FILTER_CHAIN::equals)) {
8283
logError(method.element(), "Filters must contain a FilterChain Parameter");
8384
}
8485
}
@@ -223,7 +224,7 @@ void writeHandler(boolean requestScoped) {
223224
final var param = params.get(i);
224225
if (isAssignable2Interface(param.utype().mainType(), "java.lang.Exception")) {
225226
writer.append("ex");
226-
} else if ("FilterChain".equals(param.shortType())) {
227+
} else if (FILTER_CHAIN.equals(param.shortType())) {
227228
writer.append("chain");
228229
} else {
229230
param.buildParamName(writer);
@@ -242,34 +243,42 @@ void writeHandler(boolean requestScoped) {
242243
writer.append(" res.status(NO_CONTENT_204).send();").eol();
243244
writer.append(" } else {").eol();
244245
}
246+
final var uType = UType.parse(method.returnType());
245247
String indent = includeNoContent ? " " : " ";
246-
if (responseMode == ResponseMode.Templating) {
247-
writer.append(indent).append("var content = renderer.render(result);").eol();
248-
if (withContentCache) {
249-
writer.append(indent).append("contentCache.contentPut(key, content);").eol();
250-
}
251-
writeContextReturn(indent);
252-
writer.append(indent).append("res.send(content);").eol();
248+
switch (responseMode) {
249+
case ResponseMode.Templating -> {
250+
writer.append(indent).append("var content = renderer.render(result);").eol();
251+
if (withContentCache) {
252+
writer.append(indent).append("contentCache.contentPut(key, content);").eol();
253+
}
254+
writeContextReturn(indent);
255+
writer.append(indent).append("res.send(content);").eol();
253256

254-
} else if (responseMode == ResponseMode.Jstachio) {
255-
var renderer = ProcessingContext.jstacheRenderer(method.returnType());
256-
writer.append(indent).append("var content = %s(result);", renderer).eol();
257-
writeContextReturn(indent);
258-
writer.append(indent).append("res.send(content);").eol();
257+
}
258+
case ResponseMode.Jstachio -> {
259+
var renderer = ProcessingContext.jstacheRenderer(method.returnType());
260+
writer.append(indent).append("var content = %s(result);", renderer).eol();
261+
writeContextReturn(indent);
262+
writer.append(indent).append("res.send(content);").eol();
259263

260-
} else {
261-
final var uType = UType.parse(method.returnType());
262-
writeContextReturn(indent, streamingResponse(uType));
263-
if (responseMode == ResponseMode.InputStream) {
264-
writer.append(indent).append("result.transferTo(res.outputStream());", uType.shortName()).eol();
265-
} else if (responseMode == ResponseMode.Json) {
266-
if (returnTypeString()) {
267-
writer.append(indent).append("res.send(result); // send raw JSON").eol();
264+
}
265+
case ResponseMode.StreamingOutput -> {
266+
writeContextReturn(indent, streamingResponse(uType));
267+
writeStreamingOutputReturn(indent);
268+
}
269+
default -> {
270+
writeContextReturn(indent, streamingResponse(uType));
271+
if (responseMode == ResponseMode.InputStream) {
272+
writer.append(indent).append("result.transferTo(res.outputStream());", uType.shortName()).eol();
273+
} else if (responseMode == ResponseMode.Json) {
274+
if (returnTypeString()) {
275+
writer.append(indent).append("res.send(result); // send raw JSON").eol();
276+
} else {
277+
writer.append(indent).append("%sJsonType.toJson(result, JsonOutput.of(res));", uType.shortName()).eol();
278+
}
268279
} else {
269-
writer.append(indent).append("%sJsonType.toJson(result, JsonOutput.of(res));", uType.shortName()).eol();
280+
writer.append(indent).append("res.send(result);").eol();
270281
}
271-
} else {
272-
writer.append(indent).append("res.send(result);").eol();
273282
}
274283
}
275284
if (includeNoContent) {
@@ -279,8 +288,14 @@ void writeHandler(boolean requestScoped) {
279288
writer.append(" }").eol().eol();
280289
}
281290

291+
private void writeStreamingOutputReturn(String indent) {
292+
writer.append(indent).append("try (var responseOutputStream = res.outputStream()) {").eol();
293+
writer.append(indent).append(indent).append("result.write(responseOutputStream);").eol();
294+
writer.append(indent).append("}").eol();
295+
}
296+
282297
private static boolean streamingResponse(UType uType) {
283-
return uType.mainType().equals("java.util.stream.Stream");
298+
return "java.util.stream.Stream".equals(uType.mainType());
284299
}
285300

286301
enum ResponseMode {
@@ -289,6 +304,7 @@ enum ResponseMode {
289304
Jstachio,
290305
Templating,
291306
InputStream,
307+
StreamingOutput,
292308
Other
293309
}
294310

@@ -299,6 +315,9 @@ ResponseMode responseMode() {
299315
if (isInputStream(method.returnType())) {
300316
return ResponseMode.InputStream;
301317
}
318+
if (isStreamingOutput(method.returnType())) {
319+
return ResponseMode.StreamingOutput;
320+
}
302321
if (producesJson()) {
303322
return ResponseMode.Json;
304323
}
@@ -323,7 +342,7 @@ private boolean useTemplating() {
323342

324343
private static boolean isExceptionOrFilterChain(MethodParam param) {
325344
return isAssignable2Interface(param.utype().mainType(), "java.lang.Exception")
326-
|| "FilterChain".equals(param.shortType());
345+
|| FILTER_CHAIN.equals(param.shortType());
327346
}
328347

329348
private boolean isInputStream(TypeMirror type) {
@@ -420,4 +439,8 @@ private String lookupStatusCode(int statusCode) {
420439
public void buildApiDocumentation() {
421440
method.buildApiDoc();
422441
}
442+
443+
private boolean isStreamingOutput(TypeMirror type) {
444+
return isAssignable2Interface(type.toString(), "io.avaje.http.api.StreamingOutput");
445+
}
423446
}

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,19 @@ private void writeContextReturn(final String resultVariableName) {
159159
produces = MediaType.TEXT_HTML.getValue();
160160
}
161161

162+
var uType = UType.parse(method.returnType());
163+
162164
boolean applicationJson = produces == null || MediaType.APPLICATION_JSON.getValue().equalsIgnoreCase(produces);
163-
if (applicationJson || JsonBUtil.isJsonMimeType(produces)) {
165+
166+
if (isAssignable2Interface(uType.mainType(), "io.avaje.http.api.StreamingOutput")) {
167+
writer.append(" ctx.contentType(\"%s\");", produces).eol();
168+
writer.append(" try (var ctxOutputStream = ctx.outputStream()) {").eol();
169+
writer.append(" %s.write(ctxOutputStream);", resultVariableName).eol();
170+
writer.append(" } catch (java.io.IOException e) {").eol();
171+
writer.append(" throw new java.io.UncheckedIOException(e);").eol();
172+
writer.append(" }").eol();
173+
} else if (applicationJson || JsonBUtil.isJsonMimeType(produces)) {
164174
if (useJsonB) {
165-
var uType = UType.parse(method.returnType());
166175
final boolean isfuture = "java.util.concurrent.CompletableFuture".equals(uType.mainType());
167176
if (isfuture || method.isErrorMethod()) {
168177
if (isfuture) {

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

Lines changed: 17 additions & 1 deletion
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 (var ctxOutputStream = ctx.outputStream()) {").eol();
314+
writer.append(indent).append(indent).append("%s.write(ctxOutputStream);", resultVariable).eol();
315+
writer.append(indent).append("}");
316+
}
317+
302318
private void writeJsonReturn(String produces, String indent) {
303319
var uType = UType.parse(method.returnType());
304320
boolean streaming = useJsonB && streamingContent(uType);
@@ -324,7 +340,7 @@ private void writeJsonReturn(String produces, String indent) {
324340
}
325341

326342
private static boolean streamingContent(UType uType) {
327-
return uType.mainType().equals("java.util.stream.Stream");
343+
return "java.util.stream.Stream".equals(uType.mainType());
328344
}
329345

330346
private static boolean isExceptionOrFilterChain(MethodParam param) {

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<swagger.version>2.2.41</swagger.version>
2424
<jackson.version>2.14.2</jackson.version>
2525
<jex.version>3.0-RC10</jex.version>
26-
<avaje.prisms.version>1.43</avaje.prisms.version>
26+
<avaje.prisms.version>1.44-RC4</avaje.prisms.version>
2727
<project.build.outputTimestamp>2025-11-17T18:50:39Z</project.build.outputTimestamp>
2828
<module-info.shade>${project.build.directory}${file.separator}module-info.shade</module-info.shade>
2929
</properties>

tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/HelloController.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.concurrent.Executors;
1010

1111
import org.example.myapp.service.MyService;
12+
import org.example.myapp.web.other.Foo;
1213

1314
import io.avaje.http.api.BeanParam;
1415
import io.avaje.http.api.Controller;
@@ -21,11 +22,11 @@
2122
import io.avaje.http.api.Post;
2223
import io.avaje.http.api.Produces;
2324
import io.avaje.http.api.QueryParam;
25+
import io.avaje.http.api.StreamingOutput;
2426
import io.avaje.http.api.Valid;
2527
import io.javalin.http.Context;
2628
import io.swagger.v3.oas.annotations.Hidden;
2729
import jakarta.inject.Inject;
28-
import org.example.myapp.web.other.Foo;
2930

3031
/**
3132
* Hello resource manager.
@@ -152,7 +153,7 @@ CompletableFuture<List<HelloDto>> getAllAsync() {
152153
// This also helps ensure that we aren't just getting lucky with timings.
153154
try {
154155
Thread.sleep(10L);
155-
} catch (InterruptedException e) {
156+
} catch (final InterruptedException e) {
156157
throw new RuntimeException(e);
157158
}
158159

@@ -190,4 +191,12 @@ String controlStatusCode(Context ctx) {
190191
String takesNestedEnum(Foo.NestedEnum myEnum) {
191192
return "takesNestedEnum-" + myEnum;
192193
}
194+
195+
@Get("streamBytes")
196+
@Produces(value = "text/html", statusCode = 200)
197+
StreamingOutput streamBytes() {
198+
return outputStream -> outputStream.write(new byte[]{
199+
0x41, 0x76, 0x61, 0x6a, 0x65
200+
});
201+
}
193202
}

0 commit comments

Comments
 (0)