001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.http; 018 019import static org.apache.juneau.commons.utils.StringUtils.*; 020 021import java.util.*; 022 023/** 024 * Lightweight parser for HTTP header values as defined by RFC 2616. 025 * 026 * <p> 027 * Parses comma-delimited header values into {@link HeaderElement} objects, where each element 028 * has a name and optional semicolon-delimited parameters. 029 * 030 * <p> 031 * For example, the header value <js>"text/html;charset=UTF-8;q=0.9, text/json"</js> is parsed into two 032 * elements: <js>"text/html"</js> with parameters <js>"charset=UTF-8"</js> and <js>"q=0.9"</js>, 033 * and <js>"text/json"</js> with no parameters. 034 */ 035public final class HeaderValueParser { 036 037 /** 038 * Parses an HTTP header value into an array of {@link HeaderElement} objects. 039 * 040 * <p> 041 * Elements are separated by commas. Each element may have parameters separated by semicolons. 042 * Quoted strings are respected (commas and semicolons inside quotes are not treated as delimiters). 043 * 044 * @param value The header value to parse. May be <jk>null</jk> or empty. 045 * @return An array of parsed elements, never <jk>null</jk>. 046 */ 047 @SuppressWarnings({ 048 "java:S3776" // Cognitive complexity acceptable for parser logic 049 }) 050 public static HeaderElement[] readElements(String value) { 051 if (isEmpty(value)) 052 return new HeaderElement[0]; 053 054 var elements = new ArrayList<HeaderElement>(); 055 var len = value.length(); 056 var pos = 0; 057 058 while (pos < len) { 059 pos = skipWhitespace(value, pos); 060 if (pos >= len) 061 break; 062 063 var name = readName(value, pos); 064 pos += name.length(); 065 name = name.trim(); 066 067 var params = new ArrayList<NameValuePair>(); 068 069 while (pos < len && value.charAt(pos) == ';') { 070 pos++; 071 pos = skipWhitespace(value, pos); 072 073 var paramName = readToken(value, pos, '=', ';', ','); 074 pos += paramName.length(); 075 paramName = paramName.trim(); 076 077 String paramValue = null; 078 if (pos < len && value.charAt(pos) == '=') { 079 pos++; 080 pos = skipWhitespace(value, pos); 081 if (pos < len && value.charAt(pos) == '"') { 082 var q = readQuotedString(value, pos); 083 paramValue = q.text(); 084 pos = q.nextPos(); 085 } else { 086 paramValue = readToken(value, pos, ';', ','); 087 pos += paramValue.length(); 088 paramValue = paramValue.trim(); 089 } 090 } 091 092 if (!paramName.isEmpty()) 093 params.add(new BasicNameValuePair(paramName, paramValue)); 094 } 095 096 if (!name.isEmpty()) 097 elements.add(new HeaderElement(name, params.toArray(NameValuePair.EMPTY_ARRAY))); 098 099 if (pos < len && value.charAt(pos) == ',') 100 pos++; 101 } 102 103 return elements.toArray(new HeaderElement[0]); 104 } 105 106 private static String readName(String value, int pos) { 107 return readToken(value, pos, ';', ','); 108 } 109 110 private static String readToken(String value, int pos, char... delimiters) { 111 var start = pos; 112 var len = value.length(); 113 var inQuotes = false; 114 115 while (pos < len) { 116 var c = value.charAt(pos); 117 if (c == '"') 118 inQuotes = !inQuotes; 119 if (!inQuotes) { 120 for (var d : delimiters) 121 if (c == d) 122 return value.substring(start, pos); 123 } 124 pos++; 125 } 126 return value.substring(start, pos); 127 } 128 129 /** 130 * Parses a double-quoted header parameter value, honoring {@code \} escapes. 131 * 132 * @param value Full header string. 133 * @param openQuoteIndex Index of the opening {@code "} character. 134 * @return Parsed text and the index immediately after the closing {@code "}, or after the last character if 135 * the string is unterminated. 136 */ 137 private static QuotedString readQuotedString(String value, int openQuoteIndex) { 138 var sb = new StringBuilder(); 139 var pos = openQuoteIndex + 1; 140 var len = value.length(); 141 while (pos < len) { 142 var c = value.charAt(pos); 143 if (c == '\\' && pos + 1 < len) { 144 sb.append(value.charAt(pos + 1)); 145 pos += 2; 146 } else if (c == '"') { 147 return new QuotedString(sb.toString(), pos + 1); 148 } else { 149 sb.append(c); 150 pos++; 151 } 152 } 153 return new QuotedString(sb.toString(), pos); 154 } 155 156 private record QuotedString(String text, int nextPos) {} 157 158 private static int skipWhitespace(String value, int pos) { 159 while (pos < value.length() && (value.charAt(pos) == ' ' || value.charAt(pos) == '\t')) 160 pos++; 161 return pos; 162 } 163}