package com.radicaldynamic.groupinform.tasks;
import java.io.FileReader;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import org.ektorp.Attachment;
import org.ektorp.DbAccessException;
import org.supercsv.io.CsvListReader;
import org.supercsv.io.ICsvListReader;
import org.supercsv.prefs.CsvPreference;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import com.mycila.xmltool.XMLDoc;
import com.mycila.xmltool.XMLDocBuilder;
import com.mycila.xmltool.XMLTag;
import com.radicaldynamic.gcmobile.android.activities.DataImportActivity;
import com.radicaldynamic.groupinform.application.Collect;
import com.radicaldynamic.groupinform.documents.FormInstance;
import com.radicaldynamic.groupinform.documents.Generic;
import com.radicaldynamic.groupinform.listeners.DataImportListener;
import com.radicaldynamic.groupinform.logic.AccountDevice;
import com.radicaldynamic.groupinform.utilities.Base64Coder;
import com.radicaldynamic.groupinform.xform.Field;
import com.radicaldynamic.groupinform.xform.FormReader;
import com.radicaldynamic.groupinform.xform.FormWriter;
import com.radicaldynamic.groupinform.xform.Instance;
public class DataImportTask extends AsyncTask<Void, String, ArrayList<List<String>>>
{
private static final String t = "DataImportTask: ";
public final static int COMPLETE = 0;
public final static int ERROR = 1;
public final static int PROGRESS = 2;
private DataImportListener mStateListener;
private Handler mHandler;
private String mImportFilePath;
private String mImportMsg;
private int mImportMode = -1;
private boolean mImportSuccessful = false;
private String mFormDefinitionId;
// Import options
private Bundle mImportOptions;
private Bundle mFormSetup;
private FormReader mFormReader;
private XMLTag mInstanceXML;
private Map<String, Integer> mFieldImportMap = new HashMap<String, Integer>();
@Override
protected ArrayList<List<String>> doInBackground(Void... arg0)
{
final String tt = t + "doInBackground(): ";
ArrayList<List<String>> records = new ArrayList<List<String>>();
List<String> line;
int lineNumber = 0;
try {
publishProgress("Reading CSV file...");
ICsvListReader inFile = new CsvListReader(new FileReader(mImportFilePath), CsvPreference.EXCEL_PREFERENCE);
// If the user doesn't want to import the first line, skip it and discard
if (mImportOptions.getBoolean(DataImportActivity.KEY_IMPORT_OPTION_SFR, false)) {
inFile.getCSVHeader(true);
}
switch (mImportMode) {
case DataImportListener.MODE_PREVIEW:
while ((line = inFile.read()) != null) {
lineNumber = inFile.getLineNumber();
if (Collect.Log.VERBOSE) Log.v(Collect.LOGTAG, tt + "previewing line " + lineNumber + ": " + line.toString());
publishProgress("Previewing row " + lineNumber + "/5");
records.add(new ArrayList<String>(line));
// Only read a few lines in
if (lineNumber > 4)
break;
}
break;
case DataImportListener.MODE_VERIFY:
List<String> allEmailAddresses = new ArrayList<String>();
// Gather list of email addresses associated with active or unused device profiles
for (AccountDevice device : Collect.getInstance().getInformOnlineState().getAccountDevices().values()
.toArray(new AccountDevice[Collect.getInstance().getInformOnlineState().getAccountDevices().values().size()])) {
if (device.getStatus().contains("active") || device.getStatus().contains("unused"))
allEmailAddresses.add(device.getEmail());
}
while ((line = inFile.read()) != null) {
lineNumber = inFile.getLineNumber();
if (Collect.Log.VERBOSE) Log.v(Collect.LOGTAG, tt + "verifying line " + lineNumber + ": " + line.toString());
publishProgress("Verifying row " + lineNumber);
// Verify form assignment
if (mFormSetup.getInt(DataImportActivity.KEY_FORM_SETUP_ASSIGNMENT, 0) > 0) {
// 1, 2, 3, ...
int column = mFormSetup.getInt(DataImportActivity.KEY_FORM_SETUP_ASSIGNMENT, 0);
column--;
if (Collect.Log.VERBOSE) Log.v(Collect.LOGTAG, tt + "verify assignment from column " + (column + 1) + " (" + line.get(column) + ")");
// Split up potential device profile identifiers
String [] emailAddresses = line.get(column).split("\\s+");
for (int i = 0; i < emailAddresses.length; i++) {
String address = emailAddresses[i].trim().toLowerCase();
if (!allEmailAddresses.contains(address)) {
mImportMsg = startErrorMsg(lineNumber, column + 1) + address + " does not belong to a device profile in this account.\n\nPlease check that your import file and new form setup options are correct and try again.";
return null;
}
}
}
// Verify form status (be liberal)
if (mFormSetup.getInt(DataImportActivity.KEY_FORM_SETUP_STATUS, 0) > 1) {
int column = mFormSetup.getInt(DataImportActivity.KEY_FORM_SETUP_STATUS, 0);
column = column - 2;
if (Collect.Log.VERBOSE) Log.v(Collect.LOGTAG, tt + "verify form status from column " + (column + 1) + " (" + line.get(column) + ")");
String status = line.get(column).trim().toLowerCase();
if (!status.equals("draft")
&& !status.equals("incomplete")
&& !status.equals("complete")
&& !status.equals("completed")) {
mImportMsg = startErrorMsg(lineNumber, column + 1) + "\"" + line.get(column).trim() + "\" is not a valid form status. Expected draft or complete.\n\nPlease check that your import file and new form setup options are correct and try again.";
return null;
}
}
// Verify field-to-column import mappings
for (String location : mFieldImportMap.keySet().toArray(new String[mFieldImportMap.keySet().size()])) {
// Value from CSV file to test against
Integer column = mFieldImportMap.get(location);
if (column > 0) {
column--;
// Field (with information that tells us what values it expects)
Field f = mFormReader.getFlatFieldIndex().get(location);
if (Collect.Log.VERBOSE) Log.v(Collect.LOGTAG, tt + "verify field-to-column mapping " + (column + 1) + " (" + line.get(column) + ") for " + f.getType() + "." + f.getBind().getType());
}
}
}
break;
case DataImportListener.MODE_IMPORT:
Map<String, String> emailProfileIdMap = new HashMap<String, String>();
// Gather list of email addresses associated with active or unused device profiles
for (AccountDevice device : Collect.getInstance().getInformOnlineState().getAccountDevices().values()
.toArray(new AccountDevice[Collect.getInstance().getInformOnlineState().getAccountDevices().values().size()])) {
emailProfileIdMap.put(device.getEmail(), device.getId());
}
// Prepare to preserve order of records
Calendar calendar = Calendar.getInstance();
SimpleDateFormat formatter = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG);
formatter.setTimeZone(TimeZone.getDefault());
formatter.applyPattern(Generic.DATETIME);
while ((line = inFile.read()) != null) {
lineNumber = inFile.getLineNumber();
if (Collect.Log.VERBOSE) Log.v(Collect.LOGTAG, tt + "importing line " + lineNumber + ": " + line.toString());
publishProgress("Importing row " + lineNumber);
FormInstance fi = new FormInstance();
fi.setFormId(mFormDefinitionId);
// Increment time to represent order of records in CSV file
if (mImportOptions.getBoolean(DataImportActivity.KEY_IMPORT_OPTION_PRO, false)) {
fi.setDateCreated(formatter.format(calendar.getTime()));
calendar.add(Calendar.SECOND, 1);
}
// Set form assignments
int assignment = mFormSetup.getInt(DataImportActivity.KEY_FORM_SETUP_ASSIGNMENT, 0);
if (assignment > 0) {
List<String> toAssign = new ArrayList<String>();
String [] emailAddresses = line.get(assignment - 1).trim().toLowerCase().split("\\s+");
for (int i = 0; i < emailAddresses.length; i++) {
toAssign.add(emailProfileIdMap.get(emailAddresses[i].trim()));
}
fi.setAssignedTo(toAssign);
}
// Set form names
int name = mFormSetup.getInt(DataImportActivity.KEY_FORM_SETUP_NAME, 0);
if (name > 0) {
fi.setName(line.get(name - 1).trim());
}
// Set form status
int status = mFormSetup.getInt(DataImportActivity.KEY_FORM_SETUP_STATUS, 0);
if (status == 0) {
fi.setStatus(FormInstance.Status.draft);
} else if (status == 1) {
fi.setStatus(FormInstance.Status.complete);
} else if (status > 1) {
String userDefinedStatus = line.get(status - 2).trim().toLowerCase();
if (userDefinedStatus.equals("draft") || userDefinedStatus.equals("incomplete"))
fi.setStatus(FormInstance.Status.draft);
else if (userDefinedStatus.equals("complete") || userDefinedStatus.equals("completed"))
fi.setStatus(FormInstance.Status.complete);
}
// Create XForm instance
XMLDocBuilder instanceDoc = XMLDoc.newDocument(true);
mInstanceXML = instanceDoc.addRoot(mFormReader.getInstanceRoot());
mInstanceXML.gotoRoot().addAttribute("id", mFormReader.getInstanceRootId());
generateInstanceXML(null, line);
fi.addInlineAttachment(new Attachment("xml", new String(Base64Coder.encode(mInstanceXML.toBytes())).toString(), FormWriter.CONTENT_TYPE));
Collect.getInstance().getDbService().getDb().create(fi);
}
mImportMsg = "Import complete. " + lineNumber + " rows processed.";
break;
}
inFile.close();
mImportSuccessful = true;
} catch (DbAccessException e) {
mImportMsg = "Failure while writing to database:\n\n" + e.toString() + "\n\nError occured somewhere around line number " + lineNumber;
if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "error while reading CSV file for import: " + e.toString());
e.printStackTrace();
} catch (Exception e) {
mImportMsg = "Unexpected error countered:\n\n" + e.toString() + "\n\nError occured somewhere around line number " + lineNumber;
if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "error while reading CSV file for import: " + e.toString());
e.printStackTrace();
}
return records;
}
@Override
protected void onPostExecute(ArrayList<List<String>> records)
{
synchronized (this) {
if (mStateListener != null) {
Message done = Message.obtain();
done.what = COMPLETE;
mHandler.sendMessage(done);
Bundle b = new Bundle();
b.putString(DataImportListener.MESSAGE, mImportMsg);
b.putInt(DataImportListener.MODE, mImportMode);
b.putBoolean(DataImportListener.SUCCESSFUL, mImportSuccessful);
mStateListener.importTaskFinished(b, records);
}
}
}
@Override
protected void onProgressUpdate(String... values)
{
Message update = Message.obtain();
update.what = PROGRESS;
Bundle data = new Bundle();
data.putString(DataImportActivity.KEY_PROGRESS_MSG, values[0]);
update.setData(data);
mHandler.sendMessage(update);
}
public void setFieldImportMap(Map<String, Integer> map)
{
mFieldImportMap = map;
}
public void setFormDefinitionId(String id)
{
mFormDefinitionId = id;
}
public void setFormReader(FormReader fr)
{
mFormReader = fr;
}
public void setFormSetup(Bundle b)
{
mFormSetup = b;
}
public void setHandler(Handler h)
{
mHandler = h;
}
public void setImportFilePath(String filePath)
{
mImportFilePath = filePath;
}
public void setImportMode(int m)
{
mImportMode = m;
}
public void setImportOptions(Bundle b)
{
mImportOptions = b;
}
public void setListener(DataImportListener sl)
{
synchronized (this) {
mStateListener = sl;
}
}
private void generateInstanceXML(Instance incomingInstance, List<String> recordRow)
{
Iterator<Instance> instanceIterator;
if (incomingInstance == null)
instanceIterator = mFormReader.getInstance().iterator();
else
instanceIterator = incomingInstance.getChildren().iterator();
while (instanceIterator.hasNext()) {
Instance i = instanceIterator.next();
// Don't output instance tags representing repeated data
if (i.getChildren().isEmpty()) {
/*
* For some reason unknown to me we can only call gotoParent() when adding
* an empty tag. Calling it after adding an empty tag OR a tag with a text
* value causes the nesting to get screwed up. This doesn't make any sense
* to me. It might be a bug with xmltool.
*/
// Hidden instance field OR no field import map entry
if (i.getField() == null || !mFieldImportMap.containsKey(i.getField().getLocation())) {
mInstanceXML.addTag(i.getName());
mInstanceXML.gotoParent();
} else {
// If this location is recorded in the field import map it will have a corresponding column value
Integer column = mFieldImportMap.get(i.getField().getLocation());
if (column > 0) {
// Import user defined value
column--;
mInstanceXML.addTag(i.getName()).setText(recordRow.get(column).trim());
} else {
// Use template default
mInstanceXML.addTag(i.getName());
mInstanceXML.gotoParent();
}
}
}
}
}
private String startErrorMsg(int row, int column)
{
return "Error at row " + row + ", column " + column + ":\n\n";
}
}