001/*
002 * ====================================================================
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 * ====================================================================
020 *
021 * This software consists of voluntary contributions made by many
022 * individuals on behalf of the Apache Software Foundation.  For more
023 * information on the Apache Software Foundation, please see
024 * <http://www.apache.org/>.
025 *
026 */
027
028package org.apache.http.message;
029
030import org.apache.http.Header;
031import org.apache.http.HttpVersion;
032import org.apache.http.ParseException;
033import org.apache.http.ProtocolVersion;
034import org.apache.http.RequestLine;
035import org.apache.http.StatusLine;
036import org.apache.http.annotation.ThreadingBehavior;
037import org.apache.http.annotation.Contract;
038import org.apache.http.protocol.HTTP;
039import org.apache.http.util.Args;
040import org.apache.http.util.CharArrayBuffer;
041
042/**
043 * Basic parser for lines in the head section of an HTTP message.
044 * There are individual methods for parsing a request line, a
045 * status line, or a header line.
046 * The lines to parse are passed in memory, the parser does not depend
047 * on any specific IO mechanism.
048 * Instances of this class are stateless and thread-safe.
049 * Derived classes MUST maintain these properties.
050 *
051 * <p>
052 * Note: This class was created by refactoring parsing code located in
053 * various other classes. The author tags from those other classes have
054 * been replicated here, although the association with the parsing code
055 * taken from there has not been traced.
056 * </p>
057 *
058 * @since 4.0
059 */
060@Contract(threading = ThreadingBehavior.IMMUTABLE)
061public class BasicLineParser implements LineParser {
062
063    /**
064     * A default instance of this class, for use as default or fallback.
065     * Note that {@link BasicLineParser} is not a singleton, there can
066     * be many instances of the class itself and of derived classes.
067     * The instance here provides non-customized, default behavior.
068     *
069     * @deprecated (4.3) use {@link #INSTANCE}
070     */
071    @Deprecated
072    public final static BasicLineParser DEFAULT = new BasicLineParser();
073
074    public final static BasicLineParser INSTANCE = new BasicLineParser();
075
076    /**
077     * A version of the protocol to parse.
078     * The version is typically not relevant, but the protocol name.
079     */
080    protected final ProtocolVersion protocol;
081
082
083    /**
084     * Creates a new line parser for the given HTTP-like protocol.
085     *
086     * @param proto     a version of the protocol to parse, or
087     *                  {@code null} for HTTP. The actual version
088     *                  is not relevant, only the protocol name.
089     */
090    public BasicLineParser(final ProtocolVersion proto) {
091        this.protocol = proto != null? proto : HttpVersion.HTTP_1_1;
092    }
093
094
095    /**
096     * Creates a new line parser for HTTP.
097     */
098    public BasicLineParser() {
099        this(null);
100    }
101
102
103    public static
104        ProtocolVersion parseProtocolVersion(final String value,
105                                             final LineParser parser) throws ParseException {
106        Args.notNull(value, "Value");
107
108        final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
109        buffer.append(value);
110        final ParserCursor cursor = new ParserCursor(0, value.length());
111        return (parser != null ? parser : BasicLineParser.INSTANCE)
112                .parseProtocolVersion(buffer, cursor);
113    }
114
115
116    // non-javadoc, see interface LineParser
117    @Override
118    public ProtocolVersion parseProtocolVersion(final CharArrayBuffer buffer,
119                                                final ParserCursor cursor) throws ParseException {
120        Args.notNull(buffer, "Char array buffer");
121        Args.notNull(cursor, "Parser cursor");
122        final String protoname = this.protocol.getProtocol();
123        final int protolength  = protoname.length();
124
125        final int indexFrom = cursor.getPos();
126        final int indexTo = cursor.getUpperBound();
127
128        skipWhitespace(buffer, cursor);
129
130        int i = cursor.getPos();
131
132        // long enough for "HTTP/1.1"?
133        if (i + protolength + 4 > indexTo) {
134            throw new ParseException
135                ("Not a valid protocol version: " +
136                 buffer.substring(indexFrom, indexTo));
137        }
138
139        // check the protocol name and slash
140        boolean ok = true;
141        for (int j=0; ok && (j<protolength); j++) {
142            ok = (buffer.charAt(i+j) == protoname.charAt(j));
143        }
144        if (ok) {
145            ok = (buffer.charAt(i+protolength) == '/');
146        }
147        if (!ok) {
148            throw new ParseException
149                ("Not a valid protocol version: " +
150                 buffer.substring(indexFrom, indexTo));
151        }
152
153        i += protolength+1;
154
155        final int period = buffer.indexOf('.', i, indexTo);
156        if (period == -1) {
157            throw new ParseException
158                ("Invalid protocol version number: " +
159                 buffer.substring(indexFrom, indexTo));
160        }
161        final int major;
162        try {
163            major = Integer.parseInt(buffer.substringTrimmed(i, period));
164        } catch (final NumberFormatException e) {
165            throw new ParseException
166                ("Invalid protocol major version number: " +
167                 buffer.substring(indexFrom, indexTo));
168        }
169        i = period + 1;
170
171        int blank = buffer.indexOf(' ', i, indexTo);
172        if (blank == -1) {
173            blank = indexTo;
174        }
175        final int minor;
176        try {
177            minor = Integer.parseInt(buffer.substringTrimmed(i, blank));
178        } catch (final NumberFormatException e) {
179            throw new ParseException(
180                "Invalid protocol minor version number: " +
181                buffer.substring(indexFrom, indexTo));
182        }
183
184        cursor.updatePos(blank);
185
186        return createProtocolVersion(major, minor);
187
188    } // parseProtocolVersion
189
190
191    /**
192     * Creates a protocol version.
193     * Called from {@link #parseProtocolVersion}.
194     *
195     * @param major     the major version number, for example 1 in HTTP/1.0
196     * @param minor     the minor version number, for example 0 in HTTP/1.0
197     *
198     * @return  the protocol version
199     */
200    protected ProtocolVersion createProtocolVersion(final int major, final int minor) {
201        return protocol.forVersion(major, minor);
202    }
203
204
205
206    // non-javadoc, see interface LineParser
207    @Override
208    public boolean hasProtocolVersion(final CharArrayBuffer buffer,
209                                      final ParserCursor cursor) {
210        Args.notNull(buffer, "Char array buffer");
211        Args.notNull(cursor, "Parser cursor");
212        int index = cursor.getPos();
213
214        final String protoname = this.protocol.getProtocol();
215        final int  protolength = protoname.length();
216
217        if (buffer.length() < protolength+4)
218         {
219            return false; // not long enough for "HTTP/1.1"
220        }
221
222        if (index < 0) {
223            // end of line, no tolerance for trailing whitespace
224            // this works only for single-digit major and minor version
225            index = buffer.length() -4 -protolength;
226        } else if (index == 0) {
227            // beginning of line, tolerate leading whitespace
228            while ((index < buffer.length()) &&
229                    HTTP.isWhitespace(buffer.charAt(index))) {
230                 index++;
231             }
232        } // else within line, don't tolerate whitespace
233
234
235        if (index + protolength + 4 > buffer.length()) {
236            return false;
237        }
238
239
240        // just check protocol name and slash, no need to analyse the version
241        boolean ok = true;
242        for (int j=0; ok && (j<protolength); j++) {
243            ok = (buffer.charAt(index+j) == protoname.charAt(j));
244        }
245        if (ok) {
246            ok = (buffer.charAt(index+protolength) == '/');
247        }
248
249        return ok;
250    }
251
252
253
254    public static
255        RequestLine parseRequestLine(final String value,
256                                     final LineParser parser) throws ParseException {
257        Args.notNull(value, "Value");
258
259        final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
260        buffer.append(value);
261        final ParserCursor cursor = new ParserCursor(0, value.length());
262        return (parser != null ? parser : BasicLineParser.INSTANCE)
263            .parseRequestLine(buffer, cursor);
264    }
265
266
267    /**
268     * Parses a request line.
269     *
270     * @param buffer    a buffer holding the line to parse
271     *
272     * @return  the parsed request line
273     *
274     * @throws ParseException        in case of a parse error
275     */
276    @Override
277    public RequestLine parseRequestLine(final CharArrayBuffer buffer,
278                                        final ParserCursor cursor) throws ParseException {
279
280        Args.notNull(buffer, "Char array buffer");
281        Args.notNull(cursor, "Parser cursor");
282        final int indexFrom = cursor.getPos();
283        final int indexTo = cursor.getUpperBound();
284
285        try {
286            skipWhitespace(buffer, cursor);
287            int i = cursor.getPos();
288
289            int blank = buffer.indexOf(' ', i, indexTo);
290            if (blank < 0) {
291                throw new ParseException("Invalid request line: " +
292                        buffer.substring(indexFrom, indexTo));
293            }
294            final String method = buffer.substringTrimmed(i, blank);
295            cursor.updatePos(blank);
296
297            skipWhitespace(buffer, cursor);
298            i = cursor.getPos();
299
300            blank = buffer.indexOf(' ', i, indexTo);
301            if (blank < 0) {
302                throw new ParseException("Invalid request line: " +
303                        buffer.substring(indexFrom, indexTo));
304            }
305            final String uri = buffer.substringTrimmed(i, blank);
306            cursor.updatePos(blank);
307
308            final ProtocolVersion ver = parseProtocolVersion(buffer, cursor);
309
310            skipWhitespace(buffer, cursor);
311            if (!cursor.atEnd()) {
312                throw new ParseException("Invalid request line: " +
313                        buffer.substring(indexFrom, indexTo));
314            }
315
316            return createRequestLine(method, uri, ver);
317        } catch (final IndexOutOfBoundsException e) {
318            throw new ParseException("Invalid request line: " +
319                                     buffer.substring(indexFrom, indexTo));
320        }
321    } // parseRequestLine
322
323
324    /**
325     * Instantiates a new request line.
326     * Called from {@link #parseRequestLine}.
327     *
328     * @param method    the request method
329     * @param uri       the requested URI
330     * @param ver       the protocol version
331     *
332     * @return  a new status line with the given data
333     */
334    protected RequestLine createRequestLine(final String method,
335                                            final String uri,
336                                            final ProtocolVersion ver) {
337        return new BasicRequestLine(method, uri, ver);
338    }
339
340
341
342    public static
343        StatusLine parseStatusLine(final String value,
344                                   final LineParser parser) throws ParseException {
345        Args.notNull(value, "Value");
346
347        final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
348        buffer.append(value);
349        final ParserCursor cursor = new ParserCursor(0, value.length());
350        return (parser != null ? parser : BasicLineParser.INSTANCE)
351                .parseStatusLine(buffer, cursor);
352    }
353
354
355    // non-javadoc, see interface LineParser
356    @Override
357    public StatusLine parseStatusLine(final CharArrayBuffer buffer,
358                                      final ParserCursor cursor) throws ParseException {
359        Args.notNull(buffer, "Char array buffer");
360        Args.notNull(cursor, "Parser cursor");
361        final int indexFrom = cursor.getPos();
362        final int indexTo = cursor.getUpperBound();
363
364        try {
365            // handle the HTTP-Version
366            final ProtocolVersion ver = parseProtocolVersion(buffer, cursor);
367
368            // handle the Status-Code
369            skipWhitespace(buffer, cursor);
370            int i = cursor.getPos();
371
372            int blank = buffer.indexOf(' ', i, indexTo);
373            if (blank < 0) {
374                blank = indexTo;
375            }
376            final int statusCode;
377            final String s = buffer.substringTrimmed(i, blank);
378            for (int j = 0; j < s.length(); j++) {
379                if (!Character.isDigit(s.charAt(j))) {
380                    throw new ParseException(
381                            "Status line contains invalid status code: "
382                            + buffer.substring(indexFrom, indexTo));
383                }
384            }
385            try {
386                statusCode = Integer.parseInt(s);
387            } catch (final NumberFormatException e) {
388                throw new ParseException(
389                        "Status line contains invalid status code: "
390                        + buffer.substring(indexFrom, indexTo));
391            }
392            //handle the Reason-Phrase
393            i = blank;
394            final String reasonPhrase;
395            if (i < indexTo) {
396                reasonPhrase = buffer.substringTrimmed(i, indexTo);
397            } else {
398                reasonPhrase = "";
399            }
400            return createStatusLine(ver, statusCode, reasonPhrase);
401
402        } catch (final IndexOutOfBoundsException e) {
403            throw new ParseException("Invalid status line: " +
404                                     buffer.substring(indexFrom, indexTo));
405        }
406    } // parseStatusLine
407
408
409    /**
410     * Instantiates a new status line.
411     * Called from {@link #parseStatusLine}.
412     *
413     * @param ver       the protocol version
414     * @param status    the status code
415     * @param reason    the reason phrase
416     *
417     * @return  a new status line with the given data
418     */
419    protected StatusLine createStatusLine(final ProtocolVersion ver,
420                                          final int status,
421                                          final String reason) {
422        return new BasicStatusLine(ver, status, reason);
423    }
424
425
426
427    public static
428        Header parseHeader(final String value,
429                           final LineParser parser) throws ParseException {
430        Args.notNull(value, "Value");
431
432        final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
433        buffer.append(value);
434        return (parser != null ? parser : BasicLineParser.INSTANCE)
435                .parseHeader(buffer);
436    }
437
438
439    // non-javadoc, see interface LineParser
440    @Override
441    public Header parseHeader(final CharArrayBuffer buffer)
442        throws ParseException {
443
444        // the actual parser code is in the constructor of BufferedHeader
445        return new BufferedHeader(buffer);
446    }
447
448
449    /**
450     * Helper to skip whitespace.
451     */
452    protected void skipWhitespace(final CharArrayBuffer buffer, final ParserCursor cursor) {
453        int pos = cursor.getPos();
454        final int indexTo = cursor.getUpperBound();
455        while ((pos < indexTo) &&
456               HTTP.isWhitespace(buffer.charAt(pos))) {
457            pos++;
458        }
459        cursor.updatePos(pos);
460    }
461
462} // class BasicLineParser