Fixes mishandling of deleted and renamed dirs.

I wish I could have used c9a992d5469b579065a8d6d5b215c794338a725b
as it uses much less resources and doesn't have the bug.
However last time it was causing some issues with external customers
that we couldn't reproduce.

Change-Id: Ib362cfc63f5075ad5f923516ea90216a321cba72
Review-Link: https://gwt-review.googlesource.com/#/c/13924/
diff --git a/dev/core/src/com/google/gwt/dev/resource/impl/ChangedFileAccumulator.java b/dev/core/src/com/google/gwt/dev/resource/impl/ChangedFileAccumulator.java
index d788d7e..6341c1f 100644
--- a/dev/core/src/com/google/gwt/dev/resource/impl/ChangedFileAccumulator.java
+++ b/dev/core/src/com/google/gwt/dev/resource/impl/ChangedFileAccumulator.java
@@ -13,8 +13,11 @@
  */
 package com.google.gwt.dev.resource.impl;
 
+import com.google.gwt.thirdparty.guava.common.collect.BiMap;
+import com.google.gwt.thirdparty.guava.common.collect.HashBiMap;
+import com.google.gwt.thirdparty.guava.common.collect.HashMultimap;
 import com.google.gwt.thirdparty.guava.common.collect.Lists;
-import com.google.gwt.thirdparty.guava.common.collect.Maps;
+import com.google.gwt.thirdparty.guava.common.collect.Multimap;
 import com.google.gwt.thirdparty.guava.common.collect.Sets;
 
 import java.io.File;
@@ -32,7 +35,6 @@
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 
@@ -86,6 +88,11 @@
               Path changedFileName = (Path) watchEvent.context();
               Path changedPath = containingDirectory.resolve(changedFileName);
 
+              if (eventKind == StandardWatchEventKinds.ENTRY_DELETE) {
+                recursivelyRemovePath(changedPath);
+                continue;
+              }
+
               // Maybe listen to newly created directories.
               if (eventKind == StandardWatchEventKinds.ENTRY_CREATE
                   && Files.isDirectory(changedPath)) {
@@ -102,6 +109,7 @@
               }
 
               // Record changed files.
+              pathsByParentPath.put(changedPath.getParent(), changedPath);
               changedFiles.add(changedPath.toFile().getAbsoluteFile());
             }
 
@@ -120,7 +128,8 @@
   private final Set<File> changedFiles = Collections.synchronizedSet(Sets.<File> newHashSet());
   private Thread changePollerThread;
   private Exception changePollerException;
-  private final Map<WatchKey, Path> pathsByWatchKey = Maps.newHashMap();
+  private final BiMap<WatchKey, Path> pathsByWatchKey = HashBiMap.create();
+  private final Multimap<Path, Path> pathsByParentPath = HashMultimap.create();
   private final WatchService watchService;
 
   public ChangedFileAccumulator(Path rootDirectory) throws IOException {
@@ -177,6 +186,10 @@
           return FileVisitResult.SKIP_SUBTREE;
         }
 
+        if (considerPreexistingFilesChanged) {
+          changedFiles.add(currentDirectory.toFile());
+        }
+        pathsByParentPath.put(currentDirectory.getParent(), currentDirectory);
         pathsByWatchKey.put(watchKey, currentDirectory);
         return FileVisitResult.CONTINUE;
       }
@@ -191,11 +204,27 @@
         if (considerPreexistingFilesChanged) {
           changedFiles.add(file.toFile());
         }
+        pathsByParentPath.put(file.getParent(), file);
         return FileVisitResult.CONTINUE;
       }
     });
   }
 
