Extract the Web-static resources into a dedicated component

Preparation work in order to define a new type of server-side
plugin capable of:
- serving web-resources and their server-side expansions
- providing static PluginEntry for the HttpPluginServlet

Change-Id: Ib38455eed107d920389953e51dae8046360396dd
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 18efe63..485a87c 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,38 +14,28 @@
 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 com.googlesource.gerrit.plugins.web.WebPluginScanner;
+
 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;
+  private final WebPluginScanner webScanner;
 
   public ScalaPluginScanner(String pluginName, File srcFile,
       ScalaPluginScriptEngine scriptEngine) throws InvalidPluginException {
     super(pluginName, getPluginVersion(srcFile), loadScriptClasses(srcFile,
         scriptEngine), Plugin.ApiType.PLUGIN);
 
-    this.staticResourcesPath = srcFile;
+    this.webScanner = new WebPluginScanner(srcFile);
   }
 
   private static String getPluginVersion(File srcFile) {
@@ -68,64 +58,15 @@
     }
   }
 
-  @Override
   public Optional<PluginEntry> getEntry(String resourcePath) {
-    File resourceFile = getResourceFile(resourcePath);
-    if (resourceFile.exists() && resourceFile.length() > 0) {
-      return resourceOf(resourcePath);
-    } else {
-      return Optional.absent();
-    }
+    return webScanner.getEntry(resourcePath);
   }
 
-  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();
-    }
+  public InputStream getInputStream(PluginEntry entry) throws IOException {
+    return webScanner.getInputStream(entry);
   }
 
-  private File getResourceFile(String resourcePath) {
-    File resourceFile = new File(staticResourcesPath, resourcePath);
-    return resourceFile;
-  }
-
-  @Override
-  public InputStream getInputStream(PluginEntry entry)
-      throws IOException {
-    return new FileInputStream(getResourceFile(entry.getName()));
-  }
-
-  @Override
   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);
