package org.commcare.xml;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import org.commcare.CommCareApplication;
import org.commcare.data.xml.TransactionParser;
import org.commcare.android.database.user.models.FormRecord;
import org.commcare.provider.InstanceProviderAPI;
import org.commcare.provider.InstanceProviderAPI.InstanceColumns;
import org.commcare.utils.FileUtil;
import org.javarosa.core.services.storage.IStorageUtilityIndexed;
import org.javarosa.xml.util.InvalidStructureException;
import org.kxml2.io.KXmlParser;
import org.kxml2.io.KXmlSerializer;
import org.kxml2.kdom.Document;
import org.kxml2.kdom.Element;
import org.kxml2.kdom.Node;
import org.xmlpull.v1.XmlPullParserException;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
/**
* @author ctsims
*/
public class FormInstanceXmlParser extends TransactionParser<FormRecord> {
private final Context c;
private IStorageUtilityIndexed<FormRecord> storage;
/**
* An unmodifiable mapping from an installed form's namespace its install
* path.
*/
private final Map<String, String> namespaceToInstallPath;
private static int parseCount = 0;
/**
* Root directory for where instances of forms should be saved
*/
private final String rootInstanceDir;
public FormInstanceXmlParser(KXmlParser parser, Context c,
Map<String, String> namespaceToInstallPath,
String destination) {
super(parser);
this.c = c;
this.namespaceToInstallPath = namespaceToInstallPath;
this.rootInstanceDir = destination;
}
@Override
public FormRecord parse() throws InvalidStructureException, IOException, XmlPullParserException {
String xmlns = parser.getNamespace();
//Parse this subdocument into a dom
Element element = new Element();
element.setName(parser.getName());
element.setNamespace(parser.getNamespace());
element.parse(this.parser);
//Consume the end tag.
//this.parser.next();
//create an actual document out of it.
Document document = new Document();
document.addChild(Node.ELEMENT, element);
KXmlSerializer serializer = new KXmlSerializer();
String filePath = getInstanceDestination(namespaceToInstallPath.get(xmlns));
//Register this instance for inspection
ContentValues values = new ContentValues();
values.put(InstanceColumns.DISPLAY_NAME, "Historical Form");
values.put(InstanceColumns.SUBMISSION_URI, "");
values.put(InstanceColumns.INSTANCE_FILE_PATH, filePath);
values.put(InstanceColumns.JR_FORM_ID, xmlns);
values.put(InstanceColumns.STATUS, InstanceProviderAPI.STATUS_COMPLETE);
values.put(InstanceColumns.CAN_EDIT_WHEN_COMPLETE, false);
// Unindexed flag tells content provider to link this instance to a
// new, unindexed form record that isn't attached to the
// AndroidSessionWrapper
values.put(InstanceProviderAPI.UNINDEXED_SUBMISSION, true);
Uri instanceRecord =
c.getContentResolver().insert(InstanceColumns.CONTENT_URI, values);
// Find the form record attached to the form instance during insertion
IStorageUtilityIndexed<FormRecord> storage = cachedStorage();
FormRecord attachedRecord =
storage.getRecordForValue(FormRecord.META_INSTANCE_URI,
instanceRecord.toString());
if (attachedRecord == null) {
throw new RuntimeException("No FormRecord was attached to the inserted form instance");
}
OutputStream o = new FileOutputStream(filePath);
BufferedOutputStream bos = null;
try {
Cipher encrypter = Cipher.getInstance("AES");
SecretKeySpec key = new SecretKeySpec(attachedRecord.getAesKey(), "AES");
encrypter.init(Cipher.ENCRYPT_MODE, key);
CipherOutputStream cos = new CipherOutputStream(o, encrypter);
bos = new BufferedOutputStream(cos, 1024 * 256);
serializer.setOutput(bos, "UTF-8");
document.write(serializer);
} catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException e) {
// writing the form instance to xml failed, so remove the record
storage.remove(attachedRecord);
throw new RuntimeException(e.getMessage());
} finally {
//since bos might not have even been created.
if (bos != null) {
bos.close();
} else {
o.close();
}
}
return attachedRecord;
}
private IStorageUtilityIndexed<FormRecord> cachedStorage() {
if (storage == null) {
storage = CommCareApplication.instance().getUserStorage(FormRecord.class);
}
return storage;
}
/**
* Path for where a particular form instance should be stored. Creates a
* directory using the form's namespace id and the current time and returns
* a path pointing to an xml file of the same name inside that directory.
*
* Path should look something like:
* /app/{app-id}/formdata/{form-id}_{time}/{form-id}_time.xml
*
* @param formPath Path to xml file defining a form.
* @return Absolute path to file where the instance of a given form should
* be saved.
*/
private String getInstanceDestination(String formPath) {
// parseCount makes sure two instances of the same form, parsed in the
// same second don't get placed in the same file.
String time = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(Calendar.getInstance().getTime()) + parseCount++;
String formId = formPath.substring(formPath.lastIndexOf(File.separator) + 1,
formPath.lastIndexOf('.'));
String filename = formId + "_" + time;
String formInstanceDir = rootInstanceDir + filename;
if (FileUtil.createFolder(formInstanceDir)) {
return new File(formInstanceDir + File.separator + filename + ".xml").getAbsolutePath();
}
throw new RuntimeException("Couldn't create folder needed to save form instance");
}
@Override
protected void commit(FormRecord parsed) throws IOException {
//This is unused.
}
}