-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
16 changed files
with
1,310 additions
and
7 deletions.
There are no files selected for viewing
149 changes: 149 additions & 0 deletions
149
...in/java/org/eclipse/scout/rt/rest/jersey/client/multipart/MultipartMessageBodyWriter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
/* | ||
* Copyright (c) 2010-2023 BSI Business Systems Integration AG. | ||
* All rights reserved. This program and the accompanying materials | ||
* are made available under the terms of the Eclipse Public License v1.0 | ||
* which accompanies this distribution, and is available at | ||
* https://www.eclipse.org/legal/epl-v10.html | ||
* | ||
* Contributors: | ||
* BSI Business Systems Integration AG - initial API and implementation | ||
*/ | ||
package org.eclipse.scout.rt.rest.jersey.client.multipart; | ||
|
||
import static org.eclipse.scout.rt.platform.util.Assertions.assertNotNull; | ||
|
||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.OutputStream; | ||
import java.lang.annotation.Annotation; | ||
import java.lang.reflect.Type; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
import javax.ws.rs.WebApplicationException; | ||
import javax.ws.rs.client.ClientBuilder; | ||
import javax.ws.rs.core.MediaType; | ||
import javax.ws.rs.core.MultivaluedMap; | ||
import javax.ws.rs.ext.MessageBodyWriter; | ||
|
||
import org.eclipse.scout.rt.platform.exception.PlatformException; | ||
import org.eclipse.scout.rt.rest.client.IGlobalRestClientConfigurator; | ||
import org.eclipse.scout.rt.rest.client.multipart.MultipartMessage; | ||
import org.eclipse.scout.rt.rest.client.multipart.MultipartPart; | ||
|
||
/** | ||
* {@link MessageBodyWriter} for {@link MultipartMessage} used on the client side (sending). | ||
* <p> | ||
* Own implementation due to the lack of multipart support in JAX-RS. This implementation works stream-based, i.e. | ||
* doesn't require persisted files or memory allocation for the parts. It doesn't rely on any additional dependencies. | ||
* <p> | ||
* Expects that the media type contains already a boundary parameter, as added when {@link MultipartMessage#toEntity()} | ||
* is used. | ||
* <p> | ||
* Regarding imports, this class must not be part of the module <code>org.eclipse.scout.rt.rest.jersey.client</code> but | ||
* could reside in <code>org.eclipse.scout.rt.rest</code> too. Because there is no direct access to this class, this | ||
* module is used instead to hide implementation details. | ||
* <p> | ||
* Idea from <a href="https://guntherrotsch.github.io/blog_2021/jaxrs-multipart-client.html">JAX/RS Multipart Client by | ||
* Gunther Rotsch</a>. Scout uses a different approach via direct stream processing instead of working with temporary | ||
* files. {@link StandardCharsets#UTF_8} encoding is used instead of {@link StandardCharsets#US_ASCII}. | ||
*/ | ||
public class MultipartMessageBodyWriter implements MessageBodyWriter<MultipartMessage> { | ||
|
||
private static final String HTTP_LINE_DELIMITER = "\r\n"; | ||
|
||
@Override | ||
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { | ||
return MultipartMessage.class.isAssignableFrom(type) && mediaType.isCompatible(MediaType.MULTIPART_FORM_DATA_TYPE); | ||
} | ||
|
||
@Override | ||
public void writeTo(MultipartMessage t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) | ||
throws IOException, WebApplicationException { | ||
|
||
String boundary = assertNotNull(mediaType.getParameters().get(MultipartMessage.BOUNDARY_PARAMETER), | ||
"boundary is missing, make sure to set media type via {}", MultipartMessage.class.getSimpleName() + "#toEntity()"); | ||
|
||
// parts | ||
t.getParts().forEach(part -> writePart(boundary, entityStream, part)); | ||
|
||
// end boundary | ||
write(entityStream, "--" + boundary + "--" + HTTP_LINE_DELIMITER); | ||
} | ||
|
||
/** | ||
* Writes a single part to the entity stream containing | ||
* <ol> | ||
* <li>Start boundary</li> | ||
* <li>Content headers</li> | ||
* <li>Content</li> | ||
* </ol> | ||
* each separated by the HTTP line delimiter and ending with the HTTP line delimiter. | ||
*/ | ||
protected void writePart(String boundary, OutputStream entityStream, MultipartPart part) { | ||
try { | ||
// start boundary | ||
write(entityStream, "--" + boundary + HTTP_LINE_DELIMITER); | ||
|
||
// headers | ||
for (String contentHeader : getContentHeaders(part)) { | ||
write(entityStream, contentHeader + HTTP_LINE_DELIMITER); | ||
} | ||
|
||
write(entityStream, HTTP_LINE_DELIMITER); | ||
|
||
// content | ||
try (InputStream contentStream = part.getInputStream()) { | ||
contentStream.transferTo(entityStream); | ||
} | ||
|
||
write(entityStream, HTTP_LINE_DELIMITER); | ||
} | ||
catch (IOException e) { | ||
throw new PlatformException("Failed to write part", e); | ||
} | ||
} | ||
|
||
/** | ||
* Writes the given string content with {@link StandardCharsets#UTF_8} encoding. | ||
* <p> | ||
* Use for content that only uses 7-bit ASCII anyway (boundary) or part headers including part and filenames. These | ||
* are encoded as UTF-8 and not by using US-ASCII. | ||
*/ | ||
protected void write(OutputStream entityStream, String content) throws IOException { | ||
entityStream.write(content.getBytes(StandardCharsets.UTF_8)); | ||
} | ||
|
||
/** | ||
* Returns the content header for the given parts, which includes 'Content-Disposition' and if available the | ||
* 'Content-Type'. | ||
*/ | ||
protected List<String> getContentHeaders(MultipartPart part) { | ||
List<String> headers = new ArrayList<>(2); | ||
|
||
String contentDisposition = "Content-Disposition: form-data; name=\"" + part.getPartName() + "\""; | ||
if (part.getFilename() != null) { | ||
contentDisposition += "; filename=\"" + part.getFilename() + "\""; | ||
} | ||
|
||
headers.add(contentDisposition); | ||
|
||
if (part.getContentType() != null) { | ||
headers.add("Content-Type: " + part.getContentType()); | ||
} | ||
|
||
return headers; | ||
} | ||
|
||
/** | ||
* {@link IGlobalRestClientConfigurator} implementation registering {@link MultipartMessageBodyWriter}. | ||
*/ | ||
public static class ScoutMultipartClientConfigurator implements IGlobalRestClientConfigurator { | ||
|
||
@Override | ||
public void configure(ClientBuilder clientBuilder) { | ||
clientBuilder.register(MultipartMessageBodyWriter.class); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
...ava/org/eclipse/scout/rt/rest/jersey/client/multipart/MultipartMessageBodyWriterTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* | ||
* Copyright (c) 2010-2023 BSI Business Systems Integration AG. | ||
* All rights reserved. This program and the accompanying materials | ||
* are made available under the terms of the Eclipse Public License v1.0 | ||
* which accompanies this distribution, and is available at | ||
* https://www.eclipse.org/legal/epl-v10.html | ||
* | ||
* Contributors: | ||
* BSI Business Systems Integration AG - initial API and implementation | ||
*/ | ||
package org.eclipse.scout.rt.rest.jersey.client.multipart; | ||
|
||
import static org.junit.Assert.*; | ||
|
||
import javax.ws.rs.core.MediaType; | ||
|
||
import org.eclipse.scout.rt.rest.client.multipart.MultipartMessage; | ||
import org.eclipse.scout.rt.rest.client.multipart.MultipartPart; | ||
import org.junit.Test; | ||
|
||
/** | ||
* Test for {@link MultipartMessageBodyWriter}, additional tests indirectly using this class are in | ||
* {@link MultipartRestClientTest}. | ||
*/ | ||
public class MultipartMessageBodyWriterTest { | ||
|
||
@Test | ||
public void testIsWritable() { | ||
MultipartMessageBodyWriter reader = new MultipartMessageBodyWriter(); | ||
assertFalse(reader.isWriteable(String.class, null, null, MediaType.MULTIPART_FORM_DATA_TYPE)); // wrong class | ||
assertFalse(reader.isWriteable(MultipartPart.class, null, null, MediaType.MULTIPART_FORM_DATA_TYPE)); // wrong class | ||
assertFalse(reader.isWriteable(MultipartMessage.class, null, null, MediaType.TEXT_PLAIN_TYPE)); // wrong media type | ||
assertTrue(reader.isWriteable(MultipartMessage.class, null, null, MediaType.MULTIPART_FORM_DATA_TYPE)); // correct | ||
} | ||
} |
106 changes: 106 additions & 0 deletions
106
.../test/java/org/eclipse/scout/rt/rest/jersey/client/multipart/MultipartRestClientTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/* | ||
* Copyright (c) 2010-2020 BSI Business Systems Integration AG. | ||
* All rights reserved. This program and the accompanying materials | ||
* are made available under the terms of the Eclipse Public License v1.0 | ||
* which accompanies this distribution, and is available at | ||
* http://www.eclipse.org/legal/epl-v10.html | ||
* | ||
* Contributors: | ||
* BSI Business Systems Integration AG - initial API and implementation | ||
*/ | ||
package org.eclipse.scout.rt.rest.jersey.client.multipart; | ||
|
||
import static org.junit.Assert.assertEquals; | ||
|
||
import java.io.ByteArrayInputStream; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.UUID; | ||
|
||
import javax.ws.rs.client.WebTarget; | ||
import javax.ws.rs.core.MediaType; | ||
import javax.ws.rs.core.Response; | ||
|
||
import org.eclipse.scout.rt.platform.BEANS; | ||
import org.eclipse.scout.rt.platform.BeanMetaData; | ||
import org.eclipse.scout.rt.platform.IBean; | ||
import org.eclipse.scout.rt.platform.util.IOUtility; | ||
import org.eclipse.scout.rt.platform.util.uuid.IUuidProvider; | ||
import org.eclipse.scout.rt.rest.client.multipart.MultipartMessage; | ||
import org.eclipse.scout.rt.rest.client.multipart.MultipartPart; | ||
import org.eclipse.scout.rt.rest.jersey.EchoServletParameters; | ||
import org.eclipse.scout.rt.rest.jersey.JerseyTestApplication; | ||
import org.eclipse.scout.rt.rest.jersey.JerseyTestRestClientHelper; | ||
import org.eclipse.scout.rt.rest.jersey.RestClientTestEchoResponse; | ||
import org.eclipse.scout.rt.testing.platform.BeanTestingHelper; | ||
import org.eclipse.scout.rt.testing.platform.runner.PlatformTestRunner; | ||
import org.eclipse.scout.rt.testing.platform.util.uuid.FixedUuidProvider; | ||
import org.junit.AfterClass; | ||
import org.junit.Before; | ||
import org.junit.BeforeClass; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
|
||
/** | ||
* Test for {@link MultipartMessageBodyWriter} via {@link JerseyTestApplication}. | ||
*/ | ||
@RunWith(PlatformTestRunner.class) | ||
public class MultipartRestClientTest { | ||
|
||
private static IBean<?> s_uuidProvider; | ||
|
||
/** | ||
* Used in .txt resource serving as expected echo response. | ||
*/ | ||
private static final String FIXED_UUID = "3372ccc6-dada-4847-b189-3ff57a81e553"; | ||
|
||
@BeforeClass | ||
public static void beforeClass() { | ||
// Use a fixed uuid provider to create a known boundary for the multipart message. | ||
s_uuidProvider = BEANS.get(BeanTestingHelper.class).registerBean(new BeanMetaData(IUuidProvider.class, new FixedUuidProvider(UUID.fromString(FIXED_UUID)))); | ||
|
||
BEANS.get(JerseyTestApplication.class).ensureStarted(); | ||
} | ||
|
||
@AfterClass | ||
public static void afterClass() { | ||
BEANS.get(BeanTestingHelper.class).unregisterBean(s_uuidProvider); | ||
} | ||
|
||
private WebTarget m_target; | ||
|
||
@Before | ||
public void before() { | ||
m_target = BEANS.get(JerseyTestRestClientHelper.class).target("echo"); | ||
} | ||
|
||
@Test | ||
public void testMultipart() { | ||
byte[] plainTextBytes = "lorem ipsum dolor\nsit amet".getBytes(StandardCharsets.UTF_8); | ||
byte[] jsonBytes = "{ \"lorem\": \"ipsum\"}".getBytes(StandardCharsets.UTF_8); | ||
byte[] umlauteBytes = "Äpfel".getBytes(StandardCharsets.UTF_8); | ||
|
||
MultipartMessage multiPartMessage = BEANS.get(MultipartMessage.class) | ||
.addPart(MultipartPart.ofFile("plaintext", "text.txt", new ByteArrayInputStream(plainTextBytes))) | ||
.addPart(MultipartPart.ofFile("json", "json.json", new ByteArrayInputStream(jsonBytes))) | ||
.addPart(MultipartPart.ofFile("umlaute-öäü", "äpfel\uD83D\uDE00.txt", new ByteArrayInputStream(umlauteBytes))) | ||
.addPart(MultipartPart.ofField("lorem", "lorem value")) | ||
.addPart(MultipartPart.ofField("ipsum", "ipsum välüe")) | ||
.addPart(MultipartPart.ofField("dol\"or", "dolor\"value")); | ||
|
||
RestClientTestEchoResponse response = m_target | ||
.queryParam(EchoServletParameters.STATUS, Response.Status.OK.getStatusCode()) | ||
.request() | ||
.accept(MediaType.APPLICATION_JSON) | ||
.post(multiPartMessage.toEntity(), RestClientTestEchoResponse.class); | ||
|
||
String contentTypeHeader = response.getReceivedHeaders().get("Content-Type"); | ||
assertEquals("multipart/form-data;boundary=" + FIXED_UUID.replace("-", ""), contentTypeHeader); | ||
|
||
// file contains \n only, replace by \r\n as the HTTP line delimiter is using \r\n too (except the plain text bytes) | ||
String expectedEchoResponse = IOUtility.readStringUTF8(MultipartRestClientTest.class.getResourceAsStream("MultipartEchoResponse.txt")) | ||
.replaceAll("\n", "\r\n") | ||
.replaceFirst("lorem ipsum dolor\r\nsit amet", "lorem ipsum dolor\nsit amet"); | ||
|
||
assertEquals(expectedEchoResponse, response.getEcho().getBody()); | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
...est/resources/org/eclipse/scout/rt/rest/jersey/client/multipart/MultipartEchoResponse.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
--3372ccc6dada4847b1893ff57a81e553 | ||
Content-Disposition: form-data; name="plaintext"; filename="text.txt" | ||
Content-Type: text/plain | ||
|
||
lorem ipsum dolor | ||
sit amet | ||
--3372ccc6dada4847b1893ff57a81e553 | ||
Content-Disposition: form-data; name="json"; filename="json.json" | ||
Content-Type: application/json | ||
|
||
{ "lorem": "ipsum"} | ||
--3372ccc6dada4847b1893ff57a81e553 | ||
Content-Disposition: form-data; name="umlaute-öäü"; filename="äpfel😀.txt" | ||
Content-Type: text/plain | ||
|
||
Äpfel | ||
--3372ccc6dada4847b1893ff57a81e553 | ||
Content-Disposition: form-data; name="lorem" | ||
|
||
lorem value | ||
--3372ccc6dada4847b1893ff57a81e553 | ||
Content-Disposition: form-data; name="ipsum" | ||
|
||
ipsum välüe | ||
--3372ccc6dada4847b1893ff57a81e553 | ||
Content-Disposition: form-data; name="dol"or" | ||
|
||
dolor"value | ||
--3372ccc6dada4847b1893ff57a81e553-- |
File renamed without changes.
Oops, something went wrong.