View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.core5.http.message;
29  
30  import java.util.BitSet;
31  
32  import org.apache.hc.core5.annotation.Contract;
33  import org.apache.hc.core5.annotation.ThreadingBehavior;
34  import org.apache.hc.core5.http.Header;
35  import org.apache.hc.core5.http.HttpVersion;
36  import org.apache.hc.core5.http.ParseException;
37  import org.apache.hc.core5.http.ProtocolVersion;
38  import org.apache.hc.core5.util.Args;
39  import org.apache.hc.core5.util.CharArrayBuffer;
40  import org.apache.hc.core5.util.TextUtils;
41  import org.apache.hc.core5.util.Tokenizer;
42  
43  /**
44   * Default {@link org.apache.hc.core5.http.message.LineParser} implementation.
45   *
46   * @since 4.0
47   */
48  @Contract(threading = ThreadingBehavior.IMMUTABLE)
49  public class BasicLineParser implements LineParser {
50  
51      public final static BasicLineParserBasicLineParser.html#BasicLineParser">BasicLineParser INSTANCE = new BasicLineParser();
52  
53      // IMPORTANT!
54      // These private static variables must be treated as immutable and never exposed outside this class
55      private static final BitSet FULL_STOP = Tokenizer.INIT_BITSET('.');
56      private static final BitSet BLANKS = Tokenizer.INIT_BITSET(' ', '\t');
57      private static final BitSet COLON = Tokenizer.INIT_BITSET(':');
58  
59      /**
60       * A version of the protocol to parse.
61       * The version is typically not relevant, but the protocol name.
62       */
63      private final ProtocolVersion protocol;
64      private final Tokenizer tokenizer;
65  
66      /**
67       * Creates a new line parser for the given HTTP-like protocol.
68       *
69       * @param proto     a version of the protocol to parse, or
70       *                  {@code null} for HTTP. The actual version
71       *                  is not relevant, only the protocol name.
72       */
73      public BasicLineParser(final ProtocolVersion proto) {
74          this.protocol = proto != null? proto : HttpVersion.HTTP_1_1;
75          this.tokenizer = Tokenizer.INSTANCE;
76      }
77  
78      /**
79       * Creates a new line parser for HTTP.
80       */
81      public BasicLineParser() {
82          this(null);
83      }
84  
85      ProtocolVersion parseProtocolVersion(
86              final CharArrayBuffer buffer,
87              final ParserCursor cursor) throws ParseException {
88          final String protoname = this.protocol.getProtocol();
89          final int protolength  = protoname.length();
90  
91          this.tokenizer.skipWhiteSpace(buffer, cursor);
92  
93          final int pos = cursor.getPos();
94  
95          // long enough for "HTTP/1.1"?
96          if (pos + protolength + 4 > cursor.getUpperBound()) {
97              throw new ParseException("Invalid protocol version",
98                      buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
99          }
100 
101         // check the protocol name and slash
102         boolean ok = true;
103         for (int i = 0; ok && (i < protolength); i++) {
104             ok = buffer.charAt(pos + i) == protoname.charAt(i);
105         }
106         if (ok) {
107             ok = buffer.charAt(pos + protolength) == '/';
108         }
109         if (!ok) {
110             throw new ParseException("Invalid protocol version",
111                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
112         }
113 
114         cursor.updatePos(pos + protolength + 1);
115 
116         final String token1 = this.tokenizer.parseToken(buffer, cursor, FULL_STOP);
117         final int major;
118         try {
119             major = Integer.parseInt(token1);
120         } catch (final NumberFormatException e) {
121             throw new ParseException("Invalid protocol major version number",
122                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
123         }
124         if (cursor.atEnd()) {
125             throw new ParseException("Invalid protocol version",
126                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
127         }
128         cursor.updatePos(cursor.getPos() + 1);
129         final String token2 = this.tokenizer.parseToken(buffer, cursor, BLANKS);
130         final int minor;
131         try {
132             minor = Integer.parseInt(token2);
133         } catch (final NumberFormatException e) {
134             throw new ParseException("Invalid protocol minor version number",
135                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
136         }
137         return HttpVersion.get(major, minor);
138     }
139 
140     /**
141      * Parses a request line.
142      *
143      * @param buffer    a buffer holding the line to parse
144      *
145      * @return  the parsed request line
146      *
147      * @throws ParseException        in case of a parse error
148      */
149     @Override
150     public RequestLine parseRequestLine(final CharArrayBuffer buffer) throws ParseException {
151         Args.notNull(buffer, "Char array buffer");
152 
153         final ParserCursorsage/ParserCursor.html#ParserCursor">ParserCursor cursor = new ParserCursor(0, buffer.length());
154         this.tokenizer.skipWhiteSpace(buffer, cursor);
155         final String method = this.tokenizer.parseToken(buffer, cursor, BLANKS);
156         if (TextUtils.isEmpty(method)) {
157             throw new ParseException("Invalid request line",
158                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
159         }
160         this.tokenizer.skipWhiteSpace(buffer, cursor);
161         final String uri = this.tokenizer.parseToken(buffer, cursor, BLANKS);
162         if (TextUtils.isEmpty(uri)) {
163             throw new ParseException("Invalid request line",
164                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
165         }
166         final ProtocolVersion ver = parseProtocolVersion(buffer, cursor);
167         this.tokenizer.skipWhiteSpace(buffer, cursor);
168         if (!cursor.atEnd()) {
169             throw new ParseException("Invalid request line",
170                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
171         }
172         return new RequestLine(method, uri, ver);
173     }
174 
175     @Override
176     public StatusLine parseStatusLine(final CharArrayBuffer buffer) throws ParseException {
177         Args.notNull(buffer, "Char array buffer");
178 
179         final ParserCursorsage/ParserCursor.html#ParserCursor">ParserCursor cursor = new ParserCursor(0, buffer.length());
180         this.tokenizer.skipWhiteSpace(buffer, cursor);
181         final ProtocolVersion ver = parseProtocolVersion(buffer, cursor);
182         this.tokenizer.skipWhiteSpace(buffer, cursor);
183         final String s = this.tokenizer.parseToken(buffer, cursor, BLANKS);
184         for (int i = 0; i < s.length(); i++) {
185             if (!Character.isDigit(s.charAt(i))) {
186                 throw new ParseException("Status line contains invalid status code",
187                         buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
188             }
189         }
190         final int statusCode;
191         try {
192             statusCode = Integer.parseInt(s);
193         } catch (final NumberFormatException e) {
194             throw new ParseException("Status line contains invalid status code",
195                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
196         }
197         final String text = buffer.substringTrimmed(cursor.getPos(), cursor.getUpperBound());
198         return new StatusLine(ver, statusCode, text);
199     }
200 
201     @Override
202     public Header parseHeader(final CharArrayBuffer buffer) throws ParseException {
203         Args.notNull(buffer, "Char array buffer");
204 
205         final ParserCursorsage/ParserCursor.html#ParserCursor">ParserCursor cursor = new ParserCursor(0, buffer.length());
206         this.tokenizer.skipWhiteSpace(buffer, cursor);
207         final String name = this.tokenizer.parseToken(buffer, cursor, COLON);
208         if (cursor.getPos() == cursor.getLowerBound() || cursor.getPos() == cursor.getUpperBound() ||
209                 buffer.charAt(cursor.getPos()) != ':' ||
210                 TextUtils.isEmpty(name) ||
211                 Tokenizer.isWhitespace(buffer.charAt(cursor.getPos() - 1))) {
212             throw new ParseException("Invalid header",
213                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
214         }
215         final String value = buffer.substringTrimmed(cursor.getPos() + 1, cursor.getUpperBound());
216         return new BasicHeader(name, value);
217     }
218 
219 }