This is an automated email from the ASF dual-hosted git repository.
reta pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new 7f09b5eae4 CXF-9136: Add EntityPart MessageBodyReader /
MessageBodyWriter support (#2698)
7f09b5eae4 is described below
commit 7f09b5eae4420a101d07b08147d6ad08f0161ac3
Author: Andriy Redko <[email protected]>
AuthorDate: Tue Nov 11 14:20:57 2025 -0500
CXF-9136: Add EntityPart MessageBodyReader / MessageBodyWriter support
(#2698)
---
.../attachment/AttachmentBoundaryDeserializer.java | 93 +++++++++
.../cxf/attachment/AttachmentDeserializer.java | 147 +-------------
.../cxf/attachment/AttachmentDeserializerUtil.java | 175 +++++++++++++++++
.../CloseableAttachmentDeserializer.java | 26 +++
.../cxf/attachment/DelegatingInputStream.java | 7 +-
.../cxf/jaxrs/impl/EntityPartBuilderImpl.java | 93 +++++----
.../org/apache/cxf/jaxrs/impl/EntityPartImpl.java | 140 ++++++++++---
.../cxf/jaxrs/provider/EntityPartProvider.java | 216 +++++++++++++++++++++
.../apache/cxf/jaxrs/provider/ProviderFactory.java | 3 +-
.../apache/cxf/jaxrs/utils/EntityPartUtils.java | 109 +++++++++++
.../java/org/apache/cxf/jaxrs/utils/FormUtils.java | 19 ++
.../org/apache/cxf/jaxrs/utils/InjectionUtils.java | 5 +
.../org/apache/cxf/jaxrs/utils/JAXRSUtils.java | 11 +-
...e.java => ParameterizedCollectionBaseType.java} | 16 +-
.../jaxrs/utils/ParameterizedCollectionType.java | 30 +--
.../cxf/jaxrs/utils/ParameterizedListType.java | 28 +++
.../cxf/jaxrs/provider/ProviderFactoryTest.java | 54 +++---
.../apache/cxf/systest/jaxrs/EntityPartServer.java | 62 ++++++
.../apache/cxf/systest/jaxrs/EntityPartStore.java | 101 ++++++++++
.../cxf/systest/jaxrs/JAXRSEntityPartTest.java | 172 ++++++++++++++++
20 files changed, 1226 insertions(+), 281 deletions(-)
diff --git
a/core/src/main/java/org/apache/cxf/attachment/AttachmentBoundaryDeserializer.java
b/core/src/main/java/org/apache/cxf/attachment/AttachmentBoundaryDeserializer.java
new file mode 100644
index 0000000000..72675a0e5c
--- /dev/null
+++
b/core/src/main/java/org/apache/cxf/attachment/AttachmentBoundaryDeserializer.java
@@ -0,0 +1,93 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.attachment;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PushbackInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.cxf.helpers.IOUtils;
+import org.apache.cxf.message.Attachment;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.message.MessageUtils;
+
+public class AttachmentBoundaryDeserializer {
+ private static final Pattern INPUT_STREAM_BOUNDARY_PATTERN =
Pattern.compile("^--(\\S*)$", Pattern.MULTILINE);
+ private static final int PUSHBACK_AMOUNT = 2048;
+
+ private final int maxHeaderLength;
+ private final Message message;
+
+ public AttachmentBoundaryDeserializer(Message message) {
+ this.message = message;
+ this.maxHeaderLength = MessageUtils.getContextualInteger(message,
+ AttachmentDeserializer.ATTACHMENT_MAX_HEADER_SIZE,
AttachmentDeserializer.DEFAULT_MAX_HEADER_SIZE);
+ }
+
+ public Attachment read(InputStream body) throws IOException {
+ final PushbackInputStream stream = new PushbackInputStream(body,
PUSHBACK_AMOUNT);
+ final String boundaryString = findBoundaryFromInputStream(stream);
+
+ // If a boundary still wasn't found, throw an exception
+ if (null == boundaryString) {
+ throw new IOException("Couldn't determine the boundary from the
message!");
+ }
+
+ final byte[] boundary =
boundaryString.getBytes(StandardCharsets.UTF_8);
+ if (!AttachmentDeserializerUtil.readTillFirstBoundary(stream,
boundary)) {
+ throw new IOException("Couldn't find MIME boundary: " +
boundaryString);
+ }
+
+ Map<String, List<String>> ih =
AttachmentDeserializerUtil.loadPartHeaders(stream, maxHeaderLength);
+ String val = AttachmentUtil.getHeader(ih, "Content-Transfer-Encoding");
+
+ MimeBodyPartInputStream mmps = new MimeBodyPartInputStream(stream,
boundary, PUSHBACK_AMOUNT);
+ InputStream ins = AttachmentUtil.decode(mmps, val);
+ if (ins != mmps) {
+ ih.remove("Content-Transfer-Encoding");
+ }
+
+ return AttachmentUtil.createAttachment(new DelegatingInputStream(ins,
is -> stream.close()), ih, message);
+ }
+
+ private String findBoundaryFromInputStream(PushbackInputStream stream)
throws IOException {
+ //boundary should definitely be in the first 2K;
+ byte[] buf = new byte[2048];
+ int i = stream.read(buf);
+ int len = i;
+ while (i > 0 && len < buf.length) {
+ i = stream.read(buf, len, buf.length - len);
+ if (i > 0) {
+ len += i;
+ }
+ }
+ String msg = IOUtils.newStringFromBytes(buf, 0, len);
+ stream.unread(buf, 0, len);
+
+ // Use regex to get the boundary and return null if it's not found
+ Matcher m = INPUT_STREAM_BOUNDARY_PATTERN.matcher(msg);
+ return m.find() ? "--" + m.group(1) : null;
+ }
+}
diff --git
a/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializer.java
b/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializer.java
index 9782c8be11..925fd98b23 100644
--- a/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializer.java
+++ b/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializer.java
@@ -30,13 +30,10 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.TreeMap;
-import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jakarta.activation.DataSource;
-import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.common.util.SystemPropertyAction;
import org.apache.cxf.helpers.HttpHeaderHelper;
@@ -84,8 +81,6 @@ public class AttachmentDeserializer {
private static final Pattern INPUT_STREAM_BOUNDARY_PATTERN =
Pattern.compile("^--(\\S*)$", Pattern.MULTILINE);
- private static final Logger LOG =
LogUtils.getL7dLogger(AttachmentDeserializer.class);
-
private static final int PUSHBACK_AMOUNT = 2048;
private boolean lazyLoading = true;
@@ -161,11 +156,11 @@ public class AttachmentDeserializer {
boundary = boundaryString.getBytes(StandardCharsets.UTF_8);
stream = new
PushbackInputStream(message.getContent(InputStream.class), PUSHBACK_AMOUNT);
- if (!readTillFirstBoundary(stream, boundary)) {
+ if (!AttachmentDeserializerUtil.readTillFirstBoundary(stream,
boundary)) {
throw new IOException("Couldn't find MIME boundary: " +
boundaryString);
}
- Map<String, List<String>> ih = loadPartHeaders(stream);
+ Map<String, List<String>> ih =
AttachmentDeserializerUtil.loadPartHeaders(stream, maxHeaderLength);
message.put(ATTACHMENT_PART_HEADERS, ih);
String val = AttachmentUtil.getHeader(ih, "Content-Type", "; ");
if (!StringUtils.isEmpty(val)) {
@@ -230,7 +225,7 @@ public class AttachmentDeserializer {
}
stream.unread(v);
- Map<String, List<String>> headers = loadPartHeaders(stream);
+ Map<String, List<String>> headers =
AttachmentDeserializerUtil.loadPartHeaders(stream, maxHeaderLength);
return (AttachmentImpl)createAttachment(headers);
}
@@ -271,48 +266,6 @@ public class AttachmentDeserializer {
}
}
- /**
- * Move the read pointer to the begining of the first part read till the
end
- * of first boundary
- *
- * @param pushbackInStream
- * @param boundary
- * @throws IOException
- */
- private static boolean readTillFirstBoundary(PushbackInputStream
pushbackInStream,
- byte[] boundary) throws IOException {
-
- // work around a bug in PushBackInputStream where the buffer isn't
- // initialized
- // and available always returns 0.
- int value = pushbackInStream.read();
- pushbackInStream.unread(value);
- while (value != -1) {
- value = pushbackInStream.read();
- if ((byte) value == boundary[0]) {
- int boundaryIndex = 0;
- while (value != -1
- && boundaryIndex < boundary.length
- && (byte)value == boundary[boundaryIndex]) {
-
- value = pushbackInStream.read();
- if (value == -1) {
- throw new IOException("Unexpected End while searching
for first Mime Boundary");
- }
- boundaryIndex++;
- }
- if (boundaryIndex == boundary.length) {
- // boundary found, read the newline
- if (value == 13) {
- pushbackInStream.read();
- }
- return true;
- }
- }
- }
- return false;
- }
-
/**
* Create an Attachment from the MIME stream. If there is a previous
attachment
* that is not read, cache that attachment.
@@ -366,98 +319,4 @@ public class AttachmentDeserializer {
stream.unread(v);
return true;
}
-
-
-
- private Map<String, List<String>> loadPartHeaders(InputStream in) throws
IOException {
- StringBuilder buffer = new StringBuilder(128);
- StringBuilder b = new StringBuilder(128);
- Map<String, List<String>> heads = new
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-
- // loop until we hit the end or a null line
- while (readLine(in, b)) {
- // lines beginning with white space get special handling
- char c = b.charAt(0);
- if (c == ' ' || c == '\t') {
- if (buffer.length() != 0) {
- // preserve the line break and append the continuation
- buffer.append("\r\n");
- buffer.append(b);
- }
- } else {
- // if we have a line pending in the buffer, flush it
- if (buffer.length() > 0) {
- addHeaderLine(heads, buffer);
- buffer.setLength(0);
- }
- // add this to the accumulator
- buffer.append(b);
- }
- }
-
- // if we have a line pending in the buffer, flush it
- if (buffer.length() > 0) {
- addHeaderLine(heads, buffer);
- }
- return heads;
- }
-
- private boolean readLine(InputStream in, StringBuilder buffer) throws
IOException {
- if (buffer.length() != 0) {
- buffer.setLength(0);
- }
- int c;
-
- while ((c = in.read()) != -1) {
- // a linefeed is a terminator, always.
- if (c == '\n') {
- break;
- } else if (c == '\r') {
- //just ignore the CR. The next character SHOULD be an NL. If
not, we're
- //just going to discard this
- continue;
- } else {
- // just add to the buffer
- buffer.append((char)c);
- }
-
- if (buffer.length() > maxHeaderLength) {
- LOG.fine("The attachment header size has exceeded the
configured parameter: " + maxHeaderLength);
- throw new HeaderSizeExceededException();
- }
- }
-
- // no characters found...this was either an eof or a null line.
- return buffer.length() != 0;
- }
-
- private void addHeaderLine(Map<String, List<String>> heads, StringBuilder
line) {
- // null lines are a nop
- final int size = line.length();
- if (size == 0) {
- return;
- }
- int separator = line.indexOf(":");
- final String name;
- String value = "";
- if (separator == -1) {
- name = line.toString().trim();
- } else {
- name = line.substring(0, separator);
- // step past the separator. Now we need to remove any leading
white space characters.
- separator++;
-
- while (separator < size) {
- char ch = line.charAt(separator);
- if (ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n') {
- break;
- }
- separator++;
- }
- value = line.substring(separator);
- }
- List<String> v = heads.computeIfAbsent(name, k -> new ArrayList<>(1));
- v.add(value);
- }
-
}
diff --git
a/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializerUtil.java
b/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializerUtil.java
new file mode 100644
index 0000000000..98ee9a468c
--- /dev/null
+++
b/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializerUtil.java
@@ -0,0 +1,175 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.attachment;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PushbackInputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.logging.Logger;
+
+import org.apache.cxf.common.logging.LogUtils;
+
+final class AttachmentDeserializerUtil {
+ /* Keep the log under AttachmentDeserializer */
+ private static final Logger LOG =
LogUtils.getL7dLogger(AttachmentDeserializer.class);
+
+ private AttachmentDeserializerUtil() {
+ }
+
+ /**
+ * Move the read pointer to the begining of the first part read till the
end
+ * of first boundary
+ *
+ * @param pushbackInStream
+ * @param boundary
+ * @throws IOException
+ */
+ static boolean readTillFirstBoundary(PushbackInputStream pushbackInStream,
+ byte[] boundary) throws IOException {
+
+ // work around a bug in PushBackInputStream where the buffer isn't
+ // initialized
+ // and available always returns 0.
+ int value = pushbackInStream.read();
+ pushbackInStream.unread(value);
+ while (value != -1) {
+ value = pushbackInStream.read();
+ if ((byte) value == boundary[0]) {
+ int boundaryIndex = 0;
+ while (value != -1
+ && boundaryIndex < boundary.length
+ && (byte)value == boundary[boundaryIndex]) {
+
+ value = pushbackInStream.read();
+ if (value == -1) {
+ throw new IOException("Unexpected End while searching
for first Mime Boundary");
+ }
+ boundaryIndex++;
+ }
+ if (boundaryIndex == boundary.length) {
+ // boundary found, read the newline
+ if (value == 13) {
+ pushbackInStream.read();
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+
+ static Map<String, List<String>> loadPartHeaders(InputStream in, int
maxHeaderLength) throws IOException {
+ StringBuilder buffer = new StringBuilder(128);
+ StringBuilder b = new StringBuilder(128);
+ Map<String, List<String>> heads = new
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+ // loop until we hit the end or a null line
+ while (readLine(in, b, maxHeaderLength)) {
+ // lines beginning with white space get special handling
+ char c = b.charAt(0);
+ if (c == ' ' || c == '\t') {
+ if (buffer.length() != 0) {
+ // preserve the line break and append the continuation
+ buffer.append("\r\n");
+ buffer.append(b);
+ }
+ } else {
+ // if we have a line pending in the buffer, flush it
+ if (buffer.length() > 0) {
+ addHeaderLine(heads, buffer);
+ buffer.setLength(0);
+ }
+ // add this to the accumulator
+ buffer.append(b);
+ }
+ }
+
+ // if we have a line pending in the buffer, flush it
+ if (buffer.length() > 0) {
+ addHeaderLine(heads, buffer);
+ }
+ return heads;
+ }
+
+ private static boolean readLine(InputStream in, StringBuilder buffer, int
maxHeaderLength) throws IOException {
+ if (buffer.length() != 0) {
+ buffer.setLength(0);
+ }
+ int c;
+
+ while ((c = in.read()) != -1) {
+ // a linefeed is a terminator, always.
+ if (c == '\n') {
+ break;
+ } else if (c == '\r') {
+ //just ignore the CR. The next character SHOULD be an NL. If
not, we're
+ //just going to discard this
+ continue;
+ } else {
+ // just add to the buffer
+ buffer.append((char)c);
+ }
+
+ if (buffer.length() > maxHeaderLength) {
+ LOG.fine("The attachment header size has exceeded the
configured parameter: " + maxHeaderLength);
+ throw new HeaderSizeExceededException();
+ }
+ }
+
+ // no characters found...this was either an eof or a null line.
+ return buffer.length() != 0;
+ }
+
+ private static void addHeaderLine(Map<String, List<String>> heads,
StringBuilder line) {
+ // null lines are a nop
+ final int size = line.length();
+ if (size == 0) {
+ return;
+ }
+ int separator = line.indexOf(":");
+ final String name;
+ String value = "";
+ if (separator == -1) {
+ name = line.toString().trim();
+ } else {
+ name = line.substring(0, separator);
+ // step past the separator. Now we need to remove any leading
white space characters.
+ separator++;
+
+ while (separator < size) {
+ char ch = line.charAt(separator);
+ if (ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n') {
+ break;
+ }
+ separator++;
+ }
+ value = line.substring(separator);
+ }
+ List<String> v = heads.computeIfAbsent(name, k -> new ArrayList<>(1));
+ v.add(value);
+ }
+
+
+}
diff --git
a/core/src/main/java/org/apache/cxf/attachment/CloseableAttachmentDeserializer.java
b/core/src/main/java/org/apache/cxf/attachment/CloseableAttachmentDeserializer.java
new file mode 100644
index 0000000000..e047a6670e
--- /dev/null
+++
b/core/src/main/java/org/apache/cxf/attachment/CloseableAttachmentDeserializer.java
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.attachment;
+
+import java.io.IOException;
+
+interface CloseableAttachmentDeserializer {
+ void markClosed(DelegatingInputStream is) throws IOException;
+}
diff --git
a/core/src/main/java/org/apache/cxf/attachment/DelegatingInputStream.java
b/core/src/main/java/org/apache/cxf/attachment/DelegatingInputStream.java
index cc6c771168..fac31883da 100644
--- a/core/src/main/java/org/apache/cxf/attachment/DelegatingInputStream.java
+++ b/core/src/main/java/org/apache/cxf/attachment/DelegatingInputStream.java
@@ -29,13 +29,18 @@ import org.apache.cxf.io.Transferable;
public class DelegatingInputStream extends InputStream implements Transferable
{
private InputStream is;
- private AttachmentDeserializer deserializer;
+ private CloseableAttachmentDeserializer deserializer;
private boolean isClosed;
DelegatingInputStream(InputStream is, AttachmentDeserializer ads) {
+ this(is, ads::markClosed);
+ }
+
+ DelegatingInputStream(InputStream is, CloseableAttachmentDeserializer ads)
{
this.is = is;
deserializer = ads;
}
+
DelegatingInputStream(InputStream is) {
this.is = is;
deserializer = null;
diff --git
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/EntityPartBuilderImpl.java
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/EntityPartBuilderImpl.java
index 6d4a0e00fe..e5ae991fa6 100644
---
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/EntityPartBuilderImpl.java
+++
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/EntityPartBuilderImpl.java
@@ -19,11 +19,10 @@
package org.apache.cxf.jaxrs.impl;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.util.Objects;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.EntityPart;
@@ -32,7 +31,11 @@ import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.ext.ContextResolver;
+import jakarta.ws.rs.ext.ExceptionMapper;
+import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.MessageBodyWriter;
+import jakarta.ws.rs.ext.Providers;
import org.apache.cxf.BusFactory;
import org.apache.cxf.jaxrs.provider.ProviderFactory;
import org.apache.cxf.jaxrs.provider.ServerProviderFactory;
@@ -87,7 +90,6 @@ public class EntityPartBuilderImpl implements
EntityPart.Builder {
@Override
public Builder content(InputStream in) throws IllegalArgumentException {
this.content = in;
- this.type = InputStream.class;
return this;
}
@@ -109,57 +111,54 @@ public class EntityPartBuilderImpl implements
EntityPart.Builder {
@Override
public EntityPart build() throws IllegalStateException, IOException,
WebApplicationException {
- final MediaType mt = Objects.requireNonNullElse(mediaType,
MediaType.APPLICATION_OCTET_STREAM_TYPE);
-
- Message message = JAXRSUtils.getCurrentMessage();
- ProviderFactory factory = null;
+ return new EntityPartImpl(getProviderFactory(), name, fileName,
content, type, genericType, headers, mediaType);
+ }
+ private static Providers getProviderFactory() {
+ final Message message = JAXRSUtils.getCurrentMessage();
if (message == null) {
- message = new MessageImpl();
- factory =
ServerProviderFactory.createInstance(BusFactory.getThreadDefaultBus());
- } else {
- factory = ProviderFactory.getInstance(message);
- }
-
- if (genericType != null) {
- try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
- writeTo(factory, genericType, mt, message, out);
- return new EntityPartImpl(factory, name, fileName, new
ByteArrayInputStream(out.toByteArray()),
- headers, mt);
- }
- } else if (type != null) {
- try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
- writeTo(factory, type, mt, message, out);
- return new EntityPartImpl(factory, name, fileName, new
ByteArrayInputStream(out.toByteArray()),
- headers, mt);
- }
+ return new
ReaderWriterProviders(ServerProviderFactory.createInstance(BusFactory.getThreadDefaultBus()));
} else {
- throw new IllegalStateException("Either type or genericType is
expected for the content");
+ return new
ReaderWriterProviders(ProviderFactory.getInstance(message));
}
}
+
- @SuppressWarnings("unchecked")
- private <T> void writeTo(final ProviderFactory providers, final
GenericType<T> t, final MediaType mt,
- final Message message, final ByteArrayOutputStream out) throws
IOException {
-
- final MessageBodyWriter<T> writer = (MessageBodyWriter<T>) providers
- .createMessageBodyWriter(t.getRawType(), t.getType(), null, mt,
message);
+ private static final class ReaderWriterProviders implements Providers {
+ private final ProviderFactory factory;
- writer.writeTo((T) content, t.getRawType(), t.getType(), null, mt,
cast(headers), out);
- }
+ private ReaderWriterProviders(final ProviderFactory factory) {
+ this.factory = factory;
+ }
- @SuppressWarnings("unchecked")
- private <T> void writeTo(final ProviderFactory providers, final Class<T>
t, final MediaType mt,
- final Message message, final ByteArrayOutputStream out) throws
IOException {
+ @Override
+ public <T> MessageBodyWriter<T> getMessageBodyWriter(Class<T> type,
Type genericType,
+ Annotation[] annotations, MediaType mediaType) {
+ return factory.createMessageBodyWriter(type, genericType,
annotations, mediaType, getCurrentMessage());
+ }
+
+ @Override
+ public <T> MessageBodyReader<T> getMessageBodyReader(Class<T> type,
Type genericType,
+ Annotation[] annotations, MediaType mediaType) {
+ return factory.createMessageBodyReader(type, genericType,
annotations, mediaType, getCurrentMessage());
+ }
+
+ @Override
+ public <T extends Throwable> ExceptionMapper<T>
getExceptionMapper(Class<T> type) {
+ throw new UnsupportedOperationException();
+ }
- final MessageBodyWriter<T> writer = (MessageBodyWriter<T>) providers
- .createMessageBodyWriter(t, null, null, mt, message);
+ @Override
+ public <T> ContextResolver<T> getContextResolver(Class<T> contextType,
MediaType mediaType) {
+ throw new UnsupportedOperationException();
+ }
- writer.writeTo((T) content, t, null, null, mt, cast(headers), out);
- }
-
- @SuppressWarnings("unchecked")
- private static <T, U> MultivaluedMap<T, U> cast(MultivaluedMap<?, ?> p) {
- return (MultivaluedMap<T, U>)p;
- }
+ private static Message getCurrentMessage() {
+ Message message = JAXRSUtils.getCurrentMessage();
+ if (message == null) {
+ message = new MessageImpl();
+ }
+ return message;
+ }
+ };
}
diff --git
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/EntityPartImpl.java
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/EntityPartImpl.java
index d302768b66..0c1a68a3f6 100644
---
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/EntityPartImpl.java
+++
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/EntityPartImpl.java
@@ -19,8 +19,16 @@
package org.apache.cxf.jaxrs.impl;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.io.UncheckedIOException;
+import java.lang.reflect.Type;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
import java.util.Optional;
import jakarta.ws.rs.WebApplicationException;
@@ -29,27 +37,40 @@ import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyReader;
-import org.apache.cxf.jaxrs.provider.ProviderFactory;
-import org.apache.cxf.jaxrs.utils.JAXRSUtils;
-import org.apache.cxf.message.Message;
+import jakarta.ws.rs.ext.MessageBodyWriter;
+import jakarta.ws.rs.ext.Providers;
public class EntityPartImpl implements EntityPart {
private final String name;
private final String fileName;
- private final InputStream content;
+ private final Object content;
private final MultivaluedMap<String, String> headers;
private final MediaType mediaType;
- private final ProviderFactory providers;
+ private final GenericType<?> genericType;
+ private final Class<?> type;
+ private final Providers providers;
- EntityPartImpl(final ProviderFactory providers, String name, String
fileName, InputStream content,
- MultivaluedMap<String, String> headers, MediaType mediaType) {
+ //CHECKSTYLE:OFF
+ public EntityPartImpl(Providers providers, String name, String fileName,
Object content, Class<?> type,
+ GenericType<?> genericType, MultivaluedMap<String, String>
headers, MediaType mediaType) {
this.providers = providers;
this.name = name;
this.fileName = fileName;
this.content = content;
this.headers = headers;
this.mediaType = mediaType;
+ this.genericType = genericType;
+ if (type == null) {
+ if (content != null) {
+ this.type = content.getClass();
+ } else {
+ this.type = InputStream.class;
+ }
+ } else {
+ this.type = type;
+ }
}
+ //CHECKSTYLE:ON
@Override
public String getName() {
@@ -63,32 +84,98 @@ public class EntityPartImpl implements EntityPart {
@Override
public InputStream getContent() {
- return content;
+ try {
+ if (content instanceof InputStream) {
+ return (InputStream) content;
+ } else if (content instanceof byte[]) {
+ return new ByteArrayInputStream((byte[]) content);
+ } else if (fileName != null && !fileName.isBlank()) {
+ return Files.newInputStream(Path.of(fileName));
+ } else {
+ return contentAsStream();
+ }
+ } catch (final IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
}
+ @SuppressWarnings("unchecked")
+ private <T> InputStream contentAsStream() throws IOException {
+ final MediaType mt = Objects.requireNonNullElse(mediaType,
MediaType.APPLICATION_OCTET_STREAM_TYPE);
+ final Type generic = (genericType != null) ? genericType.getType() :
null;
+
+ final MessageBodyWriter<T> writer = (MessageBodyWriter<T>) providers
+ .getMessageBodyWriter(type, generic, null, mt);
+
+ if (writer == null) {
+ throw new IllegalArgumentException("No suitable MessageBodyWriter
available to handle "
+ + type.getName() + ", media type " + mediaType);
+ }
+
+ final PipedInputStream pipedInputStream = new PipedInputStream();
+ try (PipedOutputStream pipedOutputStream = new
PipedOutputStream(pipedInputStream)) {
+ writer.writeTo((T)content, (Class<T>)type, generic, null, mt,
cast(headers), pipedOutputStream);
+ return pipedInputStream;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
@Override
- public <T> T getContent(Class<T> type)
- throws IllegalArgumentException, IllegalStateException,
IOException, WebApplicationException {
- final Message message = JAXRSUtils.getCurrentMessage();
+ public <T> T getContent(Class<T> asType) throws IllegalArgumentException,
IllegalStateException,
+ IOException, WebApplicationException {
+
+ if (asType == null) {
+ throw new NullPointerException("The type is required");
+ }
- @SuppressWarnings({ "unchecked", "rawtypes" })
- final MessageBodyReader<T> reader = (MessageBodyReader) providers
- .createMessageBodyReader(type, null, null, mediaType, message);
+ if (asType.isInstance(content)) {
+ return (T) content;
+ }
- return reader.readFrom(type, null, null, mediaType, headers, content);
+ final MessageBodyReader<T> reader = (MessageBodyReader<T>) providers
+ .getMessageBodyReader(asType, null, null, mediaType);
+
+ if (reader != null) {
+ // The implementation is required to close the content stream when
this method
+ // is invoked, so it may only be invoked once.
+ try (InputStream is = getContent()) {
+ return reader.readFrom(asType, null, null, mediaType, headers,
is);
+ }
+ } else {
+ throw new IllegalArgumentException("No suitable MessageBodyReader
available to handle "
+ + asType.getName() + ", media type " + mediaType);
+ }
}
@SuppressWarnings("unchecked")
@Override
- public <T> T getContent(GenericType<T> type)
- throws IllegalArgumentException, IllegalStateException,
IOException, WebApplicationException {
- final Message message = JAXRSUtils.getCurrentMessage();
-
- @SuppressWarnings("rawtypes")
- final MessageBodyReader<T> reader = (MessageBodyReader) providers
- .createMessageBodyReader(type.getRawType(), type.getType(), null,
mediaType, message);
-
- return reader.readFrom((Class<T>) type.getRawType(), type.getType(),
null, mediaType, headers, content);
+ public <T> T getContent(GenericType<T> asType) throws
IllegalArgumentException, IllegalStateException,
+ IOException, WebApplicationException {
+
+ if (asType == null) {
+ throw new NullPointerException("The generic type is required");
+ }
+
+ if (asType.getRawType().isInstance(content)) {
+ return (T) content;
+ }
+
+ final MessageBodyReader<T> reader = (MessageBodyReader<T>) providers
+ .getMessageBodyReader(asType.getRawType(), asType.getType(), null,
mediaType);
+
+ // The implementation is required to close the content stream when
this method
+ // is invoked, so it may only be invoked once.
+ if (reader != null) {
+ // The implementation is required to close the content stream when
this method
+ // is invoked, so it may only be invoked once.
+ try (InputStream is = getContent()) {
+ return reader.readFrom((Class<T>) asType.getRawType(),
asType.getType(),
+ null, mediaType, headers, is);
+ }
+ } else {
+ throw new IllegalArgumentException("No suitable MessageBodyReader
available to handle "
+ + asType.getRawType().getName() + ", media type " + mediaType);
+ }
}
@Override
@@ -100,4 +187,9 @@ public class EntityPartImpl implements EntityPart {
public MediaType getMediaType() {
return mediaType;
}
+
+ @SuppressWarnings("unchecked")
+ private static <T, U> MultivaluedMap<T, U> cast(MultivaluedMap<?, ?> p) {
+ return (MultivaluedMap<T, U>)p;
+ }
}
diff --git
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/EntityPartProvider.java
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/EntityPartProvider.java
new file mode 100644
index 0000000000..4323ede07c
--- /dev/null
+++
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/EntityPartProvider.java
@@ -0,0 +1,216 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.jaxrs.provider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.logging.Logger;
+
+import jakarta.activation.DataHandler;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.EntityPart;
+import jakarta.ws.rs.core.GenericType;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.ext.MessageBodyReader;
+import jakarta.ws.rs.ext.MessageBodyWriter;
+import jakarta.ws.rs.ext.Provider;
+import jakarta.ws.rs.ext.Providers;
+import org.apache.cxf.attachment.AttachmentUtil;
+import org.apache.cxf.common.logging.LogUtils;
+import org.apache.cxf.common.util.StringUtils;
+import org.apache.cxf.helpers.CastUtils;
+import org.apache.cxf.jaxrs.ext.MessageContext;
+import org.apache.cxf.jaxrs.ext.multipart.Attachment;
+import org.apache.cxf.jaxrs.ext.multipart.ContentDisposition;
+import org.apache.cxf.jaxrs.ext.multipart.InputStreamDataSource;
+import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
+import org.apache.cxf.jaxrs.ext.multipart.MultipartOutputFilter;
+import org.apache.cxf.jaxrs.impl.EntityPartImpl;
+import org.apache.cxf.jaxrs.utils.ExceptionUtils;
+import org.apache.cxf.jaxrs.utils.multipart.AttachmentUtils;
+
+/**
+ * Jakarta RESTful Web Services 3.1:
+ * {@code java.util.List<EntityPart>}, Multipart data ( multipart/form-data )
+ */
+@Consumes({ "multipart/form-data" })
+@Produces({ "multipart/form-data" })
+@Provider
+public class EntityPartProvider extends AbstractConfigurableProvider
+ implements MessageBodyReader<List<EntityPart>>,
MessageBodyWriter<List<EntityPart>> {
+ private static final Logger LOG =
LogUtils.getL7dLogger(EntityPartProvider.class);
+
+ @Context private Providers providers;
+ @Context private MessageContext mc;
+ private String attachmentDir;
+ private String attachmentThreshold;
+ private String attachmentMaxSize;
+
+ public void setAttachmentDirectory(String dir) {
+ attachmentDir = dir;
+ }
+
+ public void setAttachmentThreshold(String threshold) {
+ attachmentThreshold = threshold;
+ }
+
+ public void setAttachmentMaxSize(String maxSize) {
+ attachmentMaxSize = maxSize;
+ }
+
+ @Override
+ public boolean isWriteable(Class<?> type, Type genericType,
+ Annotation[] annotations, MediaType mediaType) {
+
+ if (List.class.isAssignableFrom(type) &&
ParameterizedType.class.isInstance(genericType)) {
+ final Type arg =
((ParameterizedType)genericType).getActualTypeArguments()[0];
+ if (arg instanceof Class<?>) {
+ return EntityPart.class.isAssignableFrom((Class<?>)arg);
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void writeTo(List<EntityPart> parts, Class<?> type, Type
genericType,
+ Annotation[] anns, MediaType mediaType,
+ MultivaluedMap<String, Object> httpHeaders,
+ OutputStream os)
+ throws IOException, WebApplicationException {
+
+ final List<Attachment> handlers = convertToDataHandlers(parts, type,
genericType, anns);
+ if (mc.get(AttachmentUtils.OUT_FILTERS) != null) {
+ List<MultipartOutputFilter> filters =
CastUtils.cast((List<?>)mc.get(AttachmentUtils.OUT_FILTERS));
+ for (MultipartOutputFilter filter : filters) {
+ filter.filter(handlers);
+ }
+ }
+
+ mc.put(MultipartBody.OUTBOUND_MESSAGE_ATTACHMENTS, handlers);
+ handlers.get(0).getDataHandler().writeTo(os);
+ }
+
+ @Override
+ public boolean isReadable(Class<?> type, Type genericType, Annotation[]
annotations, MediaType mediaType) {
+ if (List.class.isAssignableFrom(type) &&
ParameterizedType.class.isInstance(genericType)) {
+ final Type arg =
((ParameterizedType)genericType).getActualTypeArguments()[0];
+ if (arg instanceof Class<?>) {
+ return EntityPart.class.isAssignableFrom((Class<?>)arg);
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public List<EntityPart> readFrom(final Class<List<EntityPart>> type, final
Type genericType,
+ final Annotation[] anns, final MediaType mediaType, final
MultivaluedMap<String, String> headers,
+ final InputStream is) throws IOException,
WebApplicationException {
+
+ checkContentLength();
+ final List<Attachment> infos = AttachmentUtils.getAttachments(
+ mc, attachmentDir, attachmentThreshold, attachmentMaxSize);
+
+ final List<EntityPart> parts = new ArrayList<>();
+ for (Attachment a : infos) {
+ parts.add(fromAttachment(a, InputStream.class, genericType, anns,
headers, mediaType));
+ }
+
+ return parts;
+ }
+
+ private <T> EntityPart fromAttachment(Attachment attachment, Class<T> c,
final Type type, Annotation[] anns,
+ final MultivaluedMap<String, String> headers, final MediaType
mediaType) throws IOException {
+
+ final GenericType<T> genericType = (type != null) ? new
GenericType<>(type) : null;
+ final MessageBodyReader<T> r = providers.getMessageBodyReader(c, type,
anns, attachment.getContentType());
+ if (r != null) {
+ final InputStream is =
attachment.getDataHandler().getInputStream();
+ final ContentDisposition cd = attachment.getContentDisposition();
+
+ final String fileName = (cd != null) ? cd.getFilename() : null;
+ final String cdName = cd == null ? null : cd.getParameter("name");
+ final String contentId = attachment.getContentId();
+ final String name = StringUtils.isEmpty(cdName) ? contentId :
cdName.replace("\"", "").replace("'", "");
+
+ if (!StringUtils.isEmpty(fileName)) {
+ return new EntityPartImpl(providers, name, fileName, is, c,
genericType, headers, mediaType);
+ } else {
+ return new EntityPartImpl(providers, name, null, is, c,
genericType, headers, mediaType);
+ }
+ } else {
+ throw ExceptionUtils.toBadRequestException(null, null);
+ }
+ }
+
+ protected void checkContentLength() {
+ if (mc != null && isPayloadEmpty(mc.getHttpHeaders())) {
+ String message = new
org.apache.cxf.common.i18n.Message("EMPTY_BODY", BUNDLE).toString();
+ LOG.warning(message);
+ throw new WebApplicationException(400);
+ }
+ }
+
+ private static <T> List<Attachment> convertToDataHandlers(final
List<EntityPart> parts, final Class<T> type,
+ final Type genericType, final Annotation[] anns) throws
IOException {
+ final List<Attachment> attachments = new ArrayList<>(parts.size());
+ for (EntityPart part: parts) {
+ attachments.add(createDataHandler(part, type, genericType, anns));
+ }
+ return attachments;
+ }
+
+ private static <T> Attachment createDataHandler(final EntityPart part,
final Class<T> type,
+ final Type genericType, final Annotation[] anns) throws
IOException {
+
+ final String mt = Objects
+ .requireNonNullElse(part.getMediaType(),
MediaType.APPLICATION_OCTET_STREAM_TYPE)
+ .toString();
+
+ final MultivaluedMap<String, String> headers = new
MultivaluedHashMap<>();
+ if (part.getHeaders() != null) {
+ headers.putAll(part.getHeaders());
+ }
+
+ return part.getFileName()
+ .map(fileName -> {
+ final ContentDisposition cd = new
ContentDisposition("form-data;name=file;filename=" + fileName);
+ return new Attachment(AttachmentUtil.BODY_ATTACHMENT_ID,
part.getContent(), cd);
+ })
+ .orElseGet(() -> {
+ headers.putSingle("Content-Type", mt);
+ return new Attachment(part.getName(),
+ new DataHandler(new
InputStreamDataSource(part.getContent(), mt)), headers);
+ });
+ }
+}
diff --git
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/ProviderFactory.java
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/ProviderFactory.java
index b9f6424a01..18f0a9e109 100644
---
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/ProviderFactory.java
+++
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/ProviderFactory.java
@@ -222,7 +222,8 @@ public abstract class ProviderFactory {
new PrimitiveTextProvider<Object>(),
JAXB_PROVIDER_CLASS.tryCreateInstance(factory.getBus()),
JAXB_ELEMENT_PROVIDER_CLASS.tryCreateInstance(factory.getBus()),
-
MULTIPART_PROVIDER_CLASS.tryCreateInstance(factory.getBus()));
+
MULTIPART_PROVIDER_CLASS.tryCreateInstance(factory.getBus()),
+ new EntityPartProvider());
final Object skipJakartaJsonProviders =
factory.getBus().getProperty(SKIP_JAKARTA_JSON_PROVIDERS_REGISTRATION);
if (!PropertyUtils.isTrue(skipJakartaJsonProviders)) {
factory.setProviders(false, false,
JSONP_PROVIDER_CLASS.tryCreateInstance(factory.getBus()),
diff --git
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/EntityPartUtils.java
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/EntityPartUtils.java
new file mode 100644
index 0000000000..08f7c2837b
--- /dev/null
+++
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/EntityPartUtils.java
@@ -0,0 +1,109 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.jaxrs.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.ws.rs.core.EntityPart;
+import jakarta.ws.rs.ext.Providers;
+import org.apache.cxf.attachment.AttachmentBoundaryDeserializer;
+import org.apache.cxf.common.util.StringUtils;
+import org.apache.cxf.jaxrs.ext.MessageContext;
+import org.apache.cxf.jaxrs.ext.multipart.Attachment;
+import org.apache.cxf.jaxrs.ext.multipart.ContentDisposition;
+import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
+import org.apache.cxf.jaxrs.impl.EntityPartImpl;
+import org.apache.cxf.jaxrs.impl.ProvidersImpl;
+import org.apache.cxf.jaxrs.utils.multipart.AttachmentUtils;
+import org.apache.cxf.message.Message;
+
+final class EntityPartUtils {
+ private static final String MULTIPART_FORM_DATA_TYPE = "form-data";
+ private static final String CONTENT_DISPOSITION_FILES_PARAM = "files";
+
+ private EntityPartUtils() {
+ }
+
+ public static List<EntityPart> getEntityParts(final MessageContext mc) {
+ return from(mc.getProviders(), AttachmentUtils.getMultipartBody(mc));
+ }
+
+ public static EntityPart getEntityPart(final String value, final Message
message) {
+ final Providers providers = new ProvidersImpl(message);
+ try (InputStream is = new
ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8))) {
+ final AttachmentBoundaryDeserializer deserializer = new
AttachmentBoundaryDeserializer(message);
+ final Attachment attachment = new
Attachment(deserializer.read(is), providers);
+ return createFromAttachment(providers, attachment);
+ } catch (IOException ex) {
+ throw ExceptionUtils.toBadRequestException(null, null);
+ }
+ }
+
+ private static List<EntityPart> from(final Providers providers, final
MultipartBody body) {
+ final List<Attachment> atts = body.getAllAttachments();
+ final List<EntityPart> parts = new ArrayList<>(atts.size());
+
+ for (Attachment a : atts) {
+ final EntityPart part = createFromAttachment(providers, a);
+ if (part != null) {
+ parts.add(part);
+ }
+ }
+
+ return parts;
+ }
+
+ private static EntityPart createFromAttachment(final Providers providers,
Attachment a) {
+ final ContentDisposition cd = a.getContentDisposition();
+ if (cd != null &&
!MULTIPART_FORM_DATA_TYPE.equalsIgnoreCase(cd.getType())) {
+ return null;
+ }
+
+ final String fileName = (cd != null) ? cd.getFilename() : null;
+ final String cdName = cd == null ? null : cd.getParameter("name");
+ final String contentId = a.getContentId();
+ final String name = StringUtils.isEmpty(cdName) ? contentId :
cdName.replace("\"", "").replace("'", "");
+ if (StringUtils.isEmpty(name) && StringUtils.isEmpty(fileName)) {
+ throw ExceptionUtils.toBadRequestException(null, null);
+ }
+
+ if (CONTENT_DISPOSITION_FILES_PARAM.equals(name)) {
+ return null;
+ }
+
+ try {
+ final InputStream is = a.getDataHandler().getInputStream();
+ if (!StringUtils.isEmpty(fileName)) {
+ return new EntityPartImpl(providers, name, fileName, is,
InputStream.class, null,
+ a.getHeaders(), a.getContentType());
+ } else {
+ return new EntityPartImpl(providers, name, null, is,
InputStream.class, null,
+ a.getHeaders(), a.getContentType());
+ }
+ } catch (IOException ex) {
+ throw ExceptionUtils.toBadRequestException(null, null);
+ }
+ }
+}
diff --git
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/FormUtils.java
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/FormUtils.java
index e4404f247c..f8443785ba 100644
--- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/FormUtils.java
+++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/FormUtils.java
@@ -35,6 +35,7 @@ import java.util.logging.Logger;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.EntityPart;
import jakarta.ws.rs.core.Form;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
@@ -231,6 +232,24 @@ public final class FormUtils {
}
}
+
+ public static void populateMapFromEntityParts(MultivaluedMap<String,
String> params,
+ List<EntityPart> parts,
+ Message m,
+ boolean decode) {
+ checkNumberOfParts(m, parts.size());
+ for (EntityPart part : parts) {
+ try {
+ final String value = part.getContent(String.class);
+ params.add(HttpUtils.urlDecode(part.getName()), decode ?
HttpUtils.urlDecode(value) : value);
+ } catch (IllegalArgumentException ex) {
+ throw ExceptionUtils.toInternalServerErrorException(ex, null);
+ } catch (IOException ex) {
+ throw ExceptionUtils.toBadRequestException(null, null);
+ }
+ }
+ }
+
public static void populateMapFromMultipart(MultivaluedMap<String, String>
params,
MultipartBody body,
diff --git
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/InjectionUtils.java
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/InjectionUtils.java
index ce12204d6c..17e15d63ff 100644
---
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/InjectionUtils.java
+++
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/InjectionUtils.java
@@ -55,6 +55,7 @@ import java.util.logging.Logger;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.EntityPart;
import jakarta.ws.rs.core.GenericEntity;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
@@ -522,6 +523,10 @@ public final class InjectionUtils {
result = null;
}
}
+
+ if (EntityPart.class.isAssignableFrom(pClass) && message != null) {
+ result = EntityPartUtils.getEntityPart(value, message);
+ }
if (result == null) {
reportServerError("WRONG_PARAMETER_TYPE", pClass.getName());
diff --git
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/JAXRSUtils.java
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/JAXRSUtils.java
index c2b1d504ec..a2ca3d98f2 100644
--- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/JAXRSUtils.java
+++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/JAXRSUtils.java
@@ -67,6 +67,7 @@ import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.Cookie;
+import jakarta.ws.rs.core.EntityPart;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
@@ -175,6 +176,7 @@ public final class JAXRSUtils {
Arrays.asList(InputStream.class, Reader.class, StreamingOutput.class));
private static final Set<String> STREAMING_LIKE_OUT_TYPES = new HashSet<>(
Arrays.asList(
+ "jakarta.ws.rs.core.EntityPart",
"org.apache.cxf.jaxrs.ext.xml.XMLSource",
"org.apache.cxf.jaxrs.ext.multipart.InputStreamDataSource",
"org.apache.cxf.jaxrs.ext.multipart.MultipartBody",
@@ -1199,8 +1201,13 @@ public final class JAXRSUtils {
} else {
if ("multipart".equalsIgnoreCase(mt.getType())
&& MediaType.MULTIPART_FORM_DATA_TYPE.isCompatible(mt)) {
- MultipartBody body = AttachmentUtils.getMultipartBody(mc);
- FormUtils.populateMapFromMultipart(params, body, m,
decode);
+ if (pClass.isAssignableFrom(EntityPart.class)) {
+ final List<EntityPart> body =
EntityPartUtils.getEntityParts(mc);
+ FormUtils.populateMapFromEntityParts(params, body, m,
decode);
+ } else {
+ MultipartBody body =
AttachmentUtils.getMultipartBody(mc);
+ FormUtils.populateMapFromMultipart(params, body, m,
decode);
+ }
} else {
org.apache.cxf.common.i18n.Message errorMsg =
new
org.apache.cxf.common.i18n.Message("WRONG_FORM_MEDIA_TYPE",
diff --git
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ParameterizedCollectionType.java
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ParameterizedCollectionBaseType.java
similarity index 73%
copy from
rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ParameterizedCollectionType.java
copy to
rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ParameterizedCollectionBaseType.java
index dad33f5c52..bdbe8a65dd 100644
---
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ParameterizedCollectionType.java
+++
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ParameterizedCollectionBaseType.java
@@ -23,19 +23,17 @@ import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
-public final class ParameterizedCollectionType implements ParameterizedType {
+abstract class ParameterizedCollectionBaseType implements ParameterizedType {
+ private final Class<? extends Collection<?>> collectionClass;
private final Class<?> collectionMemberClass;
private final Type[] typeArgs;
- public ParameterizedCollectionType(Class<?> collectionMemberClass) {
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ ParameterizedCollectionBaseType(Class<? extends Collection>
collectionClass, Class<?> collectionMemberClass) {
+ this.collectionClass = (Class<? extends Collection<?>>)
collectionClass;
this.collectionMemberClass = collectionMemberClass;
this.typeArgs = new Type[] {collectionMemberClass};
}
-
- public ParameterizedCollectionType(Type pt) {
- this.collectionMemberClass = InjectionUtils.getRawType(pt);
- this.typeArgs = new Type[] {pt};
- }
public Type[] getActualTypeArguments() {
return typeArgs;
@@ -46,10 +44,10 @@ public final class ParameterizedCollectionType implements
ParameterizedType {
}
public Type getRawType() {
- return Collection.class;
+ return collectionClass;
}
public String toString() {
- return "java.util.Collection<" + collectionMemberClass.getName() + ">";
+ return collectionClass.getName() + "<" +
collectionMemberClass.getName() + ">";
}
}
diff --git
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ParameterizedCollectionType.java
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ParameterizedCollectionType.java
index dad33f5c52..c50dafd42c 100644
---
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ParameterizedCollectionType.java
+++
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ParameterizedCollectionType.java
@@ -19,37 +19,15 @@
package org.apache.cxf.jaxrs.utils;
-import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
-public final class ParameterizedCollectionType implements ParameterizedType {
- private final Class<?> collectionMemberClass;
- private final Type[] typeArgs;
-
+public final class ParameterizedCollectionType extends
ParameterizedCollectionBaseType {
public ParameterizedCollectionType(Class<?> collectionMemberClass) {
- this.collectionMemberClass = collectionMemberClass;
- this.typeArgs = new Type[] {collectionMemberClass};
- }
-
- public ParameterizedCollectionType(Type pt) {
- this.collectionMemberClass = InjectionUtils.getRawType(pt);
- this.typeArgs = new Type[] {pt};
- }
-
- public Type[] getActualTypeArguments() {
- return typeArgs;
+ super(Collection.class, collectionMemberClass);
}
- public Type getOwnerType() {
- return null;
- }
-
- public Type getRawType() {
- return Collection.class;
- }
-
- public String toString() {
- return "java.util.Collection<" + collectionMemberClass.getName() + ">";
+ public ParameterizedCollectionType(Type pt) {
+ super(Collection.class, InjectionUtils.getRawType(pt));
}
}
diff --git
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ParameterizedListType.java
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ParameterizedListType.java
new file mode 100644
index 0000000000..91bd873bd5
--- /dev/null
+++
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ParameterizedListType.java
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.jaxrs.utils;
+
+import java.util.List;
+
+public final class ParameterizedListType extends
ParameterizedCollectionBaseType {
+ public ParameterizedListType(Class<?> collectionMemberClass) {
+ super(List.class, collectionMemberClass);
+ }
+}
diff --git
a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/ProviderFactoryTest.java
b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/ProviderFactoryTest.java
index 27592dbefc..da351ce764 100644
---
a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/ProviderFactoryTest.java
+++
b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/ProviderFactoryTest.java
@@ -184,9 +184,9 @@ public class ProviderFactoryTest {
WildcardReader2 reader2 = new WildcardReader2();
pf.registerUserProvider(reader2);
List<ProviderInfo<MessageBodyReader<?>>> readers =
pf.getMessageReaders();
- assertEquals(11, readers.size());
- assertSame(reader1, readers.get(7).getProvider());
- assertSame(reader2, readers.get(8).getProvider());
+ assertEquals(12, readers.size());
+ assertSame(reader1, readers.get(8).getProvider());
+ assertSame(reader2, readers.get(9).getProvider());
}
@Test
@@ -212,13 +212,13 @@ public class ProviderFactoryTest {
// writers
List<ProviderInfo<MessageBodyWriter<?>>> writers =
pf.getMessageWriters();
- assertEquals(10, writers.size());
- Object lastWriter = writers.get(9).getProvider();
+ assertEquals(11, writers.size());
+ Object lastWriter = writers.get(10).getProvider();
assertTrue(lastWriter instanceof StringTextProvider);
//readers
List<ProviderInfo<MessageBodyReader<?>>> readers =
pf.getMessageReaders();
- assertEquals(9, readers.size());
- Object lastReader = readers.get(8).getProvider();
+ assertEquals(10, readers.size());
+ Object lastReader = readers.get(9).getProvider();
assertTrue(lastReader instanceof StringTextProvider);
}
@Test
@@ -242,13 +242,13 @@ public class ProviderFactoryTest {
// writers
List<ProviderInfo<MessageBodyWriter<?>>> writers =
pf.getMessageWriters();
- assertEquals(10, writers.size());
- Object lastWriter = writers.get(9).getProvider();
+ assertEquals(11, writers.size());
+ Object lastWriter = writers.get(10).getProvider();
assertTrue(lastWriter instanceof StringTextProvider);
//readers
List<ProviderInfo<MessageBodyReader<?>>> readers =
pf.getMessageReaders();
- assertEquals(9, readers.size());
- Object lastReader = readers.get(8).getProvider();
+ assertEquals(10, readers.size());
+ Object lastReader = readers.get(9).getProvider();
assertTrue(lastReader instanceof StringTextProvider);
}
@SuppressWarnings("rawtypes")
@@ -276,13 +276,13 @@ public class ProviderFactoryTest {
// writers
List<ProviderInfo<MessageBodyWriter<?>>> writers =
pf.getMessageWriters();
- assertEquals(10, writers.size());
+ assertEquals(11, writers.size());
Object lastWriter = writers.get(8).getProvider();
assertFalse(lastWriter instanceof StringTextProvider);
//readers
List<ProviderInfo<MessageBodyReader<?>>> readers =
pf.getMessageReaders();
- assertEquals(9, readers.size());
- Object lastReader = readers.get(8).getProvider();
+ assertEquals(10, readers.size());
+ Object lastReader = readers.get(9).getProvider();
assertTrue(lastReader instanceof StringTextProvider);
}
@Test
@@ -309,13 +309,13 @@ public class ProviderFactoryTest {
// writers
List<ProviderInfo<MessageBodyWriter<?>>> writers =
pf.getMessageWriters();
- assertEquals(10, writers.size());
- Object lastWriter = writers.get(9).getProvider();
+ assertEquals(11, writers.size());
+ Object lastWriter = writers.get(10).getProvider();
assertTrue(lastWriter instanceof StringTextProvider);
//readers
List<ProviderInfo<MessageBodyReader<?>>> readers =
pf.getMessageReaders();
- assertEquals(9, readers.size());
- Object lastReader = readers.get(8).getProvider();
+ assertEquals(10, readers.size());
+ Object lastReader = readers.get(9).getProvider();
assertFalse(lastReader instanceof StringTextProvider);
}
@@ -401,9 +401,9 @@ public class ProviderFactoryTest {
ProviderFactory pf = ServerProviderFactory.createInstance(bus);
pf.registerUserProvider(wc2);
List<ProviderInfo<MessageBodyReader<?>>> readers =
pf.getMessageReaders();
- assertEquals(11, readers.size());
- assertSame(wc2, readers.get(7).getProvider());
- assertSame(wc1, readers.get(8).getProvider());
+ assertEquals(12, readers.size());
+ assertSame(wc2, readers.get(8).getProvider());
+ assertSame(wc1, readers.get(9).getProvider());
}
@Test
public void testCustomProviderSortingWithBus2() {
@@ -414,9 +414,9 @@ public class ProviderFactoryTest {
ProviderFactory pf = ServerProviderFactory.createInstance(bus);
pf.registerUserProvider(wc1);
List<ProviderInfo<MessageBodyReader<?>>> readers =
pf.getMessageReaders();
- assertEquals(11, readers.size());
- assertSame(wc1, readers.get(7).getProvider());
- assertSame(wc2, readers.get(8).getProvider());
+ assertEquals(12, readers.size());
+ assertSame(wc1, readers.get(8).getProvider());
+ assertSame(wc2, readers.get(9).getProvider());
}
@Test
@@ -1548,11 +1548,11 @@ public class ProviderFactoryTest {
pf.registerUserProvider(reader);
List<ProviderInfo<MessageBodyReader<?>>> readers =
pf.getMessageReaders();
- assertEquals(10, readers.size());
- assertSame(reader, readers.get(7).getProvider());
+ assertEquals(11, readers.size());
+ assertSame(reader, readers.get(8).getProvider());
pf.registerUserProvider(new ClientWildcardReader());
- assertEquals(10, pf.getMessageReaders().size());
+ assertEquals(11, pf.getMessageReaders().size());
}
private static class RuntimeExceptionA extends RuntimeException {
diff --git
a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/EntityPartServer.java
b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/EntityPartServer.java
new file mode 100644
index 0000000000..9cdf5fe6b3
--- /dev/null
+++
b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/EntityPartServer.java
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.systest.jaxrs;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.cxf.Bus;
+import org.apache.cxf.attachment.AttachmentDeserializer;
+import org.apache.cxf.endpoint.Server;
+import org.apache.cxf.ext.logging.LoggingInInterceptor;
+import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
+import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
+import org.apache.cxf.jaxrs.provider.ProviderFactory;
+import org.apache.cxf.testutil.common.AbstractServerTestServerBase;
+
+public class EntityPartServer extends AbstractServerTestServerBase {
+ public static final String PORT = allocatePort(EntityPartServer.class);
+
+ @Override
+ protected Server createServer(Bus bus) throws Exception {
+
bus.setProperty(ProviderFactory.SKIP_JAKARTA_JSON_PROVIDERS_REGISTRATION, true);
+ bus.getInInterceptors().add(new LoggingInInterceptor());
+
+ JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
+ sf.setResourceClasses(EntityPartStore.class);
+
+ Map<String, Object> props = new HashMap<>();
+ props.put(AttachmentDeserializer.ATTACHMENT_MAX_SIZE,
String.valueOf(1024 * 10));
+ props.put(AttachmentDeserializer.ATTACHMENT_MEMORY_THRESHOLD,
String.valueOf(1024 * 5));
+ props.put(AttachmentDeserializer.ATTACHMENT_MAX_HEADER_SIZE,
String.valueOf(400));
+ sf.setProperties(props);
+ //default lifecycle is per-request, change it to singleton
+ sf.setResourceProvider(EntityPartStore.class,
+ new SingletonResourceProvider(new
EntityPartStore()));
+ sf.setAddress("http://localhost:" + PORT + "/");
+
+ return sf.create();
+ }
+
+ public static void main(String[] args) throws Exception {
+ new EntityPartServer().start();
+ }
+
+}
diff --git
a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/EntityPartStore.java
b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/EntityPartStore.java
new file mode 100644
index 0000000000..f03eda7275
--- /dev/null
+++
b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/EntityPartStore.java
@@ -0,0 +1,101 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.systest.jaxrs;
+
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.EntityPart;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.Response;
+
+@Path("/bookstore")
+public class EntityPartStore {
+ @POST
+ @Path("/books/images")
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ public List<EntityPart> postImags(List<EntityPart> parts) throws
IOException {
+ final List<EntityPart> replies = new ArrayList<>(parts.size());
+
+ for (EntityPart part : parts) {
+ final Optional<String> fileName = part.getFileName();
+ final MultivaluedMap<String, String> headers = part.getHeaders();
+ final MediaType mediaType = part.getMediaType();
+ try (InputStream is = part.getContent()) {
+ replies.add(
+ EntityPart
+ .withFileName(fileName.get())
+ .headers(headers)
+ .mediaType(mediaType)
+ .content(is.readAllBytes())
+ .build());
+ }
+ }
+
+ return replies;
+ }
+
+ @POST
+ @Path("/books/jaxbform")
+ @Produces(MediaType.APPLICATION_XML)
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ public Response addBookJaxbFromForm(@FormParam("bookXML") EntityPart part)
throws Exception {
+ Book b1 = part.getContent(Book.class);
+ b1.setId(124);
+ return Response.ok(b1).build();
+ }
+
+ @POST
+ @Path("/books/jsonform")
+ @Produces(MediaType.APPLICATION_XML)
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ public Response addBookJsonFromForm(@FormParam("gazetteer") EntityPart
part) throws Exception {
+ Book b1 = part.getContent(Book.class);
+ b1.setId(124);
+ return Response.ok(b1).build();
+ }
+
+ @POST
+ @Path("/books/jsonjaxbform")
+ @Produces(MediaType.APPLICATION_XML)
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ public Response addBookJaxbJsonForm(@FormParam("bookXML") EntityPart part1,
+ @FormParam("gazetteer") EntityPart part2) throws Exception {
+ final Book b1 = part1.getContent(Book.class);
+ final Book b2 = part2.getContent(Book.class);
+ if (!"CXF in Action - 2".equals(b1.getName())
+ || !"CXF in Action - 2".equals(b2.getName())) {
+ throw new WebApplicationException();
+ }
+ b2.setId(124);
+ return Response.ok(b2).build();
+ }
+}
diff --git
a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/JAXRSEntityPartTest.java
b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/JAXRSEntityPartTest.java
new file mode 100644
index 0000000000..b536c69383
--- /dev/null
+++
b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/JAXRSEntityPartTest.java
@@ -0,0 +1,172 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.systest.jaxrs;
+
+import java.io.File;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.List;
+
+import jakarta.ws.rs.core.EntityPart;
+import jakarta.ws.rs.core.GenericType;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.apache.cxf.helpers.IOUtils;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.jaxrs.utils.ParameterizedListType;
+import org.apache.cxf.testutil.common.AbstractBusClientServerTestBase;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class JAXRSEntityPartTest extends AbstractBusClientServerTestBase {
+ public static final String PORT = EntityPartServer.PORT;
+ public static final String PORTINV =
allocatePort(JAXRSEntityPartTest.class, 1);
+
+ @BeforeClass
+ public static void startServers() throws Exception {
+ assertTrue("server did not launch correctly",
launchServer(EntityPartServer.class, true));
+ createStaticBus();
+ }
+
+ @Test
+ public void testUploadImage() throws Exception {
+ final File file = new
File(getClass().getResource("/org/apache/cxf/systest/jaxrs/resources/java.jpg")
+ .toURI().getPath());
+ final String address = "http://localhost:" + PORT +
"/bookstore/books/images";
+ final WebClient client = WebClient.create(address);
+
client.type(MediaType.MULTIPART_FORM_DATA).accept(MediaType.MULTIPART_FORM_DATA);
+
+ try (InputStream is = Files.newInputStream(file.toPath())) {
+ final EntityPart part = EntityPart
+ .withFileName(file.getName())
+ .content(is)
+ .build();
+
+ try (Response response = client.postCollection(List.of(part),
EntityPart.class)) {
+ assertThat(response.getStatus(), equalTo(200));
+
+ @SuppressWarnings("unchecked")
+ final List<EntityPart> parts = (List<EntityPart>) response
+ .readEntity(new GenericType<>(new
ParameterizedListType(EntityPart.class)));
+
+ assertThat(parts, hasSize(1));
+ assertThat(parts.get(0), is(not(nullValue())));
+
+ assertThat(parts.get(0).getFileName().isPresent(), is(true));
+ assertThat(parts.get(0).getFileName().get(),
equalTo(part.getFileName().get()));
+
+
assertArrayEquals(IOUtils.readBytesFromStream(parts.get(0).getContent()),
+
IOUtils.readBytesFromStream(Files.newInputStream(file.toPath())));
+ }
+ }
+ }
+
+ @Test
+ public void testBookJSONForm() throws Exception {
+ String address = "http://localhost:" + PORT +
"/bookstore/books/jsonform";
+ doAddFormBook(address, "attachmentFormJson", "gazetteer",
MediaType.APPLICATION_JSON, 200);
+ }
+
+ @Test
+ public void testBookJSONJAXBForm() throws Exception {
+ String address = "http://localhost:" + PORT +
"/bookstore/books/jsonjaxbform";
+
+ final WebClient client = WebClient.create(address);
+
client.type(MediaType.MULTIPART_FORM_DATA).accept(MediaType.APPLICATION_XML);
+
+ try (InputStream is1 =
+
getClass().getResourceAsStream("/org/apache/cxf/systest/jaxrs/resources/attachmentFormJaxb");
+ InputStream is2 =
+
getClass().getResourceAsStream("/org/apache/cxf/systest/jaxrs/resources/attachmentFormJson"))
{
+
+ final EntityPart part1 = EntityPart
+ .withName("bookXML")
+ .content(is1)
+ .mediaType(MediaType.APPLICATION_XML)
+ .build();
+
+ final EntityPart part2 = EntityPart
+ .withName("gazetteer")
+ .content(is2)
+ .mediaType(MediaType.APPLICATION_JSON)
+ .build();
+
+ try (Response response = client.postCollection(List.of(part1,
part2), EntityPart.class)) {
+ assertThat(response.getStatus(), equalTo(200));
+
+ try (InputStream expected =
getClass().getResourceAsStream("resources/expected_add_book.txt")) {
+
assertEquals(stripXmlInstructionIfNeeded(IOUtils.toString(expected)),
+
stripXmlInstructionIfNeeded(response.readEntity(String.class)));
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testBookJaxbForm() throws Exception {
+ String address = "http://localhost:" + PORT +
"/bookstore/books/jaxbform";
+ doAddFormBook(address, "attachmentFormJaxb", "bookXML",
MediaType.APPLICATION_XML, 200);
+ }
+
+ private void doAddFormBook(String address, String resourceName,
+ String name, String mt, int status) throws Exception {
+
+ final WebClient client = WebClient.create(address);
+
client.type(MediaType.MULTIPART_FORM_DATA).accept(MediaType.APPLICATION_XML);
+
+ try (InputStream is =
+
getClass().getResourceAsStream("/org/apache/cxf/systest/jaxrs/resources/" +
resourceName)) {
+
+ final EntityPart part = EntityPart
+ .withName(name)
+ .content(is)
+ .mediaType(mt)
+ .build();
+
+ try (Response response = client.postCollection(List.of(part),
EntityPart.class)) {
+ assertThat(response.getStatus(), equalTo(200));
+
+ try (InputStream expected =
getClass().getResourceAsStream("resources/expected_add_book.txt")) {
+
assertEquals(stripXmlInstructionIfNeeded(IOUtils.toString(expected)),
+
stripXmlInstructionIfNeeded(response.readEntity(String.class)));
+ }
+ }
+ }
+ }
+
+ private static String stripXmlInstructionIfNeeded(String str) {
+ if (str != null && str.startsWith("<?xml")) {
+ int index = str.indexOf("?>");
+ str = str.substring(index + 2);
+ }
+ return str;
+ }
+}