package org.commcare.android.resource.installers;
import android.support.v4.util.Pair;
import org.commcare.CommCareApplication;
import org.commcare.engine.resource.installers.LocalStorageUnavailableException;
import org.commcare.logging.AndroidLogger;
import org.commcare.resources.model.MissingMediaException;
import org.commcare.resources.model.Resource;
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.utils.AndroidCommCarePlatform;
import org.commcare.utils.FileUtil;
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.util.externalizable.DeserializationException;
import org.javarosa.core.util.externalizable.ExtUtil;
import org.javarosa.core.util.externalizable.PrototypeFactory;
import org.javarosa.xml.util.UnfullfilledRequirementsException;
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;
/**
* @author ctsims
*/
abstract class FileSystemInstaller implements ResourceInstaller<AndroidCommCarePlatform> {
//TODO:HAAACKY.
private static final String STAGING_EXT = "cc_app-staging";
String localLocation;
String localDestination;
private String upgradeDestination;
FileSystemInstaller() {
}
FileSystemInstaller(String localDestination, String upgradeDestination) {
this.localDestination = localDestination;
this.upgradeDestination = upgradeDestination;
}
@Override
public abstract boolean initialize(AndroidCommCarePlatform instance, boolean isUpgrade);
@Override
public boolean install(Resource r, ResourceLocation location,
Reference ref, ResourceTable table,
AndroidCommCarePlatform instance, boolean upgrade)
throws UnresolvedResourceException, UnfullfilledRequirementsException {
try {
InputStream inputFileStream;
try {
inputFileStream = ref.getStream();
} catch (FileNotFoundException e) {
// Means the reference wasn't valid so let it keep iterating through options.
return false;
}
File tempFile = new File(CommCareApplication.instance().getTempFilePath());
Reference localReference;
OutputStream outputFileStream;
try {
Pair<String, String> fileNameAndExt = getResourceName(r, location);
String referenceRoot = upgrade ? upgradeDestination : localDestination;
localReference = getEmptyLocalReference(referenceRoot, fileNameAndExt.first, fileNameAndExt.second);
outputFileStream = 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);
}
StreamsUtil.writeFromInputToOutputNew(inputFileStream, outputFileStream);
renameFile(localReference.getLocalURI(), tempFile);
//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;
} catch (SSLHandshakeException | SSLPeerUnverifiedException e) {
// SSLHandshakeException is thrown by the HttpRequestGenerator on
// 4.3 devices when the peer certificate is bad.
//
// SSLPeerUnverifiedException is thrown by the HttpRequestGenerator
// on 2.3 devices when the peer certificate is bad.
//
// Deliver these errors upstream to the SetupActivity as an
// UnresolvedResourceException
e.printStackTrace();
UnresolvedResourceException mURE =
new UnresolvedResourceException(r, "Your certificate was bad. This is often due to a mis-set phone clock.", true);
mURE.initCause(e);
throw mURE;
} catch (IOException e) {
e.printStackTrace();
throw new UnreliableSourceException(r, e.getMessage());
}
}
private void renameFile(String newFilename, File currentFile) throws LocalStorageUnavailableException {
File destination = new File(newFilename);
FileUtil.ensureFilePathExists(destination);
if (!currentFile.renameTo(destination)) {
throw new LocalStorageUnavailableException("Couldn't write to local reference " + localLocation + " to location " + newFilename + " for file system installation", localLocation);
}
}
private Reference getEmptyLocalReference(String root, String fileName, String extension)
throws InvalidReferenceException, IOException {
Reference r = ReferenceManager.instance().DeriveReference(root + "/" + fileName + extension);
int count = 0;
while (r.doesBinaryExist()) {
count++;
r = ReferenceManager.instance().DeriveReference(root + "/" + fileName + String.valueOf(count) + extension);
}
return r;
}
/**
* Perform any custom installation actions required for this resource.
*/
protected abstract int customInstall(Resource r, Reference local, boolean upgrade) throws IOException, UnresolvedResourceException;
@Override
public abstract boolean requiresRuntimeInitialization();
@Override
public boolean uninstall(Resource r) throws UnresolvedResourceException {
try {
return new File(ReferenceManager.instance().DeriveReference(this.localLocation).getLocalURI()).delete();
} catch (InvalidReferenceException e) {
throw new UnresolvedResourceException(r, "Local reference couldn't be found for resource at " + this.localLocation);
}
}
@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!
//use same filename as before
String filepart = localLocation.substring(localLocation.lastIndexOf("/"));
//Get final destination
String finalLocation = localDestination + "/" + filepart;
if (!FileSystemUtils.moveFrom(localLocation, finalLocation, false)) {
return false;
}
localLocation = finalLocation;
return true;
} catch (InvalidReferenceException e) {
return false;
}
}
@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 (!FileSystemUtils.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 (!FileSystemUtils.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());
return false;
}
}
@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 (!FileSystemUtils.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");
return false;
}
}
@Override
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.instance().DeriveReference(oldRef).getLocalURI());
File expectedFile = new File(ReferenceManager.instance().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 | IOException e) {
throw new RuntimeException(e);
}
}
@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);
}
@Override
public void writeExternal(DataOutputStream out) throws IOException {
ExtUtil.writeString(out, ExtUtil.emptyIfNull(localLocation));
ExtUtil.writeString(out, localDestination);
ExtUtil.writeString(out, upgradeDestination);
}
@Override
public boolean verifyInstallation(Resource r, Vector<MissingMediaException> issues) {
try {
Reference ref = ReferenceManager.instance().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;
}
//TODO: Put files into an arbitrary name and keep the reference. This confuses things too much
protected 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<>(r.getResourceId(), FileSystemUtils.extension(extension));
}
@Override
public void cleanup() {
}
}