package org.commcare.xml;
import android.net.ParseException;
import android.net.Uri;
import android.util.Pair;
import net.sqlcipher.database.SQLiteDatabase;
import org.commcare.CommCareApplication;
import org.commcare.cases.model.Case;
import org.commcare.engine.references.JavaHttpReference;
import org.commcare.interfaces.HttpRequestEndpoints;
import org.commcare.logging.AndroidLogger;
import org.commcare.android.database.user.models.ACase;
import org.commcare.models.database.user.models.AndroidCaseIndexTable;
import org.commcare.models.database.user.models.EntityStorageCache;
import org.commcare.utils.FileUtil;
import org.commcare.utils.GlobalConstants;
import org.javarosa.core.io.StreamsUtil;
import org.javarosa.core.reference.InvalidReferenceException;
import org.javarosa.core.reference.Reference;
import org.javarosa.core.reference.ReferenceManager;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.storage.IStorageUtilityIndexed;
import org.javarosa.core.util.PropertyUtils;
import org.kxml2.io.KXmlParser;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* @author ctsims
*/
public class AndroidCaseXmlParser extends CaseXmlParser {
private File folder;
private final boolean processAttachments = true;
private HttpRequestEndpoints generator;
private final EntityStorageCache mEntityCache;
private final AndroidCaseIndexTable mCaseIndexTable;
public AndroidCaseXmlParser(KXmlParser parser, IStorageUtilityIndexed storage,
EntityStorageCache entityCache, AndroidCaseIndexTable indexTable) {
super(parser, storage);
mEntityCache = entityCache;
mCaseIndexTable = indexTable;
}
public AndroidCaseXmlParser(KXmlParser parser, IStorageUtilityIndexed storage) {
this(parser, storage, new EntityStorageCache("case"), new AndroidCaseIndexTable());
}
public AndroidCaseXmlParser(KXmlParser parser, boolean acceptCreateOverwrites,
IStorageUtilityIndexed<Case> storage,
HttpRequestEndpoints generator) {
super(parser, acceptCreateOverwrites, storage);
this.generator = generator;
mEntityCache = new EntityStorageCache("case");
mCaseIndexTable = new AndroidCaseIndexTable();
}
@Override
protected void removeAttachment(Case caseForBlock, String attachmentName) {
if (!processAttachments) {
return;
}
//TODO: All of this code should really be somewhere else, too, since we also need to remove attachments on
//purge.
String source = caseForBlock.getAttachmentSource(attachmentName);
//TODO: Handle remote reference download?
if (source == null) {
return;
}
//Handle these cases better later.
try {
ReferenceManager.instance().DeriveReference(source).remove();
} catch (InvalidReferenceException | IOException e) {
e.printStackTrace();
}
}
protected SQLiteDatabase getDbHandle() {
return CommCareApplication.instance().getUserDbHandle();
}
@Override
public void commit(Case parsed) throws IOException {
SQLiteDatabase db;
db = getDbHandle();
db.beginTransaction();
try {
super.commit(parsed);
mEntityCache.invalidateCache(String.valueOf(parsed.getID()));
mCaseIndexTable.clearCaseIndices(parsed);
mCaseIndexTable.indexCase(parsed);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
@Override
protected String processAttachment(String src, String from, String name, KXmlParser parser) {
if (!processAttachments) {
return null;
}
//We need to figure out whether or not the attachment is local to the device or in a remote location.
if (CaseXmlParser.ATTACHMENT_FROM_LOCAL.equals(from)) {
//Parse from the local environment
if (folder == null) {
return null;
}
File source = new File(folder, src);
Pair<File, String> dest = getDestination(source.getName());
try {
FileUtil.copyFile(source, dest.first, null, null);
} catch (IOException e) {
e.printStackTrace();
return null;
}
return dest.second;
} else if (CaseXmlParser.ATTACHMENT_FROM_REMOTE.equals(from)) {
//The attachment is in remote location.
try {
Reference remote = ReferenceManager.instance().DeriveReference(src);
//TODO: Awful.
if (remote instanceof JavaHttpReference) {
((JavaHttpReference)remote).setHttpRequestor(generator);
}
//TODO: Proper URL here
Pair<File, String> dest = getDestination(src);
boolean readAttachment = false;
int tries = 2;
for (int i = 1; i <= tries; i++) {
//Delete any existing file where the incoming file is going
//(only relevant if we're retrying)
if (dest.first.exists()) {
dest.first.delete();
}
try {
dest.first.createNewFile();
} catch (IOException fe) {
Logger.log(AndroidLogger.TYPE_RESOURCES, "Couldn't create placeholder for new file at " + dest.first.getAbsolutePath());
}
try {
StreamsUtil.writeFromInputToOutputNew(remote.getStream(), new FileOutputStream(dest.first));
readAttachment = true;
break;
} catch (IOException e) {
Logger.log(AndroidLogger.TYPE_WARNING_NETWORK, "Failed reading (attempt #" + tries + ") attachment from " + src);
}
}
if (!readAttachment) {
Logger.log(AndroidLogger.TYPE_WARNING_NETWORK, "Failed to read attachment from " + src);
return null;
}
return dest.second;
//TODO: Don't Pass code review without fixing this exception handling
} catch (ParseException e) {
} catch (InvalidReferenceException e) {
//We can't go fetch this resource because we don't have access to the reference type
Logger.log(AndroidLogger.TYPE_ERROR_DESIGN, "Couldn't find attachment at reference " + e.getReferenceString());
return null;
}
return null;
}
return null;
}
/**
* Find the location for a local attachment. This location will be in the attachment
* folder, and will share the extension of the source file if available. The filename
* will be randomized, however.
*
* @param source the full path of the source of the attachment.
*/
private Pair<File, String> getDestination(String source) {
File storagePath = new File(CommCareApplication.instance().getCurrentApp().fsPath(GlobalConstants.FILE_CC_ATTACHMENTS));
String dest = PropertyUtils.genUUID().replace("-", "");
//add an extension
String fileName = Uri.parse(source).getLastPathSegment();
if (fileName != null) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot != -1) {
dest += fileName.substring(lastDot);
}
}
return new Pair<>(new File(storagePath, dest), GlobalConstants.ATTACHMENT_REF + dest);
}
@Override
protected Case buildCase(String name, String typeId) {
return new ACase(name, typeId);
}
}