View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty5.handler.codec.http;
17  
18  import io.netty5.buffer.api.Buffer;
19  import io.netty5.util.CharsetUtil;
20  
21  import java.util.regex.Matcher;
22  import java.util.regex.Pattern;
23  
24  import static io.netty5.util.internal.ObjectUtil.checkNonEmptyAfterTrim;
25  import static io.netty5.util.internal.ObjectUtil.checkPositiveOrZero;
26  import static java.util.Objects.requireNonNull;
27  
28  /**
29   * The version of HTTP or its derived protocols, such as
30   * <a href="https://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and
31   * <a href="https://en.wikipedia.org/wiki/Internet_Content_Adaptation_Protocol">ICAP</a>.
32   */
33  public class HttpVersion implements Comparable<HttpVersion> {
34  
35      private static final Pattern VERSION_PATTERN =
36          Pattern.compile("(\\S+)/(\\d+)\\.(\\d+)");
37  
38      private static final String HTTP_1_0_STRING = "HTTP/1.0";
39      private static final String HTTP_1_1_STRING = "HTTP/1.1";
40  
41      /**
42       * HTTP/1.0
43       */
44      public static final HttpVersion HTTP_1_0 = new HttpVersion("HTTP", 1, 0, false, true);
45  
46      /**
47       * HTTP/1.1
48       */
49      public static final HttpVersion HTTP_1_1 = new HttpVersion("HTTP", 1, 1, true, true);
50  
51      /**
52       * Returns an existing or new {@link HttpVersion} instance which matches to
53       * the specified protocol version string.  If the specified {@code text} is
54       * equal to {@code "HTTP/1.0"}, {@link #HTTP_1_0} will be returned.  If the
55       * specified {@code text} is equal to {@code "HTTP/1.1"}, {@link #HTTP_1_1}
56       * will be returned.  Otherwise, a new {@link HttpVersion} instance will be
57       * returned.
58       */
59      public static HttpVersion valueOf(String text) {
60          requireNonNull(text, "text");
61  
62          text = text.trim();
63  
64          if (text.isEmpty()) {
65              throw new IllegalArgumentException("text is empty (possibly HTTP/0.9)");
66          }
67  
68          // Try to match without convert to uppercase first as this is what 99% of all clients
69          // will send anyway. Also there is a change to the RFC to make it clear that it is
70          // expected to be case-sensitive
71          //
72          // See:
73          // * https://trac.tools.ietf.org/wg/httpbis/trac/ticket/1
74          // * https://trac.tools.ietf.org/wg/httpbis/trac/wiki
75          //
76          HttpVersion version = version0(text);
77          if (version == null) {
78              version = new HttpVersion(text, true);
79          }
80          return version;
81      }
82  
83      private static HttpVersion version0(String text) {
84          if (HTTP_1_1_STRING.equals(text)) {
85              return HTTP_1_1;
86          }
87          if (HTTP_1_0_STRING.equals(text)) {
88              return HTTP_1_0;
89          }
90          return null;
91      }
92  
93      private final String protocolName;
94      private final int majorVersion;
95      private final int minorVersion;
96      private final String text;
97      private final boolean keepAliveDefault;
98      private final byte[] bytes;
99  
100     /**
101      * Creates a new HTTP version with the specified version string.  You will
102      * not need to create a new instance unless you are implementing a protocol
103      * derived from HTTP, such as
104      * <a href="https://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and
105      * <a href="https://en.wikipedia.org/wiki/Internet_Content_Adaptation_Protocol">ICAP</a>.
106      *
107      * @param keepAliveDefault
108      *        {@code true} if and only if the connection is kept alive unless
109      *        the {@code "Connection"} header is set to {@code "close"} explicitly.
110      */
111     public HttpVersion(String text, boolean keepAliveDefault) {
112         text = checkNonEmptyAfterTrim(text, "text").toUpperCase();
113 
114         Matcher m = VERSION_PATTERN.matcher(text);
115         if (!m.matches()) {
116             throw new IllegalArgumentException("invalid version format: " + text);
117         }
118 
119         protocolName = m.group(1);
120         majorVersion = Integer.parseInt(m.group(2));
121         minorVersion = Integer.parseInt(m.group(3));
122         this.text = protocolName + '/' + majorVersion + '.' + minorVersion;
123         this.keepAliveDefault = keepAliveDefault;
124         bytes = null;
125     }
126 
127     /**
128      * Creates a new HTTP version with the specified protocol name and version
129      * numbers.  You will not need to create a new instance unless you are
130      * implementing a protocol derived from HTTP, such as
131      * <a href="https://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and
132      * <a href="https://en.wikipedia.org/wiki/Internet_Content_Adaptation_Protocol">ICAP</a>
133      *
134      * @param keepAliveDefault
135      *        {@code true} if and only if the connection is kept alive unless
136      *        the {@code "Connection"} header is set to {@code "close"} explicitly.
137      */
138     public HttpVersion(
139             String protocolName, int majorVersion, int minorVersion,
140             boolean keepAliveDefault) {
141         this(protocolName, majorVersion, minorVersion, keepAliveDefault, false);
142     }
143 
144     private HttpVersion(
145             String protocolName, int majorVersion, int minorVersion,
146             boolean keepAliveDefault, boolean bytes) {
147         protocolName = checkNonEmptyAfterTrim(protocolName, "protocolName").toUpperCase();
148 
149         for (int i = 0; i < protocolName.length(); i ++) {
150             if (Character.isISOControl(protocolName.charAt(i)) ||
151                     Character.isWhitespace(protocolName.charAt(i))) {
152                 throw new IllegalArgumentException("invalid character in protocolName");
153             }
154         }
155 
156         checkPositiveOrZero(majorVersion, "majorVersion");
157         checkPositiveOrZero(minorVersion, "minorVersion");
158 
159         this.protocolName = protocolName;
160         this.majorVersion = majorVersion;
161         this.minorVersion = minorVersion;
162         text = protocolName + '/' + majorVersion + '.' + minorVersion;
163         this.keepAliveDefault = keepAliveDefault;
164 
165         if (bytes) {
166             this.bytes = text.getBytes(CharsetUtil.US_ASCII);
167         } else {
168             this.bytes = null;
169         }
170     }
171 
172     /**
173      * Returns the name of the protocol such as {@code "HTTP"} in {@code "HTTP/1.0"}.
174      */
175     public String protocolName() {
176         return protocolName;
177     }
178 
179     /**
180      * Returns the name of the protocol such as {@code 1} in {@code "HTTP/1.0"}.
181      */
182     public int majorVersion() {
183         return majorVersion;
184     }
185 
186     /**
187      * Returns the name of the protocol such as {@code 0} in {@code "HTTP/1.0"}.
188      */
189     public int minorVersion() {
190         return minorVersion;
191     }
192 
193     /**
194      * Returns the full protocol version text such as {@code "HTTP/1.0"}.
195      */
196     public String text() {
197         return text;
198     }
199 
200     /**
201      * Returns {@code true} if and only if the connection is kept alive unless
202      * the {@code "Connection"} header is set to {@code "close"} explicitly.
203      */
204     public boolean isKeepAliveDefault() {
205         return keepAliveDefault;
206     }
207 
208     /**
209      * Returns the full protocol version text such as {@code "HTTP/1.0"}.
210      */
211     @Override
212     public String toString() {
213         return text();
214     }
215 
216     @Override
217     public int hashCode() {
218         return (protocolName().hashCode() * 31 + majorVersion()) * 31 +
219                minorVersion();
220     }
221 
222     @Override
223     public boolean equals(Object o) {
224         if (!(o instanceof HttpVersion)) {
225             return false;
226         }
227 
228         HttpVersion that = (HttpVersion) o;
229         return minorVersion() == that.minorVersion() &&
230                majorVersion() == that.majorVersion() &&
231                protocolName().equals(that.protocolName());
232     }
233 
234     @Override
235     public int compareTo(HttpVersion o) {
236         int v = protocolName().compareTo(o.protocolName());
237         if (v != 0) {
238             return v;
239         }
240 
241         v = majorVersion() - o.majorVersion();
242         if (v != 0) {
243             return v;
244         }
245 
246         return minorVersion() - o.minorVersion();
247     }
248 
249     void encode(Buffer buf) {
250         if (bytes == null) {
251             buf.writeCharSequence(text, CharsetUtil.US_ASCII);
252         } else {
253             buf.writeBytes(bytes);
254         }
255     }
256 }