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();
+    }
+
+}

Reply via email to