Return rendered error result

This solves the "white screen" user get as greeting when people send links to
gitiles, while users do not have any feedback that login should be performed
nor there is a link to login.

Signed-off-by: Alon Bar-Lev <alon.barlev@gmail.com>
Change-Id: Id3854ecc4fbe54e6a69112b0419edd29e8001694
Bug: https://github.com/google/gitiles/issues/145
diff --git a/java/com/google/gitiles/DefaultErrorHandlingFilter.java b/java/com/google/gitiles/DefaultErrorHandlingFilter.java
index 958b800..f558c0d 100644
--- a/java/com/google/gitiles/DefaultErrorHandlingFilter.java
+++ b/java/com/google/gitiles/DefaultErrorHandlingFilter.java
@@ -13,12 +13,12 @@
 // limitations under the License.
 package com.google.gitiles;
 
-import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
-import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
-import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
-import static org.eclipse.jgit.http.server.GitSmartHttpTools.sendError;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.collect.ImmutableMap;
+import com.google.gitiles.GitilesRequestFailureException.FailureReason;
 import java.io.IOException;
+import java.util.Map;
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
@@ -36,28 +36,56 @@
   /** HTTP header that indicates an error detail. */
   public static final String GITILES_ERROR = "X-Gitiles-Error";
 
+  private Renderer renderer;
+
+  public DefaultErrorHandlingFilter(Renderer renderer) {
+    this.renderer = renderer;
+  }
+
   @Override
   public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
       throws IOException, ServletException {
+    int status = -1;
+    String message = null;
     try {
       chain.doFilter(req, res);
     } catch (GitilesRequestFailureException e) {
       res.setHeader(GITILES_ERROR, e.getReason().toString());
-      String publicMessage = e.getPublicErrorMessage();
-      if (publicMessage != null) {
-        res.sendError(e.getReason().getHttpStatusCode(), publicMessage);
-      } else {
-        res.sendError(e.getReason().getHttpStatusCode());
-      }
+      status = e.getReason().getHttpStatusCode();
+      message = e.getPublicErrorMessage();
     } catch (RepositoryNotFoundException e) {
-      res.sendError(SC_NOT_FOUND);
+      status = FailureReason.REPOSITORY_NOT_FOUND.getHttpStatusCode();
+      message = FailureReason.REPOSITORY_NOT_FOUND.getMessage();
     } catch (AmbiguousObjectException e) {
-      res.sendError(SC_BAD_REQUEST);
+      status = FailureReason.AMBIGUOUS_OBJECT.getHttpStatusCode();
+      message = FailureReason.AMBIGUOUS_OBJECT.getMessage();
     } catch (ServiceMayNotContinueException e) {
-      sendError(req, res, e.getStatusCode(), e.getMessage());
+      status = e.getStatusCode();
+      message = e.getMessage();
     } catch (IOException | ServletException err) {
       log.warn("Internal server error", err);
-      res.sendError(SC_INTERNAL_SERVER_ERROR);
+      status = FailureReason.INTERNAL_SERVER_ERROR.getHttpStatusCode();
+      message = FailureReason.INTERNAL_SERVER_ERROR.getMessage();
     }
+    if (status != -1) {
+      res.setStatus(status);
+      renderHtml(req, res, "gitiles.error", ImmutableMap.of("title", message));
+    }
+  }
+
+  protected void renderHtml(
+      HttpServletRequest req, HttpServletResponse res, String templateName, Map<String, ?> soyData)
+      throws IOException {
+    renderer.render(req, res, templateName, startHtmlResponse(req, res, soyData));
+  }
+
+  private Map<String, ?> startHtmlResponse(
+      HttpServletRequest req, HttpServletResponse res, Map<String, ?> soyData) throws IOException {
+    res.setContentType(FormatType.HTML.getMimeType());
+    res.setCharacterEncoding(UTF_8.name());
+    BaseServlet.setNotCacheable(res);
+    Map<String, Object> allData = BaseServlet.getData(req);
+    allData.putAll(soyData);
+    return allData;
   }
 }
diff --git a/java/com/google/gitiles/GitilesFilter.java b/java/com/google/gitiles/GitilesFilter.java
index 2c810bb..254fe22 100644
--- a/java/com/google/gitiles/GitilesFilter.java
+++ b/java/com/google/gitiles/GitilesFilter.java
@@ -420,7 +420,7 @@
 
   private void setDefaultErrorHandler() {
     if (errorHandler == null) {
-      errorHandler = new DefaultErrorHandlingFilter();
+      errorHandler = new DefaultErrorHandlingFilter(renderer);
     }
   }
 
diff --git a/java/com/google/gitiles/GitilesRequestFailureException.java b/java/com/google/gitiles/GitilesRequestFailureException.java
index 316c023..dd990a8 100644
--- a/java/com/google/gitiles/GitilesRequestFailureException.java
+++ b/java/com/google/gitiles/GitilesRequestFailureException.java
@@ -20,6 +20,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
+import java.util.Optional;
 import javax.annotation.Nullable;
 
 /**
@@ -112,53 +113,64 @@
 
   @Nullable
   public String getPublicErrorMessage() {
-    return publicErrorMessage;
+    return Optional.ofNullable(publicErrorMessage).orElse(reason.getMessage());
   }
 
   /** The request failure reason. */
   public enum FailureReason {
     /** The object specified by the URL is ambiguous and Gitiles cannot identify one object. */
-    AMBIGUOUS_OBJECT(SC_BAD_REQUEST),
+    AMBIGUOUS_OBJECT(
+        SC_BAD_REQUEST,
+        "The object specified by the URL is ambiguous and Gitiles cannot identify one object"),
     /** There's nothing to show for blame (e.g. the file is empty). */
-    BLAME_REGION_NOT_FOUND(SC_NOT_FOUND),
+    BLAME_REGION_NOT_FOUND(SC_NOT_FOUND, "There's nothing to show for blame"),
     /** Cannot parse URL as a Gitiles URL. */
-    CANNOT_PARSE_GITILES_VIEW(SC_NOT_FOUND),
+    CANNOT_PARSE_GITILES_VIEW(SC_NOT_FOUND, "Cannot parse URL as a Gitiles URL"),
     /** URL parameters are not valid. */
-    INCORECT_PARAMETER(SC_BAD_REQUEST),
+    INCORECT_PARAMETER(SC_BAD_REQUEST, "URL parameters are not valid"),
     /**
      * The object specified by the URL is not suitable for the view (e.g. trying to show a blob as a
      * tree).
      */
-    INCORRECT_OBJECT_TYPE(SC_BAD_REQUEST),
+    INCORRECT_OBJECT_TYPE(
+        SC_BAD_REQUEST, "The object specified by the URL is not suitable for the view"),
     /** Markdown rendering is not enabled. */
-    MARKDOWN_NOT_ENABLED(SC_NOT_FOUND),
+    MARKDOWN_NOT_ENABLED(SC_NOT_FOUND, "Markdown rendering is not enabled"),
     /** Request is not authorized. */
-    NOT_AUTHORIZED(SC_UNAUTHORIZED),
+    NOT_AUTHORIZED(SC_UNAUTHORIZED, "Request is not authorized"),
     /** Object is not found. */
-    OBJECT_NOT_FOUND(SC_NOT_FOUND),
+    OBJECT_NOT_FOUND(SC_NOT_FOUND, "Object is not found"),
     /** Object is too large to show. */
-    OBJECT_TOO_LARGE(SC_INTERNAL_SERVER_ERROR),
+    OBJECT_TOO_LARGE(SC_INTERNAL_SERVER_ERROR, "Object is too large to show"),
     /** Repository is not found. */
-    REPOSITORY_NOT_FOUND(SC_NOT_FOUND),
+    REPOSITORY_NOT_FOUND(SC_NOT_FOUND, "Repository is not found"),
     /** Gitiles is not enabled for the repository. */
-    SERVICE_NOT_ENABLED(SC_FORBIDDEN),
+    SERVICE_NOT_ENABLED(SC_FORBIDDEN, "Gitiles is not enabled for the repository"),
     /** GitWeb URL cannot be converted to Gitiles URL. */
-    UNSUPPORTED_GITWEB_URL(SC_GONE),
+    UNSUPPORTED_GITWEB_URL(SC_GONE, "GitWeb URL cannot be converted to Gitiles URL"),
     /** The specified object's type is not supported. */
-    UNSUPPORTED_OBJECT_TYPE(SC_NOT_FOUND),
+    UNSUPPORTED_OBJECT_TYPE(SC_NOT_FOUND, "The specified object's type is not supported"),
     /** The specified format type is not supported. */
-    UNSUPPORTED_RESPONSE_FORMAT(SC_BAD_REQUEST),
+    UNSUPPORTED_RESPONSE_FORMAT(SC_BAD_REQUEST, "The specified format type is not supported"),
     /** The specified revision names are not supported. */
-    UNSUPPORTED_REVISION_NAMES(SC_BAD_REQUEST);
+    UNSUPPORTED_REVISION_NAMES(SC_BAD_REQUEST, "The specified revision names are not supported"),
+    /** Internal server error. */
+    INTERNAL_SERVER_ERROR(SC_INTERNAL_SERVER_ERROR, "Internal server error");
 
     private final int httpStatusCode;
+    private final String message;
 
-    FailureReason(int httpStatusCode) {
+    FailureReason(int httpStatusCode, String message) {
       this.httpStatusCode = httpStatusCode;
+      this.message = message;
     }
 
     public int getHttpStatusCode() {
       return httpStatusCode;
     }
+
+    public String getMessage() {
+      return message;
+    }
   }
 }
diff --git a/java/com/google/gitiles/Renderer.java b/java/com/google/gitiles/Renderer.java
index 9ead8bf..bc85290 100644
--- a/java/com/google/gitiles/Renderer.java
+++ b/java/com/google/gitiles/Renderer.java
@@ -60,6 +60,7 @@
           "Common.soy",
           "DiffDetail.soy",
           "Doc.soy",
+          "Error.soy",
           "HostIndex.soy",
           "LogDetail.soy",
           "ObjectDetail.soy",
diff --git a/javatests/com/google/gitiles/DefaultErrorHandlingFilterTest.java b/javatests/com/google/gitiles/DefaultErrorHandlingFilterTest.java
index b96e43d..0dc152a 100644
--- a/javatests/com/google/gitiles/DefaultErrorHandlingFilterTest.java
+++ b/javatests/com/google/gitiles/DefaultErrorHandlingFilterTest.java
@@ -3,7 +3,9 @@
 import static com.google.common.truth.Truth.assertThat;
 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gitiles.GitilesRequestFailureException.FailureReason;
+import java.net.URL;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -19,7 +21,12 @@
 
   @Before
   public void setUp() {
-    mf.serve("*").through(new DefaultErrorHandlingFilter()).with(new TestServlet());
+    mf.serve("*")
+        .through(
+            new DefaultErrorHandlingFilter(
+                new DefaultRenderer(
+                    GitilesServlet.STATIC_PREFIX, ImmutableList.<URL>of(), "test site")))
+        .with(new TestServlet());
   }
 
   @Test
diff --git a/resources/com/google/gitiles/templates/Error.soy b/resources/com/google/gitiles/templates/Error.soy
new file mode 100644
index 0000000..39fcef3
--- /dev/null
+++ b/resources/com/google/gitiles/templates/Error.soy
@@ -0,0 +1,39 @@
+// Copyright 2019 Google Inc. All Rights Reserved.
+//
+// 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.
+{namespace gitiles}
+
+/**
+ * HTML page for error.
+ */
+{template .error stricthtml="false"}
+  {@param? title: ?}  /** page title. */
+  {@param? menuEntries: ?}  /** menu entries. */
+  {@param? customVariant: ?}  /** variant name for custom styling. */
+  {@param? breadcrumbs: ?}  /** map of breadcrumbs for header. */
+{call .header}
+  {param title: $title /}
+  {param menuEntries: $menuEntries /}
+  {param breadcrumbs: $breadcrumbs /}
+  {param customVariant: $customVariant /}
+{/call}
+<h1>
+  {msg desc="title"}
+    {$title}
+  {/msg}
+</h1>
+
+{call .footer}
+  {param customVariant: $customVariant /}
+{/call}
+{/template}