Skip to content

Commit

Permalink
Add support for AppEngine's URLFetchService.
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeWharton committed Feb 21, 2014
1 parent 883a7c9 commit 449cfaa
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 12 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<gson.version>2.2.4</gson.version>
<okhttp.version>1.3.0</okhttp.version>
<rxjava.version>0.16.1</rxjava.version>
<appengine.version>1.8.9</appengine.version>

<!-- Converter Dependencies -->
<protobuf.version>2.5.0</protobuf.version>
Expand Down Expand Up @@ -111,6 +112,11 @@
<artifactId>rxjava-core</artifactId>
<version>${rxjava.version}</version>
</dependency>
<dependency>
<groupId>com.google.appengine</groupId>
<artifactId>appengine-api-1.0-sdk</artifactId>
<version>${appengine.version}</version>
</dependency>

<dependency>
<groupId>com.google.protobuf</groupId>
Expand Down
19 changes: 19 additions & 0 deletions retrofit/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>

<dependency>
<groupId>com.google.android</groupId>
<artifactId>android</artifactId>
Expand All @@ -33,6 +34,11 @@
<artifactId>rxjava-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.appengine</groupId>
<artifactId>appengine-api-1.0-sdk</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>junit</groupId>
Expand All @@ -55,4 +61,17 @@
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!-- The AppEngine dependency has an annotation processor we don't want to run. -->
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</plugin>
</plugins>
</build>
</project>
17 changes: 17 additions & 0 deletions retrofit/src/main/java/retrofit/Platform.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}

Expand Down Expand Up @@ -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 {
Expand Down
112 changes: 112 additions & 0 deletions retrofit/src/main/java/retrofit/appengine/UrlFetchClient.java
Original file line number Diff line number Diff line change
@@ -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<HTTPHeader> fetchHeaders = response.getHeaders();
List<Header> headers = new ArrayList<Header>(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);
}
}
7 changes: 2 additions & 5 deletions retrofit/src/test/java/retrofit/TestingUtils.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -26,11 +27,7 @@ public static TypedOutput createMultipart(Map<String, TypedOutput> 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);
}
}
131 changes: 131 additions & 0 deletions retrofit/src/test/java/retrofit/appengine/UrlFetchClientTest.java
Original file line number Diff line number Diff line change
@@ -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<HTTPHeader> 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<String, TypedOutput> bodyParams = new LinkedHashMap<String, TypedOutput>();
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<HTTPHeader> 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<Header> headers = new ArrayList<Header>();
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<HTTPHeader> 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);
}
}
Loading

0 comments on commit 449cfaa

Please sign in to comment.