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}