+    return webScanner.entries();
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/web/LookAheadFileInputStream.java b/src/main/java/com/googlesource/gerrit/plugins/web/LookAheadFileInputStream.java
new file mode 100644
index 0000000..e8c858f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/web/LookAheadFileInputStream.java
@@ -0,0 +1,106 @@
+// 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.googlesource.gerrit.plugins.web;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+class LookAheadFileInputStream extends BufferedInputStream {
+  private static final int NEWLINE_CR = '\r';
+  private static final int NEWLINE_NL = '\n';
+  private static final int BUFFER_SIZE = 1024;
+
+  private int lineNr = 1;
+  private int lastChar = NEWLINE_NL;
+
+  private final String fileExtension;
+  private final String fileName;
+  private final File currentDir;
+
+  public LookAheadFileInputStream(File inputFile) throws IOException {
+    super(new FileInputStream(inputFile), BUFFER_SIZE);
+
+    fileName = inputFile.getName();
+    fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1);
+    currentDir = inputFile.getParentFile();
+  }
+
+  @Override
+  public String toString() {
+    return "pos=" + pos + " count=" + count + " lineNr=" + lineNr
+        + " buffer=\'" + new String(buf, pos, count - pos) + "'";
+  }
+
+  public synchronized int read() throws IOException {
+    lastChar = super.read();
+    if (isNewLine()) {
+      lineNr++;
+    }
+    return lastChar;
+  }
+
+  @Override
+  public int read(byte[] b) throws IOException {
+    return read(b, 0, b.length);
+  }
+
+  @Override
+  public synchronized int read(byte[] b, int off, int len) throws IOException {
+    int numBytes = super.read(b, off, len);
+    if (numBytes > 0) {
+      lastChar = b[off + numBytes - 1];
+    }
+    return numBytes;
+  }
+
+  public synchronized boolean startsWith(String includeVirtualPrefix)
+      throws IOException {
+    mark(includeVirtualPrefix.length());
+    try {
+      byte[] cmp = new byte[includeVirtualPrefix.length()];
+      super.read(cmp);
+      return Arrays.equals(includeVirtualPrefix.getBytes(), cmp);
+    } finally {
+      reset();
+    }
+  }
+
+  public boolean isNewLine() {
+    return isNewLine(lastChar);
+  }
+
+  public boolean isNewLine(int last) {
+    return last == NEWLINE_CR || last == NEWLINE_NL;
+  }
+
+  public String getFileExtension() {
+    return fileExtension;
+  }
+
+  public int getLineNr() {
+    return lineNr;
+  }
+
+  public String getFileName() {
+    return fileName;
+  }
+
+  public File getCurrentDir() {
+    return currentDir;
+  }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/web/SSIPageInputStream.java b/src/main/java/com/googlesource/gerrit/plugins/web/SSIPageInputStream.java
new file mode 100644
index 0000000..a112dd3
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/web/SSIPageInputStream.java
@@ -0,0 +1,145 @@
+// 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.googlesource.gerrit.plugins.web;
+
+import java.io.File;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.util.Stack;
+
+public class SSIPageInputStream extends FilterInputStream {
+  private static final String INCLUDE_VIRTUAL_PREFIX =
+      "<!--#include virtual=\"";
+  private static final String INCLUDE_VIRTUAL_SUFFIX = " -->";
+
+  private LookAheadFileInputStream currentIs;
+  private Stack<LookAheadFileInputStream> fileInputStreamStack;
+  private final File basePath;
+
+  public SSIPageInputStream(File basePath, String filePath)
+      throws IOException {
+    super(new LookAheadFileInputStream(new File(basePath, filePath)));
+
+    this.basePath = basePath;
+    this.fileInputStreamStack = new Stack<>();
+    this.currentIs = (LookAheadFileInputStream) this.in;
+  }
+
+  @Override
+  public int read() throws IOException {
+    int character;
+    if (currentIs.isNewLine() && currentIs.startsWith(INCLUDE_VIRTUAL_PREFIX)) {
+      processInclude();
+      character = read();
+    } else {
+      character = currentIs.read();
+    }
+
+    if (character < 0 && !fileInputStreamStack.isEmpty()) {
+      pop();
+      return read();
+    } else {
+      return character;
+    }
+  }
+
+  private void processInclude() throws IOException {
+    push(getIncludeFileName());
+  }
+
+  private void push(String includeFileName) throws IOException {
+    fileInputStreamStack.push(currentIs);
+    File inputFile = getFile(includeFileName);
+    if (!inputFile.exists()) {
+      throw new IOException("Cannot find file '" + includeFileName
+          + "' included in " + currentIs.getFileName() + ":"
+          + currentIs.getLineNr());
+    }
+    currentIs = new LookAheadFileInputStream(inputFile);
+    in = currentIs;
+  }
+
+  private File getFile(String includeFileName) {
+    if (includeFileName.startsWith("/")) {
+      return new File(basePath, includeFileName);
+    } else {
+      return new File(currentIs.getCurrentDir(), includeFileName);
+    }
+  }
+
+  private void pop() {
+    currentIs = fileInputStreamStack.pop();
+    in = currentIs;
+  }
+
+  private String getIncludeFileName() throws IOException {
+    skipAll(INCLUDE_VIRTUAL_PREFIX.length());
+
+    StringBuilder includeFileName = new StringBuilder();
+    char last = '\0';
+    last = (char) currentIs.read();
+    while (last != '\"' && !currentIs.isNewLine() && last > 0) {
+      includeFileName.append(last);
+      last = (char) currentIs.read();
+    }
+    if (!currentIs.startsWith(INCLUDE_VIRTUAL_SUFFIX)) {
+      throw new IOException("Invalid SHTML include directive at line "
+          + currentIs.getLineNr());
+    }
+
+    skipAll(INCLUDE_VIRTUAL_SUFFIX.length());
+    return includeFileName.toString();
+  }
+
+  private void skipAll(int length) throws IOException {
+    for (long skipped = skip(length);
+        length > 0 && skipped > 0;
+        skipped = skip(length)) {
+      length -= skipped;
+    }
+  }
+
+  public int read(byte b[]) throws IOException {
+    return read(b, 0, b.length);
+  }
+
+  public int read(byte b[], int off, int len) throws IOException {
+    if (b == null) {
+      throw new NullPointerException();
+    } else if (off < 0 || len < 0 || len > b.length - off) {
+      throw new IndexOutOfBoundsException();
+    } else if (len == 0) {
+      return 0;
+    }
+
+    int c = read();
+    if (c == -1) {
+      return -1;
+    }
+    b[off] = (byte) c;
+
+    int i = 1;
+    try {
+      for (; i < len; i++) {
+        c = read();
+        if (c == -1) {
+          break;
+        }
+        b[off + i] = (byte) c;
+      }
+    } catch (IOException ee) {
+    }
+    return i;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/web/WebPluginScanner.java b/src/main/java/com/googlesource/gerrit/plugins/web/WebPluginScanner.java
new file mode 100644
index 0000000..fc73408
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/web/WebPluginScanner.java
@@ -0,0 +1,131 @@
+// 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.googlesource.gerrit.plugins.web;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.Lists;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.PluginContentScanner;
+import com.google.gerrit.server.plugins.PluginEntry;
+import com.google.inject.Inject;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+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.Map;
+import java.util.jar.Manifest;
+
+public class WebPluginScanner implements PluginContentScanner {
+  private final File staticResourcesPath;
+
+  @Inject
+  public WebPluginScanner(File rootDir) {
+    this.staticResourcesPath = rootDir;
+  }
+
+  @Override
+  public Manifest getManifest() throws IOException {
+    return new Manifest();
+  }
+
+  @Override
+  public Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> scan(
+      String pluginName, Iterable<Class<? extends Annotation>> annotations)
+      throws InvalidPluginException {
+    return Collections.emptyMap();
+  }
+
+  @Override
+  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) {
+      if (resourcePath.endsWith("html")) {
+        return Optional.of(new PluginEntry(resourcePath, file.lastModified()));
+      } else {
+        return Optional.of(new PluginEntry(resourcePath, file.lastModified(),
+            Optional.of(file.length())));
+      }
+    } else {
+      return Optional.absent();
+    }
+  }
+
+  private File getResourceFile(String resourcePath) {
+    File resourceFile = new File(staticResourcesPath, resourcePath);
+    return resourceFile;
+  }
+
+  @Override
+  public InputStream getInputStream(PluginEntry entry)
+      throws IOException {
+    String name = entry.getName();
+    if(name.endsWith("html")) {
+      return new SSIPageInputStream(staticResourcesPath, name);
+    } else {
+    return new FileInputStream(getResourceFile(name));
+    }
+  }
+
+  @Override
+  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);
+  }
+
+}