1/*
2 * Copyright (C) 2013 Square, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.squareup.okhttp;
17
18import com.squareup.okhttp.internal.Platform;
19import com.squareup.okhttp.internal.Util;
20import java.io.File;
21import java.io.FileInputStream;
22import java.io.IOException;
23import java.io.InputStream;
24import java.io.UnsupportedEncodingException;
25import java.net.MalformedURLException;
26import java.net.URI;
27import java.net.URISyntaxException;
28import java.net.URL;
29import java.util.List;
30import okio.BufferedSink;
31
32/**
33 * An HTTP request. Instances of this class are immutable if their {@link #body}
34 * is null or itself immutable.
35 */
36public final class Request {
37  private final URL url;
38  private final String method;
39  private final Headers headers;
40  private final Body body;
41  private final Object tag;
42
43  private volatile ParsedHeaders parsedHeaders; // Lazily initialized.
44  private volatile URI uri; // Lazily initialized.
45  private volatile CacheControl cacheControl; // Lazily initialized.
46
47  private Request(Builder builder) {
48    this.url = builder.url;
49    this.method = builder.method;
50    this.headers = builder.headers.build();
51    this.body = builder.body;
52    this.tag = builder.tag != null ? builder.tag : this;
53  }
54
55  public URL url() {
56    return url;
57  }
58
59  public URI uri() throws IOException {
60    try {
61      URI result = uri;
62      return result != null ? result : (uri = Platform.get().toUriLenient(url));
63    } catch (URISyntaxException e) {
64      throw new IOException(e.getMessage());
65    }
66  }
67
68  public String urlString() {
69    return url.toString();
70  }
71
72  public String method() {
73    return method;
74  }
75
76  public Headers headers() {
77    return headers;
78  }
79
80  public String header(String name) {
81    return headers.get(name);
82  }
83
84  public List<String> headers(String name) {
85    return headers.values(name);
86  }
87
88  public Body body() {
89    return body;
90  }
91
92  public Object tag() {
93    return tag;
94  }
95
96  public Builder newBuilder() {
97    return new Builder(this);
98  }
99
100  public Headers getHeaders() {
101    return headers;
102  }
103
104  public String getUserAgent() {
105    return parsedHeaders().userAgent;
106  }
107
108  public String getProxyAuthorization() {
109    return parsedHeaders().proxyAuthorization;
110  }
111
112  private ParsedHeaders parsedHeaders() {
113    ParsedHeaders result = parsedHeaders;
114    return result != null ? result : (parsedHeaders = new ParsedHeaders(headers));
115  }
116
117  /**
118   * Returns the cache control directives for this response. This is never null,
119   * even if this response contains no {@code Cache-Control} header.
120   */
121  public CacheControl cacheControl() {
122    CacheControl result = cacheControl;
123    return result != null ? result : (cacheControl = CacheControl.parse(headers));
124  }
125
126  public boolean isHttps() {
127    return url().getProtocol().equals("https");
128  }
129
130  /** Parsed request headers, computed on-demand and cached. */
131  private static class ParsedHeaders {
132    private String userAgent;
133    private String proxyAuthorization;
134
135    public ParsedHeaders(Headers headers) {
136      for (int i = 0; i < headers.size(); i++) {
137        String fieldName = headers.name(i);
138        String value = headers.value(i);
139        if ("User-Agent".equalsIgnoreCase(fieldName)) {
140          userAgent = value;
141        } else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) {
142          proxyAuthorization = value;
143        }
144      }
145    }
146  }
147
148  public abstract static class Body {
149    /** Returns the Content-Type header for this body. */
150    public abstract MediaType contentType();
151
152    /**
153     * Returns the number of bytes that will be written to {@code out} in a call
154     * to {@link #writeTo}, or -1 if that count is unknown.
155     */
156    public long contentLength() {
157      return -1;
158    }
159
160    /** Writes the content of this request to {@code out}. */
161    public abstract void writeTo(BufferedSink sink) throws IOException;
162
163    /**
164     * Returns a new request body that transmits {@code content}. If {@code
165     * contentType} lacks a charset, this will use UTF-8.
166     */
167    public static Body create(MediaType contentType, String content) {
168      contentType = contentType.charset() != null
169          ? contentType
170          : MediaType.parse(contentType + "; charset=utf-8");
171      try {
172        byte[] bytes = content.getBytes(contentType.charset().name());
173        return create(contentType, bytes);
174      } catch (UnsupportedEncodingException e) {
175        throw new AssertionError();
176      }
177    }
178
179    /** Returns a new request body that transmits {@code content}. */
180    public static Body create(final MediaType contentType, final byte[] content) {
181      if (contentType == null) throw new NullPointerException("contentType == null");
182      if (content == null) throw new NullPointerException("content == null");
183
184      return new Body() {
185        @Override public MediaType contentType() {
186          return contentType;
187        }
188
189        @Override public long contentLength() {
190          return content.length;
191        }
192
193        @Override public void writeTo(BufferedSink sink) throws IOException {
194          sink.write(content);
195        }
196      };
197    }
198
199    /** Returns a new request body that transmits the content of {@code file}. */
200    public static Body create(final MediaType contentType, final File file) {
201      if (contentType == null) throw new NullPointerException("contentType == null");
202      if (file == null) throw new NullPointerException("content == null");
203
204      return new Body() {
205        @Override public MediaType contentType() {
206          return contentType;
207        }
208
209        @Override public long contentLength() {
210          return file.length();
211        }
212
213        @Override public void writeTo(BufferedSink sink) throws IOException {
214          long length = contentLength();
215          if (length == 0) return;
216
217          InputStream in = null;
218          try {
219            in = new FileInputStream(file);
220            byte[] buffer = new byte[(int) Math.min(8192, length)];
221            for (int c; (c = in.read(buffer)) != -1; ) {
222              sink.write(buffer, 0, c);
223            }
224          } finally {
225            Util.closeQuietly(in);
226          }
227        }
228      };
229    }
230  }
231
232  public static class Builder {
233    private URL url;
234    private String method;
235    private Headers.Builder headers;
236    private Body body;
237    private Object tag;
238
239    public Builder() {
240      this.method = "GET";
241      this.headers = new Headers.Builder();
242    }
243
244    private Builder(Request request) {
245      this.url = request.url;
246      this.method = request.method;
247      this.body = request.body;
248      this.tag = request.tag;
249      this.headers = request.headers.newBuilder();
250    }
251
252    public Builder url(String url) {
253      try {
254        return url(new URL(url));
255      } catch (MalformedURLException e) {
256        throw new IllegalArgumentException("Malformed URL: " + url);
257      }
258    }
259
260    public Builder url(URL url) {
261      if (url == null) throw new IllegalArgumentException("url == null");
262      this.url = url;
263      return this;
264    }
265
266    /**
267     * Sets the header named {@code name} to {@code value}. If this request
268     * already has any headers with that name, they are all replaced.
269     */
270    public Builder header(String name, String value) {
271      headers.set(name, value);
272      return this;
273    }
274
275    /**
276     * Adds a header with {@code name} and {@code value}. Prefer this method for
277     * multiply-valued headers like "Cookie".
278     */
279    public Builder addHeader(String name, String value) {
280      headers.add(name, value);
281      return this;
282    }
283
284    public Builder removeHeader(String name) {
285      headers.removeAll(name);
286      return this;
287    }
288
289    /** Removes all headers on this builder and adds {@code headers}. */
290    public Builder headers(Headers headers) {
291      this.headers = headers.newBuilder();
292      return this;
293    }
294
295    public Builder setUserAgent(String userAgent) {
296      return header("User-Agent", userAgent);
297    }
298
299    public Builder get() {
300      return method("GET", null);
301    }
302
303    public Builder head() {
304      return method("HEAD", null);
305    }
306
307    public Builder post(Body body) {
308      return method("POST", body);
309    }
310
311    public Builder put(Body body) {
312      return method("PUT", body);
313    }
314
315    public Builder method(String method, Body body) {
316      if (method == null || method.length() == 0) {
317        throw new IllegalArgumentException("method == null || method.length() == 0");
318      }
319      this.method = method;
320      this.body = body;
321      return this;
322    }
323
324    /**
325     * Attaches {@code tag} to the request. It can be used later to cancel the
326     * request. If the tag is unspecified or null, the request is canceled by
327     * using the request itself as the tag.
328     */
329    public Builder tag(Object tag) {
330      this.tag = tag;
331      return this;
332    }
333
334    public Request build() {
335      if (url == null) throw new IllegalStateException("url == null");
336      return new Request(this);
337    }
338  }
339}
340