Skip to content

Commit

Permalink
Add PUT, DELETE support to RestAdapter
Browse files Browse the repository at this point in the history
  • Loading branch information
pforhan committed May 4, 2011
1 parent 9e61b68 commit 2e35fda
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 90 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ lib
out
build

.classpath
.project
7 changes: 4 additions & 3 deletions modules/http/src/retrofit/http/CallbackResponseHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
package retrofit.http;

import com.google.gson.Gson;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.ResponseHandler;
import org.apache.http.entity.BufferedHttpEntity;
import retrofit.core.Callback;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Support for response handlers that invoke {@link Callback}.
*
Expand Down
18 changes: 18 additions & 0 deletions modules/http/src/retrofit/http/DELETE.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package retrofit.http;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Make a DELETE request to a REST path relative to base URL.
*
* @author Rob Dickerson
*/
@Target({ METHOD })
@Retention(RUNTIME)
public @interface DELETE {
String value();
}
18 changes: 18 additions & 0 deletions modules/http/src/retrofit/http/PUT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package retrofit.http;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Make a PUT request to a REST path relative to base URL.
*
* @author Rob Dickerson
*/
@Target({ METHOD })
@Retention(RUNTIME)
public @interface PUT {
String value();
}
214 changes: 127 additions & 87 deletions modules/http/src/retrofit/http/RestAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.client.utils.URLEncodedUtils;
Expand Down Expand Up @@ -67,10 +70,10 @@
* Adapts a Java interface to a REST API. HTTP requests happen in a
* background thread. Callbacks happen in the UI thread.
*
* <p>Gets the relative path for a given method from a {@link GET} or
* {@link POST} annotation on the method. Gets the names of URL parameters
* from {@link com.google.inject.name.Named} annotations on the method
* parameters.
* <p>Gets the relative path for a given method from a {@link GET},
* {@link POST}, {@link PUT}, or {@link DELETE} annotation on the method.
* Gets the names of URL parameters from {@link com.google.inject.name.Named}
* annotations on the method parameters.
*
* <p>The last method parameter should be of type {@link Callback}. The
* JSON HTTP response will be converted to the callback's parameter type
Expand All @@ -91,11 +94,11 @@
@SuppressWarnings("unchecked")
public static <T> Module service(final Class<T> type) {
return new Module() {
public void configure(Binder binder) {
@Override public void configure(Binder binder) {
binder.bind(type).toProvider(new Provider<T>() {
@Inject RestAdapter restAdapter;

public T get() {
@Override public T get() {
RestAdapter.RestHandler handler = restAdapter.new RestHandler();
return (T) Proxy.newProxyInstance(type.getClassLoader(),
new Class<?>[] { type }, handler);
Expand All @@ -122,27 +125,37 @@ public HttpMethod getHttpMethod() {
}

private static RequestLine readHttpMethodAnnotation(Method method) {
GET getAnnotation = method.getAnnotation(GET.class);
boolean hasGet = getAnnotation != null;

POST postAnnotation = method.getAnnotation(POST.class);
boolean hasPost = postAnnotation != null;

if (hasGet && hasPost) {
Annotation httpMethod = findAnnotation(method, GET.class, null);
httpMethod = findAnnotation(method, POST.class, httpMethod);
httpMethod = findAnnotation(method, PUT.class, httpMethod);
httpMethod = findAnnotation(method, DELETE.class, httpMethod);

if (httpMethod instanceof GET) {
return new RequestLine(((GET) httpMethod).value(), HttpMethod.GET);
} else if (httpMethod instanceof POST) {
return new RequestLine(((POST) httpMethod).value(), HttpMethod.POST);
} else if (httpMethod instanceof PUT) {
return new RequestLine(((PUT) httpMethod).value(), HttpMethod.PUT);
} else if (httpMethod instanceof DELETE) {
return new RequestLine(((DELETE) httpMethod).value(), HttpMethod.DELETE);
} else {
throw new IllegalArgumentException(
"Method annotated with both GET and POST: " + method.getName());
"Method not annotated with GET, POST, PUT, or DELETE: "
+ method.getName());
}
}

if (hasGet) {
return new RequestLine(getAnnotation.value(), HttpMethod.GET);
} else if (hasPost) {
return new RequestLine(postAnnotation.value(), HttpMethod.POST);
} else {
private static <T extends Annotation> T findAnnotation(Method method,
Class<T> annotationClass, Annotation previousFind) {
T annotation = method.getAnnotation(annotationClass);
if (annotation != null && previousFind != null) {
throw new IllegalArgumentException(
"Method not annotated with GET or POST: " + method.getName());
"Method annotated with multiple HTTP method annotations: "
+ annotationClass.getSimpleName() + " and " + previousFind.getClass().getName());
}
}

return annotation;
}
/** Gets the parameter name from the @Named annotation. */
private static String getName(Annotation[] annotations, Method method,
int parameterIndex) {
Expand Down Expand Up @@ -170,80 +183,52 @@ private static <A extends Annotation> A findAnnotation(
private static enum HttpMethod {

GET {
HttpUriRequest createFrom(HttpRequestBuilder builder)
@Override HttpUriRequest createFrom(HttpRequestBuilder builder)
throws URISyntaxException {
List<NameValuePair> queryParams = builder.createParamList();
String queryString = URLEncodedUtils.format(queryParams, "UTF-8");
URI uri = URIUtils.createURI(builder.getScheme(), builder.getHost(), -1,
builder.getRelativePath(), queryString, null);
HttpGet httpGet = new HttpGet(uri);
builder.getHeaders().setOn(httpGet);
return httpGet;
HttpGet request = new HttpGet(uri);
builder.getHeaders().setOn(request);
return request;
}
},

POST {
HttpUriRequest createFrom(HttpRequestBuilder builder)
@Override HttpUriRequest createFrom(HttpRequestBuilder builder)
throws URISyntaxException {
URI uri = URIUtils.createURI(builder.getScheme(), builder.getHost(), -1,
builder.getRelativePath(), null, null);
HttpPost post = new HttpPost(uri);
addParamsToPost(post, builder);
builder.getHeaders().setOn(post);
return post;
HttpPost request = new HttpPost(uri);
addParams(request, builder);
builder.getHeaders().setOn(request);
return request;
}
},

/**
* Adds all but the last method argument as parameters of HTTP post
* object.
*/
private void addParamsToPost(HttpPost post, HttpRequestBuilder builder) {
Method method = builder.getMethod();
Object[] args = builder.getArgs();
Class<?>[] parameterTypes = method.getParameterTypes();

Annotation[][] parameterAnnotations =
method.getParameterAnnotations();
int count = parameterAnnotations.length - 1;

if (useMultipart(parameterTypes)) {
MultipartEntity form = new MultipartEntity(
HttpMultipartMode.BROWSER_COMPATIBLE);
for (int i = 0; i < count; i++) {
Object arg = args[i];
if (arg == null) continue;
Annotation[] annotations = parameterAnnotations[i];
String name = getName(annotations, method, i);
Class<?> type = parameterTypes[i];

if (TypedBytes.class.isAssignableFrom(type)) {
TypedBytes typedBytes = (TypedBytes) arg;
form.addPart(name, new TypedBytesBody(typedBytes, name));
} else {
try {
form.addPart(name, new StringBody(String.valueOf(arg)));
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
}
post.setEntity(form);
} else {
try {
List<NameValuePair> paramList = builder.createParamList();
post.setEntity(new UrlEncodedFormEntity(paramList));
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
PUT {
@Override HttpUriRequest createFrom(HttpRequestBuilder builder)
throws URISyntaxException {
URI uri = URIUtils.createURI(builder.getScheme(), builder.getHost(), -1,
builder.getRelativePath(), null, null);
HttpPut request = new HttpPut(uri);
addParams(request, builder);
builder.getHeaders().setOn(request);
return request;
}
},

/** Returns true if the post contains a file upload. */
private boolean useMultipart(Class<?>[] parameterTypes) {
for (Class<?> parameterType : parameterTypes) {
if (TypedBytes.class.isAssignableFrom(parameterType)) return true;
}
return false;
DELETE {
@Override HttpUriRequest createFrom(HttpRequestBuilder builder)
throws URISyntaxException {
List<NameValuePair> queryParams = builder.createParamList();
String queryString = URLEncodedUtils.format(queryParams, "UTF-8");
URI uri = URIUtils.createURI(builder.getScheme(), builder.getHost(), -1,
builder.getRelativePath(), queryString, null);
HttpDelete request = new HttpDelete(uri);
builder.getHeaders().setOn(request);
return request;
}
};

Expand All @@ -252,6 +237,61 @@ private boolean useMultipart(Class<?>[] parameterTypes) {
*/
abstract HttpUriRequest createFrom(HttpRequestBuilder builder)
throws URISyntaxException;

/**
* Adds all but the last method argument as parameters of HTTP request
* object.
*/
private static void addParams(HttpEntityEnclosingRequestBase request,
HttpRequestBuilder builder) {
Method method = builder.getMethod();
Object[] args = builder.getArgs();
Class<?>[] parameterTypes = method.getParameterTypes();

Annotation[][] parameterAnnotations =
method.getParameterAnnotations();
int count = parameterAnnotations.length - 1;

if (useMultipart(parameterTypes)) {
MultipartEntity form = new MultipartEntity(
HttpMultipartMode.BROWSER_COMPATIBLE);
for (int i = 0; i < count; i++) {
Object arg = args[i];
if (arg == null) continue;
Annotation[] annotations = parameterAnnotations[i];
String name = getName(annotations, method, i);
Class<?> type = parameterTypes[i];

if (TypedBytes.class.isAssignableFrom(type)) {
TypedBytes typedBytes = (TypedBytes) arg;
form.addPart(name, new TypedBytesBody(typedBytes, name));
} else {
try {
form.addPart(name, new StringBody(String.valueOf(arg)));
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
}
request.setEntity(form);
} else {
try {
List<NameValuePair> paramList = builder.createParamList();
request.setEntity(new UrlEncodedFormEntity(paramList));
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
}

/** Returns true if the parameters contain a file upload. */
private static boolean useMultipart(Class<?>[] parameterTypes) {
for (Class<?> parameterType : parameterTypes) {
if (TypedBytes.class.isAssignableFrom(parameterType)) return true;
}
return false;
}

}

/**
Expand Down Expand Up @@ -344,11 +384,11 @@ public HttpUriRequest build() throws URISyntaxException {

private class RestHandler implements InvocationHandler {

public Object invoke(Object proxy, final Method method,
@Override public Object invoke(Object proxy, final Method method,
final Object[] args) {
// Execute HTTP request in the background.
executor.execute(new Runnable() {
@SuppressWarnings("unchecked")
@Override @SuppressWarnings("unchecked")
public void run() {
backgroundInvoke(method, args);
}
Expand Down Expand Up @@ -383,7 +423,7 @@ private void backgroundInvoke(Method method, Object[] args) {
GsonResponseHandler.create(resultType, callback);

// Optionally wrap the response handler for server call profiling.
ResponseHandler<? extends Void> rh = (profiler == null) ?
ResponseHandler<Void> rh = (profiler == null) ?
gsonResponseHandler : createProfiler(gsonResponseHandler, profiler,
method, server.apiUrl());

Expand Down Expand Up @@ -471,19 +511,19 @@ public TypedBytesBody(TypedBytes typedBytes, String baseName) {
this.name = baseName + "." + typedBytes.mimeType().extension();
}

public long getContentLength() {
@Override public long getContentLength() {
return typedBytes.length();
}

public String getFilename() {
@Override public String getFilename() {
return name;
}

public String getCharset() {
@Override public String getCharset() {
return null;
}

public String getTransferEncoding() {
@Override public String getTransferEncoding() {
return MIME.ENC_BINARY;
}

Expand Down Expand Up @@ -570,7 +610,7 @@ private ProfilingResponseHandler(ResponseHandler<Void> delegate,
this.relativePath = relativePath;
}

public Void handleResponse(HttpResponse httpResponse) throws IOException {
@Override public Void handleResponse(HttpResponse httpResponse) throws IOException {
// Intercept the response and send data to profiler.
long elapsedTime = System.currentTimeMillis() - startTime;
int statusCode = httpResponse.getStatusLine().getStatusCode();
Expand Down

0 comments on commit 2e35fda

Please sign in to comment.