Skip to content

Commit

Permalink
Add a template presenting pipeline error and enhance the default erro…
Browse files Browse the repository at this point in the history
…r handler to read the error.json and render the provided template (#26)

Signed-off-by: Clement Escoffier <[email protected]>
  • Loading branch information
cescoffier committed Jun 1, 2014
1 parent 8819a0a commit b4d6d07
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<!--
#%L
Wisdom-Framework
%%
Copyright (C) 2013 - 2014 Wisdom Framework
%%
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.
#L%
-->
<!DOCTYPE html>
<html>
<head>
<title th:text="${message}">
message
</title>
<style>
html, body, pre {
margin: 0;
padding: 0;
font-family: Monaco, 'Lucida Console', monospace;
background: #ECECEC;
}

h1 {
margin: 0;
background: #A31012;
padding: 20px 45px;
color: #fff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, .3);
border-bottom: 1px solid #690000;
font-size: 28px;
}

a {
color: #D36D6D;
}

p#detail {
margin: 0;
padding: 15px 45px;
background: #F5A0A0;
border-top: 4px solid #D36D6D;
color: #730000;
text-shadow: 1px 1px 1px rgba(255, 255, 255, .3);
font-size: 14px;
border-bottom: 1px solid #BA7A7A;
}

p#detail.pre {
white-space: pre;
font-size: 13px;
overflow: auto;
}

p#detail input {
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#AE1113), to(#A31012));
border: 1px solid #790000;
padding: 3px 10px;
text-shadow: 1px 1px 0 rgba(0, 0, 0, .5);
color: white;
border-radius: 3px;
cursor: pointer;
font-family: Monaco, 'Lucida Console';
font-size: 12px;
margin: 0 10px;
display: inline-block;
position: relative;
top: -1px;
}

h2 {
margin: 0;
padding: 5px 45px;
font-size: 12px;
background: #333;
color: #fff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, .3);
border-top: 4px solid #2a2a2a;
}

pre {
margin: 0;
border-bottom: 1px solid #DDD;
text-shadow: 1px 1px 1px rgba(255, 255, 255, .5);
position: relative;
font-size: 12px;
}

pre span.line {
text-align: right;
display: inline-block;
padding: 5px 5px;
width: 30px;
background: #D6D6D6;
color: #8B8B8B;
text-shadow: 1px 1px 1px rgba(255, 255, 255, .5);
font-weight: bold;
}

pre span.code {
padding: 5px 5px;
position: absolute;
right: 0;
left: 40px;
}

pre:first-child span.code {
border-top: 4px solid #CDCDCD;
}

pre:first-child span.line {
border-top: 4px solid #B6B6B6;
}

pre.error span.line {
background: #A31012;
color: #fff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, .3);
}

pre.error {
color: #A31012;
}

pre.error span.marker {
background: #A31012;
color: #fff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, .3);
}
</style>
</head>
<body id="wisdom-error-page">
<h1 th:text="${message}">ERROR TYPE</h1>

<h2>
An error occurred while processing <a th:href="'file://' + ${file.getAbsolutePath()}" target="_blank"
th:text="${file.getName()}">FILE_NAME</a> at line
<span th:text="${line}">line</span>:<span th:text="${character}">char</span>.
</h2>

<div id="source-code" th:if="${lines}">
<pre th:each="l, iter : ${lines.focus}"
th:class="${iter.index} == ${lines.errorLine} ? error"><span
class="line" th:text="${lines.firstLine} + ${iter.index}"></span><span
class="code" th:text="${l}"></span></pre>
</div>



</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
*
Expand All @@ -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.
Expand Down Expand Up @@ -169,26 +194,80 @@ public static List<StackTraceElement> 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) {
// HEAD Implementation.
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 {
Expand All @@ -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();
Expand Down Expand Up @@ -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);
}
}
}
Expand All @@ -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<String> 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;
}
}

}
Loading

0 comments on commit b4d6d07

Please sign in to comment.