This is an automated email from the ASF dual-hosted git repository.
ddekany pushed a commit to branch 3
in repository https://gitbox.apache.org/repos/asf/freemarker.git
The following commit(s) were added to refs/heads/3 by this push:
new 911f8fa5 Forward ported from 2.3-gae: PR #89 TemplateProcessingTracer
911f8fa5 is described below
commit 911f8fa57c2f2f8959e3518aa3a53712d8e22876
Author: ddekany <[email protected]>
AuthorDate: Tue Dec 19 23:47:22 2023 +0100
Forward ported from 2.3-gae: PR #89 TemplateProcessingTracer
---
.../core/TemplateProcessingTracerTest.java | 234 +++++++++++++++++++++
.../core/ASTDirIfElseIfElseContainer.java | 24 ++-
.../freemarker/core/ASTDirListElseContainer.java | 21 +-
.../org/apache/freemarker/core/ASTElement.java | 12 +-
.../org/apache/freemarker/core/Environment.java | 26 ++-
.../org/apache/freemarker/core/MessageUtils.java | 17 +-
.../java/org/apache/freemarker/core/Template.java | 5 +-
.../freemarker/core/TemplateProcessingTracer.java | 87 ++++++++
8 files changed, 399 insertions(+), 27 deletions(-)
diff --git
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateProcessingTracerTest.java
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateProcessingTracerTest.java
new file mode 100644
index 00000000..36beec6f
--- /dev/null
+++
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateProcessingTracerTest.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.util._NullWriter;
+import org.apache.freemarker.core.util._StringUtils;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public class TemplateProcessingTracerTest {
+
+ private static final String TEMPLATE_TEXT =
+ "<#if 0 == 1>Nope.\n</#if>" +
+ "<#if 1 == 1>Yup.\n</#if>" +
+ "Always.\n" +
+ "<#list [1, 2, 3] as item>\n" +
+ "${item}<#else>\n" +
+ "Nope.\n" +
+ "</#list>\n" +
+ "<#list [] as item>\n" +
+ "${item}<#else>" +
+ "Yup.\n" +
+ "</#list>\n" +
+ "<#list 1..2 as i>${i}</#list>" +
+ "<#list 1..3 as j>${j}<#sep>, </#list>" +
+ "<#attempt>succeed<#recover>not visited</#attempt>" +
+ "<#attempt>will fail${fail}<#recover>recover</#attempt>" +
+ "<@('x'?interpret) />" +
+ "<#if true>t<#else>f</#if>" +
+ "<#if false>t<#else>f</#if>" +
+ "<#if false>t1<#elseIf false>f1<#else>f2</#if>" +
+ "<#if false>t1<#elseIf true>t2<#else>f2</#if>" +
+ "<#switch 2>" +
+ "<#case 1>C1<#break>" +
+ "<#case 2>C2<#break>" +
+ "<#case 3>C3<#break>" +
+ "<#default>D" +
+ "</#switch>" +
+ "<#switch 3>" +
+ "<#case 1>C1<#break>" +
+ "<#case 2>C3<#break>" +
+ "<#default>D" +
+ "</#switch>" +
+ "<#macro m>Hello from m!</#macro>" +
+ "Calling macro: <@m />" +
+ "<#assign t>captured</#assign>" +
+ "\n";
+
+ @Test
+ public void test() throws Exception {
+ Configuration cfg = new
Configuration.Builder(Configuration.VERSION_3_0_0).build();
+ Template t = new Template("test.f3a", TEMPLATE_TEXT, cfg);
+ TestTemplateProcessingTracer tracer = new
TestTemplateProcessingTracer();
+ Environment env = t.createProcessingEnvironment(null,
_NullWriter.INSTANCE);
+ env.setTemplateProcessingTracer(tracer);
+ env.process();
+
+ System.out.println();
+ for (String it : tracer.leafElementSourceSnippets) {
+ System.out.println(_StringUtils.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}",
+ "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",
+ " #attempt",
+ " text \"succeed\"",
+ " #attempt",
+ " #mixedContent",
+ " 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 = .nestedOutput",
+ " text \"captured\"",
+ " text \"\\n\""
+ ),
+ tracer.indentedElementDescriptions);
+ }
+
+ private static class TestTemplateProcessingTracer implements
TemplateProcessingTracer {
+ private final List<String> leafElementSourceSnippets = new
ArrayList<>();
+ private final List<String> indentedElementDescriptions = new
ArrayList<>();
+ private String indentation = null;
+
+ public void enterElement(Environment env, TracedElement tracedElement)
{
+ if (indentation == null) {
+ indentation = "";
+ } else {
+ indentation += " ";
+ }
+
+ indentedElementDescriptions.add(indentation +
tracedElement.getLabelWithParameters());
+
+ if (tracedElement.isLeaf()) {
+ int beginColumn = tracedElement.getBeginColumn();
+ int beginLine = tracedElement.getBeginLine();
+ int endLine = tracedElement.getEndLine();
+ int endColumn = tracedElement.getEndColumn();
+
+ 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(Environment env) {
+ indentation = indentation.isEmpty() ? null :
indentation.substring(0, indentation.length() - 1);
+ }
+ }
+
+}
diff --git
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfElseIfElseContainer.java
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfElseIfElseContainer.java
index 1c8fad7c..f2c3b6a4 100644
---
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfElseIfElseContainer.java
+++
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirIfElseIfElseContainer.java
@@ -40,12 +40,24 @@ final class ASTDirIfElseIfElseContainer extends
ASTDirective {
@Override
ASTElement[] execute(Environment env) throws TemplateException,
IOException {
int ln = getChildCount();
- for (int i = 0; i < ln; i++) {
- ASTDirIfOrElseOrElseIf cblock = (ASTDirIfOrElseOrElseIf)
fastGetChild(i);
- ASTExpression 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++) {
+ ASTDirIfOrElseOrElseIf cblock = (ASTDirIfOrElseOrElseIf)
fastGetChild(i);
+ ASTExpression condition = cblock.condition;
+ env.replaceElementStackTop(cblock);
+ if (condition == null || condition.evalToBoolean(env)) {
+ return cblock.getChildBuffer();
+ }
+ }
+ } else {
+ for (int i = 0; i < ln; i++) {
+ ASTDirIfOrElseOrElseIf cblock = (ASTDirIfOrElseOrElseIf)
fastGetChild(i);
+ ASTExpression condition = cblock.condition;
+ env.replaceElementStackTop(cblock);
+ if (condition == null || condition.evalToBoolean(env)) {
+ env.executeElement(cblock);
+ return null;
+ }
}
}
return null;
diff --git
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirListElseContainer.java
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirListElseContainer.java
index 47cc9f76..a3a9383c 100644
---
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirListElseContainer.java
+++
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirListElseContainer.java
@@ -38,10 +38,25 @@ class ASTDirListElseContainer extends ASTDirective {
@Override
ASTElement[] execute(Environment env) throws TemplateException,
IOException {
- if (!listPart.acceptWithResult(env)) {
- return elsePart.execute(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);
+ }
}
- return null;
+
+ if (hadItems) {
+ return null;
+ }
+
+ return new ASTElement[] { elsePart };
}
@Override
diff --git
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java
index d26ce03c..35583c45 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java
@@ -29,7 +29,7 @@ import java.util.NoSuchElementException;
* or other such non-expression node in the parsed template.
*/
//TODO [FM3] will be public
-abstract class ASTElement extends ASTNode {
+abstract class ASTElement extends ASTNode implements
TemplateProcessingTracer.TracedElement {
private static final int INITIAL_CHILD_BUFFER_CAPACITY = 6;
@@ -72,8 +72,9 @@ abstract class ASTElement extends ASTNode {
/**
* Single line description of the element, which contain information about
what kind of element it is, what are its
* parameters, but doesn't contain the child nodes. Meant to be used for
stack traces, also for tree views (that
- * don't want to show the parameters as spearate nodes). There are no
backward-compatibility guarantees regarding
- * the format used at the moment.
+ * don't want to go 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.
*
* @see #getLabelWithoutParameters()
* @see #getCanonicalForm()
@@ -144,6 +145,11 @@ abstract class ASTElement extends ASTNode {
return childCount;
}
+ @Override
+ public final boolean isLeaf() {
+ return getChildCount() == 0;
+ }
+
/**
* Return the child node at the given index.
*
diff --git
a/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
index 5351d0ca..3e2dd267 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
@@ -161,6 +161,8 @@ public final class Environment extends
MutableProcessingConfiguration<Environmen
private boolean fastInvalidReferenceExceptions;
+ private TemplateProcessingTracer templateProcessingTracer;
+
/**
* Retrieves the environment object associated with the current thread, or
{@code null} if there's no template
* processing going on in this thread. Data model implementations that
need access to the environment can call this
@@ -329,6 +331,21 @@ public final class Environment extends
MutableProcessingConfiguration<Environmen
}
}
+ /**
+ * Sets the {@link TemplateProcessingTracer} to use for this {@link
Environment};
+ * can be {@code null} to not have one. The default is also {@code null}.
+ */
+ public void setTemplateProcessingTracer(TemplateProcessingTracer
templateProcessingTracer) {
+ this.templateProcessingTracer = templateProcessingTracer;
+ }
+
+ /**
+ * Getter pair of {@link
#setTemplateProcessingTracer(TemplateProcessingTracer)}. Can be {@code null}.
+ */
+ public TemplateProcessingTracer getTemplateProcessingTracer() {
+ return templateProcessingTracer;
+ }
+
/**
* "Visit" the template element.
*/
@@ -351,7 +368,7 @@ public final class Environment extends
MutableProcessingConfiguration<Environmen
} finally {
popElement();
}
- // ATTENTION: This method body above is manually "inlined" into
visit(ASTElement[]); keep them in sync!
+ // ATTENTION: This method body above is manually "inlined" into
executeElements(ASTElement[]); keep them in sync!
}
// TODO [FM3] will be public
@@ -2410,9 +2427,16 @@ public final class Environment extends
MutableProcessingConfiguration<Environmen
this.instructionStack = instructionStack;
}
instructionStack[newSize - 1] = element;
+ if (templateProcessingTracer != null) {
+ templateProcessingTracer.enterElement(this, element);
+ }
}
void popElement() {
+ if (templateProcessingTracer != null) {
+ ASTElement element = instructionStack[instructionStackSize - 1];
+ templateProcessingTracer.exitElement(this);
+ }
instructionStackSize--;
}
diff --git
a/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtils.java
b/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtils.java
index a139a0aa..00f420be 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtils.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtils.java
@@ -19,17 +19,7 @@
package org.apache.freemarker.core;
-import java.util.Arrays;
-
-import org.apache.freemarker.core.model.TemplateBooleanModel;
-import org.apache.freemarker.core.model.TemplateCollectionModel;
-import org.apache.freemarker.core.model.TemplateDateModel;
-import org.apache.freemarker.core.model.TemplateIterableModel;
-import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
-import org.apache.freemarker.core.model.TemplateModel;
-import org.apache.freemarker.core.model.TemplateNumberModel;
-import org.apache.freemarker.core.model.TemplateSequenceModel;
-import org.apache.freemarker.core.model.TemplateStringModel;
+import org.apache.freemarker.core.model.*;
import org.apache.freemarker.core.util.BugException;
import org.apache.freemarker.core.util.StringToIndexMap;
import org.apache.freemarker.core.util.TemplateLanguageUtils;
@@ -39,6 +29,8 @@ import
org.apache.freemarker.core.valueformat.TemplateNumberFormat;
import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
import
org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+import java.util.Arrays;
+
/**
* Utilities for creating error messages (and other messages).
*/
@@ -200,7 +192,8 @@ class MessageUtils {
&& !(argExp instanceof ASTExpDot)
&& !(argExp instanceof ASTExpDynamicKeyName)
&& !(argExp instanceof ASTExpFunctionCall)
- && !(argExp instanceof ASTExpBuiltIn);
+ && !(argExp instanceof ASTExpBuiltIn)
+ && !(argExp instanceof ASTExpParenthesis);
if (needParen) sb.append('(');
sb.append(argExp.getCanonicalForm());
if (needParen) sb.append(')');
diff --git
a/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java
b/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java
index 7a3823b5..e48e03e9 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java
@@ -789,7 +789,8 @@ public class Template implements ProcessingConfiguration,
CustomStateScope {
*
* @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 org.apache.freemarker.core.ASTNode#getSource()
@@ -812,7 +813,7 @@ public class Template implements ProcessingConfiguration,
CustomStateScope {
}
}
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/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateProcessingTracer.java
b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateProcessingTracer.java
new file mode 100644
index 00000000..5d472ed3
--- /dev/null
+++
b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateProcessingTracer.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+package org.apache.freemarker.core;
+
+/**
+ * Hooks to monitor as templates run. This may be used to implement profiling,
coverage analysis, execution tracing,
+ * and other on-the-fly debugging mechanisms.
+ * <p>
+ * Use {@link
Environment#setTemplateProcessingTracer(TemplateProcessingTracer)} to set a
tracer for the current
+ * environment.
+ */
+public interface TemplateProcessingTracer {
+
+ /**
+ * Invoked by {@link Environment} whenever it starts processing a new
template element. A template element is a
+ * directive call, an interpolation (like <code>${...}</code>), a comment
block, or static text. Expressions
+ * are not template elements.
+ */
+ void enterElement(Environment env, TracedElement tracedElement);
+
+ /**
+ * Invoked by {@link Environment} whenever it completes processing a new
template element.
+ *
+ * @see #enterElement(Environment, TracedElement)
+ */
+ 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 getLabelWithParameters();
+ }
+
+}