synchronized Updater with the new version from google. appcode/140.1703 clion/140.1702
authorVladimir.Orlov <Vladimir.Orlov@jetbrains.com>
Wed, 14 Jan 2015 08:29:07 +0000 (11:29 +0300)
committerVladimir.Orlov <Vladimir.Orlov@jetbrains.com>
Wed, 14 Jan 2015 08:29:07 +0000 (11:29 +0300)
Changes:
- Strict patches
- Directories as files
- Support for moving files
- Making use of critical files
- Zip file normalization for binary patches
- Mac patches from the application folder

29 files changed:
updater/src/com/intellij/updater/BaseDeleteAction.java [deleted file]
updater/src/com/intellij/updater/BaseUpdateAction.java
updater/src/com/intellij/updater/ConsoleUpdaterUI.java
updater/src/com/intellij/updater/CreateAction.java
updater/src/com/intellij/updater/DeleteAction.java
updater/src/com/intellij/updater/DeleteZipAction.java [deleted file]
updater/src/com/intellij/updater/DiffCalculator.java
updater/src/com/intellij/updater/Digester.java
updater/src/com/intellij/updater/Patch.java
updater/src/com/intellij/updater/PatchAction.java
updater/src/com/intellij/updater/PatchFileCreator.java
updater/src/com/intellij/updater/PatchSpec.java [new file with mode: 0644]
updater/src/com/intellij/updater/RetryException.java [new file with mode: 0644]
updater/src/com/intellij/updater/Runner.java
updater/src/com/intellij/updater/SwingUpdaterUI.java
updater/src/com/intellij/updater/UpdateAction.java
updater/src/com/intellij/updater/UpdateZipAction.java
updater/src/com/intellij/updater/UpdaterUI.java
updater/src/com/intellij/updater/Utils.java
updater/src/com/intellij/updater/ValidateAction.java [new file with mode: 0644]
updater/src/com/intellij/updater/ValidationResult.java
updater/testData/Readme.txt
updater/testSrc/com/intellij/updater/DigesterTest.java
updater/testSrc/com/intellij/updater/PatchFileCreatorBinaryTest.java [new file with mode: 0644]
updater/testSrc/com/intellij/updater/PatchFileCreatorNotBinaryTest.java [new file with mode: 0644]
updater/testSrc/com/intellij/updater/PatchFileCreatorTest.java
updater/testSrc/com/intellij/updater/PatchTest.java
updater/testSrc/com/intellij/updater/RunnerTest.java
updater/testSrc/com/intellij/updater/UpdaterTestCase.java

diff --git a/updater/src/com/intellij/updater/BaseDeleteAction.java b/updater/src/com/intellij/updater/BaseDeleteAction.java
deleted file mode 100644 (file)
index 510ec97..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-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 abstract class BaseDeleteAction extends PatchAction {
-  public BaseDeleteAction(String path, long checksum) {
-    super(path, checksum);
-  }
-
-  public BaseDeleteAction(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 {
-    if (!toFile.exists() || toFile.isDirectory() || isModified(toFile)) {
-      Utils.delete(toFile); // make sure there is no directory remained on this path (may remain from previous 'create' actions
-      Utils.copy(backupFile, toFile);
-    }
-  }
-}
index 1d5e5a1cc78f61653f7c1923d7b723e2d8e4b25f..49bc87f10c208a21f032f285790d45f4b69270dd 100644 (file)
@@ -7,47 +7,79 @@ import java.io.*;
 import java.util.zip.ZipOutputStream;
 
 public abstract class BaseUpdateAction extends PatchAction {
-  public BaseUpdateAction(String path, long checksum) {
-    super(path, checksum);
+  private final String mySource;
+  protected final boolean myIsMove;
+
+  public BaseUpdateAction(Patch patch, String path, String source, long checksum, boolean move) {
+    super(patch, path, checksum);
+    myIsMove = move;
+    mySource = source;
   }
 
-  public BaseUpdateAction(DataInputStream in) throws IOException {
-    super(in);
+  public BaseUpdateAction(Patch patch, DataInputStream in) throws IOException {
+    super(patch, in);
+    mySource = in.readUTF();
+    myIsMove = in.readBoolean();
   }
 
   @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);
+  public void write(DataOutputStream out) throws IOException {
+    super.write(out);
+    out.writeUTF(mySource);
+    out.writeBoolean(myIsMove);
+  }
+
+  protected File getSource(File toDir) {
+    return new File(toDir, mySource);
+  }
+
+  public String getSourcePath() {
+    return mySource;
   }
 
   @Override
-  protected boolean shouldApplyOn(File toFile) {
+  protected boolean doShouldApply(File toDir) {
     // if the file is optional in may not exist
-    return toFile.exists();
+    return getSource(toDir).exists();
+  }
+
+  @Override
+  public void buildPatchFile(File olderDir, File newerDir, ZipOutputStream patchOutput) throws IOException {
+    doBuildPatchFile(getSource(olderDir), getFile(newerDir), patchOutput);
+  }
+
+  @Override
+  public ValidationResult validate(File toDir) throws IOException {
+    File fromFile = getSource(toDir);
+    ValidationResult result = doValidateAccess(fromFile, ValidationResult.Action.UPDATE);
+    if (result != null) return result;
+    if (!mySource.isEmpty()) {
+      result = doValidateAccess(getFile(toDir), ValidationResult.Action.UPDATE);
+      if (result != null) return result;
+    }
+    return doValidateNotChanged(fromFile, ValidationResult.Kind.ERROR, ValidationResult.Action.UPDATE);
   }
 
   @Override
   protected void doBackup(File toFile, File backupFile) throws IOException {
-    Utils.copy(toFile, backupFile);
+    Utils.mirror(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);
+    if (dest.exists() && !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);
+      Utils.mirror(backupFile, toFile);
     }
   }
 
-  protected void writeDiff(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException {
-    BufferedInputStream olderFileIn = new BufferedInputStream(new FileInputStream(olderFile));
+  protected void writeDiff(File olderFile, File newerFile, OutputStream patchOutput) throws IOException {
+    BufferedInputStream olderFileIn = new BufferedInputStream(Utils.newFileInputStream(olderFile, myPatch.isNormalized()));
     BufferedInputStream newerFileIn = new BufferedInputStream(new FileInputStream(newerFile));
     try {
       writeDiff(olderFileIn, newerFileIn, patchOutput);
@@ -58,14 +90,14 @@ public abstract class BaseUpdateAction extends PatchAction {
     }
   }
 
-  protected void writeDiff(InputStream olderFileIn, InputStream newerFileIn, ZipOutputStream patchOutput)
+  protected void writeDiff(InputStream olderFileIn, InputStream newerFileIn, OutputStream patchOutput)
     throws IOException {
     Runner.logger.info("writing diff");
     ByteArrayOutputStream diffOutput = new ByteArrayOutputStream();
     byte[] newerFileBuffer = JBDiff.bsdiff(olderFileIn, newerFileIn, diffOutput);
     diffOutput.close();
 
-    if (diffOutput.size() < newerFileBuffer.length) {
+    if (!isCritical() && diffOutput.size() < newerFileBuffer.length) {
       patchOutput.write(1);
       Utils.copyBytesToStream(diffOutput, patchOutput);
     }
@@ -83,4 +115,17 @@ public abstract class BaseUpdateAction extends PatchAction {
       Utils.copyStream(patchInput, toFileOut);
     }
   }
+
+  @Override
+  public String toString() {
+    String moveInfo = "";
+    if (!mySource.equals(myPath)) {
+      moveInfo = "[" + (myIsMove ? "= " : "~ ") + mySource + "]";
+    }
+    return super.toString() + moveInfo;
+  }
+
+  public boolean isMove() {
+    return myIsMove;
+  }
 }
index c1114dd173f6f618da80b86f87208096a6a346df..e96f90d15d5d99fca4d11e5fe4863d007060bf9c 100644 (file)
@@ -31,6 +31,17 @@ public class ConsoleUpdaterUI implements UpdaterUI {
   public void checkCancelled() throws OperationCancelledException {
   }
 
+  @Override
+  public void setDescription(String oldBuildDesc, String newBuildDesc) {
+    System.out.println("From " + oldBuildDesc + " to " + newBuildDesc);
+  }
+
+  @Override
+  public boolean showWarning(String message) {
+    System.out.println("Warning: " + message);
+    return false;
+  }
+
   public Map<String, ValidationResult.Option> askUser(List<ValidationResult> validationResults) {
     return Collections.emptyMap();
   }
index dd44c35f5df18a97e43e9b13f07c98b7cc4c02d3..67794c0b5409a625c27a6c079e6661c652df706a 100644 (file)
@@ -9,35 +9,40 @@ import java.util.zip.ZipFile;
 import java.util.zip.ZipOutputStream;
 
 public class CreateAction extends PatchAction {
-  public CreateAction(String path) {
-    super(path, -1);
+  public CreateAction(Patch patch, String path) {
+    super(patch, path, Digester.INVALID);
   }
 
-  public CreateAction(DataInputStream in) throws IOException {
-    super(in);
+  public CreateAction(Patch patch, DataInputStream in) throws IOException {
+    super(patch, in);
   }
 
+  @Override
   protected void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException {
     Runner.logger.info("building PatchFile");
     patchOutput.putNextEntry(new ZipEntry(myPath));
-
-    writeExecutableFlag(patchOutput, newerFile);
-    Utils.copyFileToStream(newerFile, patchOutput);
+    if (!newerFile.isDirectory()) {
+      writeExecutableFlag(patchOutput, newerFile);
+      Utils.copyFileToStream(newerFile, patchOutput);
+    }
 
     patchOutput.closeEntry();
   }
 
   @Override
-  protected ValidationResult doValidate(File toFile) {
+  public ValidationResult validate(File toDir) {
+    File toFile = getFile(toDir);
     ValidationResult result = doValidateAccess(toFile, ValidationResult.Action.CREATE);
     if (result != null) return result;
 
     if (toFile.exists()) {
-      return new ValidationResult(ValidationResult.Kind.CONFLICT,
-                                  myPath,
+      ValidationResult.Option[] options = myPatch.isStrict()
+                                          ? new ValidationResult.Option[]{ValidationResult.Option.REPLACE}
+                                          : new ValidationResult.Option[]{ValidationResult.Option.REPLACE, ValidationResult.Option.KEEP};
+      return new ValidationResult(ValidationResult.Kind.CONFLICT, myPath,
                                   ValidationResult.Action.CREATE,
                                   ValidationResult.ALREADY_EXISTS_MESSAGE,
-                                  ValidationResult.Option.REPLACE, ValidationResult.Option.KEEP);
+                                  options);
     }
     return null;
   }
@@ -48,17 +53,24 @@ public class CreateAction extends PatchAction {
   }
 
   @Override
-  protected void doApply(ZipFile patchFile, File toFile) throws IOException {
+  protected void doApply(ZipFile patchFile, File backupDir, 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();
+    ZipEntry entry = Utils.getZipEntry(patchFile, myPath);
+    if (entry.isDirectory()) {
+      if (!toFile.mkdir()) {
+        throw new IOException("Unable to create directory " + myPath);
+      }
+    } else {
+      InputStream in = Utils.findEntryInputStreamForEntry(patchFile, entry);
+      try {
+        boolean executable = readExecutableFlag(in);
+        Utils.copyStreamToFile(in, toFile);
+        Utils.setExecutable(toFile, executable);
+      }
+      finally {
+        in.close();
+      }
     }
   }
 
index e197cbded8e28bd05583308cb10eebcf8e4d4dee..6809cf134ae86bdf5f806d01ee1db8ce7df53cc3 100644 (file)
@@ -3,18 +3,60 @@ 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 BaseDeleteAction {
-  public DeleteAction(String path, long checksum) {
-    super(path, checksum);
+public class DeleteAction extends PatchAction {
+  public DeleteAction(Patch patch, String path, long checksum) {
+    super(patch, path, checksum);
   }
 
-  public DeleteAction(DataInputStream in) throws IOException {
-    super(in);
+  public DeleteAction(Patch patch, DataInputStream in) throws IOException {
+    super(patch, in);
   }
 
   @Override
-  protected boolean isModified(File toFile) throws IOException {
-    return myChecksum != Digester.digestRegularFile(toFile);
+  public void doBuildPatchFile(File olderDir, File newerFile, ZipOutputStream patchOutput) throws IOException {
+    // do nothing
+  }
+
+  @Override
+  public ValidationResult validate(File toDir) throws IOException {
+    File toFile = getFile(toDir);
+    ValidationResult result = doValidateAccess(toFile, ValidationResult.Action.DELETE);
+    if (result != null) return result;
+
+    if (myPatch.validateDeletion(myPath) && toFile.exists() && isModified(toFile)) {
+      ValidationResult.Option[] options = myPatch.isStrict()
+                                          ? new ValidationResult.Option[]{ValidationResult.Option.DELETE}
+                                          : new ValidationResult.Option[]{ValidationResult.Option.DELETE, ValidationResult.Option.KEEP};
+      ValidationResult.Action action = myChecksum == Digester.INVALID ? ValidationResult.Action.VALIDATE : ValidationResult.Action.DELETE;
+      String message = myChecksum == Digester.INVALID ? "Unexpected file" : "Modified";
+      return new ValidationResult(ValidationResult.Kind.CONFLICT, myPath, action, message, options);
+    }
+    return null;
+  }
+
+  @Override
+  protected boolean doShouldApply(File toDir) {
+    return getFile(toDir).exists();
+  }
+
+  @Override
+  protected void doApply(ZipFile patchFile, File backupDir, File toFile) throws IOException {
+    Utils.delete(toFile);
+  }
+
+  @Override
+  protected void doBackup(File toFile, File backupFile) throws IOException {
+    Utils.copy(toFile, backupFile);
+  }
+
+  @Override
+  protected void doRevert(File toFile, File backupFile) throws IOException {
+    if (!toFile.exists() || toFile.isDirectory() || isModified(toFile)) {
+      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/DeleteZipAction.java b/updater/src/com/intellij/updater/DeleteZipAction.java
deleted file mode 100644 (file)
index 0cd46d9..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.intellij.updater;
-
-import java.io.DataInputStream;
-import java.io.File;
-import java.io.IOException;
-
-public class DeleteZipAction extends BaseDeleteAction {
-  public DeleteZipAction(String path, long checksum) {
-    super(path, checksum);
-  }
-
-  public DeleteZipAction(DataInputStream in) throws IOException {
-    super(in);
-  }
-
-  @Override
-  protected boolean isModified(File toFile) throws IOException {
-    return myChecksum != Digester.digestFile(toFile);
-  }
-}
index 4db0033740e90348ff24ea9d99e17a2989ef770e..f5e04b5630eee29f371c2ee4ceb77b5cbb235499 100644 (file)
 package com.intellij.updater;
 
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
+import java.io.File;
+import java.util.*;
 
 public class DiffCalculator {
-  public static Result calculate(Map<String, Long> oldChecksums, Map<String, Long> newChecksums) {
+  public static Result calculate(Map<String, Long> oldChecksums, Map<String, Long> newChecksums, List<String> critical, boolean move) {
     Result result = new Result();
+    result.commonFiles = collect(oldChecksums, newChecksums, critical, true);
     result.filesToDelete = withAllRemoved(oldChecksums, newChecksums);
-    result.filesToCreate = withAllRemoved(newChecksums, oldChecksums).keySet();
-    result.filesToUpdate = collect(oldChecksums, newChecksums, false);
+
+    Map<String, Long> toUpdate = collect(oldChecksums, newChecksums, critical, false);
+    Map<String, Long> toCreate = withAllRemoved(newChecksums, oldChecksums);
+
+    // Some creates become updates if found in different directories.
+    result.filesToCreate = new LinkedHashMap<String, Long>();
+    result.filesToUpdate = new LinkedHashMap<String, Update>();
+
+    for (Map.Entry<String, Long> update : toUpdate.entrySet()) {
+      result.filesToUpdate.put(update.getKey(), new Update(update.getKey(), update.getValue(), false));
+    }
+
+    if (move) {
+      Map<Long, String> byContent = inverse(result.filesToDelete);
+      Map<String, List<String>> byName = groupFilesByName(result.filesToDelete);
+
+      // Find first by content
+      for (Map.Entry<String, Long> create : toCreate.entrySet()) {
+        boolean isDir = create.getKey().endsWith("/");
+        String source = byContent.get(create.getValue());
+        boolean found = false;
+        if (source != null && !isDir) {
+          // Found a file with the same content use it, unless it's critical
+          if (!critical.contains(source)) {
+            result.filesToUpdate.put(create.getKey(), new Update(source, result.filesToDelete.get(source), true));
+            found = true;
+          }
+        }
+        else {
+          File fileToCreate = new File(create.getKey());
+          List<String> sameName = byName.get(fileToCreate.getName());
+          if (sameName != null && !isDir) {
+            String best = findBestCandidateForMove(sameName, create.getKey());
+            // Found a file with the same name, if it's not critical use it, worst case as big as a create.
+            if (!critical.contains(best)) {
+              result.filesToUpdate.put(create.getKey(), new Update(best, result.filesToDelete.get(best), false));
+              found = true;
+            }
+          }
+        }
+        if (!found) {
+          // Fine, just create it.
+          result.filesToCreate.put(create.getKey(), create.getValue());
+        }
+      }
+    } else {
+      result.filesToCreate = toCreate;
+    }
+
     return result;
   }
 
+  private static String findBestCandidateForMove(List<String> paths, String path) {
+    int common = 0;
+    String best = "";
+    String[] dirs = path.split("/");
+    for (String other : paths) {
+      String[] others = other.split("/");
+      for (int i = 0; i < dirs.length && i < others.length; i++) {
+        if (dirs[dirs.length - i - 1].equals(others[others.length - i - 1])) {
+          if (i + 1 > common) {
+            best = other;
+            common = i + 1;
+          }
+        } else {
+          break;
+        }
+      }
+    }
+    return best;
+  }
+
+  private static Map<String, List<String>> groupFilesByName(Map<String, Long> toDelete) {
+    Map<String, List<String>> result = new HashMap<String, List<String>>();
+    for (String path : toDelete.keySet()) {
+      if (!path.endsWith("/")) {
+        String name = new File(path).getName();
+        List<String> paths = result.get(name);
+        if (paths == null) {
+          paths = new LinkedList<String>();
+          result.put(name, paths);
+        }
+        paths.add(path);
+      }
+    }
+    return result;
+  }
+
+  public static Map<Long,String> inverse(Map<String, Long> map) {
+    Map<Long, String> inv = new LinkedHashMap<Long, String>();
+    for (Map.Entry<String, Long> entry : map.entrySet()) {
+      inv.put(entry.getValue(), entry.getKey());
+    }
+    return inv;
+  }
+
   private static Map<String, Long> withAllRemoved(Map<String, Long> from, Map<String, Long> toRemove) {
-    Map<String, Long> result = new HashMap<String, Long>(from);
+    Map<String, Long> result = new LinkedHashMap<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>();
+  private static Map<String, Long> collect(Map<String, Long> older, Map<String, Long> newer, List<String> critical, boolean equal) {
+    Map<String, Long> result = new LinkedHashMap<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);
+      if (oldChecksum != null && newChecksum != null) {
+        if ((oldChecksum.equals(newChecksum) && !critical.contains(file)) == equal) {
+          result.put(file, oldChecksum);
+        }
       }
     }
     return result;
   }
 
+  public static class Update {
+    public final String source;
+    public final long checksum;
+    public final boolean move;
+
+    public Update(String source, long checksum, boolean move) {
+      this.checksum = checksum;
+      this.source = source;
+      this.move = move;
+    }
+  }
+
   public static class Result {
     public Map<String, Long> filesToDelete;
-    public Set<String> filesToCreate;
-    public Map<String, Long> filesToUpdate;
+    public Map<String, Long> filesToCreate;
+    public Map<String, Update> filesToUpdate;
+    public Map<String, Long> commonFiles;
   }
 }
index b84c33e3f6d5285fb2a4d660b210563d6e23a939..ba45c49f349e87df490536a17579117fc4db8d76 100644 (file)
@@ -4,46 +4,19 @@ import java.io.*;
 import java.util.*;
 import java.util.zip.CRC32;
 import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
 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>();
+  // CRC32 will only use the lower 32bits of long, never returning negative values.
+  public static long INVALID = -1;
+  public static long DIRECTORY = -2;
 
-    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)));
+  public static long digestRegularFile(File file, boolean normalize) throws IOException {
+    if (file.isDirectory()) {
+      return DIRECTORY;
     }
-    return result;
-  }
-
-  public static long digestFile(File file) throws IOException {
-    if (!Runner.ZIP_AS_BINARY && Utils.isZipFile(file.getName())) {
-      ZipFile zipFile;
-      try {
-        zipFile = new ZipFile(file);
-      }
-      catch (IOException e) {
-        Runner.printStackTrace(e);
-        return digestRegularFile(file);
-      }
-
-      try {
-        return doDigestZipFile(zipFile);
-      }
-      finally {
-        zipFile.close();
-      }
-    }
-    return digestRegularFile(file);
-  }
-
-  public static long digestRegularFile(File file) throws IOException {
-    InputStream in = new BufferedInputStream(new FileInputStream(file));
+    InputStream in = new BufferedInputStream(Utils.newFileInputStream(file, normalize));
     try {
       return digestStream(in);
     }
@@ -52,32 +25,43 @@ public class Digester {
     }
   }
 
-  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);
+  public static long digestZipFile(File file) throws IOException {
+    ZipFile zipFile;
+    try {
+      zipFile = new ZipFile(file);
+    } catch (ZipException e) {
+      // This was not a zip file...
+      return digestRegularFile(file, false);
     }
+    try {
+      List<ZipEntry> sorted = new ArrayList<ZipEntry>();
 
-    Collections.sort(sorted, new Comparator<ZipEntry>() {
-      public int compare(ZipEntry o1, ZipEntry o2) {
-        return o1.getName().compareTo(o2.getName());
+      Enumeration<? extends ZipEntry> temp = zipFile.entries();
+      while (temp.hasMoreElements()) {
+        ZipEntry each = temp.nextElement();
+        if (!each.isDirectory()) sorted.add(each);
       }
-    });
 
-    CRC32 crc = new CRC32();
-    for (ZipEntry each : sorted) {
-      InputStream in = zipFile.getInputStream(each);
-      try {
-        doDigestStream(in, crc);
-      }
-      finally {
-        in.close();
+      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();
+    } finally {
+      zipFile.close();
     }
-    return crc.getValue();
   }
 
   public static long digestStream(InputStream in) throws IOException {
index d9a33a6df24dbda397c0304b289b63a58f977df5..be237a443afd6306fcae0564005b3a5cbd0cbc7c 100644 (file)
@@ -6,64 +6,75 @@ import java.util.zip.ZipFile;
 
 public class Patch {
   private List<PatchAction> myActions = new ArrayList<PatchAction>();
+  private boolean myIsBinary;
+  private boolean myIsStrict;
+  private boolean myIsNormalized;
+  private String myOldBuild;
+  private String myNewBuild;
+  private String myRoot;
+  private Map<String, String> myWarnings;
+  private List<String> myDeleteFiles;
 
   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;
-  private static final int DELETE_ZIP_ACTION_KEY = 5;
-
-  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);
+  private static final int VALIDATE_ACTION_KEY = 5;
+
+  public Patch(PatchSpec spec, UpdaterUI ui) throws IOException, OperationCancelledException {
+    myIsBinary = spec.isBinary();
+    myIsStrict = spec.isStrict();
+    myIsNormalized = spec.isNormalized();
+    myOldBuild = spec.getOldVersionDescription();
+    myNewBuild = spec.getNewVersionDescription();
+    myWarnings = spec.getWarnings();
+    myDeleteFiles = spec.getDeleteFiles();
+    myRoot = spec.getRoot();
+
+    calculateActions(spec, 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;
-
+  private void calculateActions(PatchSpec spec, UpdaterUI ui) throws IOException, OperationCancelledException {
     Runner.logger.info("Calculating difference...");
     ui.startProcess("Calculating difference...");
     ui.checkCancelled();
 
-    diff = DiffCalculator.calculate(Digester.digestFiles(olderDir, ignoredFiles, ui),
-                                    Digester.digestFiles(newerDir, ignoredFiles, ui));
+    File olderDir = new File(spec.getOldFolder());
+    File newerDir = new File(spec.getNewFolder());
+    DiffCalculator.Result diff;
+    diff = DiffCalculator.calculate(digestFiles(olderDir, spec.getIgnoredFiles(), isNormalized(), ui),
+                                    digestFiles(newerDir, spec.getIgnoredFiles(), false, ui),
+                                    spec.getCriticalFiles(), true);
 
     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()) {
-      if (!Runner.ZIP_AS_BINARY && Utils.isZipFile(each.getKey())) {
-        tempActions.add(new DeleteZipAction(each.getKey(), each.getValue()));
-      } else
-      {
-        tempActions.add(new DeleteAction(each.getKey(), each.getValue()));
-      }
+      // Add them in reverse order so directory structures start deleting the files before the directory itself.
+      tempActions.add(0, new DeleteAction(this, each.getKey(), each.getValue()));
     }
 
-    for (String each : diff.filesToCreate) {
-      tempActions.add(new CreateAction(each));
+    for (String each : diff.filesToCreate.keySet()) {
+      tempActions.add(new CreateAction(this, each));
     }
 
-    for (Map.Entry<String, Long> each : diff.filesToUpdate.entrySet()) {
-      if (!Runner.ZIP_AS_BINARY && Utils.isZipFile(each.getKey())) {
-        tempActions.add(new UpdateZipAction(each.getKey(), each.getValue()));
+    for (Map.Entry<String, DiffCalculator.Update> each : diff.filesToUpdate.entrySet()) {
+      DiffCalculator.Update update = each.getValue();
+      if (!spec.isBinary() && Utils.isZipFile(each.getKey())) {
+        tempActions.add(new UpdateZipAction(this, each.getKey(), update.source, update.checksum, update.move));
       }
       else {
-        tempActions.add(new UpdateAction(each.getKey(), each.getValue()));
+        tempActions.add(new UpdateAction(this, each.getKey(), update.source, update.checksum, update.move));
+      }
+    }
+
+    if (spec.isStrict()) {
+      for (Map.Entry<String, Long> each : diff.commonFiles.entrySet()) {
+        tempActions.add(new ValidateAction(this, each.getKey(), each.getValue()));
       }
     }
 
@@ -77,8 +88,8 @@ public class Patch {
 
       if (!each.calculate(olderDir, newerDir)) continue;
       myActions.add(each);
-      each.setCritical(criticalFiles.contains(each.getPath()));
-      each.setOptional(optionalFiles.contains(each.getPath()));
+      each.setCritical(spec.getCriticalFiles().contains(each.getPath()));
+      each.setOptional(spec.getOptionalFiles().contains(each.getPath()));
     }
   }
 
@@ -89,104 +100,189 @@ public class Patch {
   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 == DeleteZipAction.class) {
-          key = DELETE_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);
-      }
+      dataOut.writeUTF(myOldBuild);
+      dataOut.writeUTF(myNewBuild);
+      dataOut.writeUTF(myRoot);
+      dataOut.writeBoolean(myIsBinary);
+      dataOut.writeBoolean(myIsStrict);
+      dataOut.writeBoolean(myIsNormalized);
+      writeMap(dataOut, myWarnings);
+      writeList(dataOut, myDeleteFiles);
+      writeActions(dataOut, myActions);
     }
     finally {
       dataOut.flush();
     }
   }
 
+  private static void writeList(DataOutputStream dataOut, List<String> list) throws  IOException {
+    dataOut.writeInt(list.size());
+    for (String string : list) {
+      dataOut.writeUTF(string);
+    }
+  }
+
+  private static void writeMap(DataOutputStream dataOut, Map<String, String> map) throws IOException {
+    dataOut.writeInt(map.size());
+    for (Map.Entry<String, String> entry : map.entrySet()) {
+      dataOut.writeUTF(entry.getKey());
+      dataOut.writeUTF(entry.getValue());
+    }
+  }
+
+  private void writeActions(DataOutputStream dataOut, List<PatchAction> actions) throws IOException {
+    dataOut.writeInt(actions.size());
+
+    for (PatchAction each : actions) {
+      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 if (clazz == ValidateAction.class) {
+        key = VALIDATE_ACTION_KEY;
+      }
+      else {
+        throw new RuntimeException("Unknown action " + each);
+      }
+      dataOut.writeInt(key);
+      each.write(dataOut);
+    }
+  }
+
   private void read(InputStream patchIn) throws IOException {
-    List<PatchAction> newActions = new ArrayList<PatchAction>();
 
     @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") DataInputStream in = new DataInputStream(patchIn);
+
+    myOldBuild = in.readUTF();
+    myNewBuild = in.readUTF();
+    myRoot = in.readUTF();
+    myIsBinary = in.readBoolean();
+    myIsStrict = in.readBoolean();
+    myIsNormalized = in.readBoolean();
+    myWarnings = readMap(in);
+    myDeleteFiles = readList(in);
+    myActions = readActions(in);
+  }
+
+  private static List<String> readList(DataInputStream in) throws IOException {
     int size = in.readInt();
+    List<String> list = new ArrayList<String>(size);
+    for (int i = 0; i < size; i++) {
+      list.add(in.readUTF());
+    }
+    return list;
+  }
 
+  private static Map<String, String> readMap(DataInputStream in) throws IOException {
+    int size = in.readInt();
+    Map<String, String> map = new HashMap<String, String>();
+    for (int i = 0; i < size; i++) {
+      String key = in.readUTF();
+      map.put(key, in.readUTF());
+    }
+    return map;
+  }
+
+  private List<PatchAction> readActions(DataInputStream in) throws IOException {
+    List<PatchAction> actions = new ArrayList<PatchAction>();
+    int size = in.readInt();
     while (size-- > 0) {
       int key = in.readInt();
       PatchAction a;
       switch (key) {
         case CREATE_ACTION_KEY:
-          a = new CreateAction(in);
+          a = new CreateAction(this, in);
           break;
         case UPDATE_ACTION_KEY:
-          a = new UpdateAction(in);
+          a = new UpdateAction(this, in);
           break;
         case UPDATE_ZIP_ACTION_KEY:
-          a = new UpdateZipAction(in);
+          a = new UpdateZipAction(this, in);
           break;
         case DELETE_ACTION_KEY:
-          a = new DeleteAction(in);
+          a = new DeleteAction(this, in);
           break;
-        case DELETE_ZIP_ACTION_KEY:
-          a = new DeleteZipAction(in);
+        case VALIDATE_ACTION_KEY:
+          a = new ValidateAction(this, in);
           break;
         default:
           throw new RuntimeException("Unknown action type " + key);
       }
-      newActions.add(a);
+      actions.add(a);
     }
+    return actions;
+  }
 
-    myActions = newActions;
+  private File toBaseDir(File toDir) throws IOException {
+    // This removes myRoot from the end of toDir. myRoot is expressed with '/' so converting to URI to normalize separators.
+    String path = toDir.toURI().getPath();
+    if (!path.endsWith(myRoot)) {
+      throw new IOException("The patch must be applied to the root folder " + myRoot);
+    }
+    return new File(path.substring(0, path.length() - myRoot.length()));
   }
 
-  public List<ValidationResult> validate(final File toDir, UpdaterUI ui) throws IOException, OperationCancelledException {
-    final LinkedHashSet<String> files = Utils.collectRelativePaths(toDir);
+  public List<ValidationResult> validate(final File rootDir, UpdaterUI ui) throws IOException, OperationCancelledException {
+    LinkedHashSet<String> files = null;
+    final File toDir = toBaseDir(rootDir);
+    boolean checkWarnings = true;
+    while (checkWarnings) {
+      files = Utils.collectRelativePaths(toDir, myIsStrict);
+      checkWarnings = false;
+      for (String file : files) {
+        String warning = myWarnings.get(file);
+        if (warning != null) {
+          if (!ui.showWarning(warning)) {
+            throw new OperationCancelledException();
+          }
+          checkWarnings = true;
+          break;
+        }
+      }
+    }
+
     final List<ValidationResult> result = new ArrayList<ValidationResult>();
 
+    if (myIsStrict) {
+      // In strict mode add delete actions for unknown files.
+      for (PatchAction action : myActions) {
+        files.remove(action.getPath());
+      }
+      for (String file : files) {
+        myActions.add(0, new DeleteAction(this, file, Digester.INVALID));
+      }
+    }
     Runner.logger.info("Validating installation...");
     forEach(myActions, "Validating installation...", ui, true,
             new ActionsProcessor() {
+              @Override
               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 rootDir,
                                  final File backupDir,
                                  final Map<String, ValidationResult.Option> options,
                                  UpdaterUI ui) throws IOException, OperationCancelledException {
 
+    final File toDir = toBaseDir(rootDir);
     List<PatchAction> actionsToProcess = new ArrayList<PatchAction>();
     for (PatchAction each : myActions) {
       if (each.shouldApply(toDir, options)) actionsToProcess.add(each);
@@ -194,6 +290,7 @@ public class Patch {
 
     forEach(actionsToProcess, "Backing up files...", ui, true,
             new ActionsProcessor() {
+              @Override
               public void forEach(PatchAction each) throws IOException {
                 each.backup(toDir, backupDir);
               }
@@ -205,9 +302,10 @@ public class Patch {
     try {
       forEach(actionsToProcess, "Applying patch...", ui, true,
               new ActionsProcessor() {
+                @Override
                 public void forEach(PatchAction each) throws IOException {
                   appliedActions.add(each);
-                  each.apply(patchFile, toDir);
+                  each.apply(patchFile, backupDir, toDir);
                 }
               });
     }
@@ -223,7 +321,7 @@ public class Patch {
     }
 
     if (shouldRevert) {
-      revert(appliedActions, backupDir, toDir, ui);
+      revert(appliedActions, backupDir, rootDir, ui);
       appliedActions.clear();
 
       if (cancelled) throw new OperationCancelledException();
@@ -235,11 +333,13 @@ public class Patch {
     return new ApplicationResult(appliedActions);
   }
 
-  public void revert(List<PatchAction> actions, final File backupDir, final File toDir, UpdaterUI ui)
+  public void revert(List<PatchAction> actions, final File backupDir, final File rootDir, UpdaterUI ui)
     throws OperationCancelledException, IOException {
     Collections.reverse(actions);
+    final File toDir = toBaseDir(rootDir);
     forEach(actions, "Reverting...", ui, false,
             new ActionsProcessor() {
+              @Override
               public void forEach(PatchAction each) throws IOException {
                 each.revert(toDir, backupDir);
               }
@@ -263,6 +363,54 @@ public class Patch {
     }
   }
 
+  public long digestFile(File toFile, boolean normalize) throws IOException {
+    if (!myIsBinary && Utils.isZipFile(toFile.getName())) {
+      return Digester.digestZipFile(toFile);
+    }
+    else {
+      return Digester.digestRegularFile(toFile, normalize);
+    }
+  }
+
+  public Map<String, Long> digestFiles(File dir, List<String> ignoredFiles, boolean normalize, UpdaterUI ui)
+    throws IOException, OperationCancelledException {
+    Map<String, Long> result = new LinkedHashMap<String, Long>();
+
+    LinkedHashSet<String> paths = Utils.collectRelativePaths(dir, myIsStrict);
+    for (String each : paths) {
+      if (ignoredFiles.contains(each)) continue;
+      ui.setStatus(each);
+      ui.checkCancelled();
+      result.put(each, digestFile(new File(dir, each), normalize));
+    }
+    return result;
+  }
+
+  public String getOldBuild() {
+    return myOldBuild;
+  }
+
+  public String getNewBuild() {
+    return myNewBuild;
+  }
+
+  public boolean isStrict() {
+    return myIsStrict;
+  }
+
+  public boolean isNormalized() {
+    return myIsNormalized;
+  }
+
+  public boolean validateDeletion(String path) {
+    for (String delete : myDeleteFiles) {
+      if (path.matches(delete)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   public interface ActionsProcessor {
     void forEach(PatchAction each) throws IOException;
   }
index e91330eae3bd867d601969eece18e28baa885044..bf7bf27c91e1d8cfbfc15939ac8986a6ebdee0d0 100644 (file)
@@ -15,13 +15,16 @@ public abstract class PatchAction {
   protected long myChecksum;
   private boolean isCritical;
   private boolean isOptional;
+  protected transient Patch myPatch;
 
-  public PatchAction(String path, long checksum) {
-    myPath = path;
+  public PatchAction(Patch patch, String path, long checksum) {
+    myPatch = patch;
     myChecksum = checksum;
+    myPath = path;
   }
 
-  public PatchAction(DataInputStream in) throws IOException {
+  public PatchAction(Patch patch, DataInputStream in) throws IOException {
+    myPatch = patch;
     myPath = in.readUTF();
     myChecksum = in.readLong();
     isCritical = in.readBoolean();
@@ -70,18 +73,14 @@ public abstract class PatchAction {
         process.terminate();
       }
     }
-    return shouldApplyOn(file);
+    return doShouldApply(toDir);
   }
 
-  protected boolean shouldApplyOn(File toFile) {
+  protected boolean doShouldApply(File toDir) {
     return true;
   }
 
-  public ValidationResult validate(File toDir) throws IOException {
-    return doValidate(getFile(toDir));
-  }
-
-  protected abstract ValidationResult doValidate(final File toFile) throws IOException;
+  protected abstract ValidationResult validate(File toDir) throws IOException;
 
   protected ValidationResult doValidateAccess(File toFile, ValidationResult.Action action) {
     if (!toFile.exists()) return null;
@@ -95,7 +94,7 @@ public abstract class PatchAction {
                                 myPath,
                                 action,
                                 ValidationResult.ACCESS_DENIED_MESSAGE,
-                                ValidationResult.Option.IGNORE);
+                                myPatch.isStrict() ? ValidationResult.Option.NONE : ValidationResult.Option.IGNORE);
   }
 
   private boolean isWritable(File toFile) {
@@ -144,11 +143,27 @@ public abstract class PatchAction {
     throws IOException {
     if (toFile.exists()) {
       if (isModified(toFile)) {
+        ValidationResult.Option[] options;
+        if (myPatch.isStrict()) {
+          if (isCritical) {
+            options = new ValidationResult.Option[]{ ValidationResult.Option.REPLACE };
+          }
+          else {
+            options = new ValidationResult.Option[]{ ValidationResult.Option.NONE };
+          }
+        } else {
+          if (isCritical) {
+            options = new ValidationResult.Option[]{ ValidationResult.Option.REPLACE, ValidationResult.Option.IGNORE };
+          }
+          else {
+            options = new ValidationResult.Option[]{ ValidationResult.Option.IGNORE };
+          }
+        }
         return new ValidationResult(kind,
                                     myPath,
                                     action,
                                     ValidationResult.MODIFIED_MESSAGE,
-                                    ValidationResult.Option.IGNORE);
+                                    options);
       }
     }
     else if (!isOptional) {
@@ -156,18 +171,20 @@ public abstract class PatchAction {
                                   myPath,
                                   action,
                                   ValidationResult.ABSENT_MESSAGE,
-                                  ValidationResult.Option.IGNORE);
+                                  myPatch.isStrict() ? ValidationResult.Option.NONE : ValidationResult.Option.IGNORE);
     }
     return null;
   }
 
-  abstract protected boolean isModified(File toFile) throws IOException;
+  protected boolean isModified(File toFile) throws IOException {
+    return myChecksum == Digester.INVALID || myChecksum != myPatch.digestFile(toFile, myPatch.isNormalized());
+  }
 
-  public void apply(ZipFile patchFile, File toDir) throws IOException {
-    doApply(patchFile, getFile(toDir));
+  public void apply(ZipFile patchFile, File backupDir, File toDir) throws IOException {
+    doApply(patchFile, backupDir, getFile(toDir));
   }
 
-  protected abstract void doApply(ZipFile patchFile, File toFile) throws IOException;
+  protected abstract void doApply(ZipFile patchFile, File backupDir, File toFile) throws IOException;
 
   public void backup(File toDir, File backupDir) throws IOException {
     doBackup(getFile(toDir), getFile(backupDir));
@@ -181,7 +198,7 @@ public abstract class PatchAction {
 
   protected abstract void doRevert(File toFile, File backupFile) throws IOException;
 
-  private File getFile(File baseDir) {
+  protected File getFile(File baseDir) {
     return new File(baseDir, myPath);
   }
 
index e8892375e204b6a4e194e2d4e83bb2c6387cabbd..6e1ed5c6b4ae2f29f8c2dce55013feecc0abd5c9 100644 (file)
@@ -1,9 +1,6 @@
 package com.intellij.updater;
 
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
+import java.io.*;
 import java.util.List;
 import java.util.Map;
 import java.util.zip.ZipEntry;
@@ -13,15 +10,9 @@ 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 {
+  public static Patch create(PatchSpec spec, File patchFile, UpdaterUI ui) throws IOException, OperationCancelledException {
 
-    Patch patchInfo = new Patch(olderDir, newerDir, ignoredFiles, criticalFiles, optionalFiles, ui);
+    Patch patchInfo = new Patch(spec, ui);
     Runner.logger.info("Creating the patch file '" + patchFile + "'...");
     ui.startProcess("Creating the patch file '" + patchFile + "'...");
     ui.checkCancelled();
@@ -34,6 +25,8 @@ public class PatchFileCreator {
       patchInfo.write(out);
       out.closeEntry();
 
+      File olderDir = new File(spec.getOldFolder());
+      File newerDir = new File(spec.getNewFolder());
       List<PatchAction> actions = patchInfo.getActions();
       for (PatchAction each : actions) {
 
@@ -46,6 +39,8 @@ public class PatchFileCreator {
     finally {
       out.close();
     }
+
+    return patchInfo;
   }
 
   public static PreparationResult prepareAndValidate(File patchFile,
@@ -55,6 +50,7 @@ public class PatchFileCreator {
 
     ZipFile zipFile = new ZipFile(patchFile);
     try {
+
       InputStream in = Utils.getEntryInputStream(zipFile, PATCH_INFO_FILE_NAME);
       try {
         patch = new Patch(in);
@@ -67,6 +63,8 @@ public class PatchFileCreator {
       zipFile.close();
     }
 
+    ui.setDescription(patch.getOldBuild(), patch.getNewBuild());
+
     List<ValidationResult> validationResults = patch.validate(toDir, ui);
     return new PreparationResult(patch, patchFile, toDir, validationResults);
   }
diff --git a/updater/src/com/intellij/updater/PatchSpec.java b/updater/src/com/intellij/updater/PatchSpec.java
new file mode 100644 (file)
index 0000000..b4daf67
--- /dev/null
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.updater;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class PatchSpec {
+  private String myOldVersionDescription = "";
+  private String myNewVersionDescription = "";
+  private String myOldFolder;
+  private String myNewFolder;
+  private String myPatchFile;
+  private String myJarFile;
+  private boolean myIsBinary;
+  private boolean myIsStrict;
+  private List<String> myIgnoredFiles = Collections.emptyList();
+  private List<String> myCriticalFiles = Collections.emptyList();
+  private List<String> myOptionalFiles = Collections.emptyList();
+  private boolean myIsNormalized;
+  private Map<String, String> myWarnings = Collections.emptyMap();
+  private List<String> myDeleteFiles = Collections.emptyList();
+  private String myRoot = "";
+
+  public String getOldVersionDescription() {
+    return myOldVersionDescription;
+  }
+
+  public PatchSpec setOldVersionDescription(String oldVersionDescription) {
+    myOldVersionDescription = oldVersionDescription;
+    return this;
+  }
+
+  public String getNewVersionDescription() {
+    return myNewVersionDescription;
+  }
+
+  public PatchSpec setNewVersionDescription(String newVersionDescription) {
+    myNewVersionDescription = newVersionDescription;
+    return this;
+  }
+
+  public String getOldFolder() {
+    return myOldFolder;
+  }
+
+  public PatchSpec setOldFolder(String oldFolder) {
+    myOldFolder = oldFolder;
+    return this;
+  }
+
+  public String getNewFolder() {
+    return myNewFolder;
+  }
+
+  public PatchSpec setNewFolder(String newFolder) {
+    myNewFolder = newFolder;
+    return this;
+  }
+
+  public String getPatchFile() {
+    return myPatchFile;
+  }
+
+  public PatchSpec setPatchFile(String patchFile) {
+    myPatchFile = patchFile;
+    return this;
+  }
+
+  public String getJarFile() {
+    return myJarFile;
+  }
+
+  public PatchSpec setJarFile(String jarFile) {
+    myJarFile = jarFile;
+    return this;
+  }
+
+  public boolean isStrict() {
+    return myIsStrict;
+  }
+
+  public PatchSpec setStrict(boolean strict) {
+    myIsStrict = strict;
+    return this;
+  }
+
+  public List<String> getIgnoredFiles() {
+    return myIgnoredFiles;
+  }
+
+  public PatchSpec setIgnoredFiles(List<String> ignoredFiles) {
+    myIgnoredFiles = ignoredFiles;
+    return this;
+  }
+
+  public List<String> getCriticalFiles() {
+    return myCriticalFiles;
+  }
+
+  public PatchSpec setCriticalFiles(List<String> criticalFiles) {
+    myCriticalFiles = criticalFiles;
+    return this;
+  }
+
+  public List<String> getOptionalFiles() {
+    return myOptionalFiles;
+  }
+
+  public PatchSpec setOptionalFiles(List<String> optionalFiles) {
+    myOptionalFiles = optionalFiles;
+    return this;
+  }
+
+  public PatchSpec setBinary(boolean binary) {
+    myIsBinary = binary;
+    return this;
+  }
+
+  public boolean isBinary() {
+    return myIsBinary;
+  }
+
+  public boolean isNormalized() {
+    return myIsNormalized;
+  }
+
+  public PatchSpec setNormalized(boolean normalized) {
+    myIsNormalized = normalized;
+    return this;
+  }
+
+  public PatchSpec setWarnings(Map<String, String> warnings) {
+    myWarnings = warnings;
+    return this;
+  }
+
+  public Map<String, String> getWarnings() {
+    return myWarnings;
+  }
+
+  public PatchSpec setDeleteFiles(List<String> deleteFiles) {
+    myDeleteFiles = deleteFiles;
+    return this;
+  }
+
+  public List<String> getDeleteFiles() {
+    return myDeleteFiles;
+  }
+
+  public PatchSpec setRoot(String root) {
+    myRoot = root;
+    return this;
+  }
+
+  public String getRoot() {
+    return myRoot;
+  }
+}
diff --git a/updater/src/com/intellij/updater/RetryException.java b/updater/src/com/intellij/updater/RetryException.java
new file mode 100644 (file)
index 0000000..e13c4eb
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.updater;
+
+import java.io.IOException;
+
+/**
+ * Exception thrown when an IOException arises when performing a patch
+ * action and it's likely that retrying will be successful.
+ */
+public class RetryException extends IOException {
+  public RetryException() {
+  }
+
+  public RetryException(String message) {
+    super(message);
+  }
+
+  public RetryException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+  public RetryException(Throwable cause) {
+    super(cause);
+  }
+}
index 354dba99713a7255a43529d1fd9888cff61a49d1..1d5d9c0217d2d4459f5ebd4d326e652ddbe676cb 100644 (file)
@@ -18,19 +18,13 @@ import java.util.zip.ZipInputStream;
 public class Runner {
   public static Logger logger = null;
 
-  /**
-   * Treats zip files as regular binary files. When false, zip/jar files are unzipped and diffed file by file.
-   * When true, the entire zip file is diffed as a single file. Set to true if preserving the timestamps of
-   * the files inside the zip is important. This variable can change via a command line option.
-   */
-  public static boolean ZIP_AS_BINARY = false;
-
   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 {
+
+    String jarFile = getArgument(args, "jar");
+    jarFile = jarFile == null ? resolveJarFile() : jarFile;
+
     if (args.length >= 6 && "create".equals(args[0])) {
       String oldVersionDesc = args[1];
       String newVersionDesc = args[2];
@@ -39,25 +33,64 @@ public class Runner {
       String patchFile = args[5];
       initLogger();
 
-      ZIP_AS_BINARY = Arrays.asList(args).contains("--zip_as_binary");
-
-      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);
+      // See usage for an explanation of these flags
+      boolean binary = Arrays.asList(args).contains("--zip_as_binary");
+      boolean strict = Arrays.asList(args).contains("--strict");
+      boolean normalized = Arrays.asList(args).contains("--normalized");
+
+      String root = getArgument(args, "root");
+      root = root == null ? "" : (root.endsWith("/") ? root : root + "/");
+
+      List<String> ignoredFiles = extractArguments(args, "ignored");
+      List<String> criticalFiles = extractArguments(args, "critical");
+      List<String> optionalFiles = extractArguments(args, "optional");
+      List<String> deleteFiles = extractArguments(args, "delete");
+      Map<String, String> warnings = buildWarningMap(extractArguments(args, "warning"));
+
+      PatchSpec spec = new PatchSpec()
+        .setOldVersionDescription(oldVersionDesc)
+        .setNewVersionDescription(newVersionDesc)
+        .setRoot(root)
+        .setOldFolder(oldFolder)
+        .setNewFolder(newFolder)
+        .setPatchFile(patchFile)
+        .setJarFile(jarFile)
+        .setStrict(strict)
+        .setBinary(binary)
+        .setNormalized(normalized)
+        .setIgnoredFiles(ignoredFiles)
+        .setCriticalFiles(criticalFiles)
+        .setOptionalFiles(optionalFiles)
+        .setDeleteFiles(deleteFiles)
+        .setWarnings(warnings);
+
+      create(spec);
     }
     else if (args.length >= 2 && "install".equals(args[0])) {
       String destFolder = args[1];
       initLogger();
       logger.info("destFolder: " + destFolder);
 
-      install(destFolder);
+      install(jarFile, destFolder);
     }
     else {
       printUsage();
     }
   }
 
+  private static Map<String, String> buildWarningMap(List<String> warnings) {
+    Map<String, String> map = new HashMap<String, String>();
+    for (String warning : warnings) {
+      int ix = warning.indexOf(":");
+      if (ix != -1) {
+        String path = warning.substring(0, ix);
+        String message = warning.substring(ix + 1).replace("\\n","\n");
+        map.put(path, message);
+      }
+    }
+    return map;
+  }
+
   // checks that log directory 1)exists 2)has write perm. and 3)has 1MB+ free space
   private static boolean isValidLogDir(String logFolder) {
     File fileLogDir = new File(logFolder);
@@ -106,7 +139,17 @@ public class Runner {
     logger.error(e.getMessage(), e);
   }
 
-  public static List<String> extractFiles(String[] args, String paramName) {
+  public static String getArgument(String[] args, String name) {
+    String flag = "--" + name + "=";
+    for (String param : args) {
+      if (param.startsWith(flag)) {
+        return param.substring(flag.length());
+      }
+    }
+    return null;
+  }
+
+  public static List<String> extractArguments(String[] args, String paramName) {
     List<String> result = new ArrayList<String>();
     for (String param : args) {
       if (param.startsWith(paramName + "=")) {
@@ -122,38 +165,51 @@ public class Runner {
 
   @SuppressWarnings("UseOfSystemOutOrSystemErr")
   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");
+    System.err.println(
+      "Usage:\n" +
+      "  Runner create <old_version> <new_version> <old_folder> <new_folder> <patch_file> [<file_set>=file1;file2;...] [<flags>]\n" +
+      "  Runner install <folder>\n" +
+      "\n" +
+      "Where:\n" +
+      "  <old_version>: A description of the version to generate the patch from.\n" +
+      "  <new_version>: A description of the version to generate the patch to.\n" +
+      "  <old_folder>: The folder where to find the old version.\n" +
+      "  <new_folder>: The folder where to find the new version.\n" +
+      "  <patch_file>: The .jar patch file to create which contains the patch and the patcher.\n" +
+      "  <file_set>: Can be one of:\n" +
+      "    ignored: The set of files that will not be included in the patch.\n" +
+      "    critical: Fully included in the patch, so they can be replaced at destination even if they have changed.\n" +
+      "    optional: A set of files that is ok for them no to exist when applying the patch.\n" +
+      "    delete: A set of regular expressions for paths that is safe to delete without user confirmation.\n" +
+      "  <flags>: Can be:\n" +
+      "    --zip_as_binary: Zip and jar files will be treated as binary files and not inspected internally.\n" +
+      "    --strict: The created patch will contain extra information to fully validate an installation. A strict\n" +
+      "              patch will only be applied if it is guaranteed that the patched version will match exactly\n" +
+      "              the source of the patch. This means that unexpected files will be deleted and all existing files\n" +
+      "              will be validated\n" +
+      "    --root=<dir>: Sets dir as the root directory of the patch. The root directory is the directory where the patch should be" +
+      "                  applied to. For example on Mac, you can diff the two .app folders and set Contents as the root." +
+      "                  The root directory is relative to <old_folder> and uses forwards-slashes as separators." +
+      "    --normalized: This creates a normalized patch. This flag only makes sense in addition to --zip_as_binary\n" +
+      "                  A normalized patch must be used to move from an installation that was patched\n" +
+      "                  in a non-binary way to a fully binary patch. This will yield a larger patch, but the\n" +
+      "                  generated patch can be applied on versions where non-binary patches have been applied to and it\n" +
+      "                  guarantees that the patched version will match exactly the original one.\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 {
+  private static void create(PatchSpec spec) 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);
-
-      logger.info("Packing JAR file: " + patchFile );
-      ui.startProcess("Packing JAR file '" + patchFile + "'...");
-
-      FileOutputStream fileOut = new FileOutputStream(patchFile);
+      PatchFileCreator.create(spec, tempPatchFile, ui);
+
+      logger.info("Packing JAR file: " + spec.getPatchFile() );
+      ui.startProcess("Packing JAR file '" + spec.getPatchFile() + "'...");
+
+      FileOutputStream fileOut = new FileOutputStream(spec.getPatchFile());
       try {
         ZipOutputWrapper out = new ZipOutputWrapper(fileOut);
-        ZipInputStream in = new ZipInputStream(new FileInputStream(resolveJarFile()));
+        ZipInputStream in = new ZipInputStream(new FileInputStream(new File(spec.getJarFile())));
         try {
           ZipEntry e;
           while ((e = in.getNextEntry()) != null) {
@@ -164,18 +220,6 @@ public class Runner {
           in.close();
         }
 
-        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
-        try {
-          Properties props = new Properties();
-          props.setProperty(OLD_BUILD_DESCRIPTION, oldBuildDesc);
-          props.setProperty(NEW_BUILD_DESCRIPTION, newBuildDesc);
-          props.store(byteOut, "");
-        }
-        finally {
-          byteOut.close();
-        }
-
-        out.zipBytes(PATCH_PROPERTIES_ENTRY, byteOut);
         out.zipFile(PATCH_FILE_NAME, tempPatchFile);
         out.finish();
       }
@@ -195,18 +239,7 @@ public class Runner {
     Utils.cleanup();
   }
 
-  private static void install(final String destFolder) throws Exception {
-    InputStream in = Runner.class.getResourceAsStream("/" + PATCH_PROPERTIES_ENTRY);
-    Properties props = new Properties();
-    if (in != null) {
-      try {
-        props.load(in);
-      }
-      finally {
-        in.close();
-      }
-    }
-
+  private static void install(final String jarFile, final String destFolder) throws Exception {
     // todo[r.sh] to delete in IDEA 14 (after a full circle of platform updates)
     if (System.getProperty("swing.defaultlaf") == null) {
       SwingUtilities.invokeAndWait(new Runnable() {
@@ -222,27 +255,25 @@ public class Runner {
       });
     }
 
-    new SwingUpdaterUI(props.getProperty(OLD_BUILD_DESCRIPTION),
-                  props.getProperty(NEW_BUILD_DESCRIPTION),
-                  new SwingUpdaterUI.InstallOperation() {
-                    public boolean execute(UpdaterUI ui) throws OperationCancelledException {
-                      logger.info("installing patch to the " + destFolder);
-                      return doInstall(ui, destFolder);
-                    }
-                  });
+    new SwingUpdaterUI(new SwingUpdaterUI.InstallOperation() {
+                         public boolean execute(UpdaterUI ui) throws OperationCancelledException {
+                           logger.info("installing patch to the " + destFolder);
+                           return doInstall(jarFile, ui, destFolder);
+                         }
+                       });
   }
 
-  private static boolean doInstall(UpdaterUI ui, String destFolder) throws OperationCancelledException {
+  private static boolean doInstall(String jarFile, UpdaterUI ui, String destFolder) throws OperationCancelledException {
     try {
       try {
         File patchFile = Utils.createTempFile();
-        ZipFile jarFile = new ZipFile(resolveJarFile());
+        ZipFile zipFile = new ZipFile(jarFile);
 
         logger.info("Extracting patch file...");
         ui.startProcess("Extracting patch file...");
         ui.setProgressIndeterminate();
         try {
-          InputStream in = Utils.getEntryInputStream(jarFile, PATCH_FILE_NAME);
+          InputStream in = Utils.getEntryInputStream(zipFile, PATCH_FILE_NAME);
           OutputStream out = new BufferedOutputStream(new FileOutputStream(patchFile));
           try {
             Utils.copyStream(in, out);
@@ -253,7 +284,7 @@ public class Runner {
           }
         }
         finally {
-          jarFile.close();
+          zipFile.close();
         }
 
         ui.checkCancelled();
@@ -270,6 +301,7 @@ public class Runner {
     }
     finally {
       try {
+        System.gc();
         cleanup(ui);
       }
       catch (IOException e) {
@@ -281,11 +313,7 @@ public class Runner {
     return false;
   }
 
-  private static File resolveJarFile() throws IOException {
-    String jar = System.getProperty("JAR_FILE");
-    if (jar != null) {
-      return new File(jar);
-    }
+  private static String 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");
@@ -299,7 +327,7 @@ public class Runner {
     String jarFileUrl = path.substring(start, end);
 
     try {
-      return new File(new URI(jarFileUrl));
+      return new File(new URI(jarFileUrl)).getAbsolutePath();
     }
     catch (URISyntaxException e) {
       printStackTrace(e);
index d44fc2e094f18d1fb177032380a168371cf5c406..88f8ff96fc7385fbc981d67db1fd34a4b9ab87c2 100644 (file)
@@ -49,7 +49,7 @@ public class SwingUpdaterUI implements UpdaterUI {
   private final JFrame myFrame;
   private boolean myApplied;
 
-  public SwingUpdaterUI(String oldBuildDesc, String newBuildDesc, InstallOperation operation) {
+  public SwingUpdaterUI(InstallOperation operation) {
     myOperation = operation;
 
     myProcessTitle = new JLabel(" ");
@@ -106,8 +106,6 @@ public class SwingUpdaterUI implements UpdaterUI {
     buttonsPanel.add(Box.createHorizontalGlue());
     buttonsPanel.add(myCancelButton);
 
-    myProcessTitle.setText("<html>Updating " + oldBuildDesc + " to " + newBuildDesc + "...");
-
     myFrame.add(processPanel, BorderLayout.CENTER);
     myFrame.add(buttonsPanel, BorderLayout.SOUTH);
 
@@ -127,6 +125,18 @@ public class SwingUpdaterUI implements UpdaterUI {
     startRequestDispatching();
   }
 
+  @Override
+  public void setDescription(String oldBuildDesc, String newBuildDesc) {
+    myProcessTitle.setText("<html>Updating " + oldBuildDesc + " to " + newBuildDesc + "...");
+  }
+
+  @Override
+  public boolean showWarning(String message) {
+    Object[] choices = new Object[] { "Retry", "Exit" };
+    int choice = JOptionPane.showOptionDialog(null, message, "Warning", JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, choices, choices[0]);
+    return choice == 0;
+  }
+
   private void startRequestDispatching() {
     new Thread(new Runnable() {
       public void run() {
@@ -214,6 +224,14 @@ public class SwingUpdaterUI implements UpdaterUI {
     try {
       SwingUtilities.invokeAndWait(new Runnable() {
         public void run() {
+          boolean proceed = true;
+          for (ValidationResult result : validationResults) {
+            if (result.options.contains(ValidationResult.Option.NONE)) {
+              proceed = false;
+              break;
+            }
+          }
+
           final JDialog dialog = new JDialog(myFrame, TITLE, true);
           dialog.setLayout(new BorderLayout());
           dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
@@ -222,12 +240,6 @@ public class SwingUpdaterUI implements UpdaterUI {
           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() {
@@ -237,11 +249,20 @@ public class SwingUpdaterUI implements UpdaterUI {
               dialog.setVisible(false);
             }
           });
-
-          buttonsPanel.add(proceedButton);
           buttonsPanel.add(cancelButton);
 
-          dialog.getRootPane().setDefaultButton(proceedButton);
+          if (proceed) {
+            JButton proceedButton = new JButton(PROCEED_BUTTON_TITLE);
+            proceedButton.addActionListener(new ActionListener() {
+              public void actionPerformed(ActionEvent e) {
+                dialog.setVisible(false);
+              }
+            });
+            buttonsPanel.add(proceedButton);
+            dialog.getRootPane().setDefaultButton(proceedButton);
+          } else {
+            dialog.getRootPane().setDefaultButton(cancelButton);
+          }
 
           JTable table = new JTable();
 
@@ -256,10 +277,16 @@ public class SwingUpdaterUI implements UpdaterUI {
             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>";
+          String message = "<html>Some conflicts were found in the installation area.<br><br>";
+
+          if (proceed) {
+            message += "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>";
+          } else {
+            message += "Some of the conflicts below do not have a solution, so the patch cannot be applied.<br>" +
+                       "Press " + CANCEL_BUTTON_TITLE + " to exit.</html>";
+          }
 
           JLabel label = new JLabel(message);
           label.setBorder(LABEL_BORDER);
@@ -354,7 +381,7 @@ public class SwingUpdaterUI implements UpdaterUI {
   }
 
   public static void main(String[] args) {
-    new SwingUpdaterUI("xxx", "yyy", new InstallOperation() {
+    new SwingUpdaterUI(new InstallOperation() {
       public boolean execute(UpdaterUI ui) throws OperationCancelledException {
         ui.startProcess("Process1");
         ui.checkCancelled();
index 08cf7a1888319e98bad638f060d2e4193a6a4df9..b9c34d8413b750508ec129c7998101de95398410 100644 (file)
@@ -6,48 +6,54 @@ 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(Patch patch, String path, String source, long checksum, boolean move) {
+    super(patch, path, source, checksum, move);
   }
 
-  public UpdateAction(DataInputStream in) throws IOException {
-    super(in);
+  public UpdateAction(Patch patch, String path, long checksum) {
+    this(patch, path, path, checksum, false);
   }
 
-  @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();
+  public UpdateAction(Patch patch, DataInputStream in) throws IOException {
+    super(patch, in);
   }
 
   @Override
-  protected boolean isModified(File toFile) throws IOException {
-    return myChecksum != Digester.digestRegularFile(toFile);
+  protected void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException {
+    if (!myIsMove) {
+      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);
+  protected void doApply(ZipFile patchFile, File backupDir, File toFile) throws IOException {
+    File source = getSource(backupDir);
+    File updated;
+    if (!myIsMove) {
+      updated = Utils.createTempFile();
+      InputStream in = Utils.findEntryInputStream(patchFile, myPath);
+      boolean executable = readExecutableFlag(in);
+
+      OutputStream out = new BufferedOutputStream(new FileOutputStream(updated));
       try {
-        applyDiff(in, oldFileIn, out);
+        InputStream oldFileIn = Utils.newFileInputStream(source, myPatch.isNormalized());
+        try {
+          applyDiff(in, oldFileIn, out);
+        }
+        finally {
+          oldFileIn.close();
+        }
       }
       finally {
-        oldFileIn.close();
+        out.close();
       }
+      Utils.setExecutable(updated, executable);
+    } else {
+      updated = source;
     }
-    finally {
-      out.close();
-    }
-
-    replaceUpdated(temp, toFile);
-    Utils.setExecutable(toFile, executable);
+    replaceUpdated(updated, toFile);
   }
 }
index d808a735f0cdd6043c74effb38ab99a5a9b4b37e..2c8e78eb14b5760f741d4618fedb047bd565885f 100644 (file)
@@ -12,24 +12,24 @@ public class UpdateZipAction extends BaseUpdateAction {
   Set<String> myFilesToUpdate;
   Set<String> myFilesToDelete;
 
-  public UpdateZipAction(String path, long checksum) {
-    super(path, checksum);
+  public UpdateZipAction(Patch patch, String path, String source, long checksum, boolean move) {
+    super(patch, path, source, checksum, move);
   }
 
   // test support
-  public UpdateZipAction(String path,
+  public UpdateZipAction(Patch patch, String path,
                          Collection<String> filesToCreate,
                          Collection<String> filesToUpdate,
                          Collection<String> filesToDelete,
                          long checksum) {
-    super(path, checksum);
+    super(patch, path, path, checksum, false);
     myFilesToCreate = new HashSet<String>(filesToCreate);
     myFilesToUpdate = new HashSet<String>(filesToUpdate);
     myFilesToDelete = new HashSet<String>(filesToDelete);
   }
 
-  public UpdateZipAction(DataInputStream in) throws IOException {
-    super(in);
+  public UpdateZipAction(Patch patch, DataInputStream in) throws IOException {
+    super(patch, in);
 
     int count = in.readInt();
     myFilesToCreate = new HashSet<String>(count);
@@ -87,9 +87,9 @@ public class UpdateZipAction extends BaseUpdateAction {
       }
     });
 
-    DiffCalculator.Result diff = DiffCalculator.calculate(oldCheckSums, newCheckSums);
+    DiffCalculator.Result diff = DiffCalculator.calculate(oldCheckSums, newCheckSums, new LinkedList<String>(), false);
 
-    myFilesToCreate = diff.filesToCreate;
+    myFilesToCreate = diff.filesToCreate.keySet();
     myFilesToUpdate = diff.filesToUpdate.keySet();
     myFilesToDelete = diff.filesToDelete.keySet();
 
@@ -154,18 +154,15 @@ public class UpdateZipAction extends BaseUpdateAction {
   }
 
   @Override
-  protected boolean isModified(File toFile) throws IOException {
-    return myChecksum != Digester.digestFile(toFile);
-  }
-
-  protected void doApply(final ZipFile patchFile, File toFile) throws IOException {
+  protected void doApply(final ZipFile patchFile, File backupDir, File toFile) throws IOException {
     File temp = Utils.createTempFile();
     FileOutputStream fileOut = new FileOutputStream(temp);
     try {
       final ZipOutputWrapper out = new ZipOutputWrapper(fileOut);
       out.setCompressionLevel(0);
 
-      processZipFile(toFile, new Processor() {
+      processZipFile(getSource(backupDir), new Processor() {
+        @Override
         public void process(ZipEntry entry, InputStream in) throws IOException {
           String path = entry.getName();
           if (myFilesToDelete.contains(path)) return;
index 85005ff81205731d2323a613451c0d96511d8347..4a38e0fea3ec5542a95ed7486338572ac65ba07c 100644 (file)
@@ -16,5 +16,15 @@ public interface UpdaterUI {
 
   void checkCancelled() throws OperationCancelledException;
 
+  void setDescription(String oldBuildDesc, String newBuildDesc);
+
+  /**
+   * Shows a warning associated with the pretense of a file and asks the user if the validation needs be retried.
+   * This function will return true iff the user wants to retry.
+   * @param message The warning message to display.
+   * @return true if the validation needs to be retried or false if te updater should quit.
+   */
+  boolean showWarning(String message);
+
   Map<String, ValidationResult.Option> askUser(List<ValidationResult> validationResults) throws OperationCancelledException;
 }
\ No newline at end of file
index bce3333e106e6420c84165248a5e87e20cc48341..b9ac29a46348d5fafe415255f7341db3dbb6d74c 100644 (file)
@@ -1,7 +1,7 @@
 package com.intellij.updater;
 
 import java.io.*;
-import java.util.LinkedHashSet;
+import java.util.*;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
@@ -73,6 +73,7 @@ public class Utils {
   public static void copy(File from, File to) throws IOException {
     Runner.logger.info("from " + from.getPath() + " to " + to.getPath());
     if (from.isDirectory()) {
+      to.mkdirs();
       File[] files = from.listFiles();
       if (files == null) throw new IOException("Cannot get directory's content: " + from);
       for (File each : files) {
@@ -91,6 +92,15 @@ public class Utils {
     }
   }
 
+
+  public static void mirror(File from, File to) throws IOException {
+    if (from.exists()) {
+      copy(from, to);
+    } else {
+      delete(to);
+    }
+  }
+
   public static void copyFileToStream(File from, OutputStream out) throws IOException {
     InputStream in = new BufferedInputStream(new FileInputStream(from));
     try {
@@ -146,40 +156,121 @@ public class Utils {
   }
 
   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");
-    Runner.logger.info("entryPath: " + entryPath);
-    return result;
+    ZipEntry entry = getZipEntry(zipFile, entryPath);
+    return findEntryInputStreamForEntry(zipFile, entry);
   }
 
   public static InputStream findEntryInputStream(ZipFile zipFile, String entryPath) throws IOException {
     ZipEntry entry = zipFile.getEntry(entryPath);
-    if (entry == null || entry.isDirectory()) return null;
+    if (entry == null) return null;
+    return findEntryInputStreamForEntry(zipFile, entry);
+  }
+
+  public static ZipEntry getZipEntry(ZipFile zipFile, String entryPath) throws IOException {
+    ZipEntry entry = zipFile.getEntry(entryPath);
+    if (entry == null) throw new IOException("Entry " + entryPath + " not found");
+    Runner.logger.info("entryPath: " + entryPath);
+    return entry;
+  }
 
-    // if isDirectory check failed, check presence of 'file/' manually
-    if (!entryPath.endsWith("/") && zipFile.getEntry(entryPath + "/") != null) return null;
+  public static InputStream findEntryInputStreamForEntry(ZipFile zipFile, ZipEntry entry) throws IOException {
+    if (entry.isDirectory()) return null;
+    // There is a bug in some JVM implementations where for a directory "X/" in a zipfile, if we do
+    // "zip.getEntry("X/").isDirectory()" returns true, but if we do "zip.getEntry("X").isDirectory()" is false.
+    // getEntry for "name" falls back to finding "X/", so here we make sure that didn't happen.
+    if (zipFile.getEntry(entry.getName() + "/") != null) return null;
 
     return new BufferedInputStream(zipFile.getInputStream(entry));
   }
 
-  public static LinkedHashSet<String> collectRelativePaths(File dir) {
+  public static LinkedHashSet<String> collectRelativePaths(File dir, boolean includeDirectories) {
     LinkedHashSet<String> result = new LinkedHashSet<String>();
-    collectRelativePaths(dir, result, null);
+    collectRelativePaths(dir, result, null, includeDirectories);
     return result;
   }
 
-  private static void collectRelativePaths(File dir, LinkedHashSet<String> result, String parentPath) {
+  private static void collectRelativePaths(File dir, LinkedHashSet<String> result, String parentPath, boolean includeDirectories) {
     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);
+        if (includeDirectories) {
+          // The trailing slash is very important, as it's used by zip to determine whether it is a directory.
+          result.add(relativePath + "/");
+        }
+        collectRelativePaths(each, result, relativePath, includeDirectories);
       }
       else {
         result.add(relativePath);
       }
     }
   }
+
+  public static InputStream newFileInputStream(File file, boolean normalize) throws IOException {
+    if (!normalize || !isZipFile(file.getName())) {
+      return new FileInputStream(file);
+    }
+    return new NormalizedZipInputStream(file);
+  }
+
+  static class NormalizedZipInputStream extends InputStream {
+
+    private ArrayList<? extends ZipEntry> myEntries;
+    private InputStream myStream = null;
+    private int myNextEntry = 0;
+    private final ZipFile myZip;
+    private byte[] myByte = new byte[1];
+
+    NormalizedZipInputStream(File file) throws IOException {
+      myZip = new ZipFile(file);
+      myEntries = Collections.list(myZip.entries());
+      Collections.sort(myEntries, new Comparator<ZipEntry>() {
+        @Override
+        public int compare(ZipEntry a, ZipEntry b) {
+          return a.getName().compareTo(b.getName());
+        }
+      });
+
+      loadNextEntry();
+    }
+
+    private void loadNextEntry() throws IOException {
+      if (myStream != null) {
+        myStream.close();
+      }
+      myStream = null;
+      while (myNextEntry < myEntries.size() && myStream == null) {
+        myStream = findEntryInputStreamForEntry(myZip, myEntries.get(myNextEntry++));
+      }
+    }
+
+    @Override
+    public int read(byte[] bytes, int off, int len) throws IOException {
+      if (myStream == null) {
+        return -1;
+      }
+      int b = myStream.read(bytes, off, len);
+      if (b == -1) {
+        loadNextEntry();
+        return read(bytes, off, len);
+      }
+      return b;
+    }
+
+    @Override
+    public int read() throws IOException {
+      int b = read(myByte, 0, 1);
+      return b == -1 ? -1 : myByte[0];
+    }
+
+    @Override
+    public void close() throws IOException {
+      if (myStream != null) {
+        myStream.close();
+      }
+      myZip.close();
+    }
+  }
 }
diff --git a/updater/src/com/intellij/updater/ValidateAction.java b/updater/src/com/intellij/updater/ValidateAction.java
new file mode 100644 (file)
index 0000000..a9ae8eb
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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 ValidateAction extends PatchAction {
+  // Only used on patch creation
+  protected transient File myOlderDir;
+
+  public ValidateAction(Patch patch, String path, long checksum) {
+    super(patch, path, checksum);
+  }
+
+  public ValidateAction(Patch patch, DataInputStream in) throws IOException {
+    super(patch, in);
+  }
+
+  @Override
+  protected void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException {
+  }
+
+  @Override
+  public ValidationResult validate(File toDir) throws IOException {
+    return doValidateNotChanged(getFile(toDir), ValidationResult.Kind.ERROR, ValidationResult.Action.VALIDATE);
+  }
+
+  @Override
+  protected void doApply(ZipFile patchFile, File backupDir, File toFile) throws IOException {
+  }
+
+  @Override
+  protected void doBackup(File toFile, File backupFile) throws IOException {
+  }
+
+  @Override
+  protected void doRevert(File toFile, File backupFile) throws IOException {
+  }
+}
index 6f56fd3bc0c3726d8168aaff9f6ebaea0df3aadf..dc5fcd40b56dfd296ff983f736a7c1471af4a934 100644 (file)
@@ -9,7 +9,7 @@ public class ValidationResult implements Comparable<ValidationResult> {
   }
 
   public enum Action {
-    CREATE("Create"), UPDATE("Update"), DELETE("Delete"), NO_ACTION("");
+    CREATE("Create"), UPDATE("Update"), DELETE("Delete"), NO_ACTION(""), VALIDATE("Validate");
 
     private final String myDisplayString;
 
@@ -24,7 +24,7 @@ public class ValidationResult implements Comparable<ValidationResult> {
   }
 
   public enum Option {
-    IGNORE, KEEP, REPLACE, DELETE, KILL_PROCESS
+    NONE, IGNORE, KEEP, REPLACE, DELETE, KILL_PROCESS
   }
 
   public static final String ABSENT_MESSAGE = "Absent";
index 3b5efe459ee000d0bedbfe4fb2f8844a9bae0260..c509af0d5f17b4b80d58537fe2b2ce3ae5a971a3 100644 (file)
@@ -71,13 +71,13 @@ Uninstalling IntelliJ IDEA
 
 Licensing & pricing
 ==========================
-  Licensing and pricing information can be found at https://www.jetbrains.com/idea/buy/index.html.
+  Licensing and pricing information can be found at http://www.jetbrains.com/idea/buy/index.html.
 
 
 IntelliJ IDEA Overview
 ==========================
   For general info and facts on IntelliJ IDEA, you can refer to IntelliJ IDEA Info Kit at
-  https://www.jetbrains.com/idea/documentation/product_info_kit.html.
+  http://www.jetbrains.com/idea/documentation/product_info_kit.html.
 
 
 IDEA Development Package
@@ -102,7 +102,7 @@ IDEA Development Package
       * StarTeam, Perforce, Subversion, Visual SourceSafe integration
       * Tomcat, Weblogic, WebSphere, Geronimo, JBoss, GlassFish, JSR45 integration
 
-  Download page: https://www.jetbrains.com/idea/download/index.html
+  Download page: http://www.jetbrains.com/idea/download/index.html
 
   Source code of additional open source plugins shipped with IntelliJ IDEA is available in the Subversion
   repository at:
@@ -139,7 +139,7 @@ IntelliJ Community Site
 Support
 =======
   For technical support and assistance, you may find necessary information at the Support page
-  (https://www.jetbrains.com/support/index.html) or contact us at support@jetbrains.com.
+  (http://www.jetbrains.com/support/index.html) or contact us at support@jetbrains.com.
 
 
 Bug Reporting:
@@ -158,7 +158,7 @@ Contacting us:
 
 
 =============
-You are encouraged to visit our IntelliJ IDEA web site at https://www.jetbrains.com/idea/
+You are encouraged to visit our IntelliJ IDEA web site at http://www.jetbrains.com/idea/
 or to contact us via e-mail at feedback@jetbrains.com if you have any comments about
 this release. In particular, we are very interested in any ease-of-use, user
 interface suggestions that you may have. We will be posting regular updates
index 3b524f8288e93ae0397324cae5d37dc2c00a489e..885db6be7b1e5b1e2b5d703ecc3906f398215243 100644 (file)
@@ -2,20 +2,33 @@ package com.intellij.updater;
 
 import org.junit.Test;
 
-import java.util.Collections;
-import java.util.Map;
+import java.io.File;
 
 import static org.junit.Assert.assertEquals;
 
 public class DigesterTest extends UpdaterTestCase {
   @Test
   public void testBasics() throws Exception {
-    Runner.initLogger();
-    Map<String, Long> checkSums = Digester.digestFiles(getDataDir(), Collections.<String>emptyList(), TEST_UI);
-    assertEquals(12, checkSums.size());
+    assertEquals(CHECKSUMS.README_TXT, Digester.digestRegularFile(new File(getDataDir(), "Readme.txt"), false));
+    assertEquals(CHECKSUMS.FOCUSKILLER_DLL, Digester.digestRegularFile(new File(getDataDir(), "/bin/focuskiller.dll"), false));
+    assertEquals(CHECKSUMS.BOOTSTRAP_JAR, Digester.digestZipFile(new File(getDataDir(), "/lib/bootstrap.jar")));
+    assertEquals(CHECKSUMS.BOOTSTRAP_JAR_BINARY, Digester.digestRegularFile(new File(getDataDir(), "/lib/bootstrap.jar"), false));
 
-    assertEquals(CHECKSUMS.README_TXT, (long)checkSums.get("Readme.txt"));
-    assertEquals(CHECKSUMS.FOCUSKILLER_DLL, (long)checkSums.get("bin/focuskiller.dll"));
-    assertEquals(CHECKSUMS.BOOTSTRAP_JAR, (long)checkSums.get("lib/bootstrap.jar"));
+    assertEquals(CHECKSUMS.ANNOTATIONS_JAR_NORM,
+                 Digester.digestRegularFile(new File(getDataDir(), "/lib/annotations.jar"), true));
+    assertEquals(CHECKSUMS.ANNOTATIONS_CHANGED_JAR_NORM,
+                 Digester.digestRegularFile(new File(getDataDir(), "/lib/annotations_changed.jar"), true));
+    assertEquals(CHECKSUMS.BOOT_JAR_NORM,
+                 Digester.digestRegularFile(new File(getDataDir(), "/lib/boot.jar"), true));
+    assertEquals(CHECKSUMS.BOOT2_JAR_NORM,
+                 Digester.digestRegularFile(new File(getDataDir(), "/lib/boot2.jar"), true));
+    assertEquals(CHECKSUMS.BOOT2_CHANGED_WITH_UNCHANGED_CONTENT_JAR_NORM,
+                 Digester.digestRegularFile(new File(getDataDir(), "/lib/boot2_changed_with_unchanged_content.jar"), true));
+    assertEquals(CHECKSUMS.BOOT_WITH_DIRECTORY_BECOMES_FILE_JAR_NORM,
+                 Digester.digestRegularFile(new File(getDataDir(), "/lib/boot_with_directory_becomes_file.jar"), true));
+    assertEquals(CHECKSUMS.BOOTSTRAP_JAR_NORM,
+                 Digester.digestRegularFile(new File(getDataDir(), "/lib/bootstrap.jar"), true));
+    assertEquals(CHECKSUMS.BOOTSTRAP_DELETED_JAR_NORM,
+                 Digester.digestRegularFile(new File(getDataDir(), "/lib/bootstrap_deleted.jar"), true));
   }
 }
\ No newline at end of file
diff --git a/updater/testSrc/com/intellij/updater/PatchFileCreatorBinaryTest.java b/updater/testSrc/com/intellij/updater/PatchFileCreatorBinaryTest.java
new file mode 100644 (file)
index 0000000..6cacfe9
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.updater;
+
+public class PatchFileCreatorBinaryTest extends PatchFileCreatorTest {
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    myPatchSpec.setBinary(true);
+  }
+}
diff --git a/updater/testSrc/com/intellij/updater/PatchFileCreatorNotBinaryTest.java b/updater/testSrc/com/intellij/updater/PatchFileCreatorNotBinaryTest.java
new file mode 100644 (file)
index 0000000..666dbc2
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.updater;
+
+import com.intellij.openapi.util.io.FileUtil;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class PatchFileCreatorNotBinaryTest extends PatchFileCreatorTest {
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+  }
+
+  @Test
+  public void failOnEmptyTargetJar() throws Exception {
+    final File sourceJar = new File(myOlderDir, "lib/empty.jar");
+    FileUtil.copy(new File(myOlderDir, "lib/annotations.jar"), sourceJar);
+
+    try {
+      final File targetJar = new File(myNewerDir, "lib/empty.jar");
+      if (targetJar.exists()) targetJar.delete();
+      assertTrue(targetJar.createNewFile());
+
+      try {
+        createPatch();
+        fail("Should have failed to create a patch against empty .jar");
+      }
+      catch (IOException e) {
+        final String reason = e.getMessage();
+        assertEquals("Corrupted target file: " + targetJar, reason);
+      }
+      finally {
+        targetJar.delete();
+      }
+    }
+    finally {
+      sourceJar.delete();
+    }
+  }
+
+  @Test
+  public void failOnEmptySourceJar() throws Exception {
+    final File sourceJar = new File(myOlderDir, "lib/empty.jar");
+    if (sourceJar.exists()) sourceJar.delete();
+    assertTrue(sourceJar.createNewFile());
+
+    try {
+      final File targetJar = new File(myNewerDir, "lib/empty.jar");
+      FileUtil.copy(new File(myNewerDir, "lib/annotations.jar"), targetJar);
+
+      try {
+        createPatch();
+        fail("Should have failed to create a patch from empty .jar");
+      }
+      catch (IOException e) {
+        final String reason = e.getMessage();
+        assertEquals("Corrupted source file: " + sourceJar, reason);
+      }
+      finally {
+        targetJar.delete();
+      }
+    }
+    finally {
+      sourceJar.delete();
+    }
+  }
+}
index 2cffb224c60f14e76db20afa8715706d1a0bdf7d..76ecdbea5b19fcb3d9d80686f8515a6f0eb651d3 100644 (file)
@@ -4,9 +4,7 @@ import com.intellij.openapi.util.io.FileUtil;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.File;
-import java.io.IOException;
-import java.io.RandomAccessFile;
+import java.io.*;
 import java.util.*;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
@@ -15,109 +13,89 @@ import java.util.zip.ZipOutputStream;
 import static org.junit.Assert.*;
 
 @SuppressWarnings("ResultOfMethodCallIgnored")
-public class PatchFileCreatorTest extends PatchTestCase {
+public abstract class PatchFileCreatorTest extends PatchTestCase {
   private File myFile;
+  protected PatchSpec myPatchSpec;
 
   @Override
   @Before
   public void setUp() throws Exception {
     super.setUp();
     myFile = getTempFile("patch.zip");
+    myPatchSpec = new PatchSpec()
+      .setOldFolder(myOlderDir.getAbsolutePath())
+      .setNewFolder(myNewerDir.getAbsolutePath());
   }
 
   @Test
   public void testCreatingAndApplying() throws Exception {
-    createPatch();
+    Patch patch = createPatch();
 
-    assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
+    assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
   }
 
   @Test
-  public void failOnEmptySourceJar() throws Exception {
-    final File sourceJar = new File(myOlderDir, "lib/empty.jar");
-    if (sourceJar.exists()) sourceJar.delete();
-    assertTrue(sourceJar.createNewFile());
+  public void testCreatingAndApplyingStrict() throws Exception {
+    myPatchSpec.setStrict(true);
+    Patch patch = createPatch();
+    assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
+  }
 
-    try {
-      final File targetJar = new File(myNewerDir, "lib/empty.jar");
-      FileUtil.copy(new File(myNewerDir, "lib/annotations.jar"), targetJar);
+  @Test
+  public void testCreatingAndApplyingOnADifferentRoot() throws Exception {
+    myPatchSpec.setRoot("bin/");
+    myPatchSpec.setStrict(true);
 
-      try {
-        createPatch();
-        fail("Should have failed to create a patch from empty .jar");
-      }
-      catch (IOException e) {
-        final String reason = e.getMessage();
-        assertEquals("Corrupted source file: " + sourceJar, reason);
-      }
-      finally {
-        targetJar.delete();
-      }
-    }
-    finally {
-      sourceJar.delete();
-    }
+    Patch patch = createPatch();
+
+    File target = new File(myOlderDir, "bin");
+    assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, target, TEST_UI));
   }
 
   @Test
-  public void failOnEmptyTargetJar() throws Exception {
-    final File sourceJar = new File(myOlderDir, "lib/empty.jar");
-    FileUtil.copy(new File(myOlderDir, "lib/annotations.jar"), sourceJar);
+  public void testCreatingAndFailingOnADifferentRoot() throws Exception {
+    myPatchSpec.setRoot("bin/");
+    myPatchSpec.setStrict(true);
 
-    try {
-      final File targetJar = new File(myNewerDir, "lib/empty.jar");
-      if (targetJar.exists()) targetJar.delete();
-      assertTrue(targetJar.createNewFile());
+    Patch patch = createPatch();
 
-      try {
-        createPatch();
-        fail("Should have failed to create a patch against empty .jar");
-      }
-      catch (IOException e) {
-        final String reason = e.getMessage();
-        assertEquals("Corrupted target file: " + targetJar, reason);
-      }
-      finally {
-        targetJar.delete();
-      }
-    }
-    finally {
-      sourceJar.delete();
-    }
+    File target = new File(myOlderDir, "bin");
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, target, TEST_UI);
+    preparationResult.patch.getActions().add(new MyFailOnApplyPatchAction(patch));
+    assertNothingHasChanged(patch, preparationResult, new HashMap<String, ValidationResult.Option>());
   }
 
   @Test
   public void testReverting() throws Exception {
-    createPatch();
+    Patch patch = createPatch();
 
     PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
-    preparationResult.patch.getActions().add(new MyFailOnApplyPatchAction());
-    assertNothingHasChanged(preparationResult, new HashMap<String, ValidationResult.Option>());
+    preparationResult.patch.getActions().add(new MyFailOnApplyPatchAction(patch));
+    assertNothingHasChanged(patch, preparationResult, new HashMap<String, ValidationResult.Option>());
   }
 
   @Test
   public void testRevertedWhenFileToDeleteIsProcessLocked() throws Exception {
     if (!UtilsTest.mIsWindows) return;
 
-    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
-                            Collections.<String>emptyList(), TEST_UI);
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
 
 
     RandomAccessFile raf = new RandomAccessFile(new File(myOlderDir, "bin/idea.bat"),"rw");
-    // Lock the file. FileLock is not good here, because we need to prevent deletion.
-    int b = raf.read();
-    raf.seek(0);
-    raf.write(b);
-
     try {
+      // Lock the file. FileLock is not good here, because we need to prevent deletion.
+      int b = raf.read();
+      raf.seek(0);
+      raf.write(b);
+
       PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
 
-      Map<String, Long> original = Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI);
+      Map<String, Long> original = patch.digestFiles(myOlderDir, Collections.<String>emptyList(), false, TEST_UI);
 
       File backup = getTempFile("backup");
       PatchFileCreator.apply(preparationResult, new HashMap<String, ValidationResult.Option>(), backup, TEST_UI);
 
-      assertEquals(original, Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI));
+      assertEquals(original, patch.digestFiles(myOlderDir, Collections.<String>emptyList(), false, TEST_UI));
     }
     finally {
       raf.close();
@@ -126,44 +104,57 @@ public class PatchFileCreatorTest extends PatchTestCase {
 
   @Test
   public void testApplyingWithAbsentFileToDelete() throws Exception {
-    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
-                            Collections.<String>emptyList(), TEST_UI);
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
 
     new File(myOlderDir, "bin/idea.bat").delete();
 
-    assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
+    assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
+  }
+
+  @Test
+  public void testApplyingWithAbsentFileToUpdateStrict() throws Exception {
+    myPatchSpec.setStrict(true);
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
+
+    new File(myOlderDir, "lib/annotations.jar").delete();
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    assertEquals(1, preparationResult.validationResults.size());
+    assertEquals(new ValidationResult(ValidationResult.Kind.ERROR,
+                                      "lib/annotations.jar",
+                                      ValidationResult.Action.UPDATE,
+                                      ValidationResult.ABSENT_MESSAGE,
+                                      ValidationResult.Option.NONE), preparationResult.validationResults.get(0));
   }
 
   @Test
   public void testApplyingWithAbsentOptionalFile() throws Exception {
     FileUtil.writeToFile(new File(myNewerDir, "bin/idea.bat"), "new content".getBytes());
 
-    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
-                            Collections.singletonList("bin/idea.bat"), TEST_UI);
+    myPatchSpec.setOptionalFiles(Collections.singletonList("bin/idea.bat"));
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
 
     new File(myOlderDir, "bin/idea.bat").delete();
 
     PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
     assertTrue(preparationResult.validationResults.isEmpty());
-    assertAppliedAndRevertedCorrectly(preparationResult);
+    assertAppliedAndRevertedCorrectly(patch, preparationResult);
   }
 
   @Test
   public void testRevertingWithAbsentFileToDelete() throws Exception {
-    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
-                            Collections.<String>emptyList(), TEST_UI);
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
 
     new File(myOlderDir, "bin/idea.bat").delete();
 
     PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
-    preparationResult.patch.getActions().add(new MyFailOnApplyPatchAction());
-    assertNothingHasChanged(preparationResult, new HashMap<String, ValidationResult.Option>());
+    preparationResult.patch.getActions().add(new MyFailOnApplyPatchAction(patch));
+    assertNothingHasChanged(patch, preparationResult, new HashMap<String, ValidationResult.Option>());
   }
 
   @Test
   public void testApplyingWithoutCriticalFiles() throws Exception {
-    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
-                            Collections.<String>emptyList(), TEST_UI);
+    PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
     PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
 
     assertTrue(PatchFileCreator.apply(preparationResult, new HashMap<String, ValidationResult.Option>(), TEST_UI));
@@ -171,24 +162,49 @@ public class PatchFileCreatorTest extends PatchTestCase {
 
   @Test
   public void testApplyingWithCriticalFiles() throws Exception {
-    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Arrays.asList("lib/annotations.jar"),
-                            Collections.<String>emptyList(), TEST_UI);
+    myPatchSpec.setCriticalFiles(Arrays.asList("lib/annotations.jar"));
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
 
-    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
+  }
 
-    assertTrue(PatchFileCreator.apply(preparationResult, new HashMap<String, ValidationResult.Option>(), TEST_UI));
-    assertAppliedCorrectly();
+  @Test
+  public void testApplyingWithModifiedCriticalFiles() throws Exception {
+    myPatchSpec.setStrict(true);
+    myPatchSpec.setCriticalFiles(Arrays.asList("lib/annotations.jar"));
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
+
+    RandomAccessFile raf = new RandomAccessFile(new File(myOlderDir, "lib/annotations.jar"), "rw");
+    raf.seek(20);
+    raf.write(42);
+    raf.close();
+
+    assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
+  }
+
+  @Test
+  public void testApplyingWithModifiedCriticalFilesAndDifferentRoot() throws Exception {
+    myPatchSpec.setStrict(true);
+    myPatchSpec.setRoot("lib/");
+    myPatchSpec.setCriticalFiles(Arrays.asList("lib/annotations.jar"));
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
+
+    RandomAccessFile raf = new RandomAccessFile(new File(myOlderDir, "lib/annotations.jar"), "rw");
+    raf.seek(20);
+    raf.write(42);
+    raf.close();
+
+    File toDir = new File(myOlderDir, "lib/");
+    assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, toDir, TEST_UI));
   }
 
   @Test
   public void testApplyingWithCaseChangedNames() throws Exception {
-    FileUtil.rename(new File(myOlderDir, "Readme.txt"),
-                    new File(myOlderDir, "README.txt"));
+    FileUtil.rename(new File(myOlderDir, "Readme.txt"), new File(myOlderDir, "README.txt"));
 
-    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
-                            Collections.<String>emptyList(), TEST_UI);
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
 
-    assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
+    assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
   }
 
   @Test
@@ -201,13 +217,11 @@ public class PatchFileCreatorTest extends PatchTestCase {
     new File(file, "subDir").mkdir();
     new File(file, "subDir/subFile.txt").createNewFile();
 
-    FileUtil.copy(new File(myOlderDir, "lib/boot.jar"),
-                  new File(myOlderDir, "lib/boot_with_directory_becomes_file.jar"));
+    FileUtil.copy(new File(myOlderDir, "lib/boot.jar"), new File(myOlderDir, "lib/boot_with_directory_becomes_file.jar"));
 
-    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
-                            Collections.<String>emptyList(), TEST_UI);
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
 
-    assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
+    assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
   }
 
   @Test
@@ -219,15 +233,14 @@ public class PatchFileCreatorTest extends PatchTestCase {
     FileUtil.copy(new File(myOlderDir, "lib/boot_with_directory_becomes_file.jar"),
                   new File(myOlderDir, "lib/boot.jar"));
 
-    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
-                            Collections.<String>emptyList(), TEST_UI);
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
 
-    assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
+    assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
   }
 
   @Test
   public void testConsideringOptions() throws Exception {
-    createPatch();
+    Patch patch = createPatch();
 
     PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
     Map<String, ValidationResult.Option> options = new HashMap<String, ValidationResult.Option>();
@@ -235,47 +248,241 @@ public class PatchFileCreatorTest extends PatchTestCase {
       options.put(each.getPath(), ValidationResult.Option.IGNORE);
     }
 
-    assertNothingHasChanged(preparationResult, options);
+    assertNothingHasChanged(patch, preparationResult, options);
+  }
+
+  @Test
+  public void testApplyWhenCommonFileChanges() throws Exception {
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
+
+    FileUtil.copy(new File(myOlderDir, "/lib/bootstrap.jar"),
+                  new File(myOlderDir, "/lib/boot.jar"));
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    assertTrue(preparationResult.validationResults.isEmpty());
+    assertAppliedAndRevertedCorrectly(patch, preparationResult);
+  }
+
+  @Test
+  public void testApplyWhenCommonFileChangesStrict() throws Exception {
+    myPatchSpec.setStrict(true);
+    PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
+
+    FileUtil.copy(new File(myOlderDir, "/lib/bootstrap.jar"), new File(myOlderDir, "/lib/boot.jar"));
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    assertEquals(1, preparationResult.validationResults.size());
+    assertEquals(
+      new ValidationResult(ValidationResult.Kind.ERROR, "lib/boot.jar", ValidationResult.Action.VALIDATE, ValidationResult.MODIFIED_MESSAGE,
+                           ValidationResult.Option.NONE), preparationResult.validationResults.get(0));
+  }
+
+  @Test
+  public void testApplyWhenNewFileExists() throws Exception {
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
+
+    FileUtil.writeToFile(new File(myOlderDir, "newfile.txt"), "hello");
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    assertTrue(preparationResult.validationResults.isEmpty());
+    assertAppliedAndRevertedCorrectly(patch, preparationResult);
   }
 
-  private void createPatch() throws IOException, OperationCancelledException {
-    PatchFileCreator.create(myOlderDir, myNewerDir, myFile,
-                            Collections.<String>emptyList(), Collections.<String>emptyList(), Collections.<String>emptyList(), TEST_UI);
+  @Test
+  public void testApplyWhenNewFileExistsStrict() throws Exception {
+    myPatchSpec.setStrict(true);
+    myPatchSpec.setDeleteFiles(Collections.singletonList("lib/java_pid.*\\.hprof"));
+
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
+
+    FileUtil.writeToFile(new File(myOlderDir, "newfile.txt"), "hello");
+    FileUtil.writeToFile(new File(myOlderDir, "lib/java_pid1234.hprof"), "bye!");
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    assertEquals(1, preparationResult.validationResults.size());
+    assertEquals(new ValidationResult(ValidationResult.Kind.CONFLICT, "newfile.txt", ValidationResult.Action.VALIDATE, "Unexpected file",
+                                      ValidationResult.Option.DELETE), preparationResult.validationResults.get(0));
+    assertAppliedAndRevertedCorrectly(patch, preparationResult);
+  }
+
+  @Test
+  public void testApplyWhenNewDeletableFileExistsStrict() throws Exception {
+    myPatchSpec.setStrict(true);
+    myPatchSpec.setDeleteFiles(Collections.singletonList("lib/java_pid.*\\.hprof"));
+
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
+
+    FileUtil.writeToFile(new File(myOlderDir, "lib/java_pid1234.hprof"), "bye!");
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    assertEquals(0, preparationResult.validationResults.size());
+    assertAppliedAndRevertedCorrectly(patch, preparationResult);
+  }
+
+  @Test
+  public void testApplyWhenNewDirectoryExistsStrict() throws Exception {
+    myPatchSpec.setStrict(true);
+    new File(myOlderDir, "delete").mkdirs();
+    FileUtil.writeToFile(new File(myOlderDir, "delete/deleteme.txt"), "bye!");
+
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
+
+    new File(myOlderDir, "unexpected_newdir").mkdirs();
+    FileUtil.writeToFile(new File(myOlderDir, "unexpected_newdir/unexpected.txt"), "bye!");
+
+    new File(myOlderDir, "newDir").mkdir();
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    assertEquals(3, preparationResult.validationResults.size());
+    assertEquals(new ValidationResult(ValidationResult.Kind.CONFLICT,
+                                      "unexpected_newdir/unexpected.txt",
+                                      ValidationResult.Action.VALIDATE,
+                                      "Unexpected file",
+                                      ValidationResult.Option.DELETE), preparationResult.validationResults.get(0));
+    assertEquals(new ValidationResult(ValidationResult.Kind.CONFLICT,
+                                      "unexpected_newdir/",
+                                      ValidationResult.Action.VALIDATE,
+                                      "Unexpected file",
+                                      ValidationResult.Option.DELETE), preparationResult.validationResults.get(1));
+    assertEquals(new ValidationResult(ValidationResult.Kind.CONFLICT,
+                                      "newDir/",
+                                      ValidationResult.Action.CREATE,
+                                      ValidationResult.ALREADY_EXISTS_MESSAGE,
+                                      ValidationResult.Option.REPLACE), preparationResult.validationResults.get(2));
+    new File(myOlderDir, "newDir").delete();
+    assertAppliedAndRevertedCorrectly(patch, preparationResult);
+  }
+
+  @Test
+  public void testMoveFileByContent() throws IOException, OperationCancelledException {
+    myPatchSpec.setStrict(true);
+    FileUtil.writeToFile(new File(myOlderDir, "move/from/this/directory/move.me"), "oldcontent");
+    FileUtil.writeToFile(new File(myOlderDir, "a/deleted/file/that/is/a/copy/move.me"), "newcontent");
+    FileUtil.writeToFile(new File(myNewerDir, "move/to/this/directory/move.me"), "newcontent");
+
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
+    PatchAction action = getAction(patch, "move/to/this/directory/move.me");
+    assertTrue(action instanceof UpdateAction);
+    UpdateAction update = (UpdateAction)action;
+    assertTrue(update.isMove());
+    assertEquals("a/deleted/file/that/is/a/copy/move.me", update.getSourcePath());
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    assertAppliedAndRevertedCorrectly(patch, preparationResult);
+  }
+
+  @Test
+  public void testMoveCriticalFileByContent() throws IOException, OperationCancelledException {
+    myPatchSpec.setStrict(true);
+    myPatchSpec.setCriticalFiles(Collections.singletonList("a/deleted/file/that/is/a/copy/move.me"));
+
+    FileUtil.writeToFile(new File(myOlderDir, "move/from/this/directory/move.me"), "oldcontent");
+    FileUtil.writeToFile(new File(myOlderDir, "a/deleted/file/that/is/a/copy/move.me"), "newcontent");
+    FileUtil.writeToFile(new File(myNewerDir, "move/to/this/directory/move.me"), "newcontent");
+
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
+    PatchAction action = getAction(patch, "move/to/this/directory/move.me");
+    assertTrue(action instanceof CreateAction);
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    assertAppliedAndRevertedCorrectly(patch, preparationResult);
+  }
+
+  @Test
+  public void testDontMoveFromDirectoryToFile() throws IOException, OperationCancelledException {
+    myPatchSpec.setStrict(true);
+    new File(myOlderDir, "from/move.me").mkdirs();
+    FileUtil.writeToFile(new File(myNewerDir, "move/to/move.me"), "different");
+
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
+    // Creating a patch would have crashed if the directory had been chosen.
+    PatchAction action = getAction(patch, "move/to/move.me");
+    assertTrue(action instanceof CreateAction);
+    action = getAction(patch, "from/move.me/");
+    assertTrue(action instanceof DeleteAction);
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    assertEquals(0, preparationResult.validationResults.size());
+    assertAppliedAndRevertedCorrectly(patch, preparationResult);
+  }
+
+  @Test
+  public void testMoveFileByLocation() throws IOException, OperationCancelledException {
+    myPatchSpec.setStrict(true);
+    FileUtil.writeToFile(new File(myOlderDir, "move/from/this/directory/move.me"), "they");
+    FileUtil.writeToFile(new File(myOlderDir, "not/from/this/one/move.me"), "are");
+    FileUtil.writeToFile(new File(myNewerDir, "move/to/this/directory/move.me"), "different");
+
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
+    PatchAction action = getAction(patch, "move/to/this/directory/move.me");
+    assertTrue(action instanceof UpdateAction);
+    UpdateAction update = (UpdateAction)action;
+    assertTrue(!update.isMove());
+    assertEquals("move/from/this/directory/move.me", update.getSourcePath());
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    assertAppliedAndRevertedCorrectly(patch, preparationResult);
+  }
+
+  protected PatchAction getAction(Patch patch, String path) {
+    for (PatchAction action : patch.getActions()) {
+      if (action.getPath().equals(path)) {
+        return action;
+      }
+    }
+    return null;
+  }
+
+  protected Patch createPatch() throws IOException, OperationCancelledException {
+    Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
     assertTrue(myFile.exists());
+    return patch;
   }
 
-  private void assertNothingHasChanged(PatchFileCreator.PreparationResult preparationResult, Map<String, ValidationResult.Option> options)
+  private void assertNothingHasChanged(Patch patch, PatchFileCreator.PreparationResult preparationResult, Map<String, ValidationResult.Option> options)
     throws Exception {
-    Map<String, Long> before = Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI);
+    Map<String, Long> before = patch.digestFiles(myOlderDir, Collections.<String>emptyList(), false, TEST_UI);
     PatchFileCreator.apply(preparationResult, options, TEST_UI);
-    Map<String, Long> after = Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI);
+    Map<String, Long> after = patch.digestFiles(myOlderDir, Collections.<String>emptyList(), false, TEST_UI);
 
-    DiffCalculator.Result diff = DiffCalculator.calculate(before, after);
+    DiffCalculator.Result diff = DiffCalculator.calculate(before, after, new LinkedList<String>(), false);
     assertTrue(diff.filesToCreate.isEmpty());
     assertTrue(diff.filesToDelete.isEmpty());
     assertTrue(diff.filesToUpdate.isEmpty());
   }
 
-  private void assertAppliedAndRevertedCorrectly(PatchFileCreator.PreparationResult preparationResult)
+  private void assertAppliedAndRevertedCorrectly(Patch patch, PatchFileCreator.PreparationResult preparationResult)
     throws IOException, OperationCancelledException {
 
-    Map<String, Long> original = Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI);
-
+    Map<String, Long> original = patch.digestFiles(myOlderDir, Collections.<String>emptyList(), false, TEST_UI);
+    Map<String, Long> target = patch.digestFiles(myNewerDir, Collections.<String>emptyList(), false, TEST_UI);
     File backup = getTempFile("backup");
 
+    HashMap<String, ValidationResult.Option> options = new HashMap<String, ValidationResult.Option>();
     for (ValidationResult each : preparationResult.validationResults) {
-      assertTrue(each.toString(), each.kind != ValidationResult.Kind.ERROR);
+      if (patch.isStrict()) {
+        assertFalse(each.options.contains(ValidationResult.Option.NONE));
+        assertTrue(each.options.size() > 0);
+        options.put(each.path, each.options.get(0));
+      } else {
+        assertTrue(each.toString(), each.kind != ValidationResult.Kind.ERROR);
+      }
     }
 
     List<PatchAction> appliedActions =
-      PatchFileCreator.apply(preparationResult, new HashMap<String, ValidationResult.Option>(), backup, TEST_UI).appliedActions;
-    assertAppliedCorrectly();
+      PatchFileCreator.apply(preparationResult, options, backup, TEST_UI).appliedActions;
+    Map<String, Long> patched = patch.digestFiles(myOlderDir, Collections.<String>emptyList(), false, TEST_UI);
+
+    if (patch.isStrict()) {
+      assertEquals(patched, target);
+    } else {
+      assertAppliedCorrectly();
+    }
 
-    assertFalse(original.equals(Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI)));
+    assertNotEquals(original, patched);
 
     PatchFileCreator.revert(preparationResult, appliedActions, backup, TEST_UI);
-
-    assertEquals(original, Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI));
+    Map<String, Long> reverted = patch.digestFiles(myOlderDir, Collections.<String>emptyList(), false, TEST_UI);
+    assertEquals(original, reverted);
   }
 
   protected void assertAppliedCorrectly() throws IOException {
@@ -333,8 +540,11 @@ public class PatchFileCreatorTest extends PatchTestCase {
   }
 
   private static class MyFailOnApplyPatchAction extends PatchAction {
-    public MyFailOnApplyPatchAction() {
-      super("_dummy_file_", -1);
+    // Only used on patch creation
+    protected transient File myOlderDir;
+
+    public MyFailOnApplyPatchAction(Patch patch) {
+      super(patch, "_dummy_file_", Digester.INVALID);
     }
 
     @Override
@@ -348,12 +558,12 @@ public class PatchFileCreatorTest extends PatchTestCase {
     }
 
     @Override
-    protected ValidationResult doValidate(File toFile) throws IOException {
+    protected ValidationResult validate(File toDir) throws IOException {
       throw new UnsupportedOperationException();
     }
 
     @Override
-    protected void doApply(ZipFile patchFile, File toFile) throws IOException {
+    protected void doApply(ZipFile patchFile, File backupDir, File toFile) throws IOException {
       throw new IOException("dummy exception");
     }
 
index b64ad9f514068381fa64ac2969d00550d1abbef7..751ce733ef8441070fa76d075de696c349a1452e 100644 (file)
@@ -20,26 +20,34 @@ public class PatchTest extends PatchTestCase {
   @Before
   public void setUp() throws Exception {
     super.setUp();
-    myPatch = new Patch(myOlderDir, myNewerDir, Collections.<String>emptyList(), Collections.<String>emptyList(),
-                        Collections.<String>emptyList(), TEST_UI);
+    PatchSpec spec = new PatchSpec()
+      .setOldFolder(myOlderDir.getAbsolutePath())
+      .setNewFolder(myNewerDir.getAbsolutePath());
+    myPatch = new Patch(spec, TEST_UI);
+  }
+
+  @Test
+  public void testDigestFiles() throws Exception {
+    Map<String, Long> checkSums = myPatch.digestFiles(getDataDir(), Collections.<String>emptyList(), false, TEST_UI);
+    assertEquals(9, checkSums.size());
   }
 
   @Test
   public void testBasics() throws Exception {
     List<PatchAction> expectedActions = Arrays.asList(
-      new CreateAction("newDir/newFile.txt"),
-      new UpdateAction("Readme.txt", CHECKSUMS.README_TXT),
-      new UpdateZipAction("lib/annotations.jar",
+      new CreateAction(myPatch, "newDir/newFile.txt"),
+      new UpdateAction(myPatch, "Readme.txt", CHECKSUMS.README_TXT),
+      new UpdateZipAction(myPatch, "lib/annotations.jar",
                           Arrays.asList("org/jetbrains/annotations/NewClass.class"),
                           Arrays.asList("org/jetbrains/annotations/Nullable.class"),
                           Arrays.asList("org/jetbrains/annotations/TestOnly.class"),
                           CHECKSUMS.ANNOTATIONS_JAR),
-      new UpdateZipAction("lib/bootstrap.jar",
+      new UpdateZipAction(myPatch, "lib/bootstrap.jar",
                           Collections.<String>emptyList(),
                           Collections.<String>emptyList(),
                           Arrays.asList("com/intellij/ide/ClassloaderUtil.class"),
                           CHECKSUMS.BOOTSTRAP_JAR),
-      new DeleteAction("bin/idea.bat", CHECKSUMS.IDEA_BAT));
+      new DeleteAction(myPatch, "bin/idea.bat", CHECKSUMS.IDEA_BAT));
     List<PatchAction> actualActions = new ArrayList<PatchAction>(myPatch.getActions());
     Collections.sort(expectedActions, COMPARATOR);
     Collections.sort(actualActions, COMPARATOR);
@@ -48,21 +56,21 @@ public class PatchTest extends PatchTestCase {
 
   @Test
   public void testCreatingWithIgnoredFiles() throws Exception {
-    myPatch = new Patch(myOlderDir,
-                        myNewerDir,
-                        Arrays.asList("Readme.txt", "bin/idea.bat"),
-                        Collections.<String>emptyList(),
-                        Collections.<String>emptyList(),
+    PatchSpec spec = new PatchSpec()
+      .setOldFolder(myOlderDir.getAbsolutePath())
+      .setNewFolder(myNewerDir.getAbsolutePath())
+      .setIgnoredFiles(Arrays.asList("Readme.txt", "bin/idea.bat"));
+    myPatch = new Patch(spec,
                         TEST_UI);
 
     List<PatchAction> expectedActions = Arrays.asList(
-      new CreateAction("newDir/newFile.txt"),
-      new UpdateZipAction("lib/annotations.jar",
+      new CreateAction(myPatch, "newDir/newFile.txt"),
+      new UpdateZipAction(myPatch, "lib/annotations.jar",
                           Arrays.asList("org/jetbrains/annotations/NewClass.class"),
                           Arrays.asList("org/jetbrains/annotations/Nullable.class"),
                           Arrays.asList("org/jetbrains/annotations/TestOnly.class"),
                           CHECKSUMS.ANNOTATIONS_JAR),
-      new UpdateZipAction("lib/bootstrap.jar",
+      new UpdateZipAction(myPatch, "lib/bootstrap.jar",
                           Collections.<String>emptyList(),
                           Collections.<String>emptyList(),
                           Arrays.asList("com/intellij/ide/ClassloaderUtil.class"),
@@ -125,8 +133,11 @@ public class PatchTest extends PatchTestCase {
                            ValidationResult.Option.IGNORE))),
                  new HashSet<ValidationResult>(myPatch.validate(myOlderDir, TEST_UI)));
 
-    myPatch = new Patch(myOlderDir, myNewerDir, Collections.<String>emptyList(), Collections.<String>emptyList(),
-                        Arrays.asList("lib/annotations.jar"), TEST_UI);
+    PatchSpec spec = new PatchSpec()
+      .setOldFolder(myOlderDir.getAbsolutePath())
+      .setNewFolder(myNewerDir.getAbsolutePath())
+      .setOptionalFiles(Arrays.asList("lib/annotations.jar"));
+    myPatch = new Patch(spec, TEST_UI);
     FileUtil.delete(new File(myOlderDir, "lib/annotations.jar"));
     assertEquals(Collections.<ValidationResult>emptyList(),
                  myPatch.validate(myOlderDir, TEST_UI));
index 22464a0fa053c1e9fe0764509d6b5a91a1ca834d..9d11618de4530c6b9371640dae9b1b458c634816 100644 (file)
@@ -14,12 +14,12 @@ public class RunnerTest extends UpdaterTestCase {
     Runner.initLogger();
 
     assertEquals(Arrays.asList("xxx", "yyy", "zzz/zzz", "aaa"),
-                 Runner.extractFiles(args, "ignored"));
+                 Runner.extractArguments(args, "ignored"));
 
     assertEquals(Arrays.asList("ccc"),
-                 Runner.extractFiles(args, "critical"));
+                 Runner.extractArguments(args, "critical"));
 
     assertEquals(Collections.<String>emptyList(),
-                 Runner.extractFiles(args, "unknown"));
+                 Runner.extractArguments(args, "unknown"));
   }
 }
index dea5fbdd7c6de1795b6e685dfbff6567af53d122..075e17b6a181c27106578cf1806d1653c3ac135b 100644 (file)
@@ -18,6 +18,15 @@ public abstract class UpdaterTestCase {
     @Override
     public void setStatus(String status) {
     }
+
+    @Override
+    public void setDescription(String oldBuildDesc, String newBuildDesc) {
+    }
+
+    @Override
+    public boolean showWarning(String message) {
+      return false;
+    }
   };
 
   protected CheckSums CHECKSUMS;
@@ -54,7 +63,16 @@ public abstract class UpdaterTestCase {
     public final long IDEA_BAT;
     public final long ANNOTATIONS_JAR;
     public final long BOOTSTRAP_JAR;
+    public final long BOOTSTRAP_JAR_BINARY;
     public final long FOCUSKILLER_DLL;
+    public final long ANNOTATIONS_JAR_NORM;
+    public final long ANNOTATIONS_CHANGED_JAR_NORM;
+    public final long BOOT_JAR_NORM;
+    public final long BOOT2_JAR_NORM;
+    public final long BOOT2_CHANGED_WITH_UNCHANGED_CONTENT_JAR_NORM;
+    public final long BOOT_WITH_DIRECTORY_BECOMES_FILE_JAR_NORM;
+    public final long BOOTSTRAP_JAR_NORM;
+    public final long BOOTSTRAP_DELETED_JAR_NORM;
 
     public CheckSums(boolean windowsLineEnds) {
       if (windowsLineEnds) {
@@ -68,6 +86,16 @@ public abstract class UpdaterTestCase {
       ANNOTATIONS_JAR = 2119442657L;
       BOOTSTRAP_JAR = 2082851308L;
       FOCUSKILLER_DLL = 1991212227L;
+      BOOTSTRAP_JAR_BINARY = 2745721972L;
+
+      ANNOTATIONS_JAR_NORM = 2119442657L;
+      ANNOTATIONS_CHANGED_JAR_NORM = 4088078858L;
+      BOOT_JAR_NORM = 3018038682L;
+      BOOT2_JAR_NORM = 2406818996L;
+      BOOT2_CHANGED_WITH_UNCHANGED_CONTENT_JAR_NORM = 2406818996L;
+      BOOT_WITH_DIRECTORY_BECOMES_FILE_JAR_NORM = 1972168924;
+      BOOTSTRAP_JAR_NORM = 2082851308;
+      BOOTSTRAP_DELETED_JAR_NORM = 544883981L;
     }
   }
 }
\ No newline at end of file