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());
+ }
+ }
+ }
+}