1/*
2 * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/message/BasicLineParser.java $
3 * $Revision: 591798 $
4 * $Date: 2007-11-04 08:19:29 -0800 (Sun, 04 Nov 2007) $
5 *
6 * ====================================================================
7 * Licensed to the Apache Software Foundation (ASF) under one
8 * or more contributor license agreements.  See the NOTICE file
9 * distributed with this work for additional information
10 * regarding copyright ownership.  The ASF licenses this file
11 * to you under the Apache License, Version 2.0 (the
12 * "License"); you may not use this file except in compliance
13 * with the License.  You may obtain a copy of the License at
14 *
15 *   http://www.apache.org/licenses/LICENSE-2.0
16 *
17 * Unless required by applicable law or agreed to in writing,
18 * software distributed under the License is distributed on an
19 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20 * KIND, either express or implied.  See the License for the
21 * specific language governing permissions and limitations
22 * under the License.
23 * ====================================================================
24 *
25 * This software consists of voluntary contributions made by many
26 * individuals on behalf of the Apache Software Foundation.  For more
27 * information on the Apache Software Foundation, please see
28 * <http://www.apache.org/>.
29 *
30 */
31
32package org.apache.http.message;
33
34import org.apache.http.HttpVersion;
35import org.apache.http.ProtocolVersion;
36import org.apache.http.ParseException;
37import org.apache.http.RequestLine;
38import org.apache.http.StatusLine;
39import org.apache.http.Header;
40import org.apache.http.protocol.HTTP;
41import org.apache.http.util.CharArrayBuffer;
42
43
44/**
45 * Basic parser for lines in the head section of an HTTP message.
46 * There are individual methods for parsing a request line, a
47 * status line, or a header line.
48 * The lines to parse are passed in memory, the parser does not depend
49 * on any specific IO mechanism.
50 * Instances of this class are stateless and thread-safe.
51 * Derived classes MUST maintain these properties.
52 *
53 * <p>
54 * Note: This class was created by refactoring parsing code located in
55 * various other classes. The author tags from those other classes have
56 * been replicated here, although the association with the parsing code
57 * taken from there has not been traced.
58 * </p>
59 *
60 * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a>
61 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
62 * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
63 * @author and others
64 */
65public class BasicLineParser implements LineParser {
66
67    /**
68     * A default instance of this class, for use as default or fallback.
69     * Note that {@link BasicLineParser} is not a singleton, there can
70     * be many instances of the class itself and of derived classes.
71     * The instance here provides non-customized, default behavior.
72     */
73    public final static BasicLineParser DEFAULT = new BasicLineParser();
74
75
76    /**
77     * A version of the protocol to parse.
78     * The version is typically not relevant, but the protocol name.
79     */
80    protected final ProtocolVersion protocol;
81
82
83    /**
84     * Creates a new line parser for the given HTTP-like protocol.
85     *
86     * @param proto     a version of the protocol to parse, or
87     *                  <code>null</code> for HTTP. The actual version
88     *                  is not relevant, only the protocol name.
89     */
90    public BasicLineParser(ProtocolVersion proto) {
91        if (proto == null) {
92            proto = HttpVersion.HTTP_1_1;
93        }
94        this.protocol = proto;
95    }
96
97
98    /**
99     * Creates a new line parser for HTTP.
100     */
101    public BasicLineParser() {
102        this(null);
103    }
104
105
106
107    public final static
108        ProtocolVersion parseProtocolVersion(String value,
109                                             LineParser parser)
110        throws ParseException {
111
112        if (value == null) {
113            throw new IllegalArgumentException
114                ("Value to parse may not be null.");
115        }
116
117        if (parser == null)
118            parser = BasicLineParser.DEFAULT;
119
120        CharArrayBuffer buffer = new CharArrayBuffer(value.length());
121        buffer.append(value);
122        ParserCursor cursor = new ParserCursor(0, value.length());
123        return parser.parseProtocolVersion(buffer, cursor);
124    }
125
126
127    // non-javadoc, see interface LineParser
128    public ProtocolVersion parseProtocolVersion(final CharArrayBuffer buffer,
129                                                final ParserCursor cursor)
130        throws ParseException {
131
132        if (buffer == null) {
133            throw new IllegalArgumentException("Char array buffer may not be null");
134        }
135        if (cursor == null) {
136            throw new IllegalArgumentException("Parser cursor may not be null");
137        }
138
139        final String protoname = this.protocol.getProtocol();
140        final int protolength  = protoname.length();
141
142        int indexFrom = cursor.getPos();
143        int indexTo = cursor.getUpperBound();
144
145        skipWhitespace(buffer, cursor);
146
147        int i = cursor.getPos();
148
149        // long enough for "HTTP/1.1"?
150        if (i + protolength + 4 > indexTo) {
151            throw new ParseException
152                ("Not a valid protocol version: " +
153                 buffer.substring(indexFrom, indexTo));
154        }
155
156        // check the protocol name and slash
157        boolean ok = true;
158        for (int j=0; ok && (j<protolength); j++) {
159            ok = (buffer.charAt(i+j) == protoname.charAt(j));
160        }
161        if (ok) {
162            ok = (buffer.charAt(i+protolength) == '/');
163        }
164        if (!ok) {
165            throw new ParseException
166                ("Not a valid protocol version: " +
167                 buffer.substring(indexFrom, indexTo));
168        }
169
170        i += protolength+1;
171
172        int period = buffer.indexOf('.', i, indexTo);
173        if (period == -1) {
174            throw new ParseException
175                ("Invalid protocol version number: " +
176                 buffer.substring(indexFrom, indexTo));
177        }
178        int major;
179        try {
180            major = Integer.parseInt(buffer.substringTrimmed(i, period));
181        } catch (NumberFormatException e) {
182            throw new ParseException
183                ("Invalid protocol major version number: " +
184                 buffer.substring(indexFrom, indexTo));
185        }
186        i = period + 1;
187
188        int blank = buffer.indexOf(' ', i, indexTo);
189        if (blank == -1) {
190            blank = indexTo;
191        }
192        int minor;
193        try {
194            minor = Integer.parseInt(buffer.substringTrimmed(i, blank));
195        } catch (NumberFormatException e) {
196            throw new ParseException(
197                "Invalid protocol minor version number: " +
198                buffer.substring(indexFrom, indexTo));
199        }
200
201        cursor.updatePos(blank);
202
203        return createProtocolVersion(major, minor);
204
205    } // parseProtocolVersion
206
207
208    /**
209     * Creates a protocol version.
210     * Called from {@link #parseProtocolVersion}.
211     *
212     * @param major     the major version number, for example 1 in HTTP/1.0
213     * @param minor     the minor version number, for example 0 in HTTP/1.0
214     *
215     * @return  the protocol version
216     */
217    protected ProtocolVersion createProtocolVersion(int major, int minor) {
218        return protocol.forVersion(major, minor);
219    }
220
221
222
223    // non-javadoc, see interface LineParser
224    public boolean hasProtocolVersion(final CharArrayBuffer buffer,
225                                      final ParserCursor cursor) {
226
227        if (buffer == null) {
228            throw new IllegalArgumentException("Char array buffer may not be null");
229        }
230        if (cursor == null) {
231            throw new IllegalArgumentException("Parser cursor may not be null");
232        }
233        int index = cursor.getPos();
234
235        final String protoname = this.protocol.getProtocol();
236        final int  protolength = protoname.length();
237
238        if (buffer.length() < protolength+4)
239            return false; // not long enough for "HTTP/1.1"
240
241        if (index < 0) {
242            // end of line, no tolerance for trailing whitespace
243            // this works only for single-digit major and minor version
244            index = buffer.length() -4 -protolength;
245        } else if (index == 0) {
246            // beginning of line, tolerate leading whitespace
247            while ((index < buffer.length()) &&
248                    HTTP.isWhitespace(buffer.charAt(index))) {
249                 index++;
250             }
251        } // else within line, don't tolerate whitespace
252
253
254        if (index + protolength + 4 > buffer.length())
255            return false;
256
257
258        // just check protocol name and slash, no need to analyse the version
259        boolean ok = true;
260        for (int j=0; ok && (j<protolength); j++) {
261            ok = (buffer.charAt(index+j) == protoname.charAt(j));
262        }
263        if (ok) {
264            ok = (buffer.charAt(index+protolength) == '/');
265        }
266
267        return ok;
268    }
269
270
271
272    public final static
273        RequestLine parseRequestLine(final String value,
274                                     LineParser parser)
275        throws ParseException {
276
277        if (value == null) {
278            throw new IllegalArgumentException
279                ("Value to parse may not be null.");
280        }
281
282        if (parser == null)
283            parser = BasicLineParser.DEFAULT;
284
285        CharArrayBuffer buffer = new CharArrayBuffer(value.length());
286        buffer.append(value);
287        ParserCursor cursor = new ParserCursor(0, value.length());
288        return parser.parseRequestLine(buffer, cursor);
289    }
290
291
292    /**
293     * Parses a request line.
294     *
295     * @param buffer    a buffer holding the line to parse
296     *
297     * @return  the parsed request line
298     *
299     * @throws ParseException        in case of a parse error
300     */
301    public RequestLine parseRequestLine(final CharArrayBuffer buffer,
302                                        final ParserCursor cursor)
303        throws ParseException {
304
305        if (buffer == null) {
306            throw new IllegalArgumentException("Char array buffer may not be null");
307        }
308        if (cursor == null) {
309            throw new IllegalArgumentException("Parser cursor may not be null");
310        }
311
312        int indexFrom = cursor.getPos();
313        int indexTo = cursor.getUpperBound();
314
315        try {
316            skipWhitespace(buffer, cursor);
317            int i = cursor.getPos();
318
319            int blank = buffer.indexOf(' ', i, indexTo);
320            if (blank < 0) {
321                throw new ParseException("Invalid request line: " +
322                        buffer.substring(indexFrom, indexTo));
323            }
324            String method = buffer.substringTrimmed(i, blank);
325            cursor.updatePos(blank);
326
327            skipWhitespace(buffer, cursor);
328            i = cursor.getPos();
329
330            blank = buffer.indexOf(' ', i, indexTo);
331            if (blank < 0) {
332                throw new ParseException("Invalid request line: " +
333                        buffer.substring(indexFrom, indexTo));
334            }
335            String uri = buffer.substringTrimmed(i, blank);
336            cursor.updatePos(blank);
337
338            ProtocolVersion ver = parseProtocolVersion(buffer, cursor);
339
340            skipWhitespace(buffer, cursor);
341            if (!cursor.atEnd()) {
342                throw new ParseException("Invalid request line: " +
343                        buffer.substring(indexFrom, indexTo));
344            }
345
346            return createRequestLine(method, uri, ver);
347        } catch (IndexOutOfBoundsException e) {
348            throw new ParseException("Invalid request line: " +
349                                     buffer.substring(indexFrom, indexTo));
350        }
351    } // parseRequestLine
352
353
354    /**
355     * Instantiates a new request line.
356     * Called from {@link #parseRequestLine}.
357     *
358     * @param method    the request method
359     * @param uri       the requested URI
360     * @param ver       the protocol version
361     *
362     * @return  a new status line with the given data
363     */
364    protected RequestLine createRequestLine(final String method,
365                                            final String uri,
366                                            final ProtocolVersion ver) {
367        return new BasicRequestLine(method, uri, ver);
368    }
369
370
371
372    public final static
373        StatusLine parseStatusLine(final String value,
374                                   LineParser parser)
375        throws ParseException {
376
377        if (value == null) {
378            throw new IllegalArgumentException
379                ("Value to parse may not be null.");
380        }
381
382        if (parser == null)
383            parser = BasicLineParser.DEFAULT;
384
385        CharArrayBuffer buffer = new CharArrayBuffer(value.length());
386        buffer.append(value);
387        ParserCursor cursor = new ParserCursor(0, value.length());
388        return parser.parseStatusLine(buffer, cursor);
389    }
390
391
392    // non-javadoc, see interface LineParser
393    public StatusLine parseStatusLine(final CharArrayBuffer buffer,
394                                      final ParserCursor cursor)
395        throws ParseException {
396
397        if (buffer == null) {
398            throw new IllegalArgumentException("Char array buffer may not be null");
399        }
400        if (cursor == null) {
401            throw new IllegalArgumentException("Parser cursor may not be null");
402        }
403
404        int indexFrom = cursor.getPos();
405        int indexTo = cursor.getUpperBound();
406
407        try {
408            // handle the HTTP-Version
409            ProtocolVersion ver = parseProtocolVersion(buffer, cursor);
410
411            // handle the Status-Code
412            skipWhitespace(buffer, cursor);
413            int i = cursor.getPos();
414
415            int blank = buffer.indexOf(' ', i, indexTo);
416            if (blank < 0) {
417                blank = indexTo;
418            }
419            int statusCode = 0;
420            try {
421                statusCode =
422                    Integer.parseInt(buffer.substringTrimmed(i, blank));
423            } catch (NumberFormatException e) {
424                throw new ParseException(
425                    "Unable to parse status code from status line: "
426                    + buffer.substring(indexFrom, indexTo));
427            }
428            //handle the Reason-Phrase
429            i = blank;
430            String reasonPhrase = null;
431            if (i < indexTo) {
432                reasonPhrase = buffer.substringTrimmed(i, indexTo);
433            } else {
434                reasonPhrase = "";
435            }
436            return createStatusLine(ver, statusCode, reasonPhrase);
437
438        } catch (IndexOutOfBoundsException e) {
439            throw new ParseException("Invalid status line: " +
440                                     buffer.substring(indexFrom, indexTo));
441        }
442    } // parseStatusLine
443
444
445    /**
446     * Instantiates a new status line.
447     * Called from {@link #parseStatusLine}.
448     *
449     * @param ver       the protocol version
450     * @param status    the status code
451     * @param reason    the reason phrase
452     *
453     * @return  a new status line with the given data
454     */
455    protected StatusLine createStatusLine(final ProtocolVersion ver,
456                                          final int status,
457                                          final String reason) {
458        return new BasicStatusLine(ver, status, reason);
459    }
460
461
462
463    public final static
464        Header parseHeader(final String value,
465                           LineParser parser)
466        throws ParseException {
467
468        if (value == null) {
469            throw new IllegalArgumentException
470                ("Value to parse may not be null");
471        }
472
473        if (parser == null)
474            parser = BasicLineParser.DEFAULT;
475
476        CharArrayBuffer buffer = new CharArrayBuffer(value.length());
477        buffer.append(value);
478        return parser.parseHeader(buffer);
479    }
480
481
482    // non-javadoc, see interface LineParser
483    public Header parseHeader(CharArrayBuffer buffer)
484        throws ParseException {
485
486        // the actual parser code is in the constructor of BufferedHeader
487        return new BufferedHeader(buffer);
488    }
489
490
491    /**
492     * Helper to skip whitespace.
493     */
494    protected void skipWhitespace(final CharArrayBuffer buffer, final ParserCursor cursor) {
495        int pos = cursor.getPos();
496        int indexTo = cursor.getUpperBound();
497        while ((pos < indexTo) &&
498               HTTP.isWhitespace(buffer.charAt(pos))) {
499            pos++;
500        }
501        cursor.updatePos(pos);
502    }
503
504} // class BasicLineParser
505