Add caching and improve performance

Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/GuardianStringKeyMultivaluedMap.java b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/GuardianStringKeyMultivaluedMap.java
new file mode 100644
index 0000000..b2605fe
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/GuardianStringKeyMultivaluedMap.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2022 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.internal.util.collection;
+
+import javax.ws.rs.core.MultivaluedMap;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * The {@link MultivaluedMap} wrapper that is able to set guards observing changes of values represented by a key.
+ * @param <V> The value type of the wrapped {@code MultivaluedMap}.
+ *
+ * @since 2.38
+ */
+public class GuardianStringKeyMultivaluedMap<V> implements MultivaluedMap<String, V> {
+
+    private final MultivaluedMap<String, V> inner;
+    private final Map<String, Boolean> guards = new HashMap<>();
+
+    public GuardianStringKeyMultivaluedMap(MultivaluedMap<String, V> inner) {
+        this.inner = inner;
+    }
+
+    @Override
+    public void putSingle(String key, V value) {
+        observe(key);
+        inner.putSingle(key, value);
+    }
+
+    @Override
+    public void add(String key, V value) {
+        observe(key);
+        inner.add(key, value);
+    }
+
+    @Override
+    public V getFirst(String key) {
+        return inner.getFirst(key);
+    }
+
+    @Override
+    public void addAll(String key, V... newValues) {
+        observe(key);
+        inner.addAll(key, newValues);
+    }
+
+    @Override
+    public void addAll(String key, List<V> valueList) {
+        observe(key);
+        inner.addAll(key, valueList);
+    }
+
+    @Override
+    public void addFirst(String key, V value) {
+        observe(key);
+        inner.addFirst(key, value);
+    }
+
+    @Override
+    public boolean equalsIgnoreValueOrder(MultivaluedMap<String, V> otherMap) {
+        return inner.equalsIgnoreValueOrder(otherMap);
+    }
+
+    @Override
+    public int size() {
+        return inner.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return inner.isEmpty();
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        return inner.containsKey(key);
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+        return inner.containsValue(value);
+    }
+
+    @Override
+    public List<V> get(Object key) {
+        return inner.get(key);
+    }
+
+    @Override
+    public List<V> put(String key, List<V> value) {
+        observe(key);
+        return inner.put(key, value);
+    }
+
+    @Override
+    public List<V> remove(Object key) {
+        if (key != null) {
+            observe(key.toString());
+        }
+        return inner.remove(key);
+    }
+
+    @Override
+    public void putAll(Map<? extends String, ? extends List<V>> m) {
+        for (String key : m.keySet()) {
+            observe(key);
+        }
+        inner.putAll(m);
+    }
+
+    @Override
+    public void clear() {
+        observeAll();
+        inner.clear();
+    }
+
+    @Override
+    public Set<String> keySet() {
+        return inner.keySet();
+    }
+
+    @Override
+    public Collection<List<V>> values() {
+        return inner.values();
+    }
+
+    @Override
+    public Set<Entry<String, List<V>>> entrySet() {
+        return inner.entrySet();
+    }
+
+    /**
+     * Observe changes of a value represented by the key.
+     * @param key the key values to observe
+     */
+    public void setGuard(String key) {
+        guards.put(key, false);
+    }
+
+    /**
+     * Get all the guarded keys
+     * @return a {@link Set} of keys guarded.
+     */
+    public Set<String> getGuards() {
+        return guards.keySet();
+    }
+
+    /**
+     * Return true when the value represented by the key has changed. Resets any observation - the operation is not idempotent.
+     * @param key the Key observed.
+     * @return whether the value represented by the key has changed.
+     */
+    public boolean isObservedAndReset(String key) {
+        Boolean observed = guards.get(key);
+        guards.put(key, false);
+        return observed != null && observed;
+    }
+
+    private void observe(String key) {
+        for (Map.Entry<String, Boolean> guard : guards.entrySet()) {
+            if (guard.getKey().equals(key)) {
+                guard.setValue(true);
+            }
+        }
+    }
+
+    private void observeAll() {
+        for (Map.Entry<String, Boolean> guard : guards.entrySet()) {
+            guard.setValue(true);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return inner.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        GuardianStringKeyMultivaluedMap<?> that = (GuardianStringKeyMultivaluedMap<?>) o;
+        return inner.equals(that.inner) && guards.equals(that.guards);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(inner, guards);
+    }
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/LRU.java b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/LRU.java
new file mode 100644
index 0000000..3338b00
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/LRU.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2022 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.internal.util.collection;
+
+import org.glassfish.jersey.internal.guava.Cache;
+import org.glassfish.jersey.internal.guava.CacheBuilder;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An abstract LRU interface wrapping an actual LRU implementation.
+ * @param <K> Key type
+ * @param <V> Value type
+ * @Since 2.38
+ */
+public abstract class LRU<K, V> {
+
+    /**
+     * Returns the value associated with {@code key} in this cache, or {@code null} if there is no
+     * cached value for {@code key}.
+     */
+    public abstract V getIfPresent(Object key);
+
+    /**
+     * Associates {@code value} with {@code key} in this cache. If the cache previously contained a
+     * value associated with {@code key}, the old value is replaced by {@code value}.
+     */
+    public abstract void put(K key, V value);
+
+    /**
+     * Create new LRU
+     * @return new LRU
+     */
+    public static <K, V> LRU<K, V> create() {
+        return LRUFactory.createLRU();
+    }
+
+    private static class LRUFactory {
+        // TODO configure via the Configuration
+        public static final int LRU_CACHE_SIZE = 128;
+        public static final long TIMEOUT = 5000L;
+        private static <K, V> LRU<K, V> createLRU() {
+            final Cache<K, V> CACHE = CacheBuilder.newBuilder()
+                    .maximumSize(LRU_CACHE_SIZE)
+                    .expireAfterAccess(TIMEOUT, TimeUnit.MILLISECONDS)
+                    .build();
+            return new LRU<K, V>() {
+                @Override
+                public V getIfPresent(Object key) {
+                    return CACHE.getIfPresent(key);
+                }
+
+                @Override
+                public void put(K key, V value) {
+                    CACHE.put(key, value);
+                }
+            };
+        }
+    }
+
+
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java
index 3c6cfdd..5b47d6b 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2022 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
@@ -102,7 +102,7 @@
      *         if the supplied header value is {@code null}.
      */
     @SuppressWarnings("unchecked")
-    private static String asString(final Object headerValue, RuntimeDelegate rd) {
+    public static String asString(final Object headerValue, RuntimeDelegate rd) {
         if (headerValue == null) {
             return null;
         }
@@ -149,7 +149,7 @@
      *                     will be called for before element conversion.
      * @return String view of header values.
      */
-    private static List<String> asStringList(final List<Object> headerValues, final RuntimeDelegate rd) {
+    public static List<String> asStringList(final List<Object> headerValues, final RuntimeDelegate rd) {
         if (headerValues == null || headerValues.isEmpty()) {
             return Collections.emptyList();
         }
@@ -191,7 +191,24 @@
             return null;
         }
 
-        final RuntimeDelegate rd = RuntimeDelegateDecorator.configured(configuration);
+        return asStringHeaders(headers, RuntimeDelegateDecorator.configured(configuration));
+    }
+
+    /**
+     * Returns string view of passed headers. Any modifications to the headers are visible to the view, the view also
+     * supports removal of elements. Does not support other modifications.
+     *
+     * @param headers headers.
+     * @param rd     {@link RuntimeDelegate} instance or {@code null} (in that case {@link RuntimeDelegate#getInstance()}
+     *               will be called for before conversion of elements).
+     * @return String view of headers or {@code null} if {code headers} input parameter is {@code null}.
+     */
+    public static MultivaluedMap<String, String> asStringHeaders(
+            final MultivaluedMap<String, Object> headers, RuntimeDelegate rd) {
+        if (headers == null) {
+            return null;
+        }
+
         return new AbstractMultivaluedMap<String, String>(
                 Views.mapView(headers, input -> HeaderUtils.asStringList(input, rd))
         ) {
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/HttpHeaderReader.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/HttpHeaderReader.java
index dacd743..a2f9eeb 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/HttpHeaderReader.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/HttpHeaderReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2022 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
@@ -23,6 +23,7 @@
 import java.util.Date;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -31,6 +32,7 @@
 import javax.ws.rs.core.Cookie;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.NewCookie;
+import org.glassfish.jersey.internal.util.collection.LRU;
 
 /**
  * An abstract pull-based reader of HTTP headers.
@@ -371,61 +373,25 @@
         return l;
     }
 
-    private static final ListElementCreator<MediaType> MEDIA_TYPE_CREATOR =
-            new ListElementCreator<MediaType>() {
-
-                @Override
-                public MediaType create(HttpHeaderReader reader) throws ParseException {
-                    return MediaTypeProvider.valueOf(reader);
-                }
-            };
-
     /**
      * TODO javadoc.
      */
     public static List<MediaType> readMediaTypes(List<MediaType> l, String header) throws ParseException {
-        return HttpHeaderReader.readList(
-                l,
-                MEDIA_TYPE_CREATOR,
-                header);
+        return MEDIA_TYPE_LIST_READER.readList(l, header);
     }
 
-    private static final ListElementCreator<AcceptableMediaType> ACCEPTABLE_MEDIA_TYPE_CREATOR =
-            new ListElementCreator<AcceptableMediaType>() {
-
-                @Override
-                public AcceptableMediaType create(HttpHeaderReader reader) throws ParseException {
-                    return AcceptableMediaType.valueOf(reader);
-                }
-            };
-
     /**
      * TODO javadoc.
      */
     public static List<AcceptableMediaType> readAcceptMediaType(String header) throws ParseException {
-        return HttpHeaderReader.readQualifiedList(
-                AcceptableMediaType.COMPARATOR,
-                ACCEPTABLE_MEDIA_TYPE_CREATOR,
-                header);
+        return ACCEPTABLE_MEDIA_TYPE_LIST_READER.readList(header);
     }
 
-    private static final ListElementCreator<QualitySourceMediaType> QUALITY_SOURCE_MEDIA_TYPE_CREATOR =
-            new ListElementCreator<QualitySourceMediaType>() {
-
-                @Override
-                public QualitySourceMediaType create(HttpHeaderReader reader) throws ParseException {
-                    return QualitySourceMediaType.valueOf(reader);
-                }
-            };
-
     /**
      * FIXME use somewhere in production code or remove.
      */
     public static List<QualitySourceMediaType> readQualitySourceMediaType(String header) throws ParseException {
-        return HttpHeaderReader.readQualifiedList(
-                QualitySourceMediaType.COMPARATOR,
-                QUALITY_SOURCE_MEDIA_TYPE_CREATOR,
-                header);
+        return QUALITY_SOURCE_MEDIA_TYPE_LIST_READER.readList(header);
     }
 
     /**
@@ -454,121 +420,246 @@
     public static List<AcceptableMediaType> readAcceptMediaType(
             final String header, final List<QualitySourceMediaType> priorityMediaTypes) throws ParseException {
 
-        return HttpHeaderReader.readQualifiedList(
-                new Comparator<AcceptableMediaType>() {
-
-                    @Override
-                    public int compare(AcceptableMediaType o1, AcceptableMediaType o2) {
-                        // FIXME what is going on here?
-                        boolean q_o1_set = false;
-                        int q_o1 = 0;
-                        boolean q_o2_set = false;
-                        int q_o2 = 0;
-                        for (QualitySourceMediaType priorityType : priorityMediaTypes) {
-                            if (!q_o1_set && MediaTypes.typeEqual(o1, priorityType)) {
-                                q_o1 = o1.getQuality() * priorityType.getQuality();
-                                q_o1_set = true;
-                            } else if (!q_o2_set && MediaTypes.typeEqual(o2, priorityType)) {
-                                q_o2 = o2.getQuality() * priorityType.getQuality();
-                                q_o2_set = true;
-                            }
-                        }
-                        int i = q_o2 - q_o1;
-                        if (i != 0) {
-                            return i;
-                        }
-
-                        i = o2.getQuality() - o1.getQuality();
-                        if (i != 0) {
-                            return i;
-                        }
-
-                        return MediaTypes.PARTIAL_ORDER_COMPARATOR.compare(o1, o2);
-                    }
-                },
-                ACCEPTABLE_MEDIA_TYPE_CREATOR,
-                header);
+        return new AcceptMediaTypeListReader(priorityMediaTypes).readList(header);
     }
-    private static final ListElementCreator<AcceptableToken> ACCEPTABLE_TOKEN_CREATOR =
-            new ListElementCreator<AcceptableToken>() {
-
-                @Override
-                public AcceptableToken create(HttpHeaderReader reader) throws ParseException {
-                    return new AcceptableToken(reader);
-                }
-            };
 
     /**
      * TODO javadoc.
      */
     public static List<AcceptableToken> readAcceptToken(String header) throws ParseException {
-        return HttpHeaderReader.readQualifiedList(ACCEPTABLE_TOKEN_CREATOR, header);
+        return ACCEPTABLE_TOKEN_LIST_READER.readList(header);
     }
 
-    private static final ListElementCreator<AcceptableLanguageTag> LANGUAGE_CREATOR =
-            new ListElementCreator<AcceptableLanguageTag>() {
-
-                @Override
-                public AcceptableLanguageTag create(HttpHeaderReader reader) throws ParseException {
-                    return new AcceptableLanguageTag(reader);
-                }
-            };
-
     /**
      * TODO javadoc.
      */
     public static List<AcceptableLanguageTag> readAcceptLanguage(String header) throws ParseException {
-        return HttpHeaderReader.readQualifiedList(LANGUAGE_CREATOR, header);
-    }
-
-    private static <T extends Qualified> List<T> readQualifiedList(ListElementCreator<T> c, String header)
-            throws ParseException {
-
-        List<T> l = readList(c, header);
-        Collections.sort(l, Quality.QUALIFIED_COMPARATOR);
-        return l;
-    }
-
-    private static <T> List<T> readQualifiedList(final Comparator<T> comparator, ListElementCreator<T> c, String header)
-            throws ParseException {
-
-        List<T> l = readList(c, header);
-        Collections.sort(l, comparator);
-        return l;
+        return ACCEPTABLE_LANGUAGE_TAG_LIST_READER.readList(header);
     }
 
     /**
      * TODO javadoc.
      */
     public static List<String> readStringList(String header) throws ParseException {
-        return readList(new ListElementCreator<String>() {
+        return STRING_LIST_READER.readList(header);
+    }
 
+    private static final MediaTypeListReader MEDIA_TYPE_LIST_READER = new MediaTypeListReader();
+    private static final AcceptableMediaTypeListReader ACCEPTABLE_MEDIA_TYPE_LIST_READER = new AcceptableMediaTypeListReader();
+    private static final QualitySourceMediaTypeListReader QUALITY_SOURCE_MEDIA_TYPE_LIST_READER =
+            new QualitySourceMediaTypeListReader();
+    private static final AcceptableTokenListReader ACCEPTABLE_TOKEN_LIST_READER = new AcceptableTokenListReader();
+    private static final AcceptableLanguageTagListReader ACCEPTABLE_LANGUAGE_TAG_LIST_READER =
+            new AcceptableLanguageTagListReader();
+    private static final StringListReader STRING_LIST_READER = new StringListReader();
+
+    private static class MediaTypeListReader extends ListReader<MediaType> {
+        private static final ListElementCreator<MediaType> MEDIA_TYPE_CREATOR =
+                new ListElementCreator<MediaType>() {
+
+                    @Override
+                    public MediaType create(HttpHeaderReader reader) throws ParseException {
+                        return MediaTypeProvider.valueOf(reader);
+                    }
+                };
+
+        List<MediaType> readList(List<MediaType> l, final String header) throws ParseException {
+            return super.readList(l, header);
+        }
+
+        private MediaTypeListReader() {
+            super(MEDIA_TYPE_CREATOR);
+        }
+    }
+
+    private static class AcceptableMediaTypeListReader extends QualifiedListReader<AcceptableMediaType> {
+        private static final ListElementCreator<AcceptableMediaType> ACCEPTABLE_MEDIA_TYPE_CREATOR =
+                new ListElementCreator<AcceptableMediaType>() {
+
+                    @Override
+                    public AcceptableMediaType create(HttpHeaderReader reader) throws ParseException {
+                        return AcceptableMediaType.valueOf(reader);
+                    }
+                };
+        private AcceptableMediaTypeListReader() {
+            super(ACCEPTABLE_MEDIA_TYPE_CREATOR, AcceptableMediaType.COMPARATOR);
+        }
+    }
+    /*
+     * TODO not used in production?
+     */
+    private static class QualitySourceMediaTypeListReader extends QualifiedListReader<QualitySourceMediaType> {
+        private static final ListElementCreator<QualitySourceMediaType> QUALITY_SOURCE_MEDIA_TYPE_CREATOR =
+                new ListElementCreator<QualitySourceMediaType>() {
+
+                    @Override
+                    public QualitySourceMediaType create(HttpHeaderReader reader) throws ParseException {
+                        return QualitySourceMediaType.valueOf(reader);
+                    }
+                };
+        private QualitySourceMediaTypeListReader() {
+            super(QUALITY_SOURCE_MEDIA_TYPE_CREATOR, QualitySourceMediaType.COMPARATOR);
+        }
+    }
+
+    /*
+     * TODO this is used in tests only
+     */
+    private static class AcceptMediaTypeListReader extends QualifiedListReader<AcceptableMediaType> {
+        AcceptMediaTypeListReader(List<QualitySourceMediaType> priorityMediaTypes) {
+            super(ACCEPTABLE_MEDIA_TYPE_CREATOR, new AcceptableMediaTypeComparator(priorityMediaTypes));
+        }
+
+        private static final ListElementCreator<AcceptableMediaType> ACCEPTABLE_MEDIA_TYPE_CREATOR =
+                new ListElementCreator<AcceptableMediaType>() {
+
+                    @Override
+                    public AcceptableMediaType create(HttpHeaderReader reader) throws ParseException {
+                        return AcceptableMediaType.valueOf(reader);
+                    }
+                };
+
+        private static class AcceptableMediaTypeComparator implements Comparator<AcceptableMediaType> {
+            private final List<QualitySourceMediaType> priorityMediaTypes;
+
+            private AcceptableMediaTypeComparator(List<QualitySourceMediaType> priorityMediaTypes) {
+                this.priorityMediaTypes = priorityMediaTypes;
+            }
+
+            @Override
+            public int compare(AcceptableMediaType o1, AcceptableMediaType o2) {
+                // FIXME what is going on here?
+                boolean q_o1_set = false;
+                int q_o1 = 0;
+                boolean q_o2_set = false;
+                int q_o2 = 0;
+                for (QualitySourceMediaType priorityType : priorityMediaTypes) {
+                    if (!q_o1_set && MediaTypes.typeEqual(o1, priorityType)) {
+                        q_o1 = o1.getQuality() * priorityType.getQuality();
+                        q_o1_set = true;
+                    } else if (!q_o2_set && MediaTypes.typeEqual(o2, priorityType)) {
+                        q_o2 = o2.getQuality() * priorityType.getQuality();
+                        q_o2_set = true;
+                    }
+                }
+                int i = q_o2 - q_o1;
+                if (i != 0) {
+                    return i;
+                }
+
+                i = o2.getQuality() - o1.getQuality();
+                if (i != 0) {
+                    return i;
+                }
+
+                return MediaTypes.PARTIAL_ORDER_COMPARATOR.compare(o1, o2);
+            }
+        };
+
+
+    }
+
+    private static class AcceptableTokenListReader extends QualifiedListReader<AcceptableToken> {
+        private static final ListElementCreator<AcceptableToken> ACCEPTABLE_TOKEN_CREATOR =
+                new ListElementCreator<AcceptableToken>() {
+
+                    @Override
+                    public AcceptableToken create(HttpHeaderReader reader) throws ParseException {
+                        return new AcceptableToken(reader);
+                    }
+                };
+        private AcceptableTokenListReader() {
+            super(ACCEPTABLE_TOKEN_CREATOR);
+        }
+    }
+
+    private static class AcceptableLanguageTagListReader extends QualifiedListReader<AcceptableLanguageTag> {
+        private static final ListElementCreator<AcceptableLanguageTag> LANGUAGE_CREATOR =
+                new ListElementCreator<AcceptableLanguageTag>() {
+
+                    @Override
+                    public AcceptableLanguageTag create(HttpHeaderReader reader) throws ParseException {
+                        return new AcceptableLanguageTag(reader);
+                    }
+                };
+        private AcceptableLanguageTagListReader() {
+            super(LANGUAGE_CREATOR);
+        }
+    }
+
+    private abstract static class QualifiedListReader<T extends Qualified> extends ListReader<T> {
+        @Override
+        public List<T> readList(String header) throws ParseException {
+            List<T> l = super.readList(header);
+            Collections.sort(l, comparator);
+            return l;
+        }
+
+        private final Comparator<T> comparator;
+        private QualifiedListReader(ListElementCreator<T> creator) {
+            this(creator, (Comparator<T>) Quality.QUALIFIED_COMPARATOR);
+        }
+
+        protected QualifiedListReader(ListElementCreator<T> creator, Comparator<T> comparator) {
+            super(creator);
+            this.comparator = comparator;
+        }
+    }
+
+    private static class StringListReader extends ListReader<String> {
+        private static final ListElementCreator<String> listElementCreator = new ListElementCreator<String>() {
             @Override
             public String create(HttpHeaderReader reader) throws ParseException {
                 reader.hasNext();
                 return reader.nextToken().toString();
             }
-        }, header);
+        };
+
+        private StringListReader() {
+            super(listElementCreator);
+        }
     }
 
-    private static <T> List<T> readList(final ListElementCreator<T> c, final String header) throws ParseException {
-        return readList(new ArrayList<T>(), c, header);
-    }
+    private abstract static class ListReader<T> {
+        private final LRU<String, List<T>> LIST_CACHE = LRU.create();
+        protected final ListElementCreator<T> creator;
 
-    private static <T> List<T> readList(final List<T> l, final ListElementCreator<T> c, final String header)
-            throws ParseException {
-
-        HttpHeaderReader reader = new HttpHeaderReaderImpl(header);
-        HttpHeaderListAdapter adapter = new HttpHeaderListAdapter(reader);
-
-        while (reader.hasNext()) {
-            l.add(c.create(adapter));
-            adapter.reset();
-            if (reader.hasNext()) {
-                reader.next();
-            }
+        protected ListReader(ListElementCreator<T> creator) {
+            this.creator = creator;
         }
 
-        return l;
+        protected List<T> readList(final String header) throws ParseException {
+            return readList(new ArrayList<T>(), header);
+        }
+
+        private List<T> readList(final List<T> l, final String header)
+                throws ParseException {
+
+//            List<T> list = null;
+            List<T> list = LIST_CACHE.getIfPresent(header);
+
+            if (list == null) {
+                synchronized (LIST_CACHE) {
+                    list = LIST_CACHE.getIfPresent(header);
+                    if (list == null) {
+                        HttpHeaderReader reader = new HttpHeaderReaderImpl(header);
+                        HttpHeaderListAdapter adapter = new HttpHeaderListAdapter(reader);
+                        list = new LinkedList<>();
+
+                        while (reader.hasNext()) {
+                            list.add(creator.create(adapter));
+                            adapter.reset();
+                            if (reader.hasNext()) {
+                                reader.next();
+                            }
+                        }
+                        LIST_CACHE.put(header, list);
+                    }
+                }
+            }
+
+            l.addAll(list);
+            return l;
+        }
     }
 }
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
index 81b9f76..15a00cb 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
@@ -50,11 +50,16 @@
 import javax.ws.rs.core.NewCookie;
 import javax.ws.rs.ext.ReaderInterceptor;
 
+import javax.ws.rs.ext.RuntimeDelegate;
 import javax.xml.transform.Source;
 
 import org.glassfish.jersey.internal.LocalizationMessages;
 import org.glassfish.jersey.internal.PropertiesDelegate;
 import org.glassfish.jersey.internal.RuntimeDelegateDecorator;
+import org.glassfish.jersey.internal.util.collection.GuardianStringKeyMultivaluedMap;
+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.MessageBodyWorkers;
 
 /**
@@ -90,11 +95,14 @@
     private static final List<AcceptableMediaType> WILDCARD_ACCEPTABLE_TYPE_SINGLETON_LIST =
             Collections.singletonList(MediaTypes.WILDCARD_ACCEPTABLE_TYPE);
 
-    private final MultivaluedMap<String, String> headers;
+    private final GuardianStringKeyMultivaluedMap<String> headers;
     private final EntityContent entityContent;
     private final boolean translateNce;
     private MessageBodyWorkers workers;
     private final Configuration configuration;
+    private final RuntimeDelegate runtimeDelegateDecorator;
+    private LazyValue<MediaType> contentTypeCache;
+    private LazyValue<List<AcceptableMediaType>> acceptTypeCache;
 
     /**
      * Input stream and its state. State is represented by the {@link Type Type enum} and
@@ -158,10 +166,16 @@
      *                      as required by JAX-RS specification on the server side.
      */
     public InboundMessageContext(Configuration configuration, boolean translateNce) {
-        this.headers = HeaderUtils.createInbound();
+        this.headers = new GuardianStringKeyMultivaluedMap<>(HeaderUtils.createInbound());
         this.entityContent = new EntityContent();
         this.translateNce = translateNce;
         this.configuration = configuration;
+        runtimeDelegateDecorator = RuntimeDelegateDecorator.configured(configuration);
+
+        contentTypeCache = contentTypeCache();
+        acceptTypeCache = acceptTypeCache();
+        headers.setGuard(HttpHeaders.CONTENT_TYPE);
+        headers.setGuard(HttpHeaders.ACCEPT);
     }
 
     /**
@@ -196,7 +210,7 @@
      * @return updated context.
      */
     public InboundMessageContext header(String name, Object value) {
-        getHeaders().add(name, HeaderUtils.asString(value, configuration));
+        getHeaders().add(name, HeaderUtils.asString(value, runtimeDelegateDecorator));
         return this;
     }
 
@@ -208,7 +222,7 @@
      * @return updated context.
      */
     public InboundMessageContext headers(String name, Object... values) {
-        this.getHeaders().addAll(name, HeaderUtils.asStringList(Arrays.asList(values), configuration));
+        this.getHeaders().addAll(name, HeaderUtils.asStringList(Arrays.asList(values), runtimeDelegateDecorator));
         return this;
     }
 
@@ -265,7 +279,7 @@
         final LinkedList<String> linkedList = new LinkedList<String>();
 
         for (Object element : values) {
-            linkedList.add(HeaderUtils.asString(element, configuration));
+            linkedList.add(HeaderUtils.asString(element, runtimeDelegateDecorator));
         }
 
         return linkedList;
@@ -332,7 +346,7 @@
         }
 
         try {
-            return converter.apply(HeaderUtils.asString(value, configuration));
+            return converter.apply(HeaderUtils.asString(value, runtimeDelegateDecorator));
         } catch (ProcessingException ex) {
             throw exception(name, value, ex);
         }
@@ -447,18 +461,26 @@
      * message entity).
      */
     public MediaType getMediaType() {
-        return singleHeader(HttpHeaders.CONTENT_TYPE, new Function<String, MediaType>() {
-            @Override
-            public MediaType apply(String input) {
-                try {
-                    return RuntimeDelegateDecorator.configured(configuration)
-                            .createHeaderDelegate(MediaType.class)
-                            .fromString(input);
-                } catch (IllegalArgumentException iae) {
-                    throw new ProcessingException(iae);
-                }
-            }
-        }, false);
+        if (headers.isObservedAndReset(HttpHeaders.CONTENT_TYPE) && contentTypeCache.isInitialized()) {
+            contentTypeCache = contentTypeCache(); // headers changed -> drop cache
+        }
+        return contentTypeCache.get();
+    }
+
+    private LazyValue<MediaType> contentTypeCache() {
+        return Values.lazy((Value<MediaType>) () -> singleHeader(
+                HttpHeaders.CONTENT_TYPE, new Function<String, MediaType>() {
+                    @Override
+                    public MediaType apply(String input) {
+                        try {
+                            return runtimeDelegateDecorator
+                                    .createHeaderDelegate(MediaType.class)
+                                    .fromString(input);
+                        } catch (IllegalArgumentException iae) {
+                            throw new ProcessingException(iae);
+                        }
+                    }
+                }, false));
     }
 
     /**
@@ -468,17 +490,26 @@
      * to their q-value, with highest preference first.
      */
     public List<AcceptableMediaType> getQualifiedAcceptableMediaTypes() {
-        final String value = getHeaderString(HttpHeaders.ACCEPT);
-
-        if (value == null || value.isEmpty()) {
-            return WILDCARD_ACCEPTABLE_TYPE_SINGLETON_LIST;
+        if (headers.isObservedAndReset(HttpHeaders.ACCEPT) && acceptTypeCache.isInitialized()) {
+            acceptTypeCache = acceptTypeCache();
         }
+        return acceptTypeCache.get();
+    }
 
-        try {
-            return Collections.unmodifiableList(HttpHeaderReader.readAcceptMediaType(value));
-        } catch (ParseException e) {
-            throw exception(HttpHeaders.ACCEPT, value, e);
-        }
+    private LazyValue<List<AcceptableMediaType>> acceptTypeCache() {
+        return Values.lazy((Value<List<AcceptableMediaType>>) () -> {
+            final String value = getHeaderString(HttpHeaders.ACCEPT);
+
+            if (value == null || value.isEmpty()) {
+                return WILDCARD_ACCEPTABLE_TYPE_SINGLETON_LIST;
+            }
+
+            try {
+                return Collections.unmodifiableList(HttpHeaderReader.readAcceptMediaType(value));
+            } catch (ParseException e) {
+                throw exception(HttpHeaders.ACCEPT, value, e);
+            }
+        });
     }
 
     /**
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
index 64a89d7..2092ad9 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2022 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
@@ -47,11 +47,16 @@
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.NewCookie;
+import javax.ws.rs.ext.RuntimeDelegate;
 
 import org.glassfish.jersey.CommonProperties;
 import org.glassfish.jersey.internal.RuntimeDelegateDecorator;
 import org.glassfish.jersey.internal.LocalizationMessages;
 import org.glassfish.jersey.internal.util.ReflectionHelper;
+import org.glassfish.jersey.internal.util.collection.GuardianStringKeyMultivaluedMap;
+import org.glassfish.jersey.internal.util.collection.LazyValue;
+import org.glassfish.jersey.internal.util.collection.Value;
+import org.glassfish.jersey.internal.util.collection.Values;
 
 /**
  * Base outbound message context implementation.
@@ -63,9 +68,11 @@
     private static final List<MediaType> WILDCARD_ACCEPTABLE_TYPE_SINGLETON_LIST =
             Collections.<MediaType>singletonList(MediaTypes.WILDCARD_ACCEPTABLE_TYPE);
 
-    private final MultivaluedMap<String, Object> headers;
+    private final GuardianStringKeyMultivaluedMap<Object> headers;
     private final CommittingOutputStream committingOutputStream;
     private Configuration configuration;
+    private RuntimeDelegate runtimeDelegateDecorator;
+    private LazyValue<MediaType> mediaTypeCache;
 
     private Object entity;
     private GenericType<?> entityType;
@@ -101,9 +108,13 @@
      */
     public OutboundMessageContext(Configuration configuration) {
         this.configuration = configuration;
-        this.headers = HeaderUtils.createOutbound();
+        this.headers = new GuardianStringKeyMultivaluedMap<>(HeaderUtils.createOutbound());
         this.committingOutputStream = new CommittingOutputStream();
         this.entityStream = committingOutputStream;
+        this.runtimeDelegateDecorator = RuntimeDelegateDecorator.configured(configuration);
+        this.mediaTypeCache = mediaTypeCache();
+
+        headers.setGuard(HttpHeaders.CONTENT_LENGTH);
     }
 
     /**
@@ -113,7 +124,8 @@
      * @param original the original outbound message context.
      */
     public OutboundMessageContext(OutboundMessageContext original) {
-        this.headers = HeaderUtils.createOutbound();
+        this.headers = new GuardianStringKeyMultivaluedMap<>(HeaderUtils.createOutbound());
+        this.headers.setGuard(HttpHeaders.CONTENT_LENGTH);
         this.headers.putAll(original.headers);
         this.committingOutputStream = new CommittingOutputStream();
         this.entityStream = committingOutputStream;
@@ -122,6 +134,8 @@
         this.entityType = original.entityType;
         this.entityAnnotations = original.entityAnnotations;
         this.configuration = original.configuration;
+        this.runtimeDelegateDecorator = original.runtimeDelegateDecorator;
+        this.mediaTypeCache = original.mediaTypeCache();
     }
 
     /**
@@ -153,7 +167,7 @@
      * @return multi-valued map of outbound message header names to their string-converted values.
      */
     public MultivaluedMap<String, String> getStringHeaders() {
-        return HeaderUtils.asStringHeaders(headers, configuration);
+        return HeaderUtils.asStringHeaders(headers, runtimeDelegateDecorator);
     }
 
     /**
@@ -173,7 +187,7 @@
      * character.
      */
     public String getHeaderString(String name) {
-        return HeaderUtils.asHeaderString(headers.get(name), RuntimeDelegateDecorator.configured(configuration));
+        return HeaderUtils.asHeaderString(headers.get(name), runtimeDelegateDecorator);
     }
 
     /**
@@ -209,7 +223,7 @@
             return valueType.cast(value);
         } else {
             try {
-                return converter.apply(HeaderUtils.asString(value, null));
+                return converter.apply(HeaderUtils.asString(value, runtimeDelegateDecorator));
             } catch (ProcessingException ex) {
                 throw exception(name, value, ex);
             }
@@ -267,8 +281,17 @@
      * message entity).
      */
     public MediaType getMediaType() {
-        return singleHeader(HttpHeaders.CONTENT_TYPE, MediaType.class, RuntimeDelegateDecorator.configured(configuration)
-                .createHeaderDelegate(MediaType.class)::fromString, false);
+        if (headers.isObservedAndReset(HttpHeaders.CONTENT_TYPE) && mediaTypeCache.isInitialized()) {
+            mediaTypeCache = mediaTypeCache(); // headers changed -> drop cache
+        }
+        return mediaTypeCache.get();
+    }
+
+    private LazyValue<MediaType> mediaTypeCache() {
+        return Values.lazy((Value<MediaType>) () ->
+                singleHeader(HttpHeaders.CONTENT_TYPE, MediaType.class, RuntimeDelegateDecorator.configured(configuration)
+                    .createHeaderDelegate(MediaType.class)::fromString, false)
+        );
     }
 
     /**
@@ -294,7 +317,7 @@
                     result.add(_value);
                 } else {
                     conversionApplied = true;
-                    result.addAll(HttpHeaderReader.readAcceptMediaType(HeaderUtils.asString(value, configuration)));
+                    result.addAll(HttpHeaderReader.readAcceptMediaType(HeaderUtils.asString(value, runtimeDelegateDecorator)));
                 }
             } catch (java.text.ParseException e) {
                 throw exception(HttpHeaders.ACCEPT, value, e);
@@ -333,7 +356,7 @@
             } else {
                 conversionApplied = true;
                 try {
-                    result.addAll(HttpHeaderReader.readAcceptLanguage(HeaderUtils.asString(value, configuration))
+                    result.addAll(HttpHeaderReader.readAcceptLanguage(HeaderUtils.asString(value, runtimeDelegateDecorator))
                                                   .stream()
                                                   .map(LanguageTag::getAsLocale)
                                                   .collect(Collectors.toList()));
@@ -366,7 +389,7 @@
         }
 
         Map<String, Cookie> result = new HashMap<String, Cookie>();
-        for (String cookie : HeaderUtils.asStringList(cookies, configuration)) {
+        for (String cookie : HeaderUtils.asStringList(cookies, runtimeDelegateDecorator)) {
             if (cookie != null) {
                 result.putAll(HttpHeaderReader.readCookies(cookie));
             }
@@ -454,7 +477,7 @@
         }
 
         Map<String, NewCookie> result = new HashMap<String, NewCookie>();
-        for (String cookie : HeaderUtils.asStringList(cookies, configuration)) {
+        for (String cookie : HeaderUtils.asStringList(cookies, runtimeDelegateDecorator)) {
             if (cookie != null) {
                 NewCookie newCookie = HttpHeaderReader.readNewCookie(cookie);
                 String cookieName = newCookie.getName();
@@ -542,7 +565,7 @@
             } else {
                 conversionApplied = true;
                 try {
-                    result.add(Link.valueOf(HeaderUtils.asString(value, configuration)));
+                    result.add(Link.valueOf(HeaderUtils.asString(value, runtimeDelegateDecorator)));
                 } catch (IllegalArgumentException e) {
                     throw exception(HttpHeaders.LINK, value, e);
                 }
@@ -863,6 +886,7 @@
 
     void setConfiguration(Configuration configuration) {
         this.configuration = configuration;
+        this.runtimeDelegateDecorator = RuntimeDelegateDecorator.configured(configuration);
     }
 
     /**
diff --git a/core-server/src/test/java/org/glassfish/jersey/server/RequestContextBuilder.java b/core-server/src/test/java/org/glassfish/jersey/server/RequestContextBuilder.java
index 9687a61..1647f13 100644
--- a/core-server/src/test/java/org/glassfish/jersey/server/RequestContextBuilder.java
+++ b/core-server/src/test/java/org/glassfish/jersey/server/RequestContextBuilder.java
@@ -28,6 +28,7 @@
 import java.util.logging.Logger;
 
 import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Configuration;
 import javax.ws.rs.core.Cookie;
 import javax.ws.rs.core.GenericEntity;
 import javax.ws.rs.core.GenericType;
@@ -161,7 +162,7 @@
     }
 
     public RequestContextBuilder type(final MediaType contentType) {
-        request.getHeaders().putSingle(HttpHeaders.CONTENT_TYPE, HeaderUtils.asString(contentType, null));
+        request.getHeaders().putSingle(HttpHeaders.CONTENT_TYPE, HeaderUtils.asString(contentType, (Configuration) null));
         return this;
     }
 
@@ -185,7 +186,7 @@
             request.getHeaders().remove(name);
             return;
         }
-        request.header(name, HeaderUtils.asString(value, null));
+        request.header(name, HeaderUtils.asString(value, (Configuration) null));
     }
 
     private void putHeaders(final String name, final Object... values) {
@@ -193,7 +194,7 @@
             request.getHeaders().remove(name);
             return;
         }
-        request.getHeaders().addAll(name, HeaderUtils.asStringList(Arrays.asList(values), null));
+        request.getHeaders().addAll(name, HeaderUtils.asStringList(Arrays.asList(values), (Configuration) null));
     }
 
     private void putHeaders(final String name, final String... values) {
diff --git a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/HeaderUtilsTest.java b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/HeaderUtilsTest.java
index 3ac4782..74318b1 100644
--- a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/HeaderUtilsTest.java
+++ b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/HeaderUtilsTest.java
@@ -25,6 +25,7 @@
 import java.util.List;
 
 import javax.ws.rs.core.AbstractMultivaluedMap;
+import javax.ws.rs.core.Configuration;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.NewCookie;
 import javax.ws.rs.ext.RuntimeDelegate;
@@ -101,19 +102,19 @@
 
     @Test
     public void testAsString() throws Exception {
-        assertNull(HeaderUtils.asString(null, null));
+        assertNull(HeaderUtils.asString(null, (Configuration) null));
 
         final String value = "value";
-        assertSame(value, HeaderUtils.asString(value, null));
+        assertSame(value, HeaderUtils.asString(value, (Configuration) null));
 
         final URI uri = new URI("test");
-        assertEquals(uri.toASCIIString(), HeaderUtils.asString(uri, null));
+        assertEquals(uri.toASCIIString(), HeaderUtils.asString(uri, (Configuration) null));
     }
 
     @Test
     public void testAsStringList() throws Exception {
-        assertNotNull(HeaderUtils.asStringList(null, null));
-        assertTrue(HeaderUtils.asStringList(null, null).isEmpty());
+        assertNotNull(HeaderUtils.asStringList(null, (Configuration) null));
+        assertTrue(HeaderUtils.asStringList(null, (Configuration) null).isEmpty());
 
         final URI uri = new URI("test");
         final List<Object> values = new LinkedList<Object>() {{
@@ -123,7 +124,7 @@
         }};
 
         // test string values
-        final List<String> stringList = HeaderUtils.asStringList(values, null);
+        final List<String> stringList = HeaderUtils.asStringList(values, (Configuration) null);
         assertEquals(Arrays.asList("value", "[null]", uri.toASCIIString()),
                      stringList);
 
@@ -138,7 +139,7 @@
 
     @Test
     public void testAsStringHeaders() throws Exception {
-        assertNull(HeaderUtils.asStringHeaders(null, null));
+        assertNull(HeaderUtils.asStringHeaders(null, (Configuration) null));
 
         final AbstractMultivaluedMap<String, Object> headers = HeaderUtils.createOutbound();
 
@@ -150,7 +151,7 @@
 
         headers.putSingle("k3", "value3");
 
-        final MultivaluedMap<String, String> stringHeaders = HeaderUtils.asStringHeaders(headers, null);
+        final MultivaluedMap<String, String> stringHeaders = HeaderUtils.asStringHeaders(headers, (Configuration) null);
 
         // test string values
         assertEquals(Arrays.asList("value", "value2"),
diff --git a/tests/e2e-entity/src/test/java/org/glassfish/jersey/tests/e2e/header/HeaderDelegateProviderTest.java b/tests/e2e-entity/src/test/java/org/glassfish/jersey/tests/e2e/header/HeaderDelegateProviderTest.java
index 9d118a5..2218d91 100644
--- a/tests/e2e-entity/src/test/java/org/glassfish/jersey/tests/e2e/header/HeaderDelegateProviderTest.java
+++ b/tests/e2e-entity/src/test/java/org/glassfish/jersey/tests/e2e/header/HeaderDelegateProviderTest.java
@@ -16,6 +16,7 @@
 
 package org.glassfish.jersey.tests.e2e.header;
 
+
 import org.glassfish.jersey.CommonProperties;
 import org.glassfish.jersey.client.ClientConfig;
 import org.glassfish.jersey.internal.ServiceFinder;
@@ -36,6 +37,7 @@
 import javax.ws.rs.container.ContainerRequestContext;
 import javax.ws.rs.container.ContainerResponseContext;
 import javax.ws.rs.container.ContainerResponseFilter;
+import javax.ws.rs.core.Configuration;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
@@ -155,7 +157,7 @@
     public void testHeaderDelegateIsUsedWhenRuntimeDelegateDecoratorIsUsed() {
         MultivaluedHashMap headers = new MultivaluedHashMap();
         headers.put(HEADER_NAME, Arrays.asList(new BeanForHeaderDelegateProviderTest()));
-        MultivaluedMap<String, String> converted = HeaderUtils.asStringHeaders(headers, null);
+        MultivaluedMap<String, String> converted = HeaderUtils.asStringHeaders(headers, (Configuration) null);
         testMap(converted, BeanForHeaderDelegateProviderTest.getValue());
 
         Client client = ClientBuilder.newClient().property(CommonProperties.METAINF_SERVICES_LOOKUP_DISABLE, false);
diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ResponseTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ResponseTest.java
index f1f42bc..5730e4b 100644
--- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ResponseTest.java
+++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ResponseTest.java
@@ -24,6 +24,7 @@
 import java.util.Locale;
 import java.util.Set;
 
+import javax.ws.rs.core.Configuration;
 import javax.ws.rs.core.Cookie;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
@@ -247,7 +248,7 @@
         }
 
         MultivaluedMap<String, String> mvp = HeaderUtils.asStringHeaders(
-                resp.getMetadata(), null);
+                resp.getMetadata(), (Configuration) null);
 
         for (String key : mvp.keySet()) {
             sb.append(indent + "Processing Key found in response: ").append(key).append(": ").append(mvp.get(key)).append("; ")
diff --git a/tests/performance/benchmarks/pom.xml b/tests/performance/benchmarks/pom.xml
index b977edc..30ce813 100644
--- a/tests/performance/benchmarks/pom.xml
+++ b/tests/performance/benchmarks/pom.xml
@@ -79,6 +79,10 @@
             <type>pom</type>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.containers</groupId>
+            <artifactId>jersey-container-jdk-http</artifactId>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/AllBenchmarks.java b/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/AllBenchmarks.java
index badc3ee..3f729d0 100644
--- a/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/AllBenchmarks.java
+++ b/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/AllBenchmarks.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2022 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
@@ -36,6 +36,7 @@
                 .include(JacksonBenchmark.class.getSimpleName())
                 .include(LocatorBenchmark.class.getSimpleName())
                 .include(JerseyUriBuilderBenchmark.class.getSimpleName())
+                .include(HeadersServerBenchmark.class.getName())
                 // Measure throughput in seconds (ops/s).
                 .mode(Mode.Throughput)
                 .timeUnit(TimeUnit.SECONDS)
diff --git a/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/HeadersClientBenchmark.java b/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/HeadersClientBenchmark.java
new file mode 100644
index 0000000..fcd137a
--- /dev/null
+++ b/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/HeadersClientBenchmark.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2022 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.tests.performance.benchmark;
+
+import com.sun.net.httpserver.HttpServer;
+import org.glassfish.jersey.CommonProperties;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
+import org.glassfish.jersey.jdkhttp.JdkHttpServerFactory;
+import org.glassfish.jersey.tests.performance.benchmark.headers.HeadersMBRW;
+import org.glassfish.jersey.tests.performance.benchmark.headers.HeadersResource;
+import org.glassfish.jersey.tests.performance.benchmark.headers.HeadersApplication;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
+@Fork(1)
+@Threads(4)
+@State(Scope.Benchmark)
+public class HeadersClientBenchmark {
+
+    static final String BASE_URI = "http://localhost:9009/headers";
+
+    private static final AtomicInteger counter = new AtomicInteger();
+    private static final MediaType MEDIA_PLAIN = MediaType.valueOf(HeadersResource.MEDIA_PLAIN);
+    private static final MediaType MEDIA_JSON = MediaType.valueOf(HeadersResource.MEDIA_JSON);
+
+    private static final boolean INCLUDE_INIT = false;
+
+    private volatile WebTarget webTarget;
+
+    @Setup
+    public void setUp() {
+        if (!INCLUDE_INIT) {
+            webTarget = ClientBuilder.newClient(config()).target(BASE_URI);
+        }
+    }
+
+    private WebTarget webTarget() {
+        return INCLUDE_INIT ? ClientBuilder.newClient(config()).target(BASE_URI) : webTarget;
+    }
+
+    private static class JdkServer {
+        private HttpServer server;
+        void start() {
+            server = JdkHttpServerFactory.createHttpServer(URI.create(BASE_URI), new HeadersApplication(), null, false);
+            server.start();
+            try {
+                TimeUnit.SECONDS.sleep(1L);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+
+        void stop() {
+            server.stop(1);
+        }
+    }
+
+    private static class GrizzlyServer {
+        private org.glassfish.grizzly.http.server.HttpServer httpServer;
+        void start() {
+            httpServer = GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), new HeadersApplication(), null, false);
+            try {
+                httpServer.start();
+                TimeUnit.SECONDS.sleep(1L);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+
+        void stop() {
+            httpServer.shutdownNow();
+        }
+    }
+
+    @Benchmark
+    public void testGetPlainTextClient() {
+        WebTarget target = webTarget().path("headers/getPlain");
+        try (Response r = target.request(MEDIA_PLAIN).get()) {
+            consume(r, HeadersResource.CONTENT_PLAIN, MEDIA_PLAIN);
+        }
+    }
+
+    @Benchmark
+    public void testGetJsonClient() {
+        WebTarget target = webTarget().path("headers/getJson");
+        try (Response r = target.request(MEDIA_JSON).get()) {
+            consume(r, HeadersResource.CONTENT_PLAIN, MEDIA_JSON);
+        }
+    }
+
+    @Benchmark
+    public void testPostPlainTextClient() {
+        WebTarget target = webTarget().path("headers/postPlain");
+        try (Response r = target.request(MEDIA_PLAIN).post(Entity.entity(HeadersResource.CONTENT_PLAIN, MEDIA_PLAIN))) {
+            consume(r, HeadersResource.CONTENT_PLAIN, MEDIA_PLAIN);
+        }
+    }
+
+    @Benchmark
+    public void testPostJsonClient() {
+        WebTarget target = webTarget().path("headers/postJson");
+        try (Response r = target.request(MEDIA_JSON).post(Entity.entity(HeadersResource.CONTENT_PLAIN, MEDIA_JSON))) {
+            consume(r, HeadersResource.CONTENT_PLAIN, MEDIA_JSON);
+        }
+    }
+
+    @Benchmark
+    public void testRandomClient() {
+        switch (counter.incrementAndGet() % 4) {
+            case 0:
+                testGetJsonClient();
+                break;
+            case 1:
+                testGetPlainTextClient();
+                break;
+            case 2:
+                testPostJsonClient();
+                break;
+            case 3:
+                testPostPlainTextClient();
+                break;
+        }
+    }
+
+    private ClientConfig config() {
+        ClientConfig config = new ClientConfig();
+        config.property(CommonProperties.PROVIDER_DEFAULT_DISABLE, "ALL");
+        config.register(HeadersMBRW.class);
+        return config;
+    }
+
+    private void consume(Response response, String expectedContent, MediaType expectedMedia) {
+        if (response.getStatus() != 200) {
+            throw new IllegalStateException("Status:" + response.getStatus());
+        }
+        String content = response.readEntity(String.class);
+        if (!expectedContent.equals(content)) {
+            throw new IllegalStateException("Content:" + content);
+        }
+        if (!expectedMedia.equals(response.getMediaType())) {
+            throw new IllegalStateException("ContentType:" + response.getMediaType());
+        }
+    }
+
+    public static void main(String[] args) throws RunnerException {
+//        JdkServer server = new JdkServer();
+        GrizzlyServer server = new GrizzlyServer();
+        server.start();
+
+        final Options opt = new OptionsBuilder()
+                // Register our benchmarks.
+                .include(HeadersClientBenchmark.class.getSimpleName())
+//               .addProfiler(org.openjdk.jmh.profile.JavaFlightRecorderProfiler.class)
+                .build();
+
+        try {
+            new Runner(opt).run();
+            //new HeadersBenchmark().testGetJsonClient();
+        } finally {
+            server.stop();
+        }
+
+    }
+}
diff --git a/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/HeadersServerBenchmark.java b/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/HeadersServerBenchmark.java
new file mode 100644
index 0000000..2494a15
--- /dev/null
+++ b/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/HeadersServerBenchmark.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2022 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.tests.performance.benchmark;
+
+import org.glassfish.jersey.server.ApplicationHandler;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.test.util.server.ContainerRequestBuilder;
+import org.glassfish.jersey.tests.performance.benchmark.headers.HeadersApplication;
+import org.glassfish.jersey.tests.performance.benchmark.headers.HeadersResource;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+import javax.ws.rs.core.MediaType;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
+@Fork(1)
+@Threads(4)
+@State(Scope.Benchmark)
+public class HeadersServerBenchmark {
+    private static final AtomicInteger counter = new AtomicInteger();
+    private static final MediaType MEDIA_PLAIN = MediaType.valueOf(HeadersResource.MEDIA_PLAIN);
+    private static final MediaType MEDIA_JSON = MediaType.valueOf(HeadersResource.MEDIA_JSON);
+
+    private volatile ApplicationHandler handler;
+
+    @Setup
+    public void start() throws Exception {
+        handler = new ApplicationHandler(new HeadersApplication());
+    }
+
+    @TearDown
+    public void shutdown() {
+        if (counter.get() != 0) {
+            System.out.append("Executed ").append(String.valueOf(counter.get())).append(" requests");
+        }
+    }
+
+    @Benchmark
+    public void testGetPlainText() throws ExecutionException, InterruptedException {
+        ContainerRequest request = ContainerRequestBuilder
+                .from("headers/getPlain", "GET", handler.getConfiguration())
+                .accept(MEDIA_PLAIN)
+                .build();
+
+        ContainerResponse response = handler.apply(request).get();
+        consume(response, HeadersResource.CONTENT_PLAIN, MEDIA_PLAIN);
+    }
+
+    @Benchmark
+    public void testGetJson() throws ExecutionException, InterruptedException {
+        ContainerRequest request = ContainerRequestBuilder
+                .from("headers/getJson", "GET", handler.getConfiguration())
+                .accept(MEDIA_JSON)
+                .build();
+
+        ContainerResponse response = handler.apply(request).get();
+        consume(response, HeadersResource.CONTENT_PLAIN, MEDIA_JSON);
+    }
+
+    @Benchmark
+    public void testPostPlainText() throws ExecutionException, InterruptedException {
+        ContainerRequest request = ContainerRequestBuilder
+                .from("headers/postPlain", "POST", handler.getConfiguration())
+                .accept(MEDIA_PLAIN)
+                .type(MEDIA_PLAIN)
+                .entity(HeadersResource.CONTENT_PLAIN, handler)
+                .build();
+
+        ContainerResponse response = handler.apply(request).get();
+        consume(response, HeadersResource.CONTENT_PLAIN, MEDIA_PLAIN);
+    }
+
+    @Benchmark
+    public void testPostJson() throws ExecutionException, InterruptedException {
+        ContainerRequest request = ContainerRequestBuilder
+                .from("headers/postJson", "POST", handler.getConfiguration())
+                .accept(MEDIA_JSON)
+                .type(MEDIA_JSON)
+                .entity(HeadersResource.CONTENT_PLAIN, handler)
+                .build();
+
+        ContainerResponse response = handler.apply(request).get();
+        consume(response, HeadersResource.CONTENT_PLAIN, MEDIA_JSON);
+    }
+
+    @Benchmark
+    public void testRandomClient() throws ExecutionException, InterruptedException {
+        switch (counter.incrementAndGet() % 4) {
+            case 0:
+                testGetJson();
+                break;
+            case 1:
+                testGetPlainText();
+                break;
+            case 2:
+                testPostJson();
+                break;
+            case 3:
+                testPostPlainText();
+                break;
+        }
+    }
+
+    private void consume(ContainerResponse response, String expectedContent, MediaType expectedMedia) {
+        if (response.getStatus() != 200) {
+            throw new IllegalStateException("Status:" + response.getStatus());
+        }
+        String content = response.getEntity().toString();
+        if (!expectedContent.equals(content)) {
+            throw new IllegalStateException("Content:" + content);
+        }
+        if (!expectedMedia.equals(response.getMediaType())) {
+            throw new IllegalStateException("ContentType:" + response.getMediaType());
+        }
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        final Options opt = new OptionsBuilder()
+                // Register our benchmarks.
+                .include(HeadersServerBenchmark.class.getSimpleName())
+//                .addProfiler(org.openjdk.jmh.profile.JavaFlightRecorderProfiler.class)
+                .build();
+
+        new Runner(opt).run();
+
+// DEBUG:
+
+//        try {
+//            HeadersServerBenchmark benchmark = new HeadersServerBenchmark();
+//            benchmark.start();
+//            for (int i = 0; i != 5; i++) {
+//                benchmark.testPostPlainText();
+//            }
+//        } catch (Exception e) {
+//            e.printStackTrace();
+//        }
+    }
+
+}
diff --git a/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/headers/HeadersApplication.java b/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/headers/HeadersApplication.java
new file mode 100644
index 0000000..6ec127d
--- /dev/null
+++ b/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/headers/HeadersApplication.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 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.tests.performance.benchmark.headers;
+
+import org.glassfish.jersey.CommonProperties;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.ServerProperties;
+
+
+public class HeadersApplication extends ResourceConfig {
+    public HeadersApplication() {
+        register(HeadersResource.class).register(HeadersMBRW.class);
+
+        // Turn off Monitoring to not affect benchmarks.
+        property(ServerProperties.MONITORING_ENABLED, false);
+        property(ServerProperties.MONITORING_STATISTICS_ENABLED, false);
+        property(ServerProperties.MONITORING_STATISTICS_MBEANS_ENABLED, false);
+
+        property(ServerProperties.WADL_FEATURE_DISABLE, true);
+        property(CommonProperties.PROVIDER_DEFAULT_DISABLE, "ALL");
+    }
+}
diff --git a/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/headers/HeadersMBRW.java b/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/headers/HeadersMBRW.java
new file mode 100644
index 0000000..ea34696
--- /dev/null
+++ b/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/headers/HeadersMBRW.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2022 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.tests.performance.benchmark.headers;
+
+import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJsonProvider;
+import org.glassfish.jersey.message.internal.ReaderWriter;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.MessageBodyWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+public class HeadersMBRW implements MessageBodyReader<String>, MessageBodyWriter<String> {
+
+    private static final JacksonJsonProvider jackson = new JacksonJsonProvider();
+
+    @Override
+    public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+        return (type == String.class && HeadersResource.MEDIA_PLAIN.equals(mediaType.toString()))
+                || (type == String.class && HeadersResource.MEDIA_JSON.equals(mediaType.toString()));
+    }
+
+    @Override
+    public String readFrom(Class<String> type, Type genericType, Annotation[] annotations, MediaType mediaType,
+                           MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
+            throws IOException, WebApplicationException {
+        switch (mediaType.toString()) {
+            case HeadersResource.MEDIA_PLAIN:
+                return ReaderWriter.readFromAsString(entityStream, MediaType.TEXT_PLAIN_TYPE);
+            case HeadersResource.MEDIA_JSON:
+                return jackson.readFrom((Class<Object>) (Class) type, genericType, annotations, mediaType,
+                        httpHeaders, entityStream).toString();
+        }
+        return null;
+    }
+
+    @Override
+    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+        return isReadable(type, genericType, annotations, mediaType);
+    }
+
+    @Override
+    public void writeTo(String s, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
+                        MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
+            throws IOException, WebApplicationException {
+        switch (mediaType.toString()) {
+            case HeadersResource.MEDIA_PLAIN:
+                ReaderWriter.writeToAsString(s, entityStream, MediaType.TEXT_PLAIN_TYPE);
+                break;
+            case HeadersResource.MEDIA_JSON:
+                jackson.writeTo(s, type, genericType, annotations, MediaType.APPLICATION_JSON_TYPE, httpHeaders, entityStream);
+                break;
+        }
+    }
+}
diff --git a/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/headers/HeadersResource.java b/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/headers/HeadersResource.java
new file mode 100644
index 0000000..86558f0
--- /dev/null
+++ b/tests/performance/benchmarks/src/main/java/org/glassfish/jersey/tests/performance/benchmark/headers/HeadersResource.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2022 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.tests.performance.benchmark.headers;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+
+@Path("/headers")
+public class HeadersResource {
+    public static final String MEDIA_PLAIN = "myapplication/someplaintext+someothertoolongtype";
+    public static final String MEDIA_JSON = "myapplication/somejson+someradiculouslylongtype";
+
+    public static final String CONTENT_PLAIN = "some plain text content does not matter";
+    public static final String CONTENT_JSON = "\"" + CONTENT_PLAIN + "\"";
+
+    @GET
+    @Produces(MEDIA_PLAIN)
+    @Path("getPlain")
+    public String getMediaPlain() {
+        return CONTENT_PLAIN;
+    }
+
+    @POST
+    @Produces(MEDIA_PLAIN)
+    @Consumes(MEDIA_PLAIN)
+    @Path("postPlain")
+    public String postMediaPlain(String content) {
+        if (!CONTENT_PLAIN.equals(content)) {
+            throw new WebApplicationException(Response.Status.EXPECTATION_FAILED);
+        }
+        return CONTENT_PLAIN;
+    }
+
+    @GET
+    @Produces(MEDIA_JSON)
+    @Path("getJson")
+    public Response getJson() {
+        return Response.ok(CONTENT_PLAIN, MEDIA_JSON).build();
+    }
+
+    @POST
+    @Produces(MEDIA_JSON)
+    @Consumes(MEDIA_JSON)
+    @Path("postJson")
+    public Response postJson(String json) {
+        if (!CONTENT_PLAIN.equals(json)) {
+            throw new WebApplicationException(Response.Status.EXPECTATION_FAILED);
+        }
+        return Response.ok(CONTENT_PLAIN, MEDIA_JSON).build();
+    }
+
+}