/**
*
*/
package org.commcare.android.models.logic;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.spec.SecretKeySpec;
import org.commcare.android.database.SqlStorage;
import org.commcare.android.database.user.models.ACase;
import org.commcare.android.database.user.models.FormRecord;
import org.commcare.android.tasks.ExceptionReportTask;
import org.commcare.android.util.FormUploadUtil;
import org.commcare.cases.ledger.Ledger;
import org.commcare.dalvik.application.CommCareApplication;
import org.commcare.data.xml.DataModelPullParser;
import org.commcare.data.xml.TransactionParser;
import org.commcare.data.xml.TransactionParserFactory;
import org.commcare.xml.AndroidCaseXmlParser;
import org.commcare.xml.LedgerXmlParsers;
import org.commcare.xml.util.InvalidStructureException;
import org.commcare.xml.util.UnfullfilledRequirementsException;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.storage.StorageFullException;
import org.kxml2.io.KXmlParser;
import org.xmlpull.v1.XmlPullParserException;
import android.content.Context;
import android.content.Intent;
import android.util.Pair;
/**
* A FormRecordProcessor keeps track of all of the logic needed to process
* forms and keep track of models/changes.
*
* TODO: We should move most of the "cleanup" task methods here.
*
* @author ctsims
*
*/
public class FormRecordProcessor {
private Context c;
SqlStorage<FormRecord> storage;
public FormRecordProcessor(Context c) {
this.c = c;
storage = CommCareApplication._().getUserStorage(FormRecord.class);
}
/**
* This is the entry point for processing a form. New transaction types should all be declared here.
*
* @param record
* @return
* @throws InvalidStructureException
* @throws IOException
* @throws XmlPullParserException
* @throws UnfullfilledRequirementsException
* @throws StorageFullException
*/
public FormRecord process(FormRecord record) throws InvalidStructureException, IOException, XmlPullParserException, UnfullfilledRequirementsException, StorageFullException {
String form = record.getPath(c);
DataModelPullParser parser;
final File f = new File(form);
final Cipher decrypter = FormUploadUtil.getDecryptCipher((new SecretKeySpec(record.getAesKey(), "AES")));
InputStream is = new CipherInputStream(new FileInputStream(f), decrypter);
parser = new DataModelPullParser(is, new TransactionParserFactory() {
public TransactionParser getParser(String name, String namespace, KXmlParser parser) {
if(LedgerXmlParsers.STOCK_XML_NAMESPACE.equals(namespace)) {
return new LedgerXmlParsers(parser, CommCareApplication._().getUserStorage(Ledger.STORAGE_KEY, Ledger.class));
}else if(name.toLowerCase().equals("case")) {
return new AndroidCaseXmlParser(parser, CommCareApplication._().getUserStorage(ACase.STORAGE_KEY, ACase.class), decrypter, null, f.getParentFile());
}
return null;
}
}, true, true);
parser.parse();
is.close();
//Let anyone who is listening know!
Intent i = new Intent("org.commcare.dalvik.api.action.data.update");
this.c.sendBroadcast(i);
return updateRecordStatus(record, FormRecord.STATUS_UNSENT);
}
public FormRecord updateRecordStatus(FormRecord record, String newStatus) throws IOException, StorageFullException{
//update the records to show that the form has been processed and is ready to be sent;
record = record.updateStatus(record.getInstanceURI().toString(), newStatus);
storage.write(record);
return record;
}
public FormRecord getRecord(int dbId) {
//this seems silly.
return storage.read(dbId);
}
/**
* Performs deep checks on the current form data to establish whether or not the files are in a consistent state.
* Returns a (human readable) report if not to aid in debugging
*
* @param r A Form Record to process
* @return A tuple whose first argument is a boolean specifying whether the record has passed the verification process.
* The second argument is a human readable report for debugging.
*/
public Pair<Boolean, String> verifyFormRecordIntegrity(FormRecord r) {
StringBuilder reporter = new StringBuilder();
try {
reporter.append("\n" + r.toString() + "\n");
String formPath;
try {
//make sure we can retrieve a record.
formPath = r.getPath(c);
} catch (FileNotFoundException e) {
e.printStackTrace();
reporter.append("ERROR - No file path found for form record. " + e.getMessage() + "\n");
return new Pair<Boolean, String>(false, reporter.toString());
}
//now, make sure there's a file there
File recordFile = new File(formPath);
if(!recordFile.exists()) {
reporter.append("ERROR - No form at file path provided\n");
return new Pair<Boolean, String>(false, reporter.toString());
}
//Give us the info about all of the files in this instance
reporter.append("\n-File Report-\n");
File folder = recordFile.getParentFile();
for(File f : folder.listFiles()) {
reporter.append(String.format("File:%s \n[Size:%s]\n[LastTouched:%s]\n",f.getName(), String.valueOf(f.length()), new Date(f.lastModified()).toString()));
}
reporter.append("\n-Instance Report-\n");
reporter.append(String.format("Size on Disk:%s\n", String.valueOf(recordFile.length())));
if(!performLinearFileScan(r, recordFile, false, reporter,"Encrypted Instance File")) { return new Pair(false, reporter.toString()); }
if(!performLinearFileScan(r, recordFile, true, reporter, "Decrypted Instance File")) { return new Pair(false, reporter.toString()); }
if(!attemptXmlScan(r, recordFile, reporter)) { return new Pair(false, reporter.toString()); }
return new Pair(true, reporter.toString());
} catch(Exception e) {
return new Pair(false, "Error while preparing attached integrity report: " + e.getMessage() + "\n" + reporter.toString());
}
}
private boolean performLinearFileScan(FormRecord r, File recordFile, boolean useCipher, StringBuilder reporter, String label) {
//Try to read the actual bytes inline
InputStream is = null;
byte[] buffer = new byte[512];
try {
//decrypter
if(useCipher) {
Cipher decrypter = FormUploadUtil.getDecryptCipher((new SecretKeySpec(r.getAesKey(), "AES")));
is = new CipherInputStream(new FileInputStream(recordFile), decrypter);
} else{
is = new FileInputStream(recordFile);
}
long accumulated = 0;
int read = 0;
while(read != -1) {
accumulated += read;
read = is.read(buffer);
}
reporter.append("PASS: Linear scan of " + label+ ". " + accumulated + " bytes read in total\n");
return true;
}catch(Exception e) {
reporter.append("FAILURE: Error during linear scan of " + label + "\n" + ExceptionReportTask.getStackTrace(e));
return false;
} finally {
try {if(is != null) { is.close(); }} catch(IOException ioe) {}
}
}
private boolean attemptXmlScan(FormRecord r, File recordFile, StringBuilder reporter) {
KXmlParser parser = new KXmlParser();
InputStream is = null;
try {
Cipher decrypter = FormUploadUtil.getDecryptCipher((new SecretKeySpec(r.getAesKey(), "AES")));
is = new CipherInputStream(new FileInputStream(recordFile), decrypter);
parser.setInput(is,"UTF-8");
parser.setFeature(KXmlParser.FEATURE_PROCESS_NAMESPACES, true);
while(parser.next() != KXmlParser.END_DOCUMENT){
//nothing
}
reporter.append("PASS: Instance file reads as valid XML\n");
return true;
} catch (Exception e) {
reporter.append("FAILURE: XML Instance file could not be validated\n" + ExceptionReportTask.getStackTrace(e));
return false;
} finally {
try {if(is != null) { is.close(); }} catch(IOException ioe) {}
}
}
}