Serve static resources from directory-based Scala scripts

When using Scala scripts inside a plugin directory, the requests
to static/<some relative path> will return the static resources
under the static subdirectory of the Scala script.

This change allows to delivery a fully feature client + server
plugin experience by leveraging HTTP/JS client UX with server-side
Scala script controllers.

ScalaPluginProvider interface aligned with the latest
changes under review on Gerrit I1d07343

Change-Id: Ib6f026197556348c2b15cc29418da6cbf4b85618
diff --git a/src/main/java/com/googlesource/gerrit/plugins/scripting/scala/ScalaPluginProvider.java b/src/main/java/com/googlesource/gerrit/plugins/scripting/scala/ScalaPluginProvider.java
index 27b7490..9eeffb5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/scripting/scala/ScalaPluginProvider.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/scripting/scala/ScalaPluginProvider.java
@@ -14,7 +14,7 @@
 
 package com.googlesource.gerrit.plugins.scripting.scala;
 
-import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.plugins.InvalidPluginException;
 import com.google.gerrit.server.plugins.ServerPlugin;
 import com.google.gerrit.server.plugins.ServerPluginProvider;
@@ -51,21 +51,25 @@
   public static final String SCALA_EXTENSION = ".scala";
 
   private final Provider<ScalaPluginScriptEngine> scriptEngineProvider;
+  private final String providerPluginName;
 
   @Inject
-  public ScalaPluginProvider(Provider<ScalaPluginScriptEngine> scriptEngineProvider) {
+  public ScalaPluginProvider(
+      Provider<ScalaPluginScriptEngine> scriptEngineProvider,
+      @PluginName String providerPluginName) {
     this.scriptEngineProvider = scriptEngineProvider;
+    this.providerPluginName = providerPluginName;
   }
 
   @Override
-  public ServerPlugin get(File srcFile, PluginUser pluginUser,
-      FileSnapshot snapshot, String pluginCanonicalWebUrl, File pluginDataDir)
+  public ServerPlugin get(File srcFile,
+      FileSnapshot snapshot, PluginDescription description)
       throws InvalidPluginException {
     ScalaPluginScriptEngine scriptEngine = scriptEngineProvider.get();
     String name = getPluginName(srcFile);
-    return new ServerPlugin(name, pluginCanonicalWebUrl, pluginUser, srcFile,
+    return new ServerPlugin(name, description.canonicalUrl, description.user, srcFile,
         snapshot, new ScalaPluginScanner(name, srcFile, scriptEngine),
-        pluginDataDir, scriptEngine.getClassLoader());
+        description.dataDir, scriptEngine.getClassLoader());
   }
 
   @Override
@@ -82,5 +86,10 @@
     }
     return srcFileName.substring(0, endPos);
   }
+
+  @Override
+  public String getProviderPluginName() {
+    return providerPluginName;
+  }
 }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/scripting/scala/ScalaPluginScanner.java b/src/main/java/com/googlesource/gerrit/plugins/scripting/scala/ScalaPluginScanner.java
index e72fc72..18efe63 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/scripting/scala/ScalaPluginScanner.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/scripting/scala/ScalaPluginScanner.java
@@ -14,33 +14,48 @@
 package com.googlesource.gerrit.plugins.scripting.scala;
 
 import com.google.common.base.Optional;
+import com.google.common.collect.Lists;
 import com.google.gerrit.server.plugins.AbstractPreloadedPluginScanner;
 import com.google.gerrit.server.plugins.InvalidPluginException;
 import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.PluginEntry;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.Enumeration;
+import java.util.List;
 import java.util.Set;
 
 public class ScalaPluginScanner extends AbstractPreloadedPluginScanner {
 
+  private final File staticResourcesPath;
+
   public ScalaPluginScanner(String pluginName, File srcFile,
-      ScalaPluginScriptEngine scriptEngine)
-      throws InvalidPluginException {
-    super(pluginName, getPluginVersion(srcFile), loadScriptClasses(srcFile, scriptEngine), Plugin.ApiType.PLUGIN);
+      ScalaPluginScriptEngine scriptEngine) throws InvalidPluginException {
+    super(pluginName, getPluginVersion(srcFile), loadScriptClasses(srcFile,
+        scriptEngine), Plugin.ApiType.PLUGIN);
+
+    this.staticResourcesPath = srcFile;
   }
 
   private static String getPluginVersion(File srcFile) {
     String srcFileName = srcFile.getName();
     int startPos = srcFileName.lastIndexOf('-');
-    if(startPos == -1) {
+    if (startPos == -1) {
       return "0";
     }
     int endPos = srcFileName.lastIndexOf('.');
-    return srcFileName.substring(startPos+1, endPos);
+    return srcFileName.substring(startPos + 1, endPos);
   }
 
   private static Set<Class<?>> loadScriptClasses(File srcFile,
@@ -54,18 +69,63 @@
   }
 
   @Override
-  public <T> Optional<T> getResource(String resourcePath, Class<? extends T> resourceClass) {
-    return Optional.absent();
+  public Optional<PluginEntry> getEntry(String resourcePath) {
+    File resourceFile = getResourceFile(resourcePath);
+    if (resourceFile.exists() && resourceFile.length() > 0) {
+      return resourceOf(resourcePath);
+    } else {
+      return Optional.absent();
+    }
+  }
+
+  private Optional<PluginEntry> resourceOf(String resourcePath) {
+    File file = getResourceFile(resourcePath);
+    if (file.exists() && file.length() > 0) {
+      return Optional.of(new PluginEntry(resourcePath, file.lastModified(), file
+          .length()));
+    } else {
+      return Optional.absent();
+    }
+  }
+
+  private File getResourceFile(String resourcePath) {
+    File resourceFile = new File(staticResourcesPath, resourcePath);
+    return resourceFile;
   }
 
   @Override
-  public Optional<InputStream> getResourceInputStream(String resourcePath)
+  public InputStream getInputStream(PluginEntry entry)
       throws IOException {
-    return Optional.absent();
+    return new FileInputStream(getResourceFile(entry.getName()));
   }
 
   @Override
-  public <T> Enumeration<T> resources(Class<? extends T> resourceClass) {
-    return Collections.emptyEnumeration();
+  public Enumeration<PluginEntry> entries() {
+    final List<PluginEntry> resourcesList = Lists.newArrayList();
+    try {
+      Files.walkFileTree(staticResourcesPath.toPath(),
+          EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
+          new SimpleFileVisitor<Path>() {
+            private int basicPathLength = staticResourcesPath.getAbsolutePath()
+                .length();
+
+            @Override
+            public FileVisitResult visitFile(Path path,
+                BasicFileAttributes attrs) throws IOException {
+              Optional<PluginEntry> resource = resourceOf(relativePathOf(path));
+              if (resource.isPresent()) {
+                resourcesList.add(resource.get());
+              }
+              return FileVisitResult.CONTINUE;
+            }
+
+            private String relativePathOf(Path path) {
+              return path.toFile().getAbsolutePath().substring(basicPathLength);
+            }
+          });
+    } catch (IOException e) {
+      new IllegalArgumentException("Cannot scan resource files in plugin", e);
+    }
+    return Collections.enumeration(resourcesList);
   }
 }