diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java index 6cbfe28bf..66f121478 100644 --- a/src/main/java/freemarker/core/Environment.java +++ b/src/main/java/freemarker/core/Environment.java @@ -188,7 +188,7 @@ public final class Environment extends Configurable { private boolean fastInvalidReferenceExceptions; - private TemplateProcessingTracer currentTracer; + private TemplateProcessingTracer templateProcessingTracer; /** * Retrieves the environment object associated with the current thread, or {@code null} if there's no template @@ -2882,10 +2882,22 @@ public TemplateModel get(String key) throws TemplateModelException { } /** - * Sets the tracer to use for this environment. + * Sets the {@link TemplateProcessingTracer} to use for this {@link Environment}; + * can be {@code null} to not have one. The default is also {@code null}. + * + * @since 2.3.33 + */ + public void setTemplateProcessingTracer(TemplateProcessingTracer templateProcessingTracer) { + this.templateProcessingTracer = templateProcessingTracer; + } + + /** + * Getter pair of {@link #setTemplateProcessingTracer(TemplateProcessingTracer)}. Can be {@code null}. + * + * @since 2.3.33 */ - public void setTracer(TemplateProcessingTracer tracer) { - currentTracer = tracer; + public TemplateProcessingTracer getTemplateProcessingTracer() { + return templateProcessingTracer; } private void pushElement(TemplateElement element) { @@ -2900,19 +2912,15 @@ private void pushElement(TemplateElement element) { this.instructionStack = instructionStack; } instructionStack[newSize - 1] = element; - if (currentTracer != null) { - currentTracer.enterElement(element.getTemplate(), - element.getBeginColumn(), element.getBeginLine(), - element.getEndColumn(), element.getEndLine(), element.isLeaf()); + if (templateProcessingTracer != null) { + templateProcessingTracer.enterElement(this, element); } } private void popElement() { - if (currentTracer != null) { + if (templateProcessingTracer != null) { TemplateElement element = instructionStack[instructionStackSize - 1]; - currentTracer.exitElement(element.getTemplate(), - element.getBeginColumn(), element.getBeginLine(), - element.getEndColumn(), element.getEndLine()); + templateProcessingTracer.exitElement(this); } instructionStackSize--; } diff --git a/src/main/java/freemarker/core/IfBlock.java b/src/main/java/freemarker/core/IfBlock.java index 223d755cb..52615d2cd 100644 --- a/src/main/java/freemarker/core/IfBlock.java +++ b/src/main/java/freemarker/core/IfBlock.java @@ -42,12 +42,24 @@ void addBlock(ConditionalBlock block) { @Override TemplateElement[] accept(Environment env) throws TemplateException, IOException { int ln = getChildCount(); - for (int i = 0; i < ln; i++) { - ConditionalBlock cblock = (ConditionalBlock) getChild(i); - Expression condition = cblock.condition; - env.replaceElementStackTop(cblock); - if (condition == null || condition.evalToBoolean(env)) { - return cblock.getChildBuffer(); + if (env.getTemplateProcessingTracer() == null) { + for (int i = 0; i < ln; i++) { + ConditionalBlock cblock = (ConditionalBlock) getChild(i); + Expression condition = cblock.condition; + env.replaceElementStackTop(cblock); + if (condition == null || condition.evalToBoolean(env)) { + return cblock.getChildBuffer(); + } + } + } else { + for (int i = 0; i < ln; i++) { + ConditionalBlock cblock = (ConditionalBlock) getChild(i); + Expression condition = cblock.condition; + env.replaceElementStackTop(cblock); + if (condition == null || condition.evalToBoolean(env)) { + env.visit(cblock); + return null; + } } } return null; diff --git a/src/main/java/freemarker/core/ListElseContainer.java b/src/main/java/freemarker/core/ListElseContainer.java index 856e5b02b..4e307c376 100644 --- a/src/main/java/freemarker/core/ListElseContainer.java +++ b/src/main/java/freemarker/core/ListElseContainer.java @@ -37,9 +37,24 @@ public ListElseContainer(IteratorBlock listPart, ElseOfList elsePart) { @Override TemplateElement[] accept(Environment env) throws TemplateException, IOException { - if (listPart.acceptWithResult(env)) { + boolean hadItems; + + TemplateProcessingTracer templateProcessingTracer = env.getTemplateProcessingTracer(); + if (templateProcessingTracer == null) { + hadItems = listPart.acceptWithResult(env); + } else { + templateProcessingTracer.enterElement(env, listPart); + try { + hadItems = listPart.acceptWithResult(env); + } finally { + templateProcessingTracer.exitElement(env); + } + } + + if (hadItems) { return null; } + return new TemplateElement[] { elsePart }; } diff --git a/src/main/java/freemarker/core/TemplateElement.java b/src/main/java/freemarker/core/TemplateElement.java index 6cb9b5411..2e79d9345 100644 --- a/src/main/java/freemarker/core/TemplateElement.java +++ b/src/main/java/freemarker/core/TemplateElement.java @@ -37,7 +37,7 @@ * it. */ @Deprecated -abstract public class TemplateElement extends TemplateObject { +abstract public class TemplateElement extends TemplateObject implements TemplateProcessingTracer.TracedElement { private static final int INITIAL_REGULATED_CHILD_BUFFER_CAPACITY = 6; @@ -89,9 +89,9 @@ abstract public class TemplateElement extends TemplateObject { * One-line description of the element, that contains all the information that is used in * {@link #getCanonicalForm()}, except the nested content (elements) of the element. The expressions inside the * element (the parameters) has to be shown. Meant to be used for stack traces, also for tree views that don't go - * down to the expression-level. There are no backward-compatibility guarantees regarding the format used ATM, but - * it must be regular enough to be machine-parseable, and it must contain all information necessary for restoring an - * AST equivalent to the original. + * down to the expression-level. There are no backward-compatibility guarantees regarding the format used, although + * it shouldn't change unless to fix a bug. It must be regular enough to be machine-parseable, and it must contain + * all information necessary for restoring an AST equivalent to the original. * * This final implementation calls {@link #dump(boolean) dump(false)}. * diff --git a/src/main/java/freemarker/core/TemplateObject.java b/src/main/java/freemarker/core/TemplateObject.java index e535d0839..7ef6cfb46 100644 --- a/src/main/java/freemarker/core/TemplateObject.java +++ b/src/main/java/freemarker/core/TemplateObject.java @@ -86,23 +86,35 @@ void setLocation(Template template, int beginColumn, int beginLine, int endColum this.endColumn = endColumn; this.endLine = endLine; } - - public final int getBeginColumn() { - return beginColumn; - } + /** + * 1-based index of the line (row) of the first character of the element in the template. + */ public final int getBeginLine() { return beginLine; } - public final int getEndColumn() { - return endColumn; + /** + * 1-based index of the column of the first character of the element in the template. + */ + public final int getBeginColumn() { + return beginColumn; } + /** + * 1-based index of the line (row) of the last character of the element in the template. + */ public final int getEndLine() { return endLine; } + /** + * 1-based index of the column of the last character of the element in the template. + */ + public final int getEndColumn() { + return endColumn; + } + /** * Returns a string that indicates * where in the template source, this object is. diff --git a/src/main/java/freemarker/core/TemplateProcessingTracer.java b/src/main/java/freemarker/core/TemplateProcessingTracer.java index 434891e24..2130e6c47 100644 --- a/src/main/java/freemarker/core/TemplateProcessingTracer.java +++ b/src/main/java/freemarker/core/TemplateProcessingTracer.java @@ -19,38 +19,77 @@ package freemarker.core; -import freemarker.ext.util.IdentityHashMap; -import freemarker.template.Configuration; import freemarker.template.Template; -import freemarker.template.TemplateDirectiveModel; -import freemarker.template.TemplateTransformModel; -import freemarker.template.utility.ObjectFactory; /** - * Run-time tracer plug-in. This may be * used to implement profiling, coverage analytis, execution tracing, + * Hooks to monitor as templates run. This may be used to implement profiling, coverage analysis, execution tracing, * and other on-the-fly debugging mechanisms. *

- * Use {@link Environment#setTracer(TemplateProcessingTracer)} to configure a tracer for the current environment. + * Use {@link Environment#setTemplateProcessingTracer(TemplateProcessingTracer)} to set a tracer for the current + * environment. * * @since 2.3.33 */ public interface TemplateProcessingTracer { /** - * Invoked by {@link Environment} whenever it starts processing a new template element. {@code - * isLeafElement} indicates whether this element is a leaf, or whether the tracer should expect - * to receive lower-level elements within the context of this one. + * Invoked by {@link Environment} whenever it starts processing a new template element. A template element is a + * directive call, an interpolation (like ${...}), a comment block, or static text. Expressions + * are not template elements. * * @since 2.3.23 */ - void enterElement(Template template, int beginColumn, int beginLine, int endColumn, int endLine, - boolean isLeafElement); + void enterElement(Environment env, TracedElement tracedElement); /** * Invoked by {@link Environment} whenever it completes processing a new template element. + * + * @see #enterElement(Environment, TracedElement) * * @since 2.3.23 */ - void exitElement(Template template, int beginColumn, int beginLine, int endColumn, int endLine); + void exitElement(Environment env); + + /** + * Information about the template element that we enter of exit. + */ + interface TracedElement { + /** + * The {@link Template} that contains this element. + */ + Template getTemplate(); + + /** + * 1-based index of the line (row) of the first character of the element in the template. + */ + int getBeginLine(); + + /** + * 1-based index of the column of the first character of the element in the template. + */ + int getBeginColumn(); + + /** + * 1-based index of the line (row) of the last character of the element in the template. + */ + int getEndColumn(); + + /** + * 1-based index of the column of the last character of the element in the template. + */ + int getEndLine(); + + /** + * If this is an element that has no nested elements. + */ + boolean isLeaf(); + + /** + * One-line description of the element, that also contains the parameter expressions, but not the nested content + * (child elements). There are no hard backward-compatibility guarantees regarding the format used, although + * it shouldn't change unless to fix a bug. + */ + String getDescription(); + } } diff --git a/src/main/java/freemarker/template/Template.java b/src/main/java/freemarker/template/Template.java index 967d222f5..c6fcc31fd 100644 --- a/src/main/java/freemarker/template/Template.java +++ b/src/main/java/freemarker/template/Template.java @@ -764,7 +764,8 @@ public void addImport(LibraryLoad ll) { * * @param beginColumn the first column of the requested source, 1-based * @param beginLine the first line of the requested source, 1-based - * @param endColumn the last column of the requested source, 1-based + * @param endColumn the last column of the requested source, 1-based. If this is beyond the last character of the + * line, it assumes that you want to whole line. * @param endLine the last line of the requested source, 1-based * * @see freemarker.core.TemplateObject#getSource() @@ -787,7 +788,7 @@ public String getSource(int beginColumn, } } int lastLineLength = lines.get(endLine).toString().length(); - int trailingCharsToDelete = lastLineLength - endColumn - 1; + int trailingCharsToDelete = endColumn < lastLineLength ? lastLineLength - endColumn - 1 : 0; buf.delete(0, beginColumn); buf.delete(buf.length() - trailingCharsToDelete, buf.length()); return buf.toString(); diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index 26bc653dc..d5969fe66 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -30140,6 +30140,21 @@ TemplateModel x = env.getVariable("x"); // get variable x + + + GitHub + PR 89: Added TemplateProcessingTracer + mechanism, that can be used to monitor coverage, and performance + inside templates as they are being + processed. For example, you could construct a heat map for how + often the different parts run, or finding the performance hot + spots. (There can be other creative uses, like watching for a + variable to have a certain value.) Use + Environment.setTemplateProcessingTracer(TemplateProcessingTracer) + to enable this kind of monitoring. (See the API docs for + more.) + diff --git a/src/test/java/freemarker/core/TemplateProcessingTracerTest.java b/src/test/java/freemarker/core/TemplateProcessingTracerTest.java index f069dc1b8..c73554d8c 100644 --- a/src/test/java/freemarker/core/TemplateProcessingTracerTest.java +++ b/src/test/java/freemarker/core/TemplateProcessingTracerTest.java @@ -20,15 +20,15 @@ import static org.junit.Assert.*; -import java.io.StringWriter; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import org.junit.Test; import freemarker.template.Configuration; import freemarker.template.Template; +import freemarker.template.utility.NullWriter; +import freemarker.template.utility.StringUtil; public class TemplateProcessingTracerTest { @@ -43,41 +43,203 @@ public class TemplateProcessingTracerTest { "<#list [] as item>\n" + "${item}<#else>" + "Yup.\n" + - "\n"; + "\n" + + "<#list 1..2 as i>${i}" + + "<#list 1..3 as j>${j}<#sep>, " + + "<#foreach k in 1..2>k=${k}" + + "<#attempt>succeed<#recover>not visited" + + "<#attempt>will fail${fail}<#recover>recover" + + "<@('x'?interpret) />" + + "<#if true>t<#else>f" + + "<#if false>t<#else>f" + + "<#if false>t1<#elseif false>f1<#else>f2" + + "<#if false>t1<#elseif true>t2<#else>f2" + + "<#switch 2>" + + "<#case 1>C1<#break>" + + "<#case 2>C2<#break>" + + "<#case 3>C3<#break>" + + "<#default>D" + + "" + + "<#switch 3>" + + "<#case 1>C1<#break>" + + "<#case 2>C3<#break>" + + "<#default>D" + + "" + + "<#macro m>Hello from m!" + + "Calling macro: <@m />" + + "<#assign t>captured" + + "\n"; @Test public void test() throws Exception { Configuration cfg = new Configuration(Configuration.VERSION_2_3_32); Template t = new Template("test.ftl", TEMPLATE_TEXT, cfg); - StringWriter sw = new StringWriter(); - Tracer tracer = new Tracer(TEMPLATE_TEXT); - Environment env = t.createProcessingEnvironment(null, sw); - env.setTracer(tracer); + TestTemplateProcessingTracer tracer = new TestTemplateProcessingTracer(); + Environment env = t.createProcessingEnvironment(null, NullWriter.INSTANCE); + env.setTemplateProcessingTracer(tracer); env.process(); - List expected = Arrays.asList("Yup.", "Always.", "${item}", "${item}", "${item}", "Yup."); - assertEquals(expected, tracer.elementsVisited); + System.out.println(); + for (String it : tracer.leafElementSourceSnippets) { + System.out.println(StringUtil.jQuote(it) + ","); + } + System.out.println(); + for (String it : tracer.indentedElementDescriptions) { + System.out.println("|" + it); + } + System.out.println(); + + assertEquals( + List.of( + "Yup.\n", + "Always.\n", + "${item}", + "${item}", + "${item}", + "Yup.\n", + "${i}", + "${i}", + "${j}", + ", ", + "${j}", + ", ", + "${j}", + "k=", + "${k}", + "k=", + "${k}", + "succeed", + "will fail", + "${fail}", + "recover", + "<@('x'?interpret) />", + "x", + "t", + "f", + "f2", + "t2", + "C2", + "<#break>", + "D", + "Calling macro: ", + "<@m />", + "Hello from m!", + "captured", + "\n" + ), + tracer.leafElementSourceSnippets); + + assertEquals( + List.of( + "root", + " #if 0 == 1", + " #if 1 == 1", + " text \"Yup.\\n\"", + " text \"Always.\\n\"", + " #list-#else-container", + " #list [1, 2, 3] as item", + " ${item}", + " ${item}", + " ${item}", + " #list-#else-container", + " #list [] as item", + " #else", + " text \"Yup.\\n\"", + " #list 1..2 as i", + " ${i}", + " ${i}", + " #list 1..3 as j", + " ${j}", + " #sep", + " text \", \"", + " ${j}", + " #sep", + " text \", \"", + " ${j}", + " #sep", + " #foreach k in 1..2", + " text \"k=\"", + " ${k}", + " text \"k=\"", + " ${k}", + " #attempt", + " text \"succeed\"", + " #attempt", + " #mixed_content", + " text \"will fail\"", + " ${fail}", + " #recover", + " text \"recover\"", + " @(\"x\"?interpret)", + " text \"x\"", + " #if-#elseif-#else-container", + " #if true", + " text \"t\"", + " #if-#elseif-#else-container", + " #else", + " text \"f\"", + " #if-#elseif-#else-container", + " #else", + " text \"f2\"", + " #if-#elseif-#else-container", + " #elseif true", + " text \"t2\"", + " #switch 2", + " #case 2", + " text \"C2\"", + " #break", + " #switch 3", + " #default", + " text \"D\"", + " #macro m", + " text \"Calling macro: \"", + " @m", + " #macro m", + " text \"Hello from m!\"", + " #assign t = .nested_output", + " text \"captured\"", + " text \"\\n\"" + ), + tracer.indentedElementDescriptions); } - private static class Tracer implements TemplateProcessingTracer { - final ArrayList elementsVisited; - final String[] templateLines; + private static class TestTemplateProcessingTracer implements TemplateProcessingTracer { + private final List leafElementSourceSnippets = new ArrayList<>(); + private final List indentedElementDescriptions = new ArrayList<>(); + private String indentation = null; - Tracer(String template) { - elementsVisited = new ArrayList<>(); - templateLines = template.split("\\n"); - } + public void enterElement(Environment env, TracedElement tracedElement) { + if (indentation == null) { + indentation = ""; + } else { + indentation += " "; + } + + indentedElementDescriptions.add(indentation + tracedElement.getDescription()); + + if (tracedElement.isLeaf()) { + int beginColumn = tracedElement.getBeginColumn(); + int beginLine = tracedElement.getBeginLine(); + int endLine = tracedElement.getEndLine(); + int endColumn = tracedElement.getEndColumn(); - public void enterElement(Template template, int beginColumn, int beginLine, int endColumn, int endLine, - boolean isLeafElement) { - if (isLeafElement) { - String line = templateLines[beginLine - 1]; - String elementText = line.substring(beginColumn - 1, - endLine == beginLine ? Math.min(endColumn, line.length()) : line.length()); - elementsVisited.add(elementText); + String suffix; + if (beginLine != endLine) { + endLine = beginLine; + endColumn = Integer.MAX_VALUE; + suffix = "[...]"; + } else { + suffix = ""; + } + + String sourceQuotation = tracedElement.getTemplate() + .getSource(beginColumn, beginLine, endColumn, endLine); + leafElementSourceSnippets.add(sourceQuotation + suffix); } } - public void exitElement(Template template, int beginColumn, int beginLine, int endColumn, int endLine) {} + public void exitElement(Environment env) { + indentation = indentation.isEmpty() ? null : indentation.substring(0, indentation.length() - 1); + } } }