Skip to content

Commit

Permalink
Merge pull request square#62 from square/jw/converter
Browse files Browse the repository at this point in the history
Abstract HTTP entity (de)serializaton to converters.
  • Loading branch information
edenman committed Oct 17, 2012
2 parents 4b39bc0 + eea6cc6 commit 25f7e1c
Show file tree
Hide file tree
Showing 12 changed files with 397 additions and 331 deletions.
152 changes: 70 additions & 82 deletions http/src/main/java/retrofit/http/CallbackResponseHandler.java
Original file line number Diff line number Diff line change
@@ -1,86 +1,95 @@
// Copyright 2010 Square, Inc.
// Copyright 2012 Square, Inc.
package retrofit.http;

import com.google.gson.Gson;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.ResponseHandler;
import org.apache.http.entity.BufferedHttpEntity;
import retrofit.http.Callback.ServerError;

import java.io.IOException;
import java.lang.reflect.Type;
import java.text.DateFormat;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Support for response handlers that invoke {@link Callback}.
*
* @author Bob Lee ([email protected])
* @author Jake Wharton ([email protected])
*/
public abstract class CallbackResponseHandler<T>
implements ResponseHandler<Void> {
public class CallbackResponseHandler<R> implements ResponseHandler<Void> {

private static final Logger LOGGER =
Logger.getLogger(CallbackResponseHandler.class.getName());
private static final Logger LOGGER = Logger.getLogger(CallbackResponseHandler.class.getName());

private final Callback<T> callback;
private final Gson gson;
private String requestUrl; // Can be null.
private final Callback<R> callback;
private final Type callbackType;
private final Converter converter;
private final String requestUrl; // Can be null.
private final Date start;
private final ThreadLocal<DateFormat> dateFormat;

protected CallbackResponseHandler(Gson gson, Callback<T> callback) {
this(gson, callback, null);
}

protected CallbackResponseHandler(Gson gson, Callback<T> callback, String requestUrl) {
this.gson = gson;
protected CallbackResponseHandler(Callback<R> callback, Type callbackType, Converter converter, String requestUrl,
Date start, ThreadLocal<DateFormat> dateFormat) {
this.callback = callback;
this.callbackType = callbackType;
this.converter = converter;
this.requestUrl = requestUrl;
this.start = start;
this.dateFormat = dateFormat;
}

/**
* Parses the HTTP entity and creates an object that will be passed to
* {@link Callback#call(T)}. Invoked in background thread.
* {@link Callback#call(R)}. Invoked in background thread.
*
* @param entity HTTP entity to read and parse, not null
* @throws IOException if a network error occurs
* @throws ServerException if the server returns an unexpected response
* @throws RuntimeException if an unexpected error occurs
* @param type destination object type which is guaranteed to match <T>
* @return parsed response
* @throws ConversionException if the server returns an unexpected response
*/
protected abstract T parse(HttpEntity entity) throws IOException,
ServerException;
protected Object parse(HttpEntity entity, Type type) throws ConversionException {
if (LOGGER.isLoggable(Level.FINE)) {
try {
entity = HttpClients.copyAndLog(entity, requestUrl, start, dateFormat.get());
} catch (IOException e) {
throw new RuntimeException(e);
}
}

return converter.to(entity, type);
}

@SuppressWarnings("unchecked")
@SuppressWarnings("unchecked") // Type is extracted from generic properties so cast is safe.
public Void handleResponse(HttpResponse response) throws IOException {
/*
* Note: An IOException thrown from here (while downloading the HTTP
* entity, for example) will propagate to the caller and be reported as a
* network error.
*
* Callback methods actually execute in the main thread, so we don't
* have to worry about unhandled exceptions thrown by them.
*/
// Note: An IOException thrown from here (while downloading the HTTP
// entity, for example) will propagate to the caller and be reported as a
// network error.
//
// Callback methods actually execute in the main thread, so we don't
// have to worry about unhandled exceptions thrown by them.

HttpEntity entity = response.getEntity();
StatusLine statusLine = response.getStatusLine();
int statusCode = statusLine.getStatusCode();
HttpEntity entity = response.getEntity();

if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
if (entity != null) {
// TODO: Use specified encoding.
String body = new String(HttpClients.entityToBytes(entity), "UTF-8");
LOGGER.fine("Session expired. Body: " + body + ". Request url " + requestUrl);
callback.sessionExpired(parseServerMessage(statusCode, body));
} else {
LOGGER.fine("Session expired. Request url " + requestUrl);
callback.sessionExpired(null);
LOGGER.fine("Session expired. Request url " + requestUrl);
ServerError error = null;
try {
error = (ServerError) parse(entity, ServerError.class);
LOGGER.fine("Server returned " + HttpStatus.SC_UNAUTHORIZED + ", " + statusLine.getReasonPhrase() + ". Body: "
+ error + ". Request url " + requestUrl);
} catch (ConversionException e) {
LOGGER.log(Level.WARNING, e.getMessage(), e);
}
callback.sessionExpired(error);
return null;
}


// 2XX == successful request
if (statusCode >= 200 && statusCode < 300) {
if (entity == null) {
Expand All @@ -90,8 +99,9 @@ public Void handleResponse(HttpResponse response) throws IOException {
}

try {
callback.call(parse(entity));
} catch (ServerException e) {
R result = (R) parse(entity, callbackType);
callback.call(result);
} catch (ConversionException e) {
LOGGER.log(Level.WARNING, e.getMessage(), e);
callback.serverError(null, statusCode);
}
Expand All @@ -100,56 +110,34 @@ public Void handleResponse(HttpResponse response) throws IOException {

// 5XX == server error
if (statusCode >= 500) {
if (entity != null) {
// TODO: Use specified encoding.
String body = new String(HttpClients.entityToBytes(entity), "UTF-8");
LOGGER.fine("Server returned " + statusCode + ", "
+ statusLine.getReasonPhrase() + ". Body: " + body + ". Request url " + requestUrl);
callback.serverError(parseServerMessage(statusCode, body), statusCode);
} else {
LOGGER.fine("Server returned " + statusCode + ", "
+ statusLine.getReasonPhrase() + ". Request url " + requestUrl);
callback.serverError(null, statusCode);
ServerError error = null;
try {
error = (ServerError) parse(entity, ServerError.class);
LOGGER.fine("Server returned " + statusCode + ", " + statusLine.getReasonPhrase() + ". Body: " + error
+ ". Request url " + requestUrl);
} catch (ConversionException e) {
LOGGER.log(Level.WARNING, e.getMessage(), e);
}
callback.serverError(error, statusCode);
return null;
}

// 4XX error
if (entity != null) {
/** Construct BufferedHttpEntity so that we can read it multiple times. */
HttpEntity bufferedEntity = new BufferedHttpEntity(entity);
// TODO: Use specified encoding.
String body = new String(HttpClients.entityToBytes(bufferedEntity),
"UTF-8");
LOGGER.fine("Server returned " + statusCode + ", "
+ statusLine.getReasonPhrase() + ". Body: " + body + ". Request url " + requestUrl);
R error = null;
try {
callback.clientError(parse(bufferedEntity), statusCode);
} catch (ServerException e) {
error = (R) parse(entity, callbackType);
LOGGER.fine("Server returned " + statusCode + ", " + statusLine.getReasonPhrase() + ". Body: " + error
+ ". Request url " + requestUrl);
} catch (ConversionException e) {
LOGGER.log(Level.WARNING, e.getMessage(), e);
callback.serverError(null, statusCode);
}
} else {
LOGGER.fine("Server returned " + statusCode + ", "
+ statusLine.getReasonPhrase() + ". Request url " + requestUrl);
callback.clientError(null, statusCode);
callback.clientError(error, statusCode);
return null;
}
return null;
}

/**
* Parses a server error message.
*/
private ServerError parseServerMessage(int statusCode, String body) {
if (statusCode == HttpStatus.SC_BAD_GATEWAY || statusCode == HttpStatus.SC_GATEWAY_TIMEOUT
|| statusCode < 500) {
try {
return gson.fromJson(body, ServerError.class);
} catch (Throwable t) {
// The server error takes precedence.
LOGGER.log(Level.WARNING, t.getMessage(), t);
}
}
LOGGER.fine("Server returned " + statusCode + ", " + statusLine.getReasonPhrase() + ". Request url " + requestUrl);
callback.clientError(null, statusCode);
return null;
}
}
17 changes: 17 additions & 0 deletions http/src/main/java/retrofit/http/ConversionException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package retrofit.http;

/** Indicate that conversion was unable to complete successfully. */
@SuppressWarnings("UnusedDeclaration")
public class ConversionException extends Exception {
public ConversionException(String message) {
super(message);
}

public ConversionException(String message, Throwable throwable) {
super(message, throwable);
}

public ConversionException(Throwable throwable) {
super(throwable);
}
}
33 changes: 33 additions & 0 deletions http/src/main/java/retrofit/http/Converter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2012 Square, Inc.
package retrofit.http;

import org.apache.http.HttpEntity;
import retrofit.io.TypedBytes;

import java.lang.reflect.Type;

/**
* Arbiter for converting objects to and from their representation in HTTP.
*
* @author Jake Wharton ([email protected])
*/
public interface Converter {
/**
* Convert an HTTP response body to a concrete object of the specified type.
*
* @param entity HTTP response body.
* @param type Target object type.
* @return Instance of {@code type} which will be cast by the caller.
* @throws ConversionException If conversion was unable to complete. This will trigger a call to
* {@link Callback#serverError(retrofit.http.Callback.ServerError, int)}.
*/
Object to(HttpEntity entity, Type type) throws ConversionException;

/**
* Convert and object to appropriate representation for HTTP transport.
*
* @param object Object instance to convert.
* @return Representation of the specified object as bytes.
*/
TypedBytes from(Object object);
}
Loading

0 comments on commit 25f7e1c

Please sign in to comment.