package cgeo.geocaching.network;
import cgeo.geocaching.CgeoApplication;
import cgeo.geocaching.files.LocalStorage;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.utils.JsonUtils;
import cgeo.geocaching.utils.Log;
import cgeo.geocaching.utils.TextUtils;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.NameValuePair;
import ch.boye.httpclientandroidlib.client.HttpClient;
import ch.boye.httpclientandroidlib.client.entity.UrlEncodedFormEntity;
import ch.boye.httpclientandroidlib.client.methods.HttpGet;
import ch.boye.httpclientandroidlib.client.methods.HttpPost;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
import ch.boye.httpclientandroidlib.client.params.ClientPNames;
import ch.boye.httpclientandroidlib.entity.StringEntity;
import ch.boye.httpclientandroidlib.entity.mime.MultipartEntity;
import ch.boye.httpclientandroidlib.entity.mime.content.FileBody;
import ch.boye.httpclientandroidlib.entity.mime.content.StringBody;
import ch.boye.httpclientandroidlib.impl.client.DecompressingHttpClient;
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
import ch.boye.httpclientandroidlib.impl.client.LaxRedirectStrategy;
import ch.boye.httpclientandroidlib.params.BasicHttpParams;
import ch.boye.httpclientandroidlib.params.CoreConnectionPNames;
import ch.boye.httpclientandroidlib.params.CoreProtocolPNames;
import ch.boye.httpclientandroidlib.params.HttpParams;
import ch.boye.httpclientandroidlib.util.EntityUtils;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.lang3.CharEncoding;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jdt.annotation.Nullable;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.regex.Pattern;
public abstract class Network {
/** User agent id */
private final static String PC_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:9.0.1) Gecko/20100101 Firefox/9.0.1";
/** Native user agent, taken from a Android 2.2 Nexus **/
private final static String NATIVE_USER_AGENT = "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1";
private static final Pattern PATTERN_PASSWORD = Pattern.compile("(?<=[\\?&])[Pp]ass(w(or)?d)?=[^$]+");
private final static HttpParams CLIENT_PARAMS = new BasicHttpParams();
static {
CLIENT_PARAMS.setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET, CharEncoding.UTF_8);
CLIENT_PARAMS.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 30000);
CLIENT_PARAMS.setParameter(CoreConnectionPNames.SO_TIMEOUT, 30000);
CLIENT_PARAMS.setParameter(ClientPNames.HANDLE_REDIRECTS, true);
}
private static String hidePassword(final String message) {
return PATTERN_PASSWORD.matcher(message).replaceAll("password=***");
}
private static HttpClient getHttpClient() {
final DefaultHttpClient client = new DefaultHttpClient();
client.setCookieStore(Cookies.cookieStore);
client.setParams(CLIENT_PARAMS);
client.setRedirectStrategy(new LaxRedirectStrategy());
return new DecompressingHttpClient(client);
}
/**
* POST HTTP request
*
* @param uri the URI to request
* @param params the parameters to add to the POST request
* @return the HTTP response, or null in case of an encoding error params
*/
@Nullable
public static HttpResponse postRequest(final String uri, final Parameters params) {
return request("POST", uri, params, null, null);
}
/**
* POST HTTP request
*
* @param uri the URI to request
* @param params the parameters to add to the POST request
* @param headers the headers to add to the request
* @return the HTTP response, or null in case of an encoding error params
*/
@Nullable
public static HttpResponse postRequest(final String uri, final Parameters params, final Parameters headers) {
return request("POST", uri, params, headers, null);
}
/**
* POST HTTP request with Json POST DATA
*
* @param uri the URI to request
* @param json the json object to add to the POST request
* @return the HTTP response, or null in case of an encoding error params
*/
@Nullable
public static HttpResponse postJsonRequest(final String uri, final ObjectNode json) {
final HttpPost request = new HttpPost(uri);
request.addHeader("Content-Type", "application/json; charset=utf-8");
if (json != null) {
try {
request.setEntity(new StringEntity(json.toString(), CharEncoding.UTF_8));
} catch (final UnsupportedEncodingException e) {
Log.e("postJsonRequest:JSON Entity: UnsupportedEncodingException", e);
return null;
}
}
return doLogRequest(request);
}
/**
* Multipart POST HTTP request
*
* @param uri the URI to request
* @param params the parameters to add to the POST request
* @param fileFieldName the name of the file field name
* @param fileContentType the content-type of the file
* @param file the file to include in the request
* @return the HTTP response, or null in case of an encoding error param
*/
@Nullable
public static HttpResponse postRequest(final String uri, final Parameters params,
final String fileFieldName, final String fileContentType, final File file) {
final MultipartEntity entity = new MultipartEntity();
for (final NameValuePair param : params) {
try {
entity.addPart(param.getName(), new StringBody(param.getValue(), TextUtils.CHARSET_UTF8));
} catch (final UnsupportedEncodingException e) {
Log.e("Network.postRequest: unsupported encoding for parameter " + param.getName(), e);
return null;
}
}
entity.addPart(fileFieldName, new FileBody(file, fileContentType));
final HttpPost request = new HttpPost(uri);
request.setEntity(entity);
addHeaders(request, null, null);
return doLogRequest(request);
}
/**
* Make an HTTP request
*
* @param method
* the HTTP method to use ("GET" or "POST")
* @param uri
* the URI to request
* @param params
* the parameters to add to the URI
* @param headers
* the headers to add to the request
* @param cacheFile
* the cache file used to cache this query
* @return the HTTP response, or null in case of an encoding error in a POST request arguments
*/
@Nullable
private static HttpResponse request(final String method, final String uri,
@Nullable final Parameters params, @Nullable final Parameters headers, @Nullable final File cacheFile) {
final HttpRequestBase request;
if (method.equals("GET")) {
final String fullUri = params == null ? uri : Uri.parse(uri).buildUpon().encodedQuery(params.toString()).build().toString();
request = new HttpGet(fullUri);
} else {
request = new HttpPost(uri);
if (params != null) {
try {
((HttpPost) request).setEntity(new UrlEncodedFormEntity(params, CharEncoding.UTF_8));
} catch (final UnsupportedEncodingException e) {
Log.e("request", e);
return null;
}
}
}
addHeaders(request, headers, cacheFile);
return doLogRequest(request);
}
/**
* Add headers to HTTP request.
* @param request
* the request to add headers to
* @param headers
* the headers to add (in addition to the standard headers), can be null
* @param cacheFile
* if non-null, the file to take ETag and If-Modified-Since information from
*/
private static void addHeaders(final HttpRequestBase request, @Nullable final Parameters headers, @Nullable final File cacheFile) {
for (final NameValuePair header : Parameters.extend(Parameters.merge(headers, cacheHeaders(cacheFile)),
"Accept-Charset", "utf-8,iso-8859-1;q=0.8,utf-16;q=0.8,*;q=0.7",
"Accept-Language", "en-US,*;q=0.9",
"X-Requested-With", "XMLHttpRequest")) {
request.setHeader(header.getName(), header.getValue());
}
request.getParams().setParameter(CoreProtocolPNames.USER_AGENT,
Settings.getUseNativeUa() ? NATIVE_USER_AGENT : PC_USER_AGENT);
}
/**
* Perform an HTTP request and log it.
*
* @param request
* the request to try
* @return
* the response, or null if there has been a failure
*/
@Nullable
private static HttpResponse doLogRequest(final HttpRequestBase request) {
if (!isNetworkConnected()) {
return null;
}
final String reqLogStr = request.getMethod() + " " + hidePassword(request.getURI().toString());
Log.d(reqLogStr);
final HttpClient client = getHttpClient();
final long before = System.currentTimeMillis();
try {
final HttpResponse response = client.execute(request);
final int status = response.getStatusLine().getStatusCode();
if (status == 200) {
Log.d(status + formatTimeSpan(before) + reqLogStr);
} else {
Log.w(status + " [" + response.getStatusLine().getReasonPhrase() + "]" + formatTimeSpan(before) + reqLogStr);
}
return response;
} catch (final Exception e) {
final String timeSpan = formatTimeSpan(before);
Log.w("Failure" + timeSpan + reqLogStr + " (" + e.toString() + ")");
}
return null;
}
@Nullable
private static Parameters cacheHeaders(@Nullable final File cacheFile) {
if (cacheFile == null || !cacheFile.exists()) {
return null;
}
final String etag = LocalStorage.getSavedHeader(cacheFile, LocalStorage.HEADER_ETAG);
if (etag != null) {
// The ETag is a more robust check than a timestamp. If we have an ETag, it is enough
// to identify the right version of the resource.
return new Parameters("If-None-Match", etag);
}
final String lastModified = LocalStorage.getSavedHeader(cacheFile, LocalStorage.HEADER_LAST_MODIFIED);
if (lastModified != null) {
return new Parameters("If-Modified-Since", lastModified);
}
return null;
}
/**
* GET HTTP request
*
* @param uri
* the URI to request
* @param params
* the parameters to add the the GET request
* @param cacheFile
* the name of the file storing the cached resource, or null not to use one
* @return the HTTP response
*/
@Nullable
public static HttpResponse getRequest(final String uri, @Nullable final Parameters params, @Nullable final File cacheFile) {
return request("GET", uri, params, null, cacheFile);
}
/**
* GET HTTP request
*
* @param uri
* the URI to request
* @param params
* the parameters to add the the GET request
* @return the HTTP response
*/
@Nullable
public static HttpResponse getRequest(final String uri, @Nullable final Parameters params) {
return request("GET", uri, params, null, null);
}
/**
* GET HTTP request
*
* @param uri
* the URI to request
* @param params
* the parameters to add the the GET request
* @param headers
* the headers to add to the GET request
* @return the HTTP response
*/
@Nullable
public static HttpResponse getRequest(final String uri, @Nullable final Parameters params, @Nullable final Parameters headers) {
return request("GET", uri, params, headers, null);
}
/**
* GET HTTP request
*
* @param uri
* the URI to request
* @return the HTTP response
*/
@Nullable
public static HttpResponse getRequest(final String uri) {
return request("GET", uri, null, null, null);
}
private static String formatTimeSpan(final long before) {
// don't use String.format in a pure logging routine, it has very bad performance
return " (" + (System.currentTimeMillis() - before) + " ms) ";
}
static public boolean isSuccess(@Nullable final HttpResponse response) {
return response != null && response.getStatusLine().getStatusCode() == 200;
}
static public boolean isPageNotFound(@Nullable final HttpResponse response) {
return response != null && response.getStatusLine().getStatusCode() == 404;
}
/**
* Get the result of a GET HTTP request returning a JSON body.
*
* @param uri the base URI of the GET HTTP request
* @param params the query parameters, or null
if there are none
* @return a JSON object if the request was successful and the body could be decoded, null
otherwise
*/
@Nullable
public static ObjectNode requestJSON(final String uri, @Nullable final Parameters params) {
final HttpResponse response = request("GET", uri, params, new Parameters("Accept", "application/json, text/javascript, */*; q=0.01"), null);
final String responseData = getResponseData(response, false);
if (responseData != null) {
try {
return (ObjectNode) JsonUtils.reader.readTree(responseData);
} catch (final IOException e) {
Log.w("requestJSON", e);
}
}
return null;
}
/**
* Get the input stream corresponding to a HTTP response if it exists.
*
* @param response a HTTP response, which can be null
* @return the input stream if the HTTP request is successful, null
otherwise
*/
@Nullable
public static InputStream getResponseStream(@Nullable final HttpResponse response) {
if (!isSuccess(response)) {
return null;
}
assert response != null;
final HttpEntity entity = response.getEntity();
if (entity == null) {
return null;
}
try {
return entity.getContent();
} catch (final IOException e) {
Log.e("Network.getResponseStream", e);
return null;
}
}
@Nullable
private static String getResponseDataNoError(final HttpResponse response, final boolean replaceWhitespace) {
try {
final String data = EntityUtils.toString(response.getEntity(), CharEncoding.UTF_8);
return replaceWhitespace ? TextUtils.replaceWhitespace(data) : data;
} catch (final Exception e) {
Log.e("getResponseData", e);
return null;
}
}
/**
* Get the body of a HTTP response.
*
* {@link TextUtils#replaceWhitespace(String)} will be called on the result
*
* @param response a HTTP response, which can be null
* @return the body if the response comes from a successful HTTP request, null
otherwise
*/
@Nullable
public static String getResponseData(@Nullable final HttpResponse response) {
return getResponseData(response, true);
}
@Nullable
public static String getResponseDataAlways(@Nullable final HttpResponse response) {
return response != null ? getResponseDataNoError(response, false) : null;
}
/**
* Get the body of a HTTP response.
*
* @param response a HTTP response, which can be null
* @param replaceWhitespace true
if {@link TextUtils#replaceWhitespace(String)}
* should be called on the body
* @return the body if the response comes from a successful HTTP request, null
otherwise
*/
@Nullable
public static String getResponseData(@Nullable final HttpResponse response, final boolean replaceWhitespace) {
if (!isSuccess(response)) {
return null;
}
assert response != null; // Caught above
return getResponseDataNoError(response, replaceWhitespace);
}
@Nullable
public static String rfc3986URLEncode(final String text) {
final String encoded = encode(text);
return encoded != null ? StringUtils.replace(encoded.replace("+", "%20"), "%7E", "~") : null;
}
@Nullable
public static String decode(final String text) {
try {
return URLDecoder.decode(text, CharEncoding.UTF_8);
} catch (final UnsupportedEncodingException e) {
Log.e("Network.decode", e);
}
return null;
}
@Nullable
public static String encode(final String text) {
try {
return URLEncoder.encode(text, CharEncoding.UTF_8);
} catch (final UnsupportedEncodingException e) {
Log.e("Network.encode", e);
}
return null;
}
private static ConnectivityManager connectivityManager = null;
/**
* Checks if the device has network connection.
*
* @return true
if the device is connected to the network.
*/
public static boolean isNetworkConnected() {
if (connectivityManager == null) {
// Concurrent assignment would not hurt
connectivityManager = (ConnectivityManager) CgeoApplication.getInstance().getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
}
final NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
return activeNetworkInfo != null && activeNetworkInfo.isConnected();
}
}