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); } }