All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.quarkus.qute.runtime.devmode.QuteErrorPageSetup Maven / Gradle / Ivy

There is a newer version: 3.17.0.CR1
Show newest version
package io.quarkus.qute.runtime.devmode;

import java.io.BufferedReader;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.stream.Collectors;

import org.jboss.logging.Logger;

import io.quarkus.dev.ErrorPageGenerators;
import io.quarkus.dev.spi.HotReplacementContext;
import io.quarkus.dev.spi.HotReplacementSetup;
import io.quarkus.qute.Engine;
import io.quarkus.qute.Escaper;
import io.quarkus.qute.EvalContext;
import io.quarkus.qute.ReflectionValueResolver;
import io.quarkus.qute.Template;
import io.quarkus.qute.ValueResolver;
import io.quarkus.runtime.TemplateHtmlBuilder;

public class QuteErrorPageSetup implements HotReplacementSetup {

    private static final Logger LOG = Logger.getLogger(QuteErrorPageSetup.class);

    private static final String TEMPLATE_EXCEPTION = "io.quarkus.qute.TemplateException";
    private static final String ORIGIN = "io.quarkus.qute.TemplateNode$Origin";

    private static final String PROBLEM_TEMPLATE = ""
            + "

#{problemIndex} {title}

\n" + "
{description}
\n" + "
\n" + "{#if realLines.get(0) > 1}...
{/if}\n" + "{#for line in sourceLines}\n" // highlight the error line - start + "{#if lineNumber is realLines.get(index)}
{/if}\n" // line number + "{realLines.get(index).pad}\n" // line content + " {line}\n" // highlight the error line - end + "{#if lineNumber is realLines.get(index)}
{#else}
{/if}\n" // point to error + "{#if lineNumber is realLines.get(index)}{space.pad}{#for i in lineCharacterStart}={/for}^
{/if}\n" + "{/for}\n" + "{#if endLinesSkipped}...{/if}\n" + "
"; private HotReplacementContext hotReplacementContext; @Override public void setupHotDeployment(HotReplacementContext context) { this.hotReplacementContext = context; ErrorPageGenerators.register(TEMPLATE_EXCEPTION, this::generatePage); } String generatePage(Throwable exception) { Escaper escaper = Escaper.builder().add('"', """).add('\'', "'") .add('&', "&").add('<', "<").add('>', ">").build(); Template problemTemplate = Engine.builder().addDefaults().addValueResolver(new ReflectionValueResolver()) .addValueResolver(new ValueResolver() { public boolean appliesTo(EvalContext context) { return context.getName().equals("pad"); } @Override public CompletionStage resolve(EvalContext context) { return CompletableFuture.completedFuture(htmlPadRight(context.getBase().toString(), 5)); } }) .build() .parse(PROBLEM_TEMPLATE); TemplateHtmlBuilder builder; List problems; Throwable[] suppressed = exception.getSuppressed(); if (suppressed.length == 0) { problems = Collections.singletonList(exception); } else { problems = Arrays.asList(suppressed); } String problemsFound = "Found " + problems.size() + " Qute problems"; builder = new TemplateHtmlBuilder("Error restarting Quarkus", problemsFound, problemsFound); // Attempt to sort problems by line Collections.sort(problems, new Comparator() { @Override public int compare(Throwable t1, Throwable t2) { Object o1 = getOrigin(t1); Object o2 = getOrigin(t2); if (o1 == o2) { return 0; } else if (o1 == null && o2 != null) { return -1; } else if (o1 != null && o2 == null) { return 1; } return Integer.compare(getLine(o1), getLine(o2)); } }); for (ListIterator it = problems.listIterator(); it.hasNext();) { Throwable problem = it.next(); builder.append(getProblemInfo(it.previousIndex() + 1, problem, problemTemplate, escaper)); } return builder.toString(); } String getProblemInfo(int index, Throwable problem, Template problemTemplate, Escaper escaper) { Object origin = getOrigin(problem); String[] messageLines = problem.getMessage().split("\\r?\\n"); if (origin == null) { return Arrays.stream(messageLines).collect(Collectors.joining("
")); } String templateId = getTemplateId(origin); int lineNumber = getLine(origin); int lineCharacterStart = getLineCharacterStart(origin); List sourceLines = new ArrayList<>(); try (BufferedReader in = getBufferedReader(templateId)) { String line = null; while ((line = in.readLine()) != null) { sourceLines.add(escaper.escape(line).replace(" ", " ")); } } catch (Exception e) { LOG.warn("Unable to read the template source: " + templateId, e); } List realLines = new ArrayList<>(); boolean endLinesSkipped = false; if (sourceLines.size() > 15) { // Line with error plus few surrounding lines int fromIndex = lineNumber > 7 ? (lineNumber - 8) : 0; int toIndex = (lineNumber + 7) > sourceLines.size() ? sourceLines.size() : lineNumber + 7; for (int j = fromIndex; j < toIndex; j++) { // e.g. [10,11,12] realLines.add(j + 1); } sourceLines = sourceLines.subList(fromIndex, toIndex); endLinesSkipped = toIndex != sourceLines.size(); } else { for (int j = 0; j < sourceLines.size(); j++) { // [1,2,3] realLines.add(j + 1); } } return problemTemplate .data("problemIndex", index) .data("title", messageLines[0]) .data("description", Arrays.stream(messageLines).skip(1).collect(Collectors.joining("
"))) .data("sourceLines", sourceLines) .data("lineNumber", lineNumber) .data("lineCharacterStart", lineCharacterStart) .data("realLines", realLines) .data("endLinesSkipped", endLinesSkipped) .data("space", " ") .render(); } static String htmlPadRight(String s, int n) { return String.format("%-" + n + "s", s).replace(" ", " "); } private BufferedReader getBufferedReader(String templateId) throws IOException { for (Path resource : hotReplacementContext.getResourcesDir()) { // src/main/resources/templates Path templates = resource.resolve("templates"); if (Files.exists(templates)) { // src/main/resources/templates/items.html Path template = templates.resolve(templateId.replace("\\", "/")); if (Files.exists(template)) { return Files.newBufferedReader(template); } } } throw new IllegalStateException("Template source not available"); } private Object getOrigin(Throwable t) { Object origin = null; try { Method getOrigin = t.getClass().getClassLoader().loadClass(TEMPLATE_EXCEPTION).getMethod("getOrigin"); origin = getOrigin.invoke(t); } catch (NoSuchMethodException | SecurityException | ClassNotFoundException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { LOG.warn("Unable to read the origin field", e); } return origin; } private String getTemplateId(Object origin) { String templateId = null; try { Method getTemplateId = origin.getClass().getClassLoader().loadClass(ORIGIN).getMethod("getTemplateId"); templateId = getTemplateId.invoke(origin).toString(); } catch (NoSuchMethodException | SecurityException | ClassNotFoundException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { LOG.warn("Unable to invoke the TemplateNode.Origin.getTemplateId() method", e); } return templateId; } private int getLine(Object origin) { int line = 0; try { Method getLine = origin.getClass().getClassLoader().loadClass(ORIGIN).getMethod("getLine"); line = (int) getLine.invoke(origin); } catch (NoSuchMethodException | SecurityException | ClassNotFoundException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { LOG.warn("Unable to invoke the TemplateNode.Origin.getLine() method", e); } return line; } private int getLineCharacterStart(Object origin) { int lineCharacter = 0; try { Method getLineCharacter = origin.getClass().getClassLoader().loadClass(ORIGIN).getMethod("getLineCharacterStart"); lineCharacter = (int) getLineCharacter.invoke(origin); } catch (NoSuchMethodException | SecurityException | ClassNotFoundException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { LOG.warn("Unable to invoke the TemplateNode.Origin.getLineCharacterStart() method", e); } return lineCharacter; } }