Implementation of EntityPart API (#4859)

* Implementation of EntityPart API

Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/multipart/JerseyEntityPart.java b/core-common/src/main/java/org/glassfish/jersey/innate/multipart/JerseyEntityPart.java
new file mode 100644
index 0000000..944a230
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/multipart/JerseyEntityPart.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.innate.multipart;
+
+import jakarta.ws.rs.core.EntityPart;
+
+import java.lang.reflect.Type;
+
+/**
+ * Jersey extended {@code EntityPart}. Contains arbitrary useful methods.
+ *
+ * @since 3.1.0
+ */
+public interface JerseyEntityPart extends EntityPart {
+    /**
+     * Converts the content stream for this part to the specified class and returns
+     * it.
+     *
+     * Subsequent invocations will result in an {@code IllegalStateException}.
+     * Likewise this method will throw an {@code IllegalStateException} if it is called after calling
+     * {@link #getContent} or similar {@code getContent} method.
+     *
+     * @param <T> type parameter of the value returned
+     * @param type the {@code Class} that the implementation should convert this
+     *             part to
+     * @param <T> the entity type
+     * @return an instance of the specified {@code Class} representing the content
+     *         of this part
+     * @throws IllegalStateException    if this method or any of the other
+     *                                  {@code getContent} methods has already been
+     *                                  invoked
+     */
+    <T> T getContent(Class<T> type, Type genericType);
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/multipart/package-info.java b/core-common/src/main/java/org/glassfish/jersey/innate/multipart/package-info.java
new file mode 100644
index 0000000..e6ebf1f
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/multipart/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2021 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
+ */
+
+/**
+ * Multipart Jersey innate classes. This innate package will be opened by JPMS only to Jersey-media-multipart.
+ */
+package org.glassfish.jersey.innate.multipart;
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/package-info.java b/core-common/src/main/java/org/glassfish/jersey/innate/package-info.java
new file mode 100644
index 0000000..b0648e7
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2021 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
+ */
+
+/**
+ * Jersey innate packages. The innate packages will not be opened by JPMS outside of Jersey.
+ */
+package org.glassfish.jersey.innate;
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/spi/EntityPartBuilderProvider.java b/core-common/src/main/java/org/glassfish/jersey/innate/spi/EntityPartBuilderProvider.java
new file mode 100644
index 0000000..606e212
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/spi/EntityPartBuilderProvider.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.innate.spi;
+
+import jakarta.ws.rs.core.EntityPart;
+
+/**
+ * Jersey extension of provider of EntityPart.Builder.
+ * A service meant to be implemented solely by Jersey.
+ *
+ * @since 3.1.0
+ */
+public interface EntityPartBuilderProvider {
+
+    /**
+     * @param partName name of the part to create within the multipart entity.
+     * @return {@link EntityPart.Builder} for building new {@link EntityPart} instances.
+     */
+    public EntityPart.Builder withName(String partName);
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/spi/package-info.java b/core-common/src/main/java/org/glassfish/jersey/innate/spi/package-info.java
new file mode 100644
index 0000000..3d70312
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/spi/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2021 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
+ */
+
+/**
+ * Common Jersey innate SPI classes. The innate package will not be opened by JPMS.
+ */
+package org.glassfish.jersey.innate.spi;
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/AbstractRuntimeDelegate.java b/core-common/src/main/java/org/glassfish/jersey/internal/AbstractRuntimeDelegate.java
index aa439db..39940fb 100644
--- a/core-common/src/main/java/org/glassfish/jersey/internal/AbstractRuntimeDelegate.java
+++ b/core-common/src/main/java/org/glassfish/jersey/internal/AbstractRuntimeDelegate.java
@@ -25,14 +25,20 @@
 import jakarta.ws.rs.core.CacheControl;
 import jakarta.ws.rs.core.Configuration;
 import jakarta.ws.rs.core.Cookie;
+import jakarta.ws.rs.core.EntityPart;
 import jakarta.ws.rs.core.EntityTag;
 import jakarta.ws.rs.core.Link;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.NewCookie;
 import jakarta.ws.rs.core.Response.ResponseBuilder;
 import jakarta.ws.rs.core.UriBuilder;
+import jakarta.ws.rs.ext.ParamConverter;
 import jakarta.ws.rs.ext.RuntimeDelegate;
 
+import org.glassfish.jersey.innate.spi.EntityPartBuilderProvider;
+import org.glassfish.jersey.internal.util.collection.LazyValue;
+import org.glassfish.jersey.internal.util.collection.Value;
+import org.glassfish.jersey.internal.util.collection.Values;
 import org.glassfish.jersey.message.internal.JerseyLink;
 import org.glassfish.jersey.message.internal.OutboundJaxrsResponse;
 import org.glassfish.jersey.message.internal.OutboundMessageContext;
@@ -50,6 +56,8 @@
 
     private final Set<HeaderDelegateProvider> hps;
     private final Map<Class<?>, HeaderDelegate<?>> map;
+    private LazyValue<EntityPartBuilderProvider> entityPartBuilderProvider = Values.lazy(
+            (Value<EntityPartBuilderProvider>) () -> findEntityPartBuilderProvider());
 
     /**
      * Initialization constructor. The injection manager will be shut down.
@@ -117,4 +125,24 @@
 
         return null;
     }
+
+    @Override
+    public EntityPart.Builder createEntityPartBuilder(String partName) throws IllegalArgumentException {
+        return entityPartBuilderProvider.get().withName(partName);
+    }
+
+    /**
+     * Obtain a {@code RuntimeDelegate} instance using the method described in {@link #getInstance}.
+     *
+     * @return an instance of {@code RuntimeDelegate}.
+     */
+    private static EntityPartBuilderProvider findEntityPartBuilderProvider() {
+        for (final EntityPartBuilderProvider entityPartBuilder : ServiceFinder.find(EntityPartBuilderProvider.class)) {
+            if (entityPartBuilder != null) {
+                return entityPartBuilder;
+            }
+        }
+
+        throw new IllegalArgumentException(LocalizationMessages.NO_ENTITYPART_BUILDER_FOUND());
+    }
 }
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/RuntimeDelegateImpl.java b/core-common/src/main/java/org/glassfish/jersey/internal/RuntimeDelegateImpl.java
index 4895008..58602e2 100644
--- a/core-common/src/main/java/org/glassfish/jersey/internal/RuntimeDelegateImpl.java
+++ b/core-common/src/main/java/org/glassfish/jersey/internal/RuntimeDelegateImpl.java
@@ -72,15 +72,6 @@
         throw new UnsupportedOperationException(LocalizationMessages.NO_CONTAINER_AVAILABLE());
     }
 
-    @Override
-    public EntityPart.Builder createEntityPartBuilder(String partName) throws IllegalArgumentException {
-        final RuntimeDelegate runtimeDelegate = findServerDelegate();
-        if (runtimeDelegate != null) {
-            return runtimeDelegate.createEntityPartBuilder(partName);
-        }
-        throw new UnsupportedOperationException(LocalizationMessages.NO_CONTAINER_AVAILABLE());
-    }
-
     // TODO : Do we need multiple RuntimeDelegates?
     private RuntimeDelegate findServerDelegate() {
         for (RuntimeDelegate delegate : ServiceFinder.find(RuntimeDelegate.class)) {
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java b/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java
index 6a234e5..d1b3e9a 100644
--- a/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java
+++ b/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java
@@ -17,12 +17,16 @@
 
 package org.glassfish.jersey.internal.inject;
 
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
 import java.security.AccessController;
 import java.text.ParseException;
 import java.util.Date;
@@ -256,6 +260,39 @@
     }
 
     /**
+     * Provider of {@link ParamConverter param converter} that convert the supplied string into a Java
+     * {@link InputStream} instance.
+     */
+    public static class InputStreamProvider implements ParamConverterProvider {
+
+        @Override
+        public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations) {
+            return rawType != InputStream.class ? null : new ParamConverter<T>() {
+
+                @Override
+                public T fromString(String value) {
+                    if (value == null) {
+                        throw new IllegalArgumentException(LocalizationMessages.METHOD_PARAMETER_CANNOT_BE_NULL("value"));
+                    }
+                    return rawType.cast(new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8)));
+                }
+
+                @Override
+                public String toString(T value) {
+                    if (value == null) {
+                        throw new IllegalArgumentException(LocalizationMessages.METHOD_PARAMETER_CANNOT_BE_NULL("value"));
+                    }
+                    try {
+                        return new String(((InputStream) value).readAllBytes());
+                    } catch (IOException ioe) {
+                        throw new ExtractorException(ioe);
+                    }
+                }
+            };
+        }
+    }
+
+    /**
      * Provider of {@link ParamConverter param converter} that produce the Optional instance
      * by invoking {@link ParamConverterProvider}.
      */
@@ -414,6 +451,7 @@
                     new TypeFromStringEnum(),
                     new TypeValueOf(),
                     new CharacterProvider(),
+                    new InputStreamProvider(),
                     new TypeFromString(),
                     new StringConstructor(),
                     new OptionalCustomProvider(manager),
diff --git a/core-common/src/main/resources/org/glassfish/jersey/internal/localization.properties b/core-common/src/main/resources/org/glassfish/jersey/internal/localization.properties
index 285b9de..b60bf25 100644
--- a/core-common/src/main/resources/org/glassfish/jersey/internal/localization.properties
+++ b/core-common/src/main/resources/org/glassfish/jersey/internal/localization.properties
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
 # Copyright (c) 2018 Payara Foundation and/or its affiliates.
 #
 # This program and the accompanying materials are made available under the
@@ -116,6 +116,7 @@
 new.cookie.is.null=New cookie is null.
 no.container.available=No container available.
 no.error.processing.in.scope=There is no error processing in scope.
+no.entitypart.builder.found="No EntityPart.Builder implementation found. Is jersey-media-multipart on a classpath?";
 not.supported.on.outbound.message=Method not supported on an outbound message.
 osgi.registry.error.opening.resource.stream=Unable to open an input stream for resource {0}.
 osgi.registry.error.processing.resource.stream=Unexpected error occurred while processing resource stream {0}.
diff --git a/core-common/src/test/java/org/glassfish/jersey/internal/TestRuntimeDelegate.java b/core-common/src/test/java/org/glassfish/jersey/internal/TestRuntimeDelegate.java
index 8d77ffc..de4815e 100644
--- a/core-common/src/test/java/org/glassfish/jersey/internal/TestRuntimeDelegate.java
+++ b/core-common/src/test/java/org/glassfish/jersey/internal/TestRuntimeDelegate.java
@@ -60,11 +60,6 @@
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
-    @Override
-    public EntityPart.Builder createEntityPartBuilder(String partName) throws IllegalArgumentException {
-        throw new UnsupportedOperationException("Not supported yet.");
-    }
-
     public void testMediaType() {
         MediaType m = new MediaType("text", "plain");
         Assert.assertNotNull(m);
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/RuntimeDelegateImpl.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/RuntimeDelegateImpl.java
index ac823d7..07d0f46 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/internal/RuntimeDelegateImpl.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/RuntimeDelegateImpl.java
@@ -111,10 +111,4 @@
             };
         });
     }
-
-    @Override
-    public EntityPart.Builder createEntityPartBuilder(String partName) throws IllegalArgumentException {
-        throw new UnsupportedOperationException("Not supported yet.");
-    }
-
 }
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/inject/FormParamValueParamProvider.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/inject/FormParamValueParamProvider.java
index f1fcbeb..de6fd72 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/internal/inject/FormParamValueParamProvider.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/inject/FormParamValueParamProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,19 +16,25 @@
 
 package org.glassfish.jersey.server.internal.inject;
 
+import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
+import java.security.AccessController;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 
 import jakarta.ws.rs.Encoded;
 import jakarta.ws.rs.FormParam;
 import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.EntityPart;
 import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.GenericType;
 import jakarta.ws.rs.core.HttpHeaders;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.MultivaluedMap;
@@ -36,8 +42,16 @@
 import jakarta.inject.Provider;
 import jakarta.inject.Singleton;
 
+import jakarta.ws.rs.ext.RuntimeDelegate;
+import org.glassfish.jersey.innate.multipart.JerseyEntityPart;
 import org.glassfish.jersey.internal.inject.ExtractorException;
+import org.glassfish.jersey.internal.inject.InjectionManager;
+import org.glassfish.jersey.internal.inject.Providers;
+import org.glassfish.jersey.internal.util.ReflectionHelper;
+import org.glassfish.jersey.internal.util.collection.LazyValue;
 import org.glassfish.jersey.internal.util.collection.NullableMultivaluedHashMap;
+import org.glassfish.jersey.internal.util.collection.Value;
+import org.glassfish.jersey.internal.util.collection.Values;
 import org.glassfish.jersey.message.internal.MediaTypes;
 import org.glassfish.jersey.message.internal.ReaderWriter;
 import org.glassfish.jersey.server.ContainerRequest;
@@ -45,6 +59,7 @@
 import org.glassfish.jersey.server.internal.InternalServerProperties;
 import org.glassfish.jersey.server.internal.LocalizationMessages;
 import org.glassfish.jersey.server.model.Parameter;
+import org.glassfish.jersey.server.spi.internal.ValueParamProvider;
 
 /**
  * Value factory provider supporting the {@link FormParam} injection annotation.
@@ -55,13 +70,17 @@
 @Singleton
 final class FormParamValueParamProvider extends AbstractValueParamProvider {
 
+    private final MultipartFormParamValueProvider multipartProvider;
     /**
      * Injection constructor.
      *
      * @param mpep extractor provider.
+     * @param injectionManager
      */
-    public FormParamValueParamProvider(Provider<MultivaluedParameterExtractorProvider> mpep) {
+    public FormParamValueParamProvider(Provider<MultivaluedParameterExtractorProvider> mpep,
+                                       InjectionManager injectionManager) {
         super(mpep, Parameter.Source.FORM);
+        this.multipartProvider = new MultipartFormParamValueProvider(injectionManager);
     }
 
     @Override
@@ -77,18 +96,24 @@
         if (e == null) {
             return null;
         }
-        return new FormParamValueProvider(e, !parameter.isEncoded());
+        return new FormParamValueProvider(e, multipartProvider, !parameter.isEncoded(), parameter);
     }
 
     private static final class FormParamValueProvider implements Function<ContainerRequest, Object> {
 
         private static final Annotation encodedAnnotation = getEncodedAnnotation();
         private final MultivaluedParameterExtractor<?> extractor;
+        private final MultipartFormParamValueProvider multipartProvider;
         private final boolean decode;
+        private final Parameter parameter;
 
-        FormParamValueProvider(MultivaluedParameterExtractor<?> extractor, boolean decode) {
+        FormParamValueProvider(MultivaluedParameterExtractor<?> extractor,
+                               MultipartFormParamValueProvider multipartProvider,
+                               boolean decode, Parameter parameter) {
             this.extractor = extractor;
+            this.multipartProvider = multipartProvider;
             this.decode = decode;
+            this.parameter = parameter;
         }
 
         private static Form getCachedForm(final ContainerRequest request, boolean decode) {
@@ -121,24 +146,27 @@
 
         @Override
         public Object apply(ContainerRequest request) {
-            Form form = getCachedForm(request, decode);
+            if (MediaTypes.typeEqual(MediaType.MULTIPART_FORM_DATA_TYPE, request.getMediaType())) {
+                return multipartProvider.apply(request, parameter);
+            } else {
+                Form form = getCachedForm(request, decode);
 
-            if (form == null) {
-                Form otherForm = getCachedForm(request, !decode);
-                if (otherForm != null) {
-                    form = switchUrlEncoding(request, otherForm);
-                    cacheForm(request, form);
-                } else {
-                    form = getForm(request);
+                if (form == null) {
+                    Form otherForm = getCachedForm(request, !decode);
+                    if (otherForm != null) {
+                        form = switchUrlEncoding(request, otherForm);
+                    } else {
+                        form = getForm(request);
+                    }
                     cacheForm(request, form);
                 }
-            }
 
-            try {
-                return extractor.extract(form.asMap());
-            } catch (ExtractorException e) {
-                throw new ParamException.FormParamException(e.getCause(),
-                        extractor.getName(), extractor.getDefaultValueString());
+                try {
+                    return extractor.extract(form.asMap());
+                } catch (ExtractorException e) {
+                    throw new ParamException.FormParamException(e.getCause(),
+                            extractor.getName(), extractor.getDefaultValueString());
+                }
             }
         }
 
@@ -199,4 +227,70 @@
             }
         }
     }
+
+    @Singleton
+    private static class MultipartFormParamValueProvider implements BiFunction<ContainerRequest, Parameter, Object> {
+        private static final class FormParamHolder {
+            @FormParam("name")
+            public static final Void dummy = null; // field to get an instance of FormParam annotation
+        }
+        private static Parameter entityPartParameter =
+                Parameter.create(
+                        EntityPart.class, EntityPart.class, false, EntityPart.class, EntityPart.class,
+                        AccessController.doPrivileged(ReflectionHelper.getDeclaredFieldsPA(FormParamHolder.class))[0]
+                            .getAnnotations()
+                );
+
+        private final InjectionManager injectionManager;
+        private final LazyValue<ValueParamProvider> entityPartProvider;
+
+        private MultipartFormParamValueProvider(InjectionManager injectionManager) {
+            this.injectionManager = injectionManager;
+
+            //Get the provider from jersey-media-multipart
+            entityPartProvider = Values.lazy((Value<ValueParamProvider>) () -> {
+                Set<ValueParamProvider> providers = Providers.getProviders(injectionManager, ValueParamProvider.class);
+                for (ValueParamProvider vfp : providers) {
+                    Function<ContainerRequest, ?> paramValueSupplier = vfp.getValueProvider(entityPartParameter);
+                    if (paramValueSupplier != null && !FormParamValueParamProvider.class.isInstance(vfp)) {
+                        return vfp;
+                    }
+                }
+                return null;
+            });
+        }
+
+
+        @Override
+        public Object apply(ContainerRequest containerRequest, Parameter parameter) {
+            Object entity = null;
+            if (entityPartProvider.get() != null) { // else jersey-multipart module is missing
+                final Function<ContainerRequest, ?> valueSupplier = entityPartProvider.get().getValueProvider(
+                        new WrappingFormParamParameter(entityPartParameter, parameter));
+                final JerseyEntityPart entityPart = (JerseyEntityPart) valueSupplier.apply(containerRequest);
+                try {
+                    entity = parameter.getType() != parameter.getRawType()
+                            ? entityPart.getContent(parameter.getRawType(), parameter.getType())
+                            : entityPart.getContent(parameter.getRawType());
+                } catch (IOException e) {
+                    throw new ProcessingException(e);
+                }
+            }
+
+            return entity;
+        }
+
+        private static class WrappingFormParamParameter extends Parameter {
+            protected WrappingFormParamParameter(Parameter entityPartDataParam, Parameter realDataParam) {
+                super(realDataParam.getAnnotations(),
+                        realDataParam.getSourceAnnotation(),
+                        realDataParam.getSource(),
+                        realDataParam.getSourceName(),
+                        entityPartDataParam.getRawType(),
+                        entityPartDataParam.getType(),
+                        realDataParam.isEncoded(),
+                        realDataParam.getDefaultValue());
+            }
+        }
+    }
 }
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/inject/ValueParamProviderConfigurator.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/inject/ValueParamProviderConfigurator.java
index b9ddc55..07337eb 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/internal/inject/ValueParamProviderConfigurator.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/inject/ValueParamProviderConfigurator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2021 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
@@ -87,7 +87,7 @@
         EntityParamValueParamProvider entityProvider = new EntityParamValueParamProvider(paramExtractor);
         suppliers.add(entityProvider);
 
-        FormParamValueParamProvider formProvider = new FormParamValueParamProvider(paramExtractor);
+        FormParamValueParamProvider formProvider = new FormParamValueParamProvider(paramExtractor, injectionManager);
         suppliers.add(formProvider);
 
         HeaderParamValueParamProvider headerProvider = new HeaderParamValueParamProvider(paramExtractor);
diff --git a/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java b/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java
index 5cd8568..8ec946a 100644
--- a/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java
+++ b/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java
@@ -17,6 +17,8 @@
 
 package org.glassfish.jersey.server.internal.inject;
 
+import java.io.IOException;
+import java.io.InputStream;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
@@ -48,6 +50,7 @@
 import org.glassfish.jersey.server.RequestContextBuilder;
 import org.glassfish.jersey.server.ResourceConfig;
 
+import org.junit.Assert;
 import org.junit.Test;
 import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertEquals;
@@ -300,6 +303,24 @@
         }
     }
 
+    @Path("/")
+    public static class InpuStreamConverterTestResource {
+        @GET
+        public String inputStream(@QueryParam("param") InputStream inputStream) throws IOException {
+            return new String(inputStream.readAllBytes());
+        }
+    }
+
+    @Test
+    public void inputStreamTest() throws ExecutionException, InterruptedException {
+        initiateWebApplication(InpuStreamConverterTestResource.class);
+
+        final ContainerResponse responseContext = getResponseContext(UriBuilder.fromPath("/")
+                .queryParam("param", "Hello").build().toString());
+
+        Assert.assertEquals("Hello", responseContext.getEntity());
+    }
+
     public static class MyEagerParamProvider implements ParamConverterProvider {
 
         @Override
diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/BodyPart.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/BodyPart.java
index 0e20e25..7287759 100644
--- a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/BodyPart.java
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/BodyPart.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2021 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
@@ -18,9 +18,11 @@
 
 import java.io.IOException;
 import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
 import java.text.ParseException;
 
 import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.GenericType;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.MultivaluedMap;
 import jakarta.ws.rs.ext.MessageBodyReader;
@@ -263,21 +265,42 @@
      * entity instance is not the unconverted content of the body part entity.
      */
     public <T> T getEntityAs(final Class<T> clazz) {
+        return getEntityAs(clazz, clazz);
+    }
+
+    /**
+     * Returns the entity after appropriate conversion to the requested type. This is useful only when the containing
+     * {@link MultiPart} instance has been received, which causes the {@code providers} property to have been set.
+     *
+     * @param genericEntity desired entity type into which the entity should be converted.
+     * @return entity after appropriate conversion to the requested type.
+     *
+     * @throws ProcessingException if an IO error arises during reading an entity.
+     * @throws IllegalArgumentException if no {@link MessageBodyReader} can be found to perform the requested conversion.
+     * @throws IllegalStateException if this method is called when the {@code providers} property has not been set or when the
+     * entity instance is not the unconverted content of the body part entity.
+     */
+    <T> T getEntityAs(final GenericType<T> genericEntity) {
+        return (T) getEntityAs(genericEntity.getRawType(), genericEntity.getType());
+    }
+
+    <T> T getEntityAs(final Class<T> type, Type genericType) {
         if (entity == null || !(entity instanceof BodyPartEntity)) {
             throw new IllegalStateException(LocalizationMessages.ENTITY_HAS_WRONG_TYPE());
         }
-        if (clazz == BodyPartEntity.class) {
-            return clazz.cast(entity);
+        if (type == BodyPartEntity.class) {
+            return type.cast(entity);
         }
 
         final Annotation[] annotations = new Annotation[0];
-        final MessageBodyReader<T> reader = messageBodyWorkers.getMessageBodyReader(clazz, clazz, annotations, mediaType);
+        final MessageBodyReader<T> reader = messageBodyWorkers.getMessageBodyReader(type, genericType, annotations, mediaType);
         if (reader == null) {
-            throw new IllegalArgumentException(LocalizationMessages.NO_AVAILABLE_MBR(clazz, mediaType));
+            throw new IllegalArgumentException(LocalizationMessages.NO_AVAILABLE_MBR(type, mediaType));
         }
 
         try {
-            return reader.readFrom(clazz, clazz, annotations, mediaType, headers, ((BodyPartEntity) entity).getInputStream());
+            return reader.readFrom(type, genericType, annotations, mediaType, headers,
+                    ((BodyPartEntity) entity).getInputStream());
         } catch (final IOException ioe) {
             throw new ProcessingException(LocalizationMessages.ERROR_READING_ENTITY(String.class), ioe);
         }
diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/FormDataBodyPart.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/FormDataBodyPart.java
index 2380c44..2643b5a 100644
--- a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/FormDataBodyPart.java
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/FormDataBodyPart.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,11 +16,18 @@
 
 package org.glassfish.jersey.media.multipart;
 
+import java.io.InputStream;
+import java.lang.reflect.Type;
 import java.text.ParseException;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.EntityPart;
+import jakarta.ws.rs.core.GenericType;
 import jakarta.ws.rs.core.MediaType;
 
+import org.glassfish.jersey.innate.multipart.JerseyEntityPart;
 import org.glassfish.jersey.media.multipart.internal.LocalizationMessages;
 import org.glassfish.jersey.message.internal.MediaTypes;
 
@@ -59,9 +66,10 @@
  * @author Paul Sandoz
  * @author Michal Gajdos
  */
-public class FormDataBodyPart extends BodyPart {
+public class FormDataBodyPart extends BodyPart implements JerseyEntityPart, EntityPart {
 
     private final boolean fileNameFix;
+    protected final AtomicBoolean contentRead = new AtomicBoolean(false);
 
     /**
      * Instantiates an unnamed new {@link FormDataBodyPart} with a
@@ -231,6 +239,43 @@
         return formDataContentDisposition.getName();
     }
 
+    @Override
+    public Optional<String> getFileName() {
+        return Optional.ofNullable(getFormDataContentDisposition().getFileName());
+    }
+
+    @Override
+    public InputStream getContent() {
+        return getContent(InputStream.class);
+    }
+
+    @Override
+    public <T> T getContent(Class<T> type) {
+        if (contentRead.compareAndExchange(false, true)) {
+            throw new IllegalStateException(LocalizationMessages.CONTENT_HAS_BEEN_ALREADY_READ());
+        }
+        final Object entity = getEntity();
+        return type.isInstance(entity) ? type.cast(entity) : getEntityAs(type);
+    }
+
+    @Override
+    public <T> T getContent(GenericType<T> type) {
+        if (contentRead.compareAndExchange(false, true)) {
+            throw new IllegalStateException(LocalizationMessages.CONTENT_HAS_BEEN_ALREADY_READ());
+        }
+        final Object entity = getEntity();
+        return type.getRawType().isInstance(entity) ? (T) entity : getEntityAs(type);
+    }
+
+    @Override
+    public <T> T getContent(Class<T> type, Type genericType) {
+        if (contentRead.compareAndExchange(false, true)) {
+            throw new IllegalStateException(LocalizationMessages.CONTENT_HAS_BEEN_ALREADY_READ());
+        }
+        final Object entity = getEntity();
+        return type.isInstance(entity) ? (T) entity : getEntityAs(type, genericType);
+    }
+
     /**
      * Sets the control name.
      *
diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/JerseyEntityPartBuilderProvider.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/JerseyEntityPartBuilderProvider.java
new file mode 100644
index 0000000..61583d1
--- /dev/null
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/JerseyEntityPartBuilderProvider.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.media.multipart;
+
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.EntityPart;
+import jakarta.ws.rs.core.GenericEntity;
+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 org.glassfish.jersey.innate.spi.EntityPartBuilderProvider;
+import org.glassfish.jersey.media.multipart.file.FileDataBodyPart;
+import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart;
+import org.glassfish.jersey.media.multipart.internal.LocalizationMessages;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Jersey implementation of {@link EntityPart.Builder}.
+ * @since 3.1.0
+ */
+public class JerseyEntityPartBuilderProvider implements EntityPartBuilderProvider {
+
+    @Override
+    public EntityPart.Builder withName(String partName) {
+        return new EnityPartBuilder().withName(partName);
+    }
+
+    private static class EnityPartBuilder implements EntityPart.Builder {
+
+        private String partName;
+        private String fileName = null;
+        private MultivaluedMap<String, String> headers = new MultivaluedHashMap<>();
+        private MediaType mediaType = null;
+        private MethodData methodData;
+
+        private EntityPart.Builder withName(String partName) {
+            this.partName = partName;
+            return this;
+        }
+
+        @Override
+        public EntityPart.Builder mediaType(MediaType mediaType) throws IllegalArgumentException {
+            this.mediaType = mediaType;
+            return this;
+        }
+
+        @Override
+        public EntityPart.Builder mediaType(String mediaTypeString) throws IllegalArgumentException {
+            this.mediaType = MediaType.valueOf(mediaTypeString);
+            return this;
+        }
+
+        @Override
+        public EntityPart.Builder header(String headerName, String... headerValues) throws IllegalArgumentException {
+            this.headers.addAll(headerName, headerValues);
+            return this;
+        }
+
+        @Override
+        public EntityPart.Builder headers(MultivaluedMap<String, String> newHeaders) throws IllegalArgumentException {
+            for (Map.Entry<String, List<String>> entry : newHeaders.entrySet()) {
+                header(entry.getKey(), entry.getValue().toArray(new String[0]));
+            }
+            return this;
+        }
+
+        @Override
+        public EntityPart.Builder fileName(String fileName) throws IllegalArgumentException {
+            this.fileName = fileName;
+            return this;
+        }
+
+        @Override
+        public EntityPart.Builder content(InputStream content) throws IllegalArgumentException {
+            methodData = new InputStreamMethodData(content);
+            return this;
+        }
+
+        @Override
+        public <T> EntityPart.Builder content(T content, Class<? extends T> type) throws IllegalArgumentException {
+            if (File.class.equals(type)) {
+                methodData = new FileMethodData((File) content);
+            } else if (InputStream.class.equals(type)) {
+                methodData = new InputStreamMethodData((InputStream) content);
+            } else {
+                methodData = new GenericData(content, null);
+            }
+            return this;
+        }
+
+        @Override
+        public <T> EntityPart.Builder content(T content, GenericType<T> type) throws IllegalArgumentException {
+            if (File.class.equals(type.getRawType())) {
+                methodData = new FileMethodData((File) content);
+            } else if (InputStream.class.equals(type.getRawType())) {
+                methodData = new InputStreamMethodData((InputStream) content);
+            } else {
+                methodData = new GenericData(content, type);
+            }
+            return this;
+        }
+
+        @Override
+        public EntityPart build() throws IllegalStateException, IOException, WebApplicationException {
+            if (methodData == null) {
+                throw new IllegalStateException(LocalizationMessages.ENTITY_CONTENT_NOT_SET());
+            }
+            final FormDataBodyPart bodyPart = methodData.build();
+            return bodyPart;
+        }
+
+
+        private abstract class MethodData<T> {
+            protected MethodData(T content) {
+                this.content = content;
+            }
+            protected final T content;
+            protected abstract FormDataBodyPart build();
+            protected void fillFormData(FormDataBodyPart bodyPart) {
+                FormDataContentDisposition contentDisposition =
+                        FormDataContentDisposition.name(partName).fileName(fileName).build();
+                bodyPart.setContentDisposition(contentDisposition);
+                if (mediaType != null) {
+                    bodyPart.setMediaType(mediaType);
+                }
+                for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
+                    bodyPart.getHeaders().addAll(entry.getKey(), entry.getValue().toArray(new String[0]));
+                }
+            }
+        }
+
+        private class InputStreamMethodData extends MethodData<InputStream> {
+            protected InputStreamMethodData(InputStream content) {
+                super(content);
+            }
+
+            @Override
+            protected FormDataBodyPart build() {
+                final StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart();
+                streamDataBodyPart.setFilename(fileName);
+                fillFormData(streamDataBodyPart);
+                streamDataBodyPart.setStreamEntity(content);
+                return streamDataBodyPart;
+            }
+        }
+
+        private class FileMethodData extends MethodData<File> {
+            protected FileMethodData(File content) {
+                super(content);
+            }
+
+            @Override
+            protected FormDataBodyPart build() {
+                final FileDataBodyPart fileDataBodyPart = new FileDataBodyPart();
+                fillFormData(fileDataBodyPart);
+                fileDataBodyPart.setFileEntity(content);
+                return fileDataBodyPart;
+            }
+        }
+
+        private class GenericData extends MethodData<Object> {
+            private final GenericType<?> genericEntity;
+
+            protected GenericData(Object content, GenericType<?> genericEntity) {
+                super(content);
+                this.genericEntity = genericEntity;
+            }
+
+            @Override
+            protected FormDataBodyPart build() {
+                final FormDataBodyPart formDataBodyPart = new FormDataBodyPart();
+                fillFormData(formDataBodyPart);
+                if (genericEntity != null && !GenericEntity.class.isInstance(content)) {
+                    GenericEntity entity = new GenericEntity(content, genericEntity.getType());
+                    formDataBodyPart.setEntity(entity);
+                } else {
+                    formDataBodyPart.setEntity(content);
+                }
+
+                return formDataBodyPart;
+            }
+        }
+    }
+}
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 3224ec5..03bf661 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, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2021 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
@@ -20,6 +20,8 @@
 import jakarta.ws.rs.core.Feature;
 import jakarta.ws.rs.core.FeatureContext;
 
+import org.glassfish.jersey.media.multipart.internal.EntityPartReader;
+import org.glassfish.jersey.media.multipart.internal.EntityPartWriter;
 import org.glassfish.jersey.media.multipart.internal.FormDataParamInjectionFeature;
 import org.glassfish.jersey.media.multipart.internal.MultiPartReaderClientSide;
 import org.glassfish.jersey.media.multipart.internal.MultiPartReaderServerSide;
@@ -45,6 +47,9 @@
 
         context.register(MultiPartWriter.class);
 
+        context.register(EntityPartReader.class);
+        context.register(EntityPartWriter.class);
+
         return true;
     }
 }
diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/MultiPartFeatureAutodiscoverable.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/MultiPartFeatureAutodiscoverable.java
new file mode 100644
index 0000000..f9cfb9d
--- /dev/null
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/MultiPartFeatureAutodiscoverable.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.media.multipart;
+
+import jakarta.ws.rs.core.FeatureContext;
+import org.glassfish.jersey.internal.spi.AutoDiscoverable;
+
+/**
+ * Automatic registration of {@link MultiPartFeature}.
+ * @since 3.1.0
+ */
+public class MultiPartFeatureAutodiscoverable implements AutoDiscoverable {
+    @Override
+    public void configure(FeatureContext context) {
+        context.register(MultiPartFeature.class);
+    }
+}
diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/file/StreamDataBodyPart.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/file/StreamDataBodyPart.java
index 3ddedf2..707bedf 100644
--- a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/file/StreamDataBodyPart.java
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/file/StreamDataBodyPart.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2021 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
@@ -18,6 +18,7 @@
 
 import java.io.InputStream;
 import java.text.MessageFormat;
+import java.util.Optional;
 
 import jakarta.ws.rs.core.MediaType;
 
@@ -275,4 +276,8 @@
         return filename;
     }
 
+    @Override
+    public Optional<String> getFileName() {
+        return Optional.ofNullable(getFilename());
+    }
 }
diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/EntityPartReader.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/EntityPartReader.java
new file mode 100644
index 0000000..864cec4
--- /dev/null
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/EntityPartReader.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2021 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.inject.Singleton;
+import jakarta.ws.rs.Consumes;
+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.ext.MessageBodyReader;
+import jakarta.ws.rs.ext.Providers;
+import org.glassfish.jersey.internal.util.ReflectionHelper;
+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.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Reader supporting List&lt;EntityPart&gt; Make sure {@code GenericEntity} class is used when reading the
+ * {@link EntityPart}s.
+ * @since 3.1.0
+ */
+@Consumes(MediaType.MULTIPART_FORM_DATA)
+@Singleton
+public class EntityPartReader implements MessageBodyReader<List<EntityPart>> {
+
+    private MultiPartReaderClientSide multiPartReaderClientSide;
+
+    @Inject
+    Providers providers;
+
+    @Override
+    public boolean isReadable(Class<?> type, Type generic, Annotation[] annotations, MediaType mediaType) {
+        return List.class.isAssignableFrom(type)
+                && ParameterizedType.class.isInstance(generic)
+                && EntityPart.class.isAssignableFrom(ReflectionHelper.getGenericTypeArgumentClasses(generic).get(0));
+    }
+
+    @Override
+    public List<EntityPart> readFrom(Class<List<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);
+            }
+        }
+
+        return entityParts;
+    }
+}
diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/EntityPartWriter.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/EntityPartWriter.java
new file mode 100644
index 0000000..f5c3472
--- /dev/null
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/EntityPartWriter.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2021 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.inject.Singleton;
+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.ext.MessageBodyWriter;
+import jakarta.ws.rs.ext.Providers;
+import org.glassfish.jersey.internal.util.ReflectionHelper;
+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.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+
+/**
+ * Writer supporting List&lt;EntityPart&gt; Make sure {@code GenericEntity} class is used when sending the
+ * {@link EntityPart}s.
+ * @since 3.1.0
+ */
+@Produces(MediaType.MULTIPART_FORM_DATA)
+@Singleton
+public class EntityPartWriter implements MessageBodyWriter<List<EntityPart>> {
+
+    private MultiPartWriter multiPartWriter;
+
+    @Inject
+    Providers providers;
+
+    @Override
+    public boolean isWriteable(Class<?> type, Type generic, Annotation[] annotations, MediaType mediaType) {
+        return List.class.isAssignableFrom(type)
+                && ParameterizedType.class.isInstance(generic)
+                && EntityPart.class.isAssignableFrom(ReflectionHelper.getGenericTypeArgumentClasses(generic).get(0));
+    }
+
+    @Override
+    public void writeTo(List<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);
+        for (EntityPart ep : entityParts) {
+            multiPart.bodyPart((BodyPart) ep);
+        }
+
+        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/main/java/org/glassfish/jersey/media/multipart/internal/FormDataParamValueParamProvider.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/FormDataParamValueParamProvider.java
index d95b320..6c834d9 100644
--- a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/FormDataParamValueParamProvider.java
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/FormDataParamValueParamProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2021 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
@@ -31,6 +31,8 @@
 import java.util.stream.Collectors;
 
 import jakarta.ws.rs.BadRequestException;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.core.EntityPart;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.MultivaluedMap;
 import jakarta.ws.rs.ext.MessageBodyReader;
@@ -59,7 +61,7 @@
 
 /**
  * Value supplier provider supporting the {@link FormDataParam} injection annotation and entity ({@link FormDataMultiPart})
- * injection.
+ * injection. Also supports {@link FormParam} {@code EntityPart} annotation injection.
  *
  * @author Craig McClanahan
  * @author Paul Sandoz
@@ -311,6 +313,43 @@
         }
     }
 
+    /**
+     * Provider supplier for list of {@link EntityPart} types injected via
+     * {@link jakarta.ws.rs.FormParam} annotation.
+     */
+    private final class ListEntityPartValueProvider extends ValueProvider<List<EntityPart>> {
+
+        private final String name;
+
+        public ListEntityPartValueProvider(final String name) {
+            this.name = name;
+        }
+
+        @Override
+        public List<EntityPart> apply(ContainerRequest request) {
+            return (List<EntityPart>) (List<?>) getEntity(request).getFields(name);
+        }
+    }
+
+    /**
+     * Provider supplier for list of {@link EntityPart} types injected via
+     * {@link jakarta.ws.rs.FormParam} annotation.
+     */
+    private final class EntityPartValueProvider extends ValueProvider<EntityPart> {
+
+        private final String name;
+
+        public EntityPartValueProvider(final String name) {
+            this.name = name;
+        }
+
+        @Override
+        public EntityPart apply(ContainerRequest request) {
+            List<FormDataBodyPart> bodyParts = getEntity(request).getFields(name);
+            return bodyParts.size() != 0 ? bodyParts.get(0) : null;
+        }
+    }
+
     private static final Set<Class<?>> TYPES = initializeTypes();
 
     private static Set<Class<?>> initializeTypes() {
@@ -344,7 +383,7 @@
      * @param extractorProvider multi-valued map parameter extractor provider.
      */
     public FormDataParamValueParamProvider(Provider<MultivaluedParameterExtractorProvider> extractorProvider) {
-        super(extractorProvider, Parameter.Source.ENTITY, Parameter.Source.UNKNOWN);
+        super(extractorProvider, Parameter.Source.ENTITY, Parameter.Source.FORM, Parameter.Source.UNKNOWN);
     }
 
     @Override
@@ -386,6 +425,16 @@
             } else {
                 return new FormDataParamValueProvider(parameter, get(parameter));
             }
+        } else if (FormParam.class.equals(parameter.getSourceAnnotation().annotationType())) {
+            final String paramName = parameter.getSourceName();
+            if (Collection.class == rawType || List.class == rawType) {
+                final Class clazz = ReflectionHelper.getGenericTypeArgumentClasses(parameter.getType()).get(0);
+                if (EntityPart.class.equals(clazz)) {
+                    return new ListEntityPartValueProvider(paramName);
+                }
+            } else if (EntityPart.class.equals(rawType)) {
+                return new EntityPartValueProvider(paramName);
+            }
         }
 
         return null;
diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/MultiPartWriter.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/MultiPartWriter.java
index 4e155f8..011096a 100644
--- a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/MultiPartWriter.java
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/internal/MultiPartWriter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2021 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
@@ -30,6 +30,7 @@
 import jakarta.ws.rs.Produces;
 import jakarta.ws.rs.WebApplicationException;
 import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.GenericEntity;
 import jakarta.ws.rs.core.HttpHeaders;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.MultivaluedMap;
@@ -195,14 +196,21 @@
                 bodyEntity = ((BodyPartEntity) bodyEntity).getInputStream();
             }
 
+            Type bodyType = bodyClass;
+            if (GenericEntity.class.isInstance(bodyEntity)) {
+                bodyClass = ((GenericEntity<?>) bodyEntity).getRawType();
+                bodyType = ((GenericEntity) bodyEntity).getType();
+                bodyEntity = ((GenericEntity<?>) bodyEntity).getEntity();
+            }
+
             final MessageBodyWriter bodyWriter = providers.getMessageBodyWriter(
                     bodyClass,
-                    bodyClass,
+                    bodyType,
                     EMPTY_ANNOTATIONS,
                     bodyMediaType);
 
             if (bodyWriter == null) {
-                throw new IllegalArgumentException(LocalizationMessages.NO_AVAILABLE_MBW(bodyClass, mediaType));
+                throw new IllegalArgumentException(LocalizationMessages.NO_AVAILABLE_MBW(bodyClass, bodyMediaType));
             }
 
             bodyWriter.writeTo(
diff --git a/media/multipart/src/main/resources/META-INF/services/org.glassfish.jersey.innate.spi.EntityPartBuilderProvider b/media/multipart/src/main/resources/META-INF/services/org.glassfish.jersey.innate.spi.EntityPartBuilderProvider
new file mode 100644
index 0000000..32738ac
--- /dev/null
+++ b/media/multipart/src/main/resources/META-INF/services/org.glassfish.jersey.innate.spi.EntityPartBuilderProvider
@@ -0,0 +1 @@
+org.glassfish.jersey.media.multipart.JerseyEntityPartBuilderProvider
\ No newline at end of file
diff --git a/media/multipart/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable b/media/multipart/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable
new file mode 100644
index 0000000..04a6776
--- /dev/null
+++ b/media/multipart/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable
@@ -0,0 +1 @@
+org.glassfish.jersey.media.multipart.MultiPartFeatureAutodiscoverable
\ No newline at end of file
diff --git a/media/multipart/src/main/resources/org/glassfish/jersey/media/multipart/internal/localization.properties b/media/multipart/src/main/resources/org/glassfish/jersey/media/multipart/internal/localization.properties
index 851c026..904214d 100644
--- a/media/multipart/src/main/resources/org/glassfish/jersey/media/multipart/internal/localization.properties
+++ b/media/multipart/src/main/resources/org/glassfish/jersey/media/multipart/internal/localization.properties
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2012, 2021 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
@@ -15,6 +15,8 @@
 #
 
 cannot.inject.file=Cannot provide file for an entity body part.
+content.has.been.already.read=Content method has already been invoked.
+entity.content.not.set=EntityPart content is not set.
 entity.has.wrong.type=Entity instance does not contain the unconverted content.
 error.parsing.content.disposition=Error parsing content disposition: {0}
 error.reading.entity=Error reading entity as {0}.
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
new file mode 100644
index 0000000..91e3368
--- /dev/null
+++ b/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/internal/EntityPartTest.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (c) 2021 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.ws.rs.Consumes;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.EntityPart;
+import jakarta.ws.rs.core.GenericEntity;
+import jakarta.ws.rs.core.GenericType;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.MessageBodyReader;
+import jakarta.ws.rs.ext.MessageBodyWriter;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.internal.util.ReflectionHelper;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.ServerProperties;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+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.nio.charset.StandardCharsets;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class EntityPartTest extends JerseyTest {
+
+    private static final GenericType<List<EntityPart>> LIST_ENTITY_PART_TYPE = new GenericType<List<EntityPart>>(){};
+    private static final GenericType<AtomicReference<String>> ATOMIC_REFERENCE_GENERIC_TYPE = new GenericType<>(){};
+
+    @Path("/")
+    public static class EntityPartTestResource {
+        @GET
+        public Response getResponse() throws IOException {
+            List<EntityPart> list = new LinkedList<>();
+            list.add(EntityPart.withName("part-01").content("TEST1").build());
+            list.add(EntityPart.withName("part-02").content("TEST2").build());
+            GenericEntity<List<EntityPart>> genericEntity = new GenericEntity<>(list) {};
+            return Response.ok(genericEntity, MediaType.MULTIPART_FORM_DATA_TYPE).build();
+        }
+
+        @POST
+        @Path("/postList")
+        public String postEntityPartList(List<EntityPart> list) throws IOException {
+            String entity = list.get(0).getContent(String.class) + list.get(1).getContent(String.class);
+            return entity;
+        }
+
+        @POST
+        @Path("/postForm")
+        public String postEntityPartForm(@FormParam("part-01") EntityPart part1, @FormParam("part-02") EntityPart part2)
+                throws IOException {
+            String entity = part1.getContent(String.class) + part2.getContent(String.class);
+            return entity;
+        }
+
+        @POST
+        @Path("/postListForm")
+        public String postEntityPartForm(@FormParam("part-0x") List<EntityPart> part)
+                throws IOException {
+            String entity = part.get(0).getContent(String.class) + part.get(1).getContent(String.class);
+            return entity;
+        }
+
+        @POST
+        @Path("/postStreams")
+        public Response postEntityStreams(@FormParam("name1") EntityPart part1,
+                                        @FormParam("name2") EntityPart part2,
+                                        @FormParam("name3") EntityPart part3) throws IOException {
+            List<EntityPart> list = new LinkedList<>();
+            list.add(EntityPart.withName(part1.getName()).fileName(part1.getFileName().get()).content(
+                    new ByteArrayInputStream(part1.getContent(String.class).getBytes(StandardCharsets.UTF_8))).build());
+            list.add(EntityPart.withName(part2.getName()).fileName(part2.getFileName().get()).content(
+                    new ByteArrayInputStream(part2.getContent(String.class).getBytes(StandardCharsets.UTF_8))).build());
+            list.add(EntityPart.withName(part3.getName()).fileName(part3.getFileName().get())
+                    .content(part3.getContent(StringHolder.class), StringHolder.class)
+                    .mediaType(part3.getMediaType()).build());
+            GenericEntity<List<EntityPart>> genericEntity = new GenericEntity<>(list) {};
+            return Response.ok(genericEntity, MediaType.MULTIPART_FORM_DATA_TYPE).build();
+        }
+
+        @POST
+        @Path("/postHeaders")
+        public Response postEntityStreams(@FormParam("name1") EntityPart part1) throws IOException {
+            List<EntityPart> list = new LinkedList<>();
+            list.add(EntityPart.withName(part1.getName()).content(part1.getContent(String.class))
+                    .headers(part1.getHeaders()).build());
+            GenericEntity<List<EntityPart>> genericEntity = new GenericEntity<>(list) {};
+            return Response.ok(genericEntity, MediaType.MULTIPART_FORM_DATA_TYPE).build();
+        }
+
+        @POST
+        @Path("/postGeneric")
+        public Response postGeneric(@FormParam("name1") EntityPart part1) throws IOException {
+            List<EntityPart> list = new LinkedList<>();
+            list.add(EntityPart.withName(part1.getName())
+                    .content(part1.getContent(ATOMIC_REFERENCE_GENERIC_TYPE), ATOMIC_REFERENCE_GENERIC_TYPE)
+                    .mediaType(MediaType.TEXT_PLAIN_TYPE)
+                    .build());
+            GenericEntity<List<EntityPart>> genericEntity = new GenericEntity<>(list) {};
+            return Response.ok(genericEntity, MediaType.MULTIPART_FORM_DATA_TYPE).build();
+        }
+
+        @POST
+        @Path("/postFormVarious")
+        public Response postFormVarious(@FormParam("name1") EntityPart part1,
+                                        @FormParam("name2") InputStream part2,
+                                        @FormParam("name3") String part3) throws IOException {
+            List<EntityPart> list = new LinkedList<>();
+            list.add(EntityPart.withName(part1.getName())
+                    .content(part1.getContent(String.class) + new String(part2.readAllBytes()) + part3)
+                    .mediaType(MediaType.TEXT_PLAIN_TYPE)
+                    .build());
+            GenericEntity<List<EntityPart>> genericEntity = new GenericEntity<>(list) {};
+            return Response.ok(genericEntity, MediaType.MULTIPART_FORM_DATA_TYPE).build();
+        }
+
+        @GET
+        @Produces(MediaType.MULTIPART_FORM_DATA)
+        @Path("/getList")
+        public List<EntityPart> getList() throws IOException {
+            List<EntityPart> list = new LinkedList<>();
+            list.add(EntityPart.withName("name1").content("data1").build());
+            return list;
+        }
+    }
+
+    public static class StringHolder extends AtomicReference<String> {
+        StringHolder(String name) {
+            set(name);
+        }
+    }
+
+    @Consumes(MediaType.TEXT_PLAIN)
+    @Produces(MediaType.TEXT_PLAIN)
+    public static class StringHolderWorker implements MessageBodyReader<StringHolder>, MessageBodyWriter<StringHolder> {
+
+        @Override
+        public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+            return type == StringHolder.class;
+        }
+
+        @Override
+        public StringHolder readFrom(Class<StringHolder> type, Type genericType, Annotation[] annotations,
+                                     MediaType mediaType, MultivaluedMap<String, String> httpHeaders,
+                                     InputStream entityStream) throws IOException, WebApplicationException {
+            final StringHolder holder = new StringHolder(new String(entityStream.readAllBytes()));
+            return holder;
+        }
+
+        @Override
+        public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+            return type == StringHolder.class;
+        }
+
+        @Override
+        public void writeTo(StringHolder s, Class<?> type, Type genericType, Annotation[] annotations,
+                            MediaType mediaType, MultivaluedMap<String, Object> httpHeaders,
+                            OutputStream entityStream) throws IOException, WebApplicationException {
+            entityStream.write(s.get().getBytes(StandardCharsets.UTF_8));
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(EntityPartTestResource.class,
+                StringHolderWorker.class, AtomicReferenceProvider.class)
+                .property(ServerProperties.WADL_FEATURE_DISABLE, true);
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.register(StringHolderWorker.class).register(AtomicReferenceProvider.class);
+    }
+
+    @Test
+    public void getEntityPartListTest() throws IOException {
+        try (Response response = target().request().get()) {
+            List<EntityPart> list = response.readEntity(LIST_ENTITY_PART_TYPE);
+            Assert.assertEquals("TEST1", list.get(0).getContent(String.class));
+            Assert.assertEquals("TEST2", list.get(1).getContent(String.class));
+        }
+    }
+
+    @Test
+    public void postEntityPartListTest() throws IOException {
+        List<EntityPart> list = new LinkedList<>();
+        list.add(EntityPart.withName("part-01").content("TEST").build());
+        list.add(EntityPart.withName("part-02").content("1").build());
+        GenericEntity<List<EntityPart>> genericEntity = new GenericEntity<>(list) {};
+        Entity entity = Entity.entity(genericEntity, MediaType.MULTIPART_FORM_DATA_TYPE);
+        try (Response response = target("/postList").request().post(entity)) {
+            Assert.assertEquals("TEST1", response.readEntity(String.class));
+        }
+    }
+
+    @Test
+    public void postEntityPartFormParamTest() throws IOException {
+        List<EntityPart> list = new LinkedList<>();
+        list.add(EntityPart.withName("part-01").content("TEST").build());
+        list.add(EntityPart.withName("part-02").content("1").build());
+        GenericEntity<List<EntityPart>> genericEntity = new GenericEntity<>(list) {};
+        Entity entity = Entity.entity(genericEntity, MediaType.MULTIPART_FORM_DATA_TYPE);
+        try (Response response = target("/postForm").request().post(entity)) {
+            Assert.assertEquals("TEST1", response.readEntity(String.class));
+        }
+    }
+
+    @Test
+    public void postListEntityPartFormParamTest() throws IOException {
+        List<EntityPart> list = new LinkedList<>();
+        list.add(EntityPart.withName("part-0x").content("TEST").build());
+        list.add(EntityPart.withName("part-0x").content("1").build());
+        GenericEntity<List<EntityPart>> genericEntity = new GenericEntity<>(list) {};
+        Entity entity = Entity.entity(genericEntity, MediaType.MULTIPART_FORM_DATA_TYPE);
+        try (Response response = target("/postListForm").request().post(entity)) {
+            Assert.assertEquals("TEST1", response.readEntity(String.class));
+        }
+    }
+
+    @Test
+    public void postEntityPartStreamsTest() throws IOException {
+        List<EntityPart> list = new LinkedList<>();
+        list.add(EntityPart.withName("name1").fileName("file1.doc").content(
+                new ByteArrayInputStream("data1".getBytes(StandardCharsets.UTF_8))).build());
+        list.add(EntityPart.withName("name2").fileName("file2.doc").content(
+                new ByteArrayInputStream("data2".getBytes(StandardCharsets.UTF_8))).build());
+        list.add(EntityPart.withName("name3").fileName("file3.xml")
+                .content(new StringHolder("data3"), StringHolder.class)
+                .mediaType(MediaType.TEXT_PLAIN_TYPE).build());
+        GenericEntity<List<EntityPart>> genericEntity = new GenericEntity<>(list) {
+        };
+        Entity entity = Entity.entity(genericEntity, MediaType.MULTIPART_FORM_DATA_TYPE);
+
+        try (Response response = target("/postStreams").request().post(entity)) {
+            List<EntityPart> result = response.readEntity(LIST_ENTITY_PART_TYPE);
+
+            EntityPart part1 = result.get(0);
+            Assert.assertEquals("name1", part1.getName());
+            Assert.assertEquals("file1.doc", part1.getFileName().get());
+            Assert.assertEquals("data1", part1.getContent(String.class));
+
+            EntityPart part2 = result.get(1);
+            Assert.assertEquals("name2", part2.getName());
+            Assert.assertEquals("file2.doc", part2.getFileName().get());
+            Assert.assertEquals("data2", part2.getContent(String.class));
+
+            EntityPart part3 = result.get(2);
+            Assert.assertEquals("name3", part3.getName());
+            Assert.assertEquals("file3.xml", part3.getFileName().get());
+            Assert.assertEquals("data3", part3.getContent(String.class));
+            Assert.assertEquals(MediaType.TEXT_PLAIN_TYPE, part3.getMediaType());
+        }
+    }
+
+    @Test
+    public void postHeaderTest() throws IOException {
+        List<EntityPart> list = new LinkedList<>();
+        list.add(EntityPart.withName("name1").content("data1")
+                .header("header-01", "value-01").build());
+        GenericEntity<List<EntityPart>> genericEntity = new GenericEntity<>(list) {};
+        Entity entity = Entity.entity(genericEntity, MediaType.MULTIPART_FORM_DATA_TYPE);
+        try (Response response = target("/postHeaders").request().post(entity)) {
+            List<EntityPart> result = response.readEntity(LIST_ENTITY_PART_TYPE);
+            Assert.assertEquals("value-01", result.get(0).getHeaders().getFirst("header-01"));
+            Assert.assertEquals("data1", result.get(0).getContent(String.class));
+        }
+    }
+
+    @Consumes(MediaType.TEXT_PLAIN)
+    @Produces(MediaType.TEXT_PLAIN)
+    public static class AtomicReferenceProvider implements
+            MessageBodyReader<AtomicReference<String>>,
+            MessageBodyWriter<AtomicReference<String>> {
+
+        @Override
+        public boolean isReadable(Class<?> type, Type generic, Annotation[] annotations, MediaType mediaType) {
+            return type == AtomicReference.class
+                    && ParameterizedType.class.isInstance(generic)
+                    && String.class.isAssignableFrom(ReflectionHelper.getGenericTypeArgumentClasses(generic).get(0));
+        }
+
+        @Override
+        public AtomicReference<String> readFrom(Class<AtomicReference<String>> type, Type genericType,
+                                                Annotation[] annotations, MediaType mediaType,
+                                                MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
+                throws IOException, WebApplicationException {
+            return new AtomicReference<String>(new String(entityStream.readAllBytes(), StandardCharsets.UTF_8));
+        }
+
+        @Override
+        public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+            return isReadable(type, genericType, annotations, mediaType);
+        }
+
+        @Override
+        public void writeTo(AtomicReference<String> stringAtomicReference, Class<?> type, Type genericType,
+                            Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders,
+                            OutputStream entityStream) throws IOException, WebApplicationException {
+            entityStream.write(stringAtomicReference.get().getBytes(StandardCharsets.UTF_8));
+        }
+    }
+
+    @Test
+    public void genericEntityTest() throws IOException {
+        List<EntityPart> list = new LinkedList<>();
+        list.add(EntityPart.withName("name1")
+                .content(new AtomicReference<String>("data1"), ATOMIC_REFERENCE_GENERIC_TYPE)
+                .mediaType(MediaType.TEXT_PLAIN_TYPE)
+                .build());
+        GenericEntity<List<EntityPart>> genericEntity = new GenericEntity<>(list) {};
+        Entity entity = Entity.entity(genericEntity, MediaType.MULTIPART_FORM_DATA_TYPE);
+        try (Response response = target("/postGeneric").request().post(entity)) {
+            List<EntityPart> result = response.readEntity(LIST_ENTITY_PART_TYPE);
+            Assert.assertEquals("data1", result.get(0).getContent(String.class));
+        }
+    }
+
+    @Test
+    public void postVariousTest() throws IOException {
+        List<EntityPart> list = new LinkedList<>();
+        list.add(EntityPart.withName("name1").content("Hello ").build());
+        list.add(EntityPart.withName("name2").content("world").build());
+        list.add(EntityPart.withName("name3").content("!").build());
+        GenericEntity<List<EntityPart>> genericEntity = new GenericEntity<>(list) {
+        };
+        Entity entity = Entity.entity(genericEntity, MediaType.MULTIPART_FORM_DATA_TYPE);
+
+        try (Response response = target("/postFormVarious").request().post(entity)) {
+            List<EntityPart> result = response.readEntity(LIST_ENTITY_PART_TYPE);
+            Assert.assertEquals("Hello world!", result.get(0).getContent(String.class));
+        }
+    }
+
+    @Test
+    public void getListTest() throws IOException {
+        try (Response response = target("/getList").request().get()) {
+            List<EntityPart> result = response.readEntity(LIST_ENTITY_PART_TYPE);
+            Assert.assertEquals("data1", result.get(0).getContent(String.class));
+        }
+    }
+}
diff --git a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/TestRuntimeDelegate.java b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/TestRuntimeDelegate.java
index 81c75d4..dd7c562 100644
--- a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/TestRuntimeDelegate.java
+++ b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/TestRuntimeDelegate.java
@@ -61,11 +61,6 @@
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
-    @Override
-    public EntityPart.Builder createEntityPartBuilder(String partName) throws IllegalArgumentException {
-        throw new UnsupportedOperationException("Not supported yet.");
-    }
-
     public void testMediaType() {
         MediaType m = new MediaType("text", "plain");
         Assert.assertNotNull(m);