001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2010–2011 ApexIdentity Inc. 015 * Portions Copyright 2011-2015 ForgeRock AS. 016 */ 017 018package org.forgerock.http.header; 019 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.HashMap; 023import java.util.Iterator; 024import java.util.List; 025import java.util.Map; 026 027import org.forgerock.http.protocol.Message; 028import org.forgerock.http.util.CaseInsensitiveMap; 029 030/** 031 * Utility class for processing values in HTTP header fields. 032 */ 033public final class HeaderUtil { 034 035 /** Static methods only. */ 036 private HeaderUtil() { 037 // No implementation required. 038 } 039 040 /** 041 * Parses an HTTP header value, splitting it into multiple values around the 042 * specified separator. Quoted strings are not split into multiple values if 043 * they contain separator characters. All leading and trailing white space 044 * in values is trimmed. All quotations remain intact. 045 * <p> 046 * Note: This method is liberal in its interpretation of malformed header 047 * values; namely the incorrect use of string and character quoting 048 * mechanisms and unquoted white space. If a {@code null} or empty string is 049 * supplied as a value, this method yields an empty list. 050 * 051 * @param value 052 * the header value to be split. 053 * @param separator 054 * the separator character to split headers around. 055 * @return A list of string representing the split values of the header. 056 */ 057 public static List<String> split(final String value, final char separator) { 058 if (separator == '"' || separator == '\\') { 059 throw new IllegalArgumentException("invalid separator: " + separator); 060 } 061 final ArrayList<String> values = new ArrayList<>(); 062 if (value != null) { 063 int length = value.length(); 064 final StringBuilder sb = new StringBuilder(); 065 boolean escaped = false; 066 boolean quoted = false; 067 for (int n = 0, cp; n < length; n += Character.charCount(cp)) { 068 cp = value.codePointAt(n); 069 if (escaped) { 070 // single-character quoting mechanism per RFC 2616 §2.2 071 sb.appendCodePoint(cp); 072 escaped = false; 073 } else if (cp == '\\') { 074 sb.appendCodePoint(cp); 075 if (quoted) { 076 // single-character quoting mechanism per RFC 2616 §2.2 077 escaped = true; 078 } 079 } else if (cp == '"') { 080 // quotation marks remain intact here 081 sb.appendCodePoint(cp); 082 quoted = !quoted; 083 } else if (cp == separator && !quoted) { 084 // only separator if not in quoted string 085 String s = sb.toString().trim(); 086 if (s.length() > 0) { 087 values.add(s); 088 } 089 // reset for next token 090 sb.setLength(0); 091 } else { 092 sb.appendCodePoint(cp); 093 } 094 } 095 final String s = sb.toString().trim(); 096 if (s.length() > 0) { 097 values.add(s); 098 } 099 } 100 return values; 101 } 102 103 /** 104 * Joins a collection of header values into a single header value, with a 105 * specified specified separator. A {@code null} or empty collection of 106 * header values yeilds a {@code null} return value. 107 * 108 * @param values 109 * the values to be joined. 110 * @param separator 111 * the separator to separate values within the returned value. 112 * @return a single header value, with values separated by the separator. 113 */ 114 public static String join(final Collection<String> values, final char separator) { 115 if (separator == '"' || separator == '\\') { 116 throw new IllegalArgumentException("invalid separator: " + separator); 117 } 118 final StringBuilder sb = new StringBuilder(); 119 if (values != null) { 120 for (final String s : values) { 121 if (s != null) { 122 if (sb.length() > 0) { 123 sb.append(separator).append(' '); 124 } 125 sb.append(s); 126 } 127 } 128 } 129 return sb.length() > 0 ? sb.toString() : null; 130 } 131 132 /** 133 * Splits a single HTTP header parameter name and value from an input string 134 * value. The input string value is presumed to have been extracted from a 135 * collection provided by the {@link #split(String, char)} method. 136 * <p> 137 * This method returns the parameter name-value pair split into an array of 138 * {@code String}s. Element {@code [0]} contains the parameter name; element 139 * {@code [1]} contains contains the parameter value or {@code null} if 140 * there is no value. 141 * <p> 142 * A value that is contained within a quoted-string is processed such that 143 * the surrounding '"' (quotation mark) characters are removed and 144 * single-character quotations hold the character being quoted without the 145 * escape '\' (backslash) character. All white space outside of the 146 * quoted-string is removed. White space within the quoted-string is 147 * retained. 148 * <p> 149 * Note: This method is liberal in its interpretation of a malformed header 150 * value; namely the incorrect use of string and character quoting 151 * mechanisms and unquoted white space. 152 * 153 * @param value 154 * the string to parse the name-value parameter from. 155 * @return the name-value pair split into a {@code String} array. 156 */ 157 public static String[] parseParameter(final String value) { 158 String[] ss = new String[2]; 159 boolean inValue = false; 160 boolean quoted = false; 161 boolean escaped = false; 162 int length = value.length(); 163 final StringBuilder sb = new StringBuilder(); 164 for (int n = 0, cp; n < length; n += Character.charCount(cp)) { 165 cp = value.codePointAt(n); 166 if (escaped) { 167 // single-character quoting mechanism per RFC 2616 §2.2 168 sb.appendCodePoint(cp); 169 escaped = false; 170 } else if (cp == '\\') { 171 if (quoted) { 172 // next character is literal 173 escaped = true; 174 } else { 175 // not quoted, push the backslash literal (header probably malformed) 176 sb.appendCodePoint(cp); 177 } 178 } else if (cp == '"') { 179 // toggle quoted status 180 quoted = !quoted; 181 } else if (!quoted && !inValue && cp == '=') { 182 // only separator if in key and not in quoted-string 183 ss[0] = sb.toString().trim(); 184 // reset for next token 185 sb.setLength(0); 186 inValue = true; 187 } else if (!quoted && Character.isWhitespace(cp)) { 188 // drop unquoted white space (header probably malformed if not at beginning or end) 189 } else { 190 sb.appendCodePoint(cp); 191 } 192 } 193 if (!inValue) { 194 ss[0] = sb.toString().trim(); 195 } else { 196 ss[1] = sb.toString(); 197 } 198 return ss; 199 } 200 201 /** 202 * Parses a set of HTTP header parameters from a collection of values. The 203 * input collection of values is presumed to have been provided from the 204 * {@link #split(String, char)} method. 205 * <p> 206 * A well-formed parameter contains an attribute and optional value, 207 * separated by an '=' (equals sign) character. If the parameter contains no 208 * value, it is represented by a {@code null} value in the returned map. 209 * <p> 210 * Values that are contained in quoted-strings are processed such that the 211 * surrounding '"' (quotation mark) characters are removed and 212 * single-character quotations hold the character being quoted without the 213 * escape '\' (backslash) character. All white space outside of 214 * quoted-strings is removed. White space within quoted-strings is retained. 215 * <p> 216 * Note: This method is liberal in its interpretation of malformed header 217 * values; namely the incorrect use of string and character quoting 218 * mechanisms and unquoted white space. 219 * 220 * @param values 221 * the HTTP header parameters. 222 * @return a map of parameter name-value pairs. 223 */ 224 public static Map<String, String> parseParameters(final Collection<String> values) { 225 final CaseInsensitiveMap<String> map = new CaseInsensitiveMap<>(new HashMap<String, String>()); 226 if (values != null) { 227 for (final String value : values) { 228 final String[] param = parseParameter(value); 229 if (param[0] != null && param[0].length() > 0 && !map.containsKey(param[0])) { 230 map.put(param[0], param[1]); 231 } 232 } 233 } 234 return map; 235 } 236 237 /** 238 * Encloses a string in quotation marks. Quotation marks and backslash 239 * characters are escaped with the single-character quoting mechanism. For 240 * more information, see <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC 241 * 2616</a> §2.2. 242 * 243 * @param value 244 * the value to be enclosed in quotation marks. 245 * @return the value enclosed in quotation marks. 246 */ 247 public static String quote(final String value) { 248 if (value == null) { 249 return null; 250 } 251 final StringBuilder sb = new StringBuilder("\""); 252 int length = value.length(); 253 for (int n = 0, cp; n < length; n += Character.charCount(cp)) { 254 cp = value.codePointAt(n); 255 if (cp == '\\' || cp == '"') { 256 sb.append('\\'); 257 } 258 sb.appendCodePoint(cp); 259 } 260 sb.append('"'); 261 return sb.toString(); 262 } 263 264 /** 265 * Parses the named header from the message as a multi-valued comma 266 * separated value. If there are multiple headers present then they are 267 * first merged and then {@link #split(String, char) split}. 268 * 269 * @param message 270 * The HTTP request or response. 271 * @param name 272 * The name of the header. 273 * @return A list of strings representing the split values of the header, 274 * which may be empty if the header was not present in the message. 275 */ 276 public static List<String> parseMultiValuedHeader(Message message, String name) { 277 final Collection<String> values = message != null && message.getHeaders().containsKey(name) 278 ? message.getHeaders().get(name).getValues() : null; 279 return parseMultiValuedHeader(join(values, ',')); 280 } 281 282 /** 283 * Parses the header content as a multi-valued comma separated value. 284 * 285 * @param header 286 * The HTTP header content. 287 * @return A list of strings representing the split values of the header, 288 * which may be empty if the header was {@code null} or empty. 289 */ 290 public static List<String> parseMultiValuedHeader(final String header) { 291 return split(header, ','); 292 } 293 294 /** 295 * Parses the named single-valued header from the message. If there are 296 * multiple headers present then only the first is used. 297 * 298 * @param message 299 * The HTTP request or response. 300 * @param name 301 * The name of the header. 302 * @return The header value, or {@code null} if the header was not present 303 * in the message. 304 */ 305 public static String parseSingleValuedHeader(Message message, String name) { 306 if (message == null || !message.getHeaders().containsKey(name)) { 307 return null; 308 } 309 final Iterator<String> iterator = message.getHeaders().get(name).getValues().iterator(); 310 final String header = iterator.hasNext() 311 ? iterator.next() : null; 312 return header != null ? header : null; 313 } 314}