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,