Merge remote-tracking branch 'origin/2.3-gae'
diff --git a/.project b/.project
index ccb2812..fcff841 100644
--- a/.project
+++ b/.project
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
- <name>FreeMarker-2.3-gae</name>
+ <name>FreeMarker-master</name>
<comment></comment>
<projects>
</projects>
@@ -29,7 +29,7 @@
</natures>
<filteredResources>
<filter>
- <id>0</id>
+ <id>1416528675344</id>
<name></name>
<type>26</type>
<matcher>
diff --git a/build.xml b/build.xml
index 236fca8..8bfe4ca 100644
--- a/build.xml
+++ b/build.xml
@@ -652,7 +652,7 @@
<u:packageAndSignDist
srcDir="${dist.dir}/bin"
- archiveNameWithoutExt="apache-freemarker-gae-${version}-bin"
+ archiveNameWithoutExt="apache-freemarker-${version}-bin"
/>
<!-- ..................................... -->
@@ -695,7 +695,7 @@
<u:packageAndSignDist
srcDir="${dist.dir}/src"
- archiveNameWithoutExt="apache-freemarker-gae-${version}-src"
+ archiveNameWithoutExt="apache-freemarker-${version}-src"
/>
</target>
@@ -780,7 +780,7 @@
</parent>
<groupId>org.freemarker</groupId>
- <artifactId>freemarker-gae</artifactId>
+ <artifactId>freemarker</artifactId>
<version>${mavenVersion}</version>
<packaging>jar</packaging>
diff --git a/osgi.bnd b/osgi.bnd
index 9cc8cd8..412055e 100644
--- a/osgi.bnd
+++ b/osgi.bnd
@@ -49,13 +49,12 @@
# This is needed for "a.class.from.another.Bundle"?new() to work.
DynamicImport-Package: *
-# The required minimum is 1.4, but we utilize 1.5 if available.
+# Use "J2SE-<utilized-version>, J2SE-<minimum-version>" (i.e., highest to lowest).
# See also: http://wiki.eclipse.org/Execution_Environments, "Compiling
# against more than is required"
-Bundle-RequiredExecutionEnvironment: J2SE-1.5, J2SE-1.4
+Bundle-RequiredExecutionEnvironment: J2SE-1.5
# Non-OSGi meta:
-Main-Class: freemarker.core.CommandLine
Extension-name: FreeMarker
Specification-Title: FreeMarker
Specification-Version: ${versionForMf}
diff --git a/src/main/java/freemarker/cache/GetLastModifiedException.java b/src/main/java/freemarker/cache/GetLastModifiedException.java
new file mode 100644
index 0000000..1f369f5
--- /dev/null
+++ b/src/main/java/freemarker/cache/GetLastModifiedException.java
@@ -0,0 +1,41 @@
+/*
+ * 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.cache;
+
+import java.io.IOException;
+
+
+/**
+ * Used be {@link TemplateLoader#getLastModified(Object)} to indicate an error getting the last modification date. That
+ * should be just an {@link IOException}, but due to backward compatibility constraints that wasn't possible;
+ * {@link TemplateLoader#getLastModified(Object)} doesn't allow throwing checked exception.
+ *
+ * @since 2.4.0
+ */
+public class GetLastModifiedException extends RuntimeException {
+
+ public GetLastModifiedException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public GetLastModifiedException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/main/java/freemarker/cache/TemplateCache.java b/src/main/java/freemarker/cache/TemplateCache.java
index 4f62d05..a357500 100644
--- a/src/main/java/freemarker/cache/TemplateCache.java
+++ b/src/main/java/freemarker/cache/TemplateCache.java
@@ -436,7 +436,7 @@
lastModified = lastModified == Long.MIN_VALUE ? templateLoader.getLastModified(source) : lastModified;
Template template = loadTemplate(
- templateLoader, source,
+ source,
name, newLookupResult.getTemplateSourceName(), locale, customLookupCondition,
encoding, parseAsFTL);
cachedTemplate.templateOrException = template;
@@ -520,7 +520,7 @@
}
private Template loadTemplate(
- final TemplateLoader templateLoader, final Object source,
+ final Object source,
final String name, final String sourceName, Locale locale, final Object customLookupCondition,
String initialEncoding, final boolean parseAsFTL) throws IOException {
final TemplateConfiguration tc;
@@ -701,7 +701,9 @@
storage.remove(tk);
}
}
- LOG.debug(debugName + " was removed from the cache, if it was there");
+ if (debug) {
+ LOG.debug(debugName + " was removed from the cache, if it was there");
+ }
}
}
@@ -875,7 +877,7 @@
/**
* This class holds the cached template and associated information
- * (the source object, and the last-checked and last-modified timestamps).
+ * (the source object (already closed), and the last-checked and last-modified time stamps).
* It is used as the value in the cached templates map. Note: this class
* is Serializable to allow custom 3rd party CacheStorage implementations
* to serialize/replicate them (see tracker issue #1926150); FreeMarker
diff --git a/src/main/java/freemarker/cache/TemplateLoader.java b/src/main/java/freemarker/cache/TemplateLoader.java
index bab7cae..a545771 100644
--- a/src/main/java/freemarker/cache/TemplateLoader.java
+++ b/src/main/java/freemarker/cache/TemplateLoader.java
@@ -48,7 +48,7 @@
* by the {@link TemplateCache}, and templates are get via the {@link TemplateCache} API-s.
*/
public interface TemplateLoader {
-
+
/**
* Finds the template in the backing storage and returns an object that identifies the storage location where the
* template can be loaded from. See the return value for more information.
@@ -92,16 +92,25 @@
throws IOException;
/**
- * Returns the time of last modification of the specified template source. This method is called after
- * <code>findTemplateSource()</code>.
+ * Returns the time of last modification of the specified template source, if the backing storage mechanism supports
+ * that.
*
* @param templateSource
* an object representing a template source, obtained through a prior call to
* {@link #findTemplateSource(String)}. This must be an object on which
* {@link TemplateLoader#closeTemplateSource(Object)} wasn't applied yet.
- * @return the time of last modification of the specified template source, or -1 if the time is not known.
+ * @return The time of last modification of the specified template source, or -1 if the time is not known. In
+ * principle, -1 should be only returned if the backing storage doesn't store last modification times, not
+ * when there was an error during getting the last modification time (then you should throw
+ * {@link GetLastModifiedException}). However, Java's {@code File} and {@code URL} API can't differentiate
+ * between these two cases, so for {@link TemplateLoader} based on those -1 is also used when an error has
+ * occurred.
+ *
+ * @throws GetLastModifiedException
+ * If there was an error when reading the last modification date (since 2.4.0). Note that for backward
+ * compatibility, this is an unchecked exception, and is not an {@link IOException}.
*/
- public long getLastModified(Object templateSource);
+ public long getLastModified(Object templateSource) throws GetLastModifiedException;
/**
* Returns the character stream of a template represented by the specified template source. This method is possibly
diff --git a/src/main/java/freemarker/core/BodyInstruction.java b/src/main/java/freemarker/core/BodyInstruction.java
index 0c82841..db7527b 100644
--- a/src/main/java/freemarker/core/BodyInstruction.java
+++ b/src/main/java/freemarker/core/BodyInstruction.java
@@ -120,7 +120,7 @@
}
class Context implements LocalContext {
- Macro.Context invokingMacroContext;
+ CallableInvocationContext invokingMacroContext;
Environment.Namespace bodyVars;
Context(Environment env) throws TemplateException {
diff --git a/src/main/java/freemarker/core/BoundCallable.java b/src/main/java/freemarker/core/BoundCallable.java
new file mode 100644
index 0000000..1855c08
--- /dev/null
+++ b/src/main/java/freemarker/core/BoundCallable.java
@@ -0,0 +1,155 @@
+/*
+ * 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 java.io.IOException;
+
+import freemarker.core.Environment.Namespace;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+
+/**
+ * A macro or function (or other future callable entity) associated to a namespace and a template.
+ *
+ * <p>
+ * With an analogy, a {@link UnboundCallable} is like a non-static {@link java.lang.reflect.Method} in Java; it
+ * describes everything about the method, but it isn't bound to any object on which the method could be called.
+ * Continuing this analogy, a {@link BoundCallable} is like a {@link java.lang.reflect.Method} paired with the object
+ * whose method it is (the {@code this} object), and is thus callable in itself. In the case of FTL macros and FTL
+ * functions, instead of a single {@code this} object, we have two such objects: a namespace and a template. (One may
+ * wonder why the namespace is not enough, given that a namespace already specifies a template (
+ * {@link Namespace#getTemplate()} ). It's because a namespace can contain macros from included templates, and so the
+ * template that the callable belongs to isn't always the same as {@link Namespace#getTemplate()}, which just gives the
+ * "root" template of the namespace. Furthermore, several namespaces my include exactly the same template, so we can't
+ * get away with a template instead of a namespace either. Also note that knowing which template we are in is needed for
+ * example to resolve relative references to other templates.)
+ *
+ * <p>
+ * Historical note: Prior to 2.4, the two concepts ({@link UnboundCallable} and {@link BoundCallable}) were these same,
+ * represented by {@link Macro}, which still exists due to backward compatibility constraints. This class extends
+ * {@link Macro} only for the sake of legacy applications which expect macro and function FTL variables to be
+ * {@link Macro}-s. Especially, this class should not extend {@link TemplateElement} (which it does, because
+ * {@link Macro} is a subclass of that), but it had to, for backward compatibility. It just delegates the {@link Macro}
+ * methods to the embedded {@link UnboundCallable}.
+ *
+ * @see UnboundCallable
+ *
+ * @since 2.4.0
+ */
+final class BoundCallable extends Macro {
+
+ private final UnboundCallable unboundCallable;
+ private final Template template;
+ private final Namespace namespace;
+
+ BoundCallable(UnboundCallable callableDefinition, Template template, Namespace namespace) {
+ this.unboundCallable = callableDefinition;
+ this.template = template;
+ this.namespace = namespace;
+ }
+
+ UnboundCallable getUnboundCallable() {
+ return unboundCallable;
+ }
+
+ Template getTemplate() {
+ return template;
+ }
+
+ Namespace getNamespace() {
+ return namespace;
+ }
+
+ @Override
+ public String toString() {
+ return "BoundCallable("
+ + "name=" + getName()
+ + ", isFunction=" + isFunction()
+ + ", template" + (template != null ? ".name=" + template.getName() : "=null")
+ + ", namespace=" + (namespace != null ? namespace.getTemplate().getName() : "null")
+ + ")";
+ }
+
+ /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+ @Override
+ public String getCatchAll() {
+ return unboundCallable.getCatchAll();
+ }
+
+ /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+ @Override
+ public String[] getArgumentNames() {
+ return unboundCallable.getArgumentNames();
+ }
+
+ /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+ @Override
+ public String getName() {
+ return unboundCallable.getName();
+ }
+
+ /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+ @Override
+ public boolean isFunction() {
+ return unboundCallable.isFunction();
+ }
+
+ /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+ @Override
+ TemplateElement[] accept(Environment env) throws TemplateException, IOException {
+ return unboundCallable.accept(env);
+ }
+
+ /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+ @Override
+ protected String dump(boolean canonical) {
+ return unboundCallable.dump(canonical);
+ }
+
+ /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+ @Override
+ String getNodeTypeSymbol() {
+ return unboundCallable.getNodeTypeSymbol();
+ }
+
+ /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+ @Override
+ int getParameterCount() {
+ return unboundCallable.getParameterCount();
+ }
+
+ /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+ @Override
+ Object getParameterValue(int idx) {
+ return unboundCallable.getParameterValue(idx);
+ }
+
+ /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+ @Override
+ ParameterRole getParameterRole(int idx) {
+ return unboundCallable.getParameterRole(idx);
+ }
+
+ @Override
+ boolean isNestedBlockRepeater() {
+ return unboundCallable.isNestedBlockRepeater();
+ }
+
+}
diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java
index ab796b0..a9aba7b 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -354,7 +354,7 @@
}
}
- throw new ParseException(buf.toString(), null, keyTk);
+ throw new ParseException(buf.toString(), (UnboundTemplate) null, keyTk);
}
while (bi instanceof ICIChainMember
diff --git a/src/main/java/freemarker/core/BuiltInWithParseTimeParameters.java b/src/main/java/freemarker/core/BuiltInWithParseTimeParameters.java
index 1820d98..5000f28 100644
--- a/src/main/java/freemarker/core/BuiltInWithParseTimeParameters.java
+++ b/src/main/java/freemarker/core/BuiltInWithParseTimeParameters.java
@@ -84,7 +84,7 @@
protected ParseException newArgumentCountException(String ordinalityDesc, Token openParen, Token closeParen) {
return new ParseException(
- "?" + key + "(...) " + ordinalityDesc + " parameters", this.getTemplate(),
+ "?" + key + "(...) " + ordinalityDesc + " parameters", this.getUnboundTemplate(),
openParen.beginLine, openParen.beginColumn,
closeParen.endLine, closeParen.endColumn);
}
diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
index 81bcf59..d70d356 100644
--- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
+++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
@@ -334,8 +334,9 @@
TemplateModel _eval(Environment env) throws TemplateException {
TemplateModel tm = target.eval(env);
target.assertNonNull(tm, env);
- // WRONG: it also had to check Macro.isFunction()
- return (tm instanceof TemplateTransformModel || tm instanceof Macro || tm instanceof TemplateDirectiveModel) ?
+ // [2.4] WRONG: it also had to check Macro.isFunction()
+ return (tm instanceof TemplateTransformModel || tm instanceof BoundCallable
+ || tm instanceof TemplateDirectiveModel) ?
TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
}
}
@@ -386,7 +387,7 @@
TemplateModel tm = target.eval(env);
target.assertNonNull(tm, env);
// WRONG: it also had to check Macro.isFunction()
- return (tm instanceof Macro) ?
+ return (tm instanceof BoundCallable) ?
TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
}
}
@@ -469,13 +470,13 @@
@Override
TemplateModel _eval(Environment env) throws TemplateException {
TemplateModel tm = target.eval(env);
- if (!(tm instanceof Macro)) {
+ if (!(tm instanceof BoundCallable)) {
throw new UnexpectedTypeException(
target, tm,
- "macro or function", new Class[] { Macro.class },
+ "macro or function", new Class[] { BoundCallable.class },
env);
} else {
- return env.getMacroNamespace((Macro) tm);
+ return ((BoundCallable) tm).getNamespace();
}
}
}
diff --git a/src/main/java/freemarker/core/BuiltInsForStringsMisc.java b/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
index 3d21c0a..355be23 100644
--- a/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
+++ b/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
@@ -22,7 +22,6 @@
import java.io.StringReader;
import freemarker.template.SimpleNumber;
-import freemarker.template.Template;
import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
@@ -57,7 +56,7 @@
}
TemplateModel calculateResult(String s, Environment env) throws TemplateException {
- Template parentTemplate = getTemplate();
+ UnboundTemplate parentTemplate = getUnboundTemplate();
Expression exp = null;
try {
@@ -77,7 +76,7 @@
}
FMParser parser = new FMParser(
- parentTemplate, false, tkMan, pCfg);
+ parentTemplate, false, tkMan, null, pCfg);
exp = parser.Expression();
} catch (TokenMgrError e) {
diff --git a/src/main/java/freemarker/core/BuiltinVariable.java b/src/main/java/freemarker/core/BuiltinVariable.java
index 5b0f4d2..807b26b 100644
--- a/src/main/java/freemarker/core/BuiltinVariable.java
+++ b/src/main/java/freemarker/core/BuiltinVariable.java
@@ -73,6 +73,8 @@
static final String URL_ESCAPING_CHARSET = "url_escaping_charset";
static final String NOW = "now";
+ private static final BoundCallable PASS_VALUE = new BoundCallable(UnboundCallable.NO_OP_MACRO, null, null);
+
static final String[] SPEC_VAR_NAMES = new String[] {
AUTO_ESC_CC,
AUTO_ESC,
@@ -160,7 +162,7 @@
sb.append(correctName);
}
}
- throw new ParseException(sb.toString(), null, nameTk);
+ throw new ParseException(sb.toString(), (UnboundTemplate) null, nameTk);
}
this.name = name.intern();
@@ -181,7 +183,7 @@
return env.getGlobalVariables();
}
if (name == LOCALS) {
- Macro.Context ctx = env.getCurrentMacroContext();
+ CallableInvocationContext ctx = env.getCurrentMacroContext();
return ctx == null ? null : ctx.getLocals();
}
if (name == DATA_MODEL || name == DATA_MODEL_CC) {
@@ -217,7 +219,7 @@
return SimpleScalar.newInstanceOrNull(env.getCurrentTemplate().getName());
}
if (name == PASS) {
- return Macro.DO_NOTHING_MACRO;
+ return PASS_VALUE;
}
if (name == OUTPUT_ENCODING || name == OUTPUT_ENCODING_CC) {
String s = env.getOutputEncoding();
diff --git a/src/main/java/freemarker/core/CallableInvocationContext.java b/src/main/java/freemarker/core/CallableInvocationContext.java
new file mode 100644
index 0000000..d2e204b
--- /dev/null
+++ b/src/main/java/freemarker/core/CallableInvocationContext.java
@@ -0,0 +1,154 @@
+/*
+ * 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 java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateModelIterator;
+
+/**
+ * The local variables and such of an FTL macro or FTL function (or other future FTL callable) call.
+ */
+class CallableInvocationContext implements LocalContext {
+ final UnboundCallable callableDefinition;
+ final Environment.Namespace localVars;
+ final TemplateElement[] nestedContentBuffer;
+ final Environment.Namespace nestedContentNamespace;
+ final Template nestedContentTemplate;
+ final List nestedContentParameterNames;
+ final LocalContextStack prevLocalContextStack;
+ final CallableInvocationContext prevMacroContext;
+
+ CallableInvocationContext(UnboundCallable callableDefinition,
+ Environment env,
+ TemplateElement[] nestedContentBuffer,
+ List nestedContentParameterNames) {
+ this.callableDefinition = callableDefinition;
+ this.localVars = env.new Namespace();
+ this.nestedContentBuffer = nestedContentBuffer;
+ this.nestedContentNamespace = env.getCurrentNamespace();
+ this.nestedContentTemplate = env.getCurrentTemplate();
+ this.nestedContentParameterNames = nestedContentParameterNames;
+ this.prevLocalContextStack = env.getLocalContextStack();
+ this.prevMacroContext = env.getCurrentMacroContext();
+ }
+
+ Macro getCallableDefinition() {
+ return callableDefinition;
+ }
+
+ // Set default parameters, check if all the required parameters are defined.
+ void sanityCheck(Environment env) throws TemplateException {
+ boolean resolvedAnArg, hasUnresolvedArg;
+ Expression firstUnresolvedExpression;
+ InvalidReferenceException firstReferenceException;
+ do {
+ firstUnresolvedExpression = null;
+ firstReferenceException = null;
+ resolvedAnArg = hasUnresolvedArg = false;
+ for (int i = 0; i < callableDefinition.getParamNames().length; ++i) {
+ String argName = callableDefinition.getParamNames()[i];
+ if (localVars.get(argName) == null) {
+ Expression valueExp = (Expression) callableDefinition.getParamDefaults().get(argName);
+ if (valueExp != null) {
+ try {
+ TemplateModel tm = valueExp.eval(env);
+ if (tm == null) {
+ if (!hasUnresolvedArg) {
+ firstUnresolvedExpression = valueExp;
+ hasUnresolvedArg = true;
+ }
+ } else {
+ localVars.put(argName, tm);
+ resolvedAnArg = true;
+ }
+ } catch (InvalidReferenceException e) {
+ if (!hasUnresolvedArg) {
+ hasUnresolvedArg = true;
+ firstReferenceException = e;
+ }
+ }
+ } else if (!env.isClassicCompatible()) {
+ boolean argWasSpecified = localVars.containsKey(argName);
+ throw new _MiscTemplateException(env,
+ new _ErrorDescriptionBuilder(new Object[] {
+ "When calling macro ", new _DelayedJQuote(callableDefinition.getName()),
+ ", required parameter ", new _DelayedJQuote(argName),
+ " (parameter #", Integer.valueOf(i + 1), ") was ",
+ (argWasSpecified
+ ? "specified, but had null/missing value."
+ : "not specified.")
+ }).tip(argWasSpecified
+ ? new Object[] {
+ "If the parameter value expression on the caller side is known to "
+ + "be legally null/missing, you may want to specify a default "
+ + "value for it with the \"!\" operator, like "
+ + "paramValue!defaultValue." }
+ : new Object[] {
+ "If the omission was deliberate, you may consider making the "
+ + "parameter optional in the macro by specifying a default value "
+ + "for it, like ", "<#macro macroName paramName=defaultExpr>", ")" }
+ ));
+ }
+ }
+ }
+ } while (resolvedAnArg && hasUnresolvedArg);
+ if (hasUnresolvedArg) {
+ if (firstReferenceException != null) {
+ throw firstReferenceException;
+ } else if (!env.isClassicCompatible()) {
+ throw InvalidReferenceException.getInstance(firstUnresolvedExpression, env);
+ }
+ }
+ }
+
+ /**
+ * @return the local variable of the given name
+ * or null if it doesn't exist.
+ */
+ public TemplateModel getLocalVariable(String name) throws TemplateModelException {
+ return localVars.get(name);
+ }
+
+ Environment.Namespace getLocals() {
+ return localVars;
+ }
+
+ /**
+ * Set a local variable in this macro
+ */
+ void setLocalVar(String name, TemplateModel var) {
+ localVars.put(name, var);
+ }
+
+ public Collection getLocalVariableNames() throws TemplateModelException {
+ HashSet result = new HashSet();
+ for (TemplateModelIterator it = localVars.keys().iterator(); it.hasNext(); ) {
+ result.add(it.next().toString());
+ }
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/freemarker/core/CommandLine.java b/src/main/java/freemarker/core/CommandLine.java
deleted file mode 100644
index 95d0164..0000000
--- a/src/main/java/freemarker/core/CommandLine.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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.template.Configuration;
-import freemarker.template.Version;
-import freemarker.template.utility.DateUtil;
-
-/**
- * FreeMarker command-line utility, the Main-Class of <tt>freemarker.jar</tt>.
- * Currently it just prints the version number.
- *
- * @deprecated Will be removed (main method in a library, often classified as CWE-489 "Leftover Debug Code").
- */
-@Deprecated
-public class CommandLine {
-
- public static void main(String[] args) {
- Version ver = Configuration.getVersion();
-
- System.out.println();
- System.out.print("FreeMarker version ");
- System.out.print(ver);
-
- /* If the version number doesn't already contain the build date and it's known, print it: */
- if (!ver.toString().endsWith("Z")
- && ver.getBuildDate() != null) {
- System.out.print(" (built on ");
- System.out.print(DateUtil.dateToISO8601String(
- ver.getBuildDate(),
- true, true, true, DateUtil.ACCURACY_SECONDS,
- DateUtil.UTC,
- new DateUtil.TrivialDateToISO8601CalendarFactory()));
- System.out.print(")");
- }
- System.out.println();
-
- if (ver.isGAECompliant() != null) {
- System.out.print("Google App Engine complian variant: ");
- System.out.println(ver.isGAECompliant().booleanValue() ? "Yes" : "No");
- }
-
- System.out.println();
- System.out.println("Copyright 2015 The Apache Software Foundation.");
- System.out.println("Licensed under the Apache License, Version 2.0");
- System.out.println();
- System.out.println("For more information and for updates visit our Web site:");
- System.out.println("http://freemarker.org/");
- System.out.println();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index 5594ba0..5d2329c 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -31,7 +31,6 @@
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
-import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -62,6 +61,7 @@
import freemarker.template.TemplateModel;
import freemarker.template.Version;
import freemarker.template._TemplateAPI;
+import freemarker.template.utility.CollectionUtils;
import freemarker.template.utility.NullArgumentException;
import freemarker.template.utility.StringUtil;
@@ -295,8 +295,12 @@
};
private Configurable parent;
+
private Properties properties;
- private HashMap<Object, Object> customAttributes;
+
+ private LinkedHashMap<Object, Object> customAttributes;
+ // Can't be final because we are cloneable:
+ private Object customAttributesLock = new Object();
private Locale locale;
private String numberFormat;
@@ -401,8 +405,6 @@
setBooleanFormat(C_TRUE_FALSE);
- customAttributes = new HashMap();
-
customDateFormats = Collections.emptyMap();
customNumberFormats = Collections.emptyMap();
}
@@ -418,14 +420,14 @@
classicCompatible = null;
templateExceptionHandler = null;
properties = new Properties(parent.properties);
- customAttributes = new HashMap(0);
}
@Override
protected Object clone() throws CloneNotSupportedException {
Configurable copy = (Configurable) super.clone();
copy.properties = new Properties(properties);
- copy.customAttributes = (HashMap) customAttributes.clone();
+ copy.customAttributesLock = new Object();
+ copy.customAttributes = customAttributes == null ? null : (LinkedHashMap) customAttributes.clone();
return copy;
}
@@ -2265,34 +2267,44 @@
}
/**
- * Internal entry point for setting unnamed custom attributes.
+ * Used internally for setting custom attributes, both named and unnamed ones.
*
* @see CustomAttribute
*/
void setCustomAttribute(Object key, Object value) {
- synchronized (customAttributes) {
+ synchronized (customAttributesLock) {
+ LinkedHashMap<Object, Object> customAttributes = this.customAttributes;
+ if (customAttributes == null) {
+ customAttributes = createInitialCustomAttributes();
+ this.customAttributes = customAttributes;
+ }
customAttributes.put(key, value);
}
}
/**
- * Internal entry point for getting unnamed custom attributes.
+ * User internally for getting unnamed custom attributes.
*
* @see CustomAttribute
*/
Object getCustomAttribute(Object key, CustomAttribute attr) {
- synchronized (customAttributes) {
- Object o = customAttributes.get(key);
- if (o == null && !customAttributes.containsKey(key)) {
- o = attr.create();
- customAttributes.put(key, o);
+ synchronized (customAttributesLock) {
+ LinkedHashMap<Object, Object> customAttributes = this.customAttributes;
+ Object value = customAttributes != null ? customAttributes.get(key) : null;
+ if (value == null && (customAttributes == null || !customAttributes.containsKey(key))) {
+ value = attr.create();
+ if (customAttributes == null) {
+ customAttributes = createInitialCustomAttributes();
+ this.customAttributes = customAttributes;
+ }
+ customAttributes.put(key, value);
}
- return o;
+ return value;
}
}
boolean isCustomAttributeSet(Object key) {
- return customAttributes.containsKey(key);
+ return getDirectCustomAttributes().containsKey(key);
}
/**
@@ -2304,8 +2316,8 @@
* @since 2.3.24
*/
void copyDirectCustomAttributes(Configurable target, boolean overwriteExisting) {
- synchronized (customAttributes) {
- for (Entry<? extends Object, ? extends Object> custAttrEnt : customAttributes.entrySet()) {
+ synchronized (customAttributesLock) {
+ for (Entry<? extends Object, ? extends Object> custAttrEnt : getDirectCustomAttributes().entrySet()) {
Object custAttrKey = custAttrEnt.getKey();
if (overwriteExisting || !target.isCustomAttributeSet(custAttrKey)) {
if (custAttrKey instanceof String) {
@@ -2319,6 +2331,28 @@
}
/**
+ * For internal usage only, returns the custom attributes set directly on this objects as a read-only {@link Map}.
+ * The returned {@link Map} won't necessarily reflect the changes made later, nor is it promised to be a snapshot.
+ */
+ private Map<? extends Object, ? extends Object> getDirectCustomAttributes() {
+ synchronized (customAttributesLock) {
+ Map<? extends Object, ? extends Object> result = customAttributes != null ? customAttributes
+ : getInitialCustomAttributes();
+ return result != null ? result : Collections.emptyMap();
+ }
+ }
+
+ /**
+ * Returns the non-{@code null} writable initial custom attribute map.
+ */
+ private LinkedHashMap<Object, Object> createInitialCustomAttributes() {
+ final Map<String, ?> initialCustomAttributes = getInitialCustomAttributes();
+ return initialCustomAttributes == null
+ ? new LinkedHashMap<Object, Object>()
+ : new LinkedHashMap<Object, Object>(initialCustomAttributes);
+ }
+
+ /**
* Sets a named custom attribute for this configurable.
*
* @param name the name of the custom attribute
@@ -2328,28 +2362,44 @@
* {@link #removeCustomAttribute(String)}.
*/
public void setCustomAttribute(String name, Object value) {
- synchronized (customAttributes) {
- customAttributes.put(name, value);
- }
+ setCustomAttribute((Object) name, value);
}
/**
- * Returns an array with names of all custom attributes defined directly
- * on this configurable. (That is, it doesn't contain the names of custom attributes
- * defined indirectly on its parent configurables.) The returned array is never null,
+ * Returns an array that contains the snapshot of the names of all custom attributes defined directly
+ * in this configurable. (That is, it doesn't contain the names of custom attributes
+ * defined indirectly on its parent configurables.) The returned array is never {@code null},
* but can be zero-length.
- * The order of elements in the returned array is not defined and can change
- * between invocations.
+ * Since 2.4.0, the order of the names is the same as the attributes were added. Before 2.4.0, the order was
+ * undefined.
*/
public String[] getCustomAttributeNames() {
- synchronized (customAttributes) {
- Collection names = new LinkedList(customAttributes.keySet());
- for (Iterator iter = names.iterator(); iter.hasNext(); ) {
- if (!(iter.next() instanceof String)) {
- iter.remove();
+ synchronized (customAttributesLock) {
+ final LinkedHashMap<Object, Object> customAttributes = this.customAttributes;
+ if (customAttributes == null) {
+ return getInitialCustomAttributeNames();
+ }
+
+ Set<Object> keys = customAttributes.keySet();
+ int stringKeyCnt = 0;
+ for (Object key : keys) {
+ if (key instanceof String) {
+ stringKeyCnt++;
}
}
- return (String[]) names.toArray(new String[names.size()]);
+
+ if (stringKeyCnt == 0) {
+ return CollectionUtils.EMPTY_STRING_ARRAY;
+ }
+
+ String[] result = new String[stringKeyCnt];
+ int i = 0;
+ for (Object key : keys) {
+ if (key instanceof String) {
+ result[i++] = (String) key;
+ }
+ }
+ return result;
}
}
@@ -2364,8 +2414,19 @@
* @param name the name of the custom attribute
*/
public void removeCustomAttribute(String name) {
- synchronized (customAttributes) {
- customAttributes.remove(name);
+ synchronized (customAttributesLock) {
+ LinkedHashMap<Object, Object> customAttributes = this.customAttributes;
+ if (customAttributes != null) {
+ customAttributes.remove(name);
+ } else {
+ Map<String, ?> initialCustomAttributes = getInitialCustomAttributes();
+ if (initialCustomAttributes == null || !initialCustomAttributes.containsKey(name)) {
+ return;
+ }
+ customAttributes = createInitialCustomAttributes();
+ this.customAttributes = customAttributes;
+ customAttributes.remove(name);
+ }
}
}
@@ -2383,10 +2444,23 @@
*/
public Object getCustomAttribute(String name) {
Object retval;
- synchronized (customAttributes) {
- retval = customAttributes.get(name);
- if (retval == null && customAttributes.containsKey(name)) {
- return null;
+ synchronized (customAttributesLock) {
+ final LinkedHashMap<Object, Object> customAttributes = this.customAttributes;
+ if (customAttributes == null) {
+ Map<String, ?> initialCustomAttributes = getInitialCustomAttributes();
+ if (initialCustomAttributes == null) {
+ retval = null;
+ } else {
+ retval = initialCustomAttributes.get(name);
+ if (retval == null && initialCustomAttributes.containsKey(name)) {
+ return null;
+ }
+ }
+ } else {
+ retval = customAttributes.get(name);
+ if (retval == null && customAttributes.containsKey(name)) {
+ return null;
+ }
}
}
if (retval == null && parent != null) {
@@ -2395,6 +2469,40 @@
return retval;
}
+ /**
+ * Returns the initial (default) set of custom attributes, or {@code null}. The returned {@link Map} must not be
+ * modified. It shouldn't be accessed during template parsing, as during that the content is possibly changing.
+ *
+ * <p>
+ * This was added so that it can be overidden in {@link Template}, where it gives the custom attributes defined in
+ * the {@code #ftl} header. The returned {@link Map} must not be modified! When the custom attributes need to be
+ * written, a copy of this {@link Map} will be made, modified, and after that this {@link Map} isn't used anymore
+ * from this {@link Configurable} instance.
+ *
+ * @since 2.4.0
+ */
+ protected Map<String, ?> getInitialCustomAttributes() {
+ return null;
+ }
+
+ /**
+ * Returns the initial (default) set of custom attribute names (maybe an empty array, but not {@code null}).
+ */
+ private String[] getInitialCustomAttributeNames() {
+ Map<String, ?> initalCustomAttributes = getInitialCustomAttributes();
+ if (initalCustomAttributes == null) {
+ return CollectionUtils.EMPTY_STRING_ARRAY;
+ }
+
+ final Set<String> keys = initalCustomAttributes.keySet();
+ final String[] result = new String[keys.size()];
+ int i = 0;
+ for (String key : keys) {
+ result[i++] = key;
+ }
+ return result;
+ }
+
protected void doAutoImportsAndIncludes(Environment env)
throws TemplateException, IOException {
if (parent != null) parent.doAutoImportsAndIncludes(env);
diff --git a/src/main/java/freemarker/core/DebugBreak.java b/src/main/java/freemarker/core/DebugBreak.java
index 2fa1cf3..32efcac 100644
--- a/src/main/java/freemarker/core/DebugBreak.java
+++ b/src/main/java/freemarker/core/DebugBreak.java
@@ -40,7 +40,7 @@
@Override
protected TemplateElement[] accept(Environment env) throws TemplateException, IOException {
if (!DebuggerService.suspendEnvironment(
- env, this.getTemplate().getSourceName(), getChild(0).getBeginLine())) {
+ env, this.getUnboundTemplate().getSourceName(), getChild(0).getBeginLine())) {
return getChild(0).accept(env);
} else {
throw new StopException(env, "Stopped by debugger");
diff --git a/src/main/java/freemarker/core/DirectiveCallPlace.java b/src/main/java/freemarker/core/DirectiveCallPlace.java
index a16c311..3adaaef 100644
--- a/src/main/java/freemarker/core/DirectiveCallPlace.java
+++ b/src/main/java/freemarker/core/DirectiveCallPlace.java
@@ -36,12 +36,6 @@
* directive call is first executed, via {@link #getOrCreateCustomData(Object, ObjectFactory)}.
*
* <p>
- * Currently this method doesn't give you access to the {@link Template} object, because it's probable that future
- * versions of FreeMarker will be able to use the same parsed representation of a "file" for multiple {@link Template}
- * objects. Then the call place will be bound to the parsed representation, not to the {@link Template} objects that are
- * based on it.
- *
- * <p>
* <b>Don't implement this interface yourself</b>, as new methods can be added to it any time! It's only meant to be
* implemented by the FreeMarker core.
*
@@ -56,6 +50,11 @@
public interface DirectiveCallPlace {
/**
+ * The template that directly contains the invocation of the directive.
+ */
+ UnboundTemplate getUnboundTemplate();
+
+ /**
* The 1-based column number of the first character of the directive call in the template source code, or -1 if it's
* not known.
*/
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index 3d61aaa..a0ff3af 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -157,11 +157,14 @@
private Collator cachedCollator;
+ private Template currentTemplate;
+ private Namespace currentNamespace;
+ private CallableInvocationContext currentMacroContext;
+
private Writer out;
- private Macro.Context currentMacroContext;
private LocalContextStack localContextStack;
private final Namespace mainNamespace;
- private Namespace currentNamespace, globalNamespace;
+ private Namespace globalNamespace;
private HashMap loadedLibs;
private Configurable legacyParent;
@@ -169,7 +172,6 @@
private Throwable lastThrowable;
private TemplateModel lastReturnValue;
- private HashMap macroToNamespaceLookup = new HashMap();
private TemplateNodeModel currentVisitorNode;
private TemplateSequenceModel nodeNamespaces;
@@ -201,9 +203,10 @@
configuration = template.getConfiguration();
this.globalNamespace = new Namespace(null);
this.currentNamespace = mainNamespace = new Namespace(template);
+ this.currentTemplate = getMainTemplate();
this.out = out;
this.rootDataModel = rootDataModel;
- importMacros(template);
+ predefineCallables(template);
}
/**
@@ -212,9 +215,9 @@
* the {@link Environment} parent switchings that occur at {@code #include}/{@code #import} and {@code #nested}
* directive calls, that is, it's not very meaningful outside FreeMarker internals.
*
- * @deprecated Use {@link #getMainTemplate()} instead (or {@link #getCurrentNamespace()} and then
- * {@link Namespace#getTemplate()}); the value returned by this method is often not what you expect when
- * it comes to macro/function invocations.
+ * @deprecated Use {@link #getMainTemplate()} or {@link #getCurrentTemplate()} (also relevant,
+ * {@link #getCurrentNamespace()} and then {@link Namespace#getTemplate()}); the value returned by this
+ * method is often not what you expect when it comes to macro/function invocations.
*/
@Deprecated
public Template getTemplate() {
@@ -252,8 +255,7 @@
*/
@SuppressFBWarnings(value = "RANGE_ARRAY_INDEX", justification = "False alarm")
public Template getCurrentTemplate() {
- int ln = instructionStackSize;
- return ln == 0 ? getMainTemplate() : instructionStack[ln - 1].getTemplate();
+ return currentTemplate;
}
/**
@@ -547,13 +549,18 @@
* Used for {@code #nested}.
*/
void invokeNestedContent(BodyInstruction.Context bodyCtx) throws TemplateException, IOException {
- Macro.Context invokingMacroContext = getCurrentMacroContext();
+ CallableInvocationContext invokingMacroContext = getCurrentMacroContext();
LocalContextStack prevLocalContextStack = localContextStack;
TemplateElement[] nestedContentBuffer = invokingMacroContext.nestedContentBuffer;
if (nestedContentBuffer != null) {
this.currentMacroContext = invokingMacroContext.prevMacroContext;
+
+ final Namespace prevCurrentNamespace = currentNamespace;
currentNamespace = invokingMacroContext.nestedContentNamespace;
+ final Template prevCurrentTemplate = currentTemplate;
+ currentTemplate = invokingMacroContext.nestedContentTemplate;
+
final Configurable prevParent;
final boolean parentReplacementOn = isBeforeIcI2322();
prevParent = getParent();
@@ -574,7 +581,8 @@
localContextStack.pop();
}
this.currentMacroContext = invokingMacroContext;
- currentNamespace = getMacroNamespace(invokingMacroContext.getMacro());
+ currentNamespace = prevCurrentNamespace;
+ currentTemplate = prevCurrentTemplate;
if (parentReplacementOn) {
setParent(prevParent);
} else {
@@ -622,8 +630,8 @@
}
try {
TemplateModel macroOrTransform = getNodeProcessor(node);
- if (macroOrTransform instanceof Macro) {
- invoke((Macro) macroOrTransform, null, null, null, null);
+ if (macroOrTransform instanceof BoundCallable) {
+ invoke((BoundCallable) macroOrTransform, null, null, null, null);
} else if (macroOrTransform instanceof TemplateTransformModel) {
visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null);
} else {
@@ -678,8 +686,8 @@
void fallback() throws TemplateException, IOException {
TemplateModel macroOrTransform = getNodeProcessor(currentNodeName, currentNodeNS, nodeNamespaceIndex);
- if (macroOrTransform instanceof Macro) {
- invoke((Macro) macroOrTransform, null, null, null, null);
+ if (macroOrTransform instanceof BoundCallable) {
+ invoke((BoundCallable) macroOrTransform, null, null, null, null);
} else if (macroOrTransform instanceof TemplateTransformModel) {
visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null);
}
@@ -688,30 +696,34 @@
/**
* Calls the macro or function with the given arguments and nested block.
*/
- void invoke(Macro macro,
+ void invoke(BoundCallable boundCallable,
Map namedArgs, List positionalArgs,
List bodyParameterNames, TemplateElement[] childBuffer) throws TemplateException, IOException {
- if (macro == Macro.DO_NOTHING_MACRO) {
+ UnboundCallable unboundCallable = boundCallable.getUnboundCallable();
+ if (unboundCallable == UnboundCallable.NO_OP_MACRO) {
return;
}
- pushElement(macro);
+ pushElement(unboundCallable);
try {
- final Macro.Context macroCtx = macro.new Context(this, childBuffer, bodyParameterNames);
- setMacroContextLocalsFromArguments(macroCtx, macro, namedArgs, positionalArgs);
+ final CallableInvocationContext macroCtx = new CallableInvocationContext(unboundCallable, this, childBuffer, bodyParameterNames);
+ setMacroContextLocalsFromArguments(macroCtx, unboundCallable, namedArgs, positionalArgs);
- final Macro.Context prevMacroCtx = currentMacroContext;
+ final CallableInvocationContext prevMacroCtx = currentMacroContext;
currentMacroContext = macroCtx;
final LocalContextStack prevLocalContextStack = localContextStack;
localContextStack = null;
- final Namespace prevNamespace = currentNamespace;
- currentNamespace = (Namespace) macroToNamespaceLookup.get(macro);
+ final Namespace prevCurrentNamespace = currentNamespace;
+ currentNamespace = boundCallable.getNamespace();
+
+ final Template prevCurrentTemplate = currentTemplate;
+ currentTemplate = boundCallable.getTemplate();
try {
macroCtx.sanityCheck(this);
- visit(macro.getChildBuffer());
+ visit(unboundCallable.getChildBuffer());
} catch (ReturnInstruction.Return re) {
// Not an error, just a <#return>
} catch (TemplateException te) {
@@ -719,7 +731,8 @@
} finally {
currentMacroContext = prevMacroCtx;
localContextStack = prevLocalContextStack;
- currentNamespace = prevNamespace;
+ currentNamespace = prevCurrentNamespace;
+ currentTemplate = prevCurrentTemplate;
}
} finally {
popElement();
@@ -730,10 +743,10 @@
* Sets the local variables corresponding to the macro call arguments in the macro context.
*/
private void setMacroContextLocalsFromArguments(
- final Macro.Context macroCtx,
- final Macro macro,
+ final CallableInvocationContext macroCtx,
+ final UnboundCallable unboundCallable,
final Map namedArgs, final List positionalArgs) throws TemplateException, _MiscTemplateException {
- String catchAllParamName = macro.getCatchAll();
+ String catchAllParamName = unboundCallable.getCatchAll();
if (namedArgs != null) {
final SimpleHash catchAllParamValue;
if (catchAllParamName != null) {
@@ -746,7 +759,7 @@
for (Iterator it = namedArgs.entrySet().iterator(); it.hasNext();) {
final Map.Entry argNameAndValExp = (Map.Entry) it.next();
final String argName = (String) argNameAndValExp.getKey();
- final boolean isArgNameDeclared = macro.hasArgNamed(argName);
+ final boolean isArgNameDeclared = unboundCallable.hasArgNamed(argName);
if (isArgNameDeclared || catchAllParamName != null) {
Expression argValueExp = (Expression) argNameAndValExp.getValue();
TemplateModel argValue = argValueExp.eval(this);
@@ -757,7 +770,7 @@
}
} else {
throw new _MiscTemplateException(this,
- (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()),
+ (unboundCallable.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(unboundCallable.getName()),
" has no parameter with name ", new _DelayedJQuote(argName), ".");
}
}
@@ -770,11 +783,11 @@
catchAllParamValue = null;
}
- String[] argNames = macro.getArgumentNamesInternal();
+ String[] argNames = unboundCallable.getArgumentNamesInternal();
final int argsCnt = positionalArgs.size();
if (argNames.length < argsCnt && catchAllParamName == null) {
throw new _MiscTemplateException(this,
- (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()),
+ (unboundCallable.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(unboundCallable.getName()),
" only accepts ", new _DelayedToString(argNames.length), " parameters, but got ",
new _DelayedToString(argsCnt), ".");
}
@@ -798,13 +811,10 @@
/**
* Defines the given macro in the current namespace (doesn't call it).
*/
- void visitMacroDef(Macro macro) {
- macroToNamespaceLookup.put(macro, currentNamespace);
- currentNamespace.put(macro.getName(), macro);
- }
-
- Namespace getMacroNamespace(Macro macro) {
- return (Namespace) macroToNamespaceLookup.get(macro);
+ void visitCallableDefinition(UnboundCallable unboundCallable) {
+ currentNamespace.put(
+ unboundCallable.getName(),
+ new BoundCallable(unboundCallable, currentTemplate, currentNamespace));
}
void recurse(TemplateNodeModel node, TemplateSequenceModel namespaces)
@@ -826,7 +836,7 @@
}
}
- Macro.Context getCurrentMacroContext() {
+ CallableInvocationContext getCurrentMacroContext() {
return currentMacroContext;
}
@@ -2135,7 +2145,7 @@
enclosingMacro, stackEl.beginLine, stackEl.beginColumn));
} else {
sb.append(MessageUtil.formatLocationForEvaluationError(
- stackEl.getTemplate(), stackEl.beginLine, stackEl.beginColumn));
+ stackEl.getUnboundTemplate(), stackEl.beginLine, stackEl.beginColumn));
}
sb.append("]");
}
@@ -2363,7 +2373,7 @@
TemplateModel result = null;
if (nsURI == null) {
result = ns.get(localName);
- if (!(result instanceof Macro) && !(result instanceof TemplateTransformModel)) {
+ if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
result = null;
}
} else {
@@ -2376,25 +2386,25 @@
}
if (prefix.length() > 0) {
result = ns.get(prefix + ":" + localName);
- if (!(result instanceof Macro) && !(result instanceof TemplateTransformModel)) {
+ if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
result = null;
}
} else {
if (nsURI.length() == 0) {
result = ns.get(Template.NO_NS_PREFIX + ":" + localName);
- if (!(result instanceof Macro) && !(result instanceof TemplateTransformModel)) {
+ if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
result = null;
}
}
if (nsURI.equals(template.getDefaultNS())) {
result = ns.get(Template.DEFAULT_NAMESPACE_PREFIX + ":" + localName);
- if (!(result instanceof Macro) && !(result instanceof TemplateTransformModel)) {
+ if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
result = null;
}
}
if (result == null) {
result = ns.get(localName);
- if (!(result instanceof Macro) && !(result instanceof TemplateTransformModel)) {
+ if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
result = null;
}
}
@@ -2500,15 +2510,21 @@
legacyParent = includedTemplate;
}
- importMacros(includedTemplate);
+ final Template prevCurrentTemplate = currentTemplate;
try {
- visit(includedTemplate.getRootTreeNode());
- } finally {
- if (parentReplacementOn) {
- setParent(prevTemplate);
+ currentTemplate = includedTemplate;
+ predefineCallables(includedTemplate);
+ try {
+ visit(includedTemplate.getRootTreeNode());
+ } finally {
+ if (parentReplacementOn) {
+ setParent(prevTemplate);
} else {
legacyParent = prevTemplate;
+ }
}
+ } finally {
+ currentTemplate = prevCurrentTemplate;
}
}
@@ -2633,9 +2649,16 @@
}
}
- void importMacros(Template template) {
- for (Iterator it = template.getMacros().values().iterator(); it.hasNext();) {
- visitMacroDef((Macro) it.next());
+ /**
+ * Used for creating the callables that are defined by #macro/#function elements that weren't executed yet.
+ */
+ void predefineCallables(Template template) {
+ final Map<String, UnboundCallable> unboundCallables
+ = _CoreAPI.getUnboundCallables(template.getUnboundTemplate());
+ if (unboundCallables != null) {
+ for (UnboundCallable unboundCallable : unboundCallables.values()) {
+ visitCallableDefinition(unboundCallable);
+ }
}
}
@@ -2754,6 +2777,12 @@
public Template getTemplate() {
return template == null ? Environment.this.getTemplate() : template;
}
+
+ @Override
+ public String toString() {
+ return StringUtil.jQuote(template.getName()) + super.toString();
+ }
+
}
private static final Writer EMPTY_BODY_WRITER = new Writer() {
diff --git a/src/main/java/freemarker/core/EvalUtil.java b/src/main/java/freemarker/core/EvalUtil.java
index 886308c..c716a54 100644
--- a/src/main/java/freemarker/core/EvalUtil.java
+++ b/src/main/java/freemarker/core/EvalUtil.java
@@ -227,7 +227,7 @@
env != null
? env.getArithmeticEngine()
: (leftExp != null
- ? leftExp.getTemplate().getArithmeticEngine()
+ ? leftExp.getUnboundTemplate().getConfiguration().getArithmeticEngine()
: ArithmeticEngine.BIGDECIMAL_ENGINE);
try {
cmpResult = ae.compareNumbers(leftNum, rightNum);
@@ -560,7 +560,7 @@
static ArithmeticEngine getArithmeticEngine(Environment env, TemplateObject tObj) {
return env != null
? env.getArithmeticEngine()
- : tObj.getTemplate().getParserConfiguration().getArithmeticEngine();
+ : tObj.getUnboundTemplate().getParserConfiguration().getArithmeticEngine();
}
}
diff --git a/src/main/java/freemarker/core/Expression.java b/src/main/java/freemarker/core/Expression.java
index 1b07c7d..fec7090 100644
--- a/src/main/java/freemarker/core/Expression.java
+++ b/src/main/java/freemarker/core/Expression.java
@@ -21,7 +21,6 @@
import freemarker.ext.beans.BeanModel;
import freemarker.template.Configuration;
-import freemarker.template.Template;
import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateDateModel;
@@ -58,8 +57,8 @@
// Hook in here to set the constant value if possible.
@Override
- void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine) {
- super.setLocation(template, beginColumn, beginLine, endColumn, endLine);
+ void setLocation(UnboundTemplate unboundTemplate, int beginColumn, int beginLine, int endColumn, int endLine) {
+ super.setLocation(unboundTemplate, beginColumn, beginLine, endColumn, endLine);
if (isLiteral()) {
try {
constantValue = _eval(null);
diff --git a/src/main/java/freemarker/core/IfBlock.java b/src/main/java/freemarker/core/IfBlock.java
index 42526f3..69b3484 100644
--- a/src/main/java/freemarker/core/IfBlock.java
+++ b/src/main/java/freemarker/core/IfBlock.java
@@ -58,7 +58,7 @@
throws ParseException {
if (getChildCount() == 1) {
ConditionalBlock cblock = (ConditionalBlock) getChild(0);
- cblock.setLocation(getTemplate(), cblock, this);
+ cblock.setLocation(getUnboundTemplate(), cblock, this);
return cblock.postParseCleanup(stripWhitespace);
} else {
return super.postParseCleanup(stripWhitespace);
diff --git a/src/main/java/freemarker/core/Include.java b/src/main/java/freemarker/core/Include.java
index 08c8459..755a32f 100644
--- a/src/main/java/freemarker/core/Include.java
+++ b/src/main/java/freemarker/core/Include.java
@@ -41,12 +41,12 @@
private final Boolean ignoreMissingExpPrecalcedValue;
/**
- * @param template the template that this <tt>#include</tt> is a part of.
+ * @param unboundTemplate the template that this <tt>#include</tt> is a part of.
* @param includedTemplatePathExp the path of the template to be included.
* @param encodingExp the encoding to be used or null, if it's the default.
* @param parseExp whether the template should be parsed (or is raw text)
*/
- Include(Template template,
+ Include(UnboundTemplate unboundTemplate,
Expression includedTemplatePathExp,
Expression encodingExp, Expression parseExp, Expression ignoreMissingExp) throws ParseException {
this.includedTemplateNameExp = includedTemplatePathExp;
@@ -83,7 +83,7 @@
parse = Boolean.valueOf(StringUtil.getYesNo(parseExp.evalAndCoerceToPlainText(null)));
} else {
try {
- parse = Boolean.valueOf(parseExp.evalToBoolean(template.getConfiguration()));
+ parse = Boolean.valueOf(parseExp.evalToBoolean(unboundTemplate.getConfiguration()));
} catch (NonBooleanException e) {
throw new ParseException("Expected a boolean or string as the value of the parse attribute",
parseExp, e);
@@ -103,7 +103,7 @@
try {
try {
ignoreMissingExpPrecalcedValue = Boolean.valueOf(
- ignoreMissingExp.evalToBoolean(template.getConfiguration()));
+ ignoreMissingExp.evalToBoolean(unboundTemplate.getConfiguration()));
} catch (NonBooleanException e) {
throw new ParseException("Expected a boolean as the value of the \"ignore_missing\" attribute",
ignoreMissingExp, e);
@@ -122,7 +122,7 @@
final String includedTemplateName = includedTemplateNameExp.evalAndCoerceToPlainText(env);
final String fullIncludedTemplateName;
try {
- fullIncludedTemplateName = env.toFullTemplateName(getTemplate().getName(), includedTemplateName);
+ fullIncludedTemplateName = env.toFullTemplateName(env.getCurrentTemplate().getName(), includedTemplateName);
} catch (MalformedTemplateNameException e) {
throw new _MiscTemplateException(e, env,
"Malformed template name ", new _DelayedJQuote(e.getTemplateName()), ":\n",
diff --git a/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java b/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java
deleted file mode 100644
index 1f95dac..0000000
--- a/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * 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.template.Version;
-
-/**
- * Used to work around that {@link FMParser} has constructors that have separate parameters for individual settings.
- *
- * @since 2.3.24
- */
-class LegacyConstructorParserConfiguration implements ParserConfiguration {
-
- private final int tagSyntax;
- private final int namingConvention;
- private final boolean whitespaceStripping;
- private final boolean strictSyntaxMode;
- private ArithmeticEngine arithmeticEngine;
- private Integer autoEscapingPolicy;
- private OutputFormat outputFormat;
- private Boolean recognizeStandardFileExtensions;
- private final Version incompatibleImprovements;
-
- public LegacyConstructorParserConfiguration(boolean strictSyntaxMode, boolean whitespaceStripping, int tagSyntax,
- int namingConvention, Integer autoEscaping, OutputFormat outputFormat,
- Boolean recognizeStandardFileExtensions,
- Version incompatibleImprovements, ArithmeticEngine arithmeticEngine) {
- this.tagSyntax = tagSyntax;
- this.namingConvention = namingConvention;
- this.whitespaceStripping = whitespaceStripping;
- this.strictSyntaxMode = strictSyntaxMode;
- this.autoEscapingPolicy = autoEscaping;
- this.outputFormat = outputFormat;
- this.recognizeStandardFileExtensions = recognizeStandardFileExtensions;
- this.incompatibleImprovements = incompatibleImprovements;
- this.arithmeticEngine = arithmeticEngine;
- }
-
- public int getTagSyntax() {
- return tagSyntax;
- }
-
- public int getNamingConvention() {
- return namingConvention;
- }
-
- public boolean getWhitespaceStripping() {
- return whitespaceStripping;
- }
-
- public boolean getStrictSyntaxMode() {
- return strictSyntaxMode;
- }
-
- public Version getIncompatibleImprovements() {
- return incompatibleImprovements;
- }
-
- public ArithmeticEngine getArithmeticEngine() {
- if (arithmeticEngine == null) {
- throw new IllegalStateException();
- }
- return arithmeticEngine;
- }
-
- void setArithmeticEngineIfNotSet(ArithmeticEngine arithmeticEngine) {
- if (this.arithmeticEngine == null) {
- this.arithmeticEngine = arithmeticEngine;
- }
- }
-
- public int getAutoEscapingPolicy() {
- if (autoEscapingPolicy == null) {
- throw new IllegalStateException();
- }
- return autoEscapingPolicy.intValue();
- }
-
- void setAutoEscapingPolicyIfNotSet(int autoEscapingPolicy) {
- if (this.autoEscapingPolicy == null) {
- this.autoEscapingPolicy = Integer.valueOf(autoEscapingPolicy);
- }
- }
-
- public OutputFormat getOutputFormat() {
- if (outputFormat == null) {
- throw new IllegalStateException();
- }
- return outputFormat;
- }
-
- void setOutputFormatIfNotSet(OutputFormat outputFormat) {
- if (this.outputFormat == null) {
- this.outputFormat = outputFormat;
- }
- }
-
- public boolean getRecognizeStandardFileExtensions() {
- if (recognizeStandardFileExtensions == null) {
- throw new IllegalStateException();
- }
- return recognizeStandardFileExtensions.booleanValue();
- }
-
- void setRecognizeStandardFileExtensionsIfNotSet(boolean recognizeStandardFileExtensions) {
- if (this.recognizeStandardFileExtensions == null) {
- this.recognizeStandardFileExtensions = Boolean.valueOf(recognizeStandardFileExtensions);
- }
- }
-
-}
diff --git a/src/main/java/freemarker/core/LibraryLoad.java b/src/main/java/freemarker/core/LibraryLoad.java
index 9305144..4597539 100644
--- a/src/main/java/freemarker/core/LibraryLoad.java
+++ b/src/main/java/freemarker/core/LibraryLoad.java
@@ -34,15 +34,15 @@
@Deprecated
public final class LibraryLoad extends TemplateElement {
- private Expression importedTemplateNameExp;
- private String namespace;
+ private final Expression importedTemplateNameExp;
+ private final String namespace;
/**
- * @param template the template that this <tt>Include</tt> is a part of.
+ * @param unboundTemplate the template that this <tt>Include</tt> is a part of.
* @param templateName the name of the template to be included.
* @param namespace the namespace to assign this library to
*/
- LibraryLoad(Template template,
+ LibraryLoad(UnboundTemplate unboundTemplate,
Expression templateName,
String namespace) {
this.namespace = namespace;
@@ -54,7 +54,7 @@
final String importedTemplateName = importedTemplateNameExp.evalAndCoerceToPlainText(env);
final String fullImportedTemplateName;
try {
- fullImportedTemplateName = env.toFullTemplateName(getTemplate().getName(), importedTemplateName);
+ fullImportedTemplateName = env.toFullTemplateName(env.getCurrentTemplate().getName(), importedTemplateName);
} catch (MalformedTemplateNameException e) {
throw new _MiscTemplateException(e, env,
"Malformed template name ", new _DelayedJQuote(e.getTemplateName()), ":\n",
diff --git a/src/main/java/freemarker/core/Macro.java b/src/main/java/freemarker/core/Macro.java
index 162cad3..b7fa41f 100644
--- a/src/main/java/freemarker/core/Macro.java
+++ b/src/main/java/freemarker/core/Macro.java
@@ -19,309 +19,34 @@
package freemarker.core;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-
-import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
-import freemarker.template.TemplateModelException;
-import freemarker.template.TemplateModelIterator;
/**
- * An element representing a macro declaration.
+ * Exist for backward compatibility only; it has represented a macro or function declaration in the AST. The current
+ * representation isn't a public class, but is {@code insteanceof} this for backward compatibility.
+ *
+ * <p>
+ * Historical note: This class exists for backward compatibility with 2.3. 2.4 has introduced {@link UnboundTemplate}-s,
+ * thus, the definition of a callable and the runtime callable value has become to two different things:
+ * {@link UnboundCallable} and {@link BoundCallable}. Both extends this class for backward compatibility.
+ *
+ * @see UnboundCallable
+ * @see BoundCallable
*
* @deprecated Subject to be changed or renamed any time; no "stable" replacement exists yet.
*/
@Deprecated
-public final class Macro extends TemplateElement implements TemplateModel {
-
- static final Macro DO_NOTHING_MACRO = new Macro(".pass",
- Collections.EMPTY_LIST,
- Collections.EMPTY_MAP,
- null, false,
- TemplateElements.EMPTY);
+public abstract class Macro extends TemplateElement implements TemplateModel {
- final static int TYPE_MACRO = 0;
- final static int TYPE_FUNCTION = 1;
-
- private final String name;
- private final String[] paramNames;
- private final Map paramDefaults;
- private final String catchAllParamName;
- private final boolean function;
+ // Not public
+ Macro() { }
- Macro(String name, List argumentNames, Map args,
- String catchAllParamName, boolean function,
- TemplateElements children) {
- this.name = name;
- this.paramNames = (String[]) argumentNames.toArray(
- new String[argumentNames.size()]);
- this.paramDefaults = args;
-
- this.function = function;
- this.catchAllParamName = catchAllParamName;
-
- this.setChildren(children);
- }
+ public abstract String getCatchAll();
- public String getCatchAll() {
- return catchAllParamName;
- }
-
- public String[] getArgumentNames() {
- return paramNames.clone();
- }
+ public abstract String[] getArgumentNames();
- String[] getArgumentNamesInternal() {
- return paramNames;
- }
+ public abstract String getName();
- boolean hasArgNamed(String name) {
- return paramDefaults.containsKey(name);
- }
-
- public String getName() {
- return name;
- }
-
- @Override
- TemplateElement[] accept(Environment env) {
- env.visitMacroDef(this);
- return null;
- }
-
- @Override
- protected String dump(boolean canonical) {
- StringBuilder sb = new StringBuilder();
- if (canonical) sb.append('<');
- sb.append(getNodeTypeSymbol());
- sb.append(' ');
- sb.append(_CoreStringUtils.toFTLTopLevelTragetIdentifier(name));
- if (function) sb.append('(');
- int argCnt = paramNames.length;
- for (int i = 0; i < argCnt; i++) {
- if (function) {
- if (i != 0) {
- sb.append(", ");
- }
- } else {
- sb.append(' ');
- }
- String argName = paramNames[i];
- sb.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(argName));
- if (paramDefaults != null && paramDefaults.get(argName) != null) {
- sb.append('=');
- Expression defaultExpr = (Expression) paramDefaults.get(argName);
- if (function) {
- sb.append(defaultExpr.getCanonicalForm());
- } else {
- MessageUtil.appendExpressionAsUntearable(sb, defaultExpr);
- }
- }
- }
- if (catchAllParamName != null) {
- if (function) {
- if (argCnt != 0) {
- sb.append(", ");
- }
- } else {
- sb.append(' ');
- }
- sb.append(catchAllParamName);
- sb.append("...");
- }
- if (function) sb.append(')');
- if (canonical) {
- sb.append('>');
- sb.append(getChildrenCanonicalForm());
- sb.append("</").append(getNodeTypeSymbol()).append('>');
- }
- return sb.toString();
- }
-
- @Override
- String getNodeTypeSymbol() {
- return function ? "#function" : "#macro";
- }
-
- public boolean isFunction() {
- return function;
- }
-
- class Context implements LocalContext {
- final Environment.Namespace localVars;
- final TemplateElement[] nestedContentBuffer;
- final Environment.Namespace nestedContentNamespace;
- final List nestedContentParameterNames;
- final LocalContextStack prevLocalContextStack;
- final Context prevMacroContext;
-
- Context(Environment env,
- TemplateElement[] nestedContentBuffer,
- List nestedContentParameterNames) {
- this.localVars = env.new Namespace();
- this.nestedContentBuffer = nestedContentBuffer;
- this.nestedContentNamespace = env.getCurrentNamespace();
- this.nestedContentParameterNames = nestedContentParameterNames;
- this.prevLocalContextStack = env.getLocalContextStack();
- this.prevMacroContext = env.getCurrentMacroContext();
- }
-
-
- Macro getMacro() {
- return Macro.this;
- }
-
- // Set default parameters, check if all the required parameters are defined.
- void sanityCheck(Environment env) throws TemplateException {
- boolean resolvedAnArg, hasUnresolvedArg;
- Expression firstUnresolvedExpression;
- InvalidReferenceException firstReferenceException;
- do {
- firstUnresolvedExpression = null;
- firstReferenceException = null;
- resolvedAnArg = hasUnresolvedArg = false;
- for (int i = 0; i < paramNames.length; ++i) {
- String argName = paramNames[i];
- if (localVars.get(argName) == null) {
- Expression valueExp = (Expression) paramDefaults.get(argName);
- if (valueExp != null) {
- try {
- TemplateModel tm = valueExp.eval(env);
- if (tm == null) {
- if (!hasUnresolvedArg) {
- firstUnresolvedExpression = valueExp;
- hasUnresolvedArg = true;
- }
- } else {
- localVars.put(argName, tm);
- resolvedAnArg = true;
- }
- } catch (InvalidReferenceException e) {
- if (!hasUnresolvedArg) {
- hasUnresolvedArg = true;
- firstReferenceException = e;
- }
- }
- } else if (!env.isClassicCompatible()) {
- boolean argWasSpecified = localVars.containsKey(argName);
- throw new _MiscTemplateException(env,
- new _ErrorDescriptionBuilder(
- "When calling macro ", new _DelayedJQuote(name),
- ", required parameter ", new _DelayedJQuote(argName),
- " (parameter #", Integer.valueOf(i + 1), ") was ",
- (argWasSpecified
- ? "specified, but had null/missing value."
- : "not specified.")
- ).tip(argWasSpecified
- ? new Object[] {
- "If the parameter value expression on the caller side is known to "
- + "be legally null/missing, you may want to specify a default "
- + "value for it with the \"!\" operator, like "
- + "paramValue!defaultValue." }
- : new Object[] {
- "If the omission was deliberate, you may consider making the "
- + "parameter optional in the macro by specifying a default value "
- + "for it, like ", "<#macro macroName paramName=defaultExpr>", ")" }
- ));
- }
- }
- }
- } while (resolvedAnArg && hasUnresolvedArg);
- if (hasUnresolvedArg) {
- if (firstReferenceException != null) {
- throw firstReferenceException;
- } else if (!env.isClassicCompatible()) {
- throw InvalidReferenceException.getInstance(firstUnresolvedExpression, env);
- }
- }
- }
-
- /**
- * @return the local variable of the given name
- * or null if it doesn't exist.
- */
- public TemplateModel getLocalVariable(String name) throws TemplateModelException {
- return localVars.get(name);
- }
-
- Environment.Namespace getLocals() {
- return localVars;
- }
-
- /**
- * Set a local variable in this macro
- */
- void setLocalVar(String name, TemplateModel var) {
- localVars.put(name, var);
- }
-
- public Collection getLocalVariableNames() throws TemplateModelException {
- HashSet result = new HashSet();
- for (TemplateModelIterator it = localVars.keys().iterator(); it.hasNext(); ) {
- result.add(it.next().toString());
- }
- return result;
- }
- }
-
- @Override
- int getParameterCount() {
- return 1/*name*/ + paramNames.length * 2/*name=default*/ + 1/*catchAll*/ + 1/*type*/;
- }
-
- @Override
- Object getParameterValue(int idx) {
- if (idx == 0) {
- return name;
- } else {
- final int argDescsEnd = paramNames.length * 2 + 1;
- if (idx < argDescsEnd) {
- String paramName = paramNames[(idx - 1) / 2];
- if (idx % 2 != 0) {
- return paramName;
- } else {
- return paramDefaults.get(paramName);
- }
- } else if (idx == argDescsEnd) {
- return catchAllParamName;
- } else if (idx == argDescsEnd + 1) {
- return Integer.valueOf(function ? TYPE_FUNCTION : TYPE_MACRO);
- } else {
- throw new IndexOutOfBoundsException();
- }
- }
- }
-
- @Override
- ParameterRole getParameterRole(int idx) {
- if (idx == 0) {
- return ParameterRole.ASSIGNMENT_TARGET;
- } else {
- final int argDescsEnd = paramNames.length * 2 + 1;
- if (idx < argDescsEnd) {
- if (idx % 2 != 0) {
- return ParameterRole.PARAMETER_NAME;
- } else {
- return ParameterRole.PARAMETER_DEFAULT;
- }
- } else if (idx == argDescsEnd) {
- return ParameterRole.CATCH_ALL_PARAMETER_NAME;
- } else if (idx == argDescsEnd + 1) {
- return ParameterRole.AST_NODE_SUBTYPE;
- } else {
- throw new IndexOutOfBoundsException();
- }
- }
- }
-
- @Override
- boolean isNestedBlockRepeater() {
- // Because of recursive calls
- return true;
- }
+ public abstract boolean isFunction();
}
diff --git a/src/main/java/freemarker/core/MessageUtil.java b/src/main/java/freemarker/core/MessageUtil.java
index 3aebacc..8b65dc8 100644
--- a/src/main/java/freemarker/core/MessageUtil.java
+++ b/src/main/java/freemarker/core/MessageUtil.java
@@ -21,7 +21,6 @@
import java.util.ArrayList;
-import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
@@ -56,37 +55,39 @@
// Can't be instantiated
private MessageUtil() { }
- static String formatLocationForSimpleParsingError(Template template, int line, int column) {
- return formatLocation("in", template, line, column);
+ static String formatLocationForSimpleParsingError(UnboundTemplate unboundTemplate, int line, int column) {
+ return formatLocation("in", unboundTemplate, line, column);
}
static String formatLocationForSimpleParsingError(String templateSourceName, int line, int column) {
return formatLocation("in", templateSourceName, line, column);
}
- static String formatLocationForDependentParsingError(Template template, int line, int column) {
- return formatLocation("on", template, line, column);
+ static String formatLocationForDependentParsingError(UnboundTemplate unboundTemplate, int line, int column) {
+ return formatLocation("on", unboundTemplate, line, column);
}
static String formatLocationForDependentParsingError(String templateSourceName, int line, int column) {
return formatLocation("on", templateSourceName, line, column);
}
- static String formatLocationForEvaluationError(Template template, int line, int column) {
- return formatLocation("at", template, line, column);
+ static String formatLocationForEvaluationError(UnboundTemplate unboundTemplate, int line, int column) {
+ return formatLocation("at", unboundTemplate, line, column);
}
static String formatLocationForEvaluationError(Macro macro, int line, int column) {
- Template t = macro.getTemplate();
- return formatLocation("at", t != null ? t.getSourceName() : null, macro.getName(), macro.isFunction(), line, column);
+ UnboundTemplate t = macro.getUnboundTemplate();
+ return formatLocation("at",
+ t != null ? t.getSourceName() : null, macro.getName(), macro.isFunction(), line, column);
}
static String formatLocationForEvaluationError(String templateSourceName, int line, int column) {
return formatLocation("at", templateSourceName, line, column);
}
- private static String formatLocation(String preposition, Template template, int line, int column) {
- return formatLocation(preposition, template != null ? template.getSourceName() : null, line, column);
+ private static String formatLocation(String preposition, UnboundTemplate unboundTemplate, int line, int column) {
+ return formatLocation(preposition,
+ unboundTemplate != null ? unboundTemplate.getSourceName() : null, line, column);
}
private static String formatLocation(String preposition, String templateSourceName, int line, int column) {
diff --git a/src/main/java/freemarker/core/MethodCall.java b/src/main/java/freemarker/core/MethodCall.java
index 1cb42de..664eb90 100644
--- a/src/main/java/freemarker/core/MethodCall.java
+++ b/src/main/java/freemarker/core/MethodCall.java
@@ -64,16 +64,17 @@
: arguments.getValueList(env);
Object result = targetMethod.exec(argumentStrings);
return env.getObjectWrapper().wrap(result);
- } else if (targetModel instanceof Macro) {
- Macro func = (Macro) targetModel;
+ } else if (targetModel instanceof BoundCallable) {
+ final BoundCallable boundFunc = (BoundCallable) targetModel;
+ final Macro unboundFunc = boundFunc.getUnboundCallable();
env.setLastReturnValue(null);
- if (!func.isFunction()) {
+ if (!unboundFunc.isFunction()) {
throw new _MiscTemplateException(env, "A macro cannot be called in an expression. (Functions can be.)");
}
Writer prevOut = env.getOut();
try {
env.setOut(NullWriter.INSTANCE);
- env.invoke(func, null, arguments.items, null, null);
+ env.invoke(boundFunc, null, arguments.items, null, null);
} catch (IOException e) {
// Should not occur
throw new TemplateException("Unexpected exception during function execution", e, env);
diff --git a/src/main/java/freemarker/core/NewBI.java b/src/main/java/freemarker/core/NewBI.java
index c7842de..c719c9d 100644
--- a/src/main/java/freemarker/core/NewBI.java
+++ b/src/main/java/freemarker/core/NewBI.java
@@ -23,7 +23,6 @@
import freemarker.ext.beans.BeansWrapper;
import freemarker.template.ObjectWrapper;
-import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModel;
@@ -48,7 +47,7 @@
@Override
TemplateModel _eval(Environment env)
throws TemplateException {
- return new ConstructorFunction(target.evalAndCoerceToPlainText(env), env, target.getTemplate());
+ return new ConstructorFunction(target.evalAndCoerceToPlainText(env), env);
}
class ConstructorFunction implements TemplateMethodModelEx {
@@ -56,9 +55,9 @@
private final Class cl;
private final Environment env;
- public ConstructorFunction(String classname, Environment env, Template template) throws TemplateException {
+ public ConstructorFunction(String classname, Environment env) throws TemplateException {
this.env = env;
- cl = env.getNewBuiltinClassResolver().resolve(classname, env, template);
+ cl = env.getNewBuiltinClassResolver().resolve(classname, env, env.getCurrentTemplate());
if (!TemplateModel.class.isAssignableFrom(cl)) {
throw new _MiscTemplateException(NewBI.this, env,
"Class ", cl.getName(), " does not implement freemarker.template.TemplateModel");
diff --git a/src/main/java/freemarker/core/NonUserDefinedDirectiveLikeException.java b/src/main/java/freemarker/core/NonUserDefinedDirectiveLikeException.java
index ba37df1..0237714 100644
--- a/src/main/java/freemarker/core/NonUserDefinedDirectiveLikeException.java
+++ b/src/main/java/freemarker/core/NonUserDefinedDirectiveLikeException.java
@@ -32,7 +32,7 @@
class NonUserDefinedDirectiveLikeException extends UnexpectedTypeException {
private static final Class[] EXPECTED_TYPES = new Class[] {
- TemplateDirectiveModel.class, TemplateTransformModel.class, Macro.class };
+ TemplateDirectiveModel.class, TemplateTransformModel.class, BoundCallable.class };
public NonUserDefinedDirectiveLikeException(Environment env) {
super(env, "Expecting user-defined directive, transform or macro value here");
diff --git a/src/main/java/freemarker/core/ParseException.java b/src/main/java/freemarker/core/ParseException.java
index b7cacf8..7540d52 100644
--- a/src/main/java/freemarker/core/ParseException.java
+++ b/src/main/java/freemarker/core/ParseException.java
@@ -45,6 +45,13 @@
public class ParseException extends IOException implements FMParserConstants {
/**
+ * The version identifier for this Serializable class.
+ * Increment only if the <i>serialized</i> form of the
+ * class changes.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
* This is the last token that has been consumed successfully. If
* this object has been created due to a parse error, the token
* following this token will (therefore) be the first error token.
@@ -135,16 +142,19 @@
}
/**
+ * @deprecated Use {@link #ParseException(String, UnboundTemplate, int, int, int, int)} instead.
* @since 2.3.21
- */
+ /
public ParseException(String description, Template template,
int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber) {
this(description, template, lineNumber, columnNumber, endLineNumber, endColumnNumber, null);
}
/**
+ * @deprecated Use {@link #ParseException(String, UnboundTemplate, int, int, int, int, Throwable)} instead.
* @since 2.3.21
*/
+ @Deprecated
public ParseException(String description, Template template,
int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber,
Throwable cause) {
@@ -156,7 +166,7 @@
}
/**
- * @deprecated Use {@link #ParseException(String, Template, int, int, int, int)} instead, as IDE-s need the end
+ * @deprecated Use {@link #ParseException(String, UnboundTemplate, int, int, int, int)} instead, as IDE-s need the end
* position of the error too.
* @since 2.3.20
*/
@@ -166,8 +176,8 @@
}
/**
- * @deprecated Use {@link #ParseException(String, Template, int, int, int, int, Throwable)} instead, as IDE-s need
- * the end position of the error too.
+ * @deprecated Use {@link #ParseException(String, UnboundTemplate, int, int, int, int, Throwable)} instead, as
+ * IDE-s need the end position of the error too.
* @since 2.3.20
*/
@Deprecated
@@ -180,15 +190,19 @@
}
/**
+ * @deprecated Use {@link #ParseException(String, UnboundTemplate, Token)} instead.
* @since 2.3.20
*/
+ @Deprecated
public ParseException(String description, Template template, Token tk) {
this(description, template, tk, null);
}
/**
+ * @deprecated Use {@link #ParseException(String, UnboundTemplate, Token, Throwable)} instead.
* @since 2.3.20
*/
+ @Deprecated
public ParseException(String description, Template template, Token tk, Throwable cause) {
this(description,
template == null ? null : template.getSourceName(),
@@ -198,6 +212,45 @@
}
/**
+ * @since 2.4.0
+ */
+ public ParseException(String description, UnboundTemplate unboundTemplate,
+ int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber) {
+ this(description, unboundTemplate, lineNumber, columnNumber, endLineNumber, endColumnNumber, null);
+ }
+
+ /**
+ * @since 2.4.0
+ */
+ public ParseException(String description, UnboundTemplate unboundTemplate,
+ int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber,
+ Throwable cause) {
+ this(description,
+ unboundTemplate == null ? null : unboundTemplate.getSourceName(),
+ lineNumber, columnNumber,
+ endLineNumber, endColumnNumber,
+ cause);
+ }
+
+ /**
+ * @since 2.4.0
+ */
+ public ParseException(String description, UnboundTemplate unboundTemplate, Token tk) {
+ this(description, unboundTemplate, tk, null);
+ }
+
+ /**
+ * @since 2.4.0
+ */
+ public ParseException(String description, UnboundTemplate unboundTemplate, Token tk, Throwable cause) {
+ this(description,
+ unboundTemplate == null ? null : unboundTemplate.getSourceName(),
+ tk.beginLine, tk.beginColumn,
+ tk.endLine, tk.endColumn,
+ cause);
+ }
+
+ /**
* @since 2.3.20
*/
public ParseException(String description, TemplateObject tobj) {
@@ -209,7 +262,7 @@
*/
public ParseException(String description, TemplateObject tobj, Throwable cause) {
this(description,
- tobj.getTemplate() == null ? null : tobj.getTemplate().getSourceName(),
+ tobj.getUnboundTemplate() == null ? null : tobj.getUnboundTemplate().getSourceName(),
tobj.beginLine, tobj.beginColumn,
tobj.endLine, tobj.endColumn,
cause);
diff --git a/src/main/java/freemarker/core/PropertySetting.java b/src/main/java/freemarker/core/PropertySetting.java
index 1cb81a3..e975f84 100644
--- a/src/main/java/freemarker/core/PropertySetting.java
+++ b/src/main/java/freemarker/core/PropertySetting.java
@@ -102,7 +102,7 @@
}
}
}
- throw new ParseException(sb.toString(), null, keyTk);
+ throw new ParseException(sb.toString(), (UnboundTemplate) null, keyTk);
}
this.key = key;
diff --git a/src/main/java/freemarker/core/StringLiteral.java b/src/main/java/freemarker/core/StringLiteral.java
index aa32d3e..91c429a 100644
--- a/src/main/java/freemarker/core/StringLiteral.java
+++ b/src/main/java/freemarker/core/StringLiteral.java
@@ -23,7 +23,6 @@
import java.util.List;
import freemarker.template.SimpleScalar;
-import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateScalarModel;
@@ -50,7 +49,7 @@
// but we can't fix this backward compatibly.
if (value.length() > 3 && (value.indexOf("${") >= 0 || value.indexOf("#{") >= 0)) {
- Template parentTemplate = getTemplate();
+ UnboundTemplate parentTemplate = getUnboundTemplate();
try {
FMParserTokenManager tkMan = new FMParserTokenManager(
@@ -59,7 +58,7 @@
beginLine, beginColumn + 1,
value.length()));
- FMParser parser = new FMParser(parentTemplate, false, tkMan, parentTemplate.getParserConfiguration());
+ FMParser parser = new FMParser(parentTemplate, false, tkMan, null, parentTemplate.getParserConfiguration());
// We continue from the parent parser's current state:
parser.setupStringLiteralMode(parentTkMan, outputFormat);
try {
diff --git a/src/main/java/freemarker/core/TemplateElementArrayBuilder.java b/src/main/java/freemarker/core/TemplateElementArrayBuilder.java
index 0126c19..f3b9a3d 100644
--- a/src/main/java/freemarker/core/TemplateElementArrayBuilder.java
+++ b/src/main/java/freemarker/core/TemplateElementArrayBuilder.java
@@ -80,7 +80,7 @@
} else {
MixedContent mixedContent = new MixedContent();
mixedContent.setChildren(this);
- mixedContent.setLocation(first.getTemplate(), first, getLast());
+ mixedContent.setLocation(first.getUnboundTemplate(), first, getLast());
return mixedContent;
}
}
@@ -94,7 +94,7 @@
if (count != 0) {
TemplateElement first = buffer[0];
mixedContent.setChildren(this);
- mixedContent.setLocation(first.getTemplate(), first, getLast());
+ mixedContent.setLocation(first.getUnboundTemplate(), first, getLast());
}
return mixedContent;
}
diff --git a/src/main/java/freemarker/core/TemplateObject.java b/src/main/java/freemarker/core/TemplateObject.java
index f3f9e05..805af1c 100644
--- a/src/main/java/freemarker/core/TemplateObject.java
+++ b/src/main/java/freemarker/core/TemplateObject.java
@@ -34,7 +34,7 @@
@Deprecated
public abstract class TemplateObject {
- private Template template;
+ private UnboundTemplate unboundTemplate;
int beginColumn, beginLine, endColumn, endLine;
/** This is needed for an ?eval hack; the expression AST nodes will be the descendants of the template, however,
@@ -42,89 +42,112 @@
* by a negative line numbers, starting from this constant as line 1. */
static final int RUNTIME_EVAL_LINE_DISPLACEMENT = -1000000000;
- final void setLocation(Template template, Token begin, Token end) {
- setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+ final void setLocation(UnboundTemplate unboundTemplate, Token begin, Token end) {
+ setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
}
- final void setLocation(Template template, Token tagBegin, Token tagEnd, TemplateElements children) {
+ final void setLocation(UnboundTemplate unboundTemplate, Token tagBegin, Token tagEnd, TemplateElements children) {
TemplateElement lastChild = children.getLast();
if (lastChild != null) {
// [<#if exp>children]<#else>
- setLocation(template, tagBegin, lastChild);
+ setLocation(unboundTemplate, tagBegin, lastChild);
} else {
// [<#if exp>]<#else>
- setLocation(template, tagBegin, tagEnd);
+ setLocation(unboundTemplate, tagBegin, tagEnd);
}
}
- final void setLocation(Template template, Token begin, TemplateObject end) {
- setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+ final void setLocation(UnboundTemplate unboundTemplate, Token begin, TemplateObject end) {
+ setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
}
- final void setLocation(Template template, TemplateObject begin, Token end) {
- setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+ final void setLocation(UnboundTemplate unboundTemplate, TemplateObject begin, Token end) {
+ setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
}
- final void setLocation(Template template, TemplateObject begin, TemplateObject end) {
- setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+ final void setLocation(UnboundTemplate unboundTemplate, TemplateObject begin, TemplateObject end) {
+ setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
}
- void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine) {
- this.template = template;
+ void setLocation(UnboundTemplate unboundTemplate, int beginColumn, int beginLine, int endColumn, int endLine) {
+ this.unboundTemplate = unboundTemplate;
this.beginColumn = beginColumn;
this.beginLine = beginLine;
this.endColumn = endColumn;
this.endLine = endLine;
}
+ /**
+ * <b>Internal API - subject to change</b>
+ */
public final int getBeginColumn() {
return beginColumn;
}
+ /**
+ * <b>Internal API - subject to change</b>
+ */
public final int getBeginLine() {
return beginLine;
}
+ /**
+ * <b>Internal API - subject to change</b>
+ */
public final int getEndColumn() {
return endColumn;
}
+ /**
+ * <b>Internal API - subject to change</b>
+ */
public final int getEndLine() {
return endLine;
}
/**
+ * <b>Internal API - subject to change</b>;
* Returns a string that indicates
* where in the template source, this object is.
*/
public String getStartLocation() {
- return MessageUtil.formatLocationForEvaluationError(template, beginLine, beginColumn);
+ return MessageUtil.formatLocationForEvaluationError(unboundTemplate, beginLine, beginColumn);
}
/**
- * As of 2.3.20. the same as {@link #getStartLocation}. Meant to be used where there's a risk of XSS
+ * <b>Internal API - subject to change</b>
+ *
+ * <p>As of 2.3.20. the same as {@link #getStartLocation}. Meant to be used where there's a risk of XSS
* when viewing error messages.
*/
public String getStartLocationQuoted() {
return getStartLocation();
}
+ /**
+ * <b>Internal API - subject to change</b>
+ */
public String getEndLocation() {
- return MessageUtil.formatLocationForEvaluationError(template, endLine, endColumn);
+ return MessageUtil.formatLocationForEvaluationError(unboundTemplate, endLine, endColumn);
}
/**
- * As of 2.3.20. the same as {@link #getEndLocation}. Meant to be used where there's a risk of XSS
+ * <b>Internal API - subject to change</b>
+ *
+ * <p>As of 2.3.20. the same as {@link #getEndLocation}. Meant to be used where there's a risk of XSS
* when viewing error messages.
*/
public String getEndLocationQuoted() {
return getEndLocation();
}
+ /**
+ * <b>Internal API - subject to change</b>
+ */
public final String getSource() {
String s;
- if (template != null) {
- s = template.getSource(beginColumn, beginLine, endColumn, endLine);
+ if (unboundTemplate != null) {
+ s = unboundTemplate.getSource(beginColumn, beginLine, endColumn, endLine);
} else {
s = null;
}
@@ -145,8 +168,10 @@
}
/**
- * @return whether the point in the template file specified by the
- * column and line numbers is contained within this template object.
+ * <b>Internal API - subject to change</b>
+ *
+ * @return whether the point in the template file specified by the column and line numbers is contained within this
+ * template object.
*/
public boolean contains(int column, int line) {
if (line < beginLine || line > endLine) {
@@ -166,15 +191,16 @@
}
/**
- * @deprecated This method will be removed in FreeMarker 2.4 because of architectural changes!
+ * <b>Internal API - subject to change</b>
+ *
+ * @since 2.4.0
*/
- @Deprecated
- public Template getTemplate() {
- return template;
+ public UnboundTemplate getUnboundTemplate() {
+ return unboundTemplate;
}
-
+
TemplateObject copyLocationFrom(TemplateObject from) {
- template = from.template;
+ unboundTemplate = from.unboundTemplate;
beginColumn = from.beginColumn;
beginLine = from.beginLine;
endColumn = from.endColumn;
@@ -183,8 +209,9 @@
}
/**
- * FTL generated from the AST of the node, which must be parseable to an AST that does the same as the original
- * source, assuming we turn off automatic white-space removal when parsing the canonical form.
+ * <b>Internal API - subject to change</b>; FTL generated from the AST of the node, which must be parseable to an
+ * AST that does the same as the original source, assuming we turn off automatic white-space removal when parsing
+ * the canonical form.
*
* @see TemplateElement#getDescription()
* @see #getNodeTypeSymbol()
diff --git a/src/main/java/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java b/src/main/java/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java
index 26d54aa..51ce3fc 100644
--- a/src/main/java/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java
+++ b/src/main/java/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java
@@ -79,7 +79,7 @@
static class ThreadInterruptionCheck extends TemplateElement {
private ThreadInterruptionCheck(TemplateElement te) throws ParseException {
- setLocation(te.getTemplate(), te.beginColumn, te.beginLine, te.beginColumn, te.beginLine);
+ setLocation(te.getUnboundTemplate(), te.beginColumn, te.beginLine, te.beginColumn, te.beginLine);
}
@Override
diff --git a/src/main/java/freemarker/core/TokenMgrError.java b/src/main/java/freemarker/core/TokenMgrError.java
index ae37ae6..5f46a14 100644
--- a/src/main/java/freemarker/core/TokenMgrError.java
+++ b/src/main/java/freemarker/core/TokenMgrError.java
@@ -21,6 +21,8 @@
import freemarker.template.Template;
+
+
/**
* Exception thrown on lower (lexical) level parsing errors. Shouldn't reach normal FreeMarker users, as FreeMarker
* usually catches this and wraps it into a {@link ParseException}.
@@ -32,6 +34,14 @@
* @see ParseException
*/
public class TokenMgrError extends Error {
+
+ /**
+ * The version identifier for this Serializable class.
+ * Increment only if the <i>serialized</i> form of the
+ * class changes.
+ */
+ private static final long serialVersionUID = 1L;
+
/*
* Ordinals for various reasons why an Error of this type can be thrown.
*/
@@ -67,59 +77,57 @@
private Integer endLineNumber, endColumnNumber;
/**
- * Replaces unprintable characters by their espaced (or unicode escaped)
+ * Replaces unprintable characters by their escaped (or unicode escaped)
* equivalents in the given string
*/
protected static final String addEscapes(String str) {
- StringBuilder retval = new StringBuilder();
- char ch;
- for (int i = 0; i < str.length(); i++) {
- switch (str.charAt(i))
- {
- case 0 :
- continue;
- case '\b':
- retval.append("\\b");
- continue;
- case '\t':
- retval.append("\\t");
- continue;
- case '\n':
- retval.append("\\n");
- continue;
- case '\f':
- retval.append("\\f");
- continue;
- case '\r':
- retval.append("\\r");
- continue;
- case '\"':
- retval.append("\\\"");
- continue;
- case '\'':
- retval.append("\\\'");
- continue;
- case '\\':
- retval.append("\\\\");
- continue;
- default:
- if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
- String s = "0000" + Integer.toString(ch, 16);
- retval.append("\\u" + s.substring(s.length() - 4, s.length()));
- } else {
- retval.append(ch);
- }
- continue;
- }
- }
- return retval.toString();
+ StringBuilder retval = new StringBuilder();
+ char ch;
+ for (int i = 0; i < str.length(); i++) {
+ switch (str.charAt(i))
+ {
+ case '\b':
+ retval.append("\\b");
+ continue;
+ case '\t':
+ retval.append("\\t");
+ continue;
+ case '\n':
+ retval.append("\\n");
+ continue;
+ case '\f':
+ retval.append("\\f");
+ continue;
+ case '\r':
+ retval.append("\\r");
+ continue;
+ case '\"':
+ retval.append("\\\"");
+ continue;
+ case '\'':
+ retval.append("\\\'");
+ continue;
+ case '\\':
+ retval.append("\\\\");
+ continue;
+ default:
+ if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
+ String s = "0000" + Integer.toString(ch, 16);
+ retval.append("\\u" + s.substring(s.length() - 4, s.length()));
+ } else {
+ retval.append(ch);
+ }
+ continue;
+ }
+ }
+ return retval.toString();
}
/**
- * Returns a detailed message for the Error when it's thrown by the
+ * Returns a detailed message for the Error when it is thrown by the
* token manager to indicate a lexical error.
- * Parameters :
- * EOFSeen : indicates if EOF caused the lexicl error
+ * Parameters :
+ * EOFSeen : indicates if EOF caused the lexical error
* curLexState : lexical state in which this error occurred
* errorLine : line number when the error occurred
* errorColumn : column number when the error occurred
@@ -127,10 +135,13 @@
* curchar : the offending character
* Note: You can customize the lexical error message by modifying this method.
*/
- protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) {
- return("Lexical error: encountered " +
- (EOFSeen ? "<EOF> " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int) curChar + "), ") +
- "after \"" + addEscapes(errorAfter) + "\".");
+ protected static String LexicalErr(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, int curChar) {
+ char curChar1 = (char) curChar;
+ return("Lexical error at line " +
+ errorLine + ", column " +
+ errorColumn + ". Encountered: " +
+ (EOFSeen ? "<EOF> " : ("\"" + addEscapes(String.valueOf(curChar1)) + "\"") + " (" + curChar + "), ") +
+ "after : \"" + addEscapes(errorAfter) + "\"");
}
/**
@@ -188,17 +199,8 @@
this.endColumnNumber = Integer.valueOf(endColumnNumber);
}
- /**
- * Overload for JavaCC 6 compatibility.
- *
- * @since 2.3.24
- */
- TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, int curChar, int reason) {
- this(EOFSeen, lexState, errorLine, errorColumn, errorAfter, (char) curChar, reason);
- }
-
- public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) {
- this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason);
+ public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, int curChar, int reason) {
+ this(LexicalErr(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason);
this.lineNumber = Integer.valueOf(errorLine); // In J2SE there was no Integer.valueOf(int)
this.columnNumber = Integer.valueOf(errorColumn);
@@ -251,9 +253,20 @@
return detail;
}
- public ParseException toParseException(Template template) {
+ /**
+ * @deprecated Use {@link #toParseException(UnboundTemplate)} instead.
+ */
+ @Deprecated
+public ParseException toParseException(Template template) {
+ return toParseException(template.getUnboundTemplate());
+ }
+
+ /**
+ * @since 2.4.0
+ */
+ public ParseException toParseException(UnboundTemplate unboundTemplate) {
return new ParseException(getDetail(),
- template,
+ unboundTemplate,
getLineNumber() != null ? getLineNumber().intValue() : 0,
getColumnNumber() != null ? getColumnNumber().intValue() : 0,
getEndLineNumber() != null ? getEndLineNumber().intValue() : 0,
diff --git a/src/main/java/freemarker/core/UnboundCallable.java b/src/main/java/freemarker/core/UnboundCallable.java
new file mode 100644
index 0000000..4066818
--- /dev/null
+++ b/src/main/java/freemarker/core/UnboundCallable.java
@@ -0,0 +1,240 @@
+/*
+ * 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 java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import freemarker.template.TemplateModel;
+
+/**
+ * Represents the definition of a macro or function (or other future callable entity) in the AST. For understanding
+ * related concepts more, see {@link BoundCallable}.
+ *
+ * <p>
+ * Historical note: Prior to 2.4, the two concepts ({@link UnboundCallable} and {@link BoundCallable}) were these same,
+ * represented by {@link Macro}, which still exists due to backward compatibility constraints, but now is abstract and
+ * is implemented by this class. This class should not implement {@link TemplateModel} (which it does, because
+ * {@link Macro} implements it), but it had to, for backward compatibility.
+ *
+ * @see BoundCallable
+ *
+ * @since 2.4.0
+ */
+class UnboundCallable extends Macro {
+
+ static final UnboundCallable NO_OP_MACRO = new UnboundCallable(".pass",
+ Collections.EMPTY_LIST,
+ Collections.EMPTY_MAP,
+ null, false,
+ TemplateElements.EMPTY);
+
+ final static int TYPE_MACRO = 0;
+ final static int TYPE_FUNCTION = 1;
+
+ private final String name;
+ private final String[] paramNames;
+ private final Map paramDefaults;
+ private final String catchAllParamName;
+ private final boolean function;
+
+ UnboundCallable(String name, List argumentNames, Map args,
+ String catchAllParamName, boolean function,
+ TemplateElements children) {
+ this.name = name;
+ this.paramNames = (String[]) argumentNames.toArray(
+ new String[argumentNames.size()]);
+ this.paramDefaults = args;
+
+ this.function = function;
+ this.catchAllParamName = catchAllParamName;
+
+ this.setChildren(children);
+ }
+
+ String[] getParamNames() {
+ return paramNames;
+ }
+
+ Map getParamDefaults() {
+ return paramDefaults;
+ }
+
+ @Override
+ public String getCatchAll() {
+ return catchAllParamName;
+ }
+
+ @Override
+ public String[] getArgumentNames() {
+ return paramNames.clone();
+ }
+
+ String[] getArgumentNamesInternal() {
+ return paramNames;
+ }
+
+ boolean hasArgNamed(String name) {
+ return paramDefaults.containsKey(name);
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ TemplateElement[] accept(Environment env) {
+ env.visitCallableDefinition(this);
+ return null;
+ }
+
+ @Override
+ protected String dump(boolean canonical) {
+ StringBuilder sb = new StringBuilder();
+ if (canonical) sb.append('<');
+ sb.append(getNodeTypeSymbol());
+ sb.append(' ');
+ sb.append(_CoreStringUtils.toFTLTopLevelTragetIdentifier(name));
+ if (function) sb.append('(');
+ int argCnt = paramNames.length;
+ for (int i = 0; i < argCnt; i++) {
+ if (function) {
+ if (i != 0) {
+ sb.append(", ");
+ }
+ } else {
+ sb.append(' ');
+ }
+ String argName = paramNames[i];
+ sb.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(argName));
+ if (paramDefaults != null && paramDefaults.get(argName) != null) {
+ sb.append('=');
+ Expression defaultExpr = (Expression) paramDefaults.get(argName);
+ if (function) {
+ sb.append(defaultExpr.getCanonicalForm());
+ } else {
+ MessageUtil.appendExpressionAsUntearable(sb, defaultExpr);
+ }
+ }
+ }
+ if (catchAllParamName != null) {
+ if (function) {
+ if (argCnt != 0) {
+ sb.append(", ");
+ }
+ } else {
+ sb.append(' ');
+ }
+ sb.append(catchAllParamName);
+ sb.append("...");
+ }
+ if (function) sb.append(')');
+ if (canonical) {
+ sb.append('>');
+ sb.append(getChildrenCanonicalForm());
+ sb.append("</").append(getNodeTypeSymbol()).append('>');
+ }
+ return sb.toString();
+ }
+
+ @Override
+ String getNodeTypeSymbol() {
+ return function ? "#function" : "#macro";
+ }
+
+ @Override
+ boolean isShownInStackTrace() {
+ return false;
+ }
+
+ @Override
+ boolean isNestedBlockRepeater() {
+ // Because of recursive calls
+ return true;
+ }
+ @Override
+ public boolean isFunction() {
+ return function;
+ }
+
+ @Override
+ int getParameterCount() {
+ return 1/*name*/ + paramNames.length * 2/*name=default*/ + 1/*catchAll*/ + 1/*type*/;
+ }
+
+ @Override
+ Object getParameterValue(int idx) {
+ if (idx == 0) {
+ return name;
+ } else {
+ final int argDescsEnd = paramNames.length * 2 + 1;
+ if (idx < argDescsEnd) {
+ String paramName = paramNames[(idx - 1) / 2];
+ if (idx % 2 != 0) {
+ return paramName;
+ } else {
+ return paramDefaults.get(paramName);
+ }
+ } else if (idx == argDescsEnd) {
+ return catchAllParamName;
+ } else if (idx == argDescsEnd + 1) {
+ return Integer.valueOf(function ? TYPE_FUNCTION : TYPE_MACRO);
+ } else {
+ throw new IndexOutOfBoundsException();
+ }
+ }
+ }
+
+ @Override
+ ParameterRole getParameterRole(int idx) {
+ if (idx == 0) {
+ return ParameterRole.ASSIGNMENT_TARGET;
+ } else {
+ final int argDescsEnd = paramNames.length * 2 + 1;
+ if (idx < argDescsEnd) {
+ if (idx % 2 != 0) {
+ return ParameterRole.PARAMETER_NAME;
+ } else {
+ return ParameterRole.PARAMETER_DEFAULT;
+ }
+ } else if (idx == argDescsEnd) {
+ return ParameterRole.CATCH_ALL_PARAMETER_NAME;
+ } else if (idx == argDescsEnd + 1) {
+ return ParameterRole.AST_NODE_SUBTYPE;
+ } else {
+ throw new IndexOutOfBoundsException();
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ final UnboundTemplate unboundTemplate = getUnboundTemplate();
+ return "UnboundCallable("
+ + "name=" + getName()
+ + ", isFunction=" + isFunction()
+ + ", unboundTemplate"
+ + (unboundTemplate != null ? ".sourceName=" + unboundTemplate.getSourceName() : "=null")
+ + ")";
+ }
+
+}
diff --git a/src/main/java/freemarker/core/UnboundTemplate.java b/src/main/java/freemarker/core/UnboundTemplate.java
new file mode 100644
index 0000000..7f7e96e
--- /dev/null
+++ b/src/main/java/freemarker/core/UnboundTemplate.java
@@ -0,0 +1,602 @@
+/*
+ * 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 java.io.BufferedReader;
+import java.io.FilterReader;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import freemarker.cache.TemplateLoader;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.Template.WrongEncodingException;
+import freemarker.template.Version;
+import freemarker.template._TemplateAPI;
+import freemarker.template.utility.NullArgumentException;
+
+/**
+ * The parsed representation of a template that's not yet bound to the {@link Template} properties that doesn't
+ * influence the result of the parsing. This information wasn't separated from {@link Template} in FreeMarker 2.3.x,
+ * and was factored out from it into this class in 2.4.0, to allow more efficient caching.
+ *
+ * @since 2.4.0
+ */
+public final class UnboundTemplate {
+
+ public static final String DEFAULT_NAMESPACE_PREFIX = "D";
+ public static final String NO_NS_PREFIX = "N";
+
+ private final String sourceName;
+ private final Configuration cfg;
+ private final ParserConfiguration parserCfg;
+ private final Version templateLanguageVersion;
+
+ /** Attributes added via {@code <#ftl attributes=...>}. */
+ private LinkedHashMap<String, Object> customAttributes;
+ private final Map<String, UnboundCallable> unboundCallables = new HashMap<String, UnboundCallable>(0);
+ // Earlier it was a Vector, so I thought the safest is to keep it synchronized:
+ private final List<LibraryLoad> imports = Collections.synchronizedList(new ArrayList<LibraryLoad>(0));
+ private final TemplateElement rootElement;
+ private String defaultNamespaceURI;
+ private final int actualTagSyntax;
+ private final int actualNamingConvention;
+ private OutputFormat outputFormat;
+ private boolean autoEscaping;
+
+ private final String templateSpecifiedEncoding;
+
+ private final ArrayList lines = new ArrayList();
+
+ private Map<String, String> prefixToNamespaceURIMapping;
+ private Map<String, String> namespaceURIToPrefixMapping;
+
+ /**
+ * @param reader
+ * Reads the template source code
+ * @param cfg
+ * The FreeMarker configuration settings; the resulting {@link UnboundTemplate} will be bound to this.
+ * @param customParserCfg
+ * Overrides the parsing related configuration settings of the {@link Configuration} parameter; can be
+ * {@code null}. See the similar paramter of
+ * {@link Template#Template(String, String, Reader, Configuration, ParserConfiguration, String)} for more
+ * details.
+ * @param assumedEncoding
+ * This is the name of the charset that we are supposed to be using. This is only needed to check if the
+ * encoding specified in the {@code #ftl} header (if any) matches this. If this is non-{@code null} and
+ * they don't match, a {@link WrongEncodingException} will be thrown by the parser.
+ * @param sourceName
+ * Shown in error messages as the template "file" location.
+ */
+ UnboundTemplate(Reader reader, String sourceName, Configuration cfg, ParserConfiguration customParserCfg,
+ String assumedEncoding)
+ throws IOException {
+ NullArgumentException.check(cfg);
+ this.cfg = cfg;
+ this.parserCfg = customParserCfg != null ? customParserCfg : cfg;
+ this.sourceName = sourceName;
+ this.templateLanguageVersion = normalizeTemplateLanguageVersion(
+ getParserConfiguration().getIncompatibleImprovements());
+
+ LineTableBuilder ltbReader;
+ try {
+ if (!(reader instanceof BufferedReader) && !(reader instanceof StringReader)) {
+ reader = new BufferedReader(reader, 0x1000);
+ }
+ ltbReader = new LineTableBuilder(reader);
+ reader = ltbReader;
+
+ try {
+ FMParser parser = new FMParser(this, reader, assumedEncoding, getParserConfiguration());
+
+ TemplateElement rootElement;
+ try {
+ rootElement = parser.Root();
+ } catch (IndexOutOfBoundsException exc) {
+ // There's a JavaCC bug where the Reader throws a RuntimeExcepton and then JavaCC fails with
+ // IndexOutOfBoundsException. If that wasn't the case, we just rethrow. Otherwise we suppress the
+ // IndexOutOfBoundsException and let the real cause to be thrown later.
+ if (!ltbReader.hasFailure()) {
+ throw exc;
+ }
+ rootElement = null;
+ }
+ this.rootElement = rootElement;
+
+ this.actualTagSyntax = parser._getLastTagSyntax();
+ this.actualNamingConvention = parser._getLastNamingConvention();
+ this.templateSpecifiedEncoding = parser._getTemplateSpecifiedEncoding();
+ } catch (TokenMgrError exc) {
+ // TokenMgrError VS ParseException is not an interesting difference for the user, so we just convert it
+ // to ParseException
+ throw exc.toParseException(this);
+ }
+ } catch (ParseException e) {
+ e.setTemplateName(getSourceName());
+ throw e;
+ } finally {
+ reader.close();
+ }
+
+ // Throws any exception that JavaCC has silently treated as EOF:
+ ltbReader.throwFailure();
+
+ if (prefixToNamespaceURIMapping != null) {
+ prefixToNamespaceURIMapping = Collections.unmodifiableMap(prefixToNamespaceURIMapping);
+ namespaceURIToPrefixMapping = Collections.unmodifiableMap(namespaceURIToPrefixMapping);
+ }
+ }
+
+ /**
+ * Creates a plain text (unparsed) template.
+ */
+ static UnboundTemplate newPlainTextUnboundTemplate(String content, String sourceName, Configuration cfg) {
+ UnboundTemplate template;
+ try {
+ template = new UnboundTemplate(new StringReader("X"), sourceName, cfg, null, null);
+ } catch (IOException e) {
+ throw new BugException("Plain text template creation failed", e);
+ }
+ ((TextBlock) template.rootElement).replaceText(content);
+ return template;
+ }
+
+ private static Version normalizeTemplateLanguageVersion(Version incompatibleImprovements) {
+ _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
+ int v = incompatibleImprovements.intValue();
+ if (v < _TemplateAPI.VERSION_INT_2_3_19) {
+ return Configuration.VERSION_2_3_0;
+ } else if (v > _TemplateAPI.VERSION_INT_2_3_21) {
+ return Configuration.VERSION_2_3_21;
+ } else { // if 2.3.19 or 2.3.20 or 2.3.21
+ return incompatibleImprovements;
+ }
+ }
+
+ /**
+ * Returns a string representing the raw template text in canonical form.
+ */
+ @Override
+ public String toString() {
+ StringWriter sw = new StringWriter();
+ try {
+ dump(sw);
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe.getMessage());
+ }
+ return sw.toString();
+ }
+
+ /**
+ * The name that was actually used to load this template from the {@link TemplateLoader} (or from other custom
+ * storage mechanism). This is what should be shown in error messages as the error location.
+ *
+ * @see Template#getSourceName()
+ */
+ public String getSourceName() {
+ return sourceName;
+ }
+
+ /**
+ * Return the template language (FTL) version used by this template. For now (2.3.21) this is the same as
+ * {@link Configuration#getIncompatibleImprovements()}, except that it's normalized to the lowest version where the
+ * template language was changed.
+ */
+ public Version getTemplateLanguageVersion() {
+ return templateLanguageVersion;
+ }
+
+ /**
+ * See {@link Template#getActualTagSyntax()}.
+ */
+ public int getActualTagSyntax() {
+ return actualTagSyntax;
+ }
+
+ /**
+ * See {@link Template#getActualNamingConvention()}.
+ */
+ public int getActualNamingConvention() {
+ return actualNamingConvention;
+ }
+
+ /**
+ * See {@link Template#getOutputFormat()}.
+ */
+ public OutputFormat getOutputFormat() {
+ return outputFormat;
+ }
+
+ /**
+ * Meant to be called by the parser only.
+ */
+ void setOutputFormat(OutputFormat outputFormat) {
+ this.outputFormat = outputFormat;
+ }
+
+ /**
+ * See {@link Template#getAutoEscaping()}.
+ */
+ public boolean getAutoEscaping() {
+ return autoEscaping;
+ }
+
+ /**
+ * Meant to be called by the parser only.
+ */
+ void setAutoEscaping(boolean autoEscaping) {
+ this.autoEscaping = autoEscaping;
+ }
+
+ public Configuration getConfiguration() {
+ return cfg;
+ }
+
+ /**
+ * Returns the parser configuration that was in effect when creating this template; never {@code null}.
+ * See {@link Template#getParserConfiguration()} for details.
+ */
+ public ParserConfiguration getParserConfiguration() {
+ return parserCfg;
+ }
+
+ /**
+ * Dump the raw template in canonical form.
+ */
+ public void dump(PrintStream ps) {
+ ps.print(rootElement.getCanonicalForm());
+ }
+
+ /**
+ * Dump the raw template in canonical form.
+ */
+ public void dump(Writer out) throws IOException {
+ out.write(rootElement.getCanonicalForm());
+ }
+
+ /**
+ * Called by code internally to maintain a table of macros
+ */
+ void addUnboundCallable(UnboundCallable unboundCallable) {
+ unboundCallables.put(unboundCallable.getName(), unboundCallable);
+ }
+
+ /**
+ * Called by code internally to maintain a list of imports
+ */
+ void addImport(LibraryLoad libLoad) {
+ imports.add(libLoad);
+ }
+
+ /**
+ * Returns the template source at the location specified by the coordinates given, or {@code null} if unavailable.
+ *
+ * @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 endLine
+ * the last line of the requested source, 1-based
+ * @see freemarker.core.TemplateObject#getSource()
+ */
+ public String getSource(int beginColumn,
+ int beginLine,
+ int endColumn,
+ int endLine) {
+ if (beginLine < 1 || endLine < 1) return null; // dynamically ?eval-ed expressions has no source available
+
+ // Our container is zero-based.
+ --beginLine;
+ --beginColumn;
+ --endColumn;
+ --endLine;
+ StringBuilder buf = new StringBuilder();
+ for (int i = beginLine; i <= endLine; i++) {
+ if (i < lines.size()) {
+ buf.append(lines.get(i));
+ }
+ }
+ int lastLineLength = lines.get(endLine).toString().length();
+ int trailingCharsToDelete = lastLineLength - endColumn - 1;
+ buf.delete(0, beginColumn);
+ buf.delete(buf.length() - trailingCharsToDelete, buf.length());
+ return buf.toString();
+ }
+
+ /**
+ * Used internally by the parser.
+ */
+ void setCustomAttribute(String key, Object value) {
+ LinkedHashMap<String, Object> attrs = customAttributes;
+ if (attrs == null) {
+ attrs = new LinkedHashMap<String, Object>();
+ customAttributes = attrs;
+ }
+ attrs.put(key, value);
+ }
+
+ /**
+ * Returns the {@link Map} of custom attributes that are normally coming from the {@code #ftl} header, or
+ * {@code null} if there was none. The returned {@code Map} must not be modified, and might changes during
+ * template parsing as new attributes are added by the parser (i.e., it's not a snapshot).
+ */
+ Map<String, ?> getCustomAttributes() {
+ return this.customAttributes;
+ }
+
+ /**
+ * @return the root TemplateElement object.
+ */
+ TemplateElement getRootTreeNode() {
+ return rootElement;
+ }
+
+ Map<String, UnboundCallable> getUnboundCallables() {
+ return unboundCallables;
+ }
+
+ List<LibraryLoad> getImports() {
+ return imports;
+ }
+
+ /**
+ * This is used internally.
+ */
+ void addPrefixToNamespaceURIMapping(String prefix, String nsURI) {
+ if (nsURI.length() == 0) {
+ throw new IllegalArgumentException("Cannot map empty string URI");
+ }
+ if (prefix.length() == 0) {
+ throw new IllegalArgumentException("Cannot map empty string prefix");
+ }
+ if (prefix.equals(NO_NS_PREFIX)) {
+ throw new IllegalArgumentException("The prefix: " + prefix
+ + " cannot be registered, it's reserved for special internal use.");
+ }
+
+ if (prefixToNamespaceURIMapping != null) {
+ if (prefixToNamespaceURIMapping.containsKey(prefix)) {
+ throw new IllegalArgumentException("The prefix: '" + prefix + "' was repeated. This is illegal.");
+ }
+ if (namespaceURIToPrefixMapping.containsKey(nsURI)) {
+ throw new IllegalArgumentException("The namespace URI: " + nsURI
+ + " cannot be mapped to 2 different prefixes.");
+ }
+ }
+
+ if (prefix.equals(DEFAULT_NAMESPACE_PREFIX)) {
+ this.defaultNamespaceURI = nsURI;
+ } else {
+ if (prefixToNamespaceURIMapping == null) {
+ prefixToNamespaceURIMapping = new HashMap<String, String>();
+ namespaceURIToPrefixMapping = new HashMap<String, String>();
+ }
+ prefixToNamespaceURIMapping.put(prefix, nsURI);
+ namespaceURIToPrefixMapping.put(nsURI, prefix);
+ }
+ }
+
+ public String getDefaultNamespaceURI() {
+ return this.defaultNamespaceURI;
+ }
+
+ /**
+ * @return The namespace URI mapped to this node value prefix, or {@code null}.
+ */
+ public String getNamespaceURIForPrefix(String prefix) {
+ if (prefix.equals("")) {
+ return defaultNamespaceURI == null ? "" : defaultNamespaceURI;
+ }
+
+ final Map<String, String> m = prefixToNamespaceURIMapping;
+ return m != null ? m.get(prefix) : null;
+ }
+
+ /**
+ * The encoding (charset name) specified by the template itself (as of 2.3.22, via {@code <#ftl encoding=...>}), or
+ * {@code null} if none was specified.
+ */
+ public String getTemplateSpecifiedEncoding() {
+ return templateSpecifiedEncoding;
+ }
+
+ /**
+ * @return the prefix mapped to this nsURI in this template. (Or null if there is none.)
+ */
+ public String getPrefixForNamespaceURI(String nsURI) {
+ if (nsURI == null) {
+ return null;
+ }
+ if (nsURI.length() == 0) {
+ return defaultNamespaceURI == null ? "" : NO_NS_PREFIX;
+ }
+ if (nsURI.equals(defaultNamespaceURI)) {
+ return "";
+ }
+
+ final Map<String, String> m = namespaceURIToPrefixMapping;
+ return m != null ? m.get(nsURI) : null;
+ }
+
+ /**
+ * @return the prefixed name, based on the ns_prefixes defined in this template's header for the local name and node
+ * namespace passed in as parameters.
+ */
+ public String getPrefixedName(String localName, String nsURI) {
+ if (nsURI == null || nsURI.length() == 0) {
+ if (defaultNamespaceURI != null) {
+ return NO_NS_PREFIX + ":" + localName;
+ } else {
+ return localName;
+ }
+ }
+ if (nsURI.equals(defaultNamespaceURI)) {
+ return localName;
+ }
+ String prefix = getPrefixForNamespaceURI(nsURI);
+ if (prefix == null) {
+ return null;
+ }
+ return prefix + ":" + localName;
+ }
+
+ /**
+ * @return an array of the {@link TemplateElement}s containing the given column and line numbers.
+ */
+ List<TemplateElement> containingElements(int column, int line) {
+ final ArrayList<TemplateElement> elements = new ArrayList<TemplateElement>();
+ TemplateElement element = rootElement;
+ mainloop: while (element.contains(column, line)) {
+ elements.add(element);
+ for (Enumeration enumeration = element.children(); enumeration.hasMoreElements(); ) {
+ TemplateElement elem = (TemplateElement) enumeration.nextElement();
+ if (elem.contains(column, line)) {
+ element = elem;
+ continue mainloop;
+ }
+ }
+ break;
+ }
+ return elements.isEmpty() ? null : elements;
+ }
+
+ /**
+ * Reader that builds up the line table info for us, and also helps in working around JavaCC's exception
+ * suppression.
+ */
+ private class LineTableBuilder extends FilterReader {
+
+ private final StringBuilder lineBuf = new StringBuilder();
+ int lastChar;
+ boolean closed;
+
+ /** Needed to work around JavaCC behavior where it silently treats any errors as EOF. */
+ private Exception failure;
+
+ /**
+ * @param r the character stream to wrap
+ */
+ LineTableBuilder(Reader r) {
+ super(r);
+ }
+
+ public boolean hasFailure() {
+ return failure != null;
+ }
+
+ public void throwFailure() throws IOException {
+ if (failure != null) {
+ if (failure instanceof IOException) {
+ throw (IOException) failure;
+ }
+ if (failure instanceof RuntimeException) {
+ throw (RuntimeException) failure;
+ }
+ throw new UndeclaredThrowableException(failure);
+ }
+ }
+
+ @Override
+ public int read() throws IOException {
+ try {
+ int c = in.read();
+ handleChar(c);
+ return c;
+ } catch (Exception e) {
+ throw rememberException(e);
+ }
+ }
+
+ private IOException rememberException(Exception e) throws IOException {
+ // JavaCC used to read from the Reader after it was closed. So we must not treat that as a failure.
+ if (!closed) {
+ failure = e;
+ }
+ if (e instanceof IOException) {
+ return (IOException) e;
+ }
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ }
+ throw new UndeclaredThrowableException(e);
+ }
+
+ @Override
+ public int read(char cbuf[], int off, int len) throws IOException {
+ try {
+ int numchars = in.read(cbuf, off, len);
+ for (int i = off; i < off + numchars; i++) {
+ char c = cbuf[i];
+ handleChar(c);
+ }
+ return numchars;
+ } catch (Exception e) {
+ throw rememberException(e);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (lineBuf.length() > 0) {
+ lines.add(lineBuf.toString());
+ lineBuf.setLength(0);
+ }
+ super.close();
+ closed = true;
+ }
+
+ private void handleChar(int c) {
+ if (c == '\n' || c == '\r') {
+ if (lastChar == '\r' && c == '\n') { // CRLF under Windoze
+ int lastIndex = lines.size() - 1;
+ String lastLine = (String) lines.get(lastIndex);
+ lines.set(lastIndex, lastLine + '\n');
+ } else {
+ lineBuf.append((char) c);
+ lines.add(lineBuf.toString());
+ lineBuf.setLength(0);
+ }
+ } else if (c == '\t') {
+ int numSpaces = 8 - (lineBuf.length() % 8);
+ for (int i = 0; i < numSpaces; i++) {
+ lineBuf.append(' ');
+ }
+ } else {
+ lineBuf.append((char) c);
+ }
+ lastChar = c;
+ }
+ }
+
+}
diff --git a/src/main/java/freemarker/core/UnifiedCall.java b/src/main/java/freemarker/core/UnifiedCall.java
index 6608b0a..04a6d3a 100644
--- a/src/main/java/freemarker/core/UnifiedCall.java
+++ b/src/main/java/freemarker/core/UnifiedCall.java
@@ -70,17 +70,18 @@
@Override
TemplateElement[] accept(Environment env) throws TemplateException, IOException {
- TemplateModel tm = nameExp.eval(env);
- if (tm == Macro.DO_NOTHING_MACRO) return null; // shortcut here.
- if (tm instanceof Macro) {
- Macro macro = (Macro) tm;
- if (macro.isFunction() && !legacySyntax) {
+ final TemplateModel tm = nameExp.eval(env);
+ if (tm == UnboundCallable.NO_OP_MACRO) return null; // shortcut here.
+ if (tm instanceof BoundCallable) {
+ final BoundCallable boundMacro = (BoundCallable) tm;
+ final Macro unboundMacro = boundMacro.getUnboundCallable();
+ if (unboundMacro.isFunction() && !legacySyntax) {
throw new _MiscTemplateException(env,
- "Routine ", new _DelayedJQuote(macro.getName()), " is a function, not a directive. "
+ "Routine ", new _DelayedJQuote(unboundMacro.getName()), " is a function, not a directive. "
+ "Functions can only be called from expressions, like in ${f()}, ${x + f()} or ",
"<@someDirective someParam=f() />", ".");
}
- env.invoke(macro, namedArgs, positionalArgs, bodyParameterNames, getChildBuffer());
+ env.invoke(boundMacro, namedArgs, positionalArgs, bodyParameterNames, getChildBuffer());
} else {
boolean isDirectiveModel = tm instanceof TemplateDirectiveModel;
if (isDirectiveModel || tm instanceof TemplateTransformModel) {
@@ -328,6 +329,10 @@
}
+ public String getTemplateSourceName() {
+ return getUnboundTemplate().getSourceName();
+ }
+
@Override
boolean isNestedBlockRepeater() {
return true;
diff --git a/src/test/java/freemarker/core/LegacyFMParserConstructorsTest.java b/src/main/java/freemarker/core/_2_4_OrLaterMarker.java
similarity index 67%
rename from src/test/java/freemarker/core/LegacyFMParserConstructorsTest.java
rename to src/main/java/freemarker/core/_2_4_OrLaterMarker.java
index 738d227..81736a1 100644
--- a/src/test/java/freemarker/core/LegacyFMParserConstructorsTest.java
+++ b/src/main/java/freemarker/core/_2_4_OrLaterMarker.java
@@ -16,22 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
+
package freemarker.core;
-import org.junit.Test;
+/**
+ * Don't use this; used internally by FreeMarker, might changes without notice.
+ * Used internally for detecting duplicate FreeMarker jar-s with different versions.
+ */
+public class _2_4_OrLaterMarker {
-public class LegacyFMParserConstructorsTest {
-
- @Test
- public void test1() throws ParseException {
- FMParser parser = new FMParser("x");
- parser.Root();
- }
-
- @Test
- public void testCreateExpressionParser() throws ParseException {
- FMParser parser = FMParser.createExpressionParser("x + y");
- parser.Expression();
- }
-
}
diff --git a/src/main/java/freemarker/core/_CoreAPI.java b/src/main/java/freemarker/core/_CoreAPI.java
index df95196..d5b3977 100644
--- a/src/main/java/freemarker/core/_CoreAPI.java
+++ b/src/main/java/freemarker/core/_CoreAPI.java
@@ -19,9 +19,15 @@
package freemarker.core;
+import java.io.IOException;
+import java.io.Reader;
import java.io.Writer;
import java.util.Collection;
import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
@@ -156,6 +162,80 @@
Environment.outputInstructionStack(instructionStackSnapshot, terseMode, pw);
}
+ public static Map<String, ?> getCustomAttributes(UnboundTemplate unboundTemplate) {
+ return unboundTemplate.getCustomAttributes();
+ }
+
+ /**
+ * For emulating legacy {@link Template#addMacro(Macro)}.
+ */
+ public static void addMacro(UnboundTemplate unboundTemplate, Macro macro) {
+ final UnboundCallable unboundCallable = macroToUnboundCallable(macro);
+ unboundTemplate.addUnboundCallable(unboundCallable);
+ }
+
+ /**
+ * In 2.4 {@link Macro} was split to {@link BoundCallable} and {@link UnboundCallable}, but because of BC
+ * constraints sometimes we can only expect a {@link Macro}, but that can always converted to
+ * {@link UnboundCallable}.
+ */
+ private static UnboundCallable macroToUnboundCallable(Macro macro) {
+ if (macro instanceof UnboundCallable) {
+ // It's coming from the AST:
+ return (UnboundCallable) macro;
+ } else if (macro instanceof BoundCallable) {
+ // It's coming from an FTL variable:
+ return ((BoundCallable) macro).getUnboundCallable();
+ } else if (macro == null) {
+ return null;
+ } else {
+ // Impossible, Macro should have only two subclasses.
+ throw new BugException();
+ }
+ }
+
+ public static void addImport(UnboundTemplate unboundTemplate, LibraryLoad libLoad) {
+ unboundTemplate.addImport(libLoad);
+ }
+
+ public static UnboundTemplate newUnboundTemplate(Reader reader, String sourceName,
+ Configuration cfg, ParserConfiguration parserCfg, String assumedEncoding) throws IOException {
+ return new UnboundTemplate(reader, sourceName, cfg, parserCfg, assumedEncoding);
+ }
+
+ public static boolean isBoundCallable(Object obj) {
+ return obj instanceof BoundCallable;
+ }
+
+ public static UnboundTemplate newPlainTextUnboundTemplate(String content, String sourceName, Configuration config) {
+ return UnboundTemplate.newPlainTextUnboundTemplate(content, sourceName, config);
+ }
+
+ /** Used for implementing the deprecated {@link Template} method with similar name. */
+ public static TemplateElement getRootTreeNode(UnboundTemplate unboundTemplate) {
+ return unboundTemplate.getRootTreeNode();
+ }
+
+ /** Used for implementing the deprecated {@link Template} method with similar name. */
+ public static Map<String, UnboundCallable> getUnboundCallables(UnboundTemplate unboundTemplate) {
+ return unboundTemplate.getUnboundCallables();
+ }
+
+ /** Used for implementing the deprecated {@link Template} method with similar name. */
+ public static List getImports(UnboundTemplate unboundTemplate) {
+ return unboundTemplate.getImports();
+ }
+
+ /** Used for implementing the deprecated {@link Template} method with similar name. */
+ public static void addPrefixNSMapping(UnboundTemplate unboundTemplate, String prefix, String nsURI) {
+ unboundTemplate.addPrefixToNamespaceURIMapping(prefix, nsURI);
+ }
+
+ /** Used for implementing the deprecated {@link Template} method with similar name. */
+ public static List<TemplateElement> containingElements(UnboundTemplate unboundTemplate, int column, int line) {
+ return unboundTemplate.containingElements(column, line);
+ }
+
/**
* ATTENTION: This is used by https://github.com/kenshoo/freemarker-online. Don't break backward
* compatibility without updating that project too!
@@ -172,9 +252,221 @@
throws NestedContentNotSupportedException {
NestedContentNotSupportedException.check(body);
}
+
+ public static Map<String, Macro> createAdapterMacroMapForUnboundCallables(UnboundTemplate unboundTemplate) {
+ return new AdapterMacroMap(getUnboundCallables(unboundTemplate));
+ }
- static final public void replaceText(TextBlock textBlock, String text) {
- textBlock.replaceText(text);
+ /**
+ * Wraps a {@code Map<String, UnboundCallable>} as if it was a {@code Map<String, Macro>}. This is for backward
+ * compatibility. The important use case is being able to put any {@link Macro} subclass into this {@code Map},
+ * despite that the backing {@link Macro} can only store {@code UnboundCallable}. Reading works a bit strangely,
+ * because if you put a non-{@link UnboundCallable} value in, then get it with the same key, you get the
+ * corresponding {@link UnboundCallable} back instead of the original object. That's also a {@code Macro} though.
+ */
+ private static class AdapterMacroMap implements Map<String, Macro> {
+
+ private final Map<String, UnboundCallable> adapted;
+
+ public AdapterMacroMap(Map<String, UnboundCallable> unboundCallableMap) {
+ this.adapted = unboundCallableMap;
+ }
+
+ public int size() {
+ return adapted.size();
+ }
+
+ public boolean isEmpty() {
+ return adapted.isEmpty();
+ }
+
+ public boolean containsKey(Object key) {
+ return adapted.containsKey(key);
+ }
+
+ public boolean containsValue(Object value) {
+ return adapted.containsValue(value);
+ }
+
+ public UnboundCallable get(Object key) {
+ return adapted.get(key);
+ }
+
+ public UnboundCallable put(String key, Macro value) {
+ return adapted.put(key, macroToUnboundCallable(value));
+ }
+
+ public UnboundCallable remove(Object key) {
+ return adapted.remove(key);
+ }
+
+ public void putAll(Map<? extends String, ? extends Macro> t) {
+ for (Map.Entry<? extends String, ? extends Macro> ent : t.entrySet()) {
+ put(ent.getKey(), ent.getValue());
+ }
+ }
+
+ public void clear() {
+ adapted.clear();
+ }
+
+ public Set<String> keySet() {
+ return adapted.keySet();
+ }
+
+ public Collection<Macro> values() {
+ // According the Map API, this Collection doesn't allows adding elements, it's safe to treat as a
+ // Collection<Macro>.
+ return (Collection) adapted.values();
+ }
+
+ public Set<Entry<String, Macro>> entrySet() {
+ // According the Map API, this set doesn't allows adding elements, but, the Map.Entry-s are still
+ // modifiable.
+ return new AdapterMacroMapEntrySet(adapted.entrySet());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return adapted.equals(o);
+ }
+
+ @Override
+ public int hashCode() {
+ return adapted.hashCode();
+ }
+
+ }
+
+ /** Helper for {@link AdapterMacroMap}. */
+ private static class AdapterMacroMapEntrySet implements Set<Map.Entry<String, Macro>> {
+
+ private final Set<Map.Entry<String, UnboundCallable>> adapted;
+
+ public AdapterMacroMapEntrySet(Set<Entry<String, UnboundCallable>> adapted) {
+ this.adapted = adapted;
+ }
+
+ public int size() {
+ return adapted.size();
+ }
+
+ public boolean isEmpty() {
+ return adapted.isEmpty();
+ }
+
+ public boolean contains(Object o) {
+ return adapted.contains(o);
+ }
+
+ public Iterator<Entry<String, Macro>> iterator() {
+ return new AdapterMacroMapEntrySetIterator(adapted.iterator());
+ }
+
+ public Object[] toArray() {
+ return adapted.toArray();
+ }
+
+ public <T> T[] toArray(T[] a) {
+ return adapted.toArray(a);
+ }
+
+ public boolean add(Entry<String, Macro> o) {
+ // Won't be allowed anyway
+ return adapted.add((Entry) o);
+ }
+
+ public boolean remove(Object o) {
+ return adapted.remove(o);
+ }
+
+ public boolean containsAll(Collection<?> c) {
+ return adapted.containsAll(c);
+ }
+
+ public boolean addAll(Collection<? extends Entry<String, Macro>> c) {
+ // Won't be allowed anyway
+ return adapted.addAll((Collection) c);
+ }
+
+ public boolean retainAll(Collection<?> c) {
+ return adapted.retainAll(c);
+ }
+
+ public boolean removeAll(Collection<?> c) {
+ return adapted.removeAll(c);
+ }
+
+ public void clear() {
+ adapted.clear();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return adapted.equals(o);
+ }
+
+ @Override
+ public int hashCode() {
+ return adapted.hashCode();
+ }
+
+ }
+
+ /** Helper for {@link AdapterMacroMap}. */
+ private static class AdapterMacroMapEntrySetIterator implements Iterator<Map.Entry<String, Macro>> {
+
+ private final Iterator<Map.Entry<String, UnboundCallable>> adapted;
+
+ public AdapterMacroMapEntrySetIterator(Iterator<Entry<String, UnboundCallable>> adapted) {
+ this.adapted = adapted;
+ }
+
+ public boolean hasNext() {
+ return adapted.hasNext();
+ }
+
+ public Entry<String, Macro> next() {
+ return new AdapterMacroMapEntry(adapted.next());
+ }
+
+ public void remove() {
+ adapted.remove();
+ }
+
+ }
+
+ /** Helper for {@link AdapterMacroMap}. */
+ private static class AdapterMacroMapEntry implements Map.Entry<String, Macro> {
+
+ private final Map.Entry<String, UnboundCallable> adapted;
+
+ public AdapterMacroMapEntry(Entry<String, UnboundCallable> adapted) {
+ this.adapted = adapted;
+ }
+
+ public String getKey() {
+ return adapted.getKey();
+ }
+
+ public UnboundCallable getValue() {
+ return adapted.getValue();
+ }
+
+ public UnboundCallable setValue(Macro value) {
+ return adapted.setValue(macroToUnboundCallable(value));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return adapted.equals(o);
+ }
+
+ @Override
+ public int hashCode() {
+ return adapted.hashCode();
+ }
+
}
/**
diff --git a/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java b/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java
index a4b4731..b478713 100644
--- a/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java
+++ b/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java
@@ -208,7 +208,9 @@
}
private void appendParts(StringBuilder sb, Object[] parts) {
- Template template = this.template != null ? this.template : (blamed != null ? blamed.getTemplate() : null);
+ UnboundTemplate unboundTemplate = this.template != null
+ ? this.template.getUnboundTemplate()
+ : (blamed != null ? blamed.getUnboundTemplate() : null);
for (int i = 0; i < parts.length; i++) {
Object partObj = parts[i];
if (partObj instanceof Object[]) {
@@ -220,7 +222,7 @@
partStr = "null";
}
- if (template != null) {
+ if (unboundTemplate != null) {
if (partStr.length() > 4
&& partStr.charAt(0) == '<'
&& (
@@ -228,7 +230,7 @@
|| (partStr.charAt(1) == '/') && (partStr.charAt(2) == '#' || partStr.charAt(2) == '@')
)
&& partStr.charAt(partStr.length() - 1) == '>') {
- if (template.getActualTagSyntax() == Configuration.SQUARE_BRACKET_TAG_SYNTAX) {
+ if (unboundTemplate.getActualTagSyntax() == Configuration.SQUARE_BRACKET_TAG_SYNTAX) {
sb.append('[');
sb.append(partStr.substring(1, partStr.length() - 1));
sb.append(']');
diff --git a/src/main/java/freemarker/ext/dom/Transform.java b/src/main/java/freemarker/ext/dom/Transform.java
deleted file mode 100644
index 5df9683..0000000
--- a/src/main/java/freemarker/ext/dom/Transform.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * 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.ext.dom;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.util.Locale;
-import java.util.StringTokenizer;
-
-import freemarker.core.Environment;
-import freemarker.template.Configuration;
-import freemarker.template.Template;
-
-/**
- * A class that contains a main() method for command-line invocation of a FreeMarker XML transformation.
- *
- * @deprecated Will be removed (main method in a library, often classified as CWE-489 "Leftover Debug Code").
- */
-@Deprecated
-public class Transform {
-
- private File inputFile, ftlFile, outputFile;
- private String encoding;
- private Locale locale;
- private Configuration cfg;
-
- /**
- * @deprecated Will be removed (main method in a library, often classified as CWE-489 "Leftover Debug Code").
- */
- @Deprecated
- static public void main(String[] args) {
- try {
- Transform proc = transformFromArgs(args);
- proc.transform();
- } catch (IllegalArgumentException iae) {
- System.err.println(iae.getMessage());
- usage();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- /**
- * @param inputFile
- * The file from which to read the XML input
- * @param ftlFile
- * The file containing the template
- * @param outputFile
- * The file to which to output. If this is null, we use stdout.
- * @param locale
- * The locale to use. If this is null, we use the platform default.
- * @param encoding
- * The character encoding to use for output, if this is null, we use the platform default
- */
- Transform(File inputFile, File ftlFile, File outputFile, Locale locale, String encoding) throws IOException {
- if (encoding == null) {
- encoding = System.getProperty("file.encoding");
- }
- if (locale == null) {
- locale = Locale.getDefault();
- }
- this.encoding = encoding;
- this.locale = locale;
- this.inputFile = inputFile;
- this.ftlFile = ftlFile;
- this.outputFile = outputFile;
- File ftlDirectory = ftlFile.getAbsoluteFile().getParentFile();
- cfg = new Configuration();
- cfg.setDirectoryForTemplateLoading(ftlDirectory);
- }
-
- /**
- * Performs the transformation.
- */
- void transform() throws Exception {
- String templateName = ftlFile.getName();
- Template template = cfg.getTemplate(templateName, locale);
- NodeModel rootNode = NodeModel.parse(inputFile);
- OutputStream outputStream = System.out;
- if (outputFile != null) {
- outputStream = new FileOutputStream(outputFile);
- }
- Writer outputWriter = new OutputStreamWriter(outputStream, encoding);
- try {
- template.process(null, outputWriter, null, rootNode);
- } finally {
- if (outputFile != null)
- outputWriter.close();
- }
- }
-
- static Transform transformFromArgs(String[] args) throws IOException {
- int i = 0;
- String input = null, output = null, ftl = null, loc = null, enc = null;
- while (i < args.length) {
- String dashArg = args[i++];
- if (i >= args.length) {
- throw new IllegalArgumentException("");
- }
- String arg = args[i++];
- if (dashArg.equals("-in")) {
- if (input != null) {
- throw new IllegalArgumentException("The input file should only be specified once");
- }
- input = arg;
- } else if (dashArg.equals("-ftl")) {
- if (ftl != null) {
- throw new IllegalArgumentException("The ftl file should only be specified once");
- }
- ftl = arg;
- } else if (dashArg.equals("-out")) {
- if (output != null) {
- throw new IllegalArgumentException("The output file should only be specified once");
- }
- output = arg;
- } else if (dashArg.equals("-locale")) {
- if (loc != null) {
- throw new IllegalArgumentException("The locale should only be specified once");
- }
- loc = arg;
- } else if (dashArg.equals("-encoding")) {
- if (enc != null) {
- throw new IllegalArgumentException("The encoding should only be specified once");
- }
- enc = arg;
- } else {
- throw new IllegalArgumentException("Unknown input argument: " + dashArg);
- }
- }
- if (input == null) {
- throw new IllegalArgumentException("No input file specified.");
- }
- if (ftl == null) {
- throw new IllegalArgumentException("No ftl file specified.");
- }
- File inputFile = new File(input).getAbsoluteFile();
- File ftlFile = new File(ftl).getAbsoluteFile();
- if (!inputFile.exists()) {
- throw new IllegalArgumentException("Input file does not exist: " + input);
- }
- if (!ftlFile.exists()) {
- throw new IllegalArgumentException("FTL file does not exist: " + ftl);
- }
- if (!inputFile.isFile() || !inputFile.canRead()) {
- throw new IllegalArgumentException("Input file must be a readable file: " + input);
- }
- if (!ftlFile.isFile() || !ftlFile.canRead()) {
- throw new IllegalArgumentException("FTL file must be a readable file: " + ftl);
- }
- File outputFile = null;
- if (output != null) {
- outputFile = new File(output).getAbsoluteFile();
- File outputDirectory = outputFile.getParentFile();
- if (!outputDirectory.exists() || !outputDirectory.canWrite()) {
- throw new IllegalArgumentException("The output directory must exist and be writable: "
- + outputDirectory);
- }
- }
- Locale locale = Locale.getDefault();
- if (loc != null) {
- locale = localeFromString(loc);
- }
- return new Transform(inputFile, ftlFile, outputFile, locale, enc);
- }
-
- static Locale localeFromString(String ls) {
- if (ls == null) ls = "";
- String lang = "", country = "", variant = "";
- StringTokenizer st = new StringTokenizer(ls, "_-,");
- if (st.hasMoreTokens()) {
- lang = st.nextToken();
- if (st.hasMoreTokens()) {
- country = st.nextToken();
- if (st.hasMoreTokens()) {
- variant = st.nextToken();
- }
- }
- return new Locale(lang, country, variant);
- } else {
- return Locale.getDefault();
- }
- }
-
- static void usage() {
- System.err
- .println("Usage: java freemarker.ext.dom.Transform -in <xmlfile> -ftl <ftlfile> [-out <outfile>] [-locale <locale>] [-encoding <encoding>]");
- // Security: prevents shutting down the container from a template:
- if (Environment.getCurrentEnvironment() == null) {
- System.exit(-1);
- }
- }
-}
diff --git a/src/main/java/freemarker/ext/jdom/NodeListModel.java b/src/main/java/freemarker/ext/jdom/NodeListModel.java
index 077a7e0..aa187e0 100644
--- a/src/main/java/freemarker/ext/jdom/NodeListModel.java
+++ b/src/main/java/freemarker/ext/jdom/NodeListModel.java
@@ -19,7 +19,6 @@
package freemarker.ext.jdom;
-import java.io.FileReader;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
@@ -49,9 +48,7 @@
import org.jdom.Text;
import org.jdom.output.XMLOutputter;
-import freemarker.template.SimpleHash;
import freemarker.template.SimpleScalar;
-import freemarker.template.Template;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateMethodModel;
@@ -1125,29 +1122,6 @@
}
}
- /**
- * Loads a template from a file passed as the first argument, loads an XML
- * document from the standard input, passes it to the template as variable
- * <tt>document</tt> and writes the result of template processing to
- * standard output.
- *
- * @deprecated Will be removed (main method in a library, often classified as CWE-489 "Leftover Debug Code").
- */
- @Deprecated
- public static void main(String[] args)
- throws Exception {
- org.jdom.input.SAXBuilder builder = new org.jdom.input.SAXBuilder();
- Document document = builder.build(System.in);
- SimpleHash model = new SimpleHash();
- model.put("document", new NodeListModel(document));
- FileReader fr = new FileReader(args[0]);
- Template template = new Template(args[0], fr);
- Writer w = new java.io.OutputStreamWriter(System.out);
- template.process(model, w);
- w.flush();
- w.close();
- }
-
private static final class AttributeXMLOutputter extends XMLOutputter {
public void output(Attribute attribute, Writer out)
throws IOException {
diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java
index 82c46b3..5cf17df 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -390,6 +390,9 @@
/** FreeMarker version 2.3.23 (an {@link #Configuration(Version) incompatible improvements break-point}) */
public static final Version VERSION_2_3_23 = new Version(2, 3, 23);
+
+ /** FreeMarker version 2.4.0 (an {@link #Configuration(Version) incompatible improvements break-point}) */
+ public static final Version VERSION_2_4_0 = new Version(2, 4, 0);
/** FreeMarker version 2.3.24 (an {@link #Configuration(Version) incompatible improvements break-point}) */
public static final Version VERSION_2_3_24 = new Version(2, 3, 24);
@@ -445,22 +448,22 @@
}
}
- private static final String FM_24_DETECTION_CLASS_NAME = "freemarker.core._2_4_OrLaterMarker";
- private static final boolean FM_24_DETECTED;
+ private static final String FM_23_DETECTION_CLASS_NAME = "freemarker.core.CommandLine";
+ private static final boolean FM_23_DETECTED;
static {
- boolean fm24detected;
+ boolean fm23detected;
try {
- Class.forName(FM_24_DETECTION_CLASS_NAME);
- fm24detected = true;
+ Class.forName(FM_23_DETECTION_CLASS_NAME);
+ fm23detected = true;
} catch (ClassNotFoundException e) {
- fm24detected = false;
+ fm23detected = false;
} catch (LinkageError e) {
- fm24detected = true;
+ fm23detected = true;
} catch (Throwable e) {
// Unexpected. We assume that there's no clash.
- fm24detected = false;
+ fm23detected = false;
}
- FM_24_DETECTED = fm24detected;
+ FM_23_DETECTED = fm23detected;
}
private final static Object defaultConfigLock = new Object();
@@ -811,9 +814,9 @@
}
private static void checkFreeMarkerVersionClash() {
- if (FM_24_DETECTED) {
- throw new RuntimeException("Clashing FreeMarker versions (" + VERSION + " and some post-2.3.x) detected: "
- + "found post-2.3.x class " + FM_24_DETECTION_CLASS_NAME + ". You probably have two different "
+ if (FM_23_DETECTED) {
+ throw new RuntimeException("Clashing FreeMarker versions (" + VERSION + " and some pre-2.4) detected: "
+ + "found pre-2.4 class " + FM_23_DETECTION_CLASS_NAME + ". You probably have two different "
+ "freemarker.jar-s in the classpath.");
}
}
@@ -2502,10 +2505,10 @@
}
/**
- * Sets the character set encoding to use for templates of
- * a given locale. If there is no explicit encoding set for some
- * locale, then the default encoding will be used, what you can
- * set with {@link #setDefaultEncoding}.
+ * Sets the charset (encoding) to use for parsing templates that are requested for a given locale. If there is no
+ * explicit encoding set for some locale, then the default encoding will be used, which you can set with
+ * {@link #setDefaultEncoding}. By default there are no encodings set for any locale, so always
+ * {@link #setDefaultEncoding} will be used.
*
* @see #clearEncodingMap
* @see #loadBuiltInEncodingMap
diff --git a/src/main/java/freemarker/template/Template.java b/src/main/java/freemarker/template/Template.java
index f785768..91ee466 100644
--- a/src/main/java/freemarker/template/Template.java
+++ b/src/main/java/freemarker/template/Template.java
@@ -19,30 +19,21 @@
package freemarker.template;
-import java.io.BufferedReader;
-import java.io.FilterReader;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
-import java.lang.reflect.UndeclaredThrowableException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
-import java.util.Vector;
import freemarker.cache.TemplateCache;
import freemarker.cache.TemplateLoader;
import freemarker.cache.TemplateLookupStrategy;
-import freemarker.core.BugException;
import freemarker.core.Configurable;
import freemarker.core.Environment;
-import freemarker.core.FMParser;
import freemarker.core.LibraryLoad;
import freemarker.core.Macro;
import freemarker.core.OutputFormat;
@@ -50,68 +41,56 @@
import freemarker.core.ParserConfiguration;
import freemarker.core.TemplateConfiguration;
import freemarker.core.TemplateElement;
-import freemarker.core.TextBlock;
-import freemarker.core.TokenMgrError;
+import freemarker.core.UnboundTemplate;
import freemarker.core._CoreAPI;
import freemarker.debug.impl.DebuggerService;
/**
- * <p>
- * Stores an already parsed template, ready to be processed (rendered) for unlimited times, possibly from multiple
- * threads.
+ * <p>Stores an already parsed template, ready to be processed (rendered) for unlimited times, possibly from
+ * multiple threads.
*
- * <p>
- * Typically, you will use {@link Configuration#getTemplate(String)} to create/get {@link Template} objects, so you
- * don't construct them directly. But you can also construct a template from a {@link Reader} or a {@link String} that
- * contains the template source code. But then it's important to know that while the resulting {@link Template} is
- * efficient for later processing, creating a new {@link Template} itself is relatively expensive. So try to re-use
- * {@link Template} objects if possible. {@link Configuration#getTemplate(String)} (and its overloads) does that
- * (caching {@link Template}-s) for you, but the constructor of course doesn't, so it's up to you to solve then.
+ * <p>Typically, you will use {@link Configuration#getTemplate(String)} to create/get {@link Template} objects, so
+ * you don't construct them directly. But you can also construct a template from a {@link Reader} or a {@link String}
+ * that contains the template source code. But then it's
+ * important to know that while the resulting {@link Template} is efficient for later processing, creating a new
+ * {@link Template} itself is relatively expensive. So try to re-use {@link Template} objects if possible.
+ * {@link Configuration#getTemplate(String)} does that (caching {@link Template}-s) for you, but the constructor of
+ * course doesn't, so it's up to you to solve then.
*
- * <p>
- * Objects of this class meant to be handled as immutable and thus thread-safe. However, it has some setter methods for
- * changing FreeMarker settings. Those must not be used while the template is being processed, or if the template object
- * is already accessible from multiple threads. If some templates need different settings that those coming from the
- * shared {@link Configuration}, and you are using {@link Configuration#getTemplate(String)} (or its overloads), then
- * see {@link Configuration#setTemplateConfigurations(freemarker.cache.TemplateConfigurationFactory)}.
+ * <p>Objects of this class meant to be handled as immutable and thus thread-safe. However, it has some setter methods
+ * for changing FreeMarker settings. Those must not be used while the template is being processed, or if the
+ * template object is already accessible from multiple threads.
*/
public class Template extends Configurable {
- public static final String DEFAULT_NAMESPACE_PREFIX = "D";
- public static final String NO_NS_PREFIX = "N";
+ public static final String DEFAULT_NAMESPACE_PREFIX = UnboundTemplate.DEFAULT_NAMESPACE_PREFIX;
+ public static final String NO_NS_PREFIX = UnboundTemplate.NO_NS_PREFIX;
- /** This is only non-null during parsing. It's used internally to make some information available through the
- * Template API-s earlier than the parsing was finished. */
- private transient FMParser parser;
-
- private Map macros = new HashMap();
- private List imports = new Vector();
- private TemplateElement rootElement;
- private String encoding, defaultNS;
- private Object customLookupCondition;
- private int actualTagSyntax;
- private int actualNamingConvention;
- private boolean autoEscaping;
- private OutputFormat outputFormat;
+ private final UnboundTemplate unboundTemplate;
private final String name;
- private final String sourceName;
- private final ArrayList lines = new ArrayList();
- private final ParserConfiguration parserConfiguration;
- private Map prefixToNamespaceURILookup = new HashMap();
- private Map namespaceURIToPrefixLookup = new HashMap();
- private Version templateLanguageVersion;
+ private String encoding;
+ private Object customLookupCondition;
/**
* A prime constructor to which all other constructors should
* delegate directly or indirectly.
*/
- private Template(String name, String sourceName, Configuration cfg, ParserConfiguration customParserConfiguration) {
+ private Template(UnboundTemplate unboundTemplate, String name, Configuration cfg) {
super(toNonNull(cfg));
+ this.unboundTemplate = unboundTemplate;
this.name = name;
- this.sourceName = sourceName;
- this.templateLanguageVersion = normalizeTemplateLanguageVersion(toNonNull(cfg).getIncompatibleImprovements());
- this.parserConfiguration = customParserConfiguration != null ? customParserConfiguration : getConfiguration();
}
+ /**
+ * To be used internally only!
+ */
+ Template(UnboundTemplate unboundTemplate,
+ String name, Locale locale, Object customLookupCondition,
+ Configuration cfg) {
+ this(unboundTemplate, name, cfg);
+ this.setLocale(locale);
+ this.setCustomLookupCondition(customLookupCondition);
+ }
+
private static Configuration toNonNull(Configuration cfg) {
return cfg != null ? cfg : Configuration.getDefaultConfiguration();
}
@@ -199,88 +178,51 @@
*
* @since 2.3.22
*/
- @Deprecated
- public Template(
- String name, String sourceName, Reader reader, Configuration cfg, String encoding) throws IOException {
- this(name, sourceName, reader, cfg, null, encoding);
- }
-
+ @Deprecated
+ public Template(
+ String name, String sourceName, Reader reader, Configuration cfg, String encoding) throws IOException {
+ this(name, sourceName, reader, cfg, null, encoding);
+ }
+
/**
* Same as {@link #Template(String, String, Reader, Configuration, String)}, but also specifies a
* {@link TemplateConfiguration}. This is mostly meant to be used by FreeMarker internally, but advanced users might
* still find this useful.
*
- * @param customParserConfiguration
+ * @param customParserCfg
* Overrides the parsing related configuration settings of the {@link Configuration} parameter; can be
* {@code null}. This is useful as the {@link Configuration} is normally a singleton shared by all
- * templates, and so it's not good for specifying template-specific settings. (While {@link Template}
- * itself has methods to specify settings just for that template, those don't influence the parsing, and
- * you only have opportunity to call them after the parsing anyway.) This objects is often a
- * {@link TemplateConfiguration} whose parent is the {@link Configuration} parameter, and then it
+ * templates, and so it's not good for specifying template-specific settings. (While
+ * {@link Template} itself has methods to specify settings just for that template, those don't influence
+ * the parsing, and you only have opportunity to call them after the parsing anyway.) This objects is
+ * often a {@link TemplateConfiguration} whose parent is the {@link Configuration} parameter, and then it
* practically just overrides some of the parser settings, as the others are inherited from the
- * {@link Configuration}. Note that if this is a {@link TemplateConfiguration}, you will also want to
- * call {@link TemplateConfiguration#apply(Template)} on the resulting {@link Template} so that
+ * {@link Configuration}. Note that if this is a {@link TemplateConfiguration}, you will also want to call
+ * {@link TemplateConfiguration#apply(Template)} on the resulting {@link Template} so that
* {@link Configurable} settings will be set too, because this constructor only uses it as a
- * {@link ParserConfiguration}.
+ * {@link ParserConfiguration}.
* @param encoding
* Same as in {@link #Template(String, String, Reader, Configuration, String)}. When it's non-{@code
- * null}, it overrides the value coming from the {@link TemplateConfiguration#getEncoding()} method of
- * the {@code templateConfiguration} parameter.
+ * null}, it overrides the value coming from the {@code TemplateConfiguration#getEncoding()} method of the
+ * {@code templateConfigurer} parameter.
*
* @since 2.3.24
*/
public Template(
- String name, String sourceName, Reader reader,
- Configuration cfg, ParserConfiguration customParserConfiguration,
+ String name, String sourceName, Reader reader, Configuration cfg, ParserConfiguration customParserCfg,
String encoding) throws IOException {
- this(name, sourceName, cfg, customParserConfiguration);
-
- this.setEncoding(encoding);
- LineTableBuilder ltbReader;
- try {
- if (!(reader instanceof BufferedReader) && !(reader instanceof StringReader)) {
- reader = new BufferedReader(reader, 0x1000);
- }
- ltbReader = new LineTableBuilder(reader);
- reader = ltbReader;
-
- try {
- parser = new FMParser(this, reader, getParserConfiguration());
- try {
- this.rootElement = parser.Root();
- } catch (IndexOutOfBoundsException exc) {
- // There's a JavaCC bug where the Reader throws a RuntimeExcepton and then JavaCC fails with
- // IndexOutOfBoundsException. If that wasn't the case, we just rethrow. Otherwise we suppress the
- // IndexOutOfBoundsException and let the real cause to be thrown later.
- if (!ltbReader.hasFailure()) {
- throw exc;
- }
- rootElement = null;
- }
- this.actualTagSyntax = parser._getLastTagSyntax();
- this.actualNamingConvention = parser._getLastNamingConvention();
- } catch (TokenMgrError exc) {
- // TokenMgrError VS ParseException is not an interesting difference for the user, so we just convert it
- // to ParseException
- throw exc.toParseException(this);
- } finally {
- parser = null;
- }
- } catch (ParseException e) {
- e.setTemplateName(getSourceName());
- throw e;
- } finally {
- reader.close();
- }
-
- // Throws any exception that JavaCC has silently treated as EOF:
- ltbReader.throwFailure();
-
+ this(
+ _CoreAPI.newUnboundTemplate(
+ reader,
+ sourceName != null ? sourceName : name,
+ toNonNull(cfg),
+ customParserCfg,
+ encoding),
+ name, cfg);
+ this.encoding = encoding;
DebuggerService.registerTemplate(this);
- namespaceURIToPrefixLookup = Collections.unmodifiableMap(namespaceURIToPrefixLookup);
- prefixToNamespaceURILookup = Collections.unmodifiableMap(prefixToNamespaceURILookup);
}
-
+
/**
* Equivalent to {@link #Template(String, Reader, Configuration)
* Template(name, reader, null)}.
@@ -295,19 +237,6 @@
}
/**
- * Only meant to be used internally.
- *
- * @deprecated Has problems setting actualTagSyntax and templateLanguageVersion; will be removed in 2.4.
- */
- @Deprecated
- // [2.4] remove this
- Template(String name, TemplateElement root, Configuration cfg) {
- this(name, null, cfg, (ParserConfiguration) null);
- this.rootElement = root;
- DebuggerService.registerTemplate(this);
- }
-
- /**
* Same as {@link #getPlainTextTemplate(String, String, String, Configuration)} with {@code null} {@code sourceName}
* argument.
*/
@@ -330,27 +259,11 @@
* @since 2.3.22
*/
static public Template getPlainTextTemplate(String name, String sourceName, String content, Configuration config) {
- Template template;
- try {
- template = new Template(name, sourceName, new StringReader("X"), config);
- } catch (IOException e) {
- throw new BugException("Plain text template creation failed", e);
- }
- _CoreAPI.replaceText((TextBlock) template.rootElement, content);
- DebuggerService.registerTemplate(template);
- return template;
- }
-
- private static Version normalizeTemplateLanguageVersion(Version incompatibleImprovements) {
- _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
- int v = incompatibleImprovements.intValue();
- if (v < _TemplateAPI.VERSION_INT_2_3_19) {
- return Configuration.VERSION_2_3_0;
- } else if (v > _TemplateAPI.VERSION_INT_2_3_21) {
- return Configuration.VERSION_2_3_21;
- } else { // if 2.3.19 or 2.3.20 or 2.3.21
- return incompatibleImprovements;
- }
+ Template t = new Template(
+ _CoreAPI.newPlainTextUnboundTemplate(content, sourceName != null ? sourceName : name, config),
+ name, config);
+ DebuggerService.registerTemplate(t);
+ return t;
}
/**
@@ -514,6 +427,14 @@
return sw.toString();
}
+ /**
+ * Returns the {@link UnboundTemplate} that this {@link Template} is based on.
+ *
+ * @since 2.4.0
+ */
+ public UnboundTemplate getUnboundTemplate() {
+ return unboundTemplate;
+ }
/**
* The usually path-like (or URL-like) identifier of the template, or possibly {@code null} for non-stored
@@ -560,7 +481,7 @@
* @since 2.3.22
*/
public String getSourceName() {
- return sourceName != null ? sourceName : getName();
+ return unboundTemplate.getSourceName();
}
/**
@@ -578,16 +499,18 @@
* @since 2.3.24
*/
public ParserConfiguration getParserConfiguration() {
- return parserConfiguration;
+ return unboundTemplate.getParserConfiguration();
}
/**
* Return the template language (FTL) version used by this template.
- * For now (2.3.21) this is the same as {@link Configuration#getIncompatibleImprovements()}, except
+ * For now (2.4.0) this is the same as {@link Configuration#getIncompatibleImprovements()}, except
* that it's normalized to the lowest version where the template language was changed.
+ *
+ * @since 2.4.0
*/
- Version getTemplateLanguageVersion() {
- return templateLanguageVersion;
+ public Version getTemplateLanguageVersion() {
+ return unboundTemplate.getTemplateLanguageVersion();
}
/**
@@ -604,7 +527,16 @@
}
/**
- * Returns the default character encoding used for reading included files.
+ * Returns the name of the charset used for reading included/imported template files by default.
+ *
+ * <p>
+ * At least when FreeMarker is built-in template loading mechanism is used, by default this setting is set to the
+ * same value as the {@link UnboundTemplate#getTemplateSpecifiedEncoding()} of the wrapped {@link UnboundTemplate},
+ * if that's non-{@code null}.
+ *
+ * <p>
+ * While "inheriting" charset from the referring template is not seen as a good idea anymore, it's still used by
+ * FreeMarker for backward compatibility (at least by default; as of 2.3.22 no setting exists yet to change that).
*/
public String getEncoding() {
return this.encoding;
@@ -643,7 +575,7 @@
* @since 2.3.20
*/
public int getActualTagSyntax() {
- return actualTagSyntax;
+ return unboundTemplate.getActualTagSyntax();
}
/**
@@ -656,7 +588,7 @@
* @since 2.3.23
*/
public int getActualNamingConvention() {
- return actualNamingConvention;
+ return unboundTemplate.getActualNamingConvention();
}
/**
@@ -669,14 +601,7 @@
* @since 2.3.24
*/
public OutputFormat getOutputFormat() {
- return outputFormat;
- }
-
- /**
- * Meant to be called by the parser only.
- */
- void setOutputFormat(OutputFormat outputFormat) {
- this.outputFormat = outputFormat;
+ return unboundTemplate.getOutputFormat();
}
/**
@@ -689,48 +614,37 @@
* @since 2.3.24
*/
public boolean getAutoEscaping() {
- return autoEscaping;
- }
-
- /**
- * Meant to be called by the parser only.
- */
- void setAutoEscaping(boolean autoEscaping) {
- this.autoEscaping = autoEscaping;
+ return unboundTemplate.getAutoEscaping();
}
/**
* Dump the raw template in canonical form.
*/
public void dump(PrintStream ps) {
- ps.print(rootElement.getCanonicalForm());
+ unboundTemplate.dump(ps);
}
/**
* Dump the raw template in canonical form.
*/
public void dump(Writer out) throws IOException {
- out.write(rootElement.getCanonicalForm());
+ unboundTemplate.dump(out);
}
/**
- * Called by code internally to maintain a table of macros
- *
* @deprecated Should only be used internally, and might will be removed later.
*/
@Deprecated
public void addMacro(Macro macro) {
- macros.put(macro.getName(), macro);
+ _CoreAPI.addMacro(unboundTemplate, macro);
}
/**
- * Called by code internally to maintain a list of imports
- *
* @deprecated Should only be used internally, and might will be removed later.
*/
@Deprecated
- public void addImport(LibraryLoad ll) {
- imports.add(ll);
+ public void addImport(LibraryLoad libLoad) {
+ _CoreAPI.addImport(unboundTemplate, libLoad);
}
/**
@@ -741,136 +655,8 @@
* @param endLine the last line of the requested source, 1-based
* @see freemarker.core.TemplateObject#getSource()
*/
- public String getSource(int beginColumn,
- int beginLine,
- int endColumn,
- int endLine) {
- if (beginLine < 1 || endLine < 1) return null; // dynamically ?eval-ed expressions has no source available
-
- // Our container is zero-based.
- --beginLine;
- --beginColumn;
- --endColumn;
- --endLine;
- StringBuilder buf = new StringBuilder();
- for (int i = beginLine ; i <= endLine; i++) {
- if (i < lines.size()) {
- buf.append(lines.get(i));
- }
- }
- int lastLineLength = lines.get(endLine).toString().length();
- int trailingCharsToDelete = lastLineLength - endColumn - 1;
- buf.delete(0, beginColumn);
- buf.delete(buf.length() - trailingCharsToDelete, buf.length());
- return buf.toString();
- }
-
- /**
- * Reader that builds up the line table info for us, and also helps in working around JavaCC's exception
- * suppression.
- */
- private class LineTableBuilder extends FilterReader {
-
- private final StringBuilder lineBuf = new StringBuilder();
- int lastChar;
- boolean closed;
-
- /** Needed to work around JavaCC behavior where it silently treats any errors as EOF. */
- private Exception failure;
-
- /**
- * @param r the character stream to wrap
- */
- LineTableBuilder(Reader r) {
- super(r);
- }
-
- public boolean hasFailure() {
- return failure != null;
- }
-
- public void throwFailure() throws IOException {
- if (failure != null) {
- if (failure instanceof IOException) {
- throw (IOException) failure;
- }
- if (failure instanceof RuntimeException) {
- throw (RuntimeException) failure;
- }
- throw new UndeclaredThrowableException(failure);
- }
- }
-
- @Override
- public int read() throws IOException {
- try {
- int c = in.read();
- handleChar(c);
- return c;
- } catch (Exception e) {
- throw rememberException(e);
- }
- }
-
- private IOException rememberException(Exception e) throws IOException {
- // JavaCC used to read from the Reader after it was closed. So we must not treat that as a failure.
- if (!closed) {
- failure = e;
- }
- if (e instanceof IOException) {
- return (IOException) e;
- }
- if (e instanceof RuntimeException) {
- throw (RuntimeException) e;
- }
- throw new UndeclaredThrowableException(e);
- }
-
- @Override
- public int read(char cbuf[], int off, int len) throws IOException {
- try {
- int numchars = in.read(cbuf, off, len);
- for (int i = off; i < off + numchars; i++) {
- char c = cbuf[i];
- handleChar(c);
- }
- return numchars;
- } catch (Exception e) {
- throw rememberException(e);
- }
- }
-
- @Override
- public void close() throws IOException {
- if (lineBuf.length() > 0) {
- lines.add(lineBuf.toString());
- lineBuf.setLength(0);
- }
- super.close();
- closed = true;
- }
-
- private void handleChar(int c) {
- if (c == '\n' || c == '\r') {
- if (lastChar == '\r' && c == '\n') { // CRLF under Windoze
- int lastIndex = lines.size() - 1;
- String lastLine = (String) lines.get(lastIndex);
- lines.set(lastIndex, lastLine + '\n');
- } else {
- lineBuf.append((char) c);
- lines.add(lineBuf.toString());
- lineBuf.setLength(0);
- }
- } else if (c == '\t') {
- int numSpaces = 8 - (lineBuf.length() % 8);
- for (int i = 0; i < numSpaces; i++) {
- lineBuf.append(' ');
- }
- } else {
- lineBuf.append((char) c);
- }
- lastChar = c;
- }
+ public String getSource(int beginColumn, int beginLine, int endColumn, int endLine) {
+ return unboundTemplate.getSource(beginColumn, beginLine, endColumn, endLine);
}
/**
@@ -878,15 +664,35 @@
*/
@Deprecated
public TemplateElement getRootTreeNode() {
- return rootElement;
+ return _CoreAPI.getRootTreeNode(unboundTemplate);
}
/**
+ * For 2.3 backward compatibility. Initialized on demand.
+ */
+ private volatile Map<String, Macro> legacyMacroMap;
+
+ /**
+ * Returns the {@link Map} that maps the macro names to the actual macros. This map shouldn't be modified; if you
+ * absolutely has to use these deprecated API-s for adding a macro, at least use {@link #addMacro(Macro)}.
+ * (Specifying the {@link Map} key has no purpose anyway, as the macro will be always defined with its original
+ * name, as returned by {@link Macro#getName()}.)
+ *
* @deprecated Should only be used internally, and might will be removed later.
*/
@Deprecated
public Map getMacros() {
- return macros;
+ Map<String, Macro> legacyMacroMap = this.legacyMacroMap;
+ if (legacyMacroMap == null) {
+ synchronized (this) {
+ legacyMacroMap = this.legacyMacroMap;
+ if (legacyMacroMap == null) {
+ legacyMacroMap = _CoreAPI.createAdapterMacroMapForUnboundCallables(unboundTemplate);
+ this.legacyMacroMap = legacyMacroMap;
+ }
+ }
+ }
+ return legacyMacroMap;
}
/**
@@ -894,7 +700,7 @@
*/
@Deprecated
public List getImports() {
- return imports;
+ return _CoreAPI.getImports(unboundTemplate);
}
/**
@@ -904,57 +710,25 @@
*/
@Deprecated
public void addPrefixNSMapping(String prefix, String nsURI) {
- if (nsURI.length() == 0) {
- throw new IllegalArgumentException("Cannot map empty string URI");
- }
- if (prefix.length() == 0) {
- throw new IllegalArgumentException("Cannot map empty string prefix");
- }
- if (prefix.equals(NO_NS_PREFIX)) {
- throw new IllegalArgumentException("The prefix: " + prefix + " cannot be registered, it's reserved for special internal use.");
- }
- if (prefixToNamespaceURILookup.containsKey(prefix)) {
- throw new IllegalArgumentException("The prefix: '" + prefix + "' was repeated. This is illegal.");
- }
- if (namespaceURIToPrefixLookup.containsKey(nsURI)) {
- throw new IllegalArgumentException("The namespace URI: " + nsURI + " cannot be mapped to 2 different prefixes.");
- }
- if (prefix.equals(DEFAULT_NAMESPACE_PREFIX)) {
- this.defaultNS = nsURI;
- } else {
- prefixToNamespaceURILookup.put(prefix, nsURI);
- namespaceURIToPrefixLookup.put(nsURI, prefix);
- }
+ _CoreAPI.addPrefixNSMapping(unboundTemplate, prefix, nsURI);
}
public String getDefaultNS() {
- return this.defaultNS;
+ return unboundTemplate.getDefaultNamespaceURI();
}
/**
* @return the NamespaceUri mapped to this prefix in this template. (Or null if there is none.)
*/
public String getNamespaceForPrefix(String prefix) {
- if (prefix.equals("")) {
- return defaultNS == null ? "" : defaultNS;
- }
- return (String) prefixToNamespaceURILookup.get(prefix);
+ return unboundTemplate.getNamespaceURIForPrefix(prefix);
}
/**
* @return the prefix mapped to this nsURI in this template. (Or null if there is none.)
*/
public String getPrefixForNamespace(String nsURI) {
- if (nsURI == null) {
- return null;
- }
- if (nsURI.length() == 0) {
- return defaultNS == null ? "" : NO_NS_PREFIX;
- }
- if (nsURI.equals(defaultNS)) {
- return "";
- }
- return (String) namespaceURIToPrefixLookup.get(nsURI);
+ return unboundTemplate.getPrefixForNamespaceURI(nsURI);
}
/**
@@ -963,47 +737,27 @@
* passed in as parameters.
*/
public String getPrefixedName(String localName, String nsURI) {
- if (nsURI == null || nsURI.length() == 0) {
- if (defaultNS != null) {
- return NO_NS_PREFIX + ":" + localName;
- } else {
- return localName;
- }
- }
- if (nsURI.equals(defaultNS)) {
- return localName;
- }
- String prefix = getPrefixForNamespace(nsURI);
- if (prefix == null) {
- return null;
- }
- return prefix + ":" + localName;
+ return unboundTemplate.getPrefixedName(localName, nsURI);
}
/**
* @return an array of the {@link TemplateElement}s containing the given column and line numbers.
* @deprecated Should only be used internally, and might will be removed later.
+ *
+ * @deprecated The objects building up templates aren't part of the published API, and are subject to change.
*/
@Deprecated
public List containingElements(int column, int line) {
- final ArrayList elements = new ArrayList();
- TemplateElement element = rootElement;
- mainloop: while (element.contains(column, line)) {
- elements.add(element);
- for (Enumeration enumeration = element.children(); enumeration.hasMoreElements(); ) {
- TemplateElement elem = (TemplateElement) enumeration.nextElement();
- if (elem.contains(column, line)) {
- element = elem;
- continue mainloop;
- }
- }
- break;
- }
- return elements.isEmpty() ? null : elements;
+ return _CoreAPI.containingElements(unboundTemplate, column, line);
+ }
+
+ @Override
+ protected Map<String, ?> getInitialCustomAttributes() {
+ return _CoreAPI.getCustomAttributes(unboundTemplate);
}
/**
- * Thrown by the {@link Template} constructors that specify a non-{@code null} encoding whoch doesn't match the
+ * Thrown by the {@link Template} constructors that specify a non-{@code null} encoding which doesn't match the
* encoding specified in the {@code #ftl} header of the template.
*/
static public class WrongEncodingException extends ParseException {
@@ -1055,4 +809,3 @@
}
}
-
diff --git a/src/main/java/freemarker/template/TemplateException.java b/src/main/java/freemarker/template/TemplateException.java
index 7845bf8..96fb5b7 100644
--- a/src/main/java/freemarker/template/TemplateException.java
+++ b/src/main/java/freemarker/template/TemplateException.java
@@ -33,6 +33,7 @@
import freemarker.core.ParseException;
import freemarker.core.TemplateElement;
import freemarker.core.TemplateObject;
+import freemarker.core.UnboundTemplate;
import freemarker.core._CoreAPI;
import freemarker.core._ErrorDescriptionBuilder;
import freemarker.template.utility.CollectionUtils;
@@ -199,11 +200,11 @@
: (
ftlInstructionStackSnapshot != null && ftlInstructionStackSnapshot.length != 0
? ftlInstructionStackSnapshot[0] : null);
- // Line number blow 0 means no info, negative means position in ?eval-ed value that we won't use here.
+ // Line number below 0 means no info, negative means position in ?eval-ed value that we won't use here.
if (templateObject != null && templateObject.getBeginLine() > 0) {
- final Template template = templateObject.getTemplate();
- templateName = template != null ? template.getName() : null;
- templateSourceName = template != null ? template.getSourceName() : null;
+ final UnboundTemplate unboundTemplate = templateObject.getUnboundTemplate();
+ templateName = getTemplateNameOrNull(unboundTemplate);
+ templateSourceName = unboundTemplate != null ? unboundTemplate.getSourceName() : null;
lineNumber = Integer.valueOf(templateObject.getBeginLine());
columnNumber = Integer.valueOf(templateObject.getBeginColumn());
endLineNumber = Integer.valueOf(templateObject.getEndLine());
@@ -214,6 +215,19 @@
}
}
}
+
+ private String getTemplateNameOrNull(final UnboundTemplate unboundTemplate) {
+ if (unboundTemplate == null) {
+ return null;
+ }
+
+ Template template = env != null ? env.getCurrentTemplate() : null;
+ if (template == null) {
+ return null;
+ }
+
+ return template.getUnboundTemplate() == unboundTemplate ? template.getName() : null;
+ }
/**
* @deprecated Java 1.4 has introduced {@link #getCause()} - use that instead, especially as this can't return
diff --git a/src/main/java/freemarker/template/_TemplateAPI.java b/src/main/java/freemarker/template/_TemplateAPI.java
index 765383e..8ca2642 100644
--- a/src/main/java/freemarker/template/_TemplateAPI.java
+++ b/src/main/java/freemarker/template/_TemplateAPI.java
@@ -19,6 +19,7 @@
package freemarker.template;
+import java.util.Locale;
import java.util.Set;
import freemarker.cache.CacheStorage;
@@ -26,8 +27,8 @@
import freemarker.cache.TemplateLookupStrategy;
import freemarker.cache.TemplateNameFormat;
import freemarker.core.Expression;
-import freemarker.core.OutputFormat;
import freemarker.core.TemplateObject;
+import freemarker.core.UnboundTemplate;
import freemarker.template.utility.NullArgumentException;
/**
@@ -44,7 +45,7 @@
public static final int VERSION_INT_2_3_22 = Configuration.VERSION_2_3_22.intValue();
public static final int VERSION_INT_2_3_23 = Configuration.VERSION_2_3_23.intValue();
public static final int VERSION_INT_2_3_24 = Configuration.VERSION_2_3_24.intValue();
- public static final int VERSION_INT_2_4_0 = Version.intValueFor(2, 4, 0);
+ public static final int VERSION_INT_2_4_0 = Configuration.VERSION_2_4_0.intValue();
public static void checkVersionNotNullAndSupported(Version incompatibleImprovements) {
NullArgumentException.check("incompatibleImprovements", incompatibleImprovements);
@@ -54,19 +55,21 @@
+ incompatibleImprovements + ", but the installed FreeMarker version is only "
+ Configuration.getVersion() + ". You may need to upgrade FreeMarker in your project.");
}
+ if (iciV > Configuration.VERSION_2_3_24.intValue() && iciV < Configuration.VERSION_2_4_0.intValue()) {
+ throw new IllegalArgumentException("The FreeMarker version requested by \"incompatibleImprovements\" was "
+ + incompatibleImprovements + ", but the installed FreeMarker version ("
+ + Configuration.getVersion() + ") doesn't know a such high 2.3.x version. "
+ + "You may need to upgrade FreeMarker in your project.");
+ }
if (iciV < VERSION_INT_2_3_0) {
throw new IllegalArgumentException("\"incompatibleImprovements\" must be at least 2.3.0.");
}
}
public static int getTemplateLanguageVersionAsInt(TemplateObject to) {
- return getTemplateLanguageVersionAsInt(to.getTemplate());
+ return to.getUnboundTemplate().getTemplateLanguageVersion().intValue();
}
- public static int getTemplateLanguageVersionAsInt(Template t) {
- return t.getTemplateLanguageVersion().intValue();
- }
-
/** For unit testing only */
public static void DefaultObjectWrapperFactory_clearInstanceCache() {
DefaultObjectWrapperBuilder.clearInstanceCache();
@@ -100,16 +103,17 @@
/**
* [FM 2.4] getSettingNames() becomes to public; remove this.
*/
- public static Set/*<String>*/ getConfigurationSettingNames(Configuration cfg, boolean camelCase) {
+ public static Set<String> getConfigurationSettingNames(Configuration cfg, boolean camelCase) {
return cfg.getSettingNames(camelCase);
}
- public static void setAutoEscaping(Template t, boolean autoEscaping) {
- t.setAutoEscaping(autoEscaping);
- }
-
- public static void setOutputFormat(Template t, OutputFormat outputFormat) {
- t.setOutputFormat(outputFormat);
+ /** Eventually, this constructor should become public, and then we don't need this anymore. */
+ public static Template unboundTemplateToTemplate(UnboundTemplate unboundTemplate,
+ String name, Locale locale, Object customLookupCondition,
+ Configuration cfg) {
+ return new Template(unboundTemplate,
+ name, locale, customLookupCondition,
+ cfg);
}
public static void validateAutoEscapingPolicyValue(int autoEscaping) {
diff --git a/src/main/java/freemarker/template/utility/ClassUtil.java b/src/main/java/freemarker/template/utility/ClassUtil.java
index fe8d753..4ddd741 100644
--- a/src/main/java/freemarker/template/utility/ClassUtil.java
+++ b/src/main/java/freemarker/template/utility/ClassUtil.java
@@ -25,6 +25,7 @@
import freemarker.core.Environment;
import freemarker.core.Macro;
import freemarker.core.TemplateMarkupOutputModel;
+import freemarker.core._CoreAPI;
import freemarker.ext.beans.BeanModel;
import freemarker.ext.beans.BooleanModel;
import freemarker.ext.beans.CollectionModel;
@@ -291,7 +292,7 @@
appendTemplateModelTypeName(sb, typeNamesAppended, primaryInterface);
}
- if (tm instanceof Macro) {
+ if (_CoreAPI.isBoundCallable(tm)) {
appendTypeName(sb, typeNamesAppended, ((Macro) tm).isFunction() ? "function" : "macro");
}
diff --git a/src/main/java/freemarker/template/utility/CollectionUtils.java b/src/main/java/freemarker/template/utility/CollectionUtils.java
index 38e1b91..83747a9 100644
--- a/src/main/java/freemarker/template/utility/CollectionUtils.java
+++ b/src/main/java/freemarker/template/utility/CollectionUtils.java
@@ -26,6 +26,11 @@
public static final Object[] EMPTY_OBJECT_ARRAY = new Object[] { };
+ /**
+ * @since 2.3.22
+ */
+ public static final String[] EMPTY_STRING_ARRAY = new String[] { };
+
public static final Class[] EMPTY_CLASS_ARRAY = new Class[] { };
/**
diff --git a/src/main/java/freemarker/template/utility/StringUtil.java b/src/main/java/freemarker/template/utility/StringUtil.java
index 58d5ffb..cb8a438 100644
--- a/src/main/java/freemarker/template/utility/StringUtil.java
+++ b/src/main/java/freemarker/template/utility/StringUtil.java
@@ -556,7 +556,7 @@
do {
buf.append(s.substring(bidx, idx));
if (idx >= lidx) {
- throw new ParseException("The last character of string literal is backslash", 0,0);
+ throw new ParseException("The last character of string literal is backslash", 0, 0);
}
char c = s.charAt(idx + 1);
switch (c) {
diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj
index b800eb4..6757894 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
Binary files differ
diff --git a/src/main/resources/freemarker/version.properties b/src/main/resources/freemarker/version.properties
index e209196..4080c2b 100644
--- a/src/main/resources/freemarker/version.properties
+++ b/src/main/resources/freemarker/version.properties
@@ -57,11 +57,11 @@
# continue working without modification or recompilation.
# - When the major version number is increased, major backward
# compatibility violations are allowed, but still should be avoided.
-version=2.3.24-rc01-incubating
+version=2.4.0-nightly_@timestampInVersion@-incubating
# This exists as oss.sonatype only allows SNAPSHOT and final releases,
# so instead 2.3.21-rc01 and such we have to use 2.3.21-SNAPSHOT there.
# For final releases it's the same as "version".
-mavenVersion=2.3.24-rc01-incubating
+mavenVersion=2.4.0-SNAPSHOT
# Version string that conforms to OSGi
# ------------------------------------
@@ -75,7 +75,7 @@
# 2.4.0.pre01
# 2.4.0.nightly_@timestampInVersion@
# During Apache Incubation, "-incubating" is added to this string.
-versionForOSGi=2.3.24.rc01-incubating
+versionForOSGi=2.4.0.nightly_@timestampInVersion@-incubating
# Version string that conforms to legacy MF
# -----------------------------------------
@@ -93,10 +93,11 @@
# "97 denotes "nightly", 98 denotes "pre", 99 denotes "rc" build.
# In general, for the nightly/preview/rc Y of version 2.X, the versionForMf is
# 2.X-1.(99|98).Y. Note the X-1.
-versionForMf=2.3.23.99.1
+versionForMf=2.3.97
# The date of the build.
# This should be automatically filled by the building tool (Ant).
buildTimestamp=@timestampNice@
+# 2.4.0+ must be always GAE compliant, but we keep this here for B.C.
isGAECompliant=true
diff --git a/src/manual/change-log-2.4.txt b/src/manual/change-log-2.4.txt
new file mode 100644
index 0000000..1f58955
--- /dev/null
+++ b/src/manual/change-log-2.4.txt
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+This is the (draft) of the 2.4.0 change log so far. It's not in the Manual yet, as merging in XDocBook XML
+changes from the 2.3 branch would be rather difficult.
+
+Non-backward compatible (all are unlikely to affect real world applications):
+- Removed command-line tools (main methods in a library, often classified as CWE-489 "Leftover Debug Code"):
+ - freemarker.core.CommandLine (jar main-class). This has only printed the current version and copyright.
+ - freemarker.ext.dom.Transform. This was used to transform an XML file to output file via a template file.
+ Surpassed by FMPP long ago. (If there will be demand for it, it can be still reintroduced in a separate jar.)
+ - freemarker.template.utility.ToCanonical class: Converted a template to its canonical form. As the canonical
+ form was heavily broken before 2.3.21, and is still not perfect, yet nobody has complained, it's assumed
+ that canonicalization isn't used, hence nor the CLI to it.
+ - freemarker.ext.jdom.NodeListModel.main() method: Was used to transform an XML given on stdin with the specified
+ template file and write the output to stdout. The data model used for this transformation (JDOM NodeListModel) was
+ deprecated long ago.
+- Removed TemplateObject.getTemplate(), added getUnboundTemplate() instead. TemplateObject is not to be confused with
+ Template, and is only exploited by very few for some deeper tricks. Although TemplateObject is technically
+ (historically) public, it was marked as internal API and was excluded from the JavaDoc for a while.
+- When TemplateCache2 will be merged in: In 2.3.x, a cached error was thrown as a plain IOException with the original
+ (cached) exception as its cause exception.
+ This is not possible starting from 2.4.x, because to prevent memory leaks, the cache doesn't hold reference to the
+ cached exception any more, instead it only stores a String description of it (class name, message, and the same for
+ each its cause exceptions). The cached error will be throw as a CachedTemplateLoadingException (an IOException
+ subclass), which contains the string description of the original exception in its getMessage() value.
+- Removed deprecated *package visible* method:
+ Template#Template(String name, TemplateElement root, Configuration cfg)
+- freemarker.core.FMParser API changes. These were marked as internal and deprecated for a while,
+ and are very unlikely to be used. They could be exploited for tools that try to tokenize String-s as FTL or such.
+ - freemarker.core.FMParser constructors were removed or has become non-public
+ - freemarker.core.FMParser.createExpressionParser war removed
+
+New features:
+- There are no separate org.freemarker:freemarker-gae and org.freemarker:freemarker artifacts any more.
+ There's only org.freemarker:freemarker, which is GAE compatible.
+- TemplateLoader.getLastModified(Object templateSource) can now throw GetLastModifiedException to indicate an
+ error instead of returning -1 (which doesn't count as an error).
+- Custom attributes now keep the order of adding them
+- Bug fixed: When the same template was both #include-d and #import-ed, the macros from the #import-ed template has
+ ran in the namespace of the #import-ed template.
+
+Notes on internal changes (some of this need not be in Version History):
+- The main change is splitting Template to Template and UnboundTemplate. With an analogy, if UnboundTemplate is
+ the class of the template, then Template is the instance of the template. The content of UnboundTemplate only
+ depends on the content of the actual template file, the ParserSettings, and on its *source* path, while Template
+ contains all the settings defined in by the Configurable class and the name with which the template was requested.
+ This splitting allows more efficient caching when due to the template lookup mechanism, multiple Template-s
+ share the same actual template file.
diff --git a/src/test/java/freemarker/core/CustomAttributeInUnboundTemplatesTest.java b/src/test/java/freemarker/core/CustomAttributeInUnboundTemplatesTest.java
new file mode 100644
index 0000000..4452dcd
--- /dev/null
+++ b/src/test/java/freemarker/core/CustomAttributeInUnboundTemplatesTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.IOException;
+import java.io.StringReader;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+
+@SuppressWarnings("boxing")
+public class CustomAttributeInUnboundTemplatesTest {
+
+ @Test
+ public void inFtlHeaderTest() throws IOException {
+ Template t = new Template(null, "<#ftl attributes={'a': 1?int}>", new Configuration(Configuration.VERSION_2_3_23));
+ t.setCustomAttribute("b", 2);
+ assertEquals(1, t.getCustomAttribute("a"));
+ assertEquals(2, t.getCustomAttribute("b"));
+ assertEquals(ImmutableMap.of("a", 1), t.getUnboundTemplate().getCustomAttributes());
+ }
+
+ @Test
+ public void inTemplateConfigurationTest() throws IOException {
+ Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
+
+ TemplateConfiguration tc = new TemplateConfiguration();
+ tc.setCustomAttribute("a", 1);
+ tc.setParentConfiguration(cfg);
+
+ Template t = new Template(null, null, new StringReader(""), cfg, tc, null);
+ t.setCustomAttribute("b", 2);
+ assertNull(t.getCustomAttribute("a"));
+ tc.apply(t);
+ assertEquals(1, t.getCustomAttribute("a"));
+ assertEquals(2, t.getCustomAttribute("b"));
+ assertNull(t.getUnboundTemplate().getCustomAttributes());
+ }
+
+}
diff --git a/src/test/java/freemarker/core/DirectiveCallPlaceTest.java b/src/test/java/freemarker/core/DirectiveCallPlaceTest.java
index 6fc03f3..db93d8d 100644
--- a/src/test/java/freemarker/core/DirectiveCallPlaceTest.java
+++ b/src/test/java/freemarker/core/DirectiveCallPlaceTest.java
@@ -198,7 +198,7 @@
Writer out = env.getOut();
DirectiveCallPlace callPlace = env.getCurrentDirectiveCallPlace();
out.write("[");
- out.write(getTemplateSourceName(callPlace));
+ out.write(callPlace.getUnboundTemplate().getSourceName());
out.write(":");
out.write(Integer.toString(callPlace.getBeginLine()));
out.write(":");
@@ -212,10 +212,6 @@
body.render(out);
}
}
-
- private String getTemplateSourceName(DirectiveCallPlace callPlace) {
- return ((UnifiedCall) callPlace).getTemplate().getSourceName();
- }
}
diff --git a/src/test/java/freemarker/core/EnvironmentGetTemplateVariantsTest.java b/src/test/java/freemarker/core/EnvironmentGetTemplateVariantsTest.java
index dc6eb34..1f6ec27 100644
--- a/src/test/java/freemarker/core/EnvironmentGetTemplateVariantsTest.java
+++ b/src/test/java/freemarker/core/EnvironmentGetTemplateVariantsTest.java
@@ -123,6 +123,19 @@
TEMPLATES.putTemplate("inc4",
"<@tNames />"
);
+
+ TEMPLATES.putTemplate("FM23MacroNsBug/main",
+ "<#include 'inc'>"
+ + "<#assign ns = 'main'>"
+ + "<@m />\n"
+ + "<#import 'inc' as i>"
+ + "<@i.m />\n"
+ + "<@m />\n");
+ TEMPLATES.putTemplate("FM23MacroNsBug/inc",
+ "<#assign ns = 'inc'>"
+ + "<#macro m>"
+ + "ns: ${ns}"
+ + "</#macro>");
}
private final static String EXPECTED_2_3_21 =
@@ -132,31 +145,31 @@
+ "---2---\n"
+ "[impM: <t=main ct=imp mt=main>\n"
+ "{<t=main ct=main mt=main>}\n"
- + "[inc: <t=inc ct=inc mt=main>\n"
- + "[incM: <t=inc ct=inc mt=main> {<t=imp ct=inc mt=main>}]\n"
- + "[incInc: <t=inc ct=inc mt=main>\n"
- + "[incM: <t=inc ct=inc mt=main> {<t=imp ct=inc mt=main>}]\n"
+ + "[inc: <t=inc ct=inc cnst=imp mt=main>\n"
+ + "[incM: <t=inc ct=inc cnst=imp mt=main> {<t=imp ct=inc cnst=imp mt=main>}]\n"
+ + "[incInc: <t=inc ct=inc cnst=imp mt=main>\n"
+ + "[incM: <t=inc ct=inc cnst=imp mt=main> {<t=imp ct=inc cnst=imp mt=main>}]\n"
+ "]\n"
+ "]\n"
- + "[incM: <t=main ct=inc mt=main> {<t=imp ct=imp mt=main>}]\n"
+ + "[incM: <t=main ct=inc cnst=imp mt=main> {<t=imp ct=imp mt=main>}]\n"
+ "]\n"
+ "---3---\n"
- + "[inc: <t=inc ct=inc mt=main>\n"
- + "[incM: <t=inc ct=inc mt=main> {<t=main ct=inc mt=main>}]\n"
- + "[incInc: <t=inc ct=inc mt=main>\n"
- + "[incM: <t=inc ct=inc mt=main> {<t=main ct=inc mt=main>}]\n"
+ + "[inc: <t=inc ct=inc cnst=main mt=main>\n"
+ + "[incM: <t=inc ct=inc cnst=main mt=main> {<t=main ct=inc cnst=main mt=main>}]\n"
+ + "[incInc: <t=inc ct=inc cnst=main mt=main>\n"
+ + "[incM: <t=inc ct=inc cnst=main mt=main> {<t=main ct=inc cnst=main mt=main>}]\n"
+ "]\n"
+ "]\n"
+ "---4---\n"
- + "[incM: <t=main ct=inc mt=main> {<t=main ct=main mt=main>}]\n"
+ + "[incM: <t=main ct=inc cnst=main mt=main> {<t=main ct=main mt=main>}]\n"
+ "---5---\n"
- + "[inc2: <t=inc2 ct=inc2 mt=main>\n"
+ + "[inc2: <t=inc2 ct=inc2 cnst=main mt=main>\n"
+ "[impM: <t=inc2 ct=imp mt=main>\n"
- + "{<t=main ct=inc2 mt=main>}\n"
- + "[inc: <t=inc ct=inc mt=main>\n"
- + "[incM: <t=inc ct=inc mt=main> {<t=imp ct=inc mt=main>}]\n"
+ + "{<t=main ct=inc2 cnst=main mt=main>}\n"
+ + "[inc: <t=inc ct=inc cnst=imp mt=main>\n"
+ + "[incM: <t=inc ct=inc cnst=imp mt=main> {<t=imp ct=inc cnst=imp mt=main>}]\n"
+ "]\n"
- + "[incM: <t=inc2 ct=inc mt=main> {<t=imp ct=imp mt=main>}]\n"
+ + "[incM: <t=inc2 ct=inc cnst=imp mt=main> {<t=imp ct=imp mt=main>}]\n"
+ "]\n"
+ "]\n"
+ "---6---\n"
@@ -165,19 +178,19 @@
+ "[imp2M: <t=main ct=imp2 mt=main> {<t=imp ct=imp mt=main>}]\n"
+ "]\n"
+ "---7---\n"
- + "[inc3: <t=inc3 ct=inc3 mt=main>\n"
- + "[mainM: <t=inc3 ct=main mt=main> {<t=main ct=inc3 mt=main>} <t=inc3 ct=main mt=main>]\n"
+ + "[inc3: <t=inc3 ct=inc3 cnst=main mt=main>\n"
+ + "[mainM: <t=inc3 ct=main mt=main> {<t=main ct=inc3 cnst=main mt=main>} <t=inc3 ct=main mt=main>]\n"
+ "]\n"
+ "[mainM: "
+ "<t=main ct=main mt=main> "
- + "{<t=main ct=main mt=main> <t=inc4 ct=inc4 mt=main> <t=main ct=main mt=main>} "
+ + "{<t=main ct=main mt=main> <t=inc4 ct=inc4 cnst=main mt=main> <t=main ct=main mt=main>} "
+ "<t=main ct=main mt=main>"
+ "]\n"
+ "<t=main ct=main mt=main>\n"
+ "---8---\n"
- + "mainF: <t=main ct=main mt=main>, impF: <t=main ct=imp mt=main>, incF: <t=main ct=inc mt=main>\n"
- ;
-
+ + "mainF: <t=main ct=main mt=main>, impF: <t=main ct=imp mt=main>, "
+ + "incF: <t=main ct=inc cnst=main mt=main>\n";
+
@Test
public void test2321() throws IOException, TemplateException {
setConfiguration(createConfiguration(Configuration.VERSION_2_3_21));
@@ -197,6 +210,15 @@
assertSame(t, env.getMainTemplate());
assertSame(t, env.getCurrentTemplate());
}
+
+ @Test
+ public void testFM23MacroNsBugFixed() throws IOException, TemplateException {
+ setConfiguration(createConfiguration(Configuration.VERSION_2_3_0));
+ assertOutputForNamed("FM23MacroNsBug/main",
+ "ns: main\n"
+ + "ns: inc\n"
+ + "ns: main\n");
+ }
private Configuration createConfiguration(Version iciVersion) {
Configuration cfg = new Configuration(iciVersion);
@@ -212,8 +234,11 @@
public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
throws TemplateException, IOException {
Writer out = env.getOut();
- final String r = "<t=" + env.getTemplate().getName() + " ct=" + env.getCurrentTemplate().getName() + " mt="
- + env.getMainTemplate().getName() + ">";
+ final Template currentTemplate = env.getCurrentTemplate();
+ final Template curNsTemplate = env.getCurrentNamespace().getTemplate();
+ final String r = "<t=" + env.getTemplate().getName() + " ct=" + currentTemplate.getName()
+ + (curNsTemplate == currentTemplate ? "" : " cnst=" + curNsTemplate.getName())
+ + " mt=" + env.getMainTemplate().getName() + ">";
out.write(r);
env.setGlobalVariable("lastTNamesResult", new SimpleScalar(r));
}
diff --git a/src/test/java/freemarker/template/CustomAttributeTest.java b/src/test/java/freemarker/template/CustomAttributeTest.java
index 9ff5152..b4795c0 100644
--- a/src/test/java/freemarker/template/CustomAttributeTest.java
+++ b/src/test/java/freemarker/template/CustomAttributeTest.java
@@ -22,7 +22,6 @@
import static org.junit.Assert.*;
import java.math.BigDecimal;
-import java.util.Arrays;
import org.junit.Test;
@@ -65,17 +64,17 @@
assertSame(VALUE_1, t.getCustomAttribute(KEY_1));
t.setCustomAttribute(KEY_2, VALUE_2);
- assertArrayEquals(new String[] { KEY_1, KEY_2 }, sort(t.getCustomAttributeNames()));
+ assertArrayEquals(new String[] { KEY_1, KEY_2 }, t.getCustomAttributeNames());
assertSame(VALUE_1, t.getCustomAttribute(KEY_1));
assertSame(VALUE_2, t.getCustomAttribute(KEY_2));
t.setCustomAttribute(KEY_1, VALUE_2);
- assertArrayEquals(new String[] { KEY_1, KEY_2 }, sort(t.getCustomAttributeNames()));
+ assertArrayEquals(new String[] { KEY_1, KEY_2 }, t.getCustomAttributeNames());
assertSame(VALUE_2, t.getCustomAttribute(KEY_1));
assertSame(VALUE_2, t.getCustomAttribute(KEY_2));
t.setCustomAttribute(KEY_1, null);
- assertArrayEquals(new String[] { KEY_1, KEY_2 }, sort(t.getCustomAttributeNames()));
+ assertArrayEquals(new String[] { KEY_1, KEY_2 }, t.getCustomAttributeNames());
assertNull(t.getCustomAttribute(KEY_1));
assertSame(VALUE_2, t.getCustomAttribute(KEY_2));
@@ -105,7 +104,7 @@
+ "}>",
new Configuration(Configuration.VERSION_2_3_22));
- assertArrayEquals(new String[] { KEY_1, KEY_2 }, sort(t.getCustomAttributeNames()));
+ assertArrayEquals(new String[] { KEY_1, KEY_2 }, t.getCustomAttributeNames());
assertEquals(VALUE_LIST, t.getCustomAttribute(KEY_1));
assertEquals(VALUE_BIGDECIMAL, t.getCustomAttribute(KEY_2));
@@ -115,7 +114,7 @@
}
@Test
- public void testFtl2Header() throws Exception {
+ public void testFtlHeader2() throws Exception {
Template t = new Template(null, "<#ftl attributes={"
+ "'" + KEY_1 + "': 'a', "
+ "'" + KEY_2 + "': 'b', "
@@ -123,20 +122,20 @@
+ "}>",
new Configuration(Configuration.VERSION_2_3_22));
- assertArrayEquals(new String[] { KEY_1, KEY_2, KEY_3 }, sort(t.getCustomAttributeNames()));
+ assertArrayEquals(new String[] { KEY_1, KEY_2, KEY_3 }, t.getCustomAttributeNames());
assertEquals("a", t.getCustomAttribute(KEY_1));
assertEquals("b", t.getCustomAttribute(KEY_2));
assertEquals("c", t.getCustomAttribute(KEY_3));
t.removeCustomAttribute(KEY_2);
- assertArrayEquals(new String[] { KEY_1, KEY_3 }, sort(t.getCustomAttributeNames()));
+ assertArrayEquals(new String[] { KEY_1, KEY_3 }, t.getCustomAttributeNames());
assertEquals("a", t.getCustomAttribute(KEY_1));
assertNull(t.getCustomAttribute(KEY_2));
assertEquals("c", t.getCustomAttribute(KEY_3));
}
@Test
- public void testFtl3Header() throws Exception {
+ public void testFtlHeader3() throws Exception {
Template t = new Template(null, "<#ftl attributes={"
+ "'" + KEY_1 + "': 'a', "
+ "'" + KEY_2 + "': 'b', "
@@ -144,21 +143,28 @@
+ "}>",
new Configuration(Configuration.VERSION_2_3_22));
- assertArrayEquals(new String[] { KEY_1, KEY_2, KEY_3 }, sort(t.getCustomAttributeNames()));
+ assertArrayEquals(new String[] { KEY_1, KEY_2, KEY_3 }, t.getCustomAttributeNames());
assertEquals("a", t.getCustomAttribute(KEY_1));
assertEquals("b", t.getCustomAttribute(KEY_2));
assertEquals("c", t.getCustomAttribute(KEY_3));
t.setCustomAttribute(KEY_2, null);
- assertArrayEquals(new String[] { KEY_1, KEY_2, KEY_3 }, sort(t.getCustomAttributeNames()));
+ assertArrayEquals(new String[] { KEY_1, KEY_2, KEY_3 }, t.getCustomAttributeNames());
assertEquals("a", t.getCustomAttribute(KEY_1));
assertNull(t.getCustomAttribute(KEY_2));
assertEquals("c", t.getCustomAttribute(KEY_3));
}
- private Object[] sort(String[] customAttributeNames) {
- Arrays.sort(customAttributeNames);
- return customAttributeNames;
+ @Test
+ public void testFtlHeaderAndNullValue() throws Exception {
+ final Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
+
+ final Template t = new Template(null, "[#ftl attributes={ '" + KEY_1 + "': 'u' }]", cfg);
+ assertEquals("u", t.getCustomAttribute(KEY_1));
+ t.setCustomAttribute(KEY_1, null);
+ assertNull(t.getCustomAttribute(KEY_1));
+ t.removeCustomAttribute(KEY_1);
+ assertEquals(null, t.getCustomAttribute(KEY_1));
}
@Test
@@ -205,6 +211,58 @@
env.process();
}
+ @Test
+ public void testScopesFallback() throws Exception {
+ final Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
+
+ final Template t = new Template(null, "[#ftl attributes={ '" + KEY_1 + "': 't' }]", cfg);
+ Environment env = t.createProcessingEnvironment(this, NullWriter.INSTANCE);
+
+ assertEquals("t", env.getCustomAttribute(KEY_1));
+ assertEquals("t", t.getCustomAttribute(KEY_1));
+ assertNull(cfg.getCustomAttribute(KEY_1));
+
+ env.setCustomAttribute(KEY_1, "e");
+ assertEquals("e", env.getCustomAttribute(KEY_1));
+ assertEquals("t", t.getCustomAttribute(KEY_1));
+ assertNull(cfg.getCustomAttribute(KEY_1));
+
+ env.setCustomAttribute(KEY_1, null);
+ assertNull(env.getCustomAttribute(KEY_1));
+ assertEquals("t", t.getCustomAttribute(KEY_1));
+ assertNull(cfg.getCustomAttribute(KEY_1));
+
+ env.removeCustomAttribute(KEY_1);
+ assertEquals("t", env.getCustomAttribute(KEY_1));
+ assertEquals("t", t.getCustomAttribute(KEY_1));
+ assertNull(cfg.getCustomAttribute(KEY_1));
+
+ cfg.setCustomAttribute(KEY_2, "c2");
+ assertEquals("c2", env.getCustomAttribute(KEY_2));
+ assertEquals("c2", t.getCustomAttribute(KEY_2));
+ assertEquals("c2", cfg.getCustomAttribute(KEY_2));
+
+ t.setCustomAttribute(KEY_2, "t2");
+ assertEquals("t2", env.getCustomAttribute(KEY_2));
+ assertEquals("t2", t.getCustomAttribute(KEY_2));
+ assertEquals("c2", cfg.getCustomAttribute(KEY_2));
+
+ t.setCustomAttribute(KEY_2, null);
+ assertNull(env.getCustomAttribute(KEY_2));
+ assertNull(t.getCustomAttribute(KEY_2));
+ assertEquals("c2", cfg.getCustomAttribute(KEY_2));
+
+ t.removeCustomAttribute(KEY_2);
+ assertEquals("c2", env.getCustomAttribute(KEY_2));
+ assertEquals("c2", t.getCustomAttribute(KEY_2));
+ assertEquals("c2", cfg.getCustomAttribute(KEY_2));
+
+ cfg.setCustomAttribute(KEY_2, "c2+");
+ assertEquals("c2+", env.getCustomAttribute(KEY_2));
+ assertEquals("c2+", t.getCustomAttribute(KEY_2));
+ assertEquals("c2+", cfg.getCustomAttribute(KEY_2));
+ }
+
public void testScopesFromTemplateStep1() throws Exception {
assertNull(CUST_ATT_TMP_1.get());
assertEquals(123, CUST_ATT_TMP_2.get());
@@ -215,13 +273,5 @@
assertNull(CUST_ATT_CFG_1.get());
assertEquals(12345, CUST_ATT_CFG_2.get());
}
-
- public void testScopesFromTemplateStep2() throws Exception {
-
- }
-
- public void testScopesFromTemplateStep3() throws Exception {
-
- }
}
diff --git a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
index f94f2d9..863a9c5 100644
--- a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
+++ b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
@@ -81,11 +81,16 @@
}
expected.add(Configuration.VERSION_2_3_21);
expected.add(Configuration.VERSION_2_3_22);
- expected.add(Configuration.VERSION_2_3_22); // no non-BC change in 2.3.23
+ expected.add(Configuration.VERSION_2_3_22);
+ expected.add(Configuration.VERSION_2_3_24);
expected.add(Configuration.VERSION_2_3_24);
List<Version> actual = new ArrayList<Version>();
for (int i = _TemplateAPI.VERSION_INT_2_3_0; i <= Configuration.getVersion().intValue(); i++) {
+ if (i > _TemplateAPI.VERSION_INT_2_3_24 && i < _TemplateAPI.VERSION_INT_2_4_0) {
+ continue;
+ }
+
int major = i / 1000000;
int minor = i % 1000000 / 1000;
int micro = i % 1000;
diff --git a/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java b/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java
index 6395263..6ef099c 100644
--- a/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java
+++ b/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java
@@ -62,7 +62,7 @@
t2.process(null, NullWriter.INSTANCE);
fail();
} catch (InvalidReferenceException e) {
- // Apparenly, it has never worked like this...
+ // Apparently, it has never worked like this...
assertEquals("i1", e.getBlamedExpressionString());
}
}
@@ -92,14 +92,10 @@
StringWriter sw = new StringWriter();
env = t2.createProcessingEnvironment(null, sw);
env.setVariable("i2", i2);
-
- try {
- env.process();
- assertEquals("2", sw.toString());
- } catch (NullPointerException e) {
- // Expected on 2.3.x, because it won't find the namespace for the macro
- // [2.4] Fix this "bug"
- }
+
+ // Works since 2.4.0, was NPE earlier
+ env.process();
+ assertEquals("2", sw.toString());
}
}
diff --git a/src/test/java/freemarker/template/MistakenlyPublicMacroAPIsTest.java b/src/test/java/freemarker/template/MistakenlyPublicMacroAPIsTest.java
index aa357f5..9ce51ca 100644
--- a/src/test/java/freemarker/template/MistakenlyPublicMacroAPIsTest.java
+++ b/src/test/java/freemarker/template/MistakenlyPublicMacroAPIsTest.java
@@ -25,6 +25,7 @@
import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;
+import java.util.Set;
import org.junit.Test;
@@ -56,6 +57,25 @@
assertEquals("123b 1b23b", getTemplateOutput(t));
}
+
+ /**
+ * Same as {@link #testMacroCopyingExploit()}, but to make it worse, it adds the macros directly through the macro
+ * {@link Map}.
+ */
+ @Test
+ public void testMacroCopyingExploitWithMapModification() throws IOException, TemplateException {
+ Template tMacros = new Template(null, "<#macro m1>1</#macro><#macro m2>2</#macro>", cfg);
+ Map<String, Macro> macros = tMacros.getMacros();
+
+ Template t = new Template(null,
+ "<@m1/><@m2/><@m3/>"
+ + "<#macro m1>1b</#macro><#macro m3>3b</#macro> "
+ + "<@m1/><@m2/><@m3/>", cfg);
+ t.getMacros().put("m1", macros.get("m1"));
+ t.getMacros().put("whatever", macros.get("m2")); // Legacy bug: Map key doesn't mater, only original macro name
+
+ assertEquals("123b 1b23b", getTemplateOutput(t));
+ }
@Test
public void testMacroCopyingExploitAndNamespaces() throws IOException, TemplateException {
@@ -79,6 +99,34 @@
assertEquals("1", getTemplateOutput(t));
}
+
+ /**
+ * Same as {@link #testMacroCopyingFromFTLVariable()}, but to make it worse, it adds the macros directly through the
+ * {@link Map}.
+ */
+ @Test
+ public void testMacroCopyingFromFTLVariableWithMapModification() throws IOException, TemplateException {
+ Template tMacros = new Template(null, "<#assign x = 0><#macro m1>${x}</#macro>", cfg);
+ Environment env = tMacros.createProcessingEnvironment(null, NullWriter.INSTANCE);
+ env.process();
+ TemplateModel m1 = env.getVariable("m1");
+ assertThat(m1, instanceOf(Macro.class));
+
+ for (int variation : new int[] { 1, 2 }) {
+ Template t = new Template(null, "<#assign x = 1><@m1/>", cfg);
+ if (variation == 1) {
+ t.getMacros().put("m1", m1);
+ } else {
+ t.getMacros().put("m1", null); // Just so it appears in the Entry Set.
+ for (Map.Entry<String, Macro> ent : (Set<Map.Entry>) t.getMacros().entrySet()) {
+ if (ent.getKey().equals("m1")) {
+ ent.setValue((Macro) m1);
+ }
+ }
+ }
+ assertEquals("1", getTemplateOutput(t));
+ }
+ }
private String getTemplateOutput(Template t) throws TemplateException, IOException {
StringWriter sw = new StringWriter();
diff --git a/src/test/resources/freemarker/core/ast-1.ast b/src/test/resources/freemarker/core/ast-1.ast
index 2956157..a0d4e06 100644
--- a/src/test/resources/freemarker/core/ast-1.ast
+++ b/src/test/resources/freemarker/core/ast-1.ast
@@ -112,7 +112,7 @@
- content: "more" // String
#text // f.c.TextBlock
- content: "\n6 " // String
- #macro // f.c.Macro
+ #macro // f.c.UnboundCallable
- assignment target: "foo" // String
- parameter name: "x" // String
- parameter default: null // Null
@@ -129,7 +129,7 @@
- passed value: y // f.c.Identifier
#text // f.c.TextBlock
- content: "\n7 " // String
- #function // f.c.Macro
+ #function // f.c.UnboundCallable
- assignment target: "foo" // String
- parameter name: "x" // String
- parameter default: null // Null
diff --git a/src/test/resources/freemarker/core/ast-assignments.ast b/src/test/resources/freemarker/core/ast-assignments.ast
index 2191544..3377ce1 100644
--- a/src/test/resources/freemarker/core/ast-assignments.ast
+++ b/src/test/resources/freemarker/core/ast-assignments.ast
@@ -92,7 +92,7 @@
- assignment source: 2 // f.c.NumberLiteral
- variable scope: "3" // Integer
- namespace: null // Null
- #macro // f.c.Macro
+ #macro // f.c.UnboundCallable
- assignment target: "m" // String
- catch-all parameter name: null // Null
- AST-node subtype: "0" // Integer
@@ -169,4 +169,4 @@
- assignment operator: "--" // String
- assignment source: null // Null
- variable scope: "1" // Integer
- - namespace: null // Null
\ No newline at end of file
+ - namespace: null // Null
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl b/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl
index fe45a0c..427b70b 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl
@@ -35,4 +35,5 @@
<#assign foo = "x">
${.vars['foo']} == x
<#assign works = .version>
-${.now?is_datetime?c} == true
\ No newline at end of file
+${.now?is_datetime?c} == true
+<@.pass />