Support a single EntityPart as an entity

Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/MultiPartFeature.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/MultiPartFeature.java
index 03bf661..c49be99 100644
--- a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/MultiPartFeature.java
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/MultiPartFeature.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2023 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -26,6 +26,8 @@
 import org.glassfish.jersey.media.multipart.internal.MultiPartReaderClientSide;
 import org.glassfish.jersey.media.multipart.internal.MultiPartReaderServerSide;
 import org.glassfish.jersey.media.multipart.internal.MultiPartWriter;
+import org.glassfish.jersey.media.multipart.internal.SingleEntityPartReader;
+import org.glassfish.jersey.media.multipart.internal.SingleEntityPartWriter;
 
 /**
  * Feature used to register Multipart providers.
@@ -49,6 +51,8 @@
 
         context.register(EntityPartReader.class);
         context.register(EntityPartWriter.class);
+        context.register(SingleEntityPartReader.class);
+        context.register(SingleEntityPartWriter.class);
 
         return true;
     }
diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/SingleEntityPartReader.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/SingleEntityPartReader.java
new file mode 100644
index 0000000..9b7589e
--- /dev/null
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/SingleEntityPartReader.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.media.multipart.internal;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.EntityPart;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.ext.MessageBodyReader;
+import jakarta.ws.rs.ext.Providers;
+import org.glassfish.jersey.media.multipart.BodyPart;
+import org.glassfish.jersey.media.multipart.FormDataBodyPart;
+import org.glassfish.jersey.media.multipart.JerseyEntityPartBuilderProvider;
+import org.glassfish.jersey.media.multipart.MultiPart;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.LinkedList;
+import java.util.List;
+
+public class SingleEntityPartReader implements MessageBodyReader<EntityPart> {
+
+    private MultiPartReaderClientSide multiPartReaderClientSide;
+
+    private final Providers providers;
+
+    @Inject
+    public SingleEntityPartReader(@Context Providers providers) {
+        this.providers = providers;
+    }
+
+    @Override
+    public boolean isReadable(Class<?> type, Type generic, Annotation[] annotations, MediaType mediaType) {
+        return EntityPart.class.isAssignableFrom(type);
+    }
+
+    @Override
+    public EntityPart readFrom(Class<EntityPart> type, Type genericType, Annotation[] annotations,
+                                     MediaType mediaType, MultivaluedMap<String, String> httpHeaders,
+                                     InputStream entityStream) throws IOException, WebApplicationException {
+
+        if (multiPartReaderClientSide == null) {
+            multiPartReaderClientSide = (MultiPartReaderClientSide) providers.getMessageBodyReader(
+                    MultiPart.class, MultiPart.class, new Annotation[0], MediaType.MULTIPART_FORM_DATA_TYPE);
+        }
+
+        final MultiPart multiPart = multiPartReaderClientSide.readFrom(
+                MultiPart.class, MultiPart.class, annotations, mediaType, httpHeaders, entityStream);
+        final List<BodyPart> bodyParts = multiPart.getBodyParts();
+        final List<EntityPart> entityParts = new LinkedList<>();
+
+        for (BodyPart bp : bodyParts) {
+            if (FormDataBodyPart.class.isInstance(bp)) {
+                entityParts.add((EntityPart) bp);
+            } else {
+                final EntityPart ep = new JerseyEntityPartBuilderProvider().withName("")
+                        .mediaType(bp.getMediaType()).content(bp.getEntity()).headers(bp.getHeaders()).build();
+                entityParts.add(ep);
+            }
+            // consume all bodyParts
+        }
+
+        return entityParts.get(0);
+    }
+}
diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/SingleEntityPartWriter.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/SingleEntityPartWriter.java
new file mode 100644
index 0000000..9150dc6
--- /dev/null
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/SingleEntityPartWriter.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.media.multipart.internal;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.EntityPart;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.ext.MessageBodyWriter;
+import jakarta.ws.rs.ext.Providers;
+import org.glassfish.jersey.media.multipart.BodyPart;
+import org.glassfish.jersey.media.multipart.MultiPart;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+public class SingleEntityPartWriter implements MessageBodyWriter<EntityPart> {
+
+    private MultiPartWriter multiPartWriter;
+
+    private final Providers providers;
+
+    @Inject
+    public SingleEntityPartWriter(@Context Providers providers) {
+        this.providers = providers;
+    }
+
+    @Override
+    public boolean isWriteable(Class<?> type, Type generic, Annotation[] annotations, MediaType mediaType) {
+        return EntityPart.class.isAssignableFrom(type);
+    }
+
+    @Override
+    public void writeTo(EntityPart entityParts, Class<?> type, Type genericType, Annotation[] annotations,
+                        MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
+            throws IOException, WebApplicationException {
+        final MultiPart multiPart = new MultiPart();
+        multiPart.setMediaType(mediaType);
+        multiPart.bodyPart((BodyPart) entityParts);
+
+        if (multiPartWriter == null) {
+            multiPartWriter = (MultiPartWriter) providers.getMessageBodyWriter(
+                    MultiPart.class, MultiPart.class, new Annotation[0], MediaType.MULTIPART_FORM_DATA_TYPE);
+        }
+
+        multiPartWriter.writeTo(multiPart, MultiPart.class, MultiPart.class,
+                annotations, multiPart.getMediaType(), httpHeaders, entityStream);
+    }
+}
diff --git a/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/internal/EntityPartTest.java b/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/internal/EntityPartTest.java
index 084238f..e6bd4e6 100644
--- a/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/internal/EntityPartTest.java
+++ b/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/internal/EntityPartTest.java
@@ -298,6 +298,21 @@
         }
     }
 
+    @Test
+    public void postHeaderNoListTest() throws IOException {
+        EntityPart entityPart = EntityPart.withName("name1").content("data1").header("header-01", "value-01").build();
+        Entity entity = Entity.entity(entityPart, MediaType.MULTIPART_FORM_DATA_TYPE);
+        try (Response response = target("/postHeaders").request().post(entity)) {
+            response.bufferEntity();
+            List<EntityPart> result = response.readEntity(LIST_ENTITY_PART_TYPE);
+            assertEquals("value-01", result.get(0).getHeaders().getFirst("header-01"));
+            assertEquals("data1", result.get(0).getContent(String.class));
+
+            EntityPart firstEntity = response.readEntity(EntityPart.class);
+            assertEquals("value-01", result.get(0).getHeaders().getFirst("header-01"));
+        }
+    }
+
     @Consumes(MediaType.TEXT_PLAIN)
     @Produces(MediaType.TEXT_PLAIN)
     public static class AtomicReferenceProvider implements