Skip to content

Support Javalin async request handling [Context#future(...)] #156

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 12, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ private static void addJsonBodyType(MethodReader methodReader, Consumer<UType> a
}

public static void writeJsonbType(UType type, Append writer) {
// Support for CompletableFuture's.
if (type.mainType().equals("java.util.concurrent.CompletableFuture")) {
writeJsonbType(type.paramRaw(), writer);
return;
}

writer.append(" this.%sJsonType = jsonB.type(", type.shortName());
if (!type.isGeneric()) {
writer.append("%s.class)", Util.shortName(PrimitiveUtil.wrap(type.full())));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,47 @@ void write(boolean requestScoped) {
}

private void writeContextReturn() {
// Support for CompletableFuture's.
final UType type = UType.parse(method.returnType());
if (type.mainType().equals("java.util.concurrent.CompletableFuture")) {
if (!type.isGeneric()) {
throw new IllegalStateException("CompletableFuture must be generic type (e.g. CompletableFuture<String>, CompletableFuture<Void>).");
}

final String futureResultVariableName = "futureResult";

writer.append(" ctx.future(() -> {").eol();
writer.append(" return result.thenAccept(%s -> {", futureResultVariableName).eol();
writer.append(" ");
this.writeContextReturn(futureResultVariableName);
writer.eol().append(" });").eol();
writer.append(" });").eol();
return;
}

// Everything else
this.writeContextReturn("result");
}

private void writeContextReturn(final String resultVariableName) {
final var produces = method.produces();
if (produces == null || MediaType.APPLICATION_JSON.getValue().equalsIgnoreCase(produces)) {
if (useJsonB) {
final var uType = UType.parse(method.returnType());
writer.append(" %sJsonType.toJson(result, ctx.contentType(\"application/json\").outputStream());", uType.shortName());
var uType = UType.parse(method.returnType());
if (uType.mainType().equals("java.util.concurrent.CompletableFuture")) {
uType = uType.paramRaw();
}

writer.append(" %sJsonType.toJson(%s, ctx.contentType(\"application/json\").outputStream());", uType.shortName(), resultVariableName);
} else {
writer.append(" ctx.json(result);");
writer.append(" ctx.json(%s);", resultVariableName);
}
} else if (MediaType.TEXT_HTML.getValue().equalsIgnoreCase(produces)) {
writer.append(" ctx.html(result);");
writer.append(" ctx.html(%s);", resultVariableName);
} else if (MediaType.TEXT_PLAIN.getValue().equalsIgnoreCase(produces)) {
writer.append(" ctx.contentType(\"text/plain\").result(result);");
writer.append(" ctx.contentType(\"text/plain\").result(%s);", resultVariableName);
} else {
writer.append(" ctx.contentType(\"%s\").result(result);", produces);
writer.append(" ctx.contentType(\"%s\").result(%s);", produces, resultVariableName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,18 @@ private void writeClassStart() {
writer.append(" private final Validator validator;").eol();
}

for (final UType type : jsonTypes.values()) {
for (UType type : jsonTypes.values()) {
// Support for CompletableFuture's.
if (type.mainType().equals("java.util.concurrent.CompletableFuture")) {
type = type.paramRaw();

if (this.jsonTypes.containsKey(type.full())) {
// Already written before -- we can skip.
continue;
}
}

// Everything else
final var typeString = PrimitiveUtil.wrap(type.shortType()).replace(",", ", ");
writer.append(" private final JsonType<%s> %sJsonType;", typeString, type.shortName()).eol();
}
Expand All @@ -107,6 +118,12 @@ private void writeClassStart() {
}
if (useJsonB) {
for (final UType type : jsonTypes.values()) {
// Skip trying to assign a global variable value for any UType that is a Completable Future. Because the paramRaw() should
// already be in this jsonTypes map anyway and write the assignment all by itself.
if (type.mainType().equals("java.util.concurrent.CompletableFuture")) {
continue;
}

JsonBUtil.writeJsonbType(type, writer);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;

import javax.validation.Valid;

Expand Down Expand Up @@ -142,6 +144,21 @@ List<HelloDto> getAll() {
return myService.findAll();
}

@Get("/async")
CompletableFuture<List<HelloDto>> getAllAsync() {
return CompletableFuture.supplyAsync(() -> {
// Simulate a delay as if an actual IO operation is being executed.
// This also helps ensure that we aren't just getting lucky with timings.
try {
Thread.sleep(10L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

return myService.findAll();
}, Executors.newSingleThreadExecutor()); // Example of how to use a custom executor.
}

// @Hidden
@Delete(":id")
void deleteById(int id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,25 @@ void hello2() {
assertThat(helloDtos).hasSize(2);
}

@Test
void helloAsyncRequestHandling() {
TypeRef<List<HelloDto>> listDto = new TypeRef<List<HelloDto>>() { };
final List<HelloDto> beans = given()
.get(baseUrl + "/hello/async")
.then()
.statusCode(200)
.extract()
.as(listDto);

assertThat(beans).hasSize(2);

final List<HelloDto> helloDtos = client.request()
.path("hello/async")
.GET().list(HelloDto.class);

assertThat(helloDtos).hasSize(2);
}

@Test
void getWithPathParamAndQueryParam() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;

import javax.validation.Valid;

Expand Down Expand Up @@ -142,6 +144,21 @@ List<HelloDto> getAll() {
return myService.findAll();
}

@Get("/async")
CompletableFuture<List<HelloDto>> getAllAsync() {
return CompletableFuture.supplyAsync(() -> {
// Simulate a delay as if an actual IO operation is being executed.
// This also helps ensure that we aren't just getting lucky with timings.
try {
Thread.sleep(10L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

return myService.findAll();
}, Executors.newSingleThreadExecutor()); // Example of how to use a custom executor.
}

// @Hidden
@Delete("{id}")
void deleteById(int id) {
Expand All @@ -159,4 +176,5 @@ String getWithMatrixParam(int year, String author, String country, String other,
String slashAccepting(String name, String nam0, String nam1) {
return "got name:" + name + " splat0:" + nam0 + " splat1:" + nam1;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,25 @@ void hello2() {
assertThat(helloDtos).hasSize(2);
}

@Test
void helloAsyncRequestHandling() {
TypeRef<List<HelloDto>> listDto = new TypeRef<List<HelloDto>>() { };
final List<HelloDto> beans = given()
.get(baseUrl + "/hello/async")
.then()
.statusCode(200)
.extract()
.as(listDto);

assertThat(beans).hasSize(2);

final List<HelloDto> helloDtos = client.request()
.path("hello/async")
.GET().list(HelloDto.class);

assertThat(helloDtos).hasSize(2);
}

@Test
void getWithPathParamAndQueryParam() {

Expand Down