Convert plugin loading code to use Path

All of the internal scanning/loading code now uses Path, as do the
plugin and data directories in SitePaths. For backwards compatibility,
we still inject the @PluginData directory as a File. We do break
backwards compatibility of the ServerPluginProvider extension point,
under the theory that there should be very few such extensions and
they can handle the pain.

Tested:
-Installed and reloaded cookbook plugin multiple times.
-Expanded dynamic *.ssh plugin loading in the cookbook, with
 monkey-testing.

Change-Id: I5212d248d9288b88d08731c9854b42196999aa73
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index a193df7..a672b177 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1717,17 +1717,18 @@
 [[data-directory]]
 == Data Directory
 
-Plugins can request a data directory with a `@PluginData` File
-dependency. A data directory will be created automatically by the
-server in `$site_path/data/$plugin_name` and passed to the plugin.
+Plugins can request a data directory with a `@PluginData` Path (or File,
+deprecated) dependency. A data directory will be created automatically
+by the server in `$site_path/data/$plugin_name` and passed to the
+plugin.
 
 Plugins can use this to store any data they want.
 
 [source,java]
 ----
 @Inject
-MyType(@PluginData java.io.File myDir) {
-  new FileInputStream(new File(myDir, "my.config"));
+MyType(@PluginData java.nio.file.Path myDir) {
+  this.in = Files.newInputStream(myDir.resolve("my.config"));
 }
 ----
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java b/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
index ffdae9d..27dc639 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
@@ -14,18 +14,18 @@
 
 package com.google.gerrit.common;
 
-import java.io.File;
+import java.nio.file.Path;
 import java.util.Objects;
 
 public class PluginData {
   public final String name;
   public final String version;
-  public final File pluginFile;
+  public final Path pluginPath;
 
-  public PluginData(String name, String version, File pluginFile) {
+  public PluginData(String name, String version, Path pluginPath) {
     this.name = name;
     this.version = version;
-    this.pluginFile = pluginFile;
+    this.pluginPath = pluginPath;
   }
 
   @Override
@@ -33,13 +33,13 @@
     if (obj instanceof PluginData) {
       PluginData o = (PluginData) obj;
       return Objects.equals(name, o.name) && Objects.equals(version, o.version)
-          && Objects.equals(pluginFile, o.pluginFile);
+          && Objects.equals(pluginPath, o.pluginPath);
     }
     return super.equals(obj);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(name, version, pluginFile);
+    return Objects.hash(name, version, pluginPath);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java
index 75238a8..1bcb5ab 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java
@@ -27,11 +27,15 @@
  * <p>
  * A plugin or extension may receive this string by Guice injection to discover
  * a directory where it can store configuration or other data that is private:
+ * <p>
+ * This binding is on both {@link java.io.File} and {@link java.nio.file.Path},
+ * pointing to the same location. The {@code File} version should be considered
+ * deprecated and may be removed in a future version.
  *
  * <pre>
  * {@literal @Inject}
- * MyType(@PluginData java.io.File myDir) {
- *   new FileInputStream(new File(myDir, &quot;my.config&quot;));
+ * MyType(@PluginData java.nio.file.Path myDir) {
+ *   this.in = Files.newInputStream(myDir.resolve(&quot;my.config&quot;));
  * }
  * </pre>
  */
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
index 893f00d..2cffe42 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.pgm.init;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.Ordering;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitStep;
@@ -26,23 +27,22 @@
 import com.google.inject.Injector;
 import com.google.inject.Singleton;
 
-import java.io.File;
-import java.io.FileFilter;
 import java.io.IOException;
 import java.net.URL;
 import java.net.URLClassLoader;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
 
 @Singleton
 public class InitPluginStepsLoader {
-  private final File pluginsDir;
+  private final Path pluginsDir;
   private final Injector initInjector;
   final ConsoleUI ui;
 
@@ -55,10 +55,10 @@
   }
 
   public Collection<InitStep> getInitSteps() {
-    List<File> jars = scanJarsInPluginsDirectory();
+    List<Path> jars = scanJarsInPluginsDirectory();
     ArrayList<InitStep> pluginsInitSteps = new ArrayList<>();
 
-    for (File jar : jars) {
+    for (Path jar : jars) {
       InitStep init = loadInitStep(jar);
       if (init != null) {
         pluginsInitSteps.add(init);
@@ -68,12 +68,12 @@
   }
 
   @SuppressWarnings("resource")
-  private InitStep loadInitStep(File jar) {
+  private InitStep loadInitStep(Path jar) {
     try {
       URLClassLoader pluginLoader =
-          new URLClassLoader(new URL[] {jar.toURI().toURL()},
+          new URLClassLoader(new URL[] {jar.toUri().toURL()},
              InitPluginStepsLoader.class.getClassLoader());
-      try (JarFile jarFile = new JarFile(jar)) {
+      try (JarFile jarFile = new JarFile(jar.toFile())) {
         Attributes jarFileAttributes = jarFile.getManifest().getMainAttributes();
         String initClassName = jarFileAttributes.getValue("Gerrit-InitStep");
         if (initClassName == null) {
@@ -86,7 +86,7 @@
       } catch (ClassCastException e) {
         ui.message(
             "WARN: InitStep from plugin %s does not implement %s (Exception: %s)",
-            jar.getName(), InitStep.class.getName(), e.getMessage());
+            jar.getFileName(), InitStep.class.getName(), e.getMessage());
         return null;
       }
     } catch (Exception e) {
@@ -97,11 +97,10 @@
     }
   }
 
-  private Injector getPluginInjector(final File jarFile) throws IOException {
-    final String pluginName =
-        MoreObjects.firstNonNull(
-            JarPluginProvider.getJarPluginName(jarFile),
-            PluginLoader.nameOf(jarFile));
+  private Injector getPluginInjector(Path jarPath) throws IOException {
+    final String pluginName = MoreObjects.firstNonNull(
+        JarPluginProvider.getJarPluginName(jarPath),
+        PluginLoader.nameOf(jarPath));
     return initInjector.createChildInjector(new AbstractModule() {
       @Override
       protected void configure() {
@@ -111,27 +110,24 @@
     });
   }
 
-  private List<File> scanJarsInPluginsDirectory() {
-    if (pluginsDir == null || !pluginsDir.exists()) {
+  private List<Path> scanJarsInPluginsDirectory() {
+    if (pluginsDir == null || !Files.isDirectory(pluginsDir)) {
       return Collections.emptyList();
     }
-    File[] matches = pluginsDir.listFiles(new FileFilter() {
+    DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
       @Override
-      public boolean accept(File pathname) {
-        String n = pathname.getName();
-        return (n.endsWith(".jar") && pathname.isFile());
+      public boolean accept(Path entry) throws IOException {
+        return entry.getFileName().toString().endsWith(".jar")
+            && Files.isRegularFile(entry);
       }
-    });
-    if (matches == null) {
-      ui.message("WARN: Cannot list %s", pluginsDir.getAbsolutePath());
+    };
+    try (DirectoryStream<Path> paths =
+        Files.newDirectoryStream(pluginsDir, filter)) {
+      return Ordering.natural().sortedCopy(paths);
+    } catch (IOException e) {
+      ui.message("WARN: Cannot list %s: %s", pluginsDir.toAbsolutePath(),
+          e.getMessage());
       return Collections.emptyList();
     }
-    Arrays.sort(matches, new Comparator<File>() {
-      @Override
-      public int compare(File o1, File o2) {
-        return o1.getName().compareTo(o2.getName());
-      }
-    });
-    return Arrays.asList(matches);
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
index cc076b5..ca4b949 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
@@ -25,9 +25,10 @@
 import com.google.inject.Injector;
 import com.google.inject.Singleton;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
@@ -55,10 +56,10 @@
     pluginsDistribution.foreach(new PluginsDistribution.Processor() {
       @Override
       public void process(String pluginName, InputStream in) throws IOException {
-        File tmpPlugin = JarPluginProvider.storeInTemp(pluginName, in, site);
+        Path tmpPlugin = JarPluginProvider.storeInTemp(pluginName, in, site);
         String pluginVersion = getVersion(tmpPlugin);
         if (deleteTempPluginFile) {
-          tmpPlugin.delete();
+          Files.delete(tmpPlugin);
         }
         result.add(new PluginData(pluginName, pluginVersion, tmpPlugin));
       }
@@ -108,37 +109,39 @@
     for (PluginData plugin : plugins) {
       String pluginName = plugin.name;
       try {
-        final File tmpPlugin = plugin.pluginFile;
+        final Path tmpPlugin = plugin.pluginPath;
 
         if (!(initFlags.installPlugins.contains(pluginName) || ui.yesno(false,
             "Install plugin %s version %s", pluginName, plugin.version))) {
-          tmpPlugin.delete();
+          Files.deleteIfExists(tmpPlugin);
           continue;
         }
 
-        final File p = new File(site.plugins_dir, plugin.name + ".jar");
-        if (p.exists()) {
+        final Path p = site.plugins_dir.resolve(plugin.name + ".jar");
+        if (Files.exists(p)) {
           final String installedPluginVersion = getVersion(p);
           if (!ui.yesno(false,
               "version %s is already installed, overwrite it",
               installedPluginVersion)) {
-            tmpPlugin.delete();
+            Files.deleteIfExists(tmpPlugin);
             continue;
           }
-          if (!p.delete()) {
+          try {
+            Files.delete(p);
+          } catch (IOException e) {
             throw new IOException("Failed to delete plugin " + pluginName
-                + ": " + p.getAbsolutePath());
+                + ": " + p.toAbsolutePath(), e);
           }
         }
-        if (!tmpPlugin.renameTo(p)) {
+        try {
+          Files.move(tmpPlugin, p);
+        } catch (IOException e) {
           throw new IOException("Failed to install plugin " + pluginName
-              + ": " + tmpPlugin.getAbsolutePath() + " -> "
-              + p.getAbsolutePath());
+              + ": " + tmpPlugin.toAbsolutePath() + " -> "
+              + p.toAbsolutePath(), e);
         }
       } finally {
-        if (plugin.pluginFile.exists()) {
-          plugin.pluginFile.delete();
-        }
+        Files.deleteIfExists(plugin.pluginPath);
       }
     }
     if (plugins.isEmpty()) {
@@ -159,11 +162,11 @@
     }
   }
 
-  private static String getVersion(final File plugin) throws IOException {
-    final JarFile jarFile = new JarFile(plugin);
+  private static String getVersion(Path plugin) throws IOException {
+    JarFile jarFile = new JarFile(plugin.toFile());
     try {
-      final Manifest manifest = jarFile.getManifest();
-      final Attributes main = manifest.getMainAttributes();
+      Manifest manifest = jarFile.getManifest();
+      Attributes main = manifest.getMainAttributes();
       return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
     } finally {
       jarFile.close();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index a3e7924..5cfcc04 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -35,8 +35,8 @@
   public final Path lib_dir;
   public final Path tmp_dir;
   public final File logs_dir;
-  public final File plugins_dir;
-  public final File data_dir;
+  public final Path plugins_dir;
+  public final Path data_dir;
   public final File mail_dir;
   public final File hooks_dir;
   public final File static_dir;
@@ -74,8 +74,8 @@
     etc_dir = p.resolve("etc");
     lib_dir = p.resolve("lib");
     tmp_dir = p.resolve("tmp");
-    plugins_dir = new File(site_path, "plugins");
-    data_dir = new File(site_path, "data");
+    plugins_dir = p.resolve("plugins");
+    data_dir = p.resolve("data");
     logs_dir = new File(site_path, "logs");
     mail_dir = etc_dir.resolve("mail").toFile();
     hooks_dir = new File(site_path, "hooks");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
index 593f2c9..9827812 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
@@ -14,17 +14,17 @@
 
 package com.google.gerrit.server.plugins;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.jar.JarFile;
 
 class CleanupHandle {
-  private final File tmpFile;
+  private final Path tmp;
   private final JarFile jarFile;
 
-  CleanupHandle(File tmpFile,
-      JarFile jarFile) {
-    this.tmpFile = tmpFile;
+  CleanupHandle(Path tmp, JarFile jarFile) {
+    this.tmp = tmp;
     this.jarFile = jarFile;
   }
 
@@ -34,12 +34,13 @@
     } catch (IOException err) {
       PluginLoader.log.error("Cannot close " + jarFile.getName(), err);
     }
-    if (!tmpFile.delete() && tmpFile.exists()) {
-      PluginLoader.log.warn("Cannot delete " + tmpFile.getAbsolutePath()
-          + ", retrying to delete it on termination of the virtual machine");
-      tmpFile.deleteOnExit();
-    } else {
-      PluginLoader.log.info("Cleaned plugin " + tmpFile.getName());
+    try {
+      Files.deleteIfExists(tmp);
+      PluginLoader.log.info("Cleaned plugin " + tmp.getFileName());
+    } catch (IOException e) {
+      PluginLoader.log.warn("Cannot delete " + tmp.toAbsolutePath()
+          + ", retrying to delete it on termination of the virtual machine", e);
+      tmp.toFile().deleteOnExit();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
index 37c05d5..dcfb52c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
@@ -24,8 +24,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
@@ -33,6 +31,7 @@
 import java.net.URLClassLoader;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
@@ -45,52 +44,50 @@
   static final String JAR_EXTENSION = ".jar";
   static final Logger log = LoggerFactory.getLogger(JarPluginProvider.class);
 
-  private final File tmpDir;
+  private final Path tmpDir;
 
   @Inject
   JarPluginProvider(SitePaths sitePaths) {
-    // TODO(dborowitz): Convert to NIO.
-    tmpDir = sitePaths.tmp_dir.toFile();
+    tmpDir = sitePaths.tmp_dir;
   }
 
   @Override
-  public boolean handles(File srcFile) {
-    String fileName = srcFile.getName();
+  public boolean handles(Path srcPath) {
+    String fileName = srcPath.getFileName().toString();
     return fileName.endsWith(JAR_EXTENSION)
         || fileName.endsWith(JAR_EXTENSION + ".disabled");
   }
 
   @Override
-  public String getPluginName(File srcFile) {
+  public String getPluginName(Path srcPath) {
     try {
-      return MoreObjects.firstNonNull(getJarPluginName(srcFile),
-          PluginLoader.nameOf(srcFile));
+      return MoreObjects.firstNonNull(getJarPluginName(srcPath),
+          PluginLoader.nameOf(srcPath));
     } catch (IOException e) {
-      throw new IllegalArgumentException("Invalid plugin file " + srcFile
+      throw new IllegalArgumentException("Invalid plugin file " + srcPath
           + ": cannot get plugin name", e);
     }
   }
 
-  public static String getJarPluginName(File srcFile) throws IOException {
-    try (JarFile jarFile = new JarFile(srcFile)) {
+  public static String getJarPluginName(Path srcPath) throws IOException {
+    try (JarFile jarFile = new JarFile(srcPath.toFile())) {
       return jarFile.getManifest().getMainAttributes()
           .getValue("Gerrit-PluginName");
     }
   }
 
   @Override
-  public ServerPlugin get(File srcFile, FileSnapshot snapshot,
+  public ServerPlugin get(Path srcPath, FileSnapshot snapshot,
       PluginDescription description) throws InvalidPluginException {
     try {
-      String name = getPluginName(srcFile);
-      String extension = getExtension(srcFile);
-      try (FileInputStream in = new FileInputStream(srcFile)) {
-        File tmp = asTemp(in, tempNameFor(name), extension, tmpDir);
-        return loadJarPlugin(name, srcFile.toPath(), snapshot, tmp,
-            description);
+      String name = getPluginName(srcPath);
+      String extension = getExtension(srcPath);
+      try (InputStream in = Files.newInputStream(srcPath)) {
+        Path tmp = asTemp(in, tempNameFor(name), extension, tmpDir);
+        return loadJarPlugin(name, srcPath, snapshot, tmp, description);
       }
     } catch (IOException e) {
-      throw new InvalidPluginException("Cannot load Jar plugin " + srcFile, e);
+      throw new InvalidPluginException("Cannot load Jar plugin " + srcPath, e);
     }
   }
 
@@ -99,8 +96,8 @@
     return "gerrit";
   }
 
-  private static String getExtension(File file) {
-    return getExtension(file.getName());
+  private static String getExtension(Path path) {
+    return getExtension(path.getFileName().toString());
   }
 
   private static String getExtension(String name) {
@@ -113,19 +110,18 @@
     return PLUGIN_TMP_PREFIX + name + "_" + fmt.format(new Date()) + "_";
   }
 
-  public static File storeInTemp(String pluginName, InputStream in,
+  public static Path storeInTemp(String pluginName, InputStream in,
       SitePaths sitePaths) throws IOException {
     if (!Files.exists(sitePaths.tmp_dir)) {
       Files.createDirectories(sitePaths.tmp_dir);
     }
-    return asTemp(in, tempNameFor(pluginName), ".jar",
-        sitePaths.tmp_dir.toFile());
+    return asTemp(in, tempNameFor(pluginName), ".jar", sitePaths.tmp_dir);
   }
 
   private ServerPlugin loadJarPlugin(String name, Path srcJar,
-      FileSnapshot snapshot, File tmp, PluginDescription description)
+      FileSnapshot snapshot, Path tmp, PluginDescription description)
       throws IOException, InvalidPluginException, MalformedURLException {
-    JarFile jarFile = new JarFile(tmp);
+    JarFile jarFile = new JarFile(tmp.toFile());
     boolean keep = false;
     try {
       Manifest manifest = jarFile.getManifest();
@@ -134,14 +130,13 @@
       List<URL> urls = new ArrayList<>(2);
       String overlay = System.getProperty("gerrit.plugin-classes");
       if (overlay != null) {
-        File classes = new File(new File(new File(overlay), name), "main");
-        if (classes.isDirectory()) {
-          log.info(String.format("plugin %s: including %s", name,
-              classes.getPath()));
-          urls.add(classes.toURI().toURL());
+        Path classes = Paths.get(overlay).resolve(name).resolve("main");
+        if (Files.isDirectory(classes)) {
+          log.info(String.format("plugin %s: including %s", name, classes));
+          urls.add(classes.toUri().toURL());
         }
       }
-      urls.add(tmp.toURI().toURL());
+      urls.add(tmp.toUri().toURL());
 
       ClassLoader pluginLoader =
           new URLClassLoader(urls.toArray(new URL[urls.size()]),
@@ -149,7 +144,7 @@
 
       JarScanner jarScanner = createJarScanner(srcJar);
       ServerPlugin plugin = new ServerPlugin(name, description.canonicalUrl,
-          description.user, srcJar.toFile(), snapshot, jarScanner,
+          description.user, srcJar, snapshot, jarScanner,
           description.dataDir, pluginLoader);
       plugin.setCleanupHandle(new CleanupHandle(tmp, jarFile));
       keep = true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java
index 82a6ad9..cf38310 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java
@@ -18,14 +18,14 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.Iterables;
 
-import java.io.File;
+import java.nio.file.Path;
 
 class MultipleProvidersForPluginException extends IllegalArgumentException {
   private static final long serialVersionUID = 1L;
 
-  MultipleProvidersForPluginException(File pluginSrcFile,
+  MultipleProvidersForPluginException(Path pluginSrcPath,
       Iterable<ServerPluginProvider> providersHandlers) {
-    super(pluginSrcFile.getAbsolutePath()
+    super(pluginSrcPath.toAbsolutePath()
         + " is claimed to be handled by more than one plugin provider: "
         + providersListToString(providersHandlers));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
index 1be65aa..6b84c21 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.plugins;
 
+import static com.google.gerrit.common.FileUtil.lastModified;
+
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
@@ -25,7 +27,6 @@
 
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 
-import java.io.File;
 import java.nio.file.Path;
 import java.util.Collections;
 import java.util.List;
@@ -81,18 +82,17 @@
   private List<ReloadableRegistrationHandle<?>> reloadableHandles;
 
   public Plugin(String name,
-      Path srcFile,
+      Path srcPath,
       PluginUser pluginUser,
       FileSnapshot snapshot,
       ApiType apiType) {
     this.name = name;
-    // TODO(dborowitz): Rename to srcPath or something.
-    this.srcFile = srcFile;
+    this.srcFile = srcPath;
     this.apiType = apiType;
     this.snapshot = snapshot;
     this.pluginUser = pluginUser;
     this.cacheKey = new Plugin.CacheKey(name);
-    this.disabled = srcFile.getFileName().toString().endsWith(".disabled");
+    this.disabled = srcPath.getFileName().toString().endsWith(".disabled");
   }
 
   public CleanupHandle getCleanupHandle() {
@@ -170,7 +170,7 @@
 
   abstract boolean canReload();
 
-  boolean isModified(File jar) {
-    return snapshot.lastModified() != jar.lastModified();
+  boolean isModified(Path jar) {
+    return snapshot.lastModified() != lastModified(jar);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java
index 0228509..1d9cd0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java
@@ -11,14 +11,15 @@
 // 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.google.gerrit.server.plugins;
 
 import com.google.common.base.Optional;
 
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.annotation.Annotation;
+import java.nio.file.NoSuchFileException;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.Map;
@@ -57,7 +58,7 @@
 
     @Override
     public InputStream getInputStream(PluginEntry entry) throws IOException {
-      throw new FileNotFoundException("Empty plugin");
+      throw new NoSuchFileException("Empty plugin");
     }
 
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginDataAsFileProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginDataAsFileProvider.java
new file mode 100644
index 0000000..5e8242b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginDataAsFileProvider.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2015 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.google.gerrit.server.plugins;
+
+import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.File;
+import java.nio.file.Path;
+
+@Singleton
+class PluginDataAsFileProvider implements Provider<File> {
+  private final Provider<Path> pathProvider;
+
+  @Inject
+  PluginDataAsFileProvider(@PluginData Provider<Path> pathProvider) {
+    this.pathProvider = pathProvider;
+  }
+
+  @Override
+  public File get() {
+    return pathProvider.get().toFile();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 3fee7d7..17bb634 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -18,6 +18,8 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicate;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Lists;
@@ -25,6 +27,7 @@
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Queues;
 import com.google.common.collect.Sets;
+import com.google.common.io.ByteStreams;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
@@ -45,15 +48,15 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
-import java.io.FileFilter;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.AbstractMap;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
@@ -72,13 +75,13 @@
   static final String PLUGIN_TMP_PREFIX = "plugin_";
   static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
 
-  public String getPluginName(File srcFile) {
-    return MoreObjects.firstNonNull(getGerritPluginName(srcFile),
-        nameOf(srcFile));
+  public String getPluginName(Path srcPath) {
+    return MoreObjects.firstNonNull(getGerritPluginName(srcPath),
+        nameOf(srcPath));
   }
 
-  private final File pluginsDir;
-  private final File dataDir;
+  private final Path pluginsDir;
+  private final Path dataDir;
   private final PluginGuiceEnvironment env;
   private final ServerInformationImpl srvInfoImpl;
   private final PluginUser.Factory pluginUserFactory;
@@ -159,7 +162,7 @@
     checkRemoteInstall();
 
     String fileName = originalName;
-    File tmp = asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir);
+    Path tmp = asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir);
     String name = MoreObjects.firstNonNull(getGerritPluginName(tmp),
         nameOf(fileName));
     if (!originalName.equals(name)) {
@@ -169,26 +172,26 @@
     }
 
     String fileExtension = getExtension(fileName);
-    File dst = new File(pluginsDir, name + fileExtension);
+    Path dst = pluginsDir.resolve(name + fileExtension);
     synchronized (this) {
       Plugin active = running.get(name);
       if (active != null) {
         fileName = active.getSrcFile().getFileName().toString();
         log.info(String.format("Replacing plugin %s", active.getName()));
-        File old = new File(pluginsDir, ".last_" + fileName);
-        old.delete();
-        active.getSrcFile().toFile().renameTo(old);
+        Path old = pluginsDir.resolve(".last_" + fileName);
+        Files.deleteIfExists(old);
+        Files.move(active.getSrcFile(), old);
       }
 
-      new File(pluginsDir, fileName + ".disabled").delete();
-      tmp.renameTo(dst);
+      Files.deleteIfExists(pluginsDir.resolve(fileName + ".disabled"));
+      Files.move(tmp, dst);
       try {
         Plugin plugin = runPlugin(name, dst, active);
         if (active == null) {
           log.info(String.format("Installed plugin %s", plugin.getName()));
         }
       } catch (PluginInstallException e) {
-        dst.delete();
+        Files.deleteIfExists(dst);
         throw e;
       }
 
@@ -196,21 +199,17 @@
     }
   }
 
-  static File asTemp(InputStream in, String prefix, String suffix, File dir)
+  static Path asTemp(InputStream in, String prefix, String suffix, Path dir)
       throws IOException {
-    File tmp = File.createTempFile(prefix, suffix, dir);
+    Path tmp = Files.createTempFile(dir, prefix, suffix);
     boolean keep = false;
-    try (FileOutputStream out = new FileOutputStream(tmp)) {
-      byte[] data = new byte[8192];
-      int n;
-      while ((n = in.read(data)) > 0) {
-        out.write(data, 0, n);
-      }
+    try (OutputStream out = Files.newOutputStream(tmp)) {
+      ByteStreams.copy(in, out);
       keep = true;
       return tmp;
     } finally {
       if (!keep) {
-        tmp.delete();
+        Files.delete(tmp);
       }
     }
   }
@@ -241,12 +240,21 @@
         }
 
         log.info(String.format("Disabling plugin %s", active.getName()));
-        File off = new File(active.getSrcFile() + ".disabled");
-        active.getSrcFile().toFile().renameTo(off);
+        Path off = active.getSrcFile().resolveSibling(
+            active.getSrcFile().getFileName() + ".disabled");
+        try {
+          Files.move(active.getSrcFile(), off);
+        } catch (IOException e) {
+          log.error("Failed to disable plugin", e);
+          // In theory we could still unload the plugin even if the rename
+          // failed. However, it would be reloaded on the next server startup,
+          // which is probably not what the user expects.
+          continue;
+        }
 
         unloadPlugin(active);
         try {
-          FileSnapshot snapshot = FileSnapshot.save(off);
+          FileSnapshot snapshot = FileSnapshot.save(off.toFile());
           Plugin offPlugin = loadPlugin(name, off, snapshot);
           disabled.put(name, offPlugin);
         } catch (Throwable e) {
@@ -279,9 +287,13 @@
         if (n.endsWith(".disabled")) {
           n = n.substring(0, n.lastIndexOf('.'));
         }
-        File on = new File(pluginsDir, n);
-        off.getSrcFile().toFile().renameTo(on);
-
+        Path on = pluginsDir.resolve(n);
+        try {
+          Files.move(off.getSrcFile(), on);
+        } catch (IOException e) {
+          log.error("Failed to move plugin " + name + " into place", e);
+          continue;
+        }
         disabled.remove(name);
         runPlugin(name, on, null);
       }
@@ -291,7 +303,7 @@
 
   @Override
   public synchronized void start() {
-    log.info("Loading plugins from " + pluginsDir.getAbsolutePath());
+    log.info("Loading plugins from " + pluginsDir.toAbsolutePath());
     srvInfoImpl.state = ServerInformation.State.STARTUP;
     rescan();
     srvInfoImpl.state = ServerInformation.State.RUNNING;
@@ -343,7 +355,7 @@
         String name = active.getName();
         try {
           log.info(String.format("Reloading plugin %s", name));
-          runPlugin(name, active.getSrcFile().toFile(), active);
+          runPlugin(name, active.getSrcFile(), active);
         } catch (PluginInstallException e) {
           log.warn(String.format("Cannot reload plugin %s", name), e.getCause());
           throw e;
@@ -355,30 +367,30 @@
   }
 
   public synchronized void rescan() {
-    Multimap<String, File> pluginsFiles = prunePlugins(pluginsDir);
+    Multimap<String, Path> pluginsFiles = prunePlugins(pluginsDir);
     if (pluginsFiles.isEmpty()) {
       return;
     }
 
     syncDisabledPlugins(pluginsFiles);
 
-    Map<String, File> activePlugins = filterDisabled(pluginsFiles);
-    for (Map.Entry<String, File> entry : jarsFirstSortedPluginsSet(activePlugins)) {
+    Map<String, Path> activePlugins = filterDisabled(pluginsFiles);
+    for (Map.Entry<String, Path> entry : jarsFirstSortedPluginsSet(activePlugins)) {
       String name = entry.getKey();
-      File file = entry.getValue();
-      String fileName = file.getName();
-      if (!isJsPlugin(fileName) && !serverPluginFactory.handles(file)) {
+      Path path = entry.getValue();
+      String fileName = path.getFileName().toString();
+      if (!isJsPlugin(fileName) && !serverPluginFactory.handles(path)) {
         log.warn("No Plugin provider was found that handles this file format: {}", fileName);
         continue;
       }
 
       FileSnapshot brokenTime = broken.get(name);
-      if (brokenTime != null && !brokenTime.isModified(file)) {
+      if (brokenTime != null && !brokenTime.isModified(path.toFile())) {
         continue;
       }
 
       Plugin active = running.get(name);
-      if (active != null && !active.isModified(file)) {
+      if (active != null && !active.isModified(path)) {
         continue;
       }
 
@@ -388,7 +400,7 @@
       }
 
       try {
-        Plugin loadedPlugin = runPlugin(name, file, active);
+        Plugin loadedPlugin = runPlugin(name, path, active);
         if (active == null && !loadedPlugin.isDisabled()) {
           log.info(String.format("Loaded plugin %s, version %s",
               loadedPlugin.getName(), loadedPlugin.getVersion()));
@@ -401,31 +413,28 @@
     cleanInBackground();
   }
 
-  private void addAllEntries(Map<String, File> from,
-      TreeSet<Entry<String, File>> to) {
-    Iterator<Entry<String, File>> it = from.entrySet().iterator();
+  private void addAllEntries(Map<String, Path> from,
+      TreeSet<Entry<String, Path>> to) {
+    Iterator<Entry<String, Path>> it = from.entrySet().iterator();
     while (it.hasNext()) {
-      Entry<String,File> entry = it.next();
+      Entry<String,Path> entry = it.next();
       to.add(new AbstractMap.SimpleImmutableEntry<>(
           entry.getKey(), entry.getValue()));
     }
   }
 
-  private TreeSet<Entry<String, File>> jarsFirstSortedPluginsSet(
-      Map<String, File> activePlugins) {
-    TreeSet<Entry<String, File>> sortedPlugins =
-        Sets.newTreeSet(new Comparator<Entry<String, File>>() {
+  private TreeSet<Entry<String, Path>> jarsFirstSortedPluginsSet(
+      Map<String, Path> activePlugins) {
+    TreeSet<Entry<String, Path>> sortedPlugins =
+        Sets.newTreeSet(new Comparator<Entry<String, Path>>() {
           @Override
-          public int compare(Entry<String, File> entry1,
-              Entry<String, File> entry2) {
-            String file1 = entry1.getValue().getName();
-            String file2 = entry2.getValue().getName();
-            int cmp = file1.compareTo(file2);
-            if (file1.endsWith(".jar")) {
-              return (file2.endsWith(".jar") ? cmp : -1);
-            } else {
-              return (file2.endsWith(".jar") ? +1 : cmp);
-            }
+          public int compare(Entry<String, Path> e1, Entry<String, Path> e2) {
+            Path n1 = e1.getValue().getFileName();
+            Path n2 = e2.getValue().getFileName();
+            return ComparisonChain.start()
+                .compareTrueFirst(n1.endsWith(".jar"), n2.endsWith(".jar"))
+                .compare(n1, n2)
+                .result();
           }
         });
 
@@ -433,14 +442,14 @@
     return sortedPlugins;
   }
 
-  private void syncDisabledPlugins(Multimap<String, File> jars) {
+  private void syncDisabledPlugins(Multimap<String, Path> jars) {
     stopRemovedPlugins(jars);
     dropRemovedDisabledPlugins(jars);
   }
 
-  private Plugin runPlugin(String name, File plugin, Plugin oldPlugin)
+  private Plugin runPlugin(String name, Path plugin, Plugin oldPlugin)
       throws PluginInstallException {
-    FileSnapshot snapshot = FileSnapshot.save(plugin);
+    FileSnapshot snapshot = FileSnapshot.save(plugin.toFile());
     try {
       Plugin newPlugin = loadPlugin(name, plugin, snapshot);
       if (newPlugin.getCleanupHandle() != null) {
@@ -480,11 +489,11 @@
     }
   }
 
-  private void stopRemovedPlugins(Multimap<String, File> jars) {
+  private void stopRemovedPlugins(Multimap<String, Path> jars) {
     Set<String> unload = Sets.newHashSet(running.keySet());
-    for (Map.Entry<String, Collection<File>> entry : jars.asMap().entrySet()) {
-      for (File file : entry.getValue()) {
-        if (!file.getName().endsWith(".disabled")) {
+    for (Map.Entry<String, Collection<Path>> entry : jars.asMap().entrySet()) {
+      for (Path path : entry.getValue()) {
+        if (!path.getFileName().toString().endsWith(".disabled")) {
           unload.remove(entry.getKey());
         }
       }
@@ -494,11 +503,11 @@
     }
   }
 
-  private void dropRemovedDisabledPlugins(Multimap<String, File> jars) {
+  private void dropRemovedDisabledPlugins(Multimap<String, Path> jars) {
     Set<String> unload = Sets.newHashSet(disabled.keySet());
-    for (Map.Entry<String, Collection<File>> entry : jars.asMap().entrySet()) {
-      for (File file : entry.getValue()) {
-        if (file.getName().endsWith(".disabled")) {
+    for (Map.Entry<String, Collection<Path>> entry : jars.asMap().entrySet()) {
+      for (Path path : entry.getValue()) {
+        if (path.getFileName().toString().endsWith(".disabled")) {
           unload.remove(entry.getKey());
         }
       }
@@ -529,8 +538,8 @@
     }
   }
 
-  public static String nameOf(File plugin) {
-    return nameOf(plugin.getName());
+  public static String nameOf(Path plugin) {
+    return nameOf(plugin.getFileName().toString());
   }
 
   private static String nameOf(String name) {
@@ -546,21 +555,21 @@
     return 0 < ext ? name.substring(ext) : "";
   }
 
-  private Plugin loadPlugin(String name, File srcPlugin, FileSnapshot snapshot)
+  private Plugin loadPlugin(String name, Path srcPlugin, FileSnapshot snapshot)
       throws InvalidPluginException {
-    String pluginName = srcPlugin.getName();
+    String pluginName = srcPlugin.getFileName().toString();
     if (isJsPlugin(pluginName)) {
-      return loadJsPlugin(name, srcPlugin.toPath(), snapshot);
+      return loadJsPlugin(name, srcPlugin, snapshot);
     } else if (serverPluginFactory.handles(srcPlugin)) {
       return loadServerPlugin(srcPlugin, snapshot);
     } else {
       throw new InvalidPluginException(String.format(
-          "Unsupported plugin type: %s", srcPlugin.getName()));
+          "Unsupported plugin type: %s", srcPlugin.getFileName()));
     }
   }
 
-  private File getPluginDataDir(String name) {
-    return new File(dataDir, name);
+  private Path getPluginDataDir(String name) {
+    return dataDir.resolve(name);
   }
 
   private String getPluginCanonicalWebUrl(String name) {
@@ -574,7 +583,7 @@
     return new JsPlugin(name, srcJar, pluginUserFactory.create(name), snapshot);
   }
 
-  private ServerPlugin loadServerPlugin(File scriptFile,
+  private ServerPlugin loadServerPlugin(Path scriptFile,
       FileSnapshot snapshot) throws InvalidPluginException {
     String name = serverPluginFactory.getPluginName(scriptFile);
     return serverPluginFactory.get(scriptFile, snapshot, new PluginDescription(
@@ -598,15 +607,15 @@
 
   // Only one active plugin per plugin name can exist for each plugin name.
   // Filter out disabled plugins and transform the multimap to a map
-  private static Map<String, File> filterDisabled(
-      Multimap<String, File> pluginFiles) {
-    Map<String, File> activePlugins = Maps.newHashMapWithExpectedSize(
-        pluginFiles.keys().size());
-    for (String name : pluginFiles.keys()) {
-      for (File pluginFile : pluginFiles.asMap().get(name)) {
-        if (!pluginFile.getName().endsWith(".disabled")) {
+  private static Map<String, Path> filterDisabled(
+      Multimap<String, Path> pluginPaths) {
+    Map<String, Path> activePlugins = Maps.newHashMapWithExpectedSize(
+        pluginPaths.keys().size());
+    for (String name : pluginPaths.keys()) {
+      for (Path pluginPath : pluginPaths.asMap().get(name)) {
+        if (!pluginPath.getFileName().toString().endsWith(".disabled")) {
           assert(!activePlugins.containsKey(name));
-          activePlugins.put(name, pluginFile);
+          activePlugins.put(name, pluginPath);
         }
       }
     }
@@ -622,37 +631,40 @@
   //
   // NOTE: Bear in mind that the plugin name can be reassigned after load by the
   //       Server plugin provider.
-  public Multimap<String, File> prunePlugins(File pluginsDir) {
-    List<File> pluginFiles = scanFilesInPluginsDirectory(pluginsDir);
-    Multimap<String, File> map;
-    map = asMultimap(pluginFiles);
+  public Multimap<String, Path> prunePlugins(Path pluginsDir) {
+    List<Path> pluginPaths = scanPathsInPluginsDirectory(pluginsDir);
+    Multimap<String, Path> map;
+    map = asMultimap(pluginPaths);
     for (String plugin : map.keySet()) {
-      Collection<File> files = map.asMap().get(plugin);
+      Collection<Path> files = map.asMap().get(plugin);
       if (files.size() == 1) {
         continue;
       }
       // retrieve enabled plugins
-      Iterable<File> enabled = filterDisabledPlugins(
-          files);
+      Iterable<Path> enabled = filterDisabledPlugins(files);
       // If we have only one (the winner) plugin, nothing to do
       if (!Iterables.skip(enabled, 1).iterator().hasNext()) {
         continue;
       }
-      File winner = Iterables.getFirst(enabled, null);
+      Path winner = Iterables.getFirst(enabled, null);
       assert(winner != null);
       // Disable all loser plugins by renaming their file names to
       // "file.disabled" and replace the disabled files in the multimap.
-      Collection<File> elementsToRemove = Lists.newArrayList();
-      Collection<File> elementsToAdd = Lists.newArrayList();
-      for (File loser : Iterables.skip(enabled, 1)) {
+      Collection<Path> elementsToRemove = Lists.newArrayList();
+      Collection<Path> elementsToAdd = Lists.newArrayList();
+      for (Path loser : Iterables.skip(enabled, 1)) {
         log.warn(String.format("Plugin <%s> was disabled, because"
              + " another plugin <%s>"
              + " with the same name <%s> already exists",
              loser, winner, plugin));
-        File disabledPlugin = new File(loser + ".disabled");
+        Path disabledPlugin = Paths.get(loser + ".disabled");
         elementsToAdd.add(disabledPlugin);
         elementsToRemove.add(loser);
-        loser.renameTo(disabledPlugin);
+        try {
+          Files.move(loser, disabledPlugin);
+        } catch (IOException e) {
+          log.warn("Failed to fully disable plugin " + loser, e);
+        }
       }
       Iterables.removeAll(files, elementsToRemove);
       Iterables.addAll(files, elementsToAdd);
@@ -660,50 +672,52 @@
     return map;
   }
 
-  private List<File> scanFilesInPluginsDirectory(File pluginsDir) {
-    if (pluginsDir == null || !pluginsDir.exists()) {
+  private List<Path> scanPathsInPluginsDirectory(Path pluginsDir) {
+    if (pluginsDir == null || !Files.exists(pluginsDir)) {
       return Collections.emptyList();
     }
-    File[] matches = pluginsDir.listFiles(new FileFilter() {
+    DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
       @Override
-      public boolean accept(File pathname) {
-        String n = pathname.getName();
+      public boolean accept(Path entry) throws IOException {
+        String n = entry.getFileName().toString();
         return !n.startsWith(".last_")
             && !n.startsWith(".next_");
       }
-    });
-    if (matches == null) {
-      log.error("Cannot list " + pluginsDir.getAbsolutePath());
-      return Collections.emptyList();
+    };
+    try (DirectoryStream<Path> files
+        = Files.newDirectoryStream(pluginsDir, filter)) {
+      return ImmutableList.copyOf(files);
+    } catch (IOException e) {
+      log.error("Cannot list " + pluginsDir.toAbsolutePath(), e);
+      return ImmutableList.of();
     }
-    return Arrays.asList(matches);
   }
 
-  private static Iterable<File> filterDisabledPlugins(
-      Collection<File> files) {
-    return Iterables.filter(files, new Predicate<File>() {
+  private static Iterable<Path> filterDisabledPlugins(
+      Collection<Path> paths) {
+    return Iterables.filter(paths, new Predicate<Path>() {
       @Override
-      public boolean apply(File file) {
-        return !file.getName().endsWith(".disabled");
+      public boolean apply(Path p) {
+        return !p.getFileName().toString().endsWith(".disabled");
       }
     });
   }
 
-  public String getGerritPluginName(File srcFile) {
-    String fileName = srcFile.getName();
+  public String getGerritPluginName(Path srcPath) {
+    String fileName = srcPath.getFileName().toString();
     if (isJsPlugin(fileName)) {
       return fileName.substring(0, fileName.length() - 3);
     }
-    if (serverPluginFactory.handles(srcFile)) {
-      return serverPluginFactory.getPluginName(srcFile);
+    if (serverPluginFactory.handles(srcPath)) {
+      return serverPluginFactory.getPluginName(srcPath);
     }
     return null;
   }
 
-  private Multimap<String, File> asMultimap(List<File> plugins) {
-    Multimap<String, File> map = LinkedHashMultimap.create();
-    for (File srcFile : plugins) {
-      map.put(getPluginName(srcFile), srcFile);
+  private Multimap<String, Path> asMultimap(List<Path> plugins) {
+    Multimap<String, Path> map = LinkedHashMultimap.create();
+    for (Path srcPath : plugins) {
+      map.put(getPluginName(srcPath), srcPath);
     }
     return map;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
index 1588800..7bb9a19 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -36,6 +36,8 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.jar.Attributes;
 import java.util.jar.Manifest;
@@ -59,7 +61,7 @@
 
   private final Manifest manifest;
   private final PluginContentScanner scanner;
-  private final File dataDir;
+  private final Path dataDir;
   private final String pluginCanonicalWebUrl;
   private final ClassLoader classLoader;
   private Class<? extends Module> sysModule;
@@ -75,12 +77,12 @@
   public ServerPlugin(String name,
       String pluginCanonicalWebUrl,
       PluginUser pluginUser,
-      File srcJar,
+      Path srcJar,
       FileSnapshot snapshot,
       PluginContentScanner scanner,
-      File dataDir,
+      Path dataDir,
       ClassLoader classLoader) throws InvalidPluginException {
-    super(name, srcJar.toPath(), pluginUser, snapshot,
+    super(name, srcJar, pluginUser, snapshot,
         Plugin.getApiType(getPluginManifest(scanner)));
     this.pluginCanonicalWebUrl = pluginCanonicalWebUrl;
     this.scanner = scanner;
@@ -128,8 +130,8 @@
     return (Class<? extends Module>) clazz;
   }
 
-  File getSrcJar() {
-    return getSrcFile().toFile();
+  Path getSrcJar() {
+    return getSrcFile();
   }
 
   private static Manifest getPluginManifest(PluginContentScanner scanner)
@@ -245,20 +247,22 @@
           .annotatedWith(PluginCanonicalWebUrl.class)
           .toInstance(pluginCanonicalWebUrl);
 
-        bind(File.class)
+        bind(Path.class)
           .annotatedWith(PluginData.class)
-          .toProvider(new Provider<File>() {
+          .toProvider(new Provider<Path>() {
             private volatile boolean ready;
 
             @Override
-            public File get() {
+            public Path get() {
               if (!ready) {
                 synchronized (dataDir) {
                   if (!ready) {
-                    if (!dataDir.exists() && !dataDir.mkdirs()) {
+                    try {
+                      Files.createDirectories(dataDir);
+                    } catch (IOException e) {
                       throw new ProvisionException(String.format(
                           "Cannot create %s for plugin %s",
-                          dataDir.getAbsolutePath(), getName()));
+                          dataDir.toAbsolutePath(), getName()), e);
                     }
                     ready = true;
                   }
@@ -267,6 +271,8 @@
               return dataDir;
             }
           });
+        bind(File.class).annotatedWith(PluginData.class)
+            .toProvider(PluginDataAsFileProvider.class);
       }
     });
     return Guice.createInjector(modules);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginProvider.java
index a529b6d..bc2432b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginProvider.java
@@ -19,7 +19,7 @@
 
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 
-import java.io.File;
+import java.nio.file.Path;
 
 /**
  * Provider of one Server plugin from one external file
@@ -31,7 +31,6 @@
  * group them into a directory tree and then load the directory
  * root as a single plugin.
  */
-// TODO(dborowitz): Convert to NIO; ensure clients can migrate.
 @ExtensionPoint
 public interface ServerPluginProvider {
 
@@ -41,7 +40,7 @@
   public class PluginDescription {
     public final PluginUser user;
     public final String canonicalUrl;
-    public final File dataDir;
+    public final Path dataDir;
 
     /**
      * Creates a new PluginDescription for ServerPluginProvider.
@@ -50,7 +49,7 @@
      * @param canonicalUrl plugin root Web URL
      * @param dataDir directory for plugin data
      */
-    public PluginDescription(PluginUser user, String canonicalUrl, File dataDir) {
+    public PluginDescription(PluginUser user, String canonicalUrl, Path dataDir) {
       this.user = user;
       this.canonicalUrl = canonicalUrl;
       this.dataDir = dataDir;
@@ -60,39 +59,39 @@
   /**
    * Declares the availability to manage an external file or directory
    *
-   * @param srcFile the external file or directory
+   * @param srcPath the external file or directory
    * @return true if file or directory can be loaded into a Server Plugin
    */
-  boolean handles(File srcFile);
+  boolean handles(Path srcPath);
 
   /**
    * Returns the plugin name of an external file or directory
    *
-   * Should be called only if {@link #handles(File) handles(srcFile)}
+   * Should be called only if {@link #handles(Path) handles(srcFile)}
    * returns true and thus srcFile is a supported plugin format.
    * An IllegalArgumentException is thrown otherwise as srcFile
    * is not a valid file format for extracting its plugin name.
    *
-   * @param srcFile external file or directory
+   * @param srcPath external file or directory
    * @return plugin name
    */
-  String getPluginName(File srcFile);
+  String getPluginName(Path srcPath);
 
   /**
    * Loads an external file or directory into a Server plugin.
    *
-   * Should be called only if {@link #handles(File) handles(srcFile)}
+   * Should be called only if {@link #handles(Path) handles(srcFile)}
    * returns true and thus srcFile is a supported plugin format.
    * An IllegalArgumentException is thrown otherwise as srcFile
    * is not a valid file format for extracting its plugin name.
    *
-   * @param srcFile external file or directory
+   * @param srcPath external file or directory
    * @param snapshot snapshot of the external file
    * @param pluginDescriptor descriptor of the ServerPlugin to load
    * @throws InvalidPluginException if plugin is supposed to be handled
    *         but cannot be loaded for any other reason
    */
-  ServerPlugin get(File srcFile, FileSnapshot snapshot,
+  ServerPlugin get(Path srcPath, FileSnapshot snapshot,
       PluginDescription pluginDescriptor) throws InvalidPluginException;
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
index 0e8bd87..afdc5b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
@@ -22,7 +22,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -38,27 +38,26 @@
   }
 
   @Override
-  public ServerPlugin get(File srcFile, FileSnapshot snapshot,
+  public ServerPlugin get(Path srcPath, FileSnapshot snapshot,
       PluginDescription pluginDescription) throws InvalidPluginException {
-    return providerOf(srcFile).get(srcFile, snapshot, pluginDescription);
+    return providerOf(srcPath).get(srcPath, snapshot, pluginDescription);
   }
 
   @Override
-  public String getPluginName(File srcFile) {
-    return providerOf(srcFile).getPluginName(srcFile);
+  public String getPluginName(Path srcPath) {
+    return providerOf(srcPath).getPluginName(srcPath);
   }
 
   @Override
-  public boolean handles(File srcFile) {
-    List<ServerPluginProvider> providers =
-        providersForHandlingPlugin(srcFile);
+  public boolean handles(Path srcPath) {
+    List<ServerPluginProvider> providers = providersForHandlingPlugin(srcPath);
     switch (providers.size()) {
       case 1:
         return true;
       case 0:
         return false;
       default:
-        throw new MultipleProvidersForPluginException(srcFile, providers);
+        throw new MultipleProvidersForPluginException(srcPath, providers);
     }
   }
 
@@ -67,27 +66,27 @@
     return "gerrit";
   }
 
-  private ServerPluginProvider providerOf(File srcFile) {
+  private ServerPluginProvider providerOf(Path srcPath) {
     List<ServerPluginProvider> providers =
-        providersForHandlingPlugin(srcFile);
+        providersForHandlingPlugin(srcPath);
     switch (providers.size()) {
       case 1:
         return providers.get(0);
       case 0:
         throw new IllegalArgumentException(
             "No ServerPluginProvider found/loaded to handle plugin file "
-                + srcFile.getAbsolutePath());
+                + srcPath.toAbsolutePath());
       default:
-        throw new MultipleProvidersForPluginException(srcFile, providers);
+        throw new MultipleProvidersForPluginException(srcPath, providers);
     }
   }
 
   private List<ServerPluginProvider> providersForHandlingPlugin(
-      final File srcFile) {
+      final Path srcPath) {
     List<ServerPluginProvider> providers = new ArrayList<>();
     for (ServerPluginProvider serverPluginProvider : serverPluginProviders) {
-      boolean handles = serverPluginProvider.handles(srcFile);
-      log.debug("File {} handled by {} ? => {}", srcFile,
+      boolean handles = serverPluginProvider.handles(srcPath);
+      log.debug("File {} handled by {} ? => {}", srcPath,
           serverPluginProvider.getProviderPluginName(), handles);
       if (handles) {
         providers.add(serverPluginProvider);
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index 4c295fd..cbcc3ca 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit 4c295fd3829725af769dc3c37db9df1c562e24a3
+Subproject commit cbcc3cac14604ff2194473b38d268dbcfa8f1dcc