package org.commcare.network;
import android.content.Context;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.commcare.data.xml.DataModelPullParser;
import org.commcare.data.xml.TransactionParserFactory;
import org.commcare.logging.AndroidLogger;
import org.commcare.tasks.templates.CommCareTask;
import org.commcare.core.network.bitcache.BitCache;
import org.commcare.core.network.bitcache.BitCacheFactory;
import org.commcare.utils.AndroidCacheDirSetup;
import org.javarosa.core.io.StreamsUtil;
import org.javarosa.core.services.Logger;
import org.javarosa.xml.util.InvalidStructureException;
import org.javarosa.xml.util.UnfullfilledRequirementsException;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.UnknownHostException;
import javax.net.ssl.SSLPeerUnverifiedException;
/**
* @author ctsims
*/
public abstract class HttpCalloutTask<R> extends CommCareTask<Object, String, HttpCalloutTask.HttpCalloutOutcomes, R> {
public enum HttpCalloutOutcomes {
NetworkFailure,
BadResponse,
AuthFailed,
UnknownError,
BadCertificate,
Success,
NetworkFailureBadPassword,
IncorrectPin
}
private final Context c;
public HttpCalloutTask(Context c) {
this.c = c;
TAG = HttpCalloutTask.class.getSimpleName();
}
protected Context getContext() {
return c;
}
@Override
protected HttpCalloutOutcomes doTaskBackground(Object... params) {
HttpCalloutOutcomes preHttpOutcome = doSetupTaskBeforeRequest();
if (preHttpOutcome != null) {
return preHttpOutcome;
}
//Since we can proceed with the task either way, but we
//still wanna know whether it failed
boolean calloutFailed = false;
if (shouldMakeHttpCallout()) {
HttpCalloutOutcomes outcome;
try {
HttpResponse response = doHttpRequest();
int responseCode = response.getStatusLine().getStatusCode();
if (responseCode >= 200 && responseCode < 300) {
outcome = doResponseSuccess(response);
} else if (responseCode == 401) {
outcome = doResponseAuthFailed(response);
} else {
outcome = doResponseOther(response);
}
} catch (ClientProtocolException | UnknownHostException e) {
outcome = HttpCalloutOutcomes.NetworkFailure;
} catch (SSLPeerUnverifiedException e) {
// Couldn't get a valid SSL certificate
outcome = HttpCalloutOutcomes.BadCertificate;
} catch (IOException e) {
//This is probably related to local files, actually
e.printStackTrace();
outcome = HttpCalloutOutcomes.NetworkFailure;
}
//If we needed the callout to succeed and it didn't, return our failure.
if (outcome != HttpCalloutOutcomes.Success) {
//TODO:Cleanup?
if (calloutSuccessRequired()) {
return outcome;
} else {
calloutFailed = true;
}
} else {
if (!processSuccessfulRequest()) {
return HttpCalloutOutcomes.BadResponse;
}
}
}
// So either we didn't need our our HTTP callout or we succeeded. Either way, move on
// to the next step
return doPostCalloutTask(calloutFailed);
}
protected boolean processSuccessfulRequest() {
return true;
}
protected HttpCalloutOutcomes doSetupTaskBeforeRequest() {
return null;
}
protected abstract HttpResponse doHttpRequest() throws ClientProtocolException, IOException;
protected HttpCalloutOutcomes doResponseSuccess(HttpResponse response) throws IOException {
beginResponseHandling(response);
InputStream input = cacheResponseOpenHandle(response);
TransactionParserFactory factory = getTransactionParserFactory();
//this is _really_ coupled, but we'll tolerate it for now because of the absurd performance gains
try {
DataModelPullParser parser = new DataModelPullParser(input, factory, true, false);
parser.parse();
return HttpCalloutOutcomes.Success;
//TODO: These are not great, long term
} catch (InvalidStructureException ise) {
ise.printStackTrace();
Logger.log(AndroidLogger.TYPE_USER, "Invalid response for auth keys: " + ise.getMessage());
return HttpCalloutOutcomes.BadResponse;
} catch (XmlPullParserException e) {
e.printStackTrace();
Logger.log(AndroidLogger.TYPE_USER, "Invalid xml response for auth keys: " + e.getMessage());
return HttpCalloutOutcomes.BadResponse;
} catch (UnfullfilledRequirementsException e) {
e.printStackTrace();
Logger.log(AndroidLogger.TYPE_USER, "Missing requirements when fetching auth keys: " + e.getMessage());
return HttpCalloutOutcomes.BadResponse;
}
}
protected abstract TransactionParserFactory getTransactionParserFactory();
protected InputStream cacheResponseOpenHandle(HttpResponse response) throws IOException {
int dataSizeGuess = -1;
if (response.containsHeader("Content-Length")) {
String length = response.getFirstHeader("Content-Length").getValue();
try {
dataSizeGuess = Integer.parseInt(length);
} catch (Exception e) {
//Whatever.
}
}
BitCache cache = BitCacheFactory.getCache(new AndroidCacheDirSetup(c), dataSizeGuess);
cache.initializeCache();
OutputStream cacheOut = cache.getCacheStream();
StreamsUtil.writeFromInputToOutputNew(response.getEntity().getContent(), cacheOut);
return cache.retrieveCache();
}
protected void beginResponseHandling(HttpResponse response) {
//Nothing unless required
}
protected HttpCalloutOutcomes doResponseAuthFailed(HttpResponse response) {
return HttpCalloutOutcomes.AuthFailed;
}
protected abstract HttpCalloutOutcomes doResponseOther(HttpResponse response);
/** Indicates whether, after doSetupTaskBeforeRequest() is executed, we actually need to
* execute the http callout. If this is false, doSetupTaskBeforeRequest() will just be
* followed by doPostCalloutTask() */
protected abstract boolean shouldMakeHttpCallout();
/**
* Indicates if we need the return status of the http callout to be SUCCESS in order to proceed
*/
protected abstract boolean calloutSuccessRequired();
protected abstract HttpCalloutOutcomes doPostCalloutTask(boolean httpFailed);
}