package com.hokolinks.utils.networking.async;
import com.hokolinks.Hoko;
import com.hokolinks.model.App;
import com.hokolinks.model.Device;
import com.hokolinks.model.exceptions.HokoException;
import com.hokolinks.utils.log.HokoLog;
import com.hokolinks.utils.networking.Networking;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.zip.GZIPInputStream;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
/**
* HttpRequest is a savable model around HttpRequests.
* It contains the path, an operation type, parameters in the form of json and the number of
* retries.
*/
public class HttpRequest implements Serializable, HostnameVerifier{
// Constants
private static final int TASK_TIMEOUT = 15000; // millis
private static final String TASK_VERSION = "v2";
private static final String TASK_FORMAT = "json";
private static String sTaskEndpoint = "https://api.hokolinks.com";
// Properties
private HokoNetworkOperationType mOperationType;
private String mUrl;
private String mToken;
private String mParameters;
private int mNumberOfRetries;
/**
* Creates a request with a type, path, token and parameters.
*
* @param operationType The operation type (e.g. GET/PUT/POST).
* @param url The url (e.g. "https://api.hokolinks.com/v1/routes.json").
* @param token The application token.
* @param parameters The parameters in json string form.
*/
public HttpRequest(HokoNetworkOperationType operationType, String url, String token,
String parameters) {
mOperationType = operationType;
mUrl = url.contains("http") ? url : HttpRequest.getURLFromPath(url);
mToken = token;
mParameters = parameters;
mNumberOfRetries = 0;
}
// Constructors
public static void setEndpoint(String endpoint) {
sTaskEndpoint = endpoint;
}
/**
* Generates the full URL, merging the endpoint, version, path and format.
*
* @param path The path component.
* @return The full URL.
*/
public static String getURLFromPath(String path) {
return sTaskEndpoint + "/" + TASK_VERSION + "/" + path + "."
+ TASK_FORMAT;
}
private static String getUserAgent() {
String environment = App.getEnvironment(Networking.getNetworking().getContext());
return "HOKO/" + Hoko.VERSION + " (" + environment + "; Linux; " + Device.getPlatform() + " " +
Device.getSystemReleaseVersion() + "; " + Device.getVendor() + " " + Device.getModel() +
" Build/" + Device.getBuildNumber() + ")";
}
private static String urlEncode(String url, String jsonString) {
if (jsonString == null) {
return url;
}
String finalURL = url;
try {
JSONObject jsonObject = new JSONObject(jsonString);
if (jsonObject.names().length() > 0) {
finalURL += "?";
}
for (Iterator<String> iterator = jsonObject.keys(); iterator.hasNext(); ) {
String key = iterator.next();
finalURL += key + "=" + jsonObject.getString(key);
if (iterator.hasNext()) {
finalURL += "&";
}
}
} catch (JSONException e) {
HokoLog.e(e);
}
return finalURL;
}
// Property Gets
public HokoNetworkOperationType getOperationType() {
return mOperationType;
}
public URL getUrl() {
try {
if (mOperationType != HokoNetworkOperationType.GET) {
return new URL(mUrl);
} else {
return new URL(urlEncode(mUrl, getParameters()));
}
} catch (MalformedURLException e) {
HokoLog.e(e);
}
return null;
}
public String getToken() {
return mToken;
}
// Runnable
public String getParameters() {
return mParameters;
}
public int getNumberOfRetries() {
return mNumberOfRetries;
}
// Networking
public void incrementNumberOfRetries() {
mNumberOfRetries++;
}
/**
* Transforms the HttpRequest to a Runnable object so it can execute the request
* on a background thread, usually inside a NetworkAsyncTask object.
*
* @return The runnable wrapper for the request.
*/
public Runnable toRunnable() {
return toRunnable(null);
}
/**
* Transforms the HttpRequest to a Runnable object with a callback so it can execute the
* request on a background thread, usually inside a NetworkAsyncTask object. It will then call
* the callback functions accordingly.
*
* @param httpCallback The HttpRequestCallback object.e
* @return The runnable wrapper for the request.
*/
public Runnable toRunnable(final HttpRequestCallback httpCallback) {
return new Runnable() {
@Override
public void run() {
try {
switch (mOperationType) {
case GET:
performGET(httpCallback);
break;
case POST:
performPOST(httpCallback);
break;
case PUT:
performPUT(httpCallback);
break;
default:
break;
}
} catch (IOException e) {
HokoLog.e(e);
if (httpCallback != null)
httpCallback.onFailure(e);
}
}
};
}
private void applyHeaders(HttpURLConnection connection, boolean postOrPut) {
connection.setConnectTimeout(TASK_TIMEOUT);
connection.setReadTimeout(TASK_TIMEOUT);
connection.setRequestProperty("Accept", "application/json");
connection.setRequestProperty("Accept-Encoding", "gzip, deflate");
if (postOrPut) {
connection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
}
if (getToken() != null) {
connection.setRequestProperty("Authorization", "Token " + getToken());
connection.setRequestProperty("Hoko-SDK-Version", Hoko.VERSION);
if (Networking.getNetworking() != null) {
connection.setRequestProperty("User-Agent", HttpRequest.getUserAgent());
connection.setRequestProperty("Hoko-SDK-Env",
App.getEnvironment(Networking.getNetworking().getContext()));
}
}
}
private void applyHttpsHostnameVerifier(HttpURLConnection connection, URL url) {
if (url.getProtocol().equals("https")) {
HttpsURLConnection httpsURLConnection = (HttpsURLConnection)connection;
httpsURLConnection.setHostnameVerifier(this);
}
}
/**
* Performs an HttpGet to the specified url, will handle the response with the callback.
*
* @param httpCallback The HttpRequestCallback object.
* @throws IOException Throws an IOException in case of a network problem.
*/
private void performGET(HttpRequestCallback httpCallback) throws IOException {
URL url = getUrl();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
applyHttpsHostnameVerifier(connection, url);
connection.setRequestMethod("GET");
applyHeaders(connection, false);
HokoLog.d("GET from " + getUrl());
handleHttpResponse(connection, httpCallback);
}
/**
* Performs an HttpPut to the specified url, will handle the response with the callback.
*
* @param httpCallback The HttpRequestCallback object.
* @throws IOException Throws an IOException in case of a network problem.
*/
private void performPUT(HttpRequestCallback httpCallback) throws IOException {
URL url = getUrl();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
applyHttpsHostnameVerifier(connection, url);
connection.setRequestMethod("PUT");
applyHeaders(connection, true);
if (getParameters() != null) {
connection.setDoOutput(true);
OutputStreamWriter outputStreamWriter =
new OutputStreamWriter(connection.getOutputStream());
outputStreamWriter.write(getParameters());
outputStreamWriter.flush();
outputStreamWriter.close();
}
HokoLog.d("PUT to " + getUrl());
handleHttpResponse(connection, httpCallback);
}
/**
* Performs an HttpPost to the specified url, will handle the response with the callback.
*
* @param httpCallback The HttpRequestCallback object.
* @throws IOException Throws an IOException in case of a network problem.
*/
private void performPOST(HttpRequestCallback httpCallback) throws IOException {
URL url = getUrl();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
applyHttpsHostnameVerifier(connection, url);
connection.setRequestMethod("POST");
applyHeaders(connection, true);
HokoLog.d("POST to " + getUrl() + " with " + getParameters());
if (getParameters() != null) {
connection.setDoOutput(true);
OutputStreamWriter outputStreamWriter =
new OutputStreamWriter(connection.getOutputStream());
outputStreamWriter.write(getParameters());
outputStreamWriter.flush();
outputStreamWriter.close();
}
handleHttpResponse(connection, httpCallback);
}
/**
* The HttpResponse handler, tries to parse the response into json, checks the status code and
* throws exceptions accordingly. Will also use the callback to notify of the response given.
*
* @param connection The HttpURLConnection object coming from a GET/POST/PUT URL connection.
* @param httpCallback The HttpRequestCallback object.
* @throws IOException Throws an IOException in case of a network problem.
*/
private void handleHttpResponse(HttpURLConnection connection, HttpRequestCallback httpCallback)
throws IOException {
InputStream input = connection.getErrorStream();
if (input == null) {
input = connection.getInputStream();
}
if ("gzip".equals(connection.getContentEncoding())) {
input = new GZIPInputStream(input);
}
String response = convertStreamToString(input);
JSONObject jsonResponse;
try {
jsonResponse = new JSONObject(response);
} catch (JSONException e) {
try {
jsonResponse = new JSONArray(response).getJSONObject(0);
} catch (JSONException e2) {
jsonResponse = new JSONObject();
}
}
if (connection.getResponseCode() >= 300) {
HokoException exception = HokoException.serverException(jsonResponse);
HokoLog.e(exception);
if (httpCallback != null) {
httpCallback.onFailure(exception);
}
} else {
if (httpCallback != null)
httpCallback.onSuccess(jsonResponse);
}
}
private String convertStreamToString(InputStream is) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
try {
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return sb.toString();
}
@Override
public boolean verify(String hostname, SSLSession session) {
return sTaskEndpoint.contains(hostname);
}
// Type Enum
/**
* The possible network operation types: GET, POST and PUT.
* Serializable for saving HokoHttpRequests to file.
*/
public enum HokoNetworkOperationType implements Serializable {
GET, POST, PUT
}
}