From 96135286ba942f45fdf57af3093660720814cea2 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 31 Aug 2023 16:04:51 +0300 Subject: [PATCH] Add support for Path as a JAX-RS method body type --- .../test/MessageBodyReaderTests.java | 2 +- .../deployment/ResteasyReactiveProcessor.java | 10 +- .../multipart/PathInputWithDeleteTest.java | 80 ++++++++++++++++ .../multipart/PathInputWithoutDeleteTest.java | 96 +++++++++++++++++++ .../runtime/QuarkusServerFileBodyHandler.java | 42 +------- .../runtime/QuarkusServerPathBodyHandler.java | 96 +++++++++++++++++++ 6 files changed, 282 insertions(+), 44 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/PathInputWithDeleteTest.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/PathInputWithoutDeleteTest.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusServerPathBodyHandler.java diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MessageBodyReaderTests.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MessageBodyReaderTests.java index ab49f42c3381b..68bd47c204deb 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MessageBodyReaderTests.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MessageBodyReaderTests.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import jakarta.ws.rs.core.HttpHeaders; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -17,6 +16,7 @@ import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.container.CompletionCallback; import jakarta.ws.rs.container.ConnectionCallback; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; 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 32ddce0dfd0ec..742bec269c29b 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 @@ -12,6 +12,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; @@ -167,6 +168,7 @@ import io.quarkus.resteasy.reactive.common.runtime.ResteasyReactiveConfig; import io.quarkus.resteasy.reactive.server.EndpointDisabled; import io.quarkus.resteasy.reactive.server.runtime.QuarkusServerFileBodyHandler; +import io.quarkus.resteasy.reactive.server.runtime.QuarkusServerPathBodyHandler; import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveInitialiser; import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveRecorder; import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveRuntimeRecorder; @@ -1024,9 +1026,13 @@ private static String determineHandledGenericTypeOfProviderInterface(Class pr } @BuildStep - public void builtInReaderOverrides(BuildProducer producer) { - producer.produce(new BuiltInReaderOverrideBuildItem(ServerFileBodyHandler.class.getName(), + public void fileHandling(BuildProducer overrideProducer, + BuildProducer readerProducer) { + overrideProducer.produce(new BuiltInReaderOverrideBuildItem(ServerFileBodyHandler.class.getName(), QuarkusServerFileBodyHandler.class.getName())); + readerProducer.produce( + new MessageBodyReaderBuildItem(QuarkusServerPathBodyHandler.class.getName(), Path.class.getName(), List.of( + 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/multipart/PathInputWithDeleteTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/PathInputWithDeleteTest.java new file mode 100644 index 0000000000000..b03d853f2fcab --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/PathInputWithDeleteTest.java @@ -0,0 +1,80 @@ +package io.quarkus.resteasy.reactive.server.test.multipart; + +import static org.hamcrest.CoreMatchers.equalTo; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.function.Supplier; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class PathInputWithDeleteTest extends AbstractMultipartTest { + + private static final java.nio.file.Path uploadDir = Paths.get("file-uploads"); + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(Resource.class) + .addAsResource(new StringAsset( + "quarkus.http.body.uploads-directory=" + + uploadDir.toString() + "\n"), + "application.properties"); + } + + }); + + private final File HTML_FILE = new File("./src/test/resources/test.html"); + private final File HTML_FILE2 = new File("./src/test/resources/test2.html"); + + @Test + public void test() throws IOException { + RestAssured.given() + .contentType("application/octet-stream") + .body(HTML_FILE) + .when() + .post("/test") + .then() + .statusCode(200) + .body(equalTo(fileSizeAsStr(HTML_FILE))); + + awaitUploadDirectoryToEmpty(uploadDir); + + RestAssured.given() + .contentType("application/octet-stream") + .body(HTML_FILE2) + .when() + .post("/test") + .then() + .statusCode(200) + .body(equalTo(fileSizeAsStr(HTML_FILE2))); + + awaitUploadDirectoryToEmpty(uploadDir); + } + + @Path("test") + public static class Resource { + + @POST + @Consumes("application/octet-stream") + public long size(java.nio.file.Path file) throws IOException { + return Files.size(file); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/PathInputWithoutDeleteTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/PathInputWithoutDeleteTest.java new file mode 100644 index 0000000000000..08b7f7181da6b --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/PathInputWithoutDeleteTest.java @@ -0,0 +1,96 @@ +package io.quarkus.resteasy.reactive.server.test.multipart; + +import static org.hamcrest.CoreMatchers.equalTo; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.function.Supplier; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class PathInputWithoutDeleteTest extends AbstractMultipartTest { + + private static final java.nio.file.Path uploadDir = Paths.get("file-uploads"); + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(Resource.class) + .addAsResource(new StringAsset( + // keep the files around so we can assert the outcome + "quarkus.http.body.delete-uploaded-files-on-end=false\nquarkus.http.body.uploads-directory=" + + uploadDir.toString() + "\n"), + "application.properties"); + } + + }); + + private final File HTML_FILE = new File("./src/test/resources/test.html"); + private final File HTML_FILE2 = new File("./src/test/resources/test2.html"); + + @BeforeEach + public void assertEmptyUploads() { + Assertions.assertTrue(isDirectoryEmpty(uploadDir)); + } + + @AfterEach + public void clearDirectory() { + clearDirectory(uploadDir); + } + + @Test + public void test() throws IOException { + RestAssured.given() + .contentType("application/octet-stream") + .body(HTML_FILE) + .when() + .post("/test") + .then() + .statusCode(200) + .body(equalTo(fileSizeAsStr(HTML_FILE))); + + // ensure that the 3 uploaded files where created on disk + Assertions.assertEquals(1, uploadDir.toFile().listFiles().length); + + RestAssured.given() + .contentType("application/octet-stream") + .body(HTML_FILE2) + .when() + .post("/test") + .then() + .statusCode(200) + .body(equalTo(fileSizeAsStr(HTML_FILE2))); + + // ensure that the 3 uploaded files where created on disk + Assertions.assertEquals(2, uploadDir.toFile().listFiles().length); + } + + @Path("test") + public static class Resource { + + @POST + @Consumes("application/octet-stream") + public long size(java.nio.file.Path file) throws IOException { + return Files.size(file); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusServerFileBodyHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusServerFileBodyHandler.java index 01e6b53b33b35..e8c8effc7300a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusServerFileBodyHandler.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusServerFileBodyHandler.java @@ -1,28 +1,24 @@ package io.quarkus.resteasy.reactive.server.runtime; +import static io.quarkus.resteasy.reactive.server.runtime.QuarkusServerPathBodyHandler.createFile; import static org.jboss.resteasy.reactive.common.providers.serialisers.FileBodyHandler.PREFIX; import static org.jboss.resteasy.reactive.common.providers.serialisers.FileBodyHandler.SUFFIX; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.UncheckedIOException; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.nio.file.Paths; import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.container.CompletionCallback; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedMap; import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.common.providers.serialisers.FileBodyHandler; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; -import org.jboss.resteasy.reactive.server.spi.RuntimeConfiguration; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyReader; import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; @@ -56,40 +52,4 @@ public File readFrom(Class type, Type genericType, Annotation[] annotation // however this should never be called in a real world scenario return FileBodyHandler.doRead(httpHeaders, entityStream, Files.createTempFile(PREFIX, SUFFIX).toFile()); } - - private Path createFile(ServerRequestContext context) throws IOException { - RuntimeConfiguration.Body runtimeBodyConfiguration = ResteasyReactiveRecorder.getCurrentDeployment() - .getRuntimeConfiguration().body(); - boolean deleteUploadedFilesOnEnd = runtimeBodyConfiguration.deleteUploadedFilesOnEnd(); - String uploadsDirectoryStr = runtimeBodyConfiguration.uploadsDirectory(); - Path uploadDirectory = Paths.get(uploadsDirectoryStr); - try { - Files.createDirectories(uploadDirectory); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - - Path file = Files.createTempFile(uploadDirectory, PREFIX, SUFFIX); - if (deleteUploadedFilesOnEnd) { - context.registerCompletionCallback(new CompletionCallback() { - @Override - public void onComplete(Throwable throwable) { - ResteasyReactiveRecorder.EXECUTOR_SUPPLIER.get().execute(new Runnable() { - @Override - public void run() { - if (Files.exists(file)) { - try { - Files.delete(file); - } catch (NoSuchFileException e) { // ignore - } catch (IOException e) { - log.error("Cannot remove uploaded file " + file, e); - } - } - } - }); - } - }); - } - return file; - } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusServerPathBodyHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusServerPathBodyHandler.java new file mode 100644 index 0000000000000..e6dcb8ff7dfa9 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusServerPathBodyHandler.java @@ -0,0 +1,96 @@ +package io.quarkus.resteasy.reactive.server.runtime; + +import static org.jboss.resteasy.reactive.common.providers.serialisers.FileBodyHandler.PREFIX; +import static org.jboss.resteasy.reactive.common.providers.serialisers.FileBodyHandler.SUFFIX; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.container.CompletionCallback; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.common.providers.serialisers.FileBodyHandler; +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; +import org.jboss.resteasy.reactive.server.spi.RuntimeConfiguration; +import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyReader; +import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; + +public class QuarkusServerPathBodyHandler implements ServerMessageBodyReader { + + private static final Logger log = Logger.getLogger(QuarkusServerPathBodyHandler.class); + + @Override + public boolean isReadable(Class type, Type genericType, ResteasyReactiveResourceInfo lazyMethod, + MediaType mediaType) { + return Path.class.equals(type); + } + + @Override + public Path readFrom(Class type, Type genericType, MediaType mediaType, ServerRequestContext context) + throws WebApplicationException, IOException { + Path file = createFile(context); + return FileBodyHandler.doRead(context.getRequestHeaders().getRequestHeaders(), context.getInputStream(), file.toFile()) + .toPath(); + } + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return File.class.equals(type); + } + + @Override + public Path readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, InputStream entityStream) + throws IOException, WebApplicationException { + // unfortunately we don't do much here to avoid the file leak + // however this should never be called in a real world scenario + return FileBodyHandler.doRead(httpHeaders, entityStream, Files.createTempFile(PREFIX, SUFFIX).toFile()).toPath(); + } + + static Path createFile(ServerRequestContext context) throws IOException { + RuntimeConfiguration.Body runtimeBodyConfiguration = ResteasyReactiveRecorder.getCurrentDeployment() + .getRuntimeConfiguration().body(); + boolean deleteUploadedFilesOnEnd = runtimeBodyConfiguration.deleteUploadedFilesOnEnd(); + String uploadsDirectoryStr = runtimeBodyConfiguration.uploadsDirectory(); + Path uploadDirectory = Paths.get(uploadsDirectoryStr); + try { + Files.createDirectories(uploadDirectory); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + Path file = Files.createTempFile(uploadDirectory, PREFIX, SUFFIX); + if (deleteUploadedFilesOnEnd) { + context.registerCompletionCallback(new CompletionCallback() { + @Override + public void onComplete(Throwable throwable) { + ResteasyReactiveRecorder.EXECUTOR_SUPPLIER.get().execute(new Runnable() { + @Override + public void run() { + if (Files.exists(file)) { + try { + Files.delete(file); + } catch (NoSuchFileException e) { // ignore + } catch (IOException e) { + log.error("Cannot remove uploaded file " + file, e); + } + } + } + }); + } + }); + } + return file; + } +}