diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java index 0bd264bb0..042f42f96 100644 --- a/src/main/java/freemarker/core/Environment.java +++ b/src/main/java/freemarker/core/Environment.java @@ -188,6 +188,8 @@ public final class Environment extends Configurable { private boolean fastInvalidReferenceExceptions; + private TemplateProcessingTracer currentTracer; + /** * 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 @@ -300,6 +302,13 @@ private void clearCachedValues() { cachedURLEscapingCharsetSet = false; } + /** + * Sets the tracer to use for this environment. + */ + public void setTracer(TemplateProcessingTracer tracer) { + currentTracer = tracer; + } + /** * Processes the template to which this environment belongs to. */ @@ -311,12 +320,14 @@ public void process() throws TemplateException, IOException { clearCachedValues(); try { doAutoImportsAndIncludes(this); + if (currentTracer != null) currentTracer.start(); visit(getTemplate().getRootTreeNode()); // It's here as we must not flush if there was an exception. if (getAutoFlush()) { out.flush(); } } finally { + if (currentTracer != null) currentTracer.end(); // It's just to allow the GC to free memory... clearCachedValues(); } @@ -2883,6 +2894,10 @@ private void pushElement(TemplateElement element) { this.instructionStack = instructionStack; } instructionStack[newSize - 1] = element; + if (currentTracer != null) { + currentTracer.trace(element.getTemplate(), element.getBeginColumn(), element.getBeginLine(), + element.getEndColumn(), element.getEndLine(), element.isLeaf()); + } } private void popElement() { diff --git a/src/main/java/freemarker/core/TemplateProcessingTracer.java b/src/main/java/freemarker/core/TemplateProcessingTracer.java new file mode 100644 index 000000000..0075a764f --- /dev/null +++ b/src/main/java/freemarker/core/TemplateProcessingTracer.java @@ -0,0 +1,63 @@ +/* + * 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 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, + * and other on-the-fly debugging mechanisms. + *

+ * Use {@link Environment#setTracer(TemplateProcessingTracer)} to configure a tracer for the current environment. + * + * @since 2.3.33 + */ +public interface TemplateProcessingTracer { + + /** + * Invoked when {@link Template.process()} starts processing the template. + * + * @since 2.3.23 + */ + void start(); + + /** + * 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. + * + * @since 2.3.23 + */ + void trace(Template template, int beginColumn, int beginLine, int endColumn, int endLine, + boolean isLeafElement); + + /** + * Invoked when template processing is finished. + * + * @since 2.3.23 + */ + void end(); + +} diff --git a/src/test/java/freemarker/core/TemplateProcessingTracerTest.java b/src/test/java/freemarker/core/TemplateProcessingTracerTest.java new file mode 100644 index 000000000..a2ebb2f44 --- /dev/null +++ b/src/test/java/freemarker/core/TemplateProcessingTracerTest.java @@ -0,0 +1,80 @@ +/* + * 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 freemarker.core; + +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; + +public class TemplateProcessingTracerTest { + + private static final String TEMPLATE_TEXT = + "<#if 0 == 1>Nope.\n" + + "<#if 1 == 1>Yup.\n" + + "Always.\n" + + "<#list [1, 2, 3] as item>\n" + + "${item}<#else>\n" + + "Nope.\n" + + "\n" + + "<#list [] as item>\n" + + "${item}<#else>\n" + + "Yup.\n" + + "\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(); + Environment env = t.createProcessingEnvironment(null, sw); + env.setTracer(tracer); + env.process(); + + List expected = Arrays.asList(2, 3, 5, 5, 5, 9); + assertEquals(expected, tracer.linesVisited); + } + + private static class Tracer implements TemplateProcessingTracer { + ArrayList linesVisited; + + Tracer() { + linesVisited = new ArrayList<>(); + } + + public void start() {} + public void end() {} + + public void trace(Template template, int beginColumn, int beginLine, int endColumn, int endLine, + boolean isLeafElement) { + if (isLeafElement) { + linesVisited.add(beginLine); + } + } + + } +}