diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index db675761..0ba85d72 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,9 @@ # d1_common_java Release Notes +## 2.5.1 (2026-08-18) +- Intent: Deliver a bug-fixed patch release following 2.5.0. +- Bug fixes: Fixed an encoding issue that prevented content containing special characters from being correctly retrieved from HTTP multipart requests (issue #13, PR #14). + ## 2.5.0 (2026-07-06) - Intent: Deliver a modernization and maintenance release focused on Java 17/21/25 compatibility, security hardening, and release pipeline updates. diff --git a/pom.xml b/pom.xml index 0256d696..21c0e0a5 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.dataone d1_common_java jar - 2.5.0 + 2.5.1 DataONE_Common_Java https://dataone.org DataONE Common Code with Service Interface Definitions @@ -171,6 +171,12 @@ jar test + + org.apache.wicket + wicket-core + 9.23.0 + test + diff --git a/src/main/java/org/dataone/mimemultipart/MultipartRequestResolver.java b/src/main/java/org/dataone/mimemultipart/MultipartRequestResolver.java index d0f23708..4b1d97a9 100644 --- a/src/main/java/org/dataone/mimemultipart/MultipartRequestResolver.java +++ b/src/main/java/org/dataone/mimemultipart/MultipartRequestResolver.java @@ -23,6 +23,7 @@ package org.dataone.mimemultipart; import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.*; import javax.servlet.ServletContext; @@ -150,7 +151,7 @@ public MultipartRequest resolveMultipart(HttpServletRequest request) throws IOEx if (item.isFormField()) { String name = item.getFieldName(); - String value = item.getString(); + String value = item.getString(StandardCharsets.UTF_8.name()); if (mpParams.containsKey(name)) { mpParams.get(name).add(value); } else { diff --git a/src/test/java/org/dataone/mimemultipart/MultipartRequestResolverTest.java b/src/test/java/org/dataone/mimemultipart/MultipartRequestResolverTest.java new file mode 100644 index 00000000..0e84ce37 --- /dev/null +++ b/src/test/java/org/dataone/mimemultipart/MultipartRequestResolverTest.java @@ -0,0 +1,147 @@ +package org.dataone.mimemultipart; + +import org.apache.http.HttpEntity; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.entity.mime.content.StringBody; +import org.apache.wicket.protocol.http.mock.MockHttpServletRequest; +import org.apache.wicket.protocol.http.mock.MockHttpSession; +import org.apache.wicket.protocol.http.mock.MockServletContext; +import org.junit.Test; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * Test the MultipartRequestResolver class + */ +public class MultipartRequestResolverTest { + private static final String ID_KEY = "id"; + private static final String ID_VALUE = "test_MultipartRequestResolver"; + private static final String QUERY_KEY = "query"; + private static final String SPECIAL_STRING = "测试"; + + private static String contentType; + + /** + * Test the resolveMultipart method to handle a multipart parameter with a special character + * value + */ + @Test + public void testResolveMultipart() throws Exception { + HttpServletRequest request = generateMultiPartServletRequest(); + MultipartRequestResolver resolver = new MultipartRequestResolver(); + MultipartRequest multipartRequest = resolver.resolveMultipart(request); + Map> multipartParams = multipartRequest.getMultipartParameters(); + assertEquals(2, multipartParams.size()); + String id = multipartParams.get(ID_KEY).get(0); + assertEquals(ID_VALUE, id); + String query = multipartParams.get(QUERY_KEY).get(0); + assertEquals(SPECIAL_STRING, query); + } + + /** + * Generate a multipart servlet request + * @return the multipart servlet request + * @throws Exception + */ + private HttpServletRequest generateMultiPartServletRequest() throws Exception { + ServletInputStream inputStream = buildMultipartStream(); + MockServletContext context = new MockServletContext(null, "/"); + MockHttpServletRequest request = + new MockHttpServletRequest(null, new MockHttpSession(context), context) { + @Override + public String getContentType() { + return contentType; + } + @Override + public ServletInputStream getInputStream() { + return inputStream; + } + }; + return request; + } + + /** + * Build a multipart servlet input stream with the special characters + * @return a ServletInputStream object + * @throws Exception + */ + private ServletInputStream buildMultipartStream() throws Exception { + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); + StringBody pidBody = new StringBody(ID_VALUE, ContentType.TEXT_PLAIN); + builder.addPart(ID_KEY, pidBody); + StringBody queryBody = new StringBody(SPECIAL_STRING, ContentType.TEXT_PLAIN.withCharset( + StandardCharsets.UTF_8)); + builder.addPart(QUERY_KEY, queryBody); + HttpEntity entity = builder.build(); + contentType = entity.getContentType().getValue(); + // Serialize request body + ByteArrayOutputStream requestContent = new ByteArrayOutputStream(); + entity.writeTo(requestContent); + ByteArrayInputStream requestInput = new ByteArrayInputStream(requestContent.toByteArray()); + ServletInputStream objectInputStream = new WrappingServletInputStream(requestInput); + return objectInputStream; + } +} + +/** + * A wrapping class to convert an InputStream object to a ServletInputStream object + */ +class WrappingServletInputStream extends ServletInputStream { + private final InputStream sourceStream; + + /** + * Create a DelegatingServletInputStream for the given source stream. + * @param sourceStream the source stream (never null) + */ + public WrappingServletInputStream(InputStream sourceStream) { + + this.sourceStream = sourceStream; + } + + /** + * Return the underlying source stream (never null). + */ + public final InputStream getSourceStream() { + return this.sourceStream; + } + + public int read() throws IOException { + return this.sourceStream.read(); + } + + public void close() throws IOException { + super.close(); + this.sourceStream.close(); + } + + + @Override + public boolean isFinished() { + return true; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener listener) { + + } + +}