Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
final class ControllerMethodWriter {

private static final String FILTER_CHAIN = "FilterChain";
private static final Map<Integer,String> statusMap = new HashMap<>();
static {
statusMap.put(200, "OK_200");
Expand Down Expand Up @@ -78,7 +79,7 @@ final class ControllerMethodWriter {
}

private void validateMethod() {
if (method.params().stream().map(MethodParam::shortType).noneMatch("FilterChain"::equals)) {
if (method.params().stream().map(MethodParam::shortType).noneMatch(FILTER_CHAIN::equals)) {
logError(method.element(), "Filters must contain a FilterChain Parameter");
}
}
Expand Down Expand Up @@ -223,7 +224,7 @@ void writeHandler(boolean requestScoped) {
final var param = params.get(i);
if (isAssignable2Interface(param.utype().mainType(), "java.lang.Exception")) {
writer.append("ex");
} else if ("FilterChain".equals(param.shortType())) {
} else if (FILTER_CHAIN.equals(param.shortType())) {
writer.append("chain");
} else {
param.buildParamName(writer);
Expand All @@ -242,34 +243,42 @@ void writeHandler(boolean requestScoped) {
writer.append(" res.status(NO_CONTENT_204).send();").eol();
writer.append(" } else {").eol();
}
final var uType = UType.parse(method.returnType());
String indent = includeNoContent ? " " : " ";
if (responseMode == ResponseMode.Templating) {
writer.append(indent).append("var content = renderer.render(result);").eol();
if (withContentCache) {
writer.append(indent).append("contentCache.contentPut(key, content);").eol();
}
writeContextReturn(indent);
writer.append(indent).append("res.send(content);").eol();
switch (responseMode) {
case ResponseMode.Templating -> {
writer.append(indent).append("var content = renderer.render(result);").eol();
if (withContentCache) {
writer.append(indent).append("contentCache.contentPut(key, content);").eol();
}
writeContextReturn(indent);
writer.append(indent).append("res.send(content);").eol();

} else if (responseMode == ResponseMode.Jstachio) {
var renderer = ProcessingContext.jstacheRenderer(method.returnType());
writer.append(indent).append("var content = %s(result);", renderer).eol();
writeContextReturn(indent);
writer.append(indent).append("res.send(content);").eol();
}
case ResponseMode.Jstachio -> {
var renderer = ProcessingContext.jstacheRenderer(method.returnType());
writer.append(indent).append("var content = %s(result);", renderer).eol();
writeContextReturn(indent);
writer.append(indent).append("res.send(content);").eol();

} else {
final var uType = UType.parse(method.returnType());
writeContextReturn(indent, streamingResponse(uType));
if (responseMode == ResponseMode.InputStream) {
writer.append(indent).append("result.transferTo(res.outputStream());", uType.shortName()).eol();
} else if (responseMode == ResponseMode.Json) {
if (returnTypeString()) {
writer.append(indent).append("res.send(result); // send raw JSON").eol();
}
case ResponseMode.StreamingOutput -> {
writeContextReturn(indent, streamingResponse(uType));
writeStreamingOutputReturn(indent);
}
default -> {
writeContextReturn(indent, streamingResponse(uType));
if (responseMode == ResponseMode.InputStream) {
writer.append(indent).append("result.transferTo(res.outputStream());", uType.shortName()).eol();
} else if (responseMode == ResponseMode.Json) {
if (returnTypeString()) {
writer.append(indent).append("res.send(result); // send raw JSON").eol();
} else {
writer.append(indent).append("%sJsonType.toJson(result, JsonOutput.of(res));", uType.shortName()).eol();
}
} else {
writer.append(indent).append("%sJsonType.toJson(result, JsonOutput.of(res));", uType.shortName()).eol();
writer.append(indent).append("res.send(result);").eol();
}
} else {
writer.append(indent).append("res.send(result);").eol();
}
}
if (includeNoContent) {
Expand All @@ -279,8 +288,14 @@ void writeHandler(boolean requestScoped) {
writer.append(" }").eol().eol();
}

private void writeStreamingOutputReturn(String indent) {
writer.append(indent).append("try (var responseOutputStream = res.outputStream()) {").eol();
writer.append(indent).append(indent).append("result.write(responseOutputStream);").eol();
writer.append(indent).append("}").eol();
}

private static boolean streamingResponse(UType uType) {
return uType.mainType().equals("java.util.stream.Stream");
return "java.util.stream.Stream".equals(uType.mainType());
}

enum ResponseMode {
Expand All @@ -289,6 +304,7 @@ enum ResponseMode {
Jstachio,
Templating,
InputStream,
StreamingOutput,
Other
}

Expand All @@ -299,6 +315,9 @@ ResponseMode responseMode() {
if (isInputStream(method.returnType())) {
return ResponseMode.InputStream;
}
if (isStreamingOutput(method.returnType())) {
return ResponseMode.StreamingOutput;
}
if (producesJson()) {
return ResponseMode.Json;
}
Expand All @@ -323,7 +342,7 @@ private boolean useTemplating() {

private static boolean isExceptionOrFilterChain(MethodParam param) {
return isAssignable2Interface(param.utype().mainType(), "java.lang.Exception")
|| "FilterChain".equals(param.shortType());
|| FILTER_CHAIN.equals(param.shortType());
}

private boolean isInputStream(TypeMirror type) {
Expand Down Expand Up @@ -420,4 +439,8 @@ private String lookupStatusCode(int statusCode) {
public void buildApiDocumentation() {
method.buildApiDoc();
}

private boolean isStreamingOutput(TypeMirror type) {
return isAssignable2Interface(type.toString(), "io.avaje.http.api.StreamingOutput");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,19 @@ private void writeContextReturn(final String resultVariableName) {
produces = MediaType.TEXT_HTML.getValue();
}

var uType = UType.parse(method.returnType());

boolean applicationJson = produces == null || MediaType.APPLICATION_JSON.getValue().equalsIgnoreCase(produces);
if (applicationJson || JsonBUtil.isJsonMimeType(produces)) {

if (isAssignable2Interface(uType.mainType(), "io.avaje.http.api.StreamingOutput")) {
writer.append(" ctx.contentType(\"%s\");", produces).eol();
writer.append(" try (var ctxOutputStream = ctx.outputStream()) {").eol();
writer.append(" %s.write(ctxOutputStream);", resultVariableName).eol();
writer.append(" } catch (java.io.IOException e) {").eol();
writer.append(" throw new java.io.UncheckedIOException(e);").eol();
writer.append(" }").eol();
} else if (applicationJson || JsonBUtil.isJsonMimeType(produces)) {
if (useJsonB) {
var uType = UType.parse(method.returnType());
final boolean isfuture = "java.util.concurrent.CompletableFuture".equals(uType.mainType());
if (isfuture || method.isErrorMethod()) {
if (isfuture) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,9 @@ private void writeContextReturn(ResponseMode responseMode, String resultVariable

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("try (var ctxOutputStream = ctx.outputStream()) {").eol();
writer.append(indent).append(indent).append("%s.write(ctxOutputStream);", resultVariable).eol();
writer.append(indent).append("}", resultVariable);
writer.append(indent).append("}");
}

private void writeJsonReturn(String produces, String indent) {
Expand Down Expand Up @@ -340,7 +340,7 @@ private void writeJsonReturn(String produces, String indent) {
}

private static boolean streamingContent(UType uType) {
return uType.mainType().equals("java.util.stream.Stream");
return "java.util.stream.Stream".equals(uType.mainType());
}

private static boolean isExceptionOrFilterChain(MethodParam param) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.concurrent.Executors;

import org.example.myapp.service.MyService;
import org.example.myapp.web.other.Foo;

import io.avaje.http.api.BeanParam;
import io.avaje.http.api.Controller;
Expand All @@ -21,11 +22,11 @@
import io.avaje.http.api.Post;
import io.avaje.http.api.Produces;
import io.avaje.http.api.QueryParam;
import io.avaje.http.api.StreamingOutput;
import io.avaje.http.api.Valid;
import io.javalin.http.Context;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.inject.Inject;
import org.example.myapp.web.other.Foo;

/**
* Hello resource manager.
Expand Down Expand Up @@ -152,7 +153,7 @@ CompletableFuture<List<HelloDto>> getAllAsync() {
// This also helps ensure that we aren't just getting lucky with timings.
try {
Thread.sleep(10L);
} catch (InterruptedException e) {
} catch (final InterruptedException e) {
throw new RuntimeException(e);
}

Expand Down Expand Up @@ -190,4 +191,12 @@ String controlStatusCode(Context ctx) {
String takesNestedEnum(Foo.NestedEnum myEnum) {
return "takesNestedEnum-" + myEnum;
}

@Get("streamBytes")
@Produces(value = "text/html", statusCode = 200)
StreamingOutput streamBytes() {
return outputStream -> outputStream.write(new byte[]{
0x41, 0x76, 0x61, 0x6a, 0x65
});
}
}
24 changes: 24 additions & 0 deletions tests/test-javalin-jsonb/src/main/resources/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,27 @@
}
}
},
"/hello/streamBytes" : {
"get" : {
"tags" : [

],
"summary" : "",
"description" : "",
"responses" : {
"200" : {
"description" : "",
"content" : {
"text/html" : {
"schema" : {
"$ref" : "#/components/schemas/StreamingOutput"
}
}
}
}
}
}
},
"/hello/takesNestedEnum" : {
"get" : {
"tags" : [
Expand Down Expand Up @@ -2357,6 +2378,9 @@
}
}
},
"StreamingOutput" : {
"type" : "object"
},
"String,?>" : {
"type" : "object"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
package org.example.myapp;

import io.avaje.http.client.*;
import io.restassured.common.mapper.TypeRef;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import org.example.myapp.web.HelloDto;
import org.junit.jupiter.api.Test;

import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;

import static io.restassured.RestAssured.get;
import static io.restassured.RestAssured.given;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.net.http.HttpResponse;
import java.util.List;
import java.util.Optional;

import org.example.myapp.web.HelloDto;
import org.junit.jupiter.api.Test;

import io.avaje.http.client.BodyReader;
import io.avaje.http.client.BodyWriter;
import io.avaje.http.client.HttpClient;
import io.avaje.http.client.HttpException;
import io.avaje.http.client.JacksonBodyAdapter;
import io.restassured.common.mapper.TypeRef;
import io.restassured.http.ContentType;
import io.restassured.response.Response;

class HelloControllerTest extends BaseWebTest {

final HttpClient client;
Expand Down Expand Up @@ -45,7 +50,7 @@ void hello() {
@Test
void hello2() {

TypeRef<List<HelloDto>> listDto = new TypeRef<>() { };
final TypeRef<List<HelloDto>> listDto = new TypeRef<>() { };
final List<HelloDto> beans = given()
.get(baseUrl + "/hello")
.then()
Expand All @@ -64,7 +69,7 @@ void hello2() {

@Test
void helloAsyncRequestHandling() {
TypeRef<List<HelloDto>> listDto = new TypeRef<>() { };
final TypeRef<List<HelloDto>> listDto = new TypeRef<>() { };
final List<HelloDto> beans = given()
.get(baseUrl + "/hello/async")
.then()
Expand Down Expand Up @@ -106,7 +111,7 @@ void getWithPathParamAndQueryParam() {

@Test
void postIt() {
HelloDto dto = new HelloDto(12, "rob", "other");
final HelloDto dto = new HelloDto(12, "rob", "other");

given().body(dto).post(baseUrl + "/hello")
.then()
Expand All @@ -129,7 +134,7 @@ void postIt() {

@Test
void saveBean() {
HelloDto dto = new HelloDto(12, "rob", "other");
final HelloDto dto = new HelloDto(12, "rob", "other");

given().body(dto).post(baseUrl + "/hello/savebean/foo")
.then()
Expand Down Expand Up @@ -217,7 +222,7 @@ void postForm_validation_expect_badRequest() {
.POST()
.asVoid();

} catch (HttpException e) {
} catch (final HttpException e) {
assertEquals(422, e.statusCode());

final HttpResponse<?> httpResponse = e.httpResponse();
Expand Down Expand Up @@ -331,4 +336,18 @@ void get_controlStatusCode_expect201() {
assertEquals("controlStatusCode", hres.body());
}

@Test
void streamBytesTest() {
final HttpResponse<String> res = client.request()
.path("hello/streamBytes")
.GET()
.asString();

final Optional<String> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import io.avaje.http.api.Produces;
import io.avaje.http.api.Put;
import io.avaje.http.api.QueryParam;
import io.avaje.http.api.StreamingOutput;
import io.avaje.http.api.Valid;
import io.helidon.common.media.type.MediaTypes;
import io.helidon.webserver.http.ServerRequest;
Expand Down Expand Up @@ -164,4 +165,10 @@ String formBean(MyForm form) {
String testBigInt(BigInteger val, BigInteger someQueryParam) {
return "hi|" + val;
}

@Get("streamBytes")
@Produces(value = "text/html", statusCode = 200)
StreamingOutput streamBytes() {
return outputStream -> outputStream.write(new byte[] {0x41, 0x76, 0x61, 0x6a, 0x65});
}
}
Loading