diff --git a/pom.xml b/pom.xml index f13bded7..623bc1cf 100644 --- a/pom.xml +++ b/pom.xml @@ -39,8 +39,9 @@ examples-jsapi json-binding microprofile-openapi + servlet-example smime + standalone-multipart tracing-example - servlet-example diff --git a/standalone-multipart/README.adoc b/standalone-multipart/README.adoc new file mode 100644 index 00000000..2c584976 --- /dev/null +++ b/standalone-multipart/README.adoc @@ -0,0 +1,33 @@ += Standalone multipart/form-data Example + +In https://jakarta.ee/specifications/restful-ws/3.1/[Jakarta RESTful Web Services 3.1] the `SeBootstrap` API was +introduced as well as the `EntityPart` API for multipart data. This example shows how to use these API's with RESTEasy. + +== Building + +To build the `standalone-multipart` quickstart you must have https://maven.apache.org/[Maven] installed and at least +Java 11. Then you simply need to run the following: + +---- +mvn clean verify +---- + +This will create a `standalone-multipart.jar` which can be executed from the command line. A test is also executed as +part of the build. + +== Running the Quickstart + +The `standalone-multipart.jar` created can be executed from the command. + +---- +java -jar target/standalone-multipart.jar +---- + +This will start an Undertow container with RESTEasy and CDI support. Then make a multipart/form-data request and print +the results of the request. You should end up seeing something like: + +--- +Container running at http://localhost:8081/ +OK +{"name":"RESTEasy","data":"test content","entity":"entity-part"} +--- \ No newline at end of file diff --git a/standalone-multipart/pom.xml b/standalone-multipart/pom.xml new file mode 100644 index 00000000..6ac076e0 --- /dev/null +++ b/standalone-multipart/pom.xml @@ -0,0 +1,172 @@ + + + + + 4.0.0 + + dev.resteasy.tools + resteasy-parent + 2.0.3.Final + + + + dev.resteasy.examples + standalone-multipart + 6.1.0.Final-SNAPSHOT + RESTEasy Quick Start: Standalone MultiPart Example + + + + 4.0.1 + 3.1.0 + 3.0.2.Final + 6.2.5.Final + 5.10.0 + + + 1.2.3 + + + + + + org.jboss.resteasy + resteasy-bom + ${version.org.jboss.resteasy} + pom + import + + + org.junit + junit-bom + ${version.org.junit} + pom + import + + + + + + + jakarta.ws.rs + jakarta.ws.rs-api + ${version.jakarta.ws.rs.api} + + + + org.jboss.logmanager + jboss-logmanager + ${version.org.jboss.logmanager} + + + + org.jboss.resteasy + resteasy-client + + + org.jboss.resteasy + resteasy-json-p-provider + + + org.jboss.resteasy + resteasy-multipart-provider + + + org.jboss.resteasy + resteasy-undertow-cdi + + + + + org.junit.jupiter + junit-jupiter + test + + + + + ${project.artifactId} + + + net.revelc.code.formatter + formatter-maven-plugin + + + net.revelc.code + impsort-maven-plugin + + + + org.jboss.jandex + jandex-maven-plugin + ${version.jandex.maven.plugin} + + + make-index + + jandex + + + + + + maven-jar-plugin + + + + dev.resteasy.examples.multipart.Main + dev.resteasy.quickstart.bootstrap + + + + + + maven-shade-plugin + + + + + module-info.class + + + + + + package + + shade + + + + + + maven-surefire-plugin + + + org.jboss.logmanager.LogManager + + + + + + + \ No newline at end of file diff --git a/standalone-multipart/src/main/java/dev/resteasy/examples/multipart/Main.java b/standalone-multipart/src/main/java/dev/resteasy/examples/multipart/Main.java new file mode 100644 index 00000000..c71ef79a --- /dev/null +++ b/standalone-multipart/src/main/java/dev/resteasy/examples/multipart/Main.java @@ -0,0 +1,82 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.resteasy.examples.multipart; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import jakarta.ws.rs.SeBootstrap; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.EntityPart; +import jakarta.ws.rs.core.GenericEntity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * An entry point for starting a REST container + * + * @author James R. Perkins + */ +public class Main { + + public static void main(final String[] args) throws Exception { + System.setProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager"); + + // Start the container + final SeBootstrap.Instance instance = SeBootstrap.start(RestActivator.class) + .thenApply(i -> { + System.out.printf("Container running at %s%n", i.configuration().baseUri()); + return i; + }).toCompletableFuture().get(); + + // Create the client and make a multipart/form-data request + try (Client client = ClientBuilder.newClient()) { + // Create the entity parts for the request + final List multipart = List.of( + EntityPart.withName("name") + .content("RESTEasy") + .mediaType(MediaType.TEXT_PLAIN_TYPE) + .build(), + EntityPart.withName("entity") + .content("entity-part") + .mediaType(MediaType.TEXT_PLAIN_TYPE) + .build(), + EntityPart.withName("data") + .content("test content".getBytes(StandardCharsets.UTF_8)) + .mediaType(MediaType.APPLICATION_OCTET_STREAM_TYPE) + .build()); + try ( + Response response = client.target(instance.configuration().baseUriBuilder().path("/api/upload")) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(new GenericEntity<>(multipart) { + }, MediaType.MULTIPART_FORM_DATA_TYPE))) { + printResponse(response); + } + } + } + + private static void printResponse(final Response response) { + System.out.println(response.getStatusInfo()); + System.out.println(response.readEntity(String.class)); + } + +} diff --git a/standalone-multipart/src/main/java/dev/resteasy/examples/multipart/RestActivator.java b/standalone-multipart/src/main/java/dev/resteasy/examples/multipart/RestActivator.java new file mode 100644 index 00000000..57a543c1 --- /dev/null +++ b/standalone-multipart/src/main/java/dev/resteasy/examples/multipart/RestActivator.java @@ -0,0 +1,38 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.resteasy.examples.multipart; + +import jakarta.enterprise.inject.Vetoed; +import jakarta.servlet.annotation.MultipartConfig; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +/** + * Activates the REST application. + * + * @author James R. Perkins + */ +@ApplicationPath("/api") +// Currently required to enable multipart/form-data in Undertow see https://issues.redhat.com/browse/RESTEASY-3376 +@MultipartConfig +// See https://issues.redhat.com/browse/RESTEASY-3376 +@Vetoed +public class RestActivator extends Application { +} diff --git a/standalone-multipart/src/main/java/dev/resteasy/examples/multipart/UploadResource.java b/standalone-multipart/src/main/java/dev/resteasy/examples/multipart/UploadResource.java new file mode 100644 index 00000000..2f6c104f --- /dev/null +++ b/standalone-multipart/src/main/java/dev/resteasy/examples/multipart/UploadResource.java @@ -0,0 +1,67 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.resteasy.examples.multipart; + +import java.io.IOException; +import java.io.InputStream; + +import jakarta.json.Json; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.ServerErrorException; +import jakarta.ws.rs.core.EntityPart; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * A simple resource for creating a greeting. + * + * @author James R. Perkins + */ +@Path("/") +public class UploadResource { + + @POST + @Path("upload") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + public Response upload(@FormParam("name") final String name, @FormParam("data") final InputStream data, + @FormParam("entity") final EntityPart entityPart) { + final JsonObjectBuilder builder = Json.createObjectBuilder(); + builder.add("name", name); + + // Read the data into a string + try (data) { + builder.add("data", new String(data.readAllBytes())); + } catch (IOException e) { + throw new ServerErrorException("Failed to read data " + data, Response.Status.BAD_REQUEST); + } + try { + builder.add(entityPart.getName(), entityPart.getContent(String.class)); + } catch (IOException e) { + throw new ServerErrorException("Failed to read entity " + entityPart, Response.Status.BAD_REQUEST); + } + return Response.ok(builder.build()).build(); + } +} diff --git a/standalone-multipart/src/main/resources/META-INF/beans.xml b/standalone-multipart/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000..acab26c3 --- /dev/null +++ b/standalone-multipart/src/main/resources/META-INF/beans.xml @@ -0,0 +1,23 @@ + + + + \ No newline at end of file diff --git a/standalone-multipart/src/main/resources/logging.properties b/standalone-multipart/src/main/resources/logging.properties new file mode 100644 index 00000000..c3c68703 --- /dev/null +++ b/standalone-multipart/src/main/resources/logging.properties @@ -0,0 +1,35 @@ +# +# JBoss, Home of Professional Open Source. +# +# Copyright 2022 Red Hat, Inc., and individual contributors +# as indicated by the @author tags. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +loggers=org.jboss.resteasy + +logger.level=INFO +logger.handlers=CONSOLE + +logger.org.jboss.resteasy.level=${log.level:INFO} + +handler.CONSOLE=org.jboss.logmanager.handlers.ConsoleHandler +handler.CONSOLE.formatter=COLOR-PATTERN +handler.CONSOLE.properties=autoFlush,target +handler.CONSOLE.autoFlush=true +handler.CONSOLE.target=SYSTEM_OUT + +formatter.COLOR-PATTERN=org.jboss.logmanager.formatters.ColorPatternFormatter +formatter.COLOR-PATTERN.properties=pattern +formatter.COLOR-PATTERN.pattern=%d{HH\:mm\:ss,SSS} %-5p [%c] (%t) %s%e%n \ No newline at end of file diff --git a/standalone-multipart/src/test/java/dev/resteasy/examples/multipart/UploadTestCase.java b/standalone-multipart/src/test/java/dev/resteasy/examples/multipart/UploadTestCase.java new file mode 100644 index 00000000..664c705b --- /dev/null +++ b/standalone-multipart/src/test/java/dev/resteasy/examples/multipart/UploadTestCase.java @@ -0,0 +1,88 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.resteasy.examples.multipart; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import jakarta.ws.rs.SeBootstrap; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.EntityPart; +import jakarta.ws.rs.core.GenericEntity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * @author James R. Perkins + */ +public class UploadTestCase { + + private static SeBootstrap.Instance INSTANCE; + + @BeforeAll + public static void startInstance() throws Exception { + INSTANCE = SeBootstrap.start(RestActivator.class) + .toCompletableFuture().get(10, TimeUnit.SECONDS); + Assertions.assertNotNull(INSTANCE, "Failed to start instance"); + } + + @AfterAll + public static void stopInstance() throws Exception { + if (INSTANCE != null) { + INSTANCE.stop() + .toCompletableFuture() + .get(10, TimeUnit.SECONDS); + } + } + + @Test + public void upload() throws Exception { + try (Client client = ClientBuilder.newClient()) { + final List multipart = List.of( + EntityPart.withName("name") + .content("RESTEasy") + .mediaType(MediaType.TEXT_PLAIN_TYPE) + .build(), + EntityPart.withName("data") + .content("test content".getBytes(StandardCharsets.UTF_8)) + .mediaType(MediaType.APPLICATION_OCTET_STREAM_TYPE) + .build(), + EntityPart.withName("entity") + .content("entity-data") + .mediaType(MediaType.TEXT_PLAIN_TYPE) + .build()); + try ( + Response response = client.target(INSTANCE.configuration().baseUriBuilder().path("api/upload")) + .request(MediaType.APPLICATION_JSON) + .post(Entity.entity(new GenericEntity<>(multipart) { + }, MediaType.MULTIPART_FORM_DATA))) { + Assertions.assertEquals(Response.Status.OK, response.getStatusInfo()); + } + } + } +}