mirror of
https://github.com/square/okhttp.git
synced 2026-01-25 16:01:38 +03:00
First step in an async HTTP API.
This is very experimental right now but it sets us up for a full asynchronous API. The current implementation builds an async API over our existing synchronous API. Future refactorings to the internals should promote the async API throughout the codebase, particularly in SPDY. That way we can have more requests in flight than threads processing those requests.
This commit is contained in:
138
okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
Normal file
138
okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
class Dispatcher {
|
||||
// TODO: thread pool size should be configurable; possibly configurable per host.
|
||||
private final ThreadPoolExecutor executorService = new ThreadPoolExecutor(
|
||||
8, 8, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
|
||||
private final Map<Object, List<Job>> enqueuedJobs = new LinkedHashMap<Object, List<Job>>();
|
||||
|
||||
public synchronized void enqueue(
|
||||
HttpURLConnection connection, Request request, Response.Receiver responseReceiver) {
|
||||
Job job = new Job(connection, request, responseReceiver);
|
||||
List<Job> jobsForTag = enqueuedJobs.get(request.tag());
|
||||
if (jobsForTag == null) {
|
||||
jobsForTag = new ArrayList<Job>(2);
|
||||
enqueuedJobs.put(request.tag(), jobsForTag);
|
||||
}
|
||||
jobsForTag.add(job);
|
||||
executorService.execute(job);
|
||||
}
|
||||
|
||||
public synchronized void cancel(Object tag) {
|
||||
List<Job> jobs = enqueuedJobs.remove(tag);
|
||||
if (jobs == null) return;
|
||||
for (Job job : jobs) {
|
||||
executorService.remove(job);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void finished(Job job) {
|
||||
List<Job> jobs = enqueuedJobs.get(job.request.tag());
|
||||
if (jobs != null) jobs.remove(job);
|
||||
}
|
||||
|
||||
public class Job implements Runnable {
|
||||
private final HttpURLConnection connection;
|
||||
private final Request request;
|
||||
private final Response.Receiver responseReceiver;
|
||||
|
||||
public Job(HttpURLConnection connection, Request request, Response.Receiver responseReceiver) {
|
||||
this.connection = connection;
|
||||
this.request = request;
|
||||
this.responseReceiver = responseReceiver;
|
||||
}
|
||||
|
||||
@Override public void run() {
|
||||
try {
|
||||
sendRequest();
|
||||
Response response = readResponse();
|
||||
responseReceiver.onResponse(response);
|
||||
} catch (IOException e) {
|
||||
responseReceiver.onFailure(new Failure.Builder()
|
||||
.request(request)
|
||||
.exception(e)
|
||||
.build());
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
finished(this);
|
||||
}
|
||||
}
|
||||
|
||||
private HttpURLConnection sendRequest()
|
||||
throws IOException {
|
||||
for (int i = 0; i < request.headerCount(); i++) {
|
||||
connection.addRequestProperty(request.headerName(i), request.headerValue(i));
|
||||
}
|
||||
Request.Body body = request.body();
|
||||
if (body != null) {
|
||||
connection.setDoOutput(true);
|
||||
body.writeTo(connection.getOutputStream());
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
private Response readResponse() throws IOException {
|
||||
int responseCode = connection.getResponseCode();
|
||||
Response.Builder responseBuilder = new Response.Builder(request, responseCode);
|
||||
|
||||
for (int i = 0; true; i++) {
|
||||
String name = connection.getHeaderFieldKey(i);
|
||||
if (name == null) break;
|
||||
String value = connection.getHeaderField(i);
|
||||
responseBuilder.addHeader(name, value);
|
||||
}
|
||||
|
||||
responseBuilder.body(new RealResponseBody(connection, connection.getInputStream()));
|
||||
// TODO: set redirectedBy
|
||||
return responseBuilder.build();
|
||||
}
|
||||
}
|
||||
|
||||
static class RealResponseBody extends Response.Body {
|
||||
private final HttpURLConnection connection;
|
||||
private final InputStream in;
|
||||
|
||||
RealResponseBody(HttpURLConnection connection, InputStream in) {
|
||||
this.connection = connection;
|
||||
this.in = in;
|
||||
}
|
||||
|
||||
@Override public String contentType() {
|
||||
return connection.getHeaderField("Content-Type");
|
||||
}
|
||||
|
||||
@Override public long contentLength() {
|
||||
return connection.getContentLength(); // TODO: getContentLengthLong
|
||||
}
|
||||
|
||||
@Override public InputStream byteStream() throws IOException {
|
||||
return in;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
okhttp/src/main/java/com/squareup/okhttp/Failure.java
Normal file
59
okhttp/src/main/java/com/squareup/okhttp/Failure.java
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp;
|
||||
|
||||
/**
|
||||
* A failure attempting to retrieve an HTTP response.
|
||||
*
|
||||
* <h3>Warning: Experimental OkHttp 2.0 API</h3>
|
||||
* This class is in beta. APIs are subject to change!
|
||||
*/
|
||||
public class Failure {
|
||||
private final Request request;
|
||||
private final Throwable exception;
|
||||
|
||||
private Failure(Builder builder) {
|
||||
this.request = builder.request;
|
||||
this.exception = builder.exception;
|
||||
}
|
||||
|
||||
public Request request() {
|
||||
return request;
|
||||
}
|
||||
|
||||
public Throwable exception() {
|
||||
return exception;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private Request request;
|
||||
private Throwable exception;
|
||||
|
||||
public Builder request(Request request) {
|
||||
this.request = request;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder exception(Throwable exception) {
|
||||
this.exception = exception;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Failure build() {
|
||||
return new Failure(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
|
||||
private boolean followProtocolRedirects = true;
|
||||
private int connectTimeout;
|
||||
private int readTimeout;
|
||||
private Dispatcher dispatcher = new Dispatcher();
|
||||
|
||||
public OkHttpClient() {
|
||||
this.failedRoutes = Collections.synchronizedSet(new LinkedHashSet<Route>());
|
||||
@@ -311,6 +312,24 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
|
||||
return transports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules {@code request} to be executed.
|
||||
*/
|
||||
public void enqueue(Request request, Response.Receiver responseReceiver) {
|
||||
// Create the HttpURLConnection immediately so the enqueued job gets the current settings of
|
||||
// this client. Otherwise changes to this client (socket factory, redirect policy, etc.) may
|
||||
// incorrectly be reflected in the request when it is dispatched later.
|
||||
dispatcher.enqueue(open(request.url()), request, responseReceiver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all scheduled tasks tagged with {@code tag}. Requests that are already
|
||||
* in flight might not be canceled.
|
||||
*/
|
||||
public void cancel(Object tag) {
|
||||
dispatcher.cancel(tag);
|
||||
}
|
||||
|
||||
public HttpURLConnection open(URL url) {
|
||||
String protocol = url.getProtocol();
|
||||
OkHttpClient copy = copyWithDefaults();
|
||||
|
||||
193
okhttp/src/main/java/com/squareup/okhttp/Request.java
Normal file
193
okhttp/src/main/java/com/squareup/okhttp/Request.java
Normal file
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp;
|
||||
|
||||
import com.squareup.okhttp.internal.http.RawHeaders;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* An HTTP request. Instances of this class are immutable if their {@link #body}
|
||||
* is null or itself immutable.
|
||||
*
|
||||
* <h3>Warning: Experimental OkHttp 2.0 API</h3>
|
||||
* This class is in beta. APIs are subject to change!
|
||||
*/
|
||||
public final class Request {
|
||||
private final URL url;
|
||||
private final String method;
|
||||
private final RawHeaders headers;
|
||||
private final Body body;
|
||||
private final Object tag;
|
||||
|
||||
private Request(Builder builder) {
|
||||
this.url = builder.url;
|
||||
this.method = builder.method;
|
||||
this.headers = new RawHeaders(builder.headers);
|
||||
this.body = builder.body;
|
||||
this.tag = builder.tag != null ? builder.tag : this;
|
||||
}
|
||||
|
||||
public URL url() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String urlString() {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
public String method() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public String header(String name) {
|
||||
return headers.get(name);
|
||||
}
|
||||
|
||||
public List<String> headers(String name) {
|
||||
return headers.values(name);
|
||||
}
|
||||
|
||||
public Set<String> headerNames() {
|
||||
return headers.names();
|
||||
}
|
||||
|
||||
public int headerCount() {
|
||||
return headers.length();
|
||||
}
|
||||
|
||||
public String headerName(int index) {
|
||||
return headers.getFieldName(index);
|
||||
}
|
||||
|
||||
public String headerValue(int index) {
|
||||
return headers.getValue(index);
|
||||
}
|
||||
|
||||
public Body body() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public Object tag() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
public abstract class Body {
|
||||
/** Returns the Content-Type header for this body, or null if the content type is unknown. */
|
||||
public String contentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns the number of bytes in this body, or -1 if that count is unknown. */
|
||||
public long contentLength() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
public abstract void writeTo(OutputStream out) throws IOException;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private URL url;
|
||||
private String method = "GET";
|
||||
private final RawHeaders headers = new RawHeaders();
|
||||
private Body body;
|
||||
private Object tag;
|
||||
|
||||
public Builder(String url) {
|
||||
url(url);
|
||||
}
|
||||
|
||||
public Builder(URL url) {
|
||||
url(url);
|
||||
}
|
||||
|
||||
public Builder url(String url) {
|
||||
try {
|
||||
this.url = new URL(url);
|
||||
return this;
|
||||
} catch (MalformedURLException e) {
|
||||
throw new IllegalArgumentException("Malformed URL: " + url);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder url(URL url) {
|
||||
if (url == null) throw new IllegalStateException("url == null");
|
||||
this.url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the header named {@code name} to {@code value}. If this request
|
||||
* already has any headers with that name, they are all replaced.
|
||||
*/
|
||||
public Builder header(String name, String value) {
|
||||
headers.set(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a header with {@code name} and {@code value}. Prefer this method for
|
||||
* multiply-valued headers like "Cookie".
|
||||
*/
|
||||
public Builder addHeader(String name, String value) {
|
||||
headers.add(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder get() {
|
||||
return method("GET", null);
|
||||
}
|
||||
|
||||
public Builder head() {
|
||||
return method("HEAD", null);
|
||||
}
|
||||
|
||||
public Builder post(Body body) {
|
||||
return method("POST", body);
|
||||
}
|
||||
|
||||
public Builder put(Body body) {
|
||||
return method("PUT", body);
|
||||
}
|
||||
|
||||
public Builder method(String method, Body body) {
|
||||
if (method == null || method.length() == 0) {
|
||||
throw new IllegalArgumentException("method == null || method.length() == 0");
|
||||
}
|
||||
this.method = method;
|
||||
this.body = body;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches {@code tag} to the request. It can be used later to cancel the
|
||||
* request. If the tag is unspecified or null, the request is canceled by
|
||||
* using the request itself as the tag.
|
||||
*/
|
||||
public Builder tag(Object tag) {
|
||||
this.tag = tag;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Request build() {
|
||||
return new Request(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
217
okhttp/src/main/java/com/squareup/okhttp/Response.java
Normal file
217
okhttp/src/main/java/com/squareup/okhttp/Response.java
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp;
|
||||
|
||||
import com.squareup.okhttp.internal.Util;
|
||||
import com.squareup.okhttp.internal.http.RawHeaders;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* An HTTP response. Instances of this class are not immutable: the response
|
||||
* body is a one-shot value that may be consumed only once. All other properties
|
||||
* are immutable.
|
||||
*
|
||||
* <h3>Warning: Experimental OkHttp 2.0 API</h3>
|
||||
* This class is in beta. APIs are subject to change!
|
||||
*/
|
||||
public final class Response {
|
||||
private final Request request;
|
||||
private final int code;
|
||||
private final RawHeaders headers;
|
||||
private final Body body;
|
||||
private final Response redirectedBy;
|
||||
|
||||
private Response(Builder builder) {
|
||||
this.request = builder.request;
|
||||
this.code = builder.code;
|
||||
this.headers = new RawHeaders(builder.headers);
|
||||
this.body = builder.body;
|
||||
this.redirectedBy = builder.redirectedBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* The wire-level request that initiated this HTTP response. This is usually
|
||||
* <strong>not</strong> the same request instance provided to the HTTP client:
|
||||
* <ul>
|
||||
* <li>It may be transformed by the HTTP client. For example, the client
|
||||
* may have added its own {@code Content-Encoding} header to enable
|
||||
* response compression.
|
||||
* <li>It may be the request generated in response to an HTTP redirect.
|
||||
* In this case the request URL may be different than the initial
|
||||
* request URL.
|
||||
* </ul>
|
||||
*/
|
||||
public Request request() {
|
||||
return request;
|
||||
}
|
||||
|
||||
public int code() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String header(String name) {
|
||||
return header(name, null);
|
||||
}
|
||||
|
||||
public String header(String name, String defaultValue) {
|
||||
String result = headers.get(name);
|
||||
return result != null ? result : defaultValue;
|
||||
}
|
||||
|
||||
public List<String> headers(String name) {
|
||||
return headers.values(name);
|
||||
}
|
||||
|
||||
public Set<String> headerNames() {
|
||||
return headers.names();
|
||||
}
|
||||
|
||||
public int headerCount() {
|
||||
return headers.length();
|
||||
}
|
||||
|
||||
public String headerName(int index) {
|
||||
return headers.getFieldName(index);
|
||||
}
|
||||
|
||||
public String headerValue(int index) {
|
||||
return headers.getValue(index);
|
||||
}
|
||||
|
||||
public Body body() {
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response for the HTTP redirect that triggered this response, or
|
||||
* null if this response wasn't triggered by an automatic redirect. The body
|
||||
* of the returned response should not be read because it has already been
|
||||
* consumed by the redirecting client.
|
||||
*/
|
||||
public Response redirectedBy() {
|
||||
return redirectedBy;
|
||||
}
|
||||
|
||||
public abstract static class Body {
|
||||
public String contentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public long contentLength() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
public abstract InputStream byteStream() throws IOException;
|
||||
|
||||
public byte[] bytes() throws IOException {
|
||||
long contentLength = contentLength();
|
||||
if (contentLength > Integer.MAX_VALUE) {
|
||||
throw new IOException("Cannot buffer entire body for content length: " + contentLength);
|
||||
}
|
||||
|
||||
if (contentLength != -1) {
|
||||
byte[] content = new byte[(int) contentLength];
|
||||
InputStream in = byteStream();
|
||||
Util.readFully(in, content);
|
||||
if (in.read() != -1) throw new IOException("Content-Length and stream length disagree");
|
||||
return content;
|
||||
|
||||
} else {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
Util.copy(byteStream(), out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response bytes as a UTF-8 character stream. Do not call this
|
||||
* method if the response content is not a UTF-8 character stream.
|
||||
*/
|
||||
public Reader charStream() throws IOException {
|
||||
// TODO: parse content-type.
|
||||
return new InputStreamReader(byteStream(), "UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response bytes as a UTF-8 string. Do not call this method if
|
||||
* the response content is not a UTF-8 character stream.
|
||||
*/
|
||||
public String string() throws IOException {
|
||||
// TODO: parse content-type.
|
||||
return new String(bytes(), "UTF-8");
|
||||
}
|
||||
}
|
||||
|
||||
public interface Receiver {
|
||||
void onFailure(Failure failure);
|
||||
void onResponse(Response response) throws IOException;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final Request request;
|
||||
private final int code;
|
||||
private final RawHeaders headers = new RawHeaders();
|
||||
private Body body;
|
||||
private Response redirectedBy;
|
||||
|
||||
public Builder(Request request, int code) {
|
||||
if (request == null) throw new IllegalArgumentException("request == null");
|
||||
if (code <= 0) throw new IllegalArgumentException("code <= 0");
|
||||
this.request = request;
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the header named {@code name} to {@code value}. If this request
|
||||
* already has any headers with that name, they are all replaced.
|
||||
*/
|
||||
public Builder header(String name, String value) {
|
||||
headers.set(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a header with {@code name} and {@code value}. Prefer this method for
|
||||
* multiply-valued headers like "Set-Cookie".
|
||||
*/
|
||||
public Builder addHeader(String name, String value) {
|
||||
headers.add(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder body(Body body) {
|
||||
this.body = body;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder redirectedBy(Response redirectedBy) {
|
||||
this.redirectedBy = redirectedBy;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Response build() {
|
||||
if (request == null) throw new IllegalStateException("Response has no request.");
|
||||
if (code == -1) throw new IllegalStateException("Response has no code.");
|
||||
return new Response(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -606,8 +606,8 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
|
||||
// the list contains "http/1.1". We do this in a separate loop
|
||||
// to avoid modifying any state before we validate the input.
|
||||
boolean containsHttp = false;
|
||||
for (int i = 0; i < transports.length; ++i) {
|
||||
if ("http/1.1".equals(transports[i])) {
|
||||
for (String transport : transports) {
|
||||
if ("http/1.1".equals(transport)) {
|
||||
containsHttp = true;
|
||||
break;
|
||||
}
|
||||
@@ -620,13 +620,13 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
|
||||
transportsList.addAll(this.transports);
|
||||
}
|
||||
|
||||
for (int i = 0; i < transports.length; ++i) {
|
||||
if (transports[i].length() == 0) {
|
||||
for (String transport : transports) {
|
||||
if (transport.length() == 0) {
|
||||
throw new IllegalArgumentException("Transport list contains an empty transport");
|
||||
}
|
||||
|
||||
if (!transportsList.contains(transports[i])) {
|
||||
transportsList.add(transports[i]);
|
||||
if (!transportsList.contains(transport)) {
|
||||
transportsList.add(transport);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* The HTTP status and unparsed header fields of a single HTTP message. Values
|
||||
@@ -248,6 +249,15 @@ public final class RawHeaders {
|
||||
return namesAndValues.get(fieldNameIndex);
|
||||
}
|
||||
|
||||
/** Returns an immutable case-insensitive set of header names. */
|
||||
public Set<String> names() {
|
||||
TreeSet<String> result = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
|
||||
for (int i = 0; i < length(); i++) {
|
||||
result.add(getFieldName(i));
|
||||
}
|
||||
return Collections.unmodifiableSet(result);
|
||||
}
|
||||
|
||||
/** Returns the value at {@code index} or null if that is out of range. */
|
||||
public String getValue(int index) {
|
||||
int valueIndex = index * 2 + 1;
|
||||
@@ -267,6 +277,20 @@ public final class RawHeaders {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns an immutable list of the header values for {@code name}. */
|
||||
public List<String> values(String name) {
|
||||
List<String> result = null;
|
||||
for (int i = 0; i < length(); i++) {
|
||||
if (name.equalsIgnoreCase(getFieldName(i))) {
|
||||
if (result == null) result = new ArrayList<String>(2);
|
||||
result.add(getValue(i));
|
||||
}
|
||||
}
|
||||
return result != null
|
||||
? Collections.unmodifiableList(result)
|
||||
: Collections.<String>emptyList();
|
||||
}
|
||||
|
||||
/** @param fieldNames a case-insensitive set of HTTP header field names. */
|
||||
public RawHeaders getAll(Set<String> fieldNames) {
|
||||
RawHeaders result = new RawHeaders();
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp.internal;
|
||||
|
||||
import com.google.mockwebserver.MockResponse;
|
||||
import com.google.mockwebserver.MockWebServer;
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
import com.squareup.okhttp.Request;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class AsyncApiTest {
|
||||
private MockWebServer server = new MockWebServer();
|
||||
private OkHttpClient client = new OkHttpClient();
|
||||
private RecordingReceiver receiver = new RecordingReceiver();
|
||||
|
||||
@After public void tearDown() throws Exception {
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
@Test public void get() throws Exception {
|
||||
server.enqueue(new MockResponse()
|
||||
.setBody("abc")
|
||||
.addHeader("Content-Type: text/plain"));
|
||||
server.play();
|
||||
|
||||
Request request = new Request.Builder(server.getUrl("/"))
|
||||
.header("User-Agent", "AsyncApiTest")
|
||||
.build();
|
||||
client.enqueue(request, receiver);
|
||||
|
||||
receiver.await(request)
|
||||
.assertCode(200)
|
||||
.assertContainsHeaders("Content-Type: text/plain")
|
||||
.assertBody("abc");
|
||||
|
||||
assertTrue(server.takeRequest().getHeaders().contains("User-Agent: AsyncApiTest"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp.internal;
|
||||
|
||||
import com.squareup.okhttp.Failure;
|
||||
import com.squareup.okhttp.Request;
|
||||
import com.squareup.okhttp.Response;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
* A received response or failure recorded by the response recorder.
|
||||
*/
|
||||
public class RecordedResponse {
|
||||
public final Request request;
|
||||
public final Response response;
|
||||
public final String body;
|
||||
public final Failure failure;
|
||||
|
||||
RecordedResponse(Request request, Response response, String body, Failure failure) {
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
this.body = body;
|
||||
this.failure = failure;
|
||||
}
|
||||
|
||||
public RecordedResponse assertCode(int expectedCode) {
|
||||
assertEquals(expectedCode, response.code());
|
||||
return this;
|
||||
}
|
||||
|
||||
public RecordedResponse assertContainsHeaders(String... expectedHeaders) {
|
||||
List<String> actualHeaders = new ArrayList<String>();
|
||||
for (int i = 0; i < response.headerCount(); i++) {
|
||||
actualHeaders.add(response.headerName(i) + ": " + response.headerValue(i));
|
||||
}
|
||||
if (!actualHeaders.containsAll(Arrays.asList(expectedHeaders))) {
|
||||
fail("Expected: " + actualHeaders + "\nto contain: " + Arrays.toString(expectedHeaders));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public RecordedResponse assertBody(String expectedBody) {
|
||||
assertEquals(expectedBody, body);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp.internal;
|
||||
|
||||
import com.squareup.okhttp.Failure;
|
||||
import com.squareup.okhttp.Request;
|
||||
import com.squareup.okhttp.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Records received HTTP responses so they can be later retrieved by tests.
|
||||
*/
|
||||
public class RecordingReceiver implements Response.Receiver {
|
||||
public static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
|
||||
|
||||
private final List<RecordedResponse> responses = new ArrayList<RecordedResponse>();
|
||||
|
||||
@Override public synchronized void onFailure(Failure failure) {
|
||||
responses.add(new RecordedResponse(failure.request(), null, null, failure));
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
@Override public synchronized void onResponse(Response response) throws IOException {
|
||||
responses.add(new RecordedResponse(
|
||||
response.request(), response, response.body().string(), null));
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the recorded response triggered by {@code request}. Throws if the
|
||||
* response isn't enqueued before the timeout.
|
||||
*/
|
||||
public synchronized RecordedResponse await(Request request) throws Exception {
|
||||
long timeoutMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + TIMEOUT_MILLIS;
|
||||
while (true) {
|
||||
for (RecordedResponse recordedResponse : responses) {
|
||||
if (recordedResponse.request == request) {
|
||||
return recordedResponse;
|
||||
}
|
||||
}
|
||||
|
||||
long nowMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
|
||||
if (nowMillis >= timeoutMillis) break;
|
||||
wait(timeoutMillis - nowMillis);
|
||||
}
|
||||
|
||||
throw new AssertionError("Timed out waiting for response to " + request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user