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}