Improve multipart behaviour to comply with MP REST Client 4.0 TCK tests

Signed-off-by: jansupol <jan.supol@oracle.com>
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 0b9da83..1ac6b75 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 @@
         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 0000000..09c751e
--- /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 de5bfc6..9c4e32d 100644
--- a/etc/config/copyright-exclude
+++ b/etc/config/copyright-exclude
@@ -93,3 +93,4 @@
 /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 2390db2..d1d97a8 100644
--- a/media/multipart/pom.xml
+++ b/media/multipart/pom.xml
@@ -92,6 +92,12 @@
             <version>${project.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.media</groupId>
+            <artifactId>jersey-media-json-processing</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
 
         <dependency>
             <groupId>org.junit.jupiter</groupId>
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 7287759..d672e1c 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> T getEntityAs(final Class<T> 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 @@
         }
 
         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 0000000..f7bfa53
--- /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<EntityPart> 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<String, byte[]> 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<EntityPart> 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<EntityPart> entityParts = (List<EntityPart>) 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 0000000..2ac045a
--- /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 0000000..ed72b76
--- /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