Merge branch 'stable-2.10'
* stable-2.10:
Update Gerrit-ApiVersion to 2.10-SNAPSHOT
Fix position of image tooltip
Add servlet to serve images
Update Gerrit-ApiVersion to 2.9.1
Change-Id: I3a9a21765d98f60448808041bb2b691cc2b7f35b
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java
index d72589c..daf5355 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java
@@ -24,6 +24,9 @@
@Override
protected void configureServlets() {
+ serveRegex("^" + ImageServlet.PATH_PREFIX + "(.+)?$")
+ .with(ImageServlet.class);
+
DynamicSet.bind(binder(), WebUiPlugin.class)
.toInstance(new GwtPlugin("imagare"));
DynamicSet.bind(binder(), WebUiPlugin.class)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/ImageServlet.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/ImageServlet.java
new file mode 100644
index 0000000..7396b9b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/ImageServlet.java
@@ -0,0 +1,293 @@
+// 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.imagare;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.hash.Hashing;
+import com.google.common.net.HttpHeaders;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.FileTypeRegistry;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.GetHead;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtexpui.server.CacheHeaders;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.PathFilter;
+
+import eu.medsea.mimeutil.MimeType;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class ImageServlet extends HttpServlet {
+ private static final long serialVersionUID = 1L;
+
+ public static final String PATH_PREFIX = "/project/";
+
+ private final ProjectControl.Factory projectControlFactory;
+ private final ProjectCache projectCache;
+ private final Provider<GetHead> getHead;
+ private final GitRepositoryManager repoManager;
+ private final FileTypeRegistry fileTypeRegistry;
+
+ @Inject
+ ImageServlet(
+ ProjectControl.Factory projectControlFactory,
+ ProjectCache projectCache,
+ Provider<GetHead> getHead,
+ GitRepositoryManager repoManager,
+ FileTypeRegistry fileTypeRegistry) {
+ this.projectControlFactory = projectControlFactory;
+ this.projectCache = projectCache;
+ this.getHead = getHead;
+ this.repoManager = repoManager;
+ this.fileTypeRegistry = fileTypeRegistry;
+ }
+
+ @Override
+ public void service(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ if (!"GET".equals(req.getMethod()) && !"HEAD".equals(req.getMethod())) {
+ CacheHeaders.setNotCacheable(res);
+ res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+ return;
+ }
+
+ ResourceKey key = ResourceKey.fromPath(req.getPathInfo());
+ ProjectState state = projectCache.get(key.project);
+ if (state == null || key.file == null) {
+ notFound(res);
+ return;
+ }
+
+ MimeType mimeType = fileTypeRegistry.getMimeType(key.file, null);
+ if (!("image".equals(mimeType.getMediaType())
+ && fileTypeRegistry.isSafeInline(mimeType))) {
+ notFound(res);
+ return;
+ }
+
+ try {
+ ProjectControl projectControl = projectControlFactory.validateFor(key.project);
+ String rev = key.revision;
+ if (rev == null || Constants.HEAD.equals(rev)) {
+ rev = getHead.get().apply(new ProjectResource(projectControl));
+ } else {
+ if (!ObjectId.isId(rev)) {
+ if (!rev.startsWith(Constants.R_REFS)) {
+ rev = Constants.R_HEADS + rev;
+ }
+ if (!projectControl.controlForRef(rev).isVisible()) {
+ notFound(res);
+ return;
+ }
+ }
+ }
+ Repository repo = repoManager.openRepository(key.project);
+ try {
+ ObjectId revId =
+ repo.resolve(rev != null ? rev : Constants.HEAD);
+ if (revId == null) {
+ notFound(res);
+ return;
+ }
+
+ if (ObjectId.isId(rev)) {
+ RevWalk rw = new RevWalk(repo);
+ try {
+ RevCommit commit = rw.parseCommit(repo.resolve(rev));
+ if (!projectControl.canReadCommit(rw, commit)) {
+ notFound(res);
+ return;
+ }
+ } finally {
+ rw.release();
+ }
+ }
+
+ String eTag = null;
+ String receivedETag = req.getHeader(HttpHeaders.IF_NONE_MATCH);
+ if (receivedETag != null) {
+ eTag = computeETag(key.project, revId, key.file);
+ if (eTag.equals(receivedETag)) {
+ res.sendError(SC_NOT_MODIFIED);
+ return;
+ }
+ }
+
+ if (!"image".equals(mimeType.getMediaType())) {
+ notFound(res);
+ return;
+ }
+
+ RevWalk rw = new RevWalk(repo);
+ try {
+ RevCommit commit = rw.parseCommit(revId);
+ RevTree tree = commit.getTree();
+ TreeWalk tw = new TreeWalk(repo);
+ try {
+ tw.addTree(tree);
+ tw.setRecursive(true);
+ tw.setFilter(PathFilter.create(key.file));
+ if (!tw.next()) {
+ notFound(res);
+ return;
+ }
+ ObjectId objectId = tw.getObjectId(0);
+ ObjectLoader loader = repo.open(objectId);
+ byte[] content = loader.getBytes(Integer.MAX_VALUE);
+
+ mimeType = fileTypeRegistry.getMimeType(key.file, content);
+ if (!"image".equals(mimeType.getMediaType())
+ || !fileTypeRegistry.isSafeInline(mimeType)) {
+ notFound(res);
+ return;
+ }
+ res.setHeader(HttpHeaders.ETAG,
+ eTag != null
+ ? eTag
+ : computeETag(key.project, revId, key.file));
+ CacheHeaders.setCacheablePrivate(res, 7, TimeUnit.DAYS, false);
+ send(req, res, content, mimeType.toString(), commit.getCommitTime());
+ return;
+ } finally {
+ tw.release();
+ }
+ } catch (IOException e) {
+ notFound(res);
+ return;
+ } finally {
+ rw.release();
+ }
+ } finally {
+ repo.close();
+ }
+ } catch (RepositoryNotFoundException | NoSuchProjectException
+ | ResourceNotFoundException | AuthException | RevisionSyntaxException e) {
+ notFound(res);
+ return;
+ }
+ }
+
+ private static String computeETag(Project.NameKey project, ObjectId revId,
+ String file) {
+ return Hashing.md5().newHasher()
+ .putUnencodedChars(project.get())
+ .putUnencodedChars(revId.getName())
+ .putUnencodedChars(file)
+ .hash().toString();
+ }
+
+ private void send(HttpServletRequest req, HttpServletResponse res,
+ byte[] content, String contentType, long lastModified) throws IOException {
+ if (0 < lastModified) {
+ long ifModifiedSince = req.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
+ if (ifModifiedSince > 0 && ifModifiedSince == lastModified) {
+ res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ return;
+ } else {
+ res.setDateHeader("Last-Modified", lastModified);
+ }
+ }
+ res.setContentType(contentType);
+ res.setCharacterEncoding(UTF_8.name());
+ res.setContentLength(content.length);
+ res.getOutputStream().write(content);
+ }
+
+ private static void notFound(HttpServletResponse res) throws IOException {
+ CacheHeaders.setNotCacheable(res);
+ res.sendError(HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ private static class ResourceKey {
+ final Project.NameKey project;
+ final String file;
+ final String revision;
+
+ static ResourceKey fromPath(String path) {
+ String project;
+ String file = null;
+ String revision = null;
+
+ if (!path.startsWith(PATH_PREFIX)) {
+ // should not happen since this servlet is only registered to handle
+ // paths that start with this prefix
+ throw new IllegalStateException("path must start with '" + PATH_PREFIX + "'");
+ }
+ path = path.substring(PATH_PREFIX.length());
+
+ int i = path.indexOf('/');
+ if (i != -1 && i != path.length() - 1) {
+ project = IdString.fromUrl(path.substring(0, i)).get();
+ String rest = path.substring(i + 1);
+
+ if (rest.startsWith("rev/")) {
+ if (rest.length() > 4) {
+ rest = rest.substring(4);
+ i = rest.indexOf('/');
+ if (i != -1 && i != path.length() - 1) {
+ revision = IdString.fromUrl(rest.substring(0, i)).get();
+ file = IdString.fromUrl(rest.substring(i + 1)).get();
+ } else {
+ revision = IdString.fromUrl(rest).get();
+ }
+ }
+ } else {
+ file = IdString.fromUrl(rest).get();
+ }
+
+ } else {
+ project = IdString.fromUrl(CharMatcher.is('/').trimTrailingFrom(path)).get();
+ }
+
+ return new ResourceKey(project, file, revision);
+ }
+
+ private ResourceKey(String p, String f, String r) {
+ project = new Project.NameKey(p);
+ file = f;
+ revision = r;
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/PostImage.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/PostImage.java
index ffc966a..de6d0da 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/PostImage.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/PostImage.java
@@ -15,6 +15,7 @@
package com.googlesource.gerrit.plugins.imagare;
import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.IdString;
@@ -76,12 +77,14 @@
private final PersonIdent myIdent;
private final String canonicalWebUrl;
private final Config cfg;
+ private final String pluginName;
@Inject
public PostImage(FileTypeRegistry registry, Provider<IdentifiedUser> self,
GitRepositoryManager repoManager, GitReferenceUpdated referenceUpdated,
ChangeHooks hooks, @GerritPersonIdent PersonIdent myIdent,
- @CanonicalWebUrl String canonicalWebUrl, @GerritServerConfig Config cfg) {
+ @CanonicalWebUrl String canonicalWebUrl, @GerritServerConfig Config cfg,
+ @PluginName String pluginName) {
this.registry = registry;
this.imageDataPattern = Pattern.compile("data:([\\w/.-]+);([\\w]+),(.*)");
this.self = self;
@@ -91,6 +94,7 @@
this.myIdent = myIdent;
this.canonicalWebUrl = canonicalWebUrl;
this.cfg = cfg;
+ this.pluginName = pluginName;
}
@Override
@@ -238,9 +242,11 @@
StringBuilder url = new StringBuilder();
url.append(canonicalWebUrl);
if (!canonicalWebUrl.endsWith("/")) {
- url.append("src/");
+ url.append("/");
}
- url.append("src/");
+ url.append("plugins/");
+ url.append(IdString.fromDecoded(pluginName).encoded());
+ url.append("/project/");
url.append(IdString.fromDecoded(project.get()).encoded());
url.append("/rev/");
url.append(IdString.fromDecoded(rev).encoded());
diff --git a/src/main/resources/static/imagare.js b/src/main/resources/static/imagare.js
index c23e5db..244350b 100644
--- a/src/main/resources/static/imagare.js
+++ b/src/main/resources/static/imagare.js
@@ -45,7 +45,7 @@
l[i].onmouseover = function (evt) {
var img = document.createElement('img');
img.setAttribute('src', this.href);
- img.setAttribute('style', 'border: 1px solid #B3B2B2; position: absolute; bottom: ' + (this.offsetHeight + 3) + 'px');
+ img.setAttribute('style', 'border: 1px solid #B3B2B2; position: absolute; top: ' + (this.offsetTop + this.offsetHeight) + 'px');
this.parentNode.insertBefore(img, this);
this.onmouseout = function (evt) {
this.parentNode.removeChild(this.previousSibling);
@@ -56,7 +56,7 @@
}
function isImage(href) {
- return href.match(window.location.hostname + '.*src/.*/rev/.*/.*\.(jpg|jpeg|png|gif|bmp|ico|svg|tif|tiff)')
+ return href.match(window.location.hostname + '.*project/.*/rev/.*/.*\.(jpg|jpeg|png|gif|bmp|ico|svg|tif|tiff)')
}
Gerrit.on('history', onHistory);