summaryrefslogtreecommitdiff
path: root/updater
diff options
context:
space:
mode:
authorJean-Baptiste Queru <jbq@google.com>2013-02-08 15:14:04 -0800
committerJean-Baptiste Queru <jbq@google.com>2013-02-08 15:14:04 -0800
commit9edc8f6b58f71ec510ba36b838f115718d9a174d (patch)
tree06f6df92024fa534ff27e1c0b5fc8b2002848093 /updater
parentb56ea2a18f232d79481e778085fd64e8ae486fc3 (diff)
downloadidea-9edc8f6b58f71ec510ba36b838f115718d9a174d.tar.gz
Snapshot of commit 84dc01e773388c2c72a1fc437f313dd5747e7809
from branch master of git://git.jetbrains.org/idea/community.git
Diffstat (limited to 'updater')
-rw-r--r--updater/src/com/intellij/updater/BaseUpdateAction.java85
-rw-r--r--updater/src/com/intellij/updater/ConsoleUpdaterUI.java40
-rw-r--r--updater/src/com/intellij/updater/CreateAction.java80
-rw-r--r--updater/src/com/intellij/updater/DeleteAction.java57
-rw-r--r--updater/src/com/intellij/updater/DiffCalculator.java42
-rw-r--r--updater/src/com/intellij/updater/Digester.java95
-rw-r--r--updater/src/com/intellij/updater/OperationCancelledException.java4
-rw-r--r--updater/src/com/intellij/updater/Patch.java262
-rw-r--r--updater/src/com/intellij/updater/PatchAction.java203
-rw-r--r--updater/src/com/intellij/updater/PatchFileCreator.java115
-rw-r--r--updater/src/com/intellij/updater/Runner.java230
-rw-r--r--updater/src/com/intellij/updater/SwingUpdaterUI.java559
-rw-r--r--updater/src/com/intellij/updater/UpdateAction.java48
-rw-r--r--updater/src/com/intellij/updater/UpdateZipAction.java246
-rw-r--r--updater/src/com/intellij/updater/UpdaterUI.java20
-rw-r--r--updater/src/com/intellij/updater/Utils.java177
-rw-r--r--updater/src/com/intellij/updater/ValidationResult.java94
-rw-r--r--updater/src/com/intellij/updater/ZipOutputWrapper.java143
-rw-r--r--updater/src/ie/wombat/jbdiff/JBDiff.java487
-rw-r--r--updater/src/ie/wombat/jbdiff/JBPatch.java152
-rw-r--r--updater/src/ie/wombat/jbdiff/Util.java112
-rw-r--r--updater/updater.iml12
22 files changed, 3263 insertions, 0 deletions
diff --git a/updater/src/com/intellij/updater/BaseUpdateAction.java b/updater/src/com/intellij/updater/BaseUpdateAction.java
new file mode 100644
index 000000000000..2ddfb685928d
--- /dev/null
+++ b/updater/src/com/intellij/updater/BaseUpdateAction.java
@@ -0,0 +1,85 @@
+package com.intellij.updater;
+
+import ie.wombat.jbdiff.JBDiff;
+import ie.wombat.jbdiff.JBPatch;
+
+import java.io.*;
+import java.util.zip.ZipOutputStream;
+
+public abstract class BaseUpdateAction extends PatchAction {
+ public BaseUpdateAction(String path, long checksum) {
+ super(path, checksum);
+ }
+
+ public BaseUpdateAction(DataInputStream in) throws IOException {
+ super(in);
+ }
+
+ @Override
+ protected ValidationResult doValidate(File toFile) throws IOException {
+ ValidationResult result = doValidateAccess(toFile, ValidationResult.Action.UPDATE);
+ if (result != null) return result;
+ return doValidateNotChanged(toFile, ValidationResult.Kind.ERROR, ValidationResult.Action.UPDATE);
+ }
+
+ @Override
+ protected boolean shouldApplyOn(File toFile) {
+ // if the file is optional in may not exist
+ return toFile.exists();
+ }
+
+ @Override
+ protected void doBackup(File toFile, File backupFile) throws IOException {
+ Utils.copy(toFile, backupFile);
+ }
+
+ protected void replaceUpdated(File from, File dest) throws IOException {
+ // on OS X code signing caches seem to be associated with specific file ids, so we need to remove the original file.
+ if (!dest.delete()) throw new IOException("Cannot delete file " + dest);
+ Utils.copy(from, dest);
+ }
+
+ @Override
+ protected void doRevert(File toFile, File backupFile) throws IOException {
+ if (!toFile.exists() || isModified(toFile)) {
+ Utils.copy(backupFile, toFile);
+ }
+ }
+
+ protected void writeDiff(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException {
+ BufferedInputStream olderFileIn = new BufferedInputStream(new FileInputStream(olderFile));
+ BufferedInputStream newerFileIn = new BufferedInputStream(new FileInputStream(newerFile));
+ try {
+ writeDiff(olderFileIn, newerFileIn, patchOutput);
+ }
+ finally {
+ olderFileIn.close();
+ newerFileIn.close();
+ }
+ }
+
+ protected void writeDiff(InputStream olderFileIn, InputStream newerFileIn, ZipOutputStream patchOutput)
+ throws IOException {
+ ByteArrayOutputStream diffOutput = new ByteArrayOutputStream();
+ byte[] newerFileBuffer = JBDiff.bsdiff(olderFileIn, newerFileIn, diffOutput);
+ diffOutput.close();
+
+ if (diffOutput.size() < newerFileBuffer.length) {
+ patchOutput.write(1);
+ Utils.copyBytesToStream(diffOutput, patchOutput);
+ }
+ else {
+ patchOutput.write(0);
+ Utils.copyBytesToStream(newerFileBuffer, patchOutput);
+ }
+ }
+
+ protected void applyDiff(InputStream patchInput, InputStream oldFileIn, OutputStream toFileOut) throws IOException {
+ if (patchInput.read() == 1) {
+ JBPatch.bspatch(oldFileIn, toFileOut, patchInput);
+ }
+ else {
+ Utils.copyStream(patchInput, toFileOut);
+ }
+ }
+}
diff --git a/updater/src/com/intellij/updater/ConsoleUpdaterUI.java b/updater/src/com/intellij/updater/ConsoleUpdaterUI.java
new file mode 100644
index 000000000000..677502eea7ec
--- /dev/null
+++ b/updater/src/com/intellij/updater/ConsoleUpdaterUI.java
@@ -0,0 +1,40 @@
+package com.intellij.updater;
+
+import java.util.Map;
+import java.util.List;
+import java.util.Collections;
+
+@SuppressWarnings({"UseOfSystemOutOrSystemErr", "CallToPrintStackTrace"})
+public class ConsoleUpdaterUI implements UpdaterUI {
+ private String myStatus;
+
+ public void startProcess(String title) {
+ System.out.println(title);
+ }
+
+ public void setProgress(int percentage) {
+ }
+
+ public void setProgressIndeterminate() {
+ }
+
+ public void setStatus(String status) {
+ System.out.println(myStatus = status);
+ }
+
+ public void showError(Throwable e) {
+ e.printStackTrace();
+ }
+
+ public void checkCancelled() throws OperationCancelledException {
+ }
+
+ public Map<String, ValidationResult.Option> askUser(List<ValidationResult> validationResults) {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public String toString() {
+ return "Status: '" + myStatus + '\'';
+ }
+}
diff --git a/updater/src/com/intellij/updater/CreateAction.java b/updater/src/com/intellij/updater/CreateAction.java
new file mode 100644
index 000000000000..aeeb5ed9d76b
--- /dev/null
+++ b/updater/src/com/intellij/updater/CreateAction.java
@@ -0,0 +1,80 @@
+package com.intellij.updater;
+
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+public class CreateAction extends PatchAction {
+ public CreateAction(String path) {
+ super(path, -1);
+ }
+
+ public CreateAction(DataInputStream in) throws IOException {
+ super(in);
+ }
+
+ protected void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException {
+ patchOutput.putNextEntry(new ZipEntry(myPath));
+
+ writeExecutableFlag(patchOutput, newerFile);
+ Utils.copyFileToStream(newerFile, patchOutput);
+
+ patchOutput.closeEntry();
+ }
+
+ @Override
+ protected ValidationResult doValidate(File toFile) {
+ ValidationResult result = doValidateAccess(toFile, ValidationResult.Action.CREATE);
+ if (result != null) return result;
+
+ if (toFile.exists()) {
+ return new ValidationResult(ValidationResult.Kind.CONFLICT,
+ myPath,
+ ValidationResult.Action.CREATE,
+ ValidationResult.ALREADY_EXISTS_MESSAGE,
+ ValidationResult.Option.REPLACE, ValidationResult.Option.KEEP);
+ }
+ return null;
+ }
+
+ @Override
+ protected void doApply(ZipFile patchFile, File toFile) throws IOException {
+ prepareToWriteFile(toFile);
+
+ InputStream in = Utils.getEntryInputStream(patchFile, myPath);
+ try {
+ boolean executable = readExecutableFlag(in);
+ Utils.copyStreamToFile(in, toFile);
+ Utils.setExecutable(toFile, executable);
+ }
+ finally {
+ in.close();
+ }
+ }
+
+ private static void prepareToWriteFile(File file) throws IOException {
+ if (file.exists()) {
+ Utils.delete(file);
+ return;
+ }
+
+ while (file != null && !file.exists()) {
+ file = file.getParentFile();
+ }
+ if (file != null && !file.isDirectory()) {
+ Utils.delete(file);
+ }
+ }
+
+ protected void doBackup(File toFile, File backupFile) {
+ // do nothing
+ }
+
+ protected void doRevert(File toFile, File backupFile) throws IOException {
+ Utils.delete(toFile);
+ }
+}
diff --git a/updater/src/com/intellij/updater/DeleteAction.java b/updater/src/com/intellij/updater/DeleteAction.java
new file mode 100644
index 000000000000..3dd1b850e415
--- /dev/null
+++ b/updater/src/com/intellij/updater/DeleteAction.java
@@ -0,0 +1,57 @@
+package com.intellij.updater;
+
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+public class DeleteAction extends PatchAction {
+ public DeleteAction(String path, long checksum) {
+ super(path, checksum);
+ }
+
+ public DeleteAction(DataInputStream in) throws IOException {
+ super(in);
+ }
+
+ @Override
+ public void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException {
+ // do nothing
+ }
+
+ @Override
+ protected ValidationResult doValidate(File toFile) throws IOException {
+ ValidationResult result = doValidateAccess(toFile, ValidationResult.Action.DELETE);
+ if (result != null) return result;
+
+ if (toFile.exists() && isModified(toFile)) {
+ return new ValidationResult(ValidationResult.Kind.CONFLICT,
+ myPath,
+ ValidationResult.Action.DELETE,
+ "Modified",
+ ValidationResult.Option.DELETE,
+ ValidationResult.Option.KEEP);
+ }
+ return null;
+ }
+
+ @Override
+ protected boolean shouldApplyOn(File toFile) {
+ return toFile.exists();
+ }
+
+ @Override
+ protected void doApply(ZipFile patchFile, File toFile) throws IOException {
+ Utils.delete(toFile);
+ }
+
+ protected void doBackup(File toFile, File backupFile) throws IOException {
+ Utils.copy(toFile, backupFile);
+ }
+
+ protected void doRevert(File toFile, File backupFile) throws IOException {
+ Utils.delete(toFile); // make sure there is no directory remained on this path (may remain from previous 'create' actions
+ Utils.copy(backupFile, toFile);
+ }
+}
diff --git a/updater/src/com/intellij/updater/DiffCalculator.java b/updater/src/com/intellij/updater/DiffCalculator.java
new file mode 100644
index 000000000000..4db0033740e9
--- /dev/null
+++ b/updater/src/com/intellij/updater/DiffCalculator.java
@@ -0,0 +1,42 @@
+package com.intellij.updater;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class DiffCalculator {
+ public static Result calculate(Map<String, Long> oldChecksums, Map<String, Long> newChecksums) {
+ Result result = new Result();
+ result.filesToDelete = withAllRemoved(oldChecksums, newChecksums);
+ result.filesToCreate = withAllRemoved(newChecksums, oldChecksums).keySet();
+ result.filesToUpdate = collect(oldChecksums, newChecksums, false);
+ return result;
+ }
+
+ private static Map<String, Long> withAllRemoved(Map<String, Long> from, Map<String, Long> toRemove) {
+ Map<String, Long> result = new HashMap<String, Long>(from);
+ for (String each : toRemove.keySet()) {
+ result.remove(each);
+ }
+ return result;
+ }
+
+ private static Map<String, Long> collect(Map<String, Long> older, Map<String, Long> newer, boolean equal) {
+ Map<String, Long> result = new HashMap<String, Long>();
+ for (Map.Entry<String, Long> each : newer.entrySet()) {
+ String file = each.getKey();
+ Long oldChecksum = older.get(file);
+ Long newChecksum = newer.get(file);
+ if (oldChecksum != null && newChecksum != null && oldChecksum.equals(newChecksum) == equal) {
+ result.put(file, oldChecksum);
+ }
+ }
+ return result;
+ }
+
+ public static class Result {
+ public Map<String, Long> filesToDelete;
+ public Set<String> filesToCreate;
+ public Map<String, Long> filesToUpdate;
+ }
+}
diff --git a/updater/src/com/intellij/updater/Digester.java b/updater/src/com/intellij/updater/Digester.java
new file mode 100644
index 000000000000..326f82961ae0
--- /dev/null
+++ b/updater/src/com/intellij/updater/Digester.java
@@ -0,0 +1,95 @@
+package com.intellij.updater;
+
+import java.io.*;
+import java.util.*;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class Digester {
+ public static Map<String, Long> digestFiles(File dir, List<String> ignoredFiles, UpdaterUI ui)
+ throws IOException, OperationCancelledException {
+ Map<String, Long> result = new HashMap<String, Long>();
+
+ LinkedHashSet<String> paths = Utils.collectRelativePaths(dir);
+ for (String each : paths) {
+ if (ignoredFiles.contains(each)) continue;
+ ui.setStatus(each);
+ ui.checkCancelled();
+ result.put(each, digestFile(new File(dir, each)));
+ }
+ return result;
+ }
+
+ public static long digestFile(File file) throws IOException {
+ if (Utils.isZipFile(file.getName())) {
+ ZipFile zipFile;
+ try {
+ zipFile = new ZipFile(file);
+ }
+ catch (IOException e) {
+ return doDigestRegularFile(file);
+ }
+
+ try {
+ return doDigestZipFile(zipFile);
+ }
+ finally {
+ zipFile.close();
+ }
+ }
+ return doDigestRegularFile(file);
+ }
+
+ private static long doDigestRegularFile(File file) throws IOException {
+ InputStream in = new BufferedInputStream(new FileInputStream(file));
+ try {
+ return digestStream(in);
+ }
+ finally {
+ in.close();
+ }
+ }
+
+ private static long doDigestZipFile(ZipFile zipFile) throws IOException {
+ List<ZipEntry> sorted = new ArrayList<ZipEntry>();
+
+ Enumeration<? extends ZipEntry> temp = zipFile.entries();
+ while (temp.hasMoreElements()) {
+ ZipEntry each = temp.nextElement();
+ if (!each.isDirectory()) sorted.add(each);
+ }
+
+ Collections.sort(sorted, new Comparator<ZipEntry>() {
+ public int compare(ZipEntry o1, ZipEntry o2) {
+ return o1.getName().compareTo(o2.getName());
+ }
+ });
+
+ CRC32 crc = new CRC32();
+ for (ZipEntry each : sorted) {
+ InputStream in = zipFile.getInputStream(each);
+ try {
+ doDigestStream(in, crc);
+ }
+ finally {
+ in.close();
+ }
+ }
+ return crc.getValue();
+ }
+
+ public static long digestStream(InputStream in) throws IOException {
+ CRC32 crc = new CRC32();
+ doDigestStream(in, crc);
+ return crc.getValue();
+ }
+
+ private static void doDigestStream(InputStream in, CRC32 crc) throws IOException {
+ final byte[] BUFFER = new byte[65536];
+ int size;
+ while ((size = in.read(BUFFER)) != -1) {
+ crc.update(BUFFER, 0, size);
+ }
+ }
+}
diff --git a/updater/src/com/intellij/updater/OperationCancelledException.java b/updater/src/com/intellij/updater/OperationCancelledException.java
new file mode 100644
index 000000000000..bb0cb7c90bbf
--- /dev/null
+++ b/updater/src/com/intellij/updater/OperationCancelledException.java
@@ -0,0 +1,4 @@
+package com.intellij.updater;
+
+public class OperationCancelledException extends Exception {
+}
diff --git a/updater/src/com/intellij/updater/Patch.java b/updater/src/com/intellij/updater/Patch.java
new file mode 100644
index 000000000000..ac787afd50b0
--- /dev/null
+++ b/updater/src/com/intellij/updater/Patch.java
@@ -0,0 +1,262 @@
+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;
+
+ 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()));
+ }
+ }
+
+ 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>();
+
+ 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) {
+ shouldRevert = true;
+ cancelled = true;
+ }
+ catch (Exception 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;
+ }
+ }
+}
diff --git a/updater/src/com/intellij/updater/PatchAction.java b/updater/src/com/intellij/updater/PatchAction.java
new file mode 100644
index 000000000000..76259f026bd1
--- /dev/null
+++ b/updater/src/com/intellij/updater/PatchAction.java
@@ -0,0 +1,203 @@
+package com.intellij.updater;
+
+import java.io.*;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.util.Map;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+public abstract class PatchAction {
+ protected String myPath;
+ protected long myChecksum;
+ private boolean isCritical;
+ private boolean isOptional;
+
+ public PatchAction(String path, long checksum) {
+ myPath = path;
+ myChecksum = checksum;
+ }
+
+ public PatchAction(DataInputStream in) throws IOException {
+ myPath = in.readUTF();
+ myChecksum = in.readLong();
+ isCritical = in.readBoolean();
+ isOptional = in.readBoolean();
+ }
+
+ public void write(DataOutputStream out) throws IOException {
+ out.writeUTF(myPath);
+ out.writeLong(myChecksum);
+ out.writeBoolean(isCritical);
+ out.writeBoolean(isOptional);
+ }
+
+ public String getPath() {
+ return myPath;
+ }
+
+ protected static void writeExecutableFlag(OutputStream out, File file) throws IOException {
+ out.write(file.canExecute() ? 1 : 0);
+ }
+
+ protected static boolean readExecutableFlag(InputStream in) throws IOException {
+ return in.read() == 1;
+ }
+
+ public boolean calculate(File olderDir, File newerDir) throws IOException {
+ return doCalculate(getFile(olderDir), getFile(newerDir));
+ }
+
+ protected boolean doCalculate(File olderFile, File newerFile) throws IOException {
+ return true;
+ }
+
+ public void buildPatchFile(File olderDir, File newerDir, ZipOutputStream patchOutput) throws IOException {
+ doBuildPatchFile(getFile(olderDir), getFile(newerDir), patchOutput);
+ }
+
+ protected abstract void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException;
+
+ public boolean shouldApply(File toDir, Map<String, ValidationResult.Option> options) {
+ ValidationResult.Option option = options.get(myPath);
+ if (option == ValidationResult.Option.KEEP || option == ValidationResult.Option.IGNORE) return false;
+ return shouldApplyOn(getFile(toDir));
+ }
+
+ protected boolean shouldApplyOn(File toFile) {
+ return true;
+ }
+
+ public ValidationResult validate(File toDir) throws IOException {
+ return doValidate(getFile(toDir));
+ }
+
+ protected abstract ValidationResult doValidate(final File toFile) throws IOException;
+
+ protected ValidationResult doValidateAccess(File toFile, ValidationResult.Action action) {
+ if (!toFile.exists()) return null;
+ if (toFile.isDirectory()) return null;
+ if (toFile.canRead() && toFile.canWrite() && isWritable(toFile)) return null;
+ return new ValidationResult(ValidationResult.Kind.ERROR,
+ myPath,
+ action,
+ ValidationResult.ACCESS_DENIED_MESSAGE,
+ ValidationResult.Option.IGNORE);
+ }
+
+ private boolean isWritable(File toFile) {
+ try {
+ FileOutputStream s = new FileOutputStream(toFile, true);
+ FileChannel ch = s.getChannel();
+ try {
+ FileLock lock = ch.tryLock();
+ if (lock == null) return false;
+ lock.release();
+ }
+ finally {
+ ch.close();
+ s.close();
+ }
+ return true;
+ }
+ catch (OverlappingFileLockException e) {
+ return false;
+ }
+ catch (IOException e) {
+ return false;
+ }
+ }
+
+ protected ValidationResult doValidateNotChanged(File toFile, ValidationResult.Kind kind, ValidationResult.Action action)
+ throws IOException {
+ if (toFile.exists()) {
+ if (isModified(toFile)) {
+ return new ValidationResult(kind,
+ myPath,
+ action,
+ ValidationResult.MODIFIED_MESSAGE,
+ ValidationResult.Option.IGNORE);
+ }
+ }
+ else if (!isOptional) {
+ return new ValidationResult(kind,
+ myPath,
+ action,
+ ValidationResult.ABSENT_MESSAGE,
+ ValidationResult.Option.IGNORE);
+ }
+ return null;
+ }
+
+ protected boolean isModified(File toFile) throws IOException {
+ return myChecksum != Digester.digestFile(toFile);
+ }
+
+ public void apply(ZipFile patchFile, File toDir) throws IOException {
+ doApply(patchFile, getFile(toDir));
+ }
+
+ protected abstract void doApply(ZipFile patchFile, File toFile) throws IOException;
+
+ public void backup(File toDir, File backupDir) throws IOException {
+ doBackup(getFile(toDir), getFile(backupDir));
+ }
+
+ protected abstract void doBackup(File toFile, File backupFile) throws IOException;
+
+ public void revert(File toDir, File backupDir) throws IOException {
+ doRevert(getFile(toDir), getFile(backupDir));
+ }
+
+ protected abstract void doRevert(File toFile, File backupFile) throws IOException;
+
+ private File getFile(File baseDir) {
+ return new File(baseDir, myPath);
+ }
+
+ public boolean isCritical() {
+ return isCritical;
+ }
+
+ public void setCritical(boolean critical) {
+ isCritical = critical;
+ }
+
+ public boolean isOptional() {
+ return isOptional;
+ }
+
+ public void setOptional(boolean optional) {
+ isOptional = optional;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "(" + myPath + ", " + myChecksum + ")";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PatchAction that = (PatchAction)o;
+
+ if (isCritical != that.isCritical) return false;
+ if (isOptional != that.isOptional) return false;
+ if (myChecksum != that.myChecksum) return false;
+ if (myPath != null ? !myPath.equals(that.myPath) : that.myPath != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = myPath != null ? myPath.hashCode() : 0;
+ result = 31 * result + (int)(myChecksum ^ (myChecksum >>> 32));
+ result = 31 * result + (isCritical ? 1 : 0);
+ result = 31 * result + (isOptional ? 1 : 0);
+ return result;
+ }
+}
diff --git a/updater/src/com/intellij/updater/PatchFileCreator.java b/updater/src/com/intellij/updater/PatchFileCreator.java
new file mode 100644
index 000000000000..4eab80cfae47
--- /dev/null
+++ b/updater/src/com/intellij/updater/PatchFileCreator.java
@@ -0,0 +1,115 @@
+package com.intellij.updater;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+public class PatchFileCreator {
+ private static final String PATCH_INFO_FILE_NAME = ".patch-info";
+
+ public static void create(File olderDir,
+ File newerDir,
+ File patchFile,
+ List<String> ignoredFiles,
+ List<String> criticalFiles,
+ List<String> optionalFiles,
+ UpdaterUI ui) throws IOException, OperationCancelledException {
+ Patch patchInfo = new Patch(olderDir, newerDir, ignoredFiles, criticalFiles, optionalFiles, ui);
+ ui.startProcess("Creating the patch file '" + patchFile + "'...");
+ ui.checkCancelled();
+
+ ZipOutputStream out = new ZipOutputStream(new FileOutputStream(patchFile));
+ try {
+ out.setLevel(9);
+
+ out.putNextEntry(new ZipEntry(PATCH_INFO_FILE_NAME));
+ patchInfo.write(out);
+ out.closeEntry();
+
+ List<PatchAction> actions = patchInfo.getActions();
+ for (PatchAction each : actions) {
+ ui.setStatus("Packing " + each.getPath());
+ ui.checkCancelled();
+ each.buildPatchFile(olderDir, newerDir, out);
+ }
+ }
+ finally {
+ out.close();
+ }
+ }
+
+ public static PreparationResult prepareAndValidate(File patchFile,
+ File toDir,
+ UpdaterUI ui) throws IOException, OperationCancelledException {
+ Patch patch;
+
+ ZipFile zipFile = new ZipFile(patchFile);
+ try {
+ InputStream in = Utils.getEntryInputStream(zipFile, PATCH_INFO_FILE_NAME);
+ try {
+ patch = new Patch(in);
+ }
+ finally {
+ in.close();
+ }
+ }
+ finally {
+ zipFile.close();
+ }
+
+ List<ValidationResult> validationResults = patch.validate(toDir, ui);
+ return new PreparationResult(patch, patchFile, toDir, validationResults);
+ }
+
+ public static boolean apply(PreparationResult preparationResult,
+ Map<String, ValidationResult.Option> options,
+ UpdaterUI ui) throws IOException, OperationCancelledException {
+ return apply(preparationResult, options, Utils.createTempDir(), ui).applied;
+ }
+
+ public static Patch.ApplicationResult apply(PreparationResult preparationResult,
+ Map<String, ValidationResult.Option> options,
+ File backupDir,
+ UpdaterUI ui) throws IOException, OperationCancelledException {
+ ZipFile zipFile = new ZipFile(preparationResult.patchFile);
+ try {
+ return preparationResult.patch.apply(zipFile, preparationResult.toDir, backupDir, options, ui);
+ }
+ finally {
+ zipFile.close();
+ }
+ }
+
+ public static void revert(PreparationResult preparationResult,
+ List<PatchAction> actionsToRevert,
+ File backupDir,
+ UpdaterUI ui) throws IOException, OperationCancelledException {
+ ZipFile zipFile = new ZipFile(preparationResult.patchFile);
+ try {
+ preparationResult.patch.revert(actionsToRevert, backupDir, preparationResult.toDir, ui);
+ }
+ finally {
+ zipFile.close();
+ }
+ }
+
+ public static class PreparationResult {
+ public Patch patch;
+ public File patchFile;
+ public File toDir;
+ public List<ValidationResult> validationResults;
+
+ public PreparationResult(Patch patch, File patchFile, File toDir, List<ValidationResult> validationResults) {
+ this.patch = patch;
+ this.patchFile = patchFile;
+ this.toDir = toDir;
+ this.validationResults = validationResults;
+ }
+ }
+}
diff --git a/updater/src/com/intellij/updater/Runner.java b/updater/src/com/intellij/updater/Runner.java
new file mode 100644
index 000000000000..e6e02a81fde7
--- /dev/null
+++ b/updater/src/com/intellij/updater/Runner.java
@@ -0,0 +1,230 @@
+package com.intellij.updater;
+
+import javax.swing.*;
+import java.io.*;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+
+public class Runner {
+ private static final String PATCH_FILE_NAME = "patch-file.zip";
+ private static final String PATCH_PROPERTIES_ENTRY = "patch.properties";
+ private static final String OLD_BUILD_DESCRIPTION = "old.build.description";
+ private static final String NEW_BUILD_DESCRIPTION = "new.build.description";
+
+ public static void main(String[] args) throws Exception {
+ if (args.length != 2 && args.length < 6) {
+ printUsage();
+ return;
+ }
+
+ String command = args[0];
+
+ if ("create".equals(command)) {
+ if (args.length < 6) {
+ printUsage();
+ return;
+ }
+ String oldVersionDesc = args[1];
+ String newVersionDesc = args[2];
+ String oldFolder = args[3];
+ String newFolder = args[4];
+ String patchFile = args[5];
+ List<String> ignoredFiles = extractFiles(args, "ignored");
+ List<String> criticalFiles = extractFiles(args, "critical");
+ List<String> optionalFiles = extractFiles(args, "optional");
+ create(oldVersionDesc, newVersionDesc, oldFolder, newFolder, patchFile, ignoredFiles, criticalFiles, optionalFiles);
+ }
+ else if ("install".equals(command)) {
+ if (args.length != 2) {
+ printUsage();
+ return;
+ }
+
+ String destFolder = args[1];
+ install(destFolder);
+ }
+ else {
+ printUsage();
+ return;
+ }
+ }
+
+ public static List<String> extractFiles(String[] args, String paramName) {
+ List<String> result = new ArrayList<String>();
+ for (String param : args) {
+ if (param.startsWith(paramName + "=")) {
+ param = param.substring((paramName + "=").length());
+ for (StringTokenizer tokenizer = new StringTokenizer(param, ";"); tokenizer.hasMoreTokens();) {
+ String each = tokenizer.nextToken();
+ result.add(each);
+ }
+ }
+ }
+ return result;
+ }
+
+ private static void printUsage() {
+ System.err.println("Usage:\n" +
+ "create <old_version_description> <new_version_description> <old_version_folder> <new_version_folder> <patch_file_name> [ignored=file1;file2;...] [critical=file1;file2;...] [optional=file1;file2;...]\n" +
+ "install <destination_folder>\n");
+ }
+
+ private static void create(String oldBuildDesc,
+ String newBuildDesc,
+ String oldFolder,
+ String newFolder,
+ String patchFile,
+ List<String> ignoredFiles,
+ List<String> criticalFiles,
+ List<String> optionalFiles) throws IOException, OperationCancelledException {
+ UpdaterUI ui = new ConsoleUpdaterUI();
+ try {
+ File tempPatchFile = Utils.createTempFile();
+ PatchFileCreator.create(new File(oldFolder),
+ new File(newFolder),
+ tempPatchFile,
+ ignoredFiles,
+ criticalFiles,
+ optionalFiles,
+ ui);
+
+ ui.startProcess("Packing jar file '" + patchFile + "'...");
+ ZipOutputWrapper out = new ZipOutputWrapper(new FileOutputStream(patchFile));
+ try {
+ ZipInputStream in = new ZipInputStream(new FileInputStream(resolveJarFile()));
+ try {
+ ZipEntry e;
+ while ((e = in.getNextEntry()) != null) {
+ out.zipEntry(e, in);
+ }
+ }
+ finally {
+ in.close();
+ }
+
+ ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+ try {
+ Properties props = new Properties();
+ props.put(OLD_BUILD_DESCRIPTION, oldBuildDesc);
+ props.put(NEW_BUILD_DESCRIPTION, newBuildDesc);
+ props.store(byteOut, "");
+ }
+ finally {
+ byteOut.close();
+ }
+
+ out.zipBytes(PATCH_PROPERTIES_ENTRY, byteOut);
+ out.zipFile(PATCH_FILE_NAME, tempPatchFile);
+ }
+ finally {
+ out.close();
+ }
+ }
+ finally {
+ cleanup(ui);
+ }
+ }
+
+ private static void cleanup(UpdaterUI ui) throws IOException {
+ ui.startProcess("Cleaning up...");
+ ui.setProgressIndeterminate();
+ Utils.cleanup();
+ }
+
+ private static void install(final String destFolder) throws Exception {
+ InputStream in = Runner.class.getResourceAsStream("/" + PATCH_PROPERTIES_ENTRY);
+ Properties props = new Properties();
+ try {
+ props.load(in);
+ }
+ finally {
+ in.close();
+ }
+
+ try {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ }
+ catch (Exception ignore) {
+ }
+
+ new SwingUpdaterUI(props.getProperty(OLD_BUILD_DESCRIPTION),
+ props.getProperty(NEW_BUILD_DESCRIPTION),
+ new SwingUpdaterUI.InstallOperation() {
+ public boolean execute(UpdaterUI ui) throws OperationCancelledException {
+ return doInstall(ui, destFolder);
+ }
+ });
+ }
+
+ private static boolean doInstall(UpdaterUI ui, String destFolder) throws OperationCancelledException {
+ try {
+ try {
+ File patchFile = Utils.createTempFile();
+ ZipFile jarFile = new ZipFile(resolveJarFile());
+
+ ui.startProcess("Extracting patch file...");
+ ui.setProgressIndeterminate();
+ try {
+ InputStream in = Utils.getEntryInputStream(jarFile, PATCH_FILE_NAME);
+ OutputStream out = new BufferedOutputStream(new FileOutputStream(patchFile));
+ try {
+ Utils.copyStream(in, out);
+ }
+ finally {
+ in.close();
+ out.close();
+ }
+ }
+ finally {
+ jarFile.close();
+ }
+
+ ui.checkCancelled();
+
+ File destDir = new File(destFolder);
+ PatchFileCreator.PreparationResult result = PatchFileCreator.prepareAndValidate(patchFile, destDir, ui);
+ Map<String, ValidationResult.Option> options = ui.askUser(result.validationResults);
+ return PatchFileCreator.apply(result, options, ui);
+ }
+ catch (IOException e) {
+ ui.showError(e);
+ }
+ }
+ finally {
+ try {
+ cleanup(ui);
+ }
+ catch (IOException e) {
+ ui.showError(e);
+ }
+ }
+
+ return false;
+ }
+
+ private static File resolveJarFile() throws IOException {
+ URL url = Runner.class.getResource("");
+ if (url == null) throw new IOException("Cannot resolve jar file path");
+ if (!"jar".equals(url.getProtocol())) throw new IOException("Patch file is not a 'jar' file");
+
+ String path = url.getPath();
+
+ int start = path.indexOf("file:/");
+ int end = path.indexOf("!/");
+ if (start == -1 || end == -1) throw new IOException("Unknown protocol: " + url);
+
+ String jarFileUrl = path.substring(start, end);
+
+ try {
+ return new File(new URI(jarFileUrl));
+ }
+ catch (URISyntaxException e) {
+ throw new IOException(e.getMessage());
+ }
+ }
+}
diff --git a/updater/src/com/intellij/updater/SwingUpdaterUI.java b/updater/src/com/intellij/updater/SwingUpdaterUI.java
new file mode 100644
index 000000000000..7d28d444519a
--- /dev/null
+++ b/updater/src/com/intellij/updater/SwingUpdaterUI.java
@@ -0,0 +1,559 @@
+package com.intellij.updater;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import javax.swing.table.AbstractTableModel;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.TableColumn;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class SwingUpdaterUI implements UpdaterUI {
+ private static final int RESULT_REQUIRES_RESTART = 42;
+
+ private static final EmptyBorder FRAME_BORDER = new EmptyBorder(8, 8, 8, 8);
+ private static final EmptyBorder LABEL_BORDER = new EmptyBorder(0, 0, 5, 0);
+ private static final EmptyBorder BUTTONS_BORDER = new EmptyBorder(5, 0, 0, 0);
+
+ private static final String TITLE = "Update";
+
+ private static final String CANCEL_BUTTON_TITLE = "Cancel";
+ private static final String EXIT_BUTTON_TITLE = "Exit";
+
+ private static final String PROCEED_BUTTON_TITLE = "Proceed";
+
+ private final InstallOperation myOperation;
+
+ private final JLabel myProcessTitle;
+ private final JProgressBar myProcessProgress;
+ private final JLabel myProcessStatus;
+ private final JTextArea myConsole;
+ private final JPanel myConsolePane;
+
+ private final JButton myCancelButton;
+
+ private final ConcurrentLinkedQueue<UpdateRequest> myQueue = new ConcurrentLinkedQueue<UpdateRequest>();
+ private final AtomicBoolean isCancelled = new AtomicBoolean(false);
+ private final AtomicBoolean isRunning = new AtomicBoolean(false);
+ private final AtomicBoolean hasError = new AtomicBoolean(false);
+ private final JFrame myFrame;
+ private boolean myApplied;
+
+ public SwingUpdaterUI(String oldBuildDesc, String newBuildDesc, InstallOperation operation) {
+ myOperation = operation;
+
+ myProcessTitle = new JLabel(" ");
+ myProcessProgress = new JProgressBar(0, 100);
+ myProcessStatus = new JLabel(" ");
+
+ myCancelButton = new JButton(CANCEL_BUTTON_TITLE);
+
+ myConsole = new JTextArea();
+ myConsole.setLineWrap(true);
+ myConsole.setWrapStyleWord(true);
+ myConsole.setCaretPosition(myConsole.getText().length());
+ myConsole.setTabSize(1);
+ myConsolePane = new JPanel(new BorderLayout());
+ myConsolePane.add(new JScrollPane(myConsole));
+ myConsolePane.setBorder(BUTTONS_BORDER);
+ myConsolePane.setVisible(false);
+
+ myCancelButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ doCancel();
+ }
+ });
+
+ myFrame = new JFrame();
+ myFrame.setTitle(TITLE);
+
+ myFrame.setLayout(new BorderLayout());
+ myFrame.getRootPane().setBorder(FRAME_BORDER);
+ myFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+
+ myFrame.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ doCancel();
+ }
+ });
+
+ JPanel processPanel = new JPanel();
+ processPanel.setLayout(new BoxLayout(processPanel, BoxLayout.Y_AXIS));
+ processPanel.add(myProcessTitle);
+ processPanel.add(myProcessProgress);
+ processPanel.add(myProcessStatus);
+
+ processPanel.add(myConsolePane);
+ for (Component each : processPanel.getComponents()) {
+ ((JComponent)each).setAlignmentX(Component.LEFT_ALIGNMENT);
+ }
+
+ JPanel buttonsPanel = new JPanel();
+ buttonsPanel.setBorder(BUTTONS_BORDER);
+ buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.X_AXIS));
+ buttonsPanel.add(Box.createHorizontalGlue());
+ buttonsPanel.add(myCancelButton);
+
+ myProcessTitle.setText("<html>Updating " + oldBuildDesc + " to " + newBuildDesc + "...");
+
+ myFrame.add(processPanel, BorderLayout.CENTER);
+ myFrame.add(buttonsPanel, BorderLayout.SOUTH);
+
+ myFrame.setMinimumSize(new Dimension(500, 50));
+ myFrame.pack();
+
+ centerOnScreen(myFrame);
+
+ myFrame.setVisible(true);
+
+ myQueue.add(new UpdateRequest() {
+ @Override
+ public void perform() {
+ doPerform();
+ }
+ });
+
+ startRequestDispatching();
+ }
+
+ private void startRequestDispatching() {
+ new Thread(new Runnable() {
+ public void run() {
+ while (true) {
+ try {
+ Thread.sleep(100);
+ }
+ catch (InterruptedException e) {
+ return;
+ }
+
+ final List<UpdateRequest> pendingRequests = new ArrayList<UpdateRequest>();
+ UpdateRequest request;
+ while ((request = myQueue.poll()) != null) {
+ pendingRequests.add(request);
+ }
+
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ for (UpdateRequest each : pendingRequests) {
+ each.perform();
+ }
+ }
+ });
+ }
+ }
+ }).start();
+ }
+
+ private static void centerOnScreen(Window frame) {
+ Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+ frame.setLocation((screenSize.width - frame.getWidth()) / 2,
+ (screenSize.height - frame.getHeight()) / 2);
+ }
+
+ private void doCancel() {
+ if (isRunning.get()) {
+ int result = JOptionPane.showConfirmDialog(myFrame,
+ "The patch has not been applied yet.\nAre you sure you want to abort the operation?",
+ TITLE, JOptionPane.YES_NO_OPTION);
+ if (result == JOptionPane.YES_OPTION) {
+ isCancelled.set(true);
+ myCancelButton.setEnabled(false);
+ }
+ }
+ else {
+ exit();
+ }
+ }
+
+ private void doPerform() {
+ isRunning.set(true);
+
+ new Thread(new Runnable() {
+ public void run() {
+ try {
+ myApplied = myOperation.execute(SwingUpdaterUI.this);
+ }
+ catch (OperationCancelledException ignore) {
+ }
+ finally {
+ isRunning.set(false);
+
+ if (hasError.get()) {
+ startProcess("Failed to apply patch");
+ setProgress(100);
+ myCancelButton.setText(EXIT_BUTTON_TITLE);
+ myCancelButton.setEnabled(true);
+ } else {
+ exit();
+ }
+ }
+ }
+ }).start();
+ }
+
+ private void exit() {
+ System.exit(myApplied ? RESULT_REQUIRES_RESTART : 0);
+ }
+
+ public Map<String, ValidationResult.Option> askUser(final List<ValidationResult> validationResults) throws OperationCancelledException {
+ if (validationResults.isEmpty()) return Collections.emptyMap();
+
+ final Map<String, ValidationResult.Option> result = new HashMap<String, ValidationResult.Option>();
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
+ final JDialog dialog = new JDialog(myFrame, TITLE, true);
+ dialog.setLayout(new BorderLayout());
+ dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
+
+ JPanel buttonsPanel = new JPanel();
+ buttonsPanel.setBorder(BUTTONS_BORDER);
+ buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.X_AXIS));
+ buttonsPanel.add(Box.createHorizontalGlue());
+ JButton proceedButton = new JButton(PROCEED_BUTTON_TITLE);
+ proceedButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ dialog.setVisible(false);
+ }
+ });
+
+ JButton cancelButton = new JButton(CANCEL_BUTTON_TITLE);
+ cancelButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ isCancelled.set(true);
+ myCancelButton.setEnabled(false);
+ dialog.setVisible(false);
+ }
+ });
+
+ buttonsPanel.add(proceedButton);
+ buttonsPanel.add(cancelButton);
+
+ dialog.getRootPane().setDefaultButton(proceedButton);
+
+ JTable table = new JTable();
+
+ table.setCellSelectionEnabled(true);
+ table.setDefaultEditor(ValidationResult.Option.class, new MyCellEditor());
+ table.setDefaultRenderer(Object.class, new MyCellRenderer());
+ MyTableModel model = new MyTableModel(validationResults);
+ table.setModel(model);
+
+ for (int i = 0; i < table.getColumnModel().getColumnCount(); i++) {
+ TableColumn each = table.getColumnModel().getColumn(i);
+ each.setPreferredWidth(MyTableModel.getColumnWidth(i, new Dimension(600, 400).width));
+ }
+
+ String message = "<html>There are some conflicts found in the installation.<br><br>" +
+ "Please select desired solutions from the " + MyTableModel.COLUMNS[MyTableModel.OPTIONS_COLUMN_INDEX] +
+ " column and press " + PROCEED_BUTTON_TITLE + ".<br>" +
+ "If you do not want to proceed with the update, please press " + CANCEL_BUTTON_TITLE + ".</html>";
+
+ JLabel label = new JLabel(message);
+ label.setBorder(LABEL_BORDER);
+ dialog.add(label, BorderLayout.NORTH);
+ dialog.add(new JScrollPane(table), BorderLayout.CENTER);
+ dialog.add(buttonsPanel, BorderLayout.SOUTH);
+
+ dialog.getRootPane().setBorder(FRAME_BORDER);
+
+ dialog.setSize(new Dimension(600, 400));
+ centerOnScreen(dialog);
+ dialog.setVisible(true);
+
+ result.putAll(model.getResult());
+ }
+ });
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ checkCancelled();
+ return result;
+ }
+
+ public void startProcess(final String title) {
+ myQueue.add(new UpdateRequest() {
+ public void perform() {
+ myProcessStatus.setText(title);
+ myProcessProgress.setIndeterminate(false);
+ myProcessProgress.setValue(0);
+ }
+ });
+ }
+
+ public void setProgress(final int percentage) {
+ myQueue.add(new UpdateRequest() {
+ public void perform() {
+ myProcessProgress.setIndeterminate(false);
+ myProcessProgress.setValue(percentage);
+ }
+ });
+ }
+
+ public void setProgressIndeterminate() {
+ myQueue.add(new UpdateRequest() {
+ public void perform() {
+ myProcessProgress.setIndeterminate(true);
+ }
+ });
+ }
+
+ public void setStatus(final String status) {
+ }
+
+ public void showError(final Throwable e) {
+ myQueue.add(new UpdateRequest() {
+ public void perform() {
+ StringWriter w = new StringWriter();
+ e.printStackTrace(new PrintWriter(w));
+ w.append("\n");
+ myConsole.append(w.getBuffer().toString());
+ if (!myConsolePane.isVisible()) {
+ myConsolePane.setVisible(true);
+ myConsolePane.setPreferredSize(new Dimension(10, 200));
+ myFrame.pack();
+ }
+ hasError.set(true);
+ }
+ });
+ }
+
+ public void checkCancelled() throws OperationCancelledException {
+ if (isCancelled.get()) throw new OperationCancelledException();
+ }
+
+ public interface InstallOperation {
+ boolean execute(UpdaterUI ui) throws OperationCancelledException;
+ }
+
+ private interface UpdateRequest {
+ void perform();
+ }
+
+ public static void main(String[] args) {
+ new SwingUpdaterUI("xxx", "yyy", new InstallOperation() {
+ public boolean execute(UpdaterUI ui) throws OperationCancelledException {
+ ui.startProcess("Process1");
+ ui.checkCancelled();
+ for (int i = 0; i < 200; i++) {
+ ui.setStatus("i = " + i);
+ ui.checkCancelled();
+ try {
+ Thread.sleep(10);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ ui.setProgress((i + 1) * 100 / 200);
+ }
+
+ ui.showError(new Throwable());
+
+ ui.startProcess("Process3");
+ ui.checkCancelled();
+ ui.setProgressIndeterminate();
+ try {
+ for (int i = 0; i < 200; i++) {
+ ui.setStatus("i = " + i);
+ ui.checkCancelled();
+ try {
+ Thread.sleep(10);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ ui.setProgress((i + 1) * 100 / 200);
+ if (i == 100) {
+ List<ValidationResult> vr = new ArrayList<ValidationResult>();
+ vr.add(new ValidationResult(ValidationResult.Kind.ERROR,
+ "foo/bar",
+ ValidationResult.Action.CREATE,
+ "Hello",
+ ValidationResult.Option.REPLACE,
+ ValidationResult.Option.KEEP));
+ vr.add(new ValidationResult(ValidationResult.Kind.CONFLICT,
+ "foo/bar/baz",
+ ValidationResult.Action.DELETE,
+ "World",
+ ValidationResult.Option.DELETE,
+ ValidationResult.Option.KEEP));
+ vr.add(new ValidationResult(ValidationResult.Kind.INFO,
+ "xxx",
+ ValidationResult.Action.NO_ACTION,
+ "bla-bla",
+ ValidationResult.Option.IGNORE));
+ ui.askUser(vr);
+ }
+ }
+ }
+ finally {
+ ui.startProcess("Process2");
+ for (int i = 0; i < 200; i++) {
+ ui.setStatus("i = " + i);
+ try {
+ Thread.sleep(10);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ ui.setProgress((i + 1) * 100 / 200);
+ }
+ }
+ return true;
+ }
+ });
+ }
+
+ private static class MyTableModel extends AbstractTableModel {
+ public static final String[] COLUMNS = new String[]{"File", "Action", "Problem", "Solution"};
+ public static final int OPTIONS_COLUMN_INDEX = 3;
+ private final List<Item> myItems = new ArrayList<Item>();
+
+ public MyTableModel(List<ValidationResult> validationResults) {
+ for (ValidationResult each : validationResults) {
+ myItems.add(new Item(each, each.options.get(0)));
+ }
+ }
+
+ public int getColumnCount() {
+ return COLUMNS.length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ return COLUMNS[column];
+ }
+
+ public static int getColumnWidth(int column, int totalWidth) {
+ switch (column) {
+ case 0:
+ return (int)(totalWidth * 0.6);
+ default:
+ return (int)(totalWidth * 0.15);
+ }
+ }
+
+ @Override
+ public Class<?> getColumnClass(int columnIndex) {
+ if (columnIndex == OPTIONS_COLUMN_INDEX) {
+ return ValidationResult.Option.class;
+ }
+ return super.getColumnClass(columnIndex);
+ }
+
+ public int getRowCount() {
+ return myItems.size();
+ }
+
+ @Override
+ public boolean isCellEditable(int rowIndex, int columnIndex) {
+ return columnIndex == OPTIONS_COLUMN_INDEX && getOptions(rowIndex).size() > 1;
+ }
+
+ @Override
+ public void setValueAt(Object value, int rowIndex, int columnIndex) {
+ if (columnIndex == OPTIONS_COLUMN_INDEX) {
+ myItems.get(rowIndex).option = (ValidationResult.Option)value;
+ }
+ }
+
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ Item item = myItems.get(rowIndex);
+ switch (columnIndex) {
+ case 0:
+ return item.validationResult.path;
+ case 1:
+ return item.validationResult.action;
+ case 2:
+ return item.validationResult.message;
+ case OPTIONS_COLUMN_INDEX:
+ return item.option;
+ }
+ return null;
+ }
+
+ public ValidationResult.Kind getKind(int rowIndex) {
+ return myItems.get(rowIndex).validationResult.kind;
+ }
+
+ public List<ValidationResult.Option> getOptions(int rowIndex) {
+ Item item = myItems.get(rowIndex);
+ return item.validationResult.options;
+ }
+
+ public Map<String, ValidationResult.Option> getResult() {
+ Map<String, ValidationResult.Option> result = new HashMap<String, ValidationResult.Option>();
+ for (Item each : myItems) {
+ result.put(each.validationResult.path, each.option);
+ }
+ return result;
+ }
+
+ private static class Item {
+ ValidationResult validationResult;
+ ValidationResult.Option option;
+
+ private Item(ValidationResult validationResult, ValidationResult.Option option) {
+ this.validationResult = validationResult;
+ this.option = option;
+ }
+ }
+ }
+
+ private static class MyCellEditor extends DefaultCellEditor {
+ public MyCellEditor() {
+ super(new JComboBox());
+ }
+
+ @Override
+ public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
+ MyTableModel tableModel = (MyTableModel)table.getModel();
+ DefaultComboBoxModel comboModel = new DefaultComboBoxModel();
+
+ for (ValidationResult.Option each : tableModel.getOptions(row)) {
+ comboModel.addElement(each);
+ }
+ ((JComboBox)editorComponent).setModel(comboModel);
+
+ return super.getTableCellEditorComponent(table, value, isSelected, row, column);
+ }
+ }
+
+ private static class MyCellRenderer extends DefaultTableCellRenderer {
+ @Override
+ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
+ Component result = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+ if (!isSelected) {
+ MyTableModel tableModel = (MyTableModel)table.getModel();
+ Color color = table.getBackground();
+
+ switch (tableModel.getKind(row)) {
+ case ERROR:
+ color = new Color(255, 175, 175);
+ break;
+ case CONFLICT:
+ color = new Color(255, 240, 240);
+ break;
+ }
+ result.setBackground(color);
+ }
+ return result;
+ }
+ }
+}
diff --git a/updater/src/com/intellij/updater/UpdateAction.java b/updater/src/com/intellij/updater/UpdateAction.java
new file mode 100644
index 000000000000..6c79c44cbc4b
--- /dev/null
+++ b/updater/src/com/intellij/updater/UpdateAction.java
@@ -0,0 +1,48 @@
+package com.intellij.updater;
+
+import java.io.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+public class UpdateAction extends BaseUpdateAction {
+ public UpdateAction(String path, long checksum) {
+ super(path, checksum);
+ }
+
+ public UpdateAction(DataInputStream in) throws IOException {
+ super(in);
+ }
+
+ @Override
+ protected void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException {
+ patchOutput.putNextEntry(new ZipEntry(myPath));
+ writeExecutableFlag(patchOutput, newerFile);
+ writeDiff(olderFile, newerFile, patchOutput);
+ patchOutput.closeEntry();
+ }
+
+ @Override
+ protected void doApply(ZipFile patchFile, File toFile) throws IOException {
+ InputStream in = Utils.findEntryInputStream(patchFile, myPath);
+ boolean executable = readExecutableFlag(in);
+
+ File temp = Utils.createTempFile();
+ OutputStream out = new BufferedOutputStream(new FileOutputStream(temp));
+ try {
+ InputStream oldFileIn = new FileInputStream(toFile);
+ try {
+ applyDiff(in, oldFileIn, out);
+ }
+ finally {
+ oldFileIn.close();
+ }
+ }
+ finally {
+ out.close();
+ }
+
+ replaceUpdated(temp, toFile);
+ Utils.setExecutable(toFile, executable);
+ }
+}
diff --git a/updater/src/com/intellij/updater/UpdateZipAction.java b/updater/src/com/intellij/updater/UpdateZipAction.java
new file mode 100644
index 000000000000..553e7d8da0a4
--- /dev/null
+++ b/updater/src/com/intellij/updater/UpdateZipAction.java
@@ -0,0 +1,246 @@
+package com.intellij.updater;
+
+import java.io.*;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+public class UpdateZipAction extends BaseUpdateAction {
+ Set<String> myFilesToCreate;
+ Set<String> myFilesToUpdate;
+ Set<String> myFilesToDelete;
+
+ public UpdateZipAction(String path, long checksum) {
+ super(path, checksum);
+ }
+
+ // test support
+ public UpdateZipAction(String path,
+ Collection<String> filesToCreate,
+ Collection<String> filesToUpdate,
+ Collection<String> filesToDelete,
+ long checksum) {
+ super(path, checksum);
+ myFilesToCreate = new HashSet<String>(filesToCreate);
+ myFilesToUpdate = new HashSet<String>(filesToUpdate);
+ myFilesToDelete = new HashSet<String>(filesToDelete);
+ }
+
+ public UpdateZipAction(DataInputStream in) throws IOException {
+ super(in);
+
+ int count = in.readInt();
+ myFilesToCreate = new HashSet<String>(count);
+ while (count-- > 0) {
+ myFilesToCreate.add(in.readUTF());
+ }
+
+ count = in.readInt();
+ myFilesToUpdate = new HashSet<String>(count);
+ while (count-- > 0) {
+ myFilesToUpdate.add(in.readUTF());
+ }
+
+ count = in.readInt();
+ myFilesToDelete = new HashSet<String>(count);
+ while (count-- > 0) {
+ myFilesToDelete.add(in.readUTF());
+ }
+ }
+
+ @Override
+ public void write(DataOutputStream out) throws IOException {
+ super.write(out);
+
+ out.writeInt(myFilesToCreate.size());
+ for (String each : myFilesToCreate) {
+ out.writeUTF(each);
+ }
+
+ out.writeInt(myFilesToUpdate.size());
+ for (String each : myFilesToUpdate) {
+ out.writeUTF(each);
+ }
+
+ out.writeInt(myFilesToDelete.size());
+ for (String each : myFilesToDelete) {
+ out.writeUTF(each);
+ }
+ }
+
+ @Override
+ protected boolean doCalculate(File olderFile, File newerFile) throws IOException {
+ final Map<String, Long> oldCheckSums = new HashMap<String, Long>();
+ final Map<String, Long> newCheckSums = new HashMap<String, Long>();
+
+ processZipFile(olderFile, new Processor() {
+ public void process(ZipEntry entry, InputStream in) throws IOException {
+ oldCheckSums.put(entry.getName(), Digester.digestStream(in));
+ }
+ });
+
+ processZipFile(newerFile, new Processor() {
+ public void process(ZipEntry entry, InputStream in) throws IOException {
+ newCheckSums.put(entry.getName(), Digester.digestStream(in));
+ }
+ });
+
+ DiffCalculator.Result diff = DiffCalculator.calculate(oldCheckSums, newCheckSums);
+
+ myFilesToCreate = diff.filesToCreate;
+ myFilesToUpdate = diff.filesToUpdate.keySet();
+ myFilesToDelete = diff.filesToDelete.keySet();
+
+ return !(myFilesToCreate.isEmpty() && myFilesToUpdate.isEmpty() && myFilesToDelete.isEmpty());
+ }
+
+ @Override
+ public void doBuildPatchFile(final File olderFile, final File newerFile, final ZipOutputStream patchOutput) throws IOException {
+ try {
+ //noinspection IOResourceOpenedButNotSafelyClosed
+ new ZipFile(newerFile).close();
+ }
+ catch (IOException e) {
+ throw new IOException("Corrupted target file: " + newerFile, e);
+ }
+
+ final Set<String> filesToProcess = new HashSet<String>(myFilesToCreate);
+ filesToProcess.addAll(myFilesToUpdate);
+ if (filesToProcess.isEmpty()) return;
+
+ final ZipFile olderZip;
+ try {
+ olderZip = new ZipFile(olderFile);
+ }
+ catch (IOException e) {
+ throw new IOException("Corrupted source file: " + olderFile, e);
+ }
+
+ try {
+ processZipFile(newerFile, new Processor() {
+ public void process(ZipEntry newerEntry, InputStream newerEntryIn) throws IOException {
+ if (newerEntry.isDirectory()) return;
+ String name = newerEntry.getName();
+ if (!filesToProcess.contains(name)) return;
+
+ try {
+ patchOutput.putNextEntry(new ZipEntry(myPath + "/" + name));
+ InputStream olderEntryIn = Utils.findEntryInputStream(olderZip, name);
+ if (olderEntryIn == null) {
+ Utils.copyStream(newerEntryIn, patchOutput);
+ }
+ else {
+ writeDiff(olderEntryIn, newerEntryIn, patchOutput);
+ }
+ patchOutput.closeEntry();
+ }
+ catch (IOException e) {
+ throw new IOException("Error building patch for .zip entry " + name, e);
+ }
+ }
+ });
+ }
+ finally {
+ olderZip.close();
+ }
+ }
+
+ protected void doApply(final ZipFile patchFile, File toFile) throws IOException {
+ File temp = Utils.createTempFile();
+ @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
+ final ZipOutputWrapper out = new ZipOutputWrapper(new FileOutputStream(temp));
+ out.setCompressionLevel(0);
+
+ try {
+ processZipFile(toFile, new Processor() {
+ public void process(ZipEntry entry, InputStream in) throws IOException {
+ String path = entry.getName();
+ if (myFilesToDelete.contains(path)) return;
+
+ if (myFilesToUpdate.contains(path)) {
+ OutputStream entryOut = out.zipStream(path);
+ try {
+ applyDiff(Utils.findEntryInputStream(patchFile, myPath + "/" + path), in, entryOut);
+ }
+ finally {
+ entryOut.close();
+ }
+ }
+ else {
+ out.zipEntry(entry, in);
+ }
+ }
+ });
+
+ for (String each : myFilesToCreate) {
+ InputStream in = Utils.getEntryInputStream(patchFile, myPath + "/" + each);
+ try {
+ out.zipEntry(each, in);
+ }
+ finally {
+ in.close();
+ }
+ }
+ }
+ finally {
+ out.close();
+ }
+
+ replaceUpdated(temp, toFile);
+ }
+
+ private static void processZipFile(File file, Processor processor) throws IOException {
+ ZipInputStream in = new ZipInputStream(new FileInputStream(file));
+ try {
+ ZipEntry inEntry;
+ Set<String> processed = new HashSet<String>();
+ while ((inEntry = in.getNextEntry()) != null) {
+ if (inEntry.isDirectory()) continue;
+ if (processed.contains(inEntry.getName())) {
+ throw new IOException("Duplicate entry '" + inEntry.getName() + "' in " + file.getPath());
+ }
+ //noinspection IOResourceOpenedButNotSafelyClosed
+ processor.process(inEntry, new BufferedInputStream(in));
+ processed.add(inEntry.getName());
+ }
+ }
+ finally {
+ in.close();
+ }
+ }
+
+ private interface Processor {
+ void process(ZipEntry entry, InputStream in) throws IOException;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + myFilesToCreate + " " + myFilesToUpdate + " " + myFilesToDelete;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+
+ UpdateZipAction that = (UpdateZipAction)o;
+
+ if (myFilesToCreate != null ? !myFilesToCreate.equals(that.myFilesToCreate) : that.myFilesToCreate != null) return false;
+ if (myFilesToUpdate != null ? !myFilesToUpdate.equals(that.myFilesToUpdate) : that.myFilesToUpdate != null) return false;
+ if (myFilesToDelete != null ? !myFilesToDelete.equals(that.myFilesToDelete) : that.myFilesToDelete != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (myFilesToCreate != null ? myFilesToCreate.hashCode() : 0);
+ result = 31 * result + (myFilesToUpdate != null ? myFilesToUpdate.hashCode() : 0);
+ result = 31 * result + (myFilesToDelete != null ? myFilesToDelete.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/updater/src/com/intellij/updater/UpdaterUI.java b/updater/src/com/intellij/updater/UpdaterUI.java
new file mode 100644
index 000000000000..85005ff81205
--- /dev/null
+++ b/updater/src/com/intellij/updater/UpdaterUI.java
@@ -0,0 +1,20 @@
+package com.intellij.updater;
+
+import java.util.List;
+import java.util.Map;
+
+public interface UpdaterUI {
+ void startProcess(String title);
+
+ void setProgress(int percentage);
+
+ void setProgressIndeterminate();
+
+ void setStatus(String status);
+
+ void showError(Throwable e);
+
+ void checkCancelled() throws OperationCancelledException;
+
+ Map<String, ValidationResult.Option> askUser(List<ValidationResult> validationResults) throws OperationCancelledException;
+} \ No newline at end of file
diff --git a/updater/src/com/intellij/updater/Utils.java b/updater/src/com/intellij/updater/Utils.java
new file mode 100644
index 000000000000..427788eaf1ac
--- /dev/null
+++ b/updater/src/com/intellij/updater/Utils.java
@@ -0,0 +1,177 @@
+package com.intellij.updater;
+
+import java.io.*;
+import java.util.LinkedHashSet;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class Utils {
+ // keep buffer static as there may be many calls of the copyStream method.
+ private static final byte[] BUFFER = new byte[64 * 1024];
+ private static File myTempDir;
+
+ public static boolean isZipFile(String fileName) {
+ return fileName.endsWith(".zip") || fileName.endsWith(".jar");
+ }
+
+ @SuppressWarnings({"SSBasedInspection"})
+ public static File createTempFile() throws IOException {
+ if (myTempDir == null) {
+ myTempDir = File.createTempFile("idea.updater", "tmp");
+ delete(myTempDir);
+ myTempDir.mkdirs();
+ }
+
+ return File.createTempFile("temp", "tmp", myTempDir);
+ }
+
+ public static File createTempDir() throws IOException {
+ File result = createTempFile();
+ delete(result);
+ result.mkdirs();
+ return result;
+ }
+
+ public static void cleanup() throws IOException {
+ if (myTempDir == null) return;
+ delete(myTempDir);
+ myTempDir = null;
+ }
+
+ public static void delete(File file) throws IOException {
+ if (file.isDirectory()) {
+ File[] files = file.listFiles();
+ if (files != null) {
+ for (File each : files) {
+ delete(each);
+ }
+ }
+ }
+ for (int i = 0; i < 10; i++) {
+ if (file.delete() || !file.exists()) return;
+ try {
+ Thread.sleep(10);
+ }
+ catch (InterruptedException ignore) {
+ }
+ }
+ if (file.exists()) throw new IOException("Cannot delete file " + file);
+ }
+
+ public static void setExecutable(File file, boolean executable) throws IOException {
+ if (executable && !file.setExecutable(true)) {
+ throw new IOException("Cannot set executable permissions for: " + file);
+ }
+ }
+
+ public static void copy(File from, File to) throws IOException {
+ if (from.isDirectory()) {
+ File[] files = from.listFiles();
+ if (files == null) throw new IOException("Cannot get directory's content: " + from);
+ for (File each : files) {
+ copy(each, new File(to, each.getName()));
+ }
+ }
+ else {
+ InputStream in = new BufferedInputStream(new FileInputStream(from));
+ try {
+ copyStreamToFile(in, to);
+ }
+ finally {
+ in.close();
+ }
+ setExecutable(to, from.canExecute());
+ }
+ }
+
+ public static void copyFileToStream(File from, OutputStream out) throws IOException {
+ InputStream in = new BufferedInputStream(new FileInputStream(from));
+ try {
+ copyStream(in, out);
+ }
+ finally {
+ in.close();
+ }
+ }
+
+ public static void copyStreamToFile(InputStream from, File to) throws IOException {
+ to.getParentFile().mkdirs();
+ OutputStream out = new BufferedOutputStream(new FileOutputStream(to));
+ try {
+ copyStream(from, out);
+ }
+ finally {
+ out.close();
+ }
+ }
+
+ public static void copyBytesToStream(ByteArrayOutputStream from, OutputStream to) throws IOException {
+ OutputStream out = new BufferedOutputStream(to);
+ try {
+ from.writeTo(out);
+ }
+ finally {
+ out.flush();
+ }
+ }
+
+ public static void copyBytesToStream(byte[] bytes, OutputStream to) throws IOException {
+ to.write(bytes);
+ }
+
+ public static byte[] readBytes(InputStream in) throws IOException {
+ ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+ try {
+ copyStream(in, byteOut);
+ }
+ finally {
+ byteOut.close();
+ }
+ return byteOut.toByteArray();
+ }
+
+ public static void copyStream(InputStream in, OutputStream out) throws IOException {
+ while (true) {
+ int read = in.read(BUFFER);
+ if (read < 0) break;
+ out.write(BUFFER, 0, read);
+ }
+ }
+
+ public static InputStream getEntryInputStream(ZipFile zipFile, String entryPath) throws IOException {
+ InputStream result = findEntryInputStream(zipFile, entryPath);
+ if (result == null) throw new IOException("Entry " + entryPath + " not found");
+ return result;
+ }
+
+ public static InputStream findEntryInputStream(ZipFile zipFile, String entryPath) throws IOException {
+ ZipEntry entry = zipFile.getEntry(entryPath);
+ if (entry == null || entry.isDirectory()) return null;
+
+ // if isDirectory check failed, check presence of 'file/' manually
+ if (!entryPath.endsWith("/") && zipFile.getEntry(entryPath + "/") != null) return null;
+
+ return new BufferedInputStream(zipFile.getInputStream(entry));
+ }
+
+ public static LinkedHashSet<String> collectRelativePaths(File dir) {
+ LinkedHashSet<String> result = new LinkedHashSet<String>();
+ collectRelativePaths(dir, result, null);
+ return result;
+ }
+
+ private static void collectRelativePaths(File dir, LinkedHashSet<String> result, String parentPath) {
+ File[] children = dir.listFiles();
+ if (children == null) return;
+
+ for (File each : children) {
+ String relativePath = (parentPath == null ? "" : parentPath + "/") + each.getName();
+ if (each.isDirectory()) {
+ collectRelativePaths(each, result, relativePath);
+ }
+ else {
+ result.add(relativePath);
+ }
+ }
+ }
+}
diff --git a/updater/src/com/intellij/updater/ValidationResult.java b/updater/src/com/intellij/updater/ValidationResult.java
new file mode 100644
index 000000000000..02c142446152
--- /dev/null
+++ b/updater/src/com/intellij/updater/ValidationResult.java
@@ -0,0 +1,94 @@
+package com.intellij.updater;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class ValidationResult implements Comparable<ValidationResult> {
+ public enum Kind {
+ INFO, CONFLICT, ERROR
+ }
+
+ public enum Action {
+ CREATE("Create"), UPDATE("Update"), DELETE("Delete"), NO_ACTION("");
+
+ private final String myDisplayString;
+
+ Action(String displayString) {
+ myDisplayString = displayString;
+ }
+
+ @Override
+ public String toString() {
+ return myDisplayString;
+ }
+ }
+
+ public enum Option {
+ IGNORE, KEEP, REPLACE, DELETE
+ }
+
+ public static final String ABSENT_MESSAGE = "Absent";
+ public static final String MODIFIED_MESSAGE = "Modified";
+ public static final String ACCESS_DENIED_MESSAGE = "Access denied";
+ public static final String ALREADY_EXISTS_MESSAGE = "Already exists";
+
+ public final Kind kind;
+ public final String path;
+ public final Action action;
+ public final String message;
+ public final List<Option> options;
+
+ public ValidationResult(Kind kind, String path, Action action, String message, Option... options) {
+ this.kind = kind;
+ this.path = path;
+ this.action = action;
+ this.message = message;
+ this.options = Arrays.asList(options);
+ }
+
+ @Override
+ public String toString() {
+ String prefix;
+ switch (kind) {
+ case CONFLICT:
+ prefix = "?";
+ break;
+ case ERROR:
+ prefix = "!";
+ break;
+ default:
+ prefix = "";
+ }
+ return prefix + action + " " + path + ": " + message + " (" + options + ")";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ValidationResult result = (ValidationResult)o;
+
+ if (action != result.action) return false;
+ if (kind != result.kind) return false;
+ if (message != null ? !message.equals(result.message) : result.message != null) return false;
+ if (options != null ? !options.equals(result.options) : result.options != null) return false;
+ if (path != null ? !path.equals(result.path) : result.path != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = kind != null ? kind.hashCode() : 0;
+ result = 31 * result + (path != null ? path.hashCode() : 0);
+ result = 31 * result + (action != null ? action.hashCode() : 0);
+ result = 31 * result + (message != null ? message.hashCode() : 0);
+ result = 31 * result + (options != null ? options.hashCode() : 0);
+ return result;
+ }
+
+ public int compareTo(ValidationResult o) {
+ return path.compareToIgnoreCase(o.path);
+ }
+}
diff --git a/updater/src/com/intellij/updater/ZipOutputWrapper.java b/updater/src/com/intellij/updater/ZipOutputWrapper.java
new file mode 100644
index 000000000000..1ccbc45ff05b
--- /dev/null
+++ b/updater/src/com/intellij/updater/ZipOutputWrapper.java
@@ -0,0 +1,143 @@
+package com.intellij.updater;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+public class ZipOutputWrapper {
+ private final ZipOutputStream myOut;
+ private final Set<String> myDirs = new HashSet<String>();
+ private boolean isCompressed = true;
+
+ public ZipOutputWrapper(OutputStream stream) {
+ myOut = new ZipOutputStream(new BufferedOutputStream(stream));
+ }
+
+ public void setCompressionLevel(int level) {
+ myOut.setLevel(level);
+ if (level == 0) {
+ myOut.setMethod(ZipEntry.STORED);
+ isCompressed = false;
+ }
+ }
+
+ public OutputStream zipStream(final String entryPath) throws IOException {
+ final ByteArrayOutputStream tempOut = new ByteArrayOutputStream();
+ return new BufferedOutputStream(new OutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ tempOut.write(b);
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ tempOut.close();
+ zipBytes(entryPath, tempOut);
+ }
+ });
+ }
+
+ public void zipEntry(ZipEntry entry, InputStream from) throws IOException {
+ if (entry.isDirectory()) {
+ addDirs(entry.getName(), true);
+ return;
+ }
+ zipEntry(entry.getName(), from);
+ }
+
+ public void zipEntry(String entryPath, InputStream from) throws IOException {
+ ByteArrayOutputStream tempOut = new ByteArrayOutputStream();
+ try {
+ Utils.copyStream(from, tempOut);
+ }
+ finally {
+ tempOut.close();
+ }
+ zipBytes(entryPath, tempOut);
+ }
+
+ public void zipBytes(String entryPath, ByteArrayOutputStream byteOut) throws IOException {
+ addDirs(entryPath, false);
+
+ ZipEntry entry = new ZipEntry(entryPath);
+ if (!isCompressed) {
+ entry.setSize(byteOut.size());
+ CRC32 crc = new CRC32();
+ crc.update(byteOut.toByteArray());
+ entry.setCrc(crc.getValue());
+ }
+
+ myOut.putNextEntry(entry);
+ try {
+ byteOut.writeTo(myOut);
+ }
+ finally {
+ myOut.closeEntry();
+ }
+ }
+
+ public void zipFile(String entryPath, File file) throws IOException {
+ if (file.isDirectory()) {
+ addDirs(entryPath, true);
+ return;
+ }
+
+ InputStream from = new BufferedInputStream(new FileInputStream(file));
+ try {
+ zipEntry(new ZipEntry(entryPath), from);
+ }
+ finally {
+ from.close();
+ }
+ }
+
+ public void zipFiles(File dir) throws IOException {
+ for (File each : dir.listFiles()) {
+ addFileToZip(each, null);
+ }
+ }
+
+ private void addFileToZip(File file, String parentPath) throws IOException {
+ String path = parentPath == null ? file.getName() : parentPath + "/" + file.getName();
+ zipFile(path, file);
+
+ if (file.isDirectory()) {
+ for (File each : file.listFiles()) {
+ addFileToZip(each, path);
+ }
+ }
+ }
+
+ private void addDirs(String relPath, boolean isDir) {
+ List<String> temp = new ArrayList<String>();
+ if (isDir && !relPath.endsWith("/")) relPath += "/";
+ int index = 0;
+ while ((index = relPath.indexOf('/', index + 1)) != -1) {
+ temp.add(relPath.substring(0, index));
+ }
+ myDirs.addAll(temp);
+ }
+
+ public void close() throws IOException {
+ try {
+ for (String each : myDirs) {
+ if (!each.endsWith("/")) each += "/";
+ ZipEntry e = new ZipEntry(each);
+ e.setMethod(ZipEntry.STORED);
+ e.setSize(0);
+ e.setCrc(0);
+ myOut.putNextEntry(e);
+ myOut.closeEntry();
+ }
+ }
+ finally {
+ myOut.close();
+ }
+ }
+}
diff --git a/updater/src/ie/wombat/jbdiff/JBDiff.java b/updater/src/ie/wombat/jbdiff/JBDiff.java
new file mode 100644
index 000000000000..92ddfd13e50a
--- /dev/null
+++ b/updater/src/ie/wombat/jbdiff/JBDiff.java
@@ -0,0 +1,487 @@
+/*
+* Copyright (c) 2005, Joe Desbonnet, (jdesbonnet@gmail.com)
+* All rights reserved.
+*
+* Redistribution and use in source and binary forms, with or without
+* modification, are permitted provided that the following conditions are met:
+* * Redistributions of source code must retain the above copyright
+* notice, this list of conditions and the following disclaimer.
+* * Redistributions in binary form must reproduce the above copyright
+* notice, this list of conditions and the following disclaimer in the
+* documentation and/or other materials provided with the distribution.
+* * Neither the name of the <organization> nor the
+* names of its contributors may be used to endorse or promote products
+* derived from this software without specific prior written permission.
+*
+* THIS SOFTWARE IS PROVIDED BY <copyright holder> ``AS IS'' AND ANY
+* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+* DISCLAIMED. IN NO EVENT SHALL <copyright holder> BE LIABLE FOR ANY
+* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+package ie.wombat.jbdiff;
+
+import com.intellij.updater.Utils;
+
+import java.io.*;
+import java.util.zip.GZIPOutputStream;
+
+/**
+ * Java Binary Diff utility. Based on
+ * bsdiff (v4.2) by Colin Percival
+ * (see http://www.daemonology.net/bsdiff/ ) and distributed under BSD license.
+ * <p/>
+ * <p>
+ * Running this on large files will probably require an increae of the default
+ * maximum heap size (use java -Xmx200m)
+ * </p>
+ *
+ * @author Joe Desbonnet, jdesbonnet@gmail.com
+ */
+
+public class JBDiff {
+
+ private static final String VERSION = "jbdiff-0.1.1";
+
+ private static final int min(int x, int y) {
+ return x < y ? x : y;
+ }
+
+ private final static void split(int[] I, int[] V, int start, int len, int h) {
+
+ int i, j, k, x, tmp, jj, kk;
+
+ if (len < 16) {
+ for (k = start; k < start + len; k += j) {
+ j = 1;
+ x = V[I[k] + h];
+ for (i = 1; k + i < start + len; i++) {
+ if (V[I[k + i] + h] < x) {
+ x = V[I[k + i] + h];
+ j = 0;
+ }
+
+ if (V[I[k + i] + h] == x) {
+ tmp = I[k + j];
+ I[k + j] = I[k + i];
+ I[k + i] = tmp;
+ j++;
+ }
+ }
+
+ for (i = 0; i < j; i++) {
+ V[I[k + i]] = k + j - 1;
+ }
+ if (j == 1) {
+ I[k] = -1;
+ }
+ }
+
+ return;
+ }
+
+ x = V[I[start + len / 2] + h];
+ jj = 0;
+ kk = 0;
+ for (i = start; i < start + len; i++) {
+ if (V[I[i] + h] < x) {
+ jj++;
+ }
+ if (V[I[i] + h] == x) {
+ kk++;
+ }
+ }
+
+ jj += start;
+ kk += jj;
+
+ i = start;
+ j = 0;
+ k = 0;
+ while (i < jj) {
+ if (V[I[i] + h] < x) {
+ i++;
+ }
+ else if (V[I[i] + h] == x) {
+ tmp = I[i];
+ I[i] = I[jj + j];
+ I[jj + j] = tmp;
+ j++;
+ }
+ else {
+ tmp = I[i];
+ I[i] = I[kk + k];
+ I[kk + k] = tmp;
+ k++;
+ }
+ }
+
+ while (jj + j < kk) {
+ if (V[I[jj + j] + h] == x) {
+ j++;
+ }
+ else {
+ tmp = I[jj + j];
+ I[jj + j] = I[kk + k];
+ I[kk + k] = tmp;
+ k++;
+ }
+ }
+
+ if (jj > start) {
+ split(I, V, start, jj - start, h);
+ }
+
+ for (i = 0; i < kk - jj; i++) {
+ V[I[jj + i]] = kk - 1;
+ }
+
+ if (jj == kk - 1) {
+ I[jj] = -1;
+ }
+
+ if (start + len > kk) {
+ split(I, V, kk, start + len - kk, h);
+ }
+ }
+
+ /**
+ * Fast suffix sporting.
+ * Larsson and Sadakane's qsufsort algorithm.
+ * See http://www.cs.lth.se/Research/Algorithms/Papers/jesper5.ps
+ *
+ * @param I
+ * @param V
+ * @param oldBuf
+ */
+ private static void qsufsort(int[] I, int[] V, byte[] oldBuf) {
+
+ int oldsize = oldBuf.length;
+
+ int[] buckets = new int[256];
+ int i, h, len;
+
+ for (i = 0; i < 256; i++) {
+ buckets[i] = 0;
+ }
+
+ for (i = 0; i < oldsize; i++) {
+ buckets[(int)oldBuf[i] & 0xff]++;
+ }
+
+ for (i = 1; i < 256; i++) {
+ buckets[i] += buckets[i - 1];
+ }
+
+ for (i = 255; i > 0; i--) {
+ buckets[i] = buckets[i - 1];
+ }
+
+ buckets[0] = 0;
+
+ for (i = 0; i < oldsize; i++) {
+ I[++buckets[(int)oldBuf[i] & 0xff]] = i;
+ }
+
+ I[0] = oldsize;
+ for (i = 0; i < oldsize; i++) {
+ V[i] = buckets[(int)oldBuf[i] & 0xff];
+ }
+ V[oldsize] = 0;
+
+ for (i = 1; i < 256; i++) {
+ if (buckets[i] == buckets[i - 1] + 1) {
+ I[buckets[i]] = -1;
+ }
+ }
+
+ I[0] = -1;
+
+ for (h = 1; I[0] != -(oldsize + 1); h += h) {
+ len = 0;
+ for (i = 0; i < oldsize + 1;) {
+ if (I[i] < 0) {
+ len -= I[i];
+ i -= I[i];
+ }
+ else {
+ //if(len) I[i-len]=-len;
+ if (len != 0) {
+ I[i - len] = -len;
+ }
+ len = V[I[i]] + 1 - i;
+ split(I, V, i, len, h);
+ i += len;
+ len = 0;
+ }
+ }
+
+ if (len != 0) {
+ I[i - len] = -len;
+ }
+ }
+
+ for (i = 0; i < oldsize + 1; i++) {
+ I[V[i]] = i;
+ }
+ }
+
+ /**
+ * Count the number of bytes that match in oldBuf (starting at offset oldOffset)
+ * and newBuf (starting at offset newOffset).
+ *
+ * @param oldBuf
+ * @param oldOffset
+ * @param newBuf
+ * @param newOffset
+ * @return
+ */
+ private final static int matchlen(byte[] oldBuf, int oldOffset, byte[] newBuf,
+ int newOffset) {
+ int end = min(oldBuf.length - oldOffset, newBuf.length - newOffset);
+ int i;
+ for (i = 0; i < end; i++) {
+ if (oldBuf[oldOffset + i] != newBuf[newOffset + i]) {
+ break;
+ }
+ }
+ return i;
+ }
+
+ private final static int search(int[] I, byte[] oldBuf, byte[] newBuf,
+ int newBufOffset, int start, int end, IntByRef pos) {
+ int x, y;
+
+ if (end - start < 2) {
+ x = matchlen(oldBuf, I[start], newBuf, newBufOffset);
+ y = matchlen(oldBuf, I[end], newBuf, newBufOffset);
+
+ if (x > y) {
+ pos.value = I[start];
+ return x;
+ }
+ else {
+ pos.value = I[end];
+ return y;
+ }
+ }
+
+ x = start + (end - start) / 2;
+ if (Util.memcmp(oldBuf, I[x], newBuf, newBufOffset) < 0) {
+ return search(I, oldBuf, newBuf, newBufOffset, x, end, pos);
+ }
+ else {
+ return search(I, oldBuf, newBuf, newBufOffset, start, x, pos);
+ }
+ }
+
+ public static byte[] bsdiff(InputStream oldFileIn, InputStream newFileIn, OutputStream diffFileOut)
+ throws IOException {
+
+ byte[] oldBuf = Utils.readBytes(oldFileIn);
+ int oldsize = oldBuf.length;
+
+ int[] I = new int[oldsize + 1];
+ int[] V = new int[oldsize + 1];
+
+ qsufsort(I, V, oldBuf);
+
+ //free(V)
+ V = null;
+ System.gc();
+
+ byte[] newBuf = Utils.readBytes(newFileIn);
+ int newsize = newBuf.length;
+
+ // diff block
+ int dblen = 0;
+ byte[] db = new byte[newsize];
+
+ // extra block
+ int eblen = 0;
+ byte[] eb = new byte[newsize];
+
+ /*
+ * Diff file is composed as follows:
+ *
+ * Header (32 bytes)
+ * Data (from offset 32 to end of file)
+ *
+ * Header:
+ * Offset 0, length 8 bytes: file magic "jbdiff40"
+ * Offset 8, length 8 bytes: length of ctrl block
+ * Offset 16, length 8 bytes: length of compressed diff block
+ * Offset 24, length 8 bytes: length of new file
+ *
+ * Data:
+ * 32 (length ctrlBlockLen): ctrlBlock
+ * 32+ctrlBlockLen (length diffBlockLen): diffBlock (gziped)
+ * 32+ctrlBlockLen+diffBlockLen (to end of file): extraBlock (gziped)
+ *
+ * ctrlBlock comprises a set of records, each record 12 bytes. A record
+ * comprises 3 x 32 bit integers. The ctrlBlock is not compressed.
+ */
+
+ ByteArrayOutputStream arrayOut = new ByteArrayOutputStream();
+ DataOutputStream diffOut = new DataOutputStream(arrayOut);
+
+ int oldscore, scsc;
+
+ int overlap, Ss, lens;
+ int i;
+ int scan = 0;
+ int len = 0;
+ int lastscan = 0;
+ int lastpos = 0;
+ int lastoffset = 0;
+
+ IntByRef pos = new IntByRef();
+ int ctrlBlockLen = 0;
+
+ while (scan < newsize) {
+
+ oldscore = 0;
+
+ for (scsc = scan += len; scan < newsize; scan++) {
+
+ len = search(I, oldBuf, newBuf, scan, 0, oldsize, pos);
+
+ for (; scsc < scan + len; scsc++) {
+ if ((scsc + lastoffset < oldsize)
+ && (oldBuf[scsc + lastoffset] == newBuf[scsc])) {
+ oldscore++;
+ }
+ }
+
+ if (((len == oldscore) && (len != 0)) || (len > oldscore + 8)) {
+ break;
+ }
+
+ if ((scan + lastoffset < oldsize)
+ && (oldBuf[scan + lastoffset] == newBuf[scan])) {
+ oldscore--;
+ }
+ }
+
+ if ((len != oldscore) || (scan == newsize)) {
+ int s = 0;
+ int Sf = 0;
+ int lenf = 0;
+ for (i = 0; (lastscan + i < scan) && (lastpos + i < oldsize);) {
+ if (oldBuf[lastpos + i] == newBuf[lastscan + i]) {
+ s++;
+ }
+ i++;
+ if (s * 2 - i > Sf * 2 - lenf) {
+ Sf = s;
+ lenf = i;
+ }
+ }
+
+ int lenb = 0;
+ if (scan < newsize) {
+ s = 0;
+ int Sb = 0;
+ for (i = 1; (scan >= lastscan + i) && (pos.value >= i); i++) {
+ if (oldBuf[pos.value - i] == newBuf[scan - i]) {
+ s++;
+ }
+ if (s * 2 - i > Sb * 2 - lenb) {
+ Sb = s;
+ lenb = i;
+ }
+ }
+ }
+
+ if (lastscan + lenf > scan - lenb) {
+ overlap = (lastscan + lenf) - (scan - lenb);
+ s = 0;
+ Ss = 0;
+ lens = 0;
+ for (i = 0; i < overlap; i++) {
+ if (newBuf[lastscan + lenf - overlap + i] == oldBuf[lastpos
+ + lenf - overlap + i]) {
+ s++;
+ }
+ if (newBuf[scan - lenb + i] == oldBuf[pos.value - lenb + i]) {
+ s--;
+ }
+ if (s > Ss) {
+ Ss = s;
+ lens = i + 1;
+ }
+ }
+
+ lenf += lens - overlap;
+ lenb -= lens;
+ }
+
+ // ? byte casting introduced here -- might affect things
+ for (i = 0; i < lenf; i++) {
+ db[dblen + i] = (byte)(newBuf[lastscan + i] - oldBuf[lastpos
+ + i]);
+ }
+
+ for (i = 0; i < (scan - lenb) - (lastscan + lenf); i++) {
+ eb[eblen + i] = newBuf[lastscan + lenf + i];
+ }
+
+ dblen += lenf;
+ eblen += (scan - lenb) - (lastscan + lenf);
+
+ /*
+ * Write control block entry (3 x int)
+ */
+ diffOut.writeInt(lenf);
+ diffOut.writeInt((scan - lenb) - (lastscan + lenf));
+ diffOut.writeInt((pos.value - lenb) - (lastpos + lenf));
+ ctrlBlockLen += 12;
+
+ lastscan = scan - lenb;
+ lastpos = pos.value - lenb;
+ lastoffset = pos.value - scan;
+ } // end if
+ } // end while loop
+
+ GZIPOutputStream gzOut;
+
+ /*
+ * Write diff block
+ */
+ gzOut = new GZIPOutputStream(diffOut);
+ gzOut.write(db, 0, dblen);
+ gzOut.finish();
+ int diffBlockLen = diffOut.size() - ctrlBlockLen;
+
+ /*
+ * Write extra block
+ */
+ gzOut = new GZIPOutputStream(diffOut);
+ gzOut.write(eb, 0, eblen);
+ gzOut.finish();
+
+ diffOut.close();
+
+ DataOutputStream headerStream = new DataOutputStream(diffFileOut);
+ headerStream.write("jbdiff40".getBytes("US-ASCII"));
+ headerStream.writeLong(ctrlBlockLen); // ctrlBlockLen (compressed)
+ headerStream.writeLong(diffBlockLen); // diffBlockLen (compressed)
+ headerStream.writeLong(newsize);
+ headerStream.flush();
+
+ Utils.copyBytesToStream(arrayOut, diffFileOut);
+
+ return newBuf;
+ }
+
+ private static class IntByRef {
+ public int value;
+ }
+}
+
diff --git a/updater/src/ie/wombat/jbdiff/JBPatch.java b/updater/src/ie/wombat/jbdiff/JBPatch.java
new file mode 100644
index 000000000000..37ef1dd013af
--- /dev/null
+++ b/updater/src/ie/wombat/jbdiff/JBPatch.java
@@ -0,0 +1,152 @@
+/*
+* Copyright (c) 2005, Joe Desbonnet, (jdesbonnet@gmail.com)
+* All rights reserved.
+*
+* Redistribution and use in source and binary forms, with or without
+* modification, are permitted provided that the following conditions are met:
+* * Redistributions of source code must retain the above copyright
+* notice, this list of conditions and the following disclaimer.
+* * Redistributions in binary form must reproduce the above copyright
+* notice, this list of conditions and the following disclaimer in the
+* documentation and/or other materials provided with the distribution.
+* * Neither the name of the <organization> nor the
+* names of its contributors may be used to endorse or promote products
+* derived from this software without specific prior written permission.
+*
+* THIS SOFTWARE IS PROVIDED BY <copyright holder> ``AS IS'' AND ANY
+* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+* DISCLAIMED. IN NO EVENT SHALL <copyright holder> BE LIABLE FOR ANY
+* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+package ie.wombat.jbdiff;
+
+import com.intellij.updater.Utils;
+
+import java.io.*;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * Java Binary patcher (based on bspatch by Colin Percival)
+ *
+ * @author Joe Desbonnet, jdesbonnet@gmail.com
+ */
+public class JBPatch {
+ public static void bspatch(InputStream oldFileIn, OutputStream newFileOut, InputStream diffFileIn)
+ throws IOException {
+
+ int oldpos, newpos;
+
+ byte[] diffData = Utils.readBytes(diffFileIn);
+
+ DataInputStream diffIn = new DataInputStream(new ByteArrayInputStream(diffData));
+
+ // headerMagic at header offset 0 (length 8 bytes)
+ long headerMagic = diffIn.readLong();
+
+ // ctrlBlockLen after gzip compression at heater offset 8 (length 8 bytes)
+ long ctrlBlockLen = diffIn.readLong();
+
+ // diffBlockLen after gzip compression at header offset 16 (length 8 bytes)
+ long diffBlockLen = diffIn.readLong();
+
+ // size of new file at header offset 24 (length 8 bytes)
+ int newsize = (int)diffIn.readLong();
+
+ /*
+ System.err.println ("newsize=" + newsize);
+ System.err.println ("ctrlBlockLen=" + ctrlBlockLen);
+ System.err.println ("diffBlockLen=" + diffBlockLen);
+ System.err.println ("newsize=" + newsize);
+ */
+
+ InputStream in;
+ in = new ByteArrayInputStream(diffData);
+ in.skip(ctrlBlockLen + 32);
+ GZIPInputStream diffBlockIn = new GZIPInputStream(in);
+
+ in = new ByteArrayInputStream(diffData);
+ in.skip(diffBlockLen + ctrlBlockLen + 32);
+ GZIPInputStream extraBlockIn = new GZIPInputStream(in);
+
+ /*
+ * Read in old file (file to be patched) to oldBuf
+ */
+ ByteArrayOutputStream oldFileByteOut = new ByteArrayOutputStream();
+ try {
+ Utils.copyStream(oldFileIn, oldFileByteOut);
+ }
+ finally {
+ oldFileByteOut.close();
+ }
+ byte[] oldBuf = oldFileByteOut.toByteArray();
+ int oldsize = oldBuf.length;
+
+ byte[] newBuf = new byte[newsize + 1];
+
+ oldpos = 0;
+ newpos = 0;
+ int[] ctrl = new int[3];
+ int nbytes;
+ while (newpos < newsize) {
+
+ for (int i = 0; i <= 2; i++) {
+ ctrl[i] = diffIn.readInt();
+ //System.err.println (" ctrl[" + i + "]=" + ctrl[i]);
+ }
+
+ if (newpos + ctrl[0] > newsize) {
+ System.err.println("Corrupt patch\n");
+ return;
+ }
+
+ /*
+ * Read ctrl[0] bytes from diffBlock stream
+ */
+
+ if (!Util.readFromStream(diffBlockIn, newBuf, newpos, ctrl[0])) {
+ System.err.println("error reading from extraIn");
+ return;
+ }
+
+ for (int i = 0; i < ctrl[0]; i++) {
+ if ((oldpos + i >= 0) && (oldpos + i < oldsize)) {
+ newBuf[newpos + i] += oldBuf[oldpos + i];
+ }
+ }
+
+ newpos += ctrl[0];
+ oldpos += ctrl[0];
+
+ if (newpos + ctrl[1] > newsize) {
+ System.err.println("Corrupt patch");
+ return;
+ }
+
+ if (!Util.readFromStream(extraBlockIn, newBuf, newpos, ctrl[1])) {
+ System.err.println("error reading from extraIn");
+ return;
+ }
+
+ newpos += ctrl[1];
+ oldpos += ctrl[2];
+ }
+
+ // TODO: Check if at end of ctrlIn
+ // TODO: Check if at the end of diffIn
+ // TODO: Check if at the end of extraIn
+
+ diffBlockIn.close();
+ extraBlockIn.close();
+ diffIn.close();
+
+ newFileOut.write(newBuf, 0, newBuf.length - 1);
+ }
+}
+
diff --git a/updater/src/ie/wombat/jbdiff/Util.java b/updater/src/ie/wombat/jbdiff/Util.java
new file mode 100644
index 000000000000..b5188976f8b0
--- /dev/null
+++ b/updater/src/ie/wombat/jbdiff/Util.java
@@ -0,0 +1,112 @@
+/*
+* Copyright (c) 2005, Joe Desbonnet, (jdesbonnet@gmail.com)
+* All rights reserved.
+*
+* Redistribution and use in source and binary forms, with or without
+* modification, are permitted provided that the following conditions are met:
+* * Redistributions of source code must retain the above copyright
+* notice, this list of conditions and the following disclaimer.
+* * Redistributions in binary form must reproduce the above copyright
+* notice, this list of conditions and the following disclaimer in the
+* documentation and/or other materials provided with the distribution.
+* * Neither the name of the <organization> nor the
+* names of its contributors may be used to endorse or promote products
+* derived from this software without specific prior written permission.
+*
+* THIS SOFTWARE IS PROVIDED BY <copyright holder> ``AS IS'' AND ANY
+* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+* DISCLAIMED. IN NO EVENT SHALL <copyright holder> BE LIABLE FOR ANY
+* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+package ie.wombat.jbdiff;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * @author Joe Desbonnet, jdesbonnet@gmail.com
+ *
+ */
+public class Util {
+
+ /**
+ * Equiv of C library memcmp().
+ *
+ * @param s1
+ * @param s1offset
+ * @param s2
+ * @param n
+ * @return
+ */
+ /*
+ public final static int memcmp(byte[] s1, int s1offset, byte[] s2, int s2offset, int n) {
+
+ if ((s1offset + n) > s1.length) {
+ n = s1.length - s1offset;
+ }
+ if ((s2offset + n) > s2.length) {
+ n = s2.length - s2offset;
+ }
+ for (int i = 0; i < n; i++) {
+ if (s1[i + s1offset] != s2[i + s2offset]) {
+ return s1[i + s1offset] < s2[i + s2offset] ? -1 : 1;
+ }
+ }
+
+ return 0;
+ }
+ */
+
+ /**
+ * Equiv of C library memcmp().
+ *
+ * @param s1
+ * @param s1offset
+ * @param s2
+ * @param n
+ * @return
+ */
+ public final static int memcmp(byte[] s1, int s1offset, byte[] s2, int s2offset) {
+
+ int n = s1.length - s1offset;
+
+ if (n > (s2.length-s2offset)) {
+ n = s2.length-s2offset;
+ }
+ for (int i = 0; i < n; i++) {
+ if (s1[i + s1offset] != s2[i + s2offset]) {
+ return s1[i + s1offset] < s2[i + s2offset] ? -1 : 1;
+ }
+ }
+
+ return 0;
+ }
+
+ public static final boolean readFromStream (InputStream in, byte[] buf, int offset, int len)
+ throws IOException
+ {
+
+ int totalBytesRead = 0;
+ int nbytes;
+
+ while ( totalBytesRead < len) {
+ nbytes = in.read(buf,offset+totalBytesRead,len-totalBytesRead);
+ if (nbytes < 0) {
+ System.err.println ("readFromStream(): returning prematurely. Read "
+ + totalBytesRead + " bytes");
+ return false;
+ }
+ totalBytesRead+=nbytes;
+ }
+
+ return true;
+ }
+
+}
diff --git a/updater/updater.iml b/updater/updater.iml
new file mode 100644
index 000000000000..c053808175a3
--- /dev/null
+++ b/updater/updater.iml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module relativePaths="true" type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ </component>
+</module>
+