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