From b4d6d0773cb8ef7a2f73f4dbfc64355c6016348a Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Sun, 1 Jun 2014 20:25:11 +0200 Subject: [PATCH] Add a template presenting pipeline error and enhance the default error handler to read the error.json and render the provided template (#26) Signed-off-by: Clement Escoffier --- .../org/wisdom/maven/mojos/CreateMojo.java | 5 + .../templates/error/pipeline.thl.html | 161 ++++++++++++++++++ .../wisdom/error/DefaultPageErrorHandler.java | 142 +++++++++++++-- .../error/DefaultPageErrorHandlerTest.java | 7 + 4 files changed, 303 insertions(+), 12 deletions(-) create mode 100644 core/wisdom-maven-plugin/src/main/resources/templates/error/pipeline.thl.html diff --git a/core/wisdom-maven-plugin/src/main/java/org/wisdom/maven/mojos/CreateMojo.java b/core/wisdom-maven-plugin/src/main/java/org/wisdom/maven/mojos/CreateMojo.java index b14704617..cefffaf31 100644 --- a/core/wisdom-maven-plugin/src/main/java/org/wisdom/maven/mojos/CreateMojo.java +++ b/core/wisdom-maven-plugin/src/main/java/org/wisdom/maven/mojos/CreateMojo.java @@ -196,6 +196,11 @@ private void copyDefaultErrorTemplates() throws IOException { is = CreateMojo.class.getClassLoader().getResourceAsStream("templates/error/500.thl.html"); FileUtils.copyInputStreamToFile(is, new File(error, "500.thl.html")); IOUtils.closeQuietly(is); + + // Copy pipeline + is = CreateMojo.class.getClassLoader().getResourceAsStream("templates/error/pipeline.thl.html"); + FileUtils.copyInputStreamToFile(is, new File(error, "pipeline.thl.html")); + IOUtils.closeQuietly(is); } private void createPackageStructure() { diff --git a/core/wisdom-maven-plugin/src/main/resources/templates/error/pipeline.thl.html b/core/wisdom-maven-plugin/src/main/resources/templates/error/pipeline.thl.html new file mode 100644 index 000000000..30ffa5f93 --- /dev/null +++ b/core/wisdom-maven-plugin/src/main/resources/templates/error/pipeline.thl.html @@ -0,0 +1,161 @@ + + + + + + message + + + + +

ERROR TYPE

+ +

+ An error occurred while processing FILE_NAME at line + line:char. +

+ +
+
+
+ + + + + \ No newline at end of file diff --git a/framework/default-error-handler/src/main/java/org/wisdom/error/DefaultPageErrorHandler.java b/framework/default-error-handler/src/main/java/org/wisdom/error/DefaultPageErrorHandler.java index 81e292a00..48458e75d 100644 --- a/framework/default-error-handler/src/main/java/org/wisdom/error/DefaultPageErrorHandler.java +++ b/framework/default-error-handler/src/main/java/org/wisdom/error/DefaultPageErrorHandler.java @@ -19,22 +19,27 @@ */ package org.wisdom.error; -import org.apache.felix.ipojo.annotations.Component; -import org.apache.felix.ipojo.annotations.Instantiate; -import org.apache.felix.ipojo.annotations.Provides; -import org.apache.felix.ipojo.annotations.Requires; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.commons.io.FileUtils; +import org.apache.felix.ipojo.annotations.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.wisdom.api.DefaultController; +import org.wisdom.api.configuration.ApplicationConfiguration; +import org.wisdom.api.content.Json; import org.wisdom.api.http.*; +import org.wisdom.api.http.Context; import org.wisdom.api.interception.Filter; import org.wisdom.api.interception.RequestContext; import org.wisdom.api.router.Route; import org.wisdom.api.router.Router; import org.wisdom.api.templates.Template; +import java.io.File; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; @@ -71,12 +76,32 @@ public class DefaultPageErrorHandler extends DefaultController implements Filter @Requires(filter = "(name=error/500)", proxy = false, optional = true, id = "500") private Template internalerror; + /** + * The 500 template. + */ + @Requires(filter = "(name=error/pipeline)", proxy = false, optional = true, id = "pipeline") + private Template pipeline; + /** * The router. */ @Requires protected Router router; + @Requires + protected ApplicationConfiguration configuration; + + @Requires + protected Json json; + + private File error; + + + @Validate + public void start() { + error = new File(configuration.getBaseDir().getParentFile(), "pipeline/error.json"); + } + /** * Generates the error page. * @@ -85,7 +110,7 @@ public class DefaultPageErrorHandler extends DefaultController implements Filter * @param e the thrown error * @return the HTTP result serving the error page */ - private Result onError(Context context, Route route, Throwable e) { + private Result renderInternalError(Context context, Route route, Throwable e) { Throwable localException = e; // If the template is not there, just wrap the exception within a JSON Object. @@ -169,6 +194,26 @@ public static List cleanup(StackTraceElement[] stack) { */ @Override public Result call(Route route, RequestContext context) throws Exception { + + // Manage the error file. + // In dev mode, if the watching pipeline throws an error, this error is stored in the error.json file + // If this file exist, we should display a page telling the user that something terrible happened in his last + // change. + if (configuration.isDev() && context.request().accepts(MimeTypes.HTML) && pipeline != null) { + // Check whether the error file is there + if (error.isFile()) { + logger().debug("Error file detected, preparing rendering"); + try { + return renderPipelineError(); + } catch (IOException e) { + LOGGER.error("An exception occurred while generating the error page for {} {}", + route.getHttpMethod(), + route.getUrl(), e); + return renderInternalError(context.context(), route, e); + } + } + } + try { Result result = context.proceed(); if (result.getStatusCode() == NOT_FOUND) { @@ -176,19 +221,53 @@ public Result call(Route route, RequestContext context) throws Exception { if (route.getHttpMethod() == HttpMethod.HEAD) { return switchToGet(route, context); } - - - return noRoute(route, result); + return renderNotFound(route, result); } return result; } catch (Exception e) { LOGGER.error("An exception occurred while processing request {} {}", route.getHttpMethod(), route.getUrl(), e); - return onError(context.context(), route, e); + return renderInternalError(context.context(), route, e); + } + } + + private Result renderPipelineError() throws IOException { + String content = FileUtils.readFileToString(error); + ObjectNode node = (ObjectNode) json.parse(content); + + String message = node.get("message").asText(); + String file = node.get("file").asText(); + int line = -1; + int character = -1; + + if (node.get("line") != null) { + line = node.get("line").asInt(); + } + + if (node.get("character") != null) { + character = node.get("character").asInt(); + } + + String fileContent = ""; + InterestingLines lines = null; + File source = new File(file); + if (source.isFile()) { + fileContent = FileUtils.readFileToString(source); + if (line != -1 && line != 0) { + lines = extractInterestedLines(fileContent, line, 4); + } } + + return internalServerError(render(pipeline, + "message", message, + "file", source, + "line", line, + "character", character, + "lines", lines, + "content", fileContent)); } - private Result noRoute(Route route, Result result) { + private Result renderNotFound(Route route, Result result) { if (noroute == null) { return result; } else { @@ -204,7 +283,7 @@ private Result switchToGet(Route route, RequestContext context) { // A HEAD request was emitted, and unfortunately, no action handled it. Switch to GET. Route getRoute = router.getRouteFor(HttpMethod.GET, route.getUrl()); if (getRoute == null || getRoute.isUnbound()) { - return noRoute(route, Results.notFound()); + return renderNotFound(route, Results.notFound()); } else { try { Result result = getRoute.invoke(); @@ -233,7 +312,7 @@ private Result switchToGet(Route route, RequestContext context) { } catch (Exception exception) { LOGGER.error("An exception occurred while processing request {} {}", route.getHttpMethod(), route.getUrl(), exception); - return onError(context.context(), route, exception); + return renderInternalError(context.context(), route, exception); } } } @@ -259,4 +338,43 @@ public Pattern uri() { public int priority() { return 1000; } + + public static class InterestingLines { + + public final int firstLine; + public final int errorLine; + public final String[] focus; + + public InterestingLines(int firstLine, String[] focus, int errorLine) { + this.firstLine = firstLine; + this.errorLine = errorLine; + this.focus = focus; + } + + } + + /** + * Extracts interesting lines to be displayed to the user. + * + * @param source the source + * @param line the line responsible of the error + * @param border number of lines to use as a border + */ + public InterestingLines extractInterestedLines(String source, int line, int border) { + try { + if (source == null) { + return null; + } + String[] lines = source.split("\n"); + int firstLine = Math.max(0, line - border); + int lastLine = Math.min(lines.length - 1, line + border); + List focusOn = new ArrayList<>(); + focusOn.addAll(Arrays.asList(lines).subList(firstLine, lastLine + 1)); + return new InterestingLines(firstLine + 1, focusOn.toArray(new String[focusOn.size()]), line - firstLine - 1); + } catch (Exception e) { + logger().error("Cannot extract the interesting lines", e); + return null; + } + } + } diff --git a/framework/default-error-handler/src/test/java/org/wisdom/error/DefaultPageErrorHandlerTest.java b/framework/default-error-handler/src/test/java/org/wisdom/error/DefaultPageErrorHandlerTest.java index fa99b560b..81217fe04 100644 --- a/framework/default-error-handler/src/test/java/org/wisdom/error/DefaultPageErrorHandlerTest.java +++ b/framework/default-error-handler/src/test/java/org/wisdom/error/DefaultPageErrorHandlerTest.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.wisdom.api.Controller; import org.wisdom.api.DefaultController; +import org.wisdom.api.configuration.ApplicationConfiguration; import org.wisdom.api.http.HttpMethod; import org.wisdom.api.http.MimeTypes; import org.wisdom.api.http.Result; @@ -76,6 +77,9 @@ public void switchToHeadWhenGetRouteExist() throws Exception { DefaultPageErrorHandler handler = new DefaultPageErrorHandler(); handler.router = mock(Router.class); + handler.configuration = mock(ApplicationConfiguration.class); + when(handler.configuration.isDev()).thenReturn(false); + Controller controller = new MyController(); Route route = new Route(HttpMethod.GET, "/", controller, controller.getClass().getMethod("action")); Route reqRoute = new Route(HttpMethod.HEAD, "/", null, null); @@ -97,8 +101,11 @@ public void switchToHeadWhenGetRouteExist() throws Exception { @Test public void switchToHeadWhenGetRouteDoesNotExist() throws Exception { DefaultPageErrorHandler handler = new DefaultPageErrorHandler(); + handler.configuration = mock(ApplicationConfiguration.class); + when(handler.configuration.isDev()).thenReturn(false); handler.router = mock(Router.class); + Route reqRoute = new Route(HttpMethod.HEAD, "/", null, null); when(handler.router.getRouteFor(HttpMethod.HEAD, "/")).thenReturn(new Route(HttpMethod.HEAD, "/", null, null));