From 653a8e6dfd33d517dfe28f4ff3bff970969c7fa0 Mon Sep 17 00:00:00 2001 From: Stephane Epardaud Date: Tue, 6 Apr 2021 17:06:06 +0200 Subject: [PATCH] Support AsyncFile, Path. Added PathPart and FilePart --- docs/src/main/asciidoc/resteasy-reactive.adoc | 20 +++ .../VertxAsyncFileMessageBodyWriter.java | 25 +++ .../runtime/ServletRequestContext.java | 23 +++ .../deployment/ResteasyReactiveProcessor.java | 6 + .../server/test/providers/FileResource.java | 74 +++++++++ .../server/test/providers/FileTestCase.java | 143 ++++++++++++++++++ .../test/providers/InvalidFileResource.java | 19 +++ .../test/providers/InvalidFileTestCase.java | 28 ++++ .../test/providers/WithWriterInterceptor.java | 15 ++ .../test/providers/WriterInterceptor.java | 19 +++ ...ServerVertxAsyncFileMessageBodyWriter.java | 65 ++++++++ .../org/jboss/resteasy/reactive/FilePart.java | 50 ++++++ .../org/jboss/resteasy/reactive/PathPart.java | 59 ++++++++ .../serialisers/FileBodyHandler.java | 2 + .../serialisers/FilePartBodyHandler.java | 33 ++++ .../serialisers/PathBodyHandler.java | 30 ++++ .../serialisers/PathPartBodyHandler.java | 47 ++++++ .../server/core/ServerSerialisers.java | 11 ++ .../startup/RuntimeInterceptorDeployment.java | 22 +++ .../startup/RuntimeResourceDeployment.java | 7 + .../server/handlers/InterceptorHandler.java | 4 + .../serialisers/ServerFileBodyHandler.java | 2 +- .../ServerFilePartBodyHandler.java | 39 +++++ .../serialisers/ServerPathBodyHandler.java | 43 ++++++ .../ServerPathPartBodyHandler.java | 44 ++++++ .../server/spi/ServerHttpResponse.java | 6 + .../VertxResteasyReactiveRequestContext.java | 22 +++ 27 files changed, 857 insertions(+), 1 deletion(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/VertxAsyncFileMessageBodyWriter.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileResource.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/InvalidFileResource.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/InvalidFileTestCase.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/WithWriterInterceptor.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/WriterInterceptor.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ServerVertxAsyncFileMessageBodyWriter.java create mode 100644 independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/FilePart.java create mode 100644 independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/PathPart.java create mode 100644 independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/FilePartBodyHandler.java create mode 100644 independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/PathBodyHandler.java create mode 100644 independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/PathPartBodyHandler.java create mode 100644 independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFilePartBodyHandler.java create mode 100644 independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java create mode 100644 independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathPartBodyHandler.java diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index 7510cc72a5c77..41dc2f46d5e1c 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -472,6 +472,26 @@ response (see <> for more advanced information). You can return any of the pre-defined types that you can read from the <>, and any other type will be mapped <>. +In addition, the following return types are also supported: + +.Table Additional response body parameter type +|=== +|Type|Usage + +|link:{jdkapi}/java/nio/file/Path.html[`Path`] +|The contents of the file specified by the given path + +|`PathPart` +|The partial contents of the file specified by the given path + +|`FilePart` +|The partial contents of a file + +|link:{vertxapi}io/vertx/core/file/AsyncFile.html[`AsyncFile`] +|Vert.x AsyncFile, which can be in full, or partial + +|=== + Alternately, you can also return a <> such as link:{mutinyapi}/io/smallrye/mutiny/Uni.html[`Uni`], link:{mutinyapi}/io/smallrye/mutiny/Multi.html[`Multi`] or link:{jdkapi}/java/util/concurrent/CompletionStage.html[`CompletionStage`] diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/VertxAsyncFileMessageBodyWriter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/VertxAsyncFileMessageBodyWriter.java new file mode 100644 index 0000000000000..2f81eff2223b5 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/VertxAsyncFileMessageBodyWriter.java @@ -0,0 +1,25 @@ +package io.quarkus.resteasy.reactive.common.runtime; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; + +import io.vertx.core.file.AsyncFile; + +public class VertxAsyncFileMessageBodyWriter implements MessageBodyWriter { + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + // allow for subtypes, such as AsyncFileImpl + return AsyncFile.class.isAssignableFrom(type); + } + + public void writeTo(AsyncFile asyncFile, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { + throw new UnsupportedOperationException("Returning an AsyncFile is not supported with WriterInterceptors"); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java index ab2a6725e428e..5873226f0ae6f 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java @@ -43,6 +43,7 @@ import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.undertow.server.HttpServerExchange; import io.undertow.server.ResponseCommitListener; +import io.vertx.core.Handler; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.net.impl.ConnectionBase; @@ -574,4 +575,26 @@ public V setValue(V value) { return old; } } + + @Override + public ServerHttpResponse sendFile(String path, long offset, long length) { + context.response().sendFile(path, offset, length); + return this; + } + + @Override + public boolean isWriteQueueFull() { + return context.response().writeQueueFull(); + } + + @Override + public ServerHttpResponse addDrainHandler(Runnable onDrain) { + context.response().drainHandler(new Handler() { + @Override + public void handle(Void event) { + onDrain.run(); + } + }); + return this; + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index f3c7233e6fe14..fdd9e5577d64c 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -99,6 +99,7 @@ import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveInitialiser; import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveRecorder; import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveRuntimeRecorder; +import io.quarkus.resteasy.reactive.server.runtime.ServerVertxAsyncFileMessageBodyWriter; import io.quarkus.resteasy.reactive.server.runtime.ServerVertxBufferMessageBodyWriter; import io.quarkus.resteasy.reactive.server.runtime.exceptionmappers.AuthenticationCompletionExceptionMapper; import io.quarkus.resteasy.reactive.server.runtime.exceptionmappers.AuthenticationFailedExceptionMapper; @@ -127,6 +128,7 @@ import io.quarkus.vertx.http.runtime.VertxHttpRecorder; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; +import io.vertx.core.file.AsyncFile; import io.vertx.ext.web.RoutingContext; public class ResteasyReactiveProcessor { @@ -148,6 +150,10 @@ void vertxIntegration(BuildProducer writerBuildItemB writerBuildItemBuildProducer.produce(new MessageBodyWriterBuildItem(ServerVertxBufferMessageBodyWriter.class.getName(), Buffer.class.getName(), Collections.singletonList(MediaType.WILDCARD), RuntimeType.SERVER, true, Priorities.USER)); + writerBuildItemBuildProducer + .produce(new MessageBodyWriterBuildItem(ServerVertxAsyncFileMessageBodyWriter.class.getName(), + AsyncFile.class.getName(), Collections.singletonList(MediaType.WILDCARD), RuntimeType.SERVER, true, + Priorities.USER)); } @BuildStep diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileResource.java new file mode 100644 index 0000000000000..43ade43b94e67 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileResource.java @@ -0,0 +1,74 @@ +package io.quarkus.resteasy.reactive.server.test.providers; + +import java.io.File; +import java.nio.file.Paths; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.jboss.resteasy.reactive.FilePart; +import org.jboss.resteasy.reactive.PathPart; + +import io.smallrye.mutiny.Uni; +import io.vertx.core.file.AsyncFile; +import io.vertx.core.file.OpenOptions; +import io.vertx.ext.web.RoutingContext; + +@Path("providers/file") +public class FileResource { + + private static final String FILE = "src/test/resources/lorem.txt"; + + @Path("file") + @GET + public File getFile() { + return new File(FILE); + } + + @Path("file-partial") + @GET + public FilePart getFilePart() { + return new FilePart(new File(FILE), 20, 10); + } + + @Path("path") + @GET + public java.nio.file.Path getPath() { + return Paths.get(FILE); + } + + @Path("path-partial") + @GET + public PathPart getPathPart() { + return new PathPart(Paths.get(FILE), 20, 10); + } + + @Path("async-file") + @GET + public Uni getAsyncFile(RoutingContext vertxRequest) { + return Uni.createFrom().emitter(emitter -> { + vertxRequest.vertx().fileSystem().open(FILE, new OpenOptions(), result -> { + if (result.succeeded()) + emitter.complete(result.result()); + else + emitter.fail(result.cause()); + }); + }); + } + + @Path("async-file-partial") + @GET + public Uni getAsyncFilePartial(RoutingContext vertxRequest) { + return Uni.createFrom().emitter(emitter -> { + vertxRequest.vertx().fileSystem().open(FILE, new OpenOptions(), result -> { + if (result.succeeded()) { + AsyncFile asyncFile = result.result(); + asyncFile.setReadPos(20); + asyncFile.setReadLength(10); + emitter.complete(asyncFile); + } else + emitter.fail(result.cause()); + }); + }); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java new file mode 100644 index 0000000000000..7ab0eb8954ddf --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java @@ -0,0 +1,143 @@ +package io.quarkus.resteasy.reactive.server.test.providers; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import javax.ws.rs.core.HttpHeaders; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.FilePart; +import org.jboss.resteasy.reactive.PathPart; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.restassured.RestAssured; + +public class FileTestCase { + + private final static String LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut\n" + + + "enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor\n" + + + "in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident,\n" + + + " sunt in culpa qui officia deserunt mollit anim id est laborum.\n" + + "\n" + + ""; + private static final String FILE = "src/test/resources/lorem.txt"; + + @TestHTTPResource + URI uri; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(FileResource.class, WithWriterInterceptor.class, WriterInterceptor.class)); + + @Test + public void testFiles() throws Exception { + // adjusting expected file size for Windows, whose git checkout will adjust line separators + String content; + if (System.lineSeparator().length() == 2) { + content = LOREM.replace("\n", System.lineSeparator()); + } else { + content = LOREM; + } + String contentLength = String.valueOf(content.length()); + RestAssured.get("/providers/file/file") + .then() + .statusCode(200) + .header(HttpHeaders.CONTENT_LENGTH, contentLength) + .body(Matchers.equalTo(content)); + RestAssured.get("/providers/file/file-partial") + .then() + .statusCode(200) + .header(HttpHeaders.CONTENT_LENGTH, "10") + .body(Matchers.equalTo(content.substring(20, 30))); + RestAssured.get("/providers/file/path") + .then() + .statusCode(200) + .header(HttpHeaders.CONTENT_LENGTH, contentLength) + .body(Matchers.equalTo(content)); + RestAssured.get("/providers/file/path-partial") + .then() + .statusCode(200) + .header(HttpHeaders.CONTENT_LENGTH, "10") + .body(Matchers.equalTo(content.substring(20, 30))); + RestAssured.get("/providers/file/async-file") + .then() + .header(HttpHeaders.CONTENT_LENGTH, Matchers.nullValue()) + .statusCode(200) + .body(Matchers.equalTo(content)); + RestAssured.get("/providers/file/async-file-partial") + .then() + .statusCode(200) + .header(HttpHeaders.CONTENT_LENGTH, Matchers.nullValue()) + .body(Matchers.equalTo(LOREM.substring(20, 30))); + } + + @Test + public void testChecks() throws IOException { + // creation-time checks + Path path = Paths.get(FILE); + // works + new PathPart(path, 10, 10); + new PathPart(path, 0, Files.size(path)); + // fails + try { + new PathPart(path, -1, 10); + Assertions.fail(); + } catch (IllegalArgumentException x) { + } + try { + new PathPart(path, 0, -1); + Assertions.fail(); + } catch (IllegalArgumentException x) { + } + try { + new PathPart(path, 0, 1000); + Assertions.fail(); + } catch (IllegalArgumentException x) { + } + try { + new PathPart(path, 250, 250); + Assertions.fail(); + } catch (IllegalArgumentException x) { + } + + File file = new File(FILE); + // works + new FilePart(file, 10, 10); + new FilePart(file, 0, file.length()); + // fails + try { + new FilePart(file, -1, 10); + Assertions.fail(); + } catch (IllegalArgumentException x) { + } + try { + new FilePart(file, 0, -1); + Assertions.fail(); + } catch (IllegalArgumentException x) { + } + try { + new FilePart(file, 0, 1000); + Assertions.fail(); + } catch (IllegalArgumentException x) { + } + try { + new FilePart(file, 250, 250); + Assertions.fail(); + } catch (IllegalArgumentException x) { + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/InvalidFileResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/InvalidFileResource.java new file mode 100644 index 0000000000000..87460972e6001 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/InvalidFileResource.java @@ -0,0 +1,19 @@ +package io.quarkus.resteasy.reactive.server.test.providers; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import io.vertx.core.file.AsyncFile; +import io.vertx.ext.web.RoutingContext; + +@Path("providers/file-invalid") +public class InvalidFileResource { + + @WithWriterInterceptor + @Path("async-file-blocking") + @GET + public AsyncFile getAsyncFileBlocking(RoutingContext vertxRequest) { + // we're not calling this anyway + return null; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/InvalidFileTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/InvalidFileTestCase.java new file mode 100644 index 0000000000000..1ac47d7ccdf1b --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/InvalidFileTestCase.java @@ -0,0 +1,28 @@ +package io.quarkus.resteasy.reactive.server.test.providers; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class InvalidFileTestCase { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(InvalidFileResource.class, WithWriterInterceptor.class, WriterInterceptor.class)) + .assertException(t -> { + while (t.getCause() != null) + t = t.getCause(); + Assertions.assertTrue( + t.getMessage().equals("Endpoints that return an AsyncFile cannot have any WriterInterceptor set")); + }); + + @Test + public void test() throws Exception { + Assertions.fail("Deployment should have failed"); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/WithWriterInterceptor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/WithWriterInterceptor.java new file mode 100644 index 0000000000000..9a71db6868751 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/WithWriterInterceptor.java @@ -0,0 +1,15 @@ +package io.quarkus.resteasy.reactive.server.test.providers; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.ws.rs.NameBinding; + +@NameBinding +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface WithWriterInterceptor { + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/WriterInterceptor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/WriterInterceptor.java new file mode 100644 index 0000000000000..4172859a43504 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/WriterInterceptor.java @@ -0,0 +1,19 @@ +package io.quarkus.resteasy.reactive.server.test.providers; + +import java.io.IOException; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.ext.Provider; +import javax.ws.rs.ext.WriterInterceptorContext; + +@WithWriterInterceptor +@Provider +public class WriterInterceptor implements javax.ws.rs.ext.WriterInterceptor { + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + System.err.println("Around write start"); + context.proceed(); + System.err.println("Around write end"); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ServerVertxAsyncFileMessageBodyWriter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ServerVertxAsyncFileMessageBodyWriter.java new file mode 100644 index 0000000000000..8578445b280aa --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ServerVertxAsyncFileMessageBodyWriter.java @@ -0,0 +1,65 @@ +package io.quarkus.resteasy.reactive.server.runtime; + +import java.lang.reflect.Type; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.Provider; + +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; +import org.jboss.resteasy.reactive.server.spi.ServerHttpResponse; +import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; +import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; + +import io.quarkus.resteasy.reactive.common.runtime.VertxAsyncFileMessageBodyWriter; +import io.vertx.core.Handler; +import io.vertx.core.file.AsyncFile; + +@Provider +public class ServerVertxAsyncFileMessageBodyWriter extends VertxAsyncFileMessageBodyWriter + implements ServerMessageBodyWriter { + + @Override + public boolean isWriteable(Class type, Type genericType, ResteasyReactiveResourceInfo target, MediaType mediaType) { + // allow for subtypes, such as AsyncFileImpl + return AsyncFile.class.isAssignableFrom(type); + } + + @Override + public void writeResponse(AsyncFile file, Type genericType, ServerRequestContext context) throws WebApplicationException { + ResteasyReactiveRequestContext ctx = ((ResteasyReactiveRequestContext) context); + ctx.suspend(); + ServerHttpResponse response = context.serverResponse(); + // Vert.x 3 doesn't allow us to read the file length + response.setChunked(true); + file.handler(buffer -> { + try { + response.write(buffer.getBytes()); + } catch (Exception x) { + // believe it or not, this throws + ctx.resume(x); + return; + } + if (response.isWriteQueueFull()) { + file.pause(); + response.addDrainHandler(new Runnable() { + @Override + public void run() { + file.resume(); + } + }); + } + }); + + file.endHandler(new Handler() { + @Override + public void handle(Void event) { + file.close(); + response.end(); + // Not sure if I need to resume, actually + ctx.resume(); + } + }); + } +} diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/FilePart.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/FilePart.java new file mode 100644 index 0000000000000..ae042d931bc78 --- /dev/null +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/FilePart.java @@ -0,0 +1,50 @@ +package org.jboss.resteasy.reactive; + +import java.io.File; + +/** + * Wrapper type representing a partial {@link File} object to be sent. + */ +public class FilePart { + + /** + * The file to send + */ + public final File file; + + /** + * The starting byte of the file + */ + public final long offset; + + /** + * The number of bytes to send + */ + public final long count; + + /** + * Create a new partial {@link File} object. + * + * @param file The file to send + * @param offset The starting byte of the file (must be >= 0) + * @param count The number of bytes to send (must be >= 0 and offset+count <= file size) + */ + public FilePart(File file, long offset, long count) { + if (!file.exists()) + throw new IllegalArgumentException("File does not exist: " + file); + if (!file.isFile()) + throw new IllegalArgumentException("File is not a regular file: " + file); + if (!file.canRead()) + throw new IllegalArgumentException("File cannot be read: " + file); + if (offset < 0) + throw new IllegalArgumentException("Offset (" + offset + ") must be >= 0: " + file); + if (count < 0) + throw new IllegalArgumentException("Count (" + count + ") must be >= 0: " + file); + if ((offset + count) > file.length()) + throw new IllegalArgumentException( + "Offset + count (" + (offset + count) + ") larger than file size (" + file.length() + "): " + file); + this.file = file; + this.offset = offset; + this.count = count; + } +} diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/PathPart.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/PathPart.java new file mode 100644 index 0000000000000..697c700febae7 --- /dev/null +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/PathPart.java @@ -0,0 +1,59 @@ +package org.jboss.resteasy.reactive; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Wrapper type representing the {@link Path} to a partial file object to be sent. + */ +public class PathPart { + + /** + * The file to send + */ + public final Path file; + + /** + * The starting byte of the file + */ + public final long offset; + + /** + * The number of bytes to send + */ + public final long count; + + /** + * Create a new partial {@link Path} object. + * + * @param path The file to send + * @param offset The starting byte of the file (must be >= 0) + * @param count The number of bytes to send (must be >= 0 and offset+count <= file size) + */ + public PathPart(Path file, long offset, long count) { + if (!Files.exists(file)) + throw new IllegalArgumentException("File does not exist: " + file); + if (!Files.isRegularFile(file)) + throw new IllegalArgumentException("File is not a regular file: " + file); + if (!Files.isReadable(file)) + throw new IllegalArgumentException("File cannot be read: " + file); + if (offset < 0) + throw new IllegalArgumentException("Offset (" + offset + ") must be >= 0: " + file); + if (count < 0) + throw new IllegalArgumentException("Count (" + count + ") must be >= 0: " + file); + long fileLength; + try { + fileLength = Files.size(file); + if ((offset + count) > fileLength) + throw new IllegalArgumentException( + "Offset + count (" + (offset + count) + ") larger than file size (" + fileLength + "): " + file); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + this.file = file; + this.offset = offset; + this.count = count; + } +} diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/FileBodyHandler.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/FileBodyHandler.java index e7e4a462d3ff2..d2484bf146444 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/FileBodyHandler.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/FileBodyHandler.java @@ -10,6 +10,7 @@ import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.MessageBodyReader; @@ -53,6 +54,7 @@ public void writeTo(File uploadFile, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException { + httpHeaders.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(uploadFile.length())); doWrite(uploadFile, entityStream); } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/FilePartBodyHandler.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/FilePartBodyHandler.java new file mode 100644 index 0000000000000..5e1439551943b --- /dev/null +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/FilePartBodyHandler.java @@ -0,0 +1,33 @@ +package org.jboss.resteasy.reactive.common.providers.serialisers; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import org.jboss.resteasy.reactive.FilePart; + +public class FilePartBodyHandler implements MessageBodyWriter { + + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return FilePart.class.isAssignableFrom(type); + } + + public void writeTo(FilePart uploadFile, Class type, Type genericType, + Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream) throws IOException { + httpHeaders.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(uploadFile.count)); + doWrite(uploadFile, entityStream); + } + + protected void doWrite(FilePart uploadFile, OutputStream out) throws IOException { + PathPartBodyHandler.doWrite(new BufferedInputStream(new FileInputStream(uploadFile.file)), uploadFile.offset, + uploadFile.count, out); + } +} diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/PathBodyHandler.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/PathBodyHandler.java new file mode 100644 index 0000000000000..f1e66eed21abc --- /dev/null +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/PathBodyHandler.java @@ -0,0 +1,30 @@ +package org.jboss.resteasy.reactive.common.providers.serialisers; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.file.Files; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; + +public class PathBodyHandler implements MessageBodyWriter { + + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return java.nio.file.Path.class.isAssignableFrom(type); + } + + public void writeTo(java.nio.file.Path uploadFile, Class type, Type genericType, + Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream) throws IOException { + httpHeaders.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(Files.size(uploadFile))); + doWrite(uploadFile, entityStream); + } + + protected void doWrite(java.nio.file.Path uploadFile, OutputStream out) throws IOException { + Files.copy(uploadFile, out); + } +} diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/PathPartBodyHandler.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/PathPartBodyHandler.java new file mode 100644 index 0000000000000..484459a03dffb --- /dev/null +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/providers/serialisers/PathPartBodyHandler.java @@ -0,0 +1,47 @@ +package org.jboss.resteasy.reactive.common.providers.serialisers; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.file.Files; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import org.jboss.resteasy.reactive.PathPart; + +public class PathPartBodyHandler implements MessageBodyWriter { + + public static final int BUFFER_SIZE = 8192; + + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return PathPart.class.isAssignableFrom(type); + } + + public void writeTo(PathPart uploadFile, Class type, Type genericType, + Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream) throws IOException { + httpHeaders.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(uploadFile.count)); + doWrite(uploadFile, entityStream); + } + + protected void doWrite(PathPart uploadFile, OutputStream out) throws IOException { + doWrite(Files.newInputStream(uploadFile.file), uploadFile.offset, uploadFile.count, out); + } + + static void doWrite(InputStream inputStream, long offset, long count, OutputStream out) throws IOException { + try (InputStream in = inputStream) { + in.skip(offset); + long remaining = count; + byte[] buf = new byte[BUFFER_SIZE]; + int n; + while ((n = in.read(buf, 0, Math.min(BUFFER_SIZE, (int) remaining))) > 0) { + out.write(buf, 0, n); + remaining -= n; + } + } + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ServerSerialisers.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ServerSerialisers.java index e816c4a7fd682..8ea1f19df6089 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ServerSerialisers.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ServerSerialisers.java @@ -29,6 +29,8 @@ import javax.ws.rs.ext.MessageBodyReader; import javax.ws.rs.ext.MessageBodyWriter; import javax.ws.rs.ext.WriterInterceptor; +import org.jboss.resteasy.reactive.FilePart; +import org.jboss.resteasy.reactive.PathPart; import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.headers.HeaderUtil; import org.jboss.resteasy.reactive.common.jaxrs.ConfigurationImpl; @@ -47,9 +49,12 @@ import org.jboss.resteasy.reactive.server.providers.serialisers.ServerCharacterMessageBodyHandler; import org.jboss.resteasy.reactive.server.providers.serialisers.ServerDefaultTextPlainBodyHandler; import org.jboss.resteasy.reactive.server.providers.serialisers.ServerFileBodyHandler; +import org.jboss.resteasy.reactive.server.providers.serialisers.ServerFilePartBodyHandler; import org.jboss.resteasy.reactive.server.providers.serialisers.ServerFormUrlEncodedProvider; import org.jboss.resteasy.reactive.server.providers.serialisers.ServerInputStreamMessageBodyHandler; import org.jboss.resteasy.reactive.server.providers.serialisers.ServerNumberMessageBodyHandler; +import org.jboss.resteasy.reactive.server.providers.serialisers.ServerPathBodyHandler; +import org.jboss.resteasy.reactive.server.providers.serialisers.ServerPathPartBodyHandler; import org.jboss.resteasy.reactive.server.providers.serialisers.ServerReaderBodyHandler; import org.jboss.resteasy.reactive.server.providers.serialisers.ServerStringMessageBodyHandler; import org.jboss.resteasy.reactive.server.spi.ServerHttpRequest; @@ -104,6 +109,12 @@ public void accept(ResteasyReactiveRequestContext context) { MediaType.WILDCARD), new BuiltinWriter(File.class, ServerFileBodyHandler.class, MediaType.WILDCARD), + new BuiltinWriter(FilePart.class, ServerFilePartBodyHandler.class, + MediaType.WILDCARD), + new BuiltinWriter(java.nio.file.Path.class, ServerPathBodyHandler.class, + MediaType.WILDCARD), + new BuiltinWriter(PathPart.class, ServerPathPartBodyHandler.class, + MediaType.WILDCARD), }; private static final String CONTENT_TYPE = "Content-Type"; // use this instead of the Vert.x constant because the TCK expects upper case diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeInterceptorDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeInterceptorDeployment.java index e7b2d3af88edc..3c4c379576f2c 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeInterceptorDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeInterceptorDeployment.java @@ -336,6 +336,28 @@ public List setupRequestFilterHandler() { } return handlers; } + + public boolean hasWriterInterceptors() { + if (method.getNameBindingNames().isEmpty() && methodSpecificReaderInterceptorsMap.isEmpty() + && methodSpecificWriterInterceptorsMap.isEmpty()) { + if (globalInterceptorHandler != null) { + return globalInterceptorHandler.hasWriterInterceptors(); + } + } else if (nameReaderInterceptorsMap.isEmpty() && nameWriterInterceptorsMap.isEmpty() + && methodSpecificReaderInterceptorsMap.isEmpty() && methodSpecificWriterInterceptorsMap.isEmpty()) { + // in this case there are no filters that match the qualifiers, so let's just reuse the global handler + if (globalInterceptorHandler != null) { + return globalInterceptorHandler.hasWriterInterceptors(); + } + } else { + // this is not optimal at all, but this method is only used for return types of AsyncFile so limited impact + TreeMap, WriterInterceptor> writerInterceptorsToUse = buildInterceptorMap( + globalWriterInterceptorsMap, nameWriterInterceptorsMap, methodSpecificWriterInterceptorsMap, method, + false); + return !writerInterceptorsToUse.isEmpty(); + } + return false; + } } /** diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index 40814b4837c6c..d24c66a8d22ee 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -170,6 +170,13 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, } } + // special case for AsyncFile which can't do async IO and handle interceptors + if (method.getReturnType().equals("Lio/vertx/core/file/AsyncFile;") + && interceptorDeployment.hasWriterInterceptors()) { + throw new RuntimeException( + "Endpoints that return an AsyncFile cannot have any WriterInterceptor set"); + } + //spec doesn't seem to test this, but RESTEasy does not run request filters again for sub resources (which makes sense) if (!locatableResource) { List containerRequestFilterHandlers = interceptorDeployment diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/InterceptorHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/InterceptorHandler.java index 52e424d098358..463ad286591f0 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/InterceptorHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/InterceptorHandler.java @@ -20,4 +20,8 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti requestContext.setWriterInterceptors(writerInterceptors); requestContext.setReaderInterceptors(readerInterceptors); } + + public boolean hasWriterInterceptors() { + return writerInterceptors != null && writerInterceptors.length > 0; + } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java index 47b5c66573c87..29ee04f42fdd0 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java @@ -36,7 +36,7 @@ public boolean isWriteable(Class type, Type genericType, ResteasyReactiveReso @Override public void writeResponse(File o, Type genericType, ServerRequestContext context) throws WebApplicationException { ServerHttpResponse vertxResponse = context.serverResponse(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ByteArrayOutputStream baos = new ByteArrayOutputStream((int) o.length()); try { doWrite(o, baos); } catch (IOException e) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFilePartBodyHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFilePartBodyHandler.java new file mode 100644 index 0000000000000..fcf169fb58f7a --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFilePartBodyHandler.java @@ -0,0 +1,39 @@ +package org.jboss.resteasy.reactive.server.providers.serialisers; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.Provider; +import org.jboss.resteasy.reactive.FilePart; +import org.jboss.resteasy.reactive.common.providers.serialisers.FilePartBodyHandler; +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; +import org.jboss.resteasy.reactive.server.spi.ServerHttpResponse; +import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; +import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; + +// TODO: this is very simplistic at the moment + +@Provider +@Produces("*/*") +@Consumes("*/*") +public class ServerFilePartBodyHandler extends FilePartBodyHandler implements ServerMessageBodyWriter { + + @Override + public long getSize(FilePart o, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return o.file.length(); + } + + @Override + public boolean isWriteable(Class type, Type genericType, ResteasyReactiveResourceInfo target, MediaType mediaType) { + return FilePart.class.isAssignableFrom(type); + } + + @Override + public void writeResponse(FilePart o, Type genericType, ServerRequestContext context) throws WebApplicationException { + ServerHttpResponse vertxResponse = context.serverResponse(); + vertxResponse.sendFile(o.file.getPath(), o.offset, o.count); + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java new file mode 100644 index 0000000000000..38c4b7278d207 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java @@ -0,0 +1,43 @@ +package org.jboss.resteasy.reactive.server.providers.serialisers; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.file.Files; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.Provider; +import org.jboss.resteasy.reactive.common.providers.serialisers.PathBodyHandler; +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; +import org.jboss.resteasy.reactive.server.spi.ServerHttpResponse; +import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; +import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; + +@Provider +@Produces("*/*") +public class ServerPathBodyHandler extends PathBodyHandler implements ServerMessageBodyWriter { + + @Override + public long getSize(java.nio.file.Path o, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + try { + return Files.size(o); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public boolean isWriteable(Class type, Type genericType, ResteasyReactiveResourceInfo target, MediaType mediaType) { + return java.nio.file.Path.class.isAssignableFrom(type); + } + + @Override + public void writeResponse(java.nio.file.Path o, Type genericType, ServerRequestContext context) + throws WebApplicationException { + ServerHttpResponse serverResponse = context.serverResponse(); + // sendFile implies end(), even though javadoc doesn't say, if you add end() it will throw + serverResponse.sendFile(o.toString(), 0, Long.MAX_VALUE); + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathPartBodyHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathPartBodyHandler.java new file mode 100644 index 0000000000000..bd586839c22df --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathPartBodyHandler.java @@ -0,0 +1,44 @@ +package org.jboss.resteasy.reactive.server.providers.serialisers; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.file.Files; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.Provider; +import org.jboss.resteasy.reactive.PathPart; +import org.jboss.resteasy.reactive.common.providers.serialisers.PathPartBodyHandler; +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; +import org.jboss.resteasy.reactive.server.spi.ServerHttpResponse; +import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; +import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; + +@Provider +@Produces("*/*") +public class ServerPathPartBodyHandler extends PathPartBodyHandler implements ServerMessageBodyWriter { + + @Override + public long getSize(PathPart o, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + try { + return Files.size(o.file); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public boolean isWriteable(Class type, Type genericType, ResteasyReactiveResourceInfo target, MediaType mediaType) { + return PathPart.class.isAssignableFrom(type); + } + + @Override + public void writeResponse(PathPart o, Type genericType, ServerRequestContext context) + throws WebApplicationException { + ServerHttpResponse serverResponse = context.serverResponse(); + // sendFile implies end(), even though javadoc doesn't say, if you add end() it will throw + serverResponse.sendFile(o.file.toString(), o.offset, o.count); + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpResponse.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpResponse.java index f65b8e21bd57c..ae4f0b3b33d45 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpResponse.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpResponse.java @@ -34,9 +34,15 @@ public interface ServerHttpResponse { CompletionStage write(byte[] data); + ServerHttpResponse sendFile(String path, long offset, long length); + OutputStream createResponseOutputStream(); void setPreCommitListener(Consumer task); ServerHttpResponse addCloseHandler(Runnable onClose); + + boolean isWriteQueueFull(); + + ServerHttpResponse addDrainHandler(Runnable onDrain); } diff --git a/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java b/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java index 04a838b51adbb..4c8981ed36085 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java @@ -347,6 +347,12 @@ public void handle(AsyncResult event) { return ret; } + @Override + public ServerHttpResponse sendFile(String path, long offset, long length) { + response.sendFile(path, offset, length); + return this; + } + @Override public OutputStream createResponseOutputStream() { return new ResteasyReactiveOutputStream(this); @@ -364,6 +370,22 @@ public void handle(Void event) { } } + @Override + public ServerHttpResponse addDrainHandler(Runnable onDrain) { + response.drainHandler(new Handler() { + @Override + public void handle(Void event) { + onDrain.run(); + } + }); + return this; + } + + @Override + public boolean isWriteQueueFull() { + return response.writeQueueFull(); + } + public HttpServerRequest vertxServerRequest() { return request; }