Merge branch 'master' into unboundTemplate
Conflicts:
src/main/java/freemarker/core/Macro.java
src/main/java/freemarker/core/_CoreAPI.java
diff --git a/src/main/java/freemarker/core/AddConcatExpression.java b/src/main/java/freemarker/core/AddConcatExpression.java
index 2c73509..164f097 100644
--- a/src/main/java/freemarker/core/AddConcatExpression.java
+++ b/src/main/java/freemarker/core/AddConcatExpression.java
@@ -60,7 +60,7 @@
ArithmeticEngine ae =
env != null
? env.getArithmeticEngine()
- : getTemplate().getArithmeticEngine();
+ : getUnboundTemplate().getConfiguration().getArithmeticEngine();
return new SimpleNumber(ae.add(first, second));
}
else if(leftModel instanceof TemplateSequenceModel && rightModel instanceof TemplateSequenceModel)
diff --git a/src/main/java/freemarker/core/ArithmeticExpression.java b/src/main/java/freemarker/core/ArithmeticExpression.java
index efd332b..90320c8 100644
--- a/src/main/java/freemarker/core/ArithmeticExpression.java
+++ b/src/main/java/freemarker/core/ArithmeticExpression.java
@@ -51,7 +51,7 @@
ArithmeticEngine ae =
env != null
? env.getArithmeticEngine()
- : getTemplate().getArithmeticEngine();
+ : getUnboundTemplate().getConfiguration().getArithmeticEngine();
switch (operator) {
case TYPE_SUBSTRACTION :
return new SimpleNumber(ae.subtract(lhoNumber, rhoNumber));
diff --git a/src/main/java/freemarker/core/AssignmentInstruction.java b/src/main/java/freemarker/core/AssignmentInstruction.java
index de0c7b9..d428f86 100644
--- a/src/main/java/freemarker/core/AssignmentInstruction.java
+++ b/src/main/java/freemarker/core/AssignmentInstruction.java
@@ -106,7 +106,7 @@
super.postParseCleanup(stripWhitespace);
if (nestedElements.size() == 1) {
Assignment ass = (Assignment) nestedElements.get(0);
- ass.setLocation(getTemplate(), this, this);
+ ass.setLocation(getUnboundTemplate(), this, this);
return ass;
}
return this;
diff --git a/src/main/java/freemarker/core/BodyInstruction.java b/src/main/java/freemarker/core/BodyInstruction.java
index 0fd2d5a..4612603 100644
--- a/src/main/java/freemarker/core/BodyInstruction.java
+++ b/src/main/java/freemarker/core/BodyInstruction.java
@@ -105,7 +105,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..efda4ef
--- /dev/null
+++ b/src/main/java/freemarker/core/BoundCallable.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky
+ *
+ * Licensed 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 {@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
+ void accept(Environment env) throws TemplateException, IOException {
+ 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);
+ }
+
+}
diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
index 99fc0b2..2f95122 100644
--- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
+++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
@@ -308,8 +308,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;
}
}
@@ -355,7 +356,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;
}
}
@@ -416,13 +417,13 @@
static class namespaceBI extends BuiltIn {
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 bf767d0..90efbee 100644
--- a/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
+++ b/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
@@ -54,13 +54,13 @@
token_source.incompatibleImprovements = _TemplateAPI.getTemplateLanguageVersionAsInt(this);
token_source.SwitchTo(FMParserConstants.FM_EXPRESSION);
FMParser parser = new FMParser(token_source);
- parser.setTemplate(getTemplate());
+ parser.setTemplate(getUnboundTemplate());
Expression exp = null;
try {
try {
exp = parser.Expression();
} catch (TokenMgrError e) {
- throw e.toParseException(getTemplate());
+ throw e.toParseException(getUnboundTemplate());
}
} catch (ParseException e) {
throw new _MiscTemplateException(this, env, new Object[] {
diff --git a/src/main/java/freemarker/core/BuiltinVariable.java b/src/main/java/freemarker/core/BuiltinVariable.java
index c1de72e..af89a89 100644
--- a/src/main/java/freemarker/core/BuiltinVariable.java
+++ b/src/main/java/freemarker/core/BuiltinVariable.java
@@ -53,6 +53,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[] {
CURRENT_NODE,
DATA_MODEL,
@@ -112,7 +114,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) {
@@ -137,7 +139,7 @@
return new SimpleScalar(env.getTemplate().getName());
}
if (name == PASS) {
- return Macro.DO_NOTHING_MACRO;
+ return PASS_VALUE;
}
if (name == VERSION) {
return new SimpleScalar(Configuration.getVersionNumber());
diff --git a/src/main/java/freemarker/core/CallableInvocationContext.java b/src/main/java/freemarker/core/CallableInvocationContext.java
new file mode 100644
index 0000000..bc83f36
--- /dev/null
+++ b/src/main/java/freemarker/core/CallableInvocationContext.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky
+ *
+ * Licensed 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 java.util.ArrayList;
+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 nestedContent;
+ final Environment.Namespace nestedContentNamespace;
+ final Template nestedContentTemplate;
+ final List nestedContentParameterNames;
+ final ArrayList prevLocalContextStack;
+ final CallableInvocationContext prevMacroContext;
+
+ CallableInvocationContext(UnboundCallable callableDefinition,
+ Environment env,
+ TemplateElement nestedContent,
+ List nestedContentParameterNames)
+ {
+ this.callableDefinition = callableDefinition;
+ this.localVars = env.new Namespace();
+ this.nestedContent = nestedContent;
+ this.nestedContentNamespace = env.getCurrentNamespace();
+ this.nestedContentTemplate = env.getCurrentTemplate();
+ this.nestedContentParameterNames = nestedContentParameterNames;
+ this.prevLocalContextStack = env.getLocalContextStack();
+ this.prevMacroContext = env.getCurrentMacroContext();
+ }
+
+ Macro getCallableDefinition() {
+ return callableDefinition;
+ }
+
+ void invoce(Environment env) throws TemplateException, IOException {
+ sanityCheck(env);
+ // Set default values for unspecified parameters
+ if (callableDefinition.nestedBlock != null) {
+ env.visit(callableDefinition.nestedBlock);
+ }
+ }
+
+ // 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 #", new Integer(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/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index 8538c95..0ae4dd5 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -27,7 +27,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
-import java.util.LinkedList;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -48,6 +48,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;
@@ -126,8 +127,12 @@
};
private Configurable parent;
+
private Properties properties;
- private HashMap 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;
@@ -228,8 +233,6 @@
// which means "not specified"
setBooleanFormat(C_TRUE_FALSE);
-
- customAttributes = new HashMap();
}
/**
@@ -243,13 +246,13 @@
classicCompatible = null;
templateExceptionHandler = null;
properties = new Properties(parent.properties);
- customAttributes = new HashMap();
}
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;
}
@@ -1479,6 +1482,10 @@
/** Returns the possible setting names. */
// [Java 5] Add type param. [FM 2.4] It must return the camelCase names, then make it public.
Set/*<String>*/ getSettingNames() {
+ return getConfigurableSettingNames();
+ }
+
+ static Set<String> getConfigurableSettingNames() {
return new _SortedArraySet(SETTING_NAMES);
}
@@ -1643,29 +1650,49 @@
}
/**
- * Internal entry point for setting unnamed custom attributes
+ * Used internally for setting custom attributes, both named and unnamed ones.
*/
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.
*/
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;
}
}
/**
+ * 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
@@ -1675,28 +1702,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;
}
}
@@ -1711,8 +1754,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);
+ }
}
}
@@ -1730,18 +1784,65 @@
*/
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) {
+ if (retval == null && parent != null) {
return parent.getCustomAttribute(name);
}
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
{
diff --git a/src/main/java/freemarker/core/DebugBreak.java b/src/main/java/freemarker/core/DebugBreak.java
index 42cf053..d3a82e6 100644
--- a/src/main/java/freemarker/core/DebugBreak.java
+++ b/src/main/java/freemarker/core/DebugBreak.java
@@ -38,7 +38,7 @@
protected void accept(Environment env) throws TemplateException, IOException
{
- if(!DebuggerService.suspendEnvironment(env, this.getTemplate().getSourceName(), nestedBlock.getBeginLine()))
+ if(!DebuggerService.suspendEnvironment(env, this.getUnboundTemplate().getSourceName(), nestedBlock.getBeginLine()))
{
nestedBlock.accept(env);
}
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index 59568e9..9fb2d43 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -67,6 +67,7 @@
import freemarker.template.utility.DateUtil;
import freemarker.template.utility.DateUtil.DateToISO8601CalendarFactory;
import freemarker.template.utility.NullWriter;
+import freemarker.template.utility.StringUtil;
import freemarker.template.utility.UndeclaredThrowableException;
/**
@@ -153,18 +154,20 @@
private Collator cachedCollator;
+ private Template currentTemplate;
+ private Namespace currentNamespace;
+ private CallableInvocationContext currentMacroContext;
+
private Writer out;
- private Macro.Context currentMacroContext;
private ArrayList localContextStack;
private final Namespace mainNamespace;
- private Namespace currentNamespace, globalNamespace;
+ private Namespace globalNamespace;
private HashMap loadedLibs;
private boolean inAttemptBlock;
private Throwable lastThrowable;
private TemplateModel lastReturnValue;
- private HashMap macroToNamespaceLookup = new HashMap();
private TemplateNodeModel currentVisitorNode;
private TemplateSequenceModel nodeNamespaces;
@@ -194,9 +197,10 @@
super(template);
this.globalNamespace = new Namespace(null);
this.currentNamespace = mainNamespace = new Namespace(template);
+ this.currentTemplate = getMainTemplate();
this.out = out;
this.rootDataModel = rootDataModel;
- importMacros(template);
+ predefineMacros(template);
}
/**
@@ -205,9 +209,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.
*/
public Template getTemplate() {
return (Template)getParent();
@@ -226,17 +230,18 @@
}
/**
- * Used only internally as of yet, no backward compatibility - Returns the {@link Template} that we are "lexically"
- * inside at moment. This template will change when entering an {@code #include} or calling a macro or function in
- * another template, or returning to yet another template with {@code #nested}. As such, it's useful in
- * {@link TemplateDirectiveModel} to find out if from where the directive was called.
+ * Returns the {@link Template} that we are "lexically" inside at the moment. This template will change when
+ * entering an {@code #include} or calling a macro or function in another template, or returning into another
+ * template with {@code #nested}. As such, it's useful in {@link TemplateDirectiveModel} to find out if from where
+ * the directive was called from.
*
* @see #getMainTemplate()
* @see #getCurrentNamespace()
+ *
+ * @since 2.4.0
*/
- Template getCurrentTemplate() {
- int ln = instructionStack.size();
- return ln == 0 ? getMainTemplate() : ((TemplateObject) instructionStack.get(ln - 1)).getTemplate();
+ public Template getCurrentTemplate() {
+ return currentTemplate;
}
/**
@@ -510,16 +515,20 @@
* Used for {@code #nested}.
*/
void invokeNestedContent(BodyInstruction.Context bodyCtx) throws TemplateException, IOException {
- Macro.Context invokingMacroContext = getCurrentMacroContext();
+ CallableInvocationContext invokingMacroContext = getCurrentMacroContext();
ArrayList prevLocalContextStack = localContextStack;
TemplateElement nestedContent = invokingMacroContext.nestedContent;
if (nestedContent != 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
- = isIcI2322OrLater();
+ final boolean parentReplacementOn = isIcI2322OrLater();
if (parentReplacementOn) {
prevParent = getParent();
setParent(currentNamespace.getTemplate());
@@ -539,7 +548,8 @@
popLocalContext();
}
this.currentMacroContext = invokingMacroContext;
- currentNamespace = getMacroNamespace(invokingMacroContext.getMacro());
+ currentNamespace = prevCurrentNamespace;
+ currentTemplate = prevCurrentTemplate;
if (parentReplacementOn) {
setParent(prevParent);
}
@@ -590,8 +600,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);
@@ -653,8 +663,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);
@@ -664,26 +674,30 @@
/**
* 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 nestedBlock) 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, nestedBlock, bodyParameterNames);
- setMacroContextLocalsFromArguments(macroCtx, macro, namedArgs, positionalArgs);
+ final CallableInvocationContext macroCtx = new CallableInvocationContext(unboundCallable, this, nestedBlock, bodyParameterNames);
+ setMacroContextLocalsFromArguments(macroCtx, unboundCallable, namedArgs, positionalArgs);
- final Macro.Context prevMacroCtx = currentMacroContext;
+ final CallableInvocationContext prevMacroCtx = currentMacroContext;
currentMacroContext = macroCtx;
final ArrayList 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();
final Configurable prevParent;
final boolean parentReplacementOn
@@ -697,7 +711,7 @@
}
try {
- macroCtx.runMacro(this);
+ macroCtx.invoce(this);
} catch (ReturnInstruction.Return re) {
// Not an error, just a <#return>
} catch (TemplateException te) {
@@ -705,7 +719,8 @@
} finally {
currentMacroContext = prevMacroCtx;
localContextStack = prevLocalContextStack;
- currentNamespace = prevNamespace;
+ currentNamespace = prevCurrentNamespace;
+ currentTemplate = prevCurrentTemplate;
if (parentReplacementOn) {
setParent(prevParent);
}
@@ -719,10 +734,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) {
@@ -735,7 +750,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);
@@ -746,7 +761,7 @@
}
} else {
throw new _MiscTemplateException(this, new Object[] {
- (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()),
+ (unboundCallable.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(unboundCallable.getName()),
" has no parameter with name ", new _DelayedJQuote(argName), "." });
}
}
@@ -759,11 +774,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, new Object[] {
- (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), "."});
}
@@ -787,13 +802,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)
@@ -816,7 +828,7 @@
}
}
- Macro.Context getCurrentMacroContext() {
+ CallableInvocationContext getCurrentMacroContext() {
return currentMacroContext;
}
@@ -1707,7 +1719,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("]");
}
@@ -1929,7 +1941,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 {
@@ -1942,25 +1954,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;
}
}
@@ -2069,14 +2081,19 @@
prevTemplate = null;
}
- importMacros(includedTemplate);
+ final Template prevCurrentTemplate = currentTemplate;
try {
- visit(includedTemplate.getRootTreeNode());
- }
- finally {
- if (parentReplacementOn) {
- setParent(prevTemplate);
+ currentTemplate = includedTemplate;
+ predefineMacros(includedTemplate);
+ try {
+ visit(includedTemplate.getRootTreeNode());
+ } finally {
+ if (parentReplacementOn) {
+ setParent(prevTemplate);
+ }
}
+ } finally {
+ currentTemplate = prevCurrentTemplate;
}
}
@@ -2201,9 +2218,9 @@
}
}
- void importMacros(Template template) {
+ void predefineMacros(Template template) {
for (Iterator it = template.getMacros().values().iterator(); it.hasNext();) {
- visitMacroDef((Macro) it.next());
+ visitCallableDefinition((UnboundCallable) it.next());
}
}
@@ -2310,6 +2327,11 @@
public Template getTemplate() {
return template == null ? Environment.this.getTemplate() : template;
}
+
+ 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 68c935a..e5bcb75 100644
--- a/src/main/java/freemarker/core/EvalUtil.java
+++ b/src/main/java/freemarker/core/EvalUtil.java
@@ -226,7 +226,7 @@
env != null
? env.getArithmeticEngine()
: (leftExp != null
- ? leftExp.getTemplate().getArithmeticEngine()
+ ? leftExp.getUnboundTemplate().getConfiguration().getArithmeticEngine()
: ArithmeticEngine.BIGDECIMAL_ENGINE);
try {
cmpResult = ae.compareNumbers(leftNum, rightNum);
diff --git a/src/main/java/freemarker/core/Expression.java b/src/main/java/freemarker/core/Expression.java
index d49dabc..ba97419 100644
--- a/src/main/java/freemarker/core/Expression.java
+++ b/src/main/java/freemarker/core/Expression.java
@@ -18,7 +18,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;
@@ -53,11 +52,11 @@
// Hook in here to set the constant value if possible.
- void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine)
+ void setLocation(UnboundTemplate unboundTemplate, int beginColumn, int beginLine, int endColumn, int endLine)
throws
ParseException
{
- super.setLocation(template, beginColumn, beginLine, endColumn, 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 eaac963..e076d73 100644
--- a/src/main/java/freemarker/core/IfBlock.java
+++ b/src/main/java/freemarker/core/IfBlock.java
@@ -58,7 +58,7 @@
if (nestedElements.size() == 1) {
ConditionalBlock cblock = (ConditionalBlock) nestedElements.get(0);
cblock.isLonelyIf = true;
- cblock.setLocation(getTemplate(), cblock, this);
+ cblock.setLocation(getUnboundTemplate(), cblock, this);
return cblock.postParseCleanup(stripWhitespace);
}
else {
diff --git a/src/main/java/freemarker/core/Include.java b/src/main/java/freemarker/core/Include.java
index 9139e09..5f8703d 100644
--- a/src/main/java/freemarker/core/Include.java
+++ b/src/main/java/freemarker/core/Include.java
@@ -38,12 +38,12 @@
private final Boolean ignoreMissing;
/**
- * @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;
@@ -80,7 +80,7 @@
parse = Boolean.valueOf(StringUtil.getYesNo(parseExp.evalAndCoerceToString(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 {
ignoreMissing = 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.evalAndCoerceToString(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, new Object[] {
"Malformed template name ", new _DelayedJQuote(e.getTemplateName()), ":\n",
diff --git a/src/main/java/freemarker/core/LibraryLoad.java b/src/main/java/freemarker/core/LibraryLoad.java
index 4825a53..5259029 100644
--- a/src/main/java/freemarker/core/LibraryLoad.java
+++ b/src/main/java/freemarker/core/LibraryLoad.java
@@ -30,15 +30,15 @@
*/
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)
{
@@ -50,7 +50,7 @@
final String importedTemplateName = importedTemplateNameExp.evalAndCoerceToString(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, new Object[] {
"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 e7a2175..069145b 100644
--- a/src/main/java/freemarker/core/Macro.java
+++ b/src/main/java/freemarker/core/Macro.java
@@ -16,312 +16,34 @@
package freemarker.core;
-import java.io.IOException;
-import java.util.ArrayList;
-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 bacward 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.
*/
-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,
- TextBlock.EMPTY_BLOCK);
+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,
- TemplateElement nestedBlock)
- {
- this.name = name;
- this.paramNames = (String[])argumentNames.toArray(
- new String[argumentNames.size()]);
- this.paramDefaults = args;
-
- this.function = function;
- this.catchAllParamName = catchAllParamName;
-
- this.nestedBlock = nestedBlock;
- }
+ public abstract String getCatchAll();
- public String getCatchAll() {
- return catchAllParamName;
- }
-
- public String[] getArgumentNames() {
- return (String[])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;
- }
-
- void accept(Environment env) {
- env.visitMacroDef(this);
- }
-
- protected String dump(boolean canonical) {
- StringBuffer sb = new StringBuffer();
- if (canonical) sb.append('<');
- sb.append(getNodeTypeSymbol());
- sb.append(' ');
- sb.append(_CoreStringUtils.toFTLTopLevelTragetIdentifier(name));
- sb.append(function ? '(' : ' ');
- int argCnt = paramNames.length;
- for (int i = 0; i < argCnt; i++) {
- if (i != 0) {
- if (function) {
- 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 (argCnt != 0) sb.append(", ");
- sb.append(catchAllParamName);
- sb.append("...");
- }
- if (function) sb.append(')');
- if (canonical) {
- sb.append('>');
- if (nestedBlock != null) {
- sb.append(nestedBlock.getCanonicalForm());
- }
- sb.append("</").append(getNodeTypeSymbol()).append('>');
- }
- return sb.toString();
- }
-
- String getNodeTypeSymbol() {
- return function ? "#function" : "#macro";
- }
-
- boolean isShownInStackTrace() {
- return false;
- }
-
- public boolean isFunction() {
- return function;
- }
-
- class Context implements LocalContext {
- final Environment.Namespace localVars;
- final TemplateElement nestedContent;
- final Environment.Namespace nestedContentNamespace;
- final List nestedContentParameterNames;
- final ArrayList prevLocalContextStack;
- final Context prevMacroContext;
-
- Context(Environment env,
- TemplateElement nestedContent,
- List nestedContentParameterNames)
- {
- this.localVars = env.new Namespace();
- this.nestedContent = nestedContent;
- this.nestedContentNamespace = env.getCurrentNamespace();
- this.nestedContentParameterNames = nestedContentParameterNames;
- this.prevLocalContextStack = env.getLocalContextStack();
- this.prevMacroContext = env.getCurrentMacroContext();
- }
-
-
- Macro getMacro() {
- return Macro.this;
- }
-
- void runMacro(Environment env) throws TemplateException, IOException {
- sanityCheck(env);
- // Set default values for unspecified parameters
- if (nestedBlock != null) {
- env.visit(nestedBlock);
- }
- }
-
- // 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(new Object[] {
- "When calling macro ", new _DelayedJQuote(name),
- ", required parameter ", new _DelayedJQuote(argName),
- " (parameter #", new Integer(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;
- }
- }
-
- int getParameterCount() {
- return 1/*name*/ + paramNames.length * 2/*name=default*/ + 1/*catchAll*/ + 1/*type*/;
- }
-
- 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 new Integer(function ? TYPE_FUNCTION : TYPE_MACRO);
- } else {
- throw new IndexOutOfBoundsException();
- }
- }
- }
-
- 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();
- }
- }
- }
+ public abstract boolean isFunction();
boolean isNestedBlockRepeater() {
// Because of recursive calls
diff --git a/src/main/java/freemarker/core/MessageUtil.java b/src/main/java/freemarker/core/MessageUtil.java
index a1f1a46..2c84912 100644
--- a/src/main/java/freemarker/core/MessageUtil.java
+++ b/src/main/java/freemarker/core/MessageUtil.java
@@ -18,7 +18,6 @@
import java.util.ArrayList;
-import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
@@ -49,37 +48,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 53d5203..6472a7d 100644
--- a/src/main/java/freemarker/core/MethodCall.java
+++ b/src/main/java/freemarker/core/MethodCall.java
@@ -62,16 +62,17 @@
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 3c23d16..1b72062 100644
--- a/src/main/java/freemarker/core/NewBI.java
+++ b/src/main/java/freemarker/core/NewBI.java
@@ -20,7 +20,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;
@@ -46,7 +45,7 @@
TemplateModel _eval(Environment env)
throws TemplateException
{
- return new ConstructorFunction(target.evalAndCoerceToString(env), env, target.getTemplate());
+ return new ConstructorFunction(target.evalAndCoerceToString(env), env);
}
class ConstructorFunction implements TemplateMethodModelEx {
@@ -54,9 +53,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, new Object[] {
"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 263944b..a41fd8c 100644
--- a/src/main/java/freemarker/core/NonUserDefinedDirectiveLikeException.java
+++ b/src/main/java/freemarker/core/NonUserDefinedDirectiveLikeException.java
@@ -29,7 +29,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 9b5e1f9..5b99d4d 100644
--- a/src/main/java/freemarker/core/ParseException.java
+++ b/src/main/java/freemarker/core/ParseException.java
@@ -129,14 +129,16 @@
}
/**
+ * @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
*/
public ParseException(String description, Template template,
@@ -150,7 +152,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
*/
@@ -159,8 +161,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
*/
public ParseException(String description, Template template, int lineNumber, int columnNumber, Throwable cause) {
@@ -172,6 +174,7 @@
}
/**
+ * @deprecated Use {@link #ParseException(String, UnboundTemplate, Token)} instead.
* @since 2.3.20
*/
public ParseException(String description, Template template, Token tk) {
@@ -179,6 +182,7 @@
}
/**
+ * @deprecated Use {@link #ParseException(String, UnboundTemplate, Token, Throwable)} instead.
* @since 2.3.20
*/
public ParseException(String description, Template template, Token tk, Throwable cause) {
@@ -190,6 +194,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) {
@@ -201,7 +244,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 6f62365..6b1d110 100644
--- a/src/main/java/freemarker/core/PropertySetting.java
+++ b/src/main/java/freemarker/core/PropertySetting.java
@@ -18,7 +18,6 @@
import java.util.Arrays;
-import freemarker.template.Template;
import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
@@ -54,11 +53,11 @@
this.value = value;
}
- void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine)
+ void setLocation(UnboundTemplate unboundTemplate, int beginColumn, int beginLine, int endColumn, int endLine)
throws
ParseException
{
- super.setLocation(template, beginColumn, beginLine, endColumn, endLine);
+ super.setLocation(unboundTemplate, beginColumn, beginLine, endColumn, endLine);
if (Arrays.binarySearch(SETTING_NAMES, key) < 0) {
StringBuffer sb = new StringBuffer();
@@ -69,8 +68,8 @@
sb.append(" Supporting camelCase setting names is planned for FreeMarker 2.4.0; check if an update is "
+ "available, and if it indeed supports camel case. "
+ "Until that, use \"").append(underscoredName).append("\".");
- } else if (((Configurable) template).getSettingNames().contains(key)
- || ((Configurable) template).getSettingNames().contains(underscoredName)) {
+ } else if (Configurable.getConfigurableSettingNames().contains(key)
+ || Configurable.getConfigurableSettingNames().contains(underscoredName)) {
sb.append(" The setting name is recognized, but changing this setting in a template isn't supported.");
} else {
sb.append(" The allowed setting names are: ");
@@ -83,7 +82,7 @@
}
throw new ParseException(
sb.toString(),
- template, beginLine, beginColumn, endLine, endColumn);
+ unboundTemplate, beginLine, beginColumn, endLine, endColumn);
}
}
diff --git a/src/main/java/freemarker/core/StringLiteral.java b/src/main/java/freemarker/core/StringLiteral.java
index 92c86b7..6efde02 100644
--- a/src/main/java/freemarker/core/StringLiteral.java
+++ b/src/main/java/freemarker/core/StringLiteral.java
@@ -41,12 +41,12 @@
FMParserTokenManager token_source = new FMParserTokenManager(scs);
token_source.onlyTextOutput = true;
FMParser parser = new FMParser(token_source);
- parser.setTemplate(getTemplate());
+ parser.setTemplate(getUnboundTemplate());
try {
dynamicValue = parser.FreeMarkerText();
}
catch(ParseException e) {
- e.setTemplateName(getTemplate().getSourceName());
+ e.setTemplateName(getUnboundTemplate().getSourceName());
throw e;
}
this.constantValue = null;
diff --git a/src/main/java/freemarker/core/TemplateObject.java b/src/main/java/freemarker/core/TemplateObject.java
index ffb4e93..6b7ec0a 100644
--- a/src/main/java/freemarker/core/TemplateObject.java
+++ b/src/main/java/freemarker/core/TemplateObject.java
@@ -16,7 +16,6 @@
package freemarker.core;
-import freemarker.template.Template;
/**
* <b>Internal API - subject to change:</b> Represent a node in the parsed template (either a {@link Expression} or a
@@ -30,7 +29,7 @@
*/
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,
@@ -38,93 +37,116 @@
* 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)
+ final void setLocation(UnboundTemplate unboundTemplate, Token begin, Token end)
throws
ParseException
{
- setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+ setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
}
- final void setLocation(Template template, Token begin, TemplateObject end)
+ final void setLocation(UnboundTemplate unboundTemplate, Token begin, TemplateObject end)
throws
ParseException
{
- setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+ setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
}
- final void setLocation(Template template, TemplateObject begin, Token end)
+ final void setLocation(UnboundTemplate unboundTemplate, TemplateObject begin, Token end)
throws
ParseException
{
- setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+ setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
}
- final void setLocation(Template template, TemplateObject begin, TemplateObject end)
+ final void setLocation(UnboundTemplate unboundTemplate, TemplateObject begin, TemplateObject end)
throws
ParseException
{
- setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+ setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
}
- void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine)
+ void setLocation(UnboundTemplate unboundTemplate, int beginColumn, int beginLine, int endColumn, int endLine)
throws
ParseException
{
- this.template = template;
+ 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;
}
@@ -144,8 +166,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) {
@@ -165,15 +189,17 @@
}
/**
- * @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
*/
- 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;
@@ -182,8 +208,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 990dca7..5709732 100644
--- a/src/main/java/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java
+++ b/src/main/java/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java
@@ -87,7 +87,7 @@
nestedMixedC = (MixedContent) nestedBlock;
} else {
nestedMixedC = new MixedContent();
- nestedMixedC.setLocation(te.getTemplate(), 0, 0, 0, 0);
+ nestedMixedC.setLocation(te.getUnboundTemplate(), 0, 0, 0, 0);
nestedMixedC.parent = te;
nestedBlock.parent = nestedMixedC;
nestedMixedC.addElement(nestedBlock);
@@ -108,7 +108,7 @@
static class ThreadInterruptionCheck extends TemplateElement {
private ThreadInterruptionCheck(TemplateElement te) throws ParseException {
- setLocation(te.getTemplate(), 0, 0, 0, 0);
+ setLocation(te.getUnboundTemplate(), 0, 0, 0, 0);
parent = te;
}
diff --git a/src/main/java/freemarker/core/TokenMgrError.java b/src/main/java/freemarker/core/TokenMgrError.java
index 34354ee..0ee0e10 100644
--- a/src/main/java/freemarker/core/TokenMgrError.java
+++ b/src/main/java/freemarker/core/TokenMgrError.java
@@ -18,6 +18,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}.
@@ -238,9 +240,19 @@
return detail;
}
+ /**
+ * @deprecated Use {@link #toParseException(UnboundTemplate)} instead.
+ */
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..902e823
--- /dev/null
+++ b/src/main/java/freemarker/core/UnboundCallable.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky
+ *
+ * Licensed 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 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,
+ TextBlock.EMPTY_BLOCK);
+
+ 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,
+ TemplateElement nestedBlock)
+ {
+ this.name = name;
+ this.paramNames = (String[])argumentNames.toArray(
+ new String[argumentNames.size()]);
+ this.paramDefaults = args;
+
+ this.function = function;
+ this.catchAllParamName = catchAllParamName;
+
+ this.nestedBlock = nestedBlock;
+ }
+
+ String[] getParamNames() {
+ return paramNames;
+ }
+
+ Map getParamDefaults() {
+ return paramDefaults;
+ }
+
+ public String getCatchAll() {
+ return catchAllParamName;
+ }
+
+ public String[] getArgumentNames() {
+ return (String[])paramNames.clone();
+ }
+
+ String[] getArgumentNamesInternal() {
+ return paramNames;
+ }
+
+ boolean hasArgNamed(String name) {
+ return paramDefaults.containsKey(name);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ void accept(Environment env) {
+ env.visitCallableDefinition(this);
+ }
+
+ protected String dump(boolean canonical) {
+ StringBuffer sb = new StringBuffer();
+ if (canonical) sb.append('<');
+ sb.append(getNodeTypeSymbol());
+ sb.append(' ');
+ sb.append(_CoreStringUtils.toFTLTopLevelTragetIdentifier(name));
+ sb.append(function ? '(' : ' ');
+ int argCnt = paramNames.length;
+ for (int i = 0; i < argCnt; i++) {
+ if (i != 0) {
+ if (function) {
+ 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 (argCnt != 0) sb.append(", ");
+ sb.append(catchAllParamName);
+ sb.append("...");
+ }
+ if (function) sb.append(')');
+ if (canonical) {
+ sb.append('>');
+ if (nestedBlock != null) {
+ sb.append(nestedBlock.getCanonicalForm());
+ }
+ sb.append("</").append(getNodeTypeSymbol()).append('>');
+ }
+ return sb.toString();
+ }
+
+ String getNodeTypeSymbol() {
+ return function ? "#function" : "#macro";
+ }
+
+ boolean isShownInStackTrace() {
+ return false;
+ }
+
+ public boolean isFunction() {
+ return function;
+ }
+
+ int getParameterCount() {
+ return 1/*name*/ + paramNames.length * 2/*name=default*/ + 1/*catchAll*/ + 1/*type*/;
+ }
+
+ 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 new Integer(function ? TYPE_FUNCTION : TYPE_MACRO);
+ } else {
+ throw new IndexOutOfBoundsException();
+ }
+ }
+ }
+
+ 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..fac5be9
--- /dev/null
+++ b/src/main/java/freemarker/core/UnboundTemplate.java
@@ -0,0 +1,471 @@
+/*
+ * Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky
+ *
+ * Licensed 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.StringWriter;
+import java.io.Writer;
+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";
+
+ /**
+ * 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 final String sourceName;
+ private final Configuration cfg;
+ private final Version templateLanguageVersion;
+
+ /** Attributes added via {@code <#ftl attributes=...>}. */
+ private LinkedHashMap<String, Object> customAttributes;
+
+ private Map<String, UnboundCallable> unboundCallables = new HashMap<String, UnboundCallable>();
+ // 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>());
+ private TemplateElement rootElement;
+ private String defaultNamespaceURI;
+ private int actualTagSyntax;
+
+ private final ArrayList lines = new ArrayList();
+
+ private Map<String, String> nodePrefixToNamespaceURIMapping = new HashMap<String, String>();
+ private Map<String, String> namespaceURIToPrefixMapping = new HashMap<String, String>();
+
+ private UnboundTemplate(String sourceName, Configuration cfg) {
+ this.sourceName = sourceName;
+
+ NullArgumentException.check(cfg);
+ this.cfg = cfg;
+
+ this.templateLanguageVersion = normalizeTemplateLanguageVersion(cfg.getIncompatibleImprovements());
+ }
+
+ /**
+ * @param reader
+ * Reads the template source code
+ * @param cfg
+ * The FreeMarker configuration settings; some of them influences parsing, also the resulting
+ * {@link UnboundTemplate} will be bound to this.
+ * @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, String assumedEncoding)
+ throws IOException {
+ this(sourceName, cfg);
+
+ try {
+ if (!(reader instanceof BufferedReader)) {
+ reader = new BufferedReader(reader, 0x1000);
+ }
+ reader = new LineTableBuilder(reader);
+
+ try {
+ parser = new FMParser(this,
+ reader, assumedEncoding,
+ cfg.getStrictSyntaxMode(),
+ cfg.getWhitespaceStripping(),
+ cfg.getTagSyntax(),
+ cfg.getIncompatibleImprovements().intValue());
+ this.rootElement = parser.Root();
+ this.actualTagSyntax = parser._getLastTagSyntax();
+ } 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();
+ }
+
+ namespaceURIToPrefixMapping = Collections.unmodifiableMap(namespaceURIToPrefixMapping);
+ nodePrefixToNamespaceURIMapping = Collections.unmodifiableMap(nodePrefixToNamespaceURIMapping);
+ }
+
+ 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;
+ }
+ }
+
+ static UnboundTemplate createPlainTextTemplate(String sourceName, String content, Configuration config) {
+ UnboundTemplate unboundTemplate = new UnboundTemplate(sourceName, config);
+ unboundTemplate.rootElement = new TextBlock(content);
+ unboundTemplate.actualTagSyntax = config.getTagSyntax();
+ return unboundTemplate;
+ }
+
+ /**
+ * Returns a string representing the raw template text in canonical form.
+ */
+ 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;
+ }
+
+ /**
+ * Returns the tag syntax the parser has chosen for this template. If the syntax could be determined, it's
+ * {@link Configuration#SQUARE_BRACKET_TAG_SYNTAX} or {@link Configuration#ANGLE_BRACKET_TAG_SYNTAX}. If the syntax
+ * couldn't be determined (like because there was no tags in the template, or it was a plain text template), this
+ * returns whatever the default is in the current configuration, so it's maybe
+ * {@link Configuration#AUTO_DETECT_TAG_SYNTAX}.
+ */
+ public int getActualTagSyntax() {
+ return actualTagSyntax;
+ }
+
+ public Configuration getConfiguration() {
+ return cfg;
+ }
+
+ /**
+ * 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;
+ StringBuffer buf = new StringBuffer();
+ 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 (nodePrefixToNamespaceURIMapping.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 {
+ nodePrefixToNamespaceURIMapping.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;
+ }
+ return nodePrefixToNamespaceURIMapping.get(prefix);
+ }
+
+ /**
+ * @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 "";
+ }
+ return namespaceURIToPrefixMapping.get(nsURI);
+ }
+
+ /**
+ * @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;
+ }
+
+ /**
+ * This is a helper class that builds up the line table info for us.
+ */
+ private class LineTableBuilder extends FilterReader {
+
+ StringBuffer lineBuf = new StringBuffer();
+ int lastChar;
+
+ /**
+ * @param r
+ * the character stream to wrap
+ */
+ LineTableBuilder(Reader r) {
+ super(r);
+ }
+
+ public int read() throws IOException {
+ int c = in.read();
+ handleChar(c);
+ return c;
+ }
+
+ public int read(char cbuf[], int off, int len) throws IOException {
+ int numchars = in.read(cbuf, off, len);
+ for (int i = off; i < off + numchars; i++) {
+ char c = cbuf[i];
+ handleChar(c);
+ }
+ return numchars;
+ }
+
+ public void close() throws IOException {
+ if (lineBuf.length() > 0) {
+ lines.add(lineBuf.toString());
+ lineBuf.setLength(0);
+ }
+ super.close();
+ }
+
+ 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 a9bdc52..4db7485 100644
--- a/src/main/java/freemarker/core/UnifiedCall.java
+++ b/src/main/java/freemarker/core/UnifiedCall.java
@@ -71,18 +71,18 @@
}
void accept(Environment env) throws TemplateException, IOException {
- TemplateModel tm = nameExp.eval(env);
- if (tm == Macro.DO_NOTHING_MACRO) return; // 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; // shortcut here.
+ if (tm instanceof BoundCallable) {
+ final BoundCallable boundMacro = (BoundCallable) tm;
+ final Macro unboundMacro = boundMacro.getUnboundCallable();
+ if (unboundMacro.isFunction() && !legacySyntax) {
throw new _MiscTemplateException(env, new Object[] {
- "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,
- nestedBlock);
+ env.invoke(boundMacro, namedArgs, positionalArgs, bodyParameterNames, nestedBlock);
}
else {
boolean isDirectiveModel = tm instanceof TemplateDirectiveModel;
@@ -337,7 +337,7 @@
}
public String getTemplateSourceName() {
- return getTemplate().getSourceName();
+ return getUnboundTemplate().getSourceName();
}
boolean isNestedBlockRepeater() {
diff --git a/src/main/java/freemarker/core/_CoreAPI.java b/src/main/java/freemarker/core/_CoreAPI.java
index 66b165b..75cfc76 100644
--- a/src/main/java/freemarker/core/_CoreAPI.java
+++ b/src/main/java/freemarker/core/_CoreAPI.java
@@ -16,11 +16,16 @@
package freemarker.core;
+import java.io.IOException;
+import java.io.Reader;
import java.io.Writer;
import java.util.Collections;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
+import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateDirectiveBody;
@@ -109,6 +114,70 @@
return cfgable.getSettingNames();
}
+ 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) {
+ // A bit of backward compatibility complication, since in 2.4 Macro was split to bound- and unbound callables:
+ final UnboundCallable unboundCallable;
+ if (macro instanceof UnboundCallable) {
+ // It's coming from the AST:
+ unboundCallable = (UnboundCallable) macro;
+ } else if (macro instanceof BoundCallable) {
+ // It's coming from an FTL variable:
+ unboundCallable = ((BoundCallable) macro).getUnboundCallable();
+ } else {
+ // Impossible, Macro should have only two subclasses.
+ throw new BugException();
+ }
+ unboundTemplate.addUnboundCallable(unboundCallable);
+ }
+
+ public static void addImport(UnboundTemplate unboundTemplate, LibraryLoad libLoad) {
+ unboundTemplate.addImport(libLoad);
+ }
+
+ public static UnboundTemplate newUnboundTemplate(Reader reader, String sourceName, Configuration cfg, String assumedEncoding) throws IOException {
+ return new UnboundTemplate(reader, sourceName, cfg, assumedEncoding);
+ }
+
+ public static boolean isBoundCallable(Object obj) {
+ return obj instanceof BoundCallable;
+ }
+
+ public static UnboundTemplate createPlainTextTemplate(String sourceName, String content, Configuration config) {
+ return UnboundTemplate.createPlainTextTemplate(sourceName, content, 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 getMacros(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!
diff --git a/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java b/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java
index cf91654..6903b86 100644
--- a/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java
+++ b/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java
@@ -204,7 +204,9 @@
}
private void appendParts(StringBuffer 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[]) {
@@ -216,7 +218,7 @@
partStr = "null";
}
- if (template != null) {
+ if (unboundTemplate != null) {
if (partStr.length() > 4
&& partStr.charAt(0) == '<'
&& (
@@ -224,7 +226,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/template/Template.java b/src/main/java/freemarker/template/Template.java
index 9ae73af..8ae8bef 100644
--- a/src/main/java/freemarker/template/Template.java
+++ b/src/main/java/freemarker/template/Template.java
@@ -16,21 +16,14 @@
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.util.ArrayList;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Vector;
import freemarker.cache.TemplateCache;
import freemarker.cache.TemplateLoader;
@@ -42,9 +35,8 @@
import freemarker.core.Macro;
import freemarker.core.ParseException;
import freemarker.core.TemplateElement;
-import freemarker.core.TextBlock;
-import freemarker.core.TokenMgrError;
-import freemarker.debug.impl.DebuggerService;
+import freemarker.core.UnboundTemplate;
+import freemarker.core._CoreAPI;
/**
* <p>Stores an already parsed template, ready to be processed (rendered) for unlimited times, possibly from
@@ -63,36 +55,27 @@
* 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 final UnboundTemplate unboundTemplate;
private final String name;
- private final String sourceName;
- private final ArrayList lines = new ArrayList();
- 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, boolean overloadSelector)
+ 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());
}
private static Configuration toNonNull(Configuration cfg) {
@@ -180,43 +163,9 @@
*/
public Template(
String name, String sourceName, Reader reader, Configuration cfg, String encoding) throws IOException {
- this(name, sourceName, cfg, true);
-
+ this(_CoreAPI.newUnboundTemplate(
+ reader, sourceName != null ? sourceName : name, toNonNull(cfg), encoding), name, cfg);
this.encoding = encoding;
- try {
- if (!(reader instanceof BufferedReader)) {
- reader = new BufferedReader(reader, 0x1000);
- }
- reader = new LineTableBuilder(reader);
-
- try {
- parser = new FMParser(this, reader,
- getConfiguration().getStrictSyntaxMode(),
- getConfiguration().getWhitespaceStripping(),
- getConfiguration().getTagSyntax(),
- getConfiguration().getIncompatibleImprovements().intValue());
- this.rootElement = parser.Root();
- this.actualTagSyntax = parser._getLastTagSyntax();
- }
- 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();
- }
- DebuggerService.registerTemplate(this);
- namespaceURIToPrefixLookup = Collections.unmodifiableMap(namespaceURIToPrefixLookup);
- prefixToNamespaceURILookup = Collections.unmodifiableMap(prefixToNamespaceURILookup);
}
/**
@@ -232,18 +181,6 @@
}
/**
- * Only meant to be used internally.
- *
- * @deprecated Has problems setting actualTagSyntax and templateLanguageVersion; will be removed in 2.4.
- */
- // [2.4] remove this
- Template(String name, TemplateElement root, Configuration cfg) {
- this(name, null, cfg, true);
- this.rootElement = root;
- DebuggerService.registerTemplate(this);
- }
-
- /**
* Same as {@link #getPlainTextTemplate(String, String, String, Configuration)} with {@code null} {@code sourceName}
* argument.
*/
@@ -266,23 +203,9 @@
* @since 2.3.22
*/
static public Template getPlainTextTemplate(String name, String sourceName, String content, Configuration config) {
- Template template = new Template(name, sourceName, config, true);
- template.rootElement = new TextBlock(content);
- template.actualTagSyntax = config.getTagSyntax();
- 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;
- }
+ return new Template(_CoreAPI.createPlainTextTemplate(
+ sourceName != null ? sourceName : name,
+ content, config), name, config);
}
/**
@@ -444,6 +367,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
@@ -490,7 +421,7 @@
* @since 2.3.22
*/
public String getSourceName() {
- return sourceName != null ? sourceName : getName();
+ return unboundTemplate.getSourceName();
}
/**
@@ -502,11 +433,13 @@
/**
* 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();
}
/**
@@ -556,39 +489,35 @@
* @since 2.3.20
*/
public int getActualTagSyntax() {
- return actualTagSyntax;
+ return unboundTemplate.getActualTagSyntax();
}
/**
* 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.
*/
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.
*/
- public void addImport(LibraryLoad ll) {
- imports.add(ll);
+ public void addImport(LibraryLoad libLoad) {
+ _CoreAPI.addImport(unboundTemplate, libLoad);
}
/**
@@ -599,114 +528,29 @@
* @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;
- StringBuffer buf = new StringBuffer();
- 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();
- }
-
- /**
- * This is a helper class that builds up the line table
- * info for us.
- */
- private class LineTableBuilder extends FilterReader {
-
- StringBuffer lineBuf = new StringBuffer();
- int lastChar;
-
- /**
- * @param r the character stream to wrap
- */
- LineTableBuilder(Reader r) {
- super(r);
- }
-
- public int read() throws IOException {
- int c = in.read();
- handleChar(c);
- return c;
- }
-
- public int read(char cbuf[], int off, int len) throws IOException {
- int numchars = in.read(cbuf, off, len);
- for (int i=off; i < off+numchars; i++) {
- char c = cbuf[i];
- handleChar(c);
- }
- return numchars;
- }
-
- public void close() throws IOException {
- if (lineBuf.length() >0) {
- lines.add(lineBuf.toString());
- lineBuf.setLength(0);
- }
- super.close();
- }
-
- 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);
}
/**
* @deprecated Should only be used internally, and might will be removed later.
*/
public TemplateElement getRootTreeNode() {
- return rootElement;
+ return _CoreAPI.getRootTreeNode(unboundTemplate);
}
/**
* @deprecated Should only be used internally, and might will be removed later.
*/
public Map getMacros() {
- return macros;
+ return _CoreAPI.getMacros(unboundTemplate);
}
/**
* @deprecated Should only be used internally, and might will be removed later.
*/
public List getImports() {
- return imports;
+ return _CoreAPI.getImports(unboundTemplate);
}
/**
@@ -715,57 +559,25 @@
* @deprecated Should only be used internally, and might will be removed later.
*/
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);
}
/**
@@ -774,46 +586,26 @@
* 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.
*/
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 {
diff --git a/src/main/java/freemarker/template/TemplateException.java b/src/main/java/freemarker/template/TemplateException.java
index 5c735d3..1016a59 100644
--- a/src/main/java/freemarker/template/TemplateException.java
+++ b/src/main/java/freemarker/template/TemplateException.java
@@ -30,6 +30,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;
@@ -40,7 +41,6 @@
*/
public class TemplateException extends Exception {
- private static final int FTL_STACK_TOP_FEW_MAX_LINES = 6;
private static final String FTL_INSTRUCTION_STACK_TRACE_TITLE
= "FTL stack trace (\"~\" means nesting-related):";
@@ -197,11 +197,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 = new Integer(templateObject.getBeginLine());
columnNumber = new Integer(templateObject.getBeginColumn());
endLineNumber = new Integer(templateObject.getEndLine());
@@ -212,6 +212,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 655d455..3e99ba9 100644
--- a/src/main/java/freemarker/template/_TemplateAPI.java
+++ b/src/main/java/freemarker/template/_TemplateAPI.java
@@ -59,13 +59,9 @@
}
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();
diff --git a/src/main/java/freemarker/template/utility/ClassUtil.java b/src/main/java/freemarker/template/utility/ClassUtil.java
index fb92843..53dd9d2 100644
--- a/src/main/java/freemarker/template/utility/ClassUtil.java
+++ b/src/main/java/freemarker/template/utility/ClassUtil.java
@@ -21,6 +21,7 @@
import freemarker.core.Environment;
import freemarker.core.Macro;
+import freemarker.core._CoreAPI;
import freemarker.ext.beans.BeanModel;
import freemarker.ext.beans.BooleanModel;
import freemarker.ext.beans.CollectionModel;
@@ -286,7 +287,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 9413c03..330fa5e 100644
--- a/src/main/java/freemarker/template/utility/CollectionUtils.java
+++ b/src/main/java/freemarker/template/utility/CollectionUtils.java
@@ -23,6 +23,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 ecf2716..ede1118 100644
--- a/src/main/java/freemarker/template/utility/StringUtil.java
+++ b/src/main/java/freemarker/template/utility/StringUtil.java
@@ -528,7 +528,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 ff778fe..4de27e1 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
@@ -38,11 +38,13 @@
public class FMParser {
// Necessary for adding macros and setting location info.
- private Template template;
+ private UnboundTemplate unboundTemplate;
+ private String assumedEncoding;
+ private boolean stripWhitespace, stripText;
// variables that keep track of whether we are in a loop or a switch.
private int loopNesting, switchNesting;
- private boolean inMacro, inFunction, stripWhitespace, stripText;
+ private boolean inMacro, inFunction;
private LinkedList escapes = new LinkedList();
private int mixedContentNesting; // for stripText
private int incompatibleImprovements;
@@ -59,32 +61,22 @@
return parser;
}
- /**
- * Constructs a new parser object.
- *
- * @param template
- * The template associated with this parser.
- * @param reader
- * The character stream to use as input
- * @param strictEscapeSyntax
- * Whether FreeMarker directives must start with a #
- */
- public FMParser(Template template, Reader reader, boolean strictEscapeSyntax, boolean stripWhitespace) {
- this(reader);
- setTemplate(template);
- token_source.setParser(this);
- token_source.strictEscapeSyntax = strictEscapeSyntax;
- this.stripWhitespace = stripWhitespace;
- }
-
- public FMParser(Template template, Reader reader, boolean strictEscapeSyntax, boolean stripWhitespace, int tagSyntax) {
- this(template, reader, strictEscapeSyntax, stripWhitespace, tagSyntax,
- Configuration.PARSED_DEFAULT_INCOMPATIBLE_ENHANCEMENTS);
- }
-
- public FMParser(Template template, Reader reader, boolean strictEscapeSyntax, boolean stripWhitespace,
+ FMParser(
+ UnboundTemplate unboundTemplate,
+ Reader reader, String assumedEncoding,
+ boolean strictEscapeSyntax, boolean stripWhitespace,
int tagSyntax, int incompatibleImprovements) {
- this(template, reader, strictEscapeSyntax, stripWhitespace);
+ this(reader);
+ this.assumedEncoding = assumedEncoding;
+
+ setTemplate(unboundTemplate);
+
+ token_source.setParser(this);
+
+ token_source.strictEscapeSyntax = strictEscapeSyntax;
+
+ this.stripWhitespace = stripWhitespace;
+
switch (tagSyntax) {
case Configuration.AUTO_DETECT_TAG_SYNTAX:
token_source.autodetectTagSyntax = true;
@@ -98,22 +90,19 @@
default:
throw new IllegalArgumentException("Illegal argument for tagSyntax");
}
+
token_source.incompatibleImprovements = incompatibleImprovements;
this.incompatibleImprovements = incompatibleImprovements;
}
- public FMParser(String template) {
- this(null, new StringReader(template), true, true);
+ void setTemplate(UnboundTemplate unboundTemplate)
+ {
+ this.unboundTemplate = unboundTemplate;
}
- void setTemplate(Template template)
+ UnboundTemplate getTemplate()
{
- this.template = template;
- }
-
- Template getTemplate()
- {
- return template;
+ return unboundTemplate;
}
/**
@@ -314,7 +303,7 @@
this.parser = parser;
}
- Template getTemplate() {
+ UnboundTemplate getTemplate() {
return parser != null ? parser.getTemplate() : null;
}
@@ -1127,7 +1116,7 @@
end = <CLOSE_PAREN>
{
result = new ParentheticalExpression(exp);
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -1171,7 +1160,7 @@
for (int i = 0; i < nots.size(); i++) {
result = new NotExpression(exp);
Token tok = (Token) nots.get(nots.size() -i -1);
- result.setLocation(template, tok, exp);
+ result.setLocation(unboundTemplate, tok, exp);
exp = result;
}
return result;
@@ -1193,7 +1182,7 @@
exp = PrimaryExpression()
{
result = new UnaryPlusMinusExpression(exp, isMinus);
- result.setLocation(template, t, exp);
+ result.setLocation(unboundTemplate, t, exp);
return result;
}
}
@@ -1225,7 +1214,7 @@
numberLiteralOnly(rhs);
result = new ArithmeticExpression(lhs, rhs, ArithmeticExpression.TYPE_SUBSTRACTION);
}
- result.setLocation(template, lhs, rhs);
+ result.setLocation(unboundTemplate, lhs, rhs);
lhs = result;
}
)*
@@ -1261,7 +1250,7 @@
numberLiteralOnly(lhs);
numberLiteralOnly(rhs);
result = new ArithmeticExpression(lhs, rhs, operation);
- result.setLocation(template, lhs, rhs);
+ result.setLocation(unboundTemplate, lhs, rhs);
lhs = result;
}
)*
@@ -1294,7 +1283,7 @@
notListLiteral(lhs, "scalar");
notListLiteral(rhs, "scalar");
result = new ComparisonExpression(lhs, rhs, t.image);
- result.setLocation(template, lhs, rhs);
+ result.setLocation(unboundTemplate, lhs, rhs);
}
]
{
@@ -1333,7 +1322,7 @@
notStringLiteral(lhs, "number");
notStringLiteral(rhs, "number");
result = new ComparisonExpression(lhs, rhs, t.image);
- result.setLocation(template, lhs, rhs);
+ result.setLocation(unboundTemplate, lhs, rhs);
}
]
{
@@ -1380,9 +1369,9 @@
Range range = new Range(lhs, rhs, endType);
if (rhs != null) {
- range.setLocation(template, lhs, rhs);
+ range.setLocation(unboundTemplate, lhs, rhs);
} else {
- range.setLocation(template, lhs, dotDot);
+ range.setLocation(unboundTemplate, lhs, dotDot);
}
result = range;
}
@@ -1409,7 +1398,7 @@
booleanLiteralOnly(lhs);
booleanLiteralOnly(rhs);
result = new AndExpression(lhs, rhs);
- result.setLocation(template, lhs, rhs);
+ result.setLocation(unboundTemplate, lhs, rhs);
lhs = result;
}
)*
@@ -1432,7 +1421,7 @@
booleanLiteralOnly(lhs);
booleanLiteralOnly(rhs);
result = new OrExpression(lhs, rhs);
- result.setLocation(template, lhs, rhs);
+ result.setLocation(unboundTemplate, lhs, rhs);
lhs = result;
}
)*
@@ -1452,7 +1441,7 @@
end = <CLOSE_BRACKET>
{
ListLiteral result = new ListLiteral(values);
- result.setLocation(template, begin, end);
+ result.setLocation(unboundTemplate, begin, end);
return result;
}
}
@@ -1469,9 +1458,9 @@
)
{
String s = t.image;
- Expression result = new NumberLiteral(template.getArithmeticEngine().toNumber(s));
+ Expression result = new NumberLiteral(unboundTemplate.getConfiguration().getArithmeticEngine().toNumber(s));
Token startToken = (op != null) ? op : t;
- result.setLocation(template, startToken, t);
+ result.setLocation(unboundTemplate, startToken, t);
return result;
}
}
@@ -1484,7 +1473,7 @@
t = <ID>
{
Identifier id = new Identifier(t.image);
- id.setLocation(template, t, t);
+ id.setLocation(unboundTemplate, t, t);
return id;
}
}
@@ -1522,7 +1511,7 @@
pe.endColumnNumber = name.endColumn;
throw pe;
}
- result.setLocation(template, dot, name);
+ result.setLocation(unboundTemplate, dot, name);
return result;
}
}
@@ -1575,9 +1564,9 @@
{
DefaultToExpression result = new DefaultToExpression(exp, rhs);
if (rhs == null) {
- result.setLocation(template, exp, t);
+ result.setLocation(unboundTemplate, exp, t);
} else {
- result.setLocation(template, exp, rhs);
+ result.setLocation(unboundTemplate, exp, rhs);
}
return result;
}
@@ -1591,7 +1580,7 @@
t = <EXISTS>
{
ExistsExpression result = new ExistsExpression(exp);
- result.setLocation(template, exp, t);
+ result.setLocation(unboundTemplate, exp, t);
return result;
}
}
@@ -1614,7 +1603,7 @@
pe.endColumnNumber = t.endColumn;
throw pe;
}
- result.setLocation(template, exp, t);
+ result.setLocation(unboundTemplate, exp, t);
return result;
}
}
@@ -1653,7 +1642,7 @@
)
{
if (!Character.isLetter(t.image.charAt(0))) {
- throw new ParseException(t.image + " is not a valid identifier.", template, t);
+ throw new ParseException(t.image + " is not a valid identifier.", unboundTemplate, t);
}
}
)
@@ -1662,7 +1651,7 @@
notStringLiteral(exp, "hash");
notBooleanLiteral(exp, "hash");
Dot dot = new Dot(exp, t.image);
- dot.setLocation(template, exp, t);
+ dot.setLocation(unboundTemplate, exp, t);
return dot;
}
}
@@ -1684,7 +1673,7 @@
notBooleanLiteral(exp, "list or hash");
notNumberLiteral(exp, "list or hash");
DynamicKeyName dkn = new DynamicKeyName(exp, arg);
- dkn.setLocation(template, exp, t);
+ dkn.setLocation(unboundTemplate, exp, t);
return dkn;
}
}
@@ -1704,7 +1693,7 @@
{
args.trimToSize();
MethodCall result = new MethodCall(exp, args);
- result.setLocation(template, exp, end);
+ result.setLocation(unboundTemplate, exp, end);
return result;
}
}
@@ -1736,7 +1725,7 @@
throw pe;
}
StringLiteral result = new StringLiteral(s);
- result.setLocation(template, t, t);
+ result.setLocation(unboundTemplate, t, t);
if (interpolate && !raw) {
if (t.image.indexOf("${") >= 0 || t.image.indexOf("#{") >= 0) result.checkInterpolation();
}
@@ -1756,7 +1745,7 @@
t = <TRUE> { result = new BooleanLiteral(true); }
)
{
- result.setLocation(template, t, t);
+ result.setLocation(unboundTemplate, t, t);
return result;
}
}
@@ -1795,7 +1784,7 @@
end = <CLOSING_CURLY_BRACKET>
{
HashLiteral result = new HashLiteral(keys, values);
- result.setLocation(template, begin, end);
+ result.setLocation(unboundTemplate, begin, end);
return result;
}
}
@@ -1819,7 +1808,7 @@
end = <CLOSING_CURLY_BRACKET>
{
DollarVariable result = new DollarVariable(exp, escapedExpression(exp));
- result.setLocation(template, begin, end);
+ result.setLocation(unboundTemplate, begin, end);
return result;
}
}
@@ -1851,15 +1840,15 @@
if (type != '-') {
switch (type) {
case 'm':
- if (minFrac != -1) throw new ParseException("Invalid formatting string", template, fmt);
+ if (minFrac != -1) throw new ParseException("Invalid formatting string", unboundTemplate, fmt);
minFrac = Integer.parseInt(token);
break;
case 'M':
- if (maxFrac != -1) throw new ParseException("Invalid formatting string", template, fmt);
+ if (maxFrac != -1) throw new ParseException("Invalid formatting string", unboundTemplate, fmt);
maxFrac = Integer.parseInt(token);
break;
default:
- throw new ParseException("Invalid formatting string", template, fmt);
+ throw new ParseException("Invalid formatting string", unboundTemplate, fmt);
}
type = '-';
} else if (token.equals("m")) {
@@ -1870,16 +1859,16 @@
throw new ParseException();
}
} catch (ParseException e) {
- throw new ParseException("Invalid format specifier " + fmt.image, template, fmt);
+ throw new ParseException("Invalid format specifier " + fmt.image, unboundTemplate, fmt);
} catch (NumberFormatException e) {
- throw new ParseException("Invalid number in the format specifier " + fmt.image, template, fmt);
+ throw new ParseException("Invalid number in the format specifier " + fmt.image, unboundTemplate, fmt);
}
}
if (maxFrac == -1) {
if (minFrac == -1) {
throw new ParseException(
- "Invalid format specification, at least one of m and M must be specified!", template, fmt);
+ "Invalid format specification, at least one of m and M must be specified!", unboundTemplate, fmt);
}
maxFrac = minFrac;
} else if (minFrac == -1) {
@@ -1887,16 +1876,16 @@
}
if (minFrac > maxFrac) {
throw new ParseException(
- "Invalid format specification, min cannot be greater than max!", template, fmt);
+ "Invalid format specification, min cannot be greater than max!", unboundTemplate, fmt);
}
if (minFrac > 50 || maxFrac > 50) {// sanity check
- throw new ParseException("Cannot specify more than 50 fraction digits", template, fmt);
+ throw new ParseException("Cannot specify more than 50 fraction digits", unboundTemplate, fmt);
}
result = new NumericalOutput(exp, minFrac, maxFrac);
} else { // if format != null
result = new NumericalOutput(exp);
}
- result.setLocation(template, begin, end);
+ result.setLocation(unboundTemplate, begin, end);
return result;
}
}
@@ -1916,7 +1905,7 @@
block = OptionalBlock()
{
cblock = new ConditionalBlock(condition, block, ConditionalBlock.TYPE_IF);
- cblock.setLocation(template, start, block);
+ cblock.setLocation(unboundTemplate, start, block);
ifBlock = new IfBlock(cblock);
}
(
@@ -1926,7 +1915,7 @@
block = OptionalBlock()
{
cblock = new ConditionalBlock(condition, block, ConditionalBlock.TYPE_ELSE_IF);
- cblock.setLocation(template, t, block);
+ cblock.setLocation(unboundTemplate, t, block);
ifBlock.addBlock(cblock);
}
)*
@@ -1935,13 +1924,13 @@
block = OptionalBlock()
{
cblock = new ConditionalBlock(null, block, ConditionalBlock.TYPE_ELSE);
- cblock.setLocation(template, t, block);
+ cblock.setLocation(unboundTemplate, t, block);
ifBlock.addBlock(cblock);
}
]
end = <END_IF>
{
- ifBlock.setLocation(template, start, end);
+ ifBlock.setLocation(unboundTemplate, start, end);
return ifBlock;
}
}
@@ -1963,7 +1952,7 @@
)
{
AttemptBlock result = new AttemptBlock(block, recoveryBlock);
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -1978,7 +1967,7 @@
block = OptionalBlock()
{
RecoveryBlock result = new RecoveryBlock(block);
- result.setLocation(template, start, block);
+ result.setLocation(unboundTemplate, start, block);
return result;
}
}
@@ -2000,7 +1989,7 @@
{
--loopNesting;
IteratorBlock result = new IteratorBlock(exp, index.image, block, false);
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -2022,7 +2011,7 @@
{
--loopNesting;
IteratorBlock result = new IteratorBlock(exp, index.image, block, true);
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -2042,7 +2031,7 @@
end = LooseDirectiveEnd()
{
VisitNode result = new VisitNode(targetNode, namespaces);
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -2071,7 +2060,7 @@
{
if (end == null) end = start;
RecurseNode result = new RecurseNode(node, namespaces);
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -2084,10 +2073,10 @@
tok = <FALLBACK>
{
if (!inMacro) {
- throw new ParseException("Cannot fall back outside a macro.", template, tok);
+ throw new ParseException("Cannot fall back outside a macro.", unboundTemplate, tok);
}
FallbackInstruction result = new FallbackInstruction();
- result.setLocation(template, tok, tok);
+ result.setLocation(unboundTemplate, tok, tok);
return result;
}
}
@@ -2104,10 +2093,10 @@
{
if (loopNesting < 1 && switchNesting < 1)
{
- throw new ParseException(start.image + " occurred outside a loop or a switch block.", template, start);
+ throw new ParseException(start.image + " occurred outside a loop or a switch block.", unboundTemplate, start);
}
BreakInstruction result = new BreakInstruction();
- result.setLocation(template, start, start);
+ result.setLocation(unboundTemplate, start, start);
return result;
}
}
@@ -2130,20 +2119,20 @@
{
if (inMacro) {
if (exp != null) {
- throw new ParseException("A macro cannot return a value", template, start);
+ throw new ParseException("A macro cannot return a value", unboundTemplate, start);
}
} else if (inFunction) {
if (exp == null) {
- throw new ParseException("A function must return a value", template, start);
+ throw new ParseException("A function must return a value", unboundTemplate, start);
}
} else {
if (exp == null) {
throw new ParseException(
- "A return instruction can only occur inside a macro or function", template, start);
+ "A return instruction can only occur inside a macro or function", unboundTemplate, start);
}
}
ReturnInstruction result = new ReturnInstruction(exp);
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -2161,7 +2150,7 @@
)
{
StopInstruction result = new StopInstruction(exp);
- result.setLocation(template, start, start);
+ result.setLocation(unboundTemplate, start, start);
return result;
}
}
@@ -2178,7 +2167,7 @@
t = <SIMPLE_NESTED>
{
result = new BodyInstruction(null);
- result.setLocation(template, t, t);
+ result.setLocation(unboundTemplate, t, t);
}
)
|
@@ -2188,13 +2177,13 @@
end = LooseDirectiveEnd()
{
result = new BodyInstruction(bodyParameters);
- result.setLocation(template, t, end);
+ result.setLocation(unboundTemplate, t, end);
}
)
)
{
if (!inMacro) {
- throw new ParseException("Cannot use a " + t.image + " instruction outside a macro.", template, t);
+ throw new ParseException("Cannot use a " + t.image + " instruction outside a macro.", unboundTemplate, t);
}
return result;
}
@@ -2208,7 +2197,7 @@
t = <FLUSH>
{
FlushInstruction result = new FlushInstruction();
- result.setLocation(template, t, t);
+ result.setLocation(unboundTemplate, t, t);
return result;
}
}
@@ -2229,7 +2218,7 @@
t = <NOTRIM> { result = new TrimInstruction(false, false); }
)
{
- result.setLocation(template, t, t);
+ result.setLocation(unboundTemplate, t, t);
return result;
}
}
@@ -2256,7 +2245,7 @@
{
scope = Assignment.LOCAL;
if (!inMacro && !inFunction) {
- throw new ParseException("Local variable assigned outside a macro.", template, start);
+ throw new ParseException("Local variable assigned outside a macro.", unboundTemplate, start);
}
}
)
@@ -2272,7 +2261,7 @@
exp = Expression()
{
ass = new Assignment(varName, exp, scope);
- ass.setLocation(template, nameExp, exp);
+ ass.setLocation(unboundTemplate, nameExp, exp);
assignments.add(ass);
}
(
@@ -2288,7 +2277,7 @@
exp = Expression()
{
ass = new Assignment(varName, exp, scope);
- ass.setLocation(template, nameExp, exp);
+ ass.setLocation(unboundTemplate, nameExp, exp);
assignments.add(ass);
}
)*
@@ -2297,7 +2286,7 @@
nsExp = Expression()
{
if (scope != Assignment.NAMESPACE) {
- throw new ParseException("Cannot assign to namespace here.", template, id);
+ throw new ParseException("Cannot assign to namespace here.", unboundTemplate, id);
}
}
]
@@ -2308,7 +2297,7 @@
ai.addAssignment((Assignment) assignments.get(i));
}
ai.setNamespaceExp(nsExp);
- ai.setLocation(template, start, end);
+ ai.setLocation(unboundTemplate, start, end);
return ai;
}
)
@@ -2319,7 +2308,7 @@
nsExp = Expression()
{
if (scope != Assignment.NAMESPACE) {
- throw new ParseException("Cannot assign to namespace here.", template, id);
+ throw new ParseException("Cannot assign to namespace here.", unboundTemplate, id);
}
}
]
@@ -2329,26 +2318,26 @@
end = <END_LOCAL>
{
if (scope != Assignment.LOCAL) {
- throw new ParseException("Mismatched assignment tags.", template, end);
+ throw new ParseException("Mismatched assignment tags.", unboundTemplate, end);
}
}
|
end = <END_ASSIGN>
{
if (scope != Assignment.NAMESPACE) {
- throw new ParseException("Mismatched assignment tags.", template, end);
+ throw new ParseException("Mismatched assignment tags.", unboundTemplate, end);
}
}
|
end = <END_GLOBAL>
{
if (scope != Assignment.GLOBAL) throw new ParseException(
- "Mismatched assignment tags", template, end);
+ "Mismatched assignment tags", unboundTemplate, end);
}
)
{
BlockAssignment ba = new BlockAssignment(block, varName, scope, nsExp);
- ba.setLocation(template, start, end);
+ ba.setLocation(unboundTemplate, start, end);
return ba;
}
)
@@ -2387,14 +2376,14 @@
: " Supporting camelCase parameter names is planned for FreeMarker 2.4.0; "
+ "check if an update is available, and if it indeed supports camel "
+ "case."),
- template, att);
+ unboundTemplate, att);
}
}
)*
end = LooseDirectiveEnd()
{
- Include result = new Include(template, nameExp, encodingExp, parseExp, ignoreMissingExp);
- result.setLocation(template, start, end);
+ Include result = new Include(unboundTemplate, nameExp, encodingExp, parseExp, ignoreMissingExp);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -2411,9 +2400,9 @@
ns = <ID>
end = LooseDirectiveEnd()
{
- LibraryLoad result = new LibraryLoad(template, nameExp, ns.image);
- result.setLocation(template, start, end);
- template.addImport(result);
+ LibraryLoad result = new LibraryLoad(unboundTemplate, nameExp, ns.image);
+ result.setLocation(unboundTemplate, start, end);
+ unboundTemplate.addImport(result);
return result;
}
}
@@ -2440,7 +2429,7 @@
)
{
if (inMacro || inFunction) {
- throw new ParseException("Macros cannot be nested.", template, start);
+ throw new ParseException("Macros cannot be nested.", unboundTemplate, start);
}
if (isFunction) inFunction = true; else inMacro = true;
}
@@ -2469,13 +2458,13 @@
if (catchAll != null) {
throw new ParseException(
"There may only be one \"catch-all\" parameter in a macro declaration, and it must be the last parameter.",
- template, arg);
+ unboundTemplate, arg);
}
if (isCatchAll) {
if (defValue != null) {
throw new ParseException(
"\"Catch-all\" macro parameter may not have a default value.",
- template, arg);
+ unboundTemplate, arg);
}
catchAll = arg.image;
} else {
@@ -2484,7 +2473,7 @@
throw new ParseException(
"In a macro declaration, parameters without a default value "
+ "must all occur before the parameters with default values.",
- template, arg);
+ unboundTemplate, arg);
}
args.put(arg.image, defValue);
}
@@ -2496,19 +2485,19 @@
(
end = <END_MACRO>
{
- if(isFunction) throw new ParseException("Expected function end tag here.", template, start);
+ if(isFunction) throw new ParseException("Expected function end tag here.", unboundTemplate, start);
}
|
end = <END_FUNCTION>
{
- if(!isFunction) throw new ParseException("Expected macro end tag here.", template, start);
+ if(!isFunction) throw new ParseException("Expected macro end tag here.", unboundTemplate, start);
}
)
{
inMacro = inFunction = false;
- Macro result = new Macro(name, argNames, args, catchAll, isFunction, block);
- result.setLocation(template, start, end);
- template.addMacro(result);
+ UnboundCallable result = new UnboundCallable(name, argNames, args, catchAll, isFunction, block);
+ result.setLocation(unboundTemplate, start, end);
+ unboundTemplate.addUnboundCallable(result);
return result;
}
}
@@ -2524,7 +2513,7 @@
end = <END_COMPRESS>
{
CompressedBlock cb = new CompressedBlock(block);
- cb.setLocation(template, start, end);
+ cb.setLocation(unboundTemplate, start, end);
return cb;
}
}
@@ -2575,9 +2564,9 @@
s = s.substring(0, s.length() -1).trim();
if (s.length() >0 && !s.equals(directiveName)) {
if (directiveName == null) {
- throw new ParseException("Expecting </@>", template, end);
+ throw new ParseException("Expecting </@>", unboundTemplate, end);
} else {
- throw new ParseException("Expecting </@> or </@" + directiveName + ">", template, end);
+ throw new ParseException("Expecting </@> or </@" + directiveName + ">", unboundTemplate, end);
}
}
}
@@ -2587,7 +2576,7 @@
TemplateElement result = (positionalArgs != null)
? new UnifiedCall(exp, positionalArgs, nestedBlock, bodyParameters)
: new UnifiedCall(exp, namedArgs, nestedBlock, bodyParameters);
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -2624,7 +2613,7 @@
result = new UnifiedCall(new Identifier(macroName), namedArgs, null, null);
}
result.legacySyntax = true;
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -2687,7 +2676,7 @@
end = UnparsedContent(start, buf)
{
Comment result = new Comment(buf.toString());
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -2702,7 +2691,7 @@
end = UnparsedContent(start, buf)
{
TextBlock result = new TextBlock(buf.toString(), true);
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -2738,7 +2727,7 @@
)
{
TransformBlock result = new TransformBlock(exp, args, content);
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -2766,7 +2755,7 @@
if (caseIns.condition == null) {
if (defaultFound) {
throw new ParseException(
- "You can only have one default case in a switch statement", template, start);
+ "You can only have one default case in a switch statement", unboundTemplate, start);
}
defaultFound = true;
}
@@ -2777,7 +2766,7 @@
end = <END_SWITCH>
{
--switchNesting;
- switchBlock.setLocation(template, start, end);
+ switchBlock.setLocation(unboundTemplate, start, end);
return switchBlock;
}
}
@@ -2798,7 +2787,7 @@
block = OptionalBlock()
{
Case result = new Case(exp, block);
- result.setLocation(template, start, block);
+ result.setLocation(unboundTemplate, start, block);
return result;
}
}
@@ -2826,7 +2815,7 @@
}
end = <END_ESCAPE>
{
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -2840,7 +2829,7 @@
start = <NOESCAPE>
{
if (escapes.isEmpty()) {
- throw new ParseException("noescape with no matching escape encountered.", template, start);
+ throw new ParseException("noescape with no matching escape encountered.", unboundTemplate, start);
}
Object escape = escapes.removeFirst();
}
@@ -2849,7 +2838,7 @@
{
escapes.addFirst(escape);
NoEscapeBlock result = new NoEscapeBlock(content);
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -2885,7 +2874,7 @@
end = LooseDirectiveEnd()
{
PropertySetting result = new PropertySetting(key.image, value);
- result.setLocation(template, start, end);
+ result.setLocation(unboundTemplate, start, end);
return result;
}
}
@@ -2989,7 +2978,7 @@
if (stripText && mixedContentNesting == 1) return TextBlock.EMPTY_BLOCK;
TextBlock result = new TextBlock(buf.toString(), false);
- result.setLocation(template, start, t);
+ result.setLocation(unboundTemplate, start, t);
return result;
}
}
@@ -3014,8 +3003,8 @@
{
buf.setLength(buf.length() - t.image.length());
if (!t.image.endsWith(";")
- && _TemplateAPI.getTemplateLanguageVersionAsInt(template) >= _TemplateAPI.VERSION_INT_2_3_21) {
- throw new ParseException("Unclosed \"" + start.image + "\"", template, start);
+ && unboundTemplate.getTemplateLanguageVersion().intValue() >= _TemplateAPI.VERSION_INT_2_3_21) {
+ throw new ParseException("Unclosed \"" + start.image + "\"", unboundTemplate, start);
}
return t;
}
@@ -3048,7 +3037,7 @@
)+
{
mixedContentNesting--;
- mixedContent.setLocation(template, begin, elem);
+ mixedContent.setLocation(unboundTemplate, begin, elem);
return mixedContent;
}
}
@@ -3079,7 +3068,7 @@
}
)+
{
- nodes.setLocation(template, begin, elem);
+ nodes.setLocation(unboundTemplate, begin, elem);
return nodes;
}
}
@@ -3136,14 +3125,13 @@
vs = ((TemplateScalarModel) exp).getAsString();
} catch (TemplateModelException tme) {}
}
- if (template != null) {
+ if (unboundTemplate != null) {
if (ks.equalsIgnoreCase("encoding")) {
if (vs == null) {
throw new ParseException("Expecting an encoding string.", exp);
}
- String encoding = template.getEncoding();
- if (encoding != null && !encoding.equalsIgnoreCase(vs)) {
- throw new Template.WrongEncodingException(vs, encoding);
+ if (assumedEncoding != null && !assumedEncoding.equalsIgnoreCase(vs)) {
+ throw new Template.WrongEncodingException(vs, assumedEncoding);
}
} else if (ks.equalsIgnoreCase("STRIP_WHITESPACE")) {
this.stripWhitespace = getBoolean(exp);
@@ -3166,7 +3154,7 @@
}
String nsURI = ((TemplateScalarModel) valueModel).getAsString();
try {
- template.addPrefixNSMapping(prefix, nsURI);
+ unboundTemplate.addPrefixToNamespaceURIMapping(prefix, nsURI);
} catch (IllegalArgumentException iae) {
throw new ParseException(iae.getMessage(), exp);
}
@@ -3183,7 +3171,7 @@
for (TemplateModelIterator it = keys.iterator(); it.hasNext();) {
String attName = ((TemplateScalarModel) it.next()).getAsString();
Object attValue = DeepUnwrap.unwrap(attributeMap.get(attName));
- template.setCustomAttribute(attName, attValue);
+ unboundTemplate.setCustomAttribute(attName, attValue);
}
} catch (TemplateModelException tme) {
}
@@ -3207,7 +3195,7 @@
: ". Supporting camelCase parameter names is planned for FreeMarker 2.4.0; "
+ "check if an update is available, and if it indeed supports camel "
+ "case. Until that, use " + correctName + " instead."),
- template, key);
+ unboundTemplate, key);
}
}
}
diff --git a/src/test/java/freemarker/core/TestEnvironmentGetTemplateVariants.java b/src/test/java/freemarker/core/TestEnvironmentGetTemplateVariants.java
index 6bb00b9..6e4f946 100644
--- a/src/test/java/freemarker/core/TestEnvironmentGetTemplateVariants.java
+++ b/src/test/java/freemarker/core/TestEnvironmentGetTemplateVariants.java
@@ -120,6 +120,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 =
@@ -129,31 +142,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"
@@ -162,19 +175,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));
@@ -194,6 +207,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 version2321) {
Configuration cfg = new Configuration(version2321);
@@ -209,8 +231,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 1394f1b..4ec2de0 100644
--- a/src/test/java/freemarker/template/CustomAttributeTest.java
+++ b/src/test/java/freemarker/template/CustomAttributeTest.java
@@ -19,7 +19,6 @@
import static org.junit.Assert.*;
import java.math.BigDecimal;
-import java.util.Arrays;
import org.junit.Test;
@@ -62,17 +61,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));
@@ -102,7 +101,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));
@@ -112,7 +111,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', "
@@ -120,20 +119,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', "
@@ -141,21 +140,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
@@ -202,6 +208,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());
@@ -212,13 +270,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/MistakenlyPublicImportAPIsTest.java b/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java
index 6eb8236..6861e3b 100644
--- a/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java
+++ b/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java
@@ -59,7 +59,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());
}
}
@@ -89,14 +89,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/resources/freemarker/core/ast-1.ast b/src/test/resources/freemarker/core/ast-1.ast
index 617f20a..c656bd7 100644
--- a/src/test/resources/freemarker/core/ast-1.ast
+++ b/src/test/resources/freemarker/core/ast-1.ast
@@ -95,7 +95,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
@@ -112,7 +112,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/test/templatesuite/templates/specialvars.ftl b/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl
index 1358ff3..39a20f4 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl
@@ -17,4 +17,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 />
\ No newline at end of file