+  private void recursivelyRemovePath(final Path path) {
+    changedFiles.add(path.toFile());
+
+    // Stop listening to this path
+    WatchKey removed = pathsByWatchKey.inverse().remove(path);
+    if (removed != null) {
+       removed.cancel();
+    }
+
+    // Remove the path and recurse into sub directories and files.
+    for (Path child : pathsByParentPath.removeAll(path)) {
+      recursivelyRemovePath(child);
+    }
+  }
+
   private void setFailed(Exception caughtException) {
     this.changePollerException = caughtException;
     shutdown();
diff --git a/dev/core/test/com/google/gwt/dev/resource/impl/ChangedFileAccumulatorTest.java b/dev/core/test/com/google/gwt/dev/resource/impl/ChangedFileAccumulatorTest.java
index 6900f90..2c395c6 100644
--- a/dev/core/test/com/google/gwt/dev/resource/impl/ChangedFileAccumulatorTest.java
+++ b/dev/core/test/com/google/gwt/dev/resource/impl/ChangedFileAccumulatorTest.java
@@ -20,14 +20,13 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.List;
-import java.util.concurrent.ExecutionException;
 
 /**
  * Tests for ChangedFileAccumulator.
  */
 public class ChangedFileAccumulatorTest extends TestCase {
 
-  public void testAddFile() throws IOException, InterruptedException, ExecutionException {
+  public void testAddFile() throws Exception {
     File rootDirectory = Files.createTempDir();
     File subDirectory = createDirectoryIn("subdir", rootDirectory);
 
@@ -46,7 +45,7 @@
     changedFileAccumulator.shutdown();
   }
 
-  public void testDeleteFile() throws IOException, InterruptedException, ExecutionException {
+  public void testDeleteFile() throws Exception {
     File rootDirectory = Files.createTempDir();
     File subDirectory = createDirectoryIn("subdir", rootDirectory);
     File originalFile = createFileIn("SomeFile.java", subDirectory);
@@ -66,8 +65,7 @@
     changedFileAccumulator.shutdown();
   }
 
-  public void testListensInNewDirectories() throws IOException, InterruptedException,
-      ExecutionException {
+  public void testListensInNewDirectories() throws Exception {
     File rootDirectory = Files.createTempDir();
 
     ChangedFileAccumulator changedFileAccumulator =
@@ -89,7 +87,7 @@
     changedFileAccumulator.shutdown();
   }
 
-  public void testModifyRepeatedly() throws IOException, InterruptedException, ExecutionException {
+  public void testModifyRepeatedly() throws Exception {
     File rootDirectory = Files.createTempDir();
     File fooFile = createFileIn("Foo.java", rootDirectory);
 
@@ -109,7 +107,7 @@
     changedFileAccumulator.shutdown();
   }
 
-  public void testMultipleListeners() throws IOException, InterruptedException, ExecutionException {
+  public void testMultipleListeners() throws Exception {
     File rootDirectory = Files.createTempDir();
     File subDirectory = createDirectoryIn("subdir", rootDirectory);
 
@@ -136,7 +134,7 @@
     changedFileAccumulator2.shutdown();
   }
 
-  public void testRenameFile() throws IOException, InterruptedException, ExecutionException {
+  public void testRenameFile() throws Exception {
     File rootDirectory = Files.createTempDir();
     File subDirectory = createDirectoryIn("subdir", rootDirectory);
     File originalFile = createFileIn("OriginalName.java", subDirectory);
@@ -158,8 +156,64 @@
     changedFileAccumulator.shutdown();
   }
 
-  public void testSymlinkInfiniteLoop() throws IOException, InterruptedException,
-      ExecutionException {
+  public void testRenameDirectory() throws Exception {
+    File rootDirectory = Files.createTempDir();
+    File subDirectory = createDirectoryIn("original_dir", rootDirectory);
+    createFileIn("Name1.java", subDirectory);
+    createFileIn("Name2.java", subDirectory);
+    File renamedSubDirectory = new File(rootDirectory, "new_dir");
+
+    ChangedFileAccumulator changedFileAccumulator =
+        new ChangedFileAccumulator(rootDirectory.toPath());
+
+    assertTrue(changedFileAccumulator.getAndClearChangedFiles().isEmpty());
+
+    subDirectory.renameTo(renamedSubDirectory);
+    waitForFileEvents();
+
+    List<File> modifiedFiles = changedFileAccumulator.getAndClearChangedFiles();
+    assertEquals(6, modifiedFiles.size());
+    assertTrue(modifiedFiles.get(0).getPath().endsWith("new_dir"));
+    assertTrue(modifiedFiles.get(1).getPath().endsWith("new_dir/Name1.java"));
+    assertTrue(modifiedFiles.get(2).getPath().endsWith("new_dir/Name2.java"));
+    assertTrue(modifiedFiles.get(3).getPath().endsWith("original_dir"));
+    assertTrue(modifiedFiles.get(4).getPath().endsWith("original_dir/Name1.java"));
+    assertTrue(modifiedFiles.get(5).getPath().endsWith("original_dir/Name2.java"));
+
+    changedFileAccumulator.shutdown();
+  }
+
+  public void testRenameParentDirectory() throws Exception {
+    File rootDirectory = Files.createTempDir();
+    File parentDirectory = createDirectoryIn("original_dir", rootDirectory);
+    File subDirectory = createDirectoryIn("subdir", parentDirectory);
+    createFileIn("Name1.java", subDirectory);
+    createFileIn("Name2.java", subDirectory);
+    File renamedParentDirectory = new File(rootDirectory, "new_dir");
+
+    ChangedFileAccumulator changedFileAccumulator =
+        new ChangedFileAccumulator(rootDirectory.toPath());
+
+    assertTrue(changedFileAccumulator.getAndClearChangedFiles().isEmpty());
+
+    parentDirectory.renameTo(renamedParentDirectory);
+    waitForFileEvents();
+
+    List<File> modifiedFiles = changedFileAccumulator.getAndClearChangedFiles();
+    assertEquals(8, modifiedFiles.size());
+    assertTrue(modifiedFiles.get(0).getPath().endsWith("new_dir"));
+    assertTrue(modifiedFiles.get(1).getPath().endsWith("new_dir/subdir"));
+    assertTrue(modifiedFiles.get(2).getPath().endsWith("new_dir/subdir/Name1.java"));
+    assertTrue(modifiedFiles.get(3).getPath().endsWith("new_dir/subdir/Name2.java"));
+    assertTrue(modifiedFiles.get(4).getPath().endsWith("original_dir"));
+    assertTrue(modifiedFiles.get(5).getPath().endsWith("original_dir/subdir"));
+    assertTrue(modifiedFiles.get(6).getPath().endsWith("original_dir/subdir/Name1.java"));
+    assertTrue(modifiedFiles.get(7).getPath().endsWith("original_dir/subdir/Name2.java"));
+
+    changedFileAccumulator.shutdown();
+  }
+
+  public void testSymlinkInfiniteLoop() throws Exception {
     File rootDirectory = Files.createTempDir();
     File subDirectory = Files.createTempDir();
 
@@ -178,14 +232,15 @@
 
     // Will throw an error if ChangedFileAccumulator got stuck in an infinite directory scan loop.
     List<File> modifiedFiles = changedFileAccumulator.getAndClearChangedFiles();
-    assertEquals(2, modifiedFiles.size());
+    assertEquals(3, modifiedFiles.size());
     assertTrue(modifiedFiles.get(0).getPath().endsWith("sublink"));
-    assertTrue(modifiedFiles.get(1).getPath().endsWith("New.java"));
+    assertEquals(modifiedFiles.get(1).getPath(), subDirectory.getPath());
+    assertTrue(modifiedFiles.get(2).getPath().endsWith("New.java"));
 
     changedFileAccumulator.shutdown();
   }
 
-  public void testSymlinks() throws IOException, InterruptedException, ExecutionException {
+  public void testSymlinks() throws Exception {
     File scratchDirectory = Files.createTempDir();
     File newFile = createFileIn("New.java", scratchDirectory);
     File rootDirectory = Files.createTempDir();
@@ -204,20 +259,22 @@
     waitForFileEvents();
 
     List<File> modifiedFiles = changedFileAccumulator.getAndClearChangedFiles();
-    assertEquals(2, modifiedFiles.size());
+
+    assertEquals(3, modifiedFiles.size());
     assertTrue(modifiedFiles.get(0).getPath().endsWith("New.java"));
     assertTrue(modifiedFiles.get(1).getPath().endsWith("sublink"));
+    assertEquals(modifiedFiles.get(2).getPath(), subDirectory.getPath());
 
     changedFileAccumulator.shutdown();
   }
 
-  private File createDirectoryIn(String fileName, File inDirectory) {
+  private static File createDirectoryIn(String fileName, File inDirectory) {
     File newDirectory = new File(inDirectory, fileName);
     newDirectory.mkdir();
     return newDirectory;
   }
 
-  private File createFileIn(String fileName, File inDirectory) throws IOException {
+  private static File createFileIn(String fileName, File inDirectory) throws IOException {
     File newFile = new File(inDirectory, fileName);
     newFile.createNewFile();
     return newFile;