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);
+ }
+
+}