Put duplicated methods into a common superclass

Signed-off-by: jansupol <jan.supol@oracle.com>
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 15a00cb..a9c6dc1 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2024 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
@@ -67,7 +67,7 @@
  *
  * @author Marek Potociar
  */
-public abstract class InboundMessageContext {
+public abstract class InboundMessageContext extends MessageHeaderMethods {
 
     private static final InputStream EMPTY = new InputStream() {
 
@@ -100,7 +100,6 @@
     private final boolean translateNce;
     private MessageBodyWorkers workers;
     private final Configuration configuration;
-    private final RuntimeDelegate runtimeDelegateDecorator;
     private LazyValue<MediaType> contentTypeCache;
     private LazyValue<List<AcceptableMediaType>> acceptTypeCache;
 
@@ -166,11 +165,11 @@
      *                      as required by JAX-RS specification on the server side.
      */
     public InboundMessageContext(Configuration configuration, boolean translateNce) {
+        super(configuration);
         this.headers = new GuardianStringKeyMultivaluedMap<>(HeaderUtils.createInbound());
         this.entityContent = new EntityContent();
         this.translateNce = translateNce;
         this.configuration = configuration;
-        runtimeDelegateDecorator = RuntimeDelegateDecorator.configured(configuration);
 
         contentTypeCache = contentTypeCache();
         acceptTypeCache = acceptTypeCache();
@@ -319,42 +318,9 @@
         return buffer.toString();
     }
 
-    /**
-     * Get a single typed header value.
-     *
-     * @param name        header name.
-     * @param converter   from string conversion function. Is expected to throw {@link ProcessingException}
-     *                    if conversion fails.
-     * @param convertNull if {@code true} this method calls the provided converter even for {@code null}. Otherwise this
-     *                    method returns the {@code null} without calling the converter.
-     * @return value of the header, or (possibly converted) {@code null} if not present.
-     */
-    private <T> T singleHeader(String name, Function<String, T> converter, boolean convertNull) {
-        final List<String> values = this.headers.get(name);
-
-        if (values == null || values.isEmpty()) {
-            return convertNull ? converter.apply(null) : null;
-        }
-        if (values.size() > 1) {
-            throw new HeaderValueException(LocalizationMessages.TOO_MANY_HEADER_VALUES(name, values.toString()),
-                    HeaderValueException.Context.INBOUND);
-        }
-
-        Object value = values.get(0);
-        if (value == null) {
-            return convertNull ? converter.apply(null) : null;
-        }
-
-        try {
-            return converter.apply(HeaderUtils.asString(value, runtimeDelegateDecorator));
-        } catch (ProcessingException ex) {
-            throw exception(name, value, ex);
-        }
-    }
-
-    private static HeaderValueException exception(final String headerName, Object headerValue, Exception e) {
-        return new HeaderValueException(LocalizationMessages.UNABLE_TO_PARSE_HEADER_VALUE(headerName, headerValue), e,
-                HeaderValueException.Context.INBOUND);
+    @Override
+    public HeaderValueException.Context getHeaderValueExceptionContext() {
+        return HeaderValueException.Context.INBOUND;
     }
 
     /**
@@ -367,24 +333,6 @@
     }
 
     /**
-     * Get message date.
-     *
-     * @return the message date, otherwise {@code null} if not present.
-     */
-    public Date getDate() {
-        return singleHeader(HttpHeaders.DATE, new Function<String, Date>() {
-            @Override
-            public Date apply(String input) {
-                try {
-                    return HttpHeaderReader.readDate(input);
-                } catch (ParseException ex) {
-                    throw new ProcessingException(ex);
-                }
-            }
-        }, false);
-    }
-
-    /**
      * Get If-Match header.
      *
      * @return the If-Match header value, otherwise {@code null} if not present.
@@ -419,42 +367,6 @@
     }
 
     /**
-     * Get the language of the entity.
-     *
-     * @return the language of the entity or {@code null} if not specified.
-     */
-    public Locale getLanguage() {
-        return singleHeader(HttpHeaders.CONTENT_LANGUAGE, new Function<String, Locale>() {
-            @Override
-            public Locale apply(String input) {
-                try {
-                    return new LanguageTag(input).getAsLocale();
-                } catch (ParseException e) {
-                    throw new ProcessingException(e);
-                }
-            }
-        }, false);
-    }
-
-    /**
-     * Get Content-Length value.
-     *
-     * @return Content-Length as integer if present and valid number. In other cases returns -1.
-     */
-    public int getLength() {
-        return singleHeader(HttpHeaders.CONTENT_LENGTH, new Function<String, Integer>() {
-            @Override
-            public Integer apply(String input) {
-                try {
-                    return (input != null && !input.isEmpty()) ? Integer.parseInt(input) : -1;
-                } catch (NumberFormatException ex) {
-                    throw new ProcessingException(ex);
-                }
-            }
-        }, true);
-    }
-
-    /**
      * Get the media type of the entity.
      *
      * @return the media type or {@code null} if not specified (e.g. there's no
@@ -569,120 +481,6 @@
     }
 
     /**
-     * Get any cookies that accompanied the request.
-     *
-     * @return a read-only map of cookie name (String) to {@link javax.ws.rs.core.Cookie}.
-     */
-    public Map<String, Cookie> getRequestCookies() {
-        List<String> cookies = this.headers.get(HttpHeaders.COOKIE);
-        if (cookies == null || cookies.isEmpty()) {
-            return Collections.emptyMap();
-        }
-
-        Map<String, Cookie> result = new HashMap<String, Cookie>();
-        for (String cookie : cookies) {
-            if (cookie != null) {
-                result.putAll(HttpHeaderReader.readCookies(cookie));
-            }
-        }
-        return result;
-    }
-
-    /**
-     * Get the allowed HTTP methods from the Allow HTTP header.
-     *
-     * @return the allowed HTTP methods, all methods will returned as upper case
-     * strings.
-     */
-    public Set<String> getAllowedMethods() {
-        final String allowed = getHeaderString(HttpHeaders.ALLOW);
-        if (allowed == null || allowed.isEmpty()) {
-            return Collections.emptySet();
-        }
-        try {
-            return new HashSet<String>(HttpHeaderReader.readStringList(allowed.toUpperCase(Locale.ROOT)));
-        } catch (java.text.ParseException e) {
-            throw exception(HttpHeaders.ALLOW, allowed, e);
-        }
-    }
-
-    /**
-     * Get any new cookies set on the response message.
-     *
-     * @return a read-only map of cookie name (String) to a {@link javax.ws.rs.core.NewCookie new cookie}.
-     */
-    public Map<String, NewCookie> getResponseCookies() {
-        List<String> cookies = this.headers.get(HttpHeaders.SET_COOKIE);
-        if (cookies == null || cookies.isEmpty()) {
-            return Collections.emptyMap();
-        }
-
-        Map<String, NewCookie> result = new HashMap<String, NewCookie>();
-        for (String cookie : cookies) {
-            if (cookie != null) {
-                NewCookie newCookie = HttpHeaderReader.readNewCookie(cookie);
-                String cookieName = newCookie.getName();
-                if (result.containsKey(cookieName)) {
-                    result.put(cookieName, HeaderUtils.getPreferredCookie(result.get(cookieName), newCookie));
-                } else {
-                    result.put(cookieName, newCookie);
-                }
-            }
-        }
-        return result;
-    }
-
-    /**
-     * Get the entity tag.
-     *
-     * @return the entity tag, otherwise {@code null} if not present.
-     */
-    public EntityTag getEntityTag() {
-        return singleHeader(HttpHeaders.ETAG, new Function<String, EntityTag>() {
-            @Override
-            public EntityTag apply(String value) {
-                return EntityTag.valueOf(value);
-            }
-        }, false);
-    }
-
-    /**
-     * Get the last modified date.
-     *
-     * @return the last modified date, otherwise {@code null} if not present.
-     */
-    public Date getLastModified() {
-        return singleHeader(HttpHeaders.LAST_MODIFIED, new Function<String, Date>() {
-            @Override
-            public Date apply(String input) {
-                try {
-                    return HttpHeaderReader.readDate(input);
-                } catch (ParseException e) {
-                    throw new ProcessingException(e);
-                }
-            }
-        }, false);
-    }
-
-    /**
-     * Get the location.
-     *
-     * @return the location URI, otherwise {@code null} if not present.
-     */
-    public URI getLocation() {
-        return singleHeader(HttpHeaders.LOCATION, new Function<String, URI>() {
-            @Override
-            public URI apply(String value) {
-                try {
-                    return URI.create(value);
-                } catch (IllegalArgumentException ex) {
-                    throw new ProcessingException(ex);
-                }
-            }
-        }, false);
-    }
-
-    /**
      * Get the links attached to the message as header.
      *
      * @return links, may return empty {@link java.util.Set} if no links are present. Never
@@ -726,57 +524,6 @@
         }
     }
 
-    /**
-     * Check if link for relation exists.
-     *
-     * @param relation link relation.
-     * @return {@code true} if the for the relation link exists, {@code false}
-     * otherwise.
-     */
-    public boolean hasLink(String relation) {
-        for (Link link : getLinks()) {
-            List<String> relations = LinkProvider.getLinkRelations(link.getRel());
-
-            if (relations != null && relations.contains(relation)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Get the link for the relation.
-     *
-     * @param relation link relation.
-     * @return the link for the relation, otherwise {@code null} if not present.
-     */
-    public Link getLink(String relation) {
-        for (Link link : getLinks()) {
-            List<String> relations = LinkProvider.getLinkRelations(link.getRel());
-            if (relations != null && relations.contains(relation)) {
-                return link;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Convenience method that returns a {@link javax.ws.rs.core.Link.Builder Link.Builder}
-     * for the relation.
-     *
-     * @param relation link relation.
-     * @return the link builder for the relation, otherwise {@code null} if not
-     * present.
-     */
-    public Link.Builder getLinkBuilder(String relation) {
-        Link link = getLink(relation);
-        if (link == null) {
-            return null;
-        }
-
-        return Link.fromLink(link);
-    }
-
     // Message entity
 
     /**
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageHeaderMethods.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageHeaderMethods.java
new file mode 100644
index 0000000..d2d69b4
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageHeaderMethods.java
@@ -0,0 +1,461 @@
+/*
+ * Copyright (c) 2024 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.message.internal;
+
+import org.glassfish.jersey.internal.LocalizationMessages;
+import org.glassfish.jersey.internal.RuntimeDelegateDecorator;
+
+import javax.ws.rs.ProcessingException;
+import javax.ws.rs.core.Configuration;
+import javax.ws.rs.core.Cookie;
+import javax.ws.rs.core.EntityTag;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Link;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.NewCookie;
+import javax.ws.rs.ext.RuntimeDelegate;
+import java.net.URI;
+import java.text.ParseException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * Common header methods for outbound and inbound messages.
+ */
+public abstract class MessageHeaderMethods {
+    protected RuntimeDelegate runtimeDelegateDecorator;
+
+    protected MessageHeaderMethods(Configuration configuration) {
+        this.runtimeDelegateDecorator = RuntimeDelegateDecorator.configured(configuration);
+    }
+
+    protected MessageHeaderMethods(MessageHeaderMethods other) {
+        this.runtimeDelegateDecorator = other.runtimeDelegateDecorator;
+    }
+
+    /**
+     * Get a message header as a single string value.
+     *
+     * Each single non-string header value is converted to String using a {@code RuntimeDelegate.HeaderDelegate} if one
+     * is available via {@code RuntimeDelegate#createHeaderDelegate(java.lang.Class)} for the header value
+     * class or using its {@code toString} method if a header delegate is not available.
+     *
+     * @param name the message header.
+     * @return the message header value. If the message header is not present then {@code null} is returned. If the message
+     * header is present but has no value then the empty string is returned. If the message header is present more than once
+     * then the values of joined together and separated by a ',' character.
+     */
+    public abstract String getHeaderString(String name);
+
+    /**
+     * Get the mutable message headers multivalued map.
+     *
+     * @return mutable multivalued map of message headers.
+     */
+    public abstract MultivaluedMap<String, ?> getHeaders();
+
+    /**
+     * Return {@link HeaderValueException.Context} type of the message context.
+     * @return {@link HeaderValueException.Context} type of the message context.
+     */
+    protected abstract HeaderValueException.Context getHeaderValueExceptionContext();
+
+    /**
+     * Get the links attached to the message as header.
+     *
+     * @return links, may return empty {@link java.util.Set} if no links are present. Never
+     * returns {@code null}.
+     */
+    public abstract Set<Link> getLinks();
+
+    /**
+     * Checks whether a header with a specific name and value (or item of the token-separated value list) exists.
+     *
+     * Each single non-string header value is converted to String using a {@code RuntimeDelegate.HeaderDelegate} if one
+     * is available via {@code RuntimeDelegate#createHeaderDelegate(java.lang.Class)} for the header value
+     * class or using its {@code toString} method if a header delegate is not available.
+     *
+     * <p>
+     * For example: {@code containsHeaderString("cache-control", ",", "no-store"::equalsIgnoreCase)} will return {@code true} if
+     * a {@code Cache-Control} header exists that has the value {@code no-store}, the value {@code No-Store} or the value
+     * {@code Max-Age, NO-STORE, no-transform}, but {@code false} when it has the value {@code no-store;no-transform}
+     * (missing comma), or the value {@code no - store} (whitespace within value).
+     *
+     * @param name the message header.
+     * @param valueSeparatorRegex Separates the header value into single values. {@code null} does not split.
+     * @param valuePredicate value must fulfil this predicate.
+     * @return {@code true} if and only if a header with the given name exists, having either a whitespace-trimmed value
+     * matching the predicate, or having at least one whitespace-trimmed single value in a token-separated list of single values.
+     */
+    public boolean containsHeaderString(String name, String valueSeparatorRegex, Predicate<String> valuePredicate) {
+        final String header = getHeaderString(name);
+        if (header == null) {
+            return false;
+        }
+        final String[] split = header.split(valueSeparatorRegex);
+        for (String s : split) {
+            if (valuePredicate.test(s.trim())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks whether a header with a specific name and value (or item of the comma-separated value list) exists.
+     *
+     * Each single non-string header value is converted to String using a {@code RuntimeDelegate.HeaderDelegate} if one
+     * is available via {@code RuntimeDelegate#createHeaderDelegate(java.lang.Class)} for the header value
+     * class or using its {@code toString} method if a header delegate is not available.
+     *
+     * <p>
+     * For example: {@code containsHeaderString("cache-control", "no-store"::equalsIgnoreCase)} will return {@code true} if
+     * a {@code Cache-Control} header exists that has the value {@code no-store}, the value {@code No-Store} or the value
+     * {@code Max-Age, NO-STORE, no-transform}, but {@code false} when it has the value {@code no-store;no-transform}
+     * (missing comma), or the value {@code no - store} (whitespace within value).
+     *
+     * @param name the message header.
+     * @param valuePredicate value must fulfil this predicate.
+     * @return {@code true} if and only if a header with the given name exists, having either a whitespace-trimmed value
+     * matching the predicate, or having at least one whitespace-trimmed single value in a comma-separated list of single values.
+     */
+    public boolean containsHeaderString(String name, Predicate<String> valuePredicate) {
+        return containsHeaderString(name, ",", valuePredicate);
+    }
+
+    /**
+     * Get the allowed HTTP methods from the Allow HTTP header.
+     *
+     * @return the allowed HTTP methods, all methods will returned as upper case
+     * strings.
+     */
+    public Set<String> getAllowedMethods() {
+        final String allowed = getHeaderString(HttpHeaders.ALLOW);
+        if (allowed == null || allowed.isEmpty()) {
+            return Collections.emptySet();
+        }
+        try {
+            return new HashSet<String>(HttpHeaderReader.readStringList(allowed.toUpperCase(Locale.ROOT)));
+        } catch (java.text.ParseException e) {
+            throw exception(HttpHeaders.ALLOW, allowed, e);
+        }
+    }
+
+    /**
+     * Get message date.
+     *
+     * @return the message date, otherwise {@code null} if not present.
+     */
+    public Date getDate() {
+        return singleHeader(HttpHeaders.DATE, Date.class, input -> {
+            try {
+                return HttpHeaderReader.readDate(input);
+            } catch (ParseException e) {
+                throw new ProcessingException(e);
+            }
+        }, false);
+    }
+
+    /**
+     * Get the entity tag.
+     *
+     * @return the entity tag, otherwise {@code null} if not present.
+     */
+    public EntityTag getEntityTag() {
+        return singleHeader(HttpHeaders.ETAG, EntityTag.class, new Function<String, EntityTag>() {
+            @Override
+            public EntityTag apply(String value) {
+                try {
+                    return value == null ? null : EntityTag.valueOf(value);
+                } catch (IllegalArgumentException ex) {
+                    throw new ProcessingException(ex);
+                }
+            }
+        }, false);
+    }
+
+    /**
+     * Get the language of the entity.
+     *
+     * @return the language of the entity or {@code null} if not specified
+     */
+    public Locale getLanguage() {
+        return singleHeader(HttpHeaders.CONTENT_LANGUAGE, Locale.class, input -> {
+            try {
+                return new LanguageTag(input).getAsLocale();
+            } catch (ParseException e) {
+                throw new ProcessingException(e);
+            }
+        }, false);
+    }
+
+    /**
+     * Get the last modified date.
+     *
+     * @return the last modified date, otherwise {@code null} if not present.
+     */
+    public Date getLastModified() {
+        return singleHeader(HttpHeaders.LAST_MODIFIED, Date.class, new Function<String, Date>() {
+            @Override
+            public Date apply(String input) {
+                try {
+                    return HttpHeaderReader.readDate(input);
+                } catch (ParseException e) {
+                    throw new ProcessingException(e);
+                }
+            }
+        }, false);
+    }
+
+    /**
+     * Get Content-Length value.
+     * <p>
+     * <B>Note</B>: {@link #getLengthLong() getLengthLong()}
+     * should be preferred over this method, since it returns a {@code long}
+     * instead and is therefore more portable.</P>
+     *
+     * @return Content-Length as a postive integer if present and valid number, {@code -1} if negative number.
+     * @throws ProcessingException when {@link Integer#parseInt(String)} (String)} throws {@link NumberFormatException}.
+     */
+    public int getLength() {
+        return singleHeader(HttpHeaders.CONTENT_LENGTH, Integer.class, input -> {
+            try {
+                if (input != null && !input.isEmpty()) {
+                    int i = Integer.parseInt(input);
+                    if (i >= 0) {
+                        return i;
+                    }
+                }
+                return -1;
+
+            } catch (NumberFormatException ex) {
+                throw new ProcessingException(ex);
+            }
+        }, true);
+    }
+
+    /**
+     * Get Content-Length value.
+     *
+     * @return Content-Length as a positive long if present and valid number, {@code -1} if negative number.
+     * @throws ProcessingException when {@link Long#parseLong(String)} throws {@link NumberFormatException}.
+     */
+    public long getLengthLong() {
+        return singleHeader(HttpHeaders.CONTENT_LENGTH, Long.class, input -> {
+            try {
+                if (input != null && !input.isEmpty()) {
+                    long l = Long.parseLong(input);
+                    if (l >= 0) {
+                        return l;
+                    }
+                }
+                return -1L;
+            } catch (NumberFormatException ex) {
+                throw new ProcessingException(ex);
+            }
+        }, true);
+    }
+
+    /**
+     * Get the link for the relation.
+     *
+     * @param relation link relation.
+     * @return the link for the relation, otherwise {@code null} if not present.
+     */
+    public Link getLink(String relation) {
+        for (Link link : getLinks()) {
+            List<String> relations = LinkProvider.getLinkRelations(link.getRel());
+            if (relations != null && relations.contains(relation)) {
+                return link;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Convenience method that returns a {@link javax.ws.rs.core.Link.Builder Link.Builder}
+     * for the relation.
+     *
+     * @param relation link relation.
+     * @return the link builder for the relation, otherwise {@code null} if not
+     * present.
+     */
+    public Link.Builder getLinkBuilder(String relation) {
+        Link link = getLink(relation);
+        if (link == null) {
+            return null;
+        }
+
+        return Link.fromLink(link);
+    }
+
+    /**
+     * Get the location.
+     *
+     * @return the location URI, otherwise {@code null} if not present.
+     */
+    public URI getLocation() {
+        return singleHeader(HttpHeaders.LOCATION, URI.class, value -> {
+            try {
+                return value == null ? null : URI.create(value);
+            } catch (IllegalArgumentException ex) {
+                throw new ProcessingException(ex);
+            }
+        }, false);
+    }
+
+    /**
+     * Get any cookies that accompanied the message.
+     *
+     * @return a read-only map of cookie name (String) to {@link javax.ws.rs.core.Cookie}.
+     */
+    public Map<String, Cookie> getRequestCookies() {
+        @SuppressWarnings("unchecked")
+        final List<Object> cookies = (List<Object>) getHeaders().get(HttpHeaders.COOKIE);
+        if (cookies == null || cookies.isEmpty()) {
+            return Collections.emptyMap();
+        }
+
+        Map<String, Cookie> result = new HashMap<String, Cookie>();
+        for (String cookie : toStringList(cookies)) {
+            if (cookie != null) {
+                result.putAll(HttpHeaderReader.readCookies(cookie));
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Get any new cookies set on the message.
+     *
+     * @return a read-only map of cookie name (String) to a {@link javax.ws.rs.core.NewCookie new cookie}.
+     */
+    public Map<String, NewCookie> getResponseCookies() {
+        @SuppressWarnings("unchecked")
+        List<Object> cookies = (List<Object>) getHeaders().get(HttpHeaders.SET_COOKIE);
+        if (cookies == null || cookies.isEmpty()) {
+            return Collections.emptyMap();
+        }
+
+        Map<String, NewCookie> result = new HashMap<String, NewCookie>();
+        for (String cookie : toStringList(cookies)) {
+            if (cookie != null) {
+                NewCookie newCookie = HttpHeaderReader.readNewCookie(cookie);
+                String cookieName = newCookie.getName();
+                if (result.containsKey(cookieName)) {
+                    result.put(cookieName, HeaderUtils.getPreferredCookie(result.get(cookieName), newCookie));
+                } else {
+                    result.put(cookieName, newCookie);
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Check if link for relation exists.
+     *
+     * @param relation link relation.
+     * @return {@code true} if the for the relation link exists, {@code false}
+     * otherwise.
+     */
+    public boolean hasLink(String relation) {
+        for (Link link : getLinks()) {
+            List<String> relations = LinkProvider.getLinkRelations(link.getRel());
+
+            if (relations != null && relations.contains(relation)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Get a single typed header value.
+     *
+     * @param <T>         header value type.
+     * @param name        header name.
+     * @param valueType   header value class.
+     * @param converter   from string conversion function. Is expected to throw {@link ProcessingException}
+     *                    if conversion fails.
+     * @param convertNull if {@code true} this method calls the provided converter even for {@code null}. Otherwise this
+     *                    method returns the {@code null} without calling the converter.
+     * @return value of the header, or (possibly converted) {@code null} if not present.
+     */
+    protected <T> T singleHeader(String name, Class<T> valueType, Function<String, T> converter, boolean convertNull) {
+        @SuppressWarnings("unchecked")
+        final List<Object> values = (List<Object>) getHeaders().get(name);
+
+        if (values == null || values.isEmpty()) {
+            return convertNull ? converter.apply(null) : null;
+        }
+        if (values.size() > 1) {
+            throw new HeaderValueException(
+                    LocalizationMessages.TOO_MANY_HEADER_VALUES(name, values.toString()),
+                    getHeaderValueExceptionContext());
+        }
+
+        Object value = values.get(0);
+        if (value == null) {
+            return convertNull ? converter.apply(null) : null;
+        }
+
+        if (HeaderValueException.Context.OUTBOUND == getHeaderValueExceptionContext() && valueType.isInstance(value)) {
+            return valueType.cast(value);
+        } else {
+            try {
+                return converter.apply(HeaderUtils.asString(value, runtimeDelegateDecorator));
+            } catch (ProcessingException ex) {
+                throw exception(name, value, ex);
+            }
+        }
+    }
+
+    /**
+     * Get a single typed header value for Inbound messages
+     *
+     * @param <T>         header value type.
+     * @param name        header name.
+     * @param converter   from string conversion function. Is expected to throw {@link ProcessingException}
+     *                    if conversion fails.
+     * @param convertNull if {@code true} this method calls the provided converter even for {@code null}. Otherwise this
+     *                    method returns the {@code null} without calling the converter.
+     * @return value of the header, or (possibly converted) {@code null} if not present.
+     */
+    protected <T> T singleHeader(String name, Function<String, T> converter, boolean convertNull) {
+        return singleHeader(name, null, converter, convertNull);
+    }
+
+    protected HeaderValueException exception(final String headerName, Object headerValue, Exception e) {
+        return new HeaderValueException(LocalizationMessages.UNABLE_TO_PARSE_HEADER_VALUE(headerName, headerValue), e,
+                getHeaderValueExceptionContext());
+    }
+
+    private List<String> toStringList(List list) {
+        return getHeaderValueExceptionContext() == HeaderValueException.Context.OUTBOUND
+                ? HeaderUtils.asStringList(list, runtimeDelegateDecorator)
+                : (List<String>) list;
+    }
+}
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 f644f7b..dd5816f 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, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2024 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
@@ -63,7 +63,7 @@
  *
  * @author Marek Potociar
  */
-public class OutboundMessageContext {
+public class OutboundMessageContext extends MessageHeaderMethods {
     private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0];
     private static final List<MediaType> WILDCARD_ACCEPTABLE_TYPE_SINGLETON_LIST =
             Collections.<MediaType>singletonList(MediaTypes.WILDCARD_ACCEPTABLE_TYPE);
@@ -71,7 +71,6 @@
     private final GuardianStringKeyMultivaluedMap<Object> headers;
     private final CommittingOutputStream committingOutputStream;
     private Configuration configuration;
-    private RuntimeDelegate runtimeDelegateDecorator;
     private LazyValue<MediaType> mediaTypeCache;
 
     private Object entity;
@@ -107,11 +106,11 @@
      * @param configuration the client/server {@link Configuration}. If {@code null}, the default behaviour is expected.
      */
     public OutboundMessageContext(Configuration configuration) {
+        super(configuration);
         this.configuration = configuration;
         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_TYPE);
@@ -124,6 +123,7 @@
      * @param original the original outbound message context.
      */
     public OutboundMessageContext(OutboundMessageContext original) {
+        super(original);
         this.headers = new GuardianStringKeyMultivaluedMap<>(HeaderUtils.createOutbound());
         this.headers.setGuard(HttpHeaders.CONTENT_TYPE);
         this.headers.putAll(original.headers);
@@ -134,7 +134,6 @@
         this.entityType = original.entityType;
         this.entityAnnotations = original.entityAnnotations;
         this.configuration = original.configuration;
-        this.runtimeDelegateDecorator = original.runtimeDelegateDecorator;
         this.mediaTypeCache = mediaTypeCache();
     }
 
@@ -190,49 +189,9 @@
         return HeaderUtils.asHeaderString(headers.get(name), runtimeDelegateDecorator);
     }
 
-    /**
-     * Get a single typed header value.
-     *
-     * @param <T>         header value type.
-     * @param name        header name.
-     * @param valueType   header value class.
-     * @param converter   from string conversion function. Is expected to throw {@link ProcessingException}
-     *                    if conversion fails.
-     * @param convertNull if {@code true} this method calls the provided converter even for {@code null}. Otherwise this
-     *                    method returns the {@code null} without calling the converter.
-     * @return value of the header, or (possibly converted) {@code null} if not present.
-     */
-    private <T> T singleHeader(String name, Class<T> valueType, Function<String, T> converter, boolean convertNull) {
-        final List<Object> values = headers.get(name);
-
-        if (values == null || values.isEmpty()) {
-            return convertNull ? converter.apply(null) : null;
-        }
-        if (values.size() > 1) {
-            throw new HeaderValueException(
-                    LocalizationMessages.TOO_MANY_HEADER_VALUES(name, values.toString()),
-                    HeaderValueException.Context.OUTBOUND);
-        }
-
-        Object value = values.get(0);
-        if (value == null) {
-            return convertNull ? converter.apply(null) : null;
-        }
-
-        if (valueType.isInstance(value)) {
-            return valueType.cast(value);
-        } else {
-            try {
-                return converter.apply(HeaderUtils.asString(value, runtimeDelegateDecorator));
-            } catch (ProcessingException ex) {
-                throw exception(name, value, ex);
-            }
-        }
-    }
-
-    private static HeaderValueException exception(final String headerName, Object headerValue, Exception e) {
-        return new HeaderValueException(LocalizationMessages.UNABLE_TO_PARSE_HEADER_VALUE(headerName, headerValue), e,
-                HeaderValueException.Context.OUTBOUND);
+    @Override
+    public HeaderValueException.Context getHeaderValueExceptionContext() {
+        return HeaderValueException.Context.OUTBOUND;
     }
 
     /**
@@ -245,36 +204,6 @@
     }
 
     /**
-     * Get message date.
-     *
-     * @return the message date, otherwise {@code null} if not present.
-     */
-    public Date getDate() {
-        return singleHeader(HttpHeaders.DATE, Date.class, input -> {
-            try {
-                return HttpHeaderReader.readDate(input);
-            } catch (ParseException e) {
-                throw new ProcessingException(e);
-            }
-        }, false);
-    }
-
-    /**
-     * Get the language of the entity.
-     *
-     * @return the language of the entity or {@code null} if not specified
-     */
-    public Locale getLanguage() {
-        return singleHeader(HttpHeaders.CONTENT_LANGUAGE, Locale.class, input -> {
-            try {
-                return new LanguageTag(input).getAsLocale();
-            } catch (ParseException e) {
-                throw new ProcessingException(e);
-            }
-        }, false);
-    }
-
-    /**
      * Get the media type of the entity.
      *
      * @return the media type or {@code null} if not specified (e.g. there's no
@@ -378,174 +307,6 @@
     }
 
     /**
-     * Get any cookies that accompanied the message.
-     *
-     * @return a read-only map of cookie name (String) to {@link javax.ws.rs.core.Cookie}.
-     */
-    public Map<String, Cookie> getRequestCookies() {
-        final List<Object> cookies = headers.get(HttpHeaders.COOKIE);
-        if (cookies == null || cookies.isEmpty()) {
-            return Collections.emptyMap();
-        }
-
-        Map<String, Cookie> result = new HashMap<String, Cookie>();
-        for (String cookie : HeaderUtils.asStringList(cookies, runtimeDelegateDecorator)) {
-            if (cookie != null) {
-                result.putAll(HttpHeaderReader.readCookies(cookie));
-            }
-        }
-        return result;
-    }
-
-    /**
-     * Get the allowed HTTP methods from the Allow HTTP header.
-     *
-     * @return the allowed HTTP methods, all methods will returned as upper case
-     * strings.
-     */
-    public Set<String> getAllowedMethods() {
-        final String allowed = getHeaderString(HttpHeaders.ALLOW);
-        if (allowed == null || allowed.isEmpty()) {
-            return Collections.emptySet();
-        }
-        try {
-            return new HashSet<String>(HttpHeaderReader.readStringList(allowed));
-        } catch (java.text.ParseException e) {
-            throw exception(HttpHeaders.ALLOW, allowed, e);
-        }
-    }
-
-    /**
-     * Get Content-Length value.
-     * <p>
-     * <B>Note</B>: {@link #getLengthLong() getLengthLong()}
-     * should be preferred over this method, since it returns a {@code long}
-     * instead and is therefore more portable.</P>
-     *
-     * @return Content-Length as a postive integer if present and valid number, {@code -1} if negative number.
-     * @throws ProcessingException when {@link Integer#parseInt(String)} (String)} throws {@link NumberFormatException}.
-     */
-    public int getLength() {
-
-        return singleHeader(HttpHeaders.CONTENT_LENGTH, Integer.class, input -> {
-            try {
-                if (input != null && !input.isEmpty()) {
-                    int i = Integer.parseInt(input);
-                    if (i >= 0) {
-                        return i;
-                    }
-                }
-                return -1;
-
-            } catch (NumberFormatException ex) {
-                throw new ProcessingException(ex);
-            }
-        }, true);
-    }
-
-    /**
-     * Get Content-Length value.
-     *
-     * @return Content-Length as a positive long if present and valid number, {@code -1} if negative number.
-     * @throws ProcessingException when {@link Long#parseLong(String)} throws {@link NumberFormatException}.
-     */
-    public long getLengthLong() {
-        return singleHeader(HttpHeaders.CONTENT_LENGTH, Long.class, input -> {
-            try {
-                if (input != null && !input.isEmpty()) {
-                    long l = Long.parseLong(input);
-                    if (l >= 0) {
-                        return l;
-                    }
-                }
-                return -1L;
-            } catch (NumberFormatException ex) {
-                throw new ProcessingException(ex);
-            }
-        }, true);
-    }
-
-    /**
-     * Get any new cookies set on the message message.
-     *
-     * @return a read-only map of cookie name (String) to a {@link javax.ws.rs.core.NewCookie new cookie}.
-     */
-    public Map<String, NewCookie> getResponseCookies() {
-        List<Object> cookies = headers.get(HttpHeaders.SET_COOKIE);
-        if (cookies == null || cookies.isEmpty()) {
-            return Collections.emptyMap();
-        }
-
-        Map<String, NewCookie> result = new HashMap<String, NewCookie>();
-        for (String cookie : HeaderUtils.asStringList(cookies, runtimeDelegateDecorator)) {
-            if (cookie != null) {
-                NewCookie newCookie = HttpHeaderReader.readNewCookie(cookie);
-                String cookieName = newCookie.getName();
-                if (result.containsKey(cookieName)) {
-                    result.put(cookieName, HeaderUtils.getPreferredCookie(result.get(cookieName), newCookie));
-                } else {
-                    result.put(cookieName, newCookie);
-                }
-            }
-        }
-        return result;
-    }
-
-    /**
-     * Get the entity tag.
-     *
-     * @return the entity tag, otherwise {@code null} if not present.
-     */
-    public EntityTag getEntityTag() {
-        return singleHeader(HttpHeaders.ETAG, EntityTag.class, new Function<String, EntityTag>() {
-            @Override
-            public EntityTag apply(String value) {
-                try {
-                    return value == null ? null : EntityTag.valueOf(value);
-                } catch (IllegalArgumentException ex) {
-                    throw new ProcessingException(ex);
-                }
-            }
-        }, false);
-    }
-
-    /**
-     * Get the last modified date.
-     *
-     * @return the last modified date, otherwise {@code null} if not present.
-     */
-    public Date getLastModified() {
-        return singleHeader(HttpHeaders.LAST_MODIFIED, Date.class, new Function<String, Date>() {
-            @Override
-            public Date apply(String input) {
-                try {
-                    return HttpHeaderReader.readDate(input);
-                } catch (ParseException e) {
-                    throw new ProcessingException(e);
-                }
-            }
-        }, false);
-    }
-
-    /**
-     * Get the location.
-     *
-     * @return the location URI, otherwise {@code null} if not present.
-     */
-    public URI getLocation() {
-        return singleHeader(HttpHeaders.LOCATION, URI.class, new Function<String, URI>() {
-            @Override
-            public URI apply(String value) {
-                try {
-                    return value == null ? null : URI.create(value);
-                } catch (IllegalArgumentException ex) {
-                    throw new ProcessingException(ex);
-                }
-            }
-        }, false);
-    }
-
-    /**
      * Get the links attached to the message as header.
      *
      * @return links, may return empty {@link java.util.Set} if no links are present. Never
@@ -583,56 +344,6 @@
         return Collections.unmodifiableSet(result);
     }
 
-    /**
-     * Check if link for relation exists.
-     *
-     * @param relation link relation.
-     * @return {@code true} if the for the relation link exists, {@code false}
-     * otherwise.
-     */
-    public boolean hasLink(String relation) {
-        for (Link link : getLinks()) {
-            List<String> relations = LinkProvider.getLinkRelations(link.getRel());
-            if (relations != null && relations.contains(relation)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Get the link for the relation.
-     *
-     * @param relation link relation.
-     * @return the link for the relation, otherwise {@code null} if not present.
-     */
-    public Link getLink(String relation) {
-        for (Link link : getLinks()) {
-            List<String> relations = LinkProvider.getLinkRelations(link.getRel());
-            if (relations != null && relations.contains(relation)) {
-                return link;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Convenience method that returns a {@link javax.ws.rs.core.Link.Builder Link.Builder}
-     * for the relation.
-     *
-     * @param relation link relation.
-     * @return the link builder for the relation, otherwise {@code null} if not
-     * present.
-     */
-    public Link.Builder getLinkBuilder(String relation) {
-        Link link = getLink(relation);
-        if (link == null) {
-            return null;
-        }
-
-        return Link.fromLink(link);
-    }
-
     // Message entity
 
     /**