diff --git a/core-client/src/main/java/org/glassfish/jersey/client/RequestProcessingInitializationStage.java b/core-client/src/main/java/org/glassfish/jersey/client/RequestProcessingInitializationStage.java
index 0b9da834ea..1ac6b75383 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/RequestProcessingInitializationStage.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/RequestProcessingInitializationStage.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,7 +16,9 @@
package org.glassfish.jersey.client;
+import java.util.Collection;
import java.util.Collections;
+import java.util.Iterator;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
@@ -26,6 +28,7 @@
import jakarta.inject.Provider;
+import org.glassfish.jersey.innate.spi.MessageBodyWorkersSettable;
import org.glassfish.jersey.internal.inject.InjectionManager;
import org.glassfish.jersey.internal.inject.Providers;
import org.glassfish.jersey.internal.util.collection.Ref;
@@ -80,6 +83,21 @@ public ClientRequest apply(ClientRequest requestContext) {
requestContext.setWriterInterceptors(writerInterceptors);
requestContext.setReaderInterceptors(readerInterceptors);
+ if (requestContext.getEntity() != null) {
+ setWorkers(requestContext.getEntity());
+ }
+
return requestContext;
}
+
+ private void setWorkers(Object entity) {
+ if (MessageBodyWorkersSettable.class.isInstance(entity)) {
+ ((MessageBodyWorkersSettable) entity).setMessageBodyWorkers(workersProvider);
+ } else if (Collection.class.isInstance(entity)) {
+ Iterator it = ((Collection) entity).iterator();
+ while (it.hasNext()) {
+ setWorkers(it.next());
+ }
+ }
+ }
}
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/spi/MessageBodyWorkersSettable.java b/core-common/src/main/java/org/glassfish/jersey/innate/spi/MessageBodyWorkersSettable.java
new file mode 100644
index 0000000000..09c751ee1d
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/spi/MessageBodyWorkersSettable.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.innate.spi;
+
+import org.glassfish.jersey.message.MessageBodyWorkers;
+
+/**
+ * Entity type that expects the {@link MessageBodyWorkers} to be set for converting the entity to another types.
+ */
+public interface MessageBodyWorkersSettable {
+
+ /**
+ * Set message body workers used to transform an entity stream into particular Java type.
+ *
+ * @param messageBodyWorkers message body workers.
+ */
+ public void setMessageBodyWorkers(final MessageBodyWorkers messageBodyWorkers);
+}
diff --git a/etc/config/copyright-exclude b/etc/config/copyright-exclude
index de5bfc6931..9c4e32df48 100644
--- a/etc/config/copyright-exclude
+++ b/etc/config/copyright-exclude
@@ -93,3 +93,4 @@ NOTICE.md
/media/json-binding/src/test/java/org/glassfish/jersey/jsonb/internal/JsonBindingProviderTest.java
/connectors/jdk-connector/src/test/resources
/tools
+/media/multipart/src/test/resources/multipart
diff --git a/media/multipart/pom.xml b/media/multipart/pom.xml
index a54c329363..6ac6735be4 100644
--- a/media/multipart/pom.xml
+++ b/media/multipart/pom.xml
@@ -92,6 +92,12 @@
${project.version}
test
+
+ org.glassfish.jersey.media
+ jersey-media-json-processing
+ ${project.version}
+ test
+
org.junit.jupiter
diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/BodyPart.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/BodyPart.java
index 7287759a3b..d672e1cbaa 100644
--- a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/BodyPart.java
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/BodyPart.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,10 +16,13 @@
package org.glassfish.jersey.media.multipart;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.text.ParseException;
+import java.util.Arrays;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.core.GenericType;
@@ -28,6 +31,7 @@
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.Providers;
+import org.glassfish.jersey.innate.spi.MessageBodyWorkersSettable;
import org.glassfish.jersey.internal.util.collection.ImmutableMultivaluedMap;
import org.glassfish.jersey.media.multipart.internal.LocalizationMessages;
import org.glassfish.jersey.message.MessageBodyWorkers;
@@ -41,7 +45,7 @@
* @author Paul Sandoz
* @author Michal Gajdos
*/
-public class BodyPart {
+public class BodyPart implements MessageBodyWorkersSettable {
protected ContentDisposition contentDisposition = null;
@@ -285,7 +289,15 @@ T getEntityAs(final GenericType genericEntity) {
}
T getEntityAs(final Class type, Type genericType) {
- if (entity == null || !(entity instanceof BodyPartEntity)) {
+ InputStream inputStream = null;
+ if (BodyPartEntity.class.isInstance(entity)) {
+ inputStream = ((BodyPartEntity) entity).getInputStream();
+ } else if (InputStream.class.isInstance(entity)) {
+ inputStream = (InputStream) entity;
+ } else if (byte[].class.isInstance(entity)) {
+ inputStream = new ByteArrayInputStream((byte[]) entity);
+ }
+ if (inputStream == null) {
throw new IllegalStateException(LocalizationMessages.ENTITY_HAS_WRONG_TYPE());
}
if (type == BodyPartEntity.class) {
@@ -299,8 +311,7 @@ T getEntityAs(final Class type, Type genericType) {
}
try {
- return reader.readFrom(type, genericType, annotations, mediaType, headers,
- ((BodyPartEntity) entity).getInputStream());
+ return reader.readFrom(type, genericType, annotations, mediaType, headers, inputStream);
} catch (final IOException ioe) {
throw new ProcessingException(LocalizationMessages.ERROR_READING_ENTITY(String.class), ioe);
}
diff --git a/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/ClientFilterTests.java b/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/ClientFilterTests.java
new file mode 100644
index 0000000000..f7bfa539d7
--- /dev/null
+++ b/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/ClientFilterTests.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.media.multipart;
+
+import jakarta.json.Json;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonArrayBuilder;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonObjectBuilder;
+import jakarta.json.JsonValue;
+import jakarta.ws.rs.BadRequestException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.EntityPart;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Tests in clientFilter before the multipart provider is invoked.
+ * Check the workers are set.
+ *
+ * Modified MP Rest Client TCK tests
+ */
+public class ClientFilterTests {
+ /**
+ * Tests that a single file is upload. The response is a simple JSON response with the file information.
+ *
+ * @throws Exception
+ * if a test error occurs
+ */
+ @Test
+ public void uploadFile() throws Exception {
+ try (Client client = createClient()) {
+ final byte[] content;
+ try (InputStream in = ClientFilterTests.class.getResourceAsStream("/multipart/test-file1.txt")) {
+ Assertions.assertNotNull(in, "Could not find /multipart/test-file1.txt");
+ content = in.readAllBytes();
+ }
+ // Send in an InputStream to ensure it works with an InputStream
+ final List files = List.of(EntityPart.withFileName("test-file1.txt")
+ .content(new ByteArrayInputStream(content))
+ .mediaType(MediaType.APPLICATION_OCTET_STREAM_TYPE)
+ .build());
+ try (Response response = client.target("http://localhost").request()
+ .post(Entity.entity(files, MediaType.MULTIPART_FORM_DATA))) {
+ Assertions.assertEquals(201, response.getStatus());
+ final JsonArray jsonArray = response.readEntity(JsonArray.class);
+ Assertions.assertNotNull(jsonArray);
+ Assertions.assertEquals(jsonArray.size(), 1);
+ final JsonObject json = jsonArray.getJsonObject(0);
+ Assertions.assertEquals(json.getString("name"), "test-file1.txt");
+ Assertions.assertEquals(json.getString("fileName"), "test-file1.txt");
+ Assertions.assertEquals(json.getString("content"), "This is a test file for file 1.");
+ }
+ }
+ }
+
+ /**
+ * Tests that two files are upload. The response is a simple JSON response with the file information.
+ *
+ * @throws Exception
+ * if a test error occurs
+ */
+ @Test
+ public void uploadMultipleFiles() throws Exception {
+ try (Client client = createClient()) {
+ final Map entityPartContent = new LinkedHashMap<>(2);
+ try (InputStream in = ClientFilterTests.class.getResourceAsStream("/multipart/test-file1.txt")) {
+ Assertions.assertNotNull(in, "Could not find /multipart/test-file1.txt");
+ entityPartContent.put("test-file1.txt", in.readAllBytes());
+ }
+ try (InputStream in = ClientFilterTests.class.getResourceAsStream("/multipart/test-file2.txt")) {
+ Assertions.assertNotNull(in, "Could not find /multipart/test-file2.txt");
+ entityPartContent.put("test-file2.txt", in.readAllBytes());
+ }
+ final List files = entityPartContent.entrySet()
+ .stream()
+ .map((entry) -> {
+ try {
+ return EntityPart.withName(entry.getKey())
+ .fileName(entry.getKey())
+ .content(entry.getValue())
+ .mediaType(MediaType.APPLICATION_OCTET_STREAM_TYPE)
+ .build();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ })
+ .collect(Collectors.toList());
+
+ try (Response response = client.target("http://localhost").request()
+ .post(Entity.entity(files, MediaType.MULTIPART_FORM_DATA))) {
+ Assertions.assertEquals(201, response.getStatus());
+ final JsonArray jsonArray = response.readEntity(JsonArray.class);
+ Assertions.assertNotNull(jsonArray);
+ Assertions.assertEquals(jsonArray.size(), 2);
+ // Don't assume the results are in a specific order
+ for (JsonValue value : jsonArray) {
+ final JsonObject json = value.asJsonObject();
+ if (json.getString("name").equals("test-file1.txt")) {
+ Assertions.assertEquals(json.getString("fileName"), "test-file1.txt");
+ Assertions.assertEquals(json.getString("content"), "This is a test file for file 1.");
+ } else if (json.getString("name").equals("test-file2.txt")) {
+ Assertions.assertEquals(json.getString("fileName"), "test-file2.txt");
+ Assertions.assertEquals(json.getString("content"), "This is a test file for file 2.");
+ } else {
+ Assertions.fail(String.format("Unexpected entry %s in JSON response: %n%s", json, jsonArray));
+ }
+ }
+ }
+ }
+ }
+
+ private static Client createClient() {
+ return ClientBuilder.newClient().register(new FileManagerFilter());
+ }
+
+ public static class FileManagerFilter implements ClientRequestFilter {
+
+ @Override
+ public void filter(final ClientRequestContext requestContext) throws IOException {
+ if (requestContext.getMethod().equals("POST")) {
+ // Download the file
+ @SuppressWarnings("unchecked")
+ final List entityParts = (List) requestContext.getEntity();
+ final JsonArrayBuilder jsonBuilder = Json.createArrayBuilder();
+ for (EntityPart part : entityParts) {
+ final JsonObjectBuilder jsonPartBuilder = Json.createObjectBuilder();
+ jsonPartBuilder.add("name", part.getName());
+ if (part.getFileName().isPresent()) {
+ jsonPartBuilder.add("fileName", part.getFileName().get());
+ } else {
+ throw new BadRequestException("No file name for entity part " + part);
+ }
+ jsonPartBuilder.add("content", part.getContent(String.class));
+ jsonBuilder.add(jsonPartBuilder);
+ }
+ requestContext.abortWith(Response.status(201).entity(jsonBuilder.build()).build());
+ } else {
+ requestContext
+ .abortWith(Response.status(Response.Status.BAD_REQUEST).entity("Invalid request").build());
+ }
+ }
+ }
+}
diff --git a/media/multipart/src/test/resources/multipart/test-file1.txt b/media/multipart/src/test/resources/multipart/test-file1.txt
new file mode 100644
index 0000000000..2ac045af83
--- /dev/null
+++ b/media/multipart/src/test/resources/multipart/test-file1.txt
@@ -0,0 +1 @@
+This is a test file for file 1.
\ No newline at end of file
diff --git a/media/multipart/src/test/resources/multipart/test-file2.txt b/media/multipart/src/test/resources/multipart/test-file2.txt
new file mode 100644
index 0000000000..ed72b764af
--- /dev/null
+++ b/media/multipart/src/test/resources/multipart/test-file2.txt
@@ -0,0 +1 @@
+This is a test file for file 2.
\ No newline at end of file