MessageSupport: methods for strict header and header element parsing
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/message/HeaderElementConsumer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/message/HeaderElementConsumer.java
new file mode 100644
index 0000000..2e47c4d
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/message/HeaderElementConsumer.java
@@ -0,0 +1,43 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.core5.http.message;
+
+import org.apache.hc.core5.http.ProtocolException;
+
+/**
+ * Abstract consumer of header elements represented by a subsequence
+ * of the given {@link CharSequence} within the given {@link ParserCursor}
+ * bounds.
+ *
+ * @since 5.5
+ */
+@FunctionalInterface
+public interface HeaderElementConsumer {
+
+    void accept(CharSequence buffer, ParserCursor cursor) throws ProtocolException;
+
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/message/MessageSupport.java b/httpcore5/src/main/java/org/apache/hc/core5/http/message/MessageSupport.java
index b17743f..4c130dd 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/message/MessageSupport.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/message/MessageSupport.java
@@ -51,6 +51,7 @@
 import org.apache.hc.core5.http.MessageHeaders;
 import org.apache.hc.core5.http.Method;
 import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.ProtocolException;
 import org.apache.hc.core5.util.Args;
 import org.apache.hc.core5.util.CharArrayBuffer;
 import org.apache.hc.core5.util.Tokenizer;
@@ -234,6 +235,78 @@ public static void parseElementList(final CharSequence src,
     }
 
     /**
+     * @since 5.5
+     */
+    public static void parseElementList(final MessageHeaders headers,
+                                        final String name,
+                                        final BiConsumer<CharSequence, ParserCursor> consumer) {
+        parseHeaders(headers, name, (charSequence, cursor) ->
+                parseElementList(charSequence, cursor, consumer));
+    }
+
+    /**
+     * @since 5.5
+     */
+    public static void parseHeaderStrict(final Header header,
+                                         final HeaderElementConsumer consumer) throws ProtocolException {
+        Args.notNull(header, "Header");
+        if (header instanceof FormattedHeader) {
+            final CharArrayBuffer buf = ((FormattedHeader) header).getBuffer();
+            final ParserCursor cursor = new ParserCursor(0, buf.length());
+            cursor.updatePos(((FormattedHeader) header).getValuePos());
+            consumer.accept(buf, cursor);
+        } else {
+            final String value = header.getValue();
+            final ParserCursor cursor = new ParserCursor(0, value.length());
+            consumer.accept(value, cursor);
+        }
+    }
+
+    /**
+     * @since 5.5
+     */
+    public static void parseHeadersStrict(final MessageHeaders headers,
+                                          final String name,
+                                          final HeaderElementConsumer consumer) throws ProtocolException {
+        Args.notNull(headers, "Message headers");
+        Args.notBlank(name, "Header name");
+        final Iterator<Header> it = headers.headerIterator(name);
+        while (it.hasNext()) {
+            parseHeaderStrict(it.next(), consumer);
+        }
+    }
+
+    /**
+     * @since 5.5
+     */
+    public static void parseElementListStrict(final CharSequence src,
+                                              final ParserCursor cursor,
+                                              final HeaderElementConsumer consumer) throws ProtocolException {
+        Args.notNull(src, "Source");
+        Args.notNull(cursor, "Cursor");
+        Args.notNull(consumer, "Consumer");
+        while (!cursor.atEnd()) {
+            consumer.accept(src, cursor);
+            if (!cursor.atEnd()) {
+                final char ch = src.charAt(cursor.getPos());
+                if (ch == ',') {
+                    cursor.updatePos(cursor.getPos() + 1);
+                }
+            }
+        }
+    }
+
+    /**
+     * @since 5.5
+     */
+    public static void parseElementListStrict(final MessageHeaders headers,
+                                              final String name,
+                                              final HeaderElementConsumer consumer) throws ProtocolException {
+        parseHeadersStrict(headers, name, (charSequence, cursor) ->
+                parseElementListStrict(charSequence, cursor, consumer));
+    }
+
+    /**
      * @since 5.4
      */
     public static void parseTokens(final CharSequence src,
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestMessageSupport.java b/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestMessageSupport.java
index 709821e..ea69a0e 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestMessageSupport.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestMessageSupport.java
@@ -32,6 +32,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
@@ -44,7 +45,9 @@
 import org.apache.hc.core5.http.HttpMessage;
 import org.apache.hc.core5.http.HttpResponse;
 import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.Method;
 import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.ProtocolException;
 import org.apache.hc.core5.http.io.entity.HttpEntities;
 import org.apache.hc.core5.http.support.BasicResponseBuilder;
 import org.apache.hc.core5.util.CharArrayBuffer;
@@ -330,6 +333,88 @@ void testParseParams() {
         Assertions.assertFalse(cursor.atEnd());
     }
 
+    static String copyHeader(final CharSequence charSequence, final ParserCursor cursor) {
+        final CharArrayBuffer buf = new CharArrayBuffer(10);
+        int pos = cursor.getPos();
+        while (pos < cursor.getUpperBound()) {
+            final char ch = charSequence.charAt(pos);
+            buf.append(ch);
+            pos++;
+        }
+        cursor.updatePos(pos);
+        return buf.toString();
+    }
+
+    static String copyToken(final CharSequence charSequence, final ParserCursor cursor) {
+        final CharArrayBuffer buf = new CharArrayBuffer(10);
+        int pos = cursor.getPos();
+        while (pos < cursor.getUpperBound()) {
+            final char ch = charSequence.charAt(pos);
+            if (ch == ',') {
+                break;
+            }
+            buf.append(ch);
+            pos++;
+        }
+        cursor.updatePos(pos);
+        return buf.substringTrimmed(0, buf.length());
+    }
+
+    @Test
+    void testParseHeaders() {
+        final HttpMessage message = new BasicHttpRequest(Method.GET, "/");
+        message.addHeader("Some-Header", "this");
+        message.addHeader("Some-Header", "that");
+        message.addHeader("Some-Header", " this, that,  what not");
+
+        final List<String> headerValues = new LinkedList<>();
+        MessageSupport.parseHeaders(message, "Some-header", (charSequence, cursor) -> {
+            final String headerValue = copyHeader(charSequence, cursor);
+            headerValues.add(headerValue);
+        });
+        Assertions.assertEquals(Arrays.asList("this", "that", " this, that,  what not"), headerValues);
+
+        final List<String> tokens = new LinkedList<>();
+        MessageSupport.parseElementList(message, "Some-header", (charSequence, cursor) -> {
+            final String token = copyToken(charSequence, cursor);
+            tokens.add(token);
+        });
+        Assertions.assertEquals(Arrays.asList("this", "that", "this", "that", "what not"), tokens);
+    }
+
+    @Test
+    void testParseHeadersStrict() throws Exception {
+        final HttpMessage message = new BasicHttpRequest(Method.GET, "/");
+        message.addHeader("Some-Header", "this");
+        message.addHeader("Some-Header", "that");
+        message.addHeader("Some-Header", " this, that,  what not");
+
+        final List<String> headerValues = new LinkedList<>();
+        MessageSupport.parseHeadersStrict(message, "Some-header", (charSequence, cursor) -> {
+            final String headerValue = copyHeader(charSequence, cursor);
+            headerValues.add(headerValue);
+        });
+        Assertions.assertEquals(Arrays.asList("this", "that", " this, that,  what not"), headerValues);
+
+        final List<String> tokens = new LinkedList<>();
+        MessageSupport.parseElementListStrict(message, "Some-header", (charSequence, cursor) -> {
+            final String token = copyToken(charSequence, cursor);
+            tokens.add(token);
+        });
+        Assertions.assertEquals(Arrays.asList("this", "that", "this", "that", "what not"), tokens);
+
+        Assertions.assertThrows(ProtocolException.class, () ->
+                MessageSupport.parseElementListStrict(
+                        message,
+                        "Some-header",
+                        (charSequence, cursor) -> {
+                    final String token = copyToken(charSequence, cursor);
+                    if (token.equalsIgnoreCase("what not")) {
+                        throw new ProtocolException("How awful!");
+                    }
+                }));
+    }
+
     @Test
     void testAddContentHeaders() {
         final HttpEntity entity = HttpEntities.create("some stuff with trailers", StandardCharsets.US_ASCII,