com.strongloop.android.remoting.adapters.RestAdapter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of strong-remoting-android Show documentation
Show all versions of strong-remoting-android Show documentation
Android client for strong-remoting
The newest version!
// Copyright (c) 2013 StrongLoop. All rights reserved.
package com.strongloop.android.remoting.adapters;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONException;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.util.Log;
import com.loopj.android.http.AsyncHttpClient;
import com.loopj.android.http.AsyncHttpResponseHandler;
import com.loopj.android.http.BinaryHttpResponseHandler;
import com.loopj.android.http.RequestParams;
import com.strongloop.android.remoting.JsonUtil;
/**
* A specific {@link Adapter} implementation for RESTful servers.
*
* In addition to implementing the {@link Adapter} interface,
* RestAdapter
contains a single {@link RestContract} to map
* remote methods to custom HTTP routes. This is only required if the HTTP
* settings have been customized on the server. When in doubt, try without.
*
* @see RestContract
*/
public class RestAdapter extends Adapter {
private static final String TAG = "remoting.RestAdapter";
private HttpClient client;
private RestContract contract;
public RestAdapter(Context context, String url) {
super(context, url);
this.contract = new RestContract();
}
/**
* Gets this adapter's {@link RestContract}, a custom contract for
* fine-grained route configuration.
* @return the contract.
*/
public RestContract getContract() {
return contract;
}
/**
* Get the underlying HTTP client. This allows subclasses to add
* custom headers like Authorization.
* @return the client.
*/
protected AsyncHttpClient getClient() { return client; }
/**
* Sets this adapter's {@link RestContract}, a custom contract for
* fine-grained route configuration.
* @param contract The contract.
*/
public void setContract(RestContract contract) {
this.contract = contract;
}
@Override
public void connect(Context context, String url) {
if (url == null) {
client = null;
}
else {
client = new HttpClient(context, url);
client.addHeader("Accept", "application/json");
}
}
@Override
public boolean isConnected() {
return client != null;
}
/**
* {@inheritDoc}
*
* @throws IllegalStateException if the contract is not set
* (see {@link #setContract(RestContract)})
* or the adapter is not connected.
*/
@Override
public void invokeStaticMethod(String method,
Map parameters,
final Callback callback) {
AsyncHttpResponseHandler httpHandler = new CallbackHandler(callback);
invokeStaticMethod(method, parameters, httpHandler);
}
/**
* {@inheritDoc}
*
* @throws IllegalStateException if the contract is not set
* (see {@link #setContract(RestContract)})
* or the adapter is not connected.
*/
@Override
public void invokeStaticMethod(String method,
Map parameters,
final BinaryCallback callback) {
AsyncHttpResponseHandler httpHandler = new BinaryHandler(callback);
invokeStaticMethod(method, parameters, httpHandler);
}
private void invokeStaticMethod(String method, Map parameters, AsyncHttpResponseHandler httpHandler) {
if (contract == null) {
throw new IllegalStateException("Invalid contract");
}
String verb = contract.getVerbForMethod(method);
String path = contract.getUrlForMethod(method, parameters);
ParameterEncoding parameterEncoding = contract.getParameterEncodingForMethod(method);
request(path, verb, parameters, parameterEncoding, httpHandler);
}
/**
* {@inheritDoc}
*
* @throws IllegalStateException if the contract is not set
* (see {@link #setContract(RestContract)})
* or the adapter is not connected.
*/
@Override
public void invokeInstanceMethod(String method,
Map constructorParameters,
Map parameters,
final Callback callback) {
AsyncHttpResponseHandler httpHandler = new CallbackHandler(callback);
invokeInstanceMethod(method, constructorParameters, parameters, httpHandler);
}
/**
* {@inheritDoc}
*
* @throws IllegalStateException if the contract is not set
* (see {@link #setContract(RestContract)})
* or the adapter is not connected.
*/
@Override
public void invokeInstanceMethod(String method,
Map constructorParameters,
Map parameters,
final BinaryCallback callback) {
AsyncHttpResponseHandler httpHandler = new BinaryHandler(callback);
invokeInstanceMethod(method, constructorParameters, parameters, httpHandler);
};
private void invokeInstanceMethod(String method,
Map constructorParameters,
Map parameters,
AsyncHttpResponseHandler httpHandler) {
if (contract == null) {
throw new IllegalStateException("Invalid contract");
}
Map combinedParameters = new HashMap();
if (constructorParameters != null) {
combinedParameters.putAll(constructorParameters);
}
if (parameters != null) {
combinedParameters.putAll(parameters);
}
String verb = contract.getVerbForMethod(method);
String path = contract.getUrlForMethod(method, combinedParameters);
ParameterEncoding parameterEncoding = contract.getParameterEncodingForMethod(method);
request(path, verb, combinedParameters, parameterEncoding, httpHandler);
}
private void request(String path,
String verb,
Map parameters,
ParameterEncoding parameterEncoding,
AsyncHttpResponseHandler responseHandler) {
if (!isConnected()) {
throw new IllegalStateException("Adapter not connected");
}
client.request(verb, path, parameters, parameterEncoding, responseHandler);
}
class CallbackHandler extends AsyncHttpResponseHandler {
private final Callback callback;
public CallbackHandler(Callback callback) {
this.callback = callback;
}
@Override
public void onSuccess(int status, Header[] headers, byte[] body) {
try {
String response = body == null ? null : new String(body, getCharset());
if (Log.isLoggable(TAG, Log.DEBUG))
Log.d(TAG, "Success (string): " + response);
callback.onSuccess(response);
} catch (Throwable t) {
callback.onError(t);
}
}
@Override
public void onFailure(int statusCode,
org.apache.http.Header[] headers,
byte[] responseBody,
java.lang.Throwable error) {
if (Log.isLoggable(TAG, Log.WARN)) {
String message;
if (error != null) {
message = error.toString();
} else {
message = statusCode + "\n";
try {
message += new String(responseBody, getCharset());
} catch (UnsupportedEncodingException e) {
message += new String(responseBody);
}
}
Log.w(TAG, "HTTP request (string) failed: " + message);
}
callback.onError(error);
}
}
class BinaryHandler extends BinaryHttpResponseHandler {
private final BinaryCallback callback;
public BinaryHandler(BinaryCallback callback) {
super(new String[]{ ".*" });
this.callback = callback;
}
@Override
public void onFailure(int statusCode,
org.apache.http.Header[] headers,
byte[] responseBody,
java.lang.Throwable error) {
if (Log.isLoggable(TAG, Log.WARN)) {
String message;
if (error != null) {
message = error.toString();
} else {
message = statusCode + "\n";
try {
message += new String(responseBody, getCharset());
} catch (UnsupportedEncodingException e) {
message += new String(responseBody);
}
}
Log.w(TAG, "HTTP request (binary) failed: " + message);
}
callback.onError(error);
}
@Override
public void onSuccess(int statusCode, Header[] headers, byte[] binaryData) {
if (Log.isLoggable(TAG, Log.DEBUG))
Log.d(TAG, "Success (binary): " + binaryData.length + " bytes");
try {
String contentType = null;
for (Header h: headers) {
if (h.getName().equalsIgnoreCase("content-type"))
contentType = h.getValue();
}
callback.onSuccess(binaryData, contentType);
} catch (Throwable t) {
callback.onError(t);
}
}
}
//
// Mimic AFNetworking as much as possible.
//
// Internally, it's using "Android Asynchronous Http Client".
// http://loopj.com/android-async-http/
// The benefit is connection pools, persistent cookies,
// an asynchronous API, Android bug workarounds, etc.
// The drawback is it doesn't support HEAD or OPTION.
//
enum ParameterEncoding {
FORM_URL,
JSON,
FORM_MULTIPART
}
private static class HttpClient extends AsyncHttpClient {
private static String getVersionName(Context context) {
String appVersion = null;
try {
PackageInfo pinfo = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
appVersion = pinfo.versionName;
}
catch (NameNotFoundException e) {
// Do nothing
}
return (appVersion != null) ? appVersion : "";
}
private static String getDeviceName() {
String deviceName = android.os.Build.MODEL;
if (deviceName == null || deviceName.length() == 0) {
deviceName = android.os.Build.DEVICE;
if (deviceName == null || deviceName.length() == 0) {
deviceName = "Unknown";
}
}
return deviceName;
}
private Context context;
private String baseUrl;
public HttpClient(Context context, String baseUrl) {
if (baseUrl == null) {
throw new IllegalArgumentException(
"The baseUrl cannot be null");
}
this.context = context;
this.baseUrl = baseUrl;
// Make sure base url ends with a trailing slash.
if (!this.baseUrl.endsWith("/")) {
this.baseUrl += "/";
}
// More useful User-Agent, similar to AFNetworing.
String appName;
if (context != null) {
String appPackageName = context.getPackageName();
String appVersion = getVersionName(context);
appName = appPackageName + "/" + appVersion;
}
else {
appName = "StongLoopRemoting App";
}
String deviceName = getDeviceName();
String androidVersion = android.os.Build.VERSION.RELEASE +
"/API-" + android.os.Build.VERSION.SDK_INT;
String userAgent = appName + " (" + deviceName +
" Android " + androidVersion + ")";
setUserAgent(userAgent);
}
public void request(String method, String path,
Map parameters,
ParameterEncoding parameterEncoding,
final AsyncHttpResponseHandler httpCallback) {
Uri.Builder uri = Uri.parse(baseUrl).buildUpon();
if (path != null) {
if (path.startsWith("/")) {
uri.appendEncodedPath(path.substring(1));
}
else {
uri.appendEncodedPath(path);
}
}
String contentType = null;
HttpEntity body = null;
RequestParams requestParams = null;
String charset = "utf-8";
if (parameters != null) {
if ("GET".equalsIgnoreCase(method) ||
"HEAD".equalsIgnoreCase(method) ||
"DELETE".equalsIgnoreCase(method)) {
for (Map.Entry entry :
flattenParameters(parameters).entrySet()) {
uri.appendQueryParameter(entry.getKey(),
String.valueOf(entry.getValue()));
}
}
else if (parameterEncoding == ParameterEncoding.FORM_URL) {
// NOTE: Code for "x-www-form-urlencoded" is not used
// and is untested.
contentType =
"application/x-www-form-urlencoded; charset=" +
charset;
List nameValuePairs =
new ArrayList();
for (Map.Entry entry :
parameters.entrySet()) {
nameValuePairs.add(
new BasicNameValuePair(entry.getKey(),
String.valueOf(entry.getValue())));
}
try {
body = new UrlEncodedFormEntity(nameValuePairs,
charset);
}
catch (UnsupportedEncodingException e) {
// Won't happen
Log.e(TAG, "Couldn't encode url params", e);
}
}
else if (parameterEncoding == ParameterEncoding.FORM_MULTIPART) {
if (!"POST".equalsIgnoreCase(method)) {
throw new UnsupportedOperationException(
"RestAdapter does not support multipart PUT requests");
}
try {
requestParams = buildRequestParameters(
flattenParameters(parameters));
} catch (FileNotFoundException e1) {
throw new IllegalArgumentException("Invalid File parameter");
}
}
else if (parameterEncoding == ParameterEncoding.JSON) {
contentType = "application/json; charset=" + charset;
String s = "";
try {
s = String.valueOf(JsonUtil.toJson(parameters));
}
catch (JSONException e) {
Log.e(TAG, "Couldn't convert parameters to JSON", e);
}
try {
body = new StringEntity(s, charset);
}
catch (UnsupportedEncodingException e) {
// Won't happen
Log.e(TAG, "Couldn't encode JSON params", e);
}
}
}
Header[] headers = {
new BasicHeader("Accept", "application/json"),
};
String url = uri.build().toString();
logRequest(method, url, body, requestParams);
if ("GET".equalsIgnoreCase(method)) {
get(context, url, headers, null, httpCallback);
}
else if ("DELETE".equalsIgnoreCase(method)) {
delete(context, url, headers, httpCallback);
}
else if ("POST".equalsIgnoreCase(method)) {
if (requestParams != null)
post(context, url, headers, requestParams, contentType, httpCallback);
else
post(context, url, headers, body, contentType, httpCallback);
}
else if ("PUT".equalsIgnoreCase(method)) {
put(context, url, headers, body, contentType, httpCallback);
}
else {
throw new IllegalArgumentException("Illegal method: " +
method + ". Only GET, POST, PUT, DELETE supported.");
}
}
private void logRequest(String method, String url, HttpEntity body, RequestParams requestParams) {
if (!Log.isLoggable(TAG, Log.DEBUG)) return;
Log.d(TAG, method + " " + url);
if (requestParams != null)
Log.d(TAG, requestParams.toString());
else if (body != null && body.isRepeatable()) {
try {
// Convert body stream to string
// Based on http://stackoverflow.com/a/5445161/69868
Scanner s = new Scanner(body.getContent()).useDelimiter("\\A");
if (s.hasNext())
Log.d(TAG, s.next());
} catch (IOException e) {
}
}
}
private Map flattenParameters(
final Map parameters) {
return flattenParameters(null, parameters);
}
@SuppressWarnings("unchecked")
private Map flattenParameters(
final String keyPrefix,
final Map parameters) {
// This method converts nested maps into a flat list
// Input: { "here": { "lat": 10, "lng": 20 }
// Output: { "here[lat]": 10, "here[lng]": 20 }
Map result = new HashMap();
for (Map.Entry entry
: parameters.entrySet()) {
String key = keyPrefix != null
? keyPrefix + "[" + entry.getKey() + "]"
: entry.getKey();
Object value = entry.getValue();
if (value instanceof Map) {
result.putAll(flattenParameters(key, (Map) value));
} else {
result.put(key, value);
}
}
return result;
}
static protected RequestParams buildRequestParameters(
Map parameters) throws FileNotFoundException
{
RequestParams requestParams = new RequestParams();
for (Map.Entry entry :
parameters.entrySet()) {
Object value = entry.getValue();
if ( value != null ) {
if ( value instanceof java.io.File ) {
requestParams.put(entry.getKey(), (java.io.File)value);
}
else if (value instanceof StreamParam) {
((StreamParam) value).putTo(requestParams, entry.getKey());
}
else if ( value instanceof String ) {
requestParams.put(entry.getKey(), (String) entry.getValue());
}
else {
throw new IllegalArgumentException(
"Unknown param type for RequestParams: "
+ value.getClass().getName());
}
}
}
return requestParams;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy