blob: 9b66535b2449c059d2d68484d1abd4052bbd8eb4 [file] [log] [blame]
package com.intellij.updater;
import java.io.*;
import java.util.*;
import java.util.zip.ZipFile;
public class Patch {
private List<PatchAction> myActions = new ArrayList<PatchAction>();
private static final int CREATE_ACTION_KEY = 1;
private static final int UPDATE_ACTION_KEY = 2;
private static final int UPDATE_ZIP_ACTION_KEY = 3;
private static final int DELETE_ACTION_KEY = 4;
public Patch(File olderDir,
File newerDir,
List<String> ignoredFiles,
List<String> criticalFiles,
List<String> optionalFiles,
UpdaterUI ui) throws IOException, OperationCancelledException {
calculateActions(olderDir, newerDir, ignoredFiles, criticalFiles, optionalFiles, ui);
}
public Patch(InputStream patchIn) throws IOException {
read(patchIn);
}
private void calculateActions(File olderDir,
File newerDir,
List<String> ignoredFiles,
List<String> criticalFiles,
List<String> optionalFiles,
UpdaterUI ui)
throws IOException, OperationCancelledException {
DiffCalculator.Result diff;
Runner.logger.info("Calculating difference...");
ui.startProcess("Calculating difference...");
ui.checkCancelled();
diff = DiffCalculator.calculate(Digester.digestFiles(olderDir, ignoredFiles, ui),
Digester.digestFiles(newerDir, ignoredFiles, ui));
List<PatchAction> tempActions = new ArrayList<PatchAction>();
// 'delete' actions before 'create' actions to prevent newly created files to be deleted if the names differ only on case.
for (Map.Entry<String, Long> each : diff.filesToDelete.entrySet()) {
tempActions.add(new DeleteAction(each.getKey(), each.getValue()));
}
for (String each : diff.filesToCreate) {
tempActions.add(new CreateAction(each));
}
for (Map.Entry<String, Long> each : diff.filesToUpdate.entrySet()) {
if (Utils.isZipFile(each.getKey())) {
tempActions.add(new UpdateZipAction(each.getKey(), each.getValue()));
}
else {
tempActions.add(new UpdateAction(each.getKey(), each.getValue()));
}
}
Runner.logger.info("Preparing actions...");
ui.startProcess("Preparing actions...");
ui.checkCancelled();
for (PatchAction each : tempActions) {
ui.setStatus(each.getPath());
ui.checkCancelled();
if (!each.calculate(olderDir, newerDir)) continue;
myActions.add(each);
each.setCritical(criticalFiles.contains(each.getPath()));
each.setOptional(optionalFiles.contains(each.getPath()));
}
}
public List<PatchAction> getActions() {
return myActions;
}
public void write(OutputStream out) throws IOException {
@SuppressWarnings("IOResourceOpenedButNotSafelyClosed") DataOutputStream dataOut = new DataOutputStream(out);
try {
dataOut.writeInt(myActions.size());
for (PatchAction each : myActions) {
int key;
Class clazz = each.getClass();
if (clazz == CreateAction.class) {
key = CREATE_ACTION_KEY;
}
else if (clazz == UpdateAction.class) {
key = UPDATE_ACTION_KEY;
}
else if (clazz == UpdateZipAction.class) {
key = UPDATE_ZIP_ACTION_KEY;
}
else if (clazz == DeleteAction.class) {
key = DELETE_ACTION_KEY;
}
else {
throw new RuntimeException("Unknown action " + each);
}
dataOut.writeInt(key);
each.write(dataOut);
}
}
finally {
dataOut.flush();
}
}
private void read(InputStream patchIn) throws IOException {
List<PatchAction> newActions = new ArrayList<PatchAction>();
@SuppressWarnings("IOResourceOpenedButNotSafelyClosed") DataInputStream in = new DataInputStream(patchIn);
int size = in.readInt();
while (size-- > 0) {
int key = in.readInt();
PatchAction a;
switch (key) {
case CREATE_ACTION_KEY:
a = new CreateAction(in);
break;
case UPDATE_ACTION_KEY:
a = new UpdateAction(in);
break;
case UPDATE_ZIP_ACTION_KEY:
a = new UpdateZipAction(in);
break;
case DELETE_ACTION_KEY:
a = new DeleteAction(in);
break;
default:
throw new RuntimeException("Unknown action type " + key);
}
newActions.add(a);
}
myActions = newActions;
}
public List<ValidationResult> validate(final File toDir, UpdaterUI ui) throws IOException, OperationCancelledException {
final LinkedHashSet<String> files = Utils.collectRelativePaths(toDir);
final List<ValidationResult> result = new ArrayList<ValidationResult>();
Runner.logger.info("Validating installation...");
forEach(myActions, "Validating installation...", ui, true,
new ActionsProcessor() {
public void forEach(PatchAction each) throws IOException {
ValidationResult validationResult = each.validate(toDir);
if (validationResult != null) result.add(validationResult);
files.remove(each.getPath());
}
});
//for (String each : files) {
// result.add(new ValidationResult(ValidationResult.Kind.INFO,
// each,
// ValidationResult.Action.NO_ACTION,
// ValidationResult.MANUALLY_ADDED_MESSAGE,
// ValidationResult.Option.KEEP, ValidationResult.Option.DELETE));
//}
return result;
}
public ApplicationResult apply(final ZipFile patchFile,
final File toDir,
final File backupDir,
final Map<String, ValidationResult.Option> options,
UpdaterUI ui) throws IOException, OperationCancelledException {
List<PatchAction> actionsToProcess = new ArrayList<PatchAction>();
for (PatchAction each : myActions) {
if (each.shouldApply(toDir, options)) actionsToProcess.add(each);
}
forEach(actionsToProcess, "Backing up files...", ui, true,
new ActionsProcessor() {
public void forEach(PatchAction each) throws IOException {
each.backup(toDir, backupDir);
}
});
final List<PatchAction> appliedActions = new ArrayList<PatchAction>();
boolean shouldRevert = false;
boolean cancelled = false;
try {
forEach(actionsToProcess, "Applying patch...", ui, true,
new ActionsProcessor() {
public void forEach(PatchAction each) throws IOException {
appliedActions.add(each);
each.apply(patchFile, toDir);
}
});
}
catch (OperationCancelledException e) {
Runner.printStackTrace(e);
shouldRevert = true;
cancelled = true;
}
catch (Throwable e) {
Runner.printStackTrace(e);
shouldRevert = true;
ui.showError(e);
}
if (shouldRevert) {
revert(appliedActions, backupDir, toDir, ui);
appliedActions.clear();
if (cancelled) throw new OperationCancelledException();
}
// on OS X we need to update bundle timestamp to reset Info.plist caches.
toDir.setLastModified(System.currentTimeMillis());
return new ApplicationResult(appliedActions);
}
public void revert(List<PatchAction> actions, final File backupDir, final File toDir, UpdaterUI ui)
throws OperationCancelledException, IOException {
Collections.reverse(actions);
forEach(actions, "Reverting...", ui, false,
new ActionsProcessor() {
public void forEach(PatchAction each) throws IOException {
each.revert(toDir, backupDir);
}
});
}
private static void forEach(List<PatchAction> actions, String title, UpdaterUI ui, boolean canBeCancelled, ActionsProcessor processor)
throws OperationCancelledException, IOException {
ui.startProcess(title);
if (canBeCancelled) ui.checkCancelled();
for (int i = 0; i < actions.size(); i++) {
PatchAction each = actions.get(i);
ui.setStatus(each.getPath());
if (canBeCancelled) ui.checkCancelled();
processor.forEach(each);
ui.setProgress((i + 1) * 100 / actions.size());
}
}
public interface ActionsProcessor {
void forEach(PatchAction each) throws IOException;
}
public static class ApplicationResult {
final boolean applied;
final List<PatchAction> appliedActions;
public ApplicationResult(List<PatchAction> appliedActions) {
this.applied = !appliedActions.isEmpty();
this.appliedActions = appliedActions;
}
}
}