package org.commcare.xml; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import javax.crypto.Cipher; import org.commcare.android.database.SqlStorage; import org.commcare.android.database.user.models.ACase; import org.commcare.android.javarosa.AndroidLogger; import org.commcare.android.logic.GlobalConstants; import org.commcare.android.net.HttpRequestGenerator; import org.commcare.android.references.JavaHttpReference; import org.commcare.android.util.AndroidStreamUtil; import org.commcare.android.util.FileUtil; import org.commcare.cases.model.Case; import org.commcare.dalvik.application.CommCareApplication; 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 android.net.ParseException; import android.net.Uri; import android.util.Pair; /** * @author ctsims * */ public class AndroidCaseXmlParser extends CaseXmlParser { Cipher attachmentCipher; Cipher userCipher; File folder; boolean processAttachments = true; HttpRequestGenerator generator; public AndroidCaseXmlParser(KXmlParser parser, IStorageUtilityIndexed storage) { super(parser, storage); } //TODO: Sync the following two constructors! public AndroidCaseXmlParser(KXmlParser parser, IStorageUtilityIndexed storage, Cipher attachmentCipher, Cipher userCipher, File folder) { this(parser, storage); this.attachmentCipher = attachmentCipher; this.userCipher = userCipher; this.folder = folder; processAttachments = true; } public AndroidCaseXmlParser(KXmlParser parser, int[] tallies, boolean b, SqlStorage<ACase> storage, HttpRequestGenerator generator) { super(parser, tallies, b, storage); this.generator = generator; } /* * (non-Javadoc) * @see org.commcare.xml.CaseXmlParser#removeAttachment(org.commcare.cases.model.Case, java.lang.String) */ @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._().DeriveReference(source).remove(); } catch (IOException e) { e.printStackTrace(); } catch (InvalidReferenceException e) { e.printStackTrace(); } } /* * (non-Javadoc) * @see org.commcare.xml.CaseXmlParser#processAttachment(java.lang.String, java.lang.String, java.lang.String, org.kxml2.io.KXmlParser) */ @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._().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 { AndroidStreamUtil.writeFromInputToOutput(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. * @return */ private Pair<File, String> getDestination(String source) { File storagePath = new File(CommCareApplication._().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<File, String>(new File(storagePath, dest), GlobalConstants.ATTACHMENT_REF + dest); } /* (non-Javadoc) * @see org.commcare.xml.CaseXmlParser#CreateCase(java.lang.String, java.lang.String) */ @Override protected Case CreateCase(String name, String typeId) { return new ACase(name, typeId); } }