package org.commcare.android.resource.installers; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Vector; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLPeerUnverifiedException; import org.commcare.android.javarosa.AndroidLogger; import org.commcare.android.util.AndroidCommCarePlatform; import org.commcare.android.util.AndroidStreamUtil; import org.commcare.android.util.FileUtil; import org.commcare.dalvik.application.CommCareApplication; import org.commcare.resources.model.MissingMediaException; import org.commcare.resources.model.Resource; import org.commcare.resources.model.ResourceInitializationException; import org.commcare.resources.model.ResourceInstaller; import org.commcare.resources.model.ResourceLocation; import org.commcare.resources.model.ResourceTable; import org.commcare.resources.model.UnreliableSourceException; import org.commcare.resources.model.UnresolvedResourceException; import org.commcare.xml.util.UnfullfilledRequirementsException; 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.util.externalizable.DeserializationException; import org.javarosa.core.util.externalizable.ExtUtil; import org.javarosa.core.util.externalizable.PrototypeFactory; import android.util.Pair; /** * @author ctsims * */ public abstract class FileSystemInstaller implements ResourceInstaller<AndroidCommCarePlatform> { //TODO:HAAACKY. private static final String STAGING_EXT = "cc_app-staging"; String localLocation; String localDestination; String upgradeDestination; public FileSystemInstaller() { } public FileSystemInstaller(String localDestination, String upgradeDestination) { this.localDestination = localDestination; this.upgradeDestination = upgradeDestination; } /* (non-Javadoc) * @see org.commcare.resources.model.ResourceInstaller#cleanup() */ @Override public void cleanup() { // TODO Auto-generated method stub } /* (non-Javadoc) * @see org.commcare.resources.model.ResourceInstaller#initialize(org.commcare.util.CommCareInstance) */ @Override public abstract boolean initialize(AndroidCommCarePlatform instance) throws ResourceInitializationException; /* (non-Javadoc) * @see org.commcare.resources.model.ResourceInstaller#install(org.commcare.resources.model.Resource, org.commcare.resources.model.ResourceLocation, org.javarosa.core.reference.Reference, org.commcare.resources.model.ResourceTable, org.commcare.util.CommCareInstance, boolean) */ @Override public boolean install(Resource r, ResourceLocation location, Reference ref, ResourceTable table, AndroidCommCarePlatform instance, boolean upgrade) throws UnresolvedResourceException, UnfullfilledRequirementsException { try { OutputStream os; Reference localReference; //Moved this up before the local stuff, in case the local reference fails, we don't want to start dealing with it InputStream input; try { input = ref.getStream(); } catch (FileNotFoundException e) { //This simply means that the reference wasn't actually valid like it thought it was (sometimes you can't tell until you try) //so let it keep iterating through options. return false; } File tempFile; //Stream to location try { Pair<String, String> fileDetails = getResourceName(r,location); //Final destination localReference = getEmptyLocalReference((upgrade ? upgradeDestination : localDestination),fileDetails.first, fileDetails.second); //Create a temporary place to store these bits tempFile = new File(CommCareApplication._().getTempFilePath()); //Make sure the stream is valid os = new FileOutputStream(tempFile); //Get the actual local file we'll be putting the data into localLocation = localReference.getURI(); } catch(InvalidReferenceException ire) { throw new LocalStorageUnavailableException("Couldn't create reference to declared location " + localLocation + " for file system installation", localLocation); } catch(IOException ioe) { throw new LocalStorageUnavailableException("Couldn't write to local reference " + localLocation + " for file system installation", localLocation); } //Write the full file to the temporary location AndroidStreamUtil.writeFromInputToOutput(input, os); //Get a cannonical path String localUri = localReference.getLocalURI(); File destination = new File(localUri); //Make sure there's a seat for the new file FileUtil.ensureFilePathExists(destination); //File written, it must be valid now, so move it into our intended location if(!tempFile.renameTo(destination)) { throw new LocalStorageUnavailableException("Couldn't write to local reference " + localLocation + " to location " + localUri + " for file system installation", localLocation); } //TODO: Sketch - if this fails, we'll still have the file at that location. int status = customInstall(r, localReference, upgrade); table.commit(r, status); if(localLocation == null) { throw new UnresolvedResourceException(r, "After install there is no local resource location"); } return true; /* * This error is thrown by the HttpRequestGenerator on 4.3 devices when the peer certificate is bad. * We catch this and deliver upstream to the SetupActivity as an UnresolvedResourceException */ } catch(SSLHandshakeException she){ she.printStackTrace(); UnresolvedResourceException mURE = new UnresolvedResourceException(r, "Your certificate was bad. This is often due to a mis-set phone clock.", true); mURE.initCause(she); throw mURE; /* * This error is thrown by the HttpRequestGenerator on 2.3 devices when the peer certificate is bad. * Handled the same as above. */ } catch(SSLPeerUnverifiedException spue){ spue.printStackTrace(); UnresolvedResourceException mURE = new UnresolvedResourceException(r, "Your certificate was bad. This is often due to a mis-set phone clock.", true); mURE.initCause(spue); throw mURE; } catch (IOException e) { e.printStackTrace(); throw new UnreliableSourceException(r, e.getMessage()); } } private Reference getEmptyLocalReference(String root, String fileName, String extension) throws InvalidReferenceException, IOException { Reference r = ReferenceManager._().DeriveReference(root + "/" + fileName + extension); int count = 0; while(r.doesBinaryExist()) { count++; r = ReferenceManager._().DeriveReference(root + "/" + fileName + String.valueOf(count) + extension); } return r; } /** * Perform any custom installation actions required for this resource. * * @param r * @param local * @param upgrade * @return * @throws IOException * @throws UnresolvedResourceException */ protected abstract int customInstall(Resource r, Reference local, boolean upgrade) throws IOException, UnresolvedResourceException; /* (non-Javadoc) * @see org.commcare.resources.model.ResourceInstaller#requiresRuntimeInitialization() */ @Override public abstract boolean requiresRuntimeInitialization(); /* (non-Javadoc) * @see org.commcare.resources.model.ResourceInstaller#uninstall(org.commcare.resources.model.Resource, org.commcare.resources.model.ResourceTable, org.commcare.resources.model.ResourceTable) */ @Override public boolean uninstall(Resource r) throws UnresolvedResourceException { try{ return new File(ReferenceManager._().DeriveReference(this.localLocation).getLocalURI()).delete(); } catch(InvalidReferenceException e) { throw new UnresolvedResourceException(r, "Local reference couldn't be found for resource at " + this.localLocation); } } /* (non-Javadoc) * @see org.commcare.resources.model.ResourceInstaller#upgrade(org.commcare.resources.model.Resource, org.commcare.resources.model.ResourceTable) */ @Override public boolean upgrade(Resource r) { try { //TODO: This process is silly! Just put the files somewhere as a resource with a unique GUID and stop shuffling them around! //TODO: Also, there's way too much duplicated code here //use same filename as before String filepart = localLocation.substring(localLocation.lastIndexOf("/")); //Get final destination String finalLocation = localDestination + "/" + filepart; if(!moveFrom(localLocation, finalLocation, false)) { return false; } localLocation = finalLocation; return true; } catch (InvalidReferenceException e) { //e.printStackTrace(); //throw new UnresolvedResourceException(r, "Invalid reference while upgrading local resource. Reference path is: " + e.getReferenceString()); return false; } } private boolean moveFrom(String oldLocation, String newLocation, boolean force) throws InvalidReferenceException { File newFile = new File(ReferenceManager._().DeriveReference(newLocation).getLocalURI()); File oldFile = new File(ReferenceManager._().DeriveReference(oldLocation).getLocalURI()); if(!oldFile.exists()) { //Nothing should be allowed to exist in the new location except for the incoming file //due to the staging rules. If there's a file there, it's this one. if(newFile.exists()) { return true; } else { //... soo.... we don't have a file. return false; } } if(oldFile.exists() && newFile.exists()) { //There's a destination file where this file is //trying to move to. Something might have failed to unstage if(force) { //If we're recovering or something, wipe out the destination. //we've gotta recover! if(!newFile.delete()) { return false; } else { //new file is gone. Let's get ours in there! } } else { //can't copy over an existing file. An unstage might have failed. return false; } } if(!(oldFile.renameTo(newFile))) { return false; } else { return true; } } /* * (non-Javadoc) * @see org.commcare.resources.model.ResourceInstaller#unstage(org.commcare.resources.model.Resource, int) */ @Override public boolean unstage(Resource r, int newStatus) { try { //Our destination/source are different depending on where we're going if(newStatus == Resource.RESOURCE_STATUS_UNSTAGED) { String newLocation = localLocation + STAGING_EXT; if(!moveFrom(localLocation, newLocation, true)) { return false; } localLocation = newLocation; return true; } else if(newStatus == Resource.RESOURCE_STATUS_UPGRADE) { //use same filename as before String filepart = localLocation.substring(localLocation.lastIndexOf("/")); //Get update destination String finalLocation = upgradeDestination + "/" + filepart; //move back to upgrade folder if(!moveFrom(localLocation, finalLocation, true)) { return false; } localLocation = finalLocation; return true; } else { Logger.log(AndroidLogger.TYPE_RESOURCES, "Couldn't figure out how to unstage to status " + newStatus); return false; } }catch (InvalidReferenceException e) { Logger.log(AndroidLogger.TYPE_RESOURCES, "Very Bad! Couldn't derive a reference to " + e.getReferenceString()); //e.printStackTrace(); //throw new UnresolvedResourceException(r, "Invalid reference while upgrading local resource. Reference path is: " + e.getReferenceString()); return false; } } /* * (non-Javadoc) * @see org.commcare.resources.model.ResourceInstaller#revert(org.commcare.resources.model.Resource, org.commcare.resources.model.ResourceTable) */ @Override public boolean revert(Resource r, ResourceTable table) { String finalLocation = null; try { //use same filename as before String filepart = localLocation.substring(localLocation.lastIndexOf("/")); //remove staging extension int stagingindex = filepart.lastIndexOf(STAGING_EXT); if(stagingindex != -1) { filepart = filepart.substring(0, stagingindex); } //Get final destination finalLocation = localDestination + "/" + filepart; if(!moveFrom(localLocation, finalLocation, true)) { return false; } localLocation = finalLocation; return true; } catch (InvalidReferenceException e) { Logger.log(AndroidLogger.TYPE_RESOURCES, "Very Bad! Couldn't restore a resource to destination" + finalLocation+ " somehow"); //e.printStackTrace(); //throw new UnresolvedResourceException(r, "Invalid reference while upgrading local resource. Reference path is: " + e.getReferenceString()); return false; } } public int rollback(Resource r) { //TODO: These filepath ops need to be the same for this all to work, //which is not super robust against changes right now. int status = r.getStatus(); File currentPointer = new File(this.localLocation); String filepart = localLocation.substring(localLocation.lastIndexOf("/")); int stagingindex = filepart.lastIndexOf(STAGING_EXT); if(stagingindex != -1) { filepart = filepart.substring(0, stagingindex); } //Expected location for the file if the operation had succeeded. String oldRef; String expectedRef; int[] rollbackPushForward; switch(status) { case Resource.RESOURCE_STATUS_INSTALL_TO_UNSTAGE: oldRef = localDestination + "/" + filepart; expectedRef = localDestination + "/" + filepart + STAGING_EXT; rollbackPushForward = new int[]{Resource.RESOURCE_STATUS_INSTALLED, Resource.RESOURCE_STATUS_UNSTAGED}; break; case Resource.RESOURCE_STATUS_UNSTAGE_TO_INSTALL: oldRef = localDestination + "/" + filepart + STAGING_EXT; expectedRef = localDestination + "/" + filepart; rollbackPushForward = new int[]{Resource.RESOURCE_STATUS_UNSTAGED, Resource.RESOURCE_STATUS_INSTALLED}; break; case Resource.RESOURCE_STATUS_UPGRADE_TO_INSTALL: oldRef = upgradeDestination + "/" + filepart; expectedRef = localDestination + "/" + filepart; rollbackPushForward = new int[]{Resource.RESOURCE_STATUS_UNSTAGED, Resource.RESOURCE_STATUS_INSTALLED}; break; case Resource.RESOURCE_STATUS_INSTALL_TO_UPGRADE: oldRef = localDestination + "/" + filepart; expectedRef = upgradeDestination + "/" + filepart; rollbackPushForward = new int[]{Resource.RESOURCE_STATUS_UNSTAGED, Resource.RESOURCE_STATUS_INSTALLED}; break; default: throw new RuntimeException("Unexpected status for rollback! " + status); } try { File preMove = new File(ReferenceManager._().DeriveReference(oldRef).getLocalURI()); File expectedFile = new File(ReferenceManager._().DeriveReference(expectedRef).getLocalURI()); //the expectation is that localReference might be pointing to the old ref which no longer exists, //in which case the moved already happened. if(currentPointer.exists()) { //This either means that the move worked (we couldn't have updated the pointer otherwise) //or that it didn't move. if(currentPointer.getCanonicalFile().equals(preMove.getCanonicalFile())) { return rollbackPushForward[0]; } else if(currentPointer.getCanonicalFile().equals(expectedFile.getCanonicalFile())) { return rollbackPushForward[1]; } else { //Uh... we should only have been able to move the file to one of those places. return -1; } } else { //The file should have already moved to its new location if(expectedFile.exists()) { localLocation = expectedRef; return rollbackPushForward[1]; } else { return -1; } } } catch (InvalidReferenceException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } /* (non-Javadoc) * @see org.javarosa.core.util.externalizable.Externalizable#readExternal(java.io.DataInputStream, org.javarosa.core.util.externalizable.PrototypeFactory) */ @Override public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { this.localLocation = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); this.localDestination = ExtUtil.readString(in); this.upgradeDestination = ExtUtil.readString(in); } /* (non-Javadoc) * @see org.javarosa.core.util.externalizable.Externalizable#writeExternal(java.io.DataOutputStream) */ @Override public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.writeString(out, ExtUtil.emptyIfNull(localLocation)); ExtUtil.writeString(out, localDestination); ExtUtil.writeString(out, upgradeDestination); } //TODO: Put files into an arbitrary name and keep the reference. This confuses things too much public Pair<String, String> getResourceName(Resource r, ResourceLocation loc) { String input = loc.getLocation(); String extension = ""; int lastDot = input.lastIndexOf("."); if(lastDot != -1) { extension =input.substring(lastDot); } return new Pair<String, String>(r.getResourceId(), extension(extension)); } //Hate this private static final String validExtChars ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; protected String extension(String input) { int invalid = -1; //we wanna go from the last "." to the next non-alphanumeric character. for(int i = 1 ; i <input.length(); ++i ){ if(validExtChars.indexOf(input.charAt(i)) == -1) { invalid = i; break; } } if(invalid == -1 ) {return input;} return input.substring(0, invalid); } public boolean verifyInstallation(Resource r, Vector<MissingMediaException> issues) { try { Reference ref = ReferenceManager._().DeriveReference(localLocation); if(!ref.doesBinaryExist()) { issues.add(new MissingMediaException(r,"File doesn't exist at: " + ref.getLocalURI())); return true; } } catch (IOException e) { issues.add(new MissingMediaException(r,"Problem accessing file at: " + localLocation)); return true; } catch (InvalidReferenceException e) { issues.add(new MissingMediaException(r,"invalid reference: " + localLocation)); return true; } return false; } }