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-2014 ForgeRock AS. 016 */ 017 018package org.forgerock.openig.header; 019 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025 026import org.forgerock.openig.util.CaseInsensitiveMap; 027 028/** 029 * Utility class for processing values in HTTP header fields. 030 */ 031public final class HeaderUtil { 032 033 /** Static methods only. */ 034 private HeaderUtil() { 035 // No implementation required. 036 } 037 038 /** 039 * Parses an HTTP header value, splitting it into multiple values around the specified 040 * separator. Quoted strings are not split into multiple values if they contain separator 041 * characters. All leading and trailing white space in values is trimmed. All quotations 042 * remain intact. 043 * <p> 044 * Note: This method is liberal in its interpretation of malformed header values; namely 045 * the incorrect use of string and character quoting mechanisms and unquoted white space. 046 * If a {@code null} or empty string is supplied as a value, this method yields an empty 047 * list. 048 * 049 * @param value the header value to be split. 050 * @param separator the separator character to split headers around. 051 * @return A list of string representing the split values of the header. 052 */ 053 public static List<String> split(final String value, final char separator) { 054 if (separator == '"' || separator == '\\') { 055 throw new IllegalArgumentException("invalid separator: " + separator); 056 } 057 final ArrayList<String> values = new ArrayList<String>(); 058 if (value != null) { 059 int length = value.length(); 060 final StringBuilder sb = new StringBuilder(); 061 boolean escaped = false; 062 boolean quoted = false; 063 for (int n = 0, cp; n < length; n += Character.charCount(cp)) { 064 cp = value.codePointAt(n); 065 if (escaped) { 066 // single-character quoting mechanism per RFC 2616 §2.2 067 sb.appendCodePoint(cp); 068 escaped = false; 069 } else if (cp == '\\') { 070 sb.appendCodePoint(cp); 071 if (quoted) { 072 // single-character quoting mechanism per RFC 2616 §2.2 073 escaped = true; 074 } 075 } else if (cp == '"') { 076 // quotation marks remain intact here 077 sb.appendCodePoint(cp); 078 quoted = !quoted; 079 } else if (cp == separator && !quoted) { 080 // only separator if not in quoted string 081 String s = sb.toString().trim(); 082 if (s.length() > 0) { 083 values.add(s); 084 } 085 // reset for next token 086 sb.setLength(0); 087 } else { 088 sb.appendCodePoint(cp); 089 } 090 } 091 final String s = sb.toString().trim(); 092 if (s.length() > 0) { 093 values.add(s); 094 } 095 } 096 return values; 097 } 098 099 /** 100 * Joins a collection of header values into a single header value, with a specified 101 * specified separator. A {@code null} or empty collection of header values yeilds a 102 * {@code null} return value. 103 * 104 * @param values the values to be joined. 105 * @param separator the separator to separate values within the returned value. 106 * @return a single header value, with values separated by the separator. 107 */ 108 public static String join(final Collection<String> values, final char separator) { 109 if (separator == '"' || separator == '\\') { 110 throw new IllegalArgumentException("invalid separator: " + separator); 111 } 112 final StringBuilder sb = new StringBuilder(); 113 if (values != null) { 114 for (final String s : values) { 115 if (s != null) { 116 if (sb.length() > 0) { 117 sb.append(separator).append(' '); 118 } 119 sb.append(s); 120 } 121 } 122 } 123 return sb.length() > 0 ? sb.toString() : null; 124 } 125 126 /** 127 * Splits a single HTTP header parameter name and value from an input string value. The 128 * input string value is presumed to have been extracted from a collection provided by the 129 * {@link #split(String, char)} method. 130 * <p> 131 * This method returns the parameter name-value pair split into an array of 132 * {@code String}s. Element {@code [0]} contains the parameter name; element {@code [1]} 133 * contains contains the parameter value or {@code null} if there is no value. 134 * <p> 135 * A value that is contained within a quoted-string is processed such that the surrounding 136 * '"' (quotation mark) characters are removed and single-character quotations hold the 137 * character being quoted without the escape '\' (backslash) character. All white space 138 * outside of the quoted-string is removed. White space within the quoted-string is 139 * retained. 140 * <p> 141 * Note: This method is liberal in its interpretation of a malformed header value; namely 142 * the incorrect use of string and character quoting mechanisms and unquoted white space. 143 * 144 * @param value the string to parse the name-value parameter from. 145 * @return the name-value pair split into a {@code String} array. 146 */ 147 public static String[] parseParameter(final String value) { 148 String[] ss = new String[2]; 149 boolean inValue = false; 150 boolean quoted = false; 151 boolean escaped = false; 152 int length = value.length(); 153 final StringBuilder sb = new StringBuilder(); 154 for (int n = 0, cp; n < length; n += Character.charCount(cp)) { 155 cp = value.codePointAt(n); 156 if (escaped) { 157 // single-character quoting mechanism per RFC 2616 §2.2 158 sb.appendCodePoint(cp); 159 escaped = false; 160 } else if (cp == '\\') { 161 if (quoted) { 162 // next character is literal 163 escaped = true; 164 } else { 165 // not quoted, push the backslash literal (header probably malformed) 166 sb.appendCodePoint(cp); 167 } 168 } else if (cp == '"') { 169 // toggle quoted status 170 quoted = !quoted; 171 } else if (!quoted && !inValue && cp == '=') { 172 // only separator if in key and not in quoted-string 173 ss[0] = sb.toString().trim(); 174 // reset for next token 175 sb.setLength(0); 176 inValue = true; 177 } else if (!quoted && Character.isWhitespace(cp)) { 178 // drop unquoted white space (header probably malformed if not at beginning or end) 179 } else { 180 sb.appendCodePoint(cp); 181 } 182 } 183 if (!inValue) { 184 ss[0] = sb.toString().trim(); 185 } else { 186 ss[1] = sb.toString(); 187 } 188 return ss; 189 } 190 191 /** 192 * Parses a set of HTTP header parameters from a collection of values. The input 193 * collection of values is presumed to have been provided from the 194 * {@link #split(String, char)} method. 195 * <p> 196 * A well-formed parameter contains an attribute and optional value, separated by an '=' 197 * (equals sign) character. If the parameter contains no value, it is represented by a 198 * {@code null} value in the returned map. 199 * <p> 200 * Values that are contained in quoted-strings are processed such that the surrounding 201 * '"' (quotation mark) characters are removed and single-character quotations hold the 202 * character being quoted without the escape '\' (backslash) character. All white space 203 * outside of quoted-strings is removed. White space within quoted-strings is retained. 204 * <p> 205 * Note: This method is liberal in its interpretation of malformed header values; namely 206 * the incorrect use of string and character quoting mechanisms and unquoted white space. 207 * 208 * @param values the HTTP header parameters. 209 * @return a map of parameter name-value pairs. 210 */ 211 public static Map<String, String> parseParameters(final Collection<String> values) { 212 final CaseInsensitiveMap<String> map = new CaseInsensitiveMap<String>(new HashMap<String, String>()); 213 if (values != null) { 214 for (final String value : values) { 215 final String[] param = parseParameter(value); 216 if (param[0] != null && param[0].length() > 0 && !map.containsKey(param[0])) { 217 map.put(param[0], param[1]); 218 } 219 } 220 } 221 return map; 222 } 223 224 /** 225 * Encloses a string in quotation marks. Quotation marks and backslash characters are 226 * escaped with the single-character quoting mechanism. For more information, see 227 * <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC 2616</a> §2.2. 228 * 229 * @param value the value to be enclosed in quotation marks. 230 * @return the value enclosed in quotation marks. 231 */ 232 public static String quote(final String value) { 233 if (value == null) { 234 return null; 235 } 236 final StringBuilder sb = new StringBuilder("\""); 237 int length = value.length(); 238 for (int n = 0, cp; n < length; n += Character.charCount(cp)) { 239 cp = value.codePointAt(n); 240 if (cp == '\\' || cp == '"') { 241 sb.append('\\'); 242 } 243 sb.appendCodePoint(cp); 244 } 245 sb.append('"'); 246 return sb.toString(); 247 } 248}