forked from square/retrofit
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request square#62 from square/jw/converter
Abstract HTTP entity (de)serializaton to converters.
- Loading branch information
Showing
12 changed files
with
397 additions
and
331 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
|
@@ -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); | ||
} | ||
|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.