From 449cfaa4b91ed28ce6928a400272d5b4542d13b7 Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Sun, 2 Feb 2014 23:28:51 -0800 Subject: [PATCH] Add support for AppEngine's URLFetchService. --- pom.xml | 6 + retrofit/pom.xml | 19 +++ retrofit/src/main/java/retrofit/Platform.java | 17 +++ .../retrofit/appengine/UrlFetchClient.java | 112 +++++++++++++++ .../src/test/java/retrofit/TestingUtils.java | 7 +- .../appengine/UrlFetchClientTest.java | 131 ++++++++++++++++++ .../retrofit/client/ApacheClientTest.java | 15 +- 7 files changed, 295 insertions(+), 12 deletions(-) create mode 100644 retrofit/src/main/java/retrofit/appengine/UrlFetchClient.java create mode 100644 retrofit/src/test/java/retrofit/appengine/UrlFetchClientTest.java diff --git a/pom.xml b/pom.xml index e5c02f88d7..2ac4e7298c 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,7 @@ 2.2.4 1.3.0 0.16.1 + 1.8.9 2.5.0 @@ -111,6 +112,11 @@ rxjava-core ${rxjava.version} + + com.google.appengine + appengine-api-1.0-sdk + ${appengine.version} + com.google.protobuf diff --git a/retrofit/pom.xml b/retrofit/pom.xml index 7cbf095770..c964f9b48f 100644 --- a/retrofit/pom.xml +++ b/retrofit/pom.xml @@ -18,6 +18,7 @@ com.google.code.gson gson + com.google.android android @@ -33,6 +34,11 @@ rxjava-core true + + com.google.appengine + appengine-api-1.0-sdk + true + junit @@ -55,4 +61,17 @@ test + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + -proc:none + + + + diff --git a/retrofit/src/main/java/retrofit/Platform.java b/retrofit/src/main/java/retrofit/Platform.java index 2276419408..3e76c3dbe4 100644 --- a/retrofit/src/main/java/retrofit/Platform.java +++ b/retrofit/src/main/java/retrofit/Platform.java @@ -24,6 +24,7 @@ import retrofit.android.AndroidApacheClient; import retrofit.android.AndroidLog; import retrofit.android.MainThreadExecutor; +import retrofit.appengine.UrlFetchClient; import retrofit.client.Client; import retrofit.client.OkClient; import retrofit.client.UrlConnectionClient; @@ -50,6 +51,11 @@ private static Platform findPlatform() { } } catch (ClassNotFoundException ignored) { } + + if (System.getProperty("com.google.appengine.runtime.version") != null) { + return new AppEngine(); + } + return new Base(); } @@ -149,6 +155,17 @@ private static class Android extends Platform { } } + private static class AppEngine extends Base { + @Override Client.Provider defaultClient() { + final UrlFetchClient client = new UrlFetchClient(); + return new Client.Provider() { + @Override public Client get() { + return client; + } + }; + } + } + /** Determine whether or not OkHttp is present on the runtime classpath. */ private static boolean hasOkHttpOnClasspath() { try { diff --git a/retrofit/src/main/java/retrofit/appengine/UrlFetchClient.java b/retrofit/src/main/java/retrofit/appengine/UrlFetchClient.java new file mode 100644 index 0000000000..fd25039c34 --- /dev/null +++ b/retrofit/src/main/java/retrofit/appengine/UrlFetchClient.java @@ -0,0 +1,112 @@ +package retrofit.appengine; + +import com.google.appengine.api.urlfetch.HTTPHeader; +import com.google.appengine.api.urlfetch.HTTPMethod; +import com.google.appengine.api.urlfetch.HTTPRequest; +import com.google.appengine.api.urlfetch.HTTPResponse; +import com.google.appengine.api.urlfetch.URLFetchService; +import com.google.appengine.api.urlfetch.URLFetchServiceFactory; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import retrofit.client.Client; +import retrofit.client.Header; +import retrofit.client.Request; +import retrofit.client.Response; +import retrofit.mime.TypedByteArray; +import retrofit.mime.TypedOutput; + +/** A {@link Client} for Google AppEngine's which uses its {@link URLFetchService}. */ +public class UrlFetchClient implements Client { + private static HTTPMethod getHttpMethod(String method) { + if ("GET".equals(method)) { + return HTTPMethod.GET; + } else if ("POST".equals(method)) { + return HTTPMethod.POST; + } else if ("PATCH".equals(method)) { + return HTTPMethod.PATCH; + } else if ("PUT".equals(method)) { + return HTTPMethod.PUT; + } else if ("DELETE".equals(method)) { + return HTTPMethod.DELETE; + } else if ("HEAD".equals(method)) { + return HTTPMethod.HEAD; + } else { + throw new IllegalStateException("Illegal HTTP method: " + method); + } + } + + private final URLFetchService urlFetchService; + + public UrlFetchClient() { + this(URLFetchServiceFactory.getURLFetchService()); + } + + public UrlFetchClient(URLFetchService urlFetchService) { + this.urlFetchService = urlFetchService; + } + + @Override public Response execute(Request request) throws IOException { + HTTPRequest fetchRequest = createRequest(request); + HTTPResponse fetchResponse = execute(urlFetchService, fetchRequest); + return parseResponse(fetchResponse); + } + + /** Execute the specified {@code request} using the provided {@code urlFetchService}. */ + protected HTTPResponse execute(URLFetchService urlFetchService, HTTPRequest request) + throws IOException { + return urlFetchService.fetch(request); + } + + static HTTPRequest createRequest(Request request) throws IOException { + HTTPMethod httpMethod = getHttpMethod(request.getMethod()); + URL url = new URL(request.getUrl()); + HTTPRequest fetchRequest = new HTTPRequest(url, httpMethod); + + for (Header header : request.getHeaders()) { + fetchRequest.addHeader(new HTTPHeader(header.getName(), header.getValue())); + } + + TypedOutput body = request.getBody(); + if (body != null) { + fetchRequest.setHeader(new HTTPHeader("Content-Type", body.mimeType())); + long length = body.length(); + if (length != -1) { + fetchRequest.setHeader(new HTTPHeader("Content-Length", String.valueOf(length))); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + body.writeTo(baos); + fetchRequest.setPayload(baos.toByteArray()); + } + + return fetchRequest; + } + + static Response parseResponse(HTTPResponse response) { + String url = response.getFinalUrl().toString(); + int status = response.getResponseCode(); + + List fetchHeaders = response.getHeaders(); + List
headers = new ArrayList
(fetchHeaders.size()); + String contentType = "application/octet-stream"; + for (HTTPHeader fetchHeader : fetchHeaders) { + String name = fetchHeader.getName(); + String value = fetchHeader.getValue(); + if ("Content-Type".equalsIgnoreCase(name)) { + contentType = value; + } + headers.add(new Header(name, value)); + } + + TypedByteArray body = null; + byte[] fetchBody = response.getContent(); + if (fetchBody != null) { + body = new TypedByteArray(contentType, fetchBody); + } + + return new Response(url, status, "", headers, body); + } +} diff --git a/retrofit/src/test/java/retrofit/TestingUtils.java b/retrofit/src/test/java/retrofit/TestingUtils.java index 709b1bcb5c..a4a031fe7a 100644 --- a/retrofit/src/test/java/retrofit/TestingUtils.java +++ b/retrofit/src/test/java/retrofit/TestingUtils.java @@ -1,6 +1,7 @@ // Copyright 2013 Square, Inc. package retrofit; +import java.io.IOException; import java.lang.reflect.Method; import java.util.Map; import retrofit.mime.MultipartTypedOutput; @@ -26,11 +27,7 @@ public static TypedOutput createMultipart(Map parts) { return typedOutput; } - public static void assertMultipart(TypedOutput typedOutput) { - assertThat(typedOutput).isInstanceOf(MultipartTypedOutput.class); - } - - public static void assertBytes(byte[] bytes, String expected) throws Exception { + public static void assertBytes(byte[] bytes, String expected) throws IOException { assertThat(new String(bytes, "UTF-8")).isEqualTo(expected); } } diff --git a/retrofit/src/test/java/retrofit/appengine/UrlFetchClientTest.java b/retrofit/src/test/java/retrofit/appengine/UrlFetchClientTest.java new file mode 100644 index 0000000000..76b942a248 --- /dev/null +++ b/retrofit/src/test/java/retrofit/appengine/UrlFetchClientTest.java @@ -0,0 +1,131 @@ +// Copyright 2014 Square, Inc. +package retrofit.appengine; + +import com.google.appengine.api.urlfetch.HTTPHeader; +import com.google.appengine.api.urlfetch.HTTPRequest; +import com.google.appengine.api.urlfetch.HTTPResponse; +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import retrofit.TestingUtils; +import retrofit.client.Header; +import retrofit.client.Request; +import retrofit.client.Response; +import retrofit.mime.TypedOutput; +import retrofit.mime.TypedString; + +import static com.google.appengine.api.urlfetch.HTTPMethod.GET; +import static com.google.appengine.api.urlfetch.HTTPMethod.POST; +import static java.util.Arrays.asList; +import static org.fest.assertions.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static retrofit.TestingUtils.assertBytes; + +public class UrlFetchClientTest { + private static final String HOST = "http://example.com"; + + @Test public void get() throws IOException { + Request request = new Request("GET", HOST + "/foo/bar/?kit=kat", null, null); + HTTPRequest fetchRequest = UrlFetchClient.createRequest(request); + + assertThat(fetchRequest.getMethod()).isEqualTo(GET); + assertThat(fetchRequest.getURL().toString()).isEqualTo(HOST + "/foo/bar/?kit=kat"); + assertThat(fetchRequest.getHeaders()).isEmpty(); + assertThat(fetchRequest.getPayload()).isNull(); + } + + @Test public void post() throws IOException { + TypedString body = new TypedString("hi"); + Request request = new Request("POST", HOST + "/foo/bar/", null, body); + HTTPRequest fetchRequest = UrlFetchClient.createRequest(request); + + assertThat(fetchRequest.getMethod()).isEqualTo(POST); + assertThat(fetchRequest.getURL().toString()).isEqualTo(HOST + "/foo/bar/"); + List fetchHeaders = fetchRequest.getHeaders(); + assertThat(fetchHeaders).hasSize(2); + assertHeader(fetchHeaders.get(0), "Content-Type", "text/plain; charset=UTF-8"); + assertHeader(fetchHeaders.get(1), "Content-Length", "2"); + assertBytes(fetchRequest.getPayload(), "hi"); + } + + @Test public void multipart() throws IOException { + Map bodyParams = new LinkedHashMap(); + bodyParams.put("foo", new TypedString("bar")); + bodyParams.put("ping", new TypedString("pong")); + TypedOutput body = TestingUtils.createMultipart(bodyParams); + Request request = new Request("POST", HOST + "/that/", null, body); + HTTPRequest fetchRequest = UrlFetchClient.createRequest(request); + + assertThat(fetchRequest.getMethod()).isEqualTo(POST); + assertThat(fetchRequest.getURL().toString()).isEqualTo(HOST + "/that/"); + List fetchHeaders = fetchRequest.getHeaders(); + assertThat(fetchHeaders).hasSize(2); + HTTPHeader headerZero = fetchHeaders.get(0); + assertThat(headerZero.getName()).isEqualTo("Content-Type"); + assertThat(headerZero.getValue()).startsWith("multipart/form-data; boundary="); + assertHeader(fetchHeaders.get(1), "Content-Length", String.valueOf(body.length())); + assertThat(fetchRequest.getPayload()).isNotEmpty(); + } + + @Test public void headers() throws IOException { + List
headers = new ArrayList
(); + headers.add(new Header("kit", "kat")); + headers.add(new Header("foo", "bar")); + Request request = new Request("GET", HOST + "/this/", headers, null); + HTTPRequest fetchRequest = UrlFetchClient.createRequest(request); + + List fetchHeaders = fetchRequest.getHeaders(); + assertThat(fetchHeaders).hasSize(2); + assertHeader(fetchHeaders.get(0), "kit", "kat"); + assertHeader(fetchHeaders.get(1), "foo", "bar"); + } + + @Test public void response() throws Exception { + HTTPResponse fetchResponse = mock(HTTPResponse.class); + when(fetchResponse.getHeaders()).thenReturn( + asList(new HTTPHeader("foo", "bar"), new HTTPHeader("kit", "kat"), + new HTTPHeader("Content-Type", "text/plain"))); + when(fetchResponse.getContent()).thenReturn("hello".getBytes("UTF-8")); + when(fetchResponse.getFinalUrl()).thenReturn(new URL(HOST + "/foo/bar/")); + when(fetchResponse.getResponseCode()).thenReturn(200); + + Response response = UrlFetchClient.parseResponse(fetchResponse); + + assertThat(response.getUrl()).isEqualTo(HOST + "/foo/bar/"); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getReason()).isEqualTo(""); + assertThat(response.getHeaders()).hasSize(3) // + .containsOnly(new Header("foo", "bar"), new Header("kit", "kat"), + new Header("Content-Type", "text/plain")); + assertBytes(ByteStreams.toByteArray(response.getBody().in()), "hello"); + } + + @Test public void emptyResponse() throws Exception { + HTTPResponse fetchResponse = mock(HTTPResponse.class); + when(fetchResponse.getHeaders()).thenReturn( + asList(new HTTPHeader("foo", "bar"), new HTTPHeader("kit", "kat"))); + when(fetchResponse.getContent()).thenReturn(null); + when(fetchResponse.getFinalUrl()).thenReturn(new URL(HOST + "/foo/bar/")); + when(fetchResponse.getResponseCode()).thenReturn(200); + + Response response = UrlFetchClient.parseResponse(fetchResponse); + + assertThat(response.getUrl()).isEqualTo(HOST + "/foo/bar/"); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getReason()).isEqualTo(""); + assertThat(response.getHeaders()).hasSize(2) // + .containsExactly(new Header("foo", "bar"), new Header("kit", "kat")); + assertThat(response.getBody()).isNull(); + } + + private static void assertHeader(HTTPHeader header, String name, String value) { + assertThat(header.getName()).isEqualTo(name); + assertThat(header.getValue()).isEqualTo(value); + } +} diff --git a/retrofit/src/test/java/retrofit/client/ApacheClientTest.java b/retrofit/src/test/java/retrofit/client/ApacheClientTest.java index 74cc4c3bb8..59dd74a07c 100644 --- a/retrofit/src/test/java/retrofit/client/ApacheClientTest.java +++ b/retrofit/src/test/java/retrofit/client/ApacheClientTest.java @@ -2,6 +2,7 @@ package retrofit.client; import com.google.common.io.ByteStreams; +import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -16,12 +17,12 @@ import org.apache.http.message.BasicStatusLine; import org.junit.Test; import retrofit.TestingUtils; +import retrofit.mime.MultipartTypedOutput; import retrofit.mime.TypedOutput; import retrofit.mime.TypedString; import static org.fest.assertions.api.Assertions.assertThat; import static retrofit.TestingUtils.assertBytes; -import static retrofit.TestingUtils.assertMultipart; import static retrofit.client.ApacheClient.TypedOutputEntity; public class ApacheClientTest { @@ -41,14 +42,14 @@ public class ApacheClientTest { } } - @Test public void post() throws Exception { + @Test public void post() throws IOException { TypedString body = new TypedString("hi"); Request request = new Request("POST", HOST + "/foo/bar/", null, body); HttpUriRequest apacheRequest = ApacheClient.createRequest(request); assertThat(apacheRequest.getMethod()).isEqualTo("POST"); assertThat(apacheRequest.getURI().toString()).isEqualTo(HOST + "/foo/bar/"); - assertThat(apacheRequest.getAllHeaders()).hasSize(0); + assertThat(apacheRequest.getAllHeaders()).isEmpty(); assertThat(apacheRequest).isInstanceOf(HttpEntityEnclosingRequest.class); HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) apacheRequest; @@ -68,12 +69,12 @@ public class ApacheClientTest { assertThat(apacheRequest.getMethod()).isEqualTo("POST"); assertThat(apacheRequest.getURI().toString()).isEqualTo(HOST + "/that/"); - assertThat(apacheRequest.getAllHeaders()).hasSize(0); + assertThat(apacheRequest.getAllHeaders()).isEmpty(); assertThat(apacheRequest).isInstanceOf(HttpEntityEnclosingRequest.class); HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) apacheRequest; TypedOutputEntity entity = (TypedOutputEntity) entityRequest.getEntity(); - assertMultipart(entity.typedOutput); + assertThat(entity.typedOutput).isInstanceOf(MultipartTypedOutput.class); // TODO test more? } @@ -93,7 +94,7 @@ public class ApacheClientTest { assertThat(foo.getValue()).isEqualTo("bar"); } - @Test public void response() throws Exception { + @Test public void response() throws IOException { StatusLine statusLine = new BasicStatusLine(HttpVersion.HTTP_1_1, 200, "OK"); HttpResponse apacheResponse = new BasicHttpResponse(statusLine); apacheResponse.setEntity(new TypedOutputEntity(new TypedString("hello"))); @@ -111,7 +112,7 @@ public class ApacheClientTest { assertBytes(ByteStreams.toByteArray(response.getBody().in()), "hello"); } - @Test public void emptyResponse() throws Exception { + @Test public void emptyResponse() throws IOException { StatusLine statusLine = new BasicStatusLine(HttpVersion.HTTP_1_1, 200, "OK"); HttpResponse apacheResponse = new BasicHttpResponse(statusLine); apacheResponse.addHeader("foo", "bar");