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