diff --git a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java b/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java new file mode 100644 index 000000000..924ace5c9 --- /dev/null +++ b/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java @@ -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()); + private final Map> enqueuedJobs = new LinkedHashMap>(); + + public synchronized void enqueue( + HttpURLConnection connection, Request request, Response.Receiver responseReceiver) { + Job job = new Job(connection, request, responseReceiver); + List jobsForTag = enqueuedJobs.get(request.tag()); + if (jobsForTag == null) { + jobsForTag = new ArrayList(2); + enqueuedJobs.put(request.tag(), jobsForTag); + } + jobsForTag.add(job); + executorService.execute(job); + } + + public synchronized void cancel(Object tag) { + List jobs = enqueuedJobs.remove(tag); + if (jobs == null) return; + for (Job job : jobs) { + executorService.remove(job); + } + } + + private synchronized void finished(Job job) { + List 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; + } + } +} diff --git a/okhttp/src/main/java/com/squareup/okhttp/Failure.java b/okhttp/src/main/java/com/squareup/okhttp/Failure.java new file mode 100644 index 000000000..b40133b60 --- /dev/null +++ b/okhttp/src/main/java/com/squareup/okhttp/Failure.java @@ -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. + * + *

Warning: Experimental OkHttp 2.0 API

+ * 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); + } + } +} diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java index 4e147f2a5..921b61aaa 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java +++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java @@ -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()); @@ -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(); diff --git a/okhttp/src/main/java/com/squareup/okhttp/Request.java b/okhttp/src/main/java/com/squareup/okhttp/Request.java new file mode 100644 index 000000000..73617716f --- /dev/null +++ b/okhttp/src/main/java/com/squareup/okhttp/Request.java @@ -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. + * + *

Warning: Experimental OkHttp 2.0 API

+ * 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 headers(String name) { + return headers.values(name); + } + + public Set 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); + } + } +} diff --git a/okhttp/src/main/java/com/squareup/okhttp/Response.java b/okhttp/src/main/java/com/squareup/okhttp/Response.java new file mode 100644 index 000000000..4896a388b --- /dev/null +++ b/okhttp/src/main/java/com/squareup/okhttp/Response.java @@ -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. + * + *

Warning: Experimental OkHttp 2.0 API

+ * 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 + * not the same request instance provided to the HTTP client: + *
    + *
  • 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. + *
  • 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. + *
+ */ + 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 headers(String name) { + return headers.values(name); + } + + public Set 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); + } + } +} diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java index f8c5e9a98..b4cf9d68a 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java @@ -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); } } diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java index eba887ec0..e5abd2cfb 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java @@ -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 names() { + TreeSet result = new TreeSet(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 values(String name) { + List result = null; + for (int i = 0; i < length(); i++) { + if (name.equalsIgnoreCase(getFieldName(i))) { + if (result == null) result = new ArrayList(2); + result.add(getValue(i)); + } + } + return result != null + ? Collections.unmodifiableList(result) + : Collections.emptyList(); + } + /** @param fieldNames a case-insensitive set of HTTP header field names. */ public RawHeaders getAll(Set fieldNames) { RawHeaders result = new RawHeaders(); diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/AsyncApiTest.java b/okhttp/src/test/java/com/squareup/okhttp/internal/AsyncApiTest.java new file mode 100644 index 000000000..6eefcf186 --- /dev/null +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/AsyncApiTest.java @@ -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")); + } +} diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/RecordedResponse.java b/okhttp/src/test/java/com/squareup/okhttp/internal/RecordedResponse.java new file mode 100644 index 000000000..388a27d1d --- /dev/null +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/RecordedResponse.java @@ -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 actualHeaders = new ArrayList(); + 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; + } +} diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/RecordingReceiver.java b/okhttp/src/test/java/com/squareup/okhttp/internal/RecordingReceiver.java new file mode 100644 index 000000000..58cd20531 --- /dev/null +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/RecordingReceiver.java @@ -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 responses = new ArrayList(); + + @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); + } +}