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