http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java index e9b2c4e..b67d54d 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTElement.java @@ -389,7 +389,7 @@ abstract class ASTElement extends ASTNode { private ASTElement getFirstLeaf() { ASTElement te = this; - while (!te.isLeaf() && !(te instanceof ASTDirMacro) && !(te instanceof ASTDirCapturingAssignment)) { + while (!te.isLeaf() && !(te instanceof ASTDirMacroOrFunction) && !(te instanceof ASTDirCapturingAssignment)) { // A macro or macro invocation is treated as a leaf here for special reasons te = te.getFirstChild(); } @@ -398,7 +398,7 @@ abstract class ASTElement extends ASTNode { private ASTElement getLastLeaf() { ASTElement te = this; - while (!te.isLeaf() && !(te instanceof ASTDirMacro) && !(te instanceof ASTDirCapturingAssignment)) { + while (!te.isLeaf() && !(te instanceof ASTDirMacroOrFunction) && !(te instanceof ASTDirCapturingAssignment)) { // A macro or macro invocation is treated as a leaf here for special reasons te = te.getLastChild(); }
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java index 74ae72f..ab50470 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java @@ -145,7 +145,7 @@ final class ASTExpBuiltInVariable extends ASTExpression { return env.getGlobalVariables(); } if (name == LOCALS) { - ASTDirMacro.Context ctx = env.getCurrentMacroContext(); + ASTDirMacroOrFunction.Context ctx = env.getCurrentMacroContext(); return ctx == null ? null : ctx.getLocals(); } if (name == DATA_MODEL) { @@ -173,7 +173,7 @@ final class ASTExpBuiltInVariable extends ASTExpression { return SimpleScalar.newInstanceOrNull(env.getCurrentTemplate().getLookupName()); } if (name == PASS) { - return ASTDirMacro.DO_NOTHING_MACRO; + return ASTDirMacroOrFunction.PASS_MACRO; } if (name == OUTPUT_ENCODING) { Charset encoding = env.getOutputEncoding(); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java index 1612a79..e5dc679 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java @@ -81,11 +81,11 @@ final class ASTExpListLiteral extends ASTExpression { /** * For {@link TemplateMethodModelEx} calls, returns the list of arguments as {@link TemplateModel}-s. */ - List/*<TemplateModel>*/ getModelList(Environment env) throws TemplateException { + List<TemplateModel> getModelList(Environment env) throws TemplateException { int size = items.size(); switch(size) { case 0: { - return Collections.EMPTY_LIST; + return Collections.emptyList(); } case 1: { return Collections.singletonList(((ASTExpression) items.get(0)).eval(env)); @@ -101,6 +101,10 @@ final class ASTExpListLiteral extends ASTExpression { } } + public int size() { + return items.size(); + } + @Override public String getCanonicalForm() { StringBuilder buf = new StringBuilder("["); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java index 4f51904..dc8f3ff 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java @@ -28,16 +28,21 @@ import java.io.Writer; import java.util.ArrayList; import java.util.List; +import org.apache.freemarker.core.model.ArgumentArrayLayout; +import org.apache.freemarker.core.model.Constants; +import org.apache.freemarker.core.model.TemplateFunctionModel; import org.apache.freemarker.core.model.TemplateMethodModel; import org.apache.freemarker.core.model.TemplateMethodModelEx; import org.apache.freemarker.core.model.TemplateModel; -import org.apache.freemarker.core.util._NullWriter; +import org.apache.freemarker.core.model.TemplateSequenceModel; +import org.apache.freemarker.core.util.CommonSupplier; +import org.apache.freemarker.core.util.FTLUtil; /** * AST expression node: {@code exp(args)}. */ -final class ASTExpMethodCall extends ASTExpression { +final class ASTExpMethodCall extends ASTExpression implements CallPlace { private final ASTExpression target; private final ASTExpListLiteral arguments; @@ -62,23 +67,55 @@ final class ASTExpMethodCall extends ASTExpression { : arguments.getValueList(env); Object result = targetMethod.exec(argumentStrings); return env.getObjectWrapper().wrap(result); - } else if (targetModel instanceof ASTDirMacro) { - ASTDirMacro func = (ASTDirMacro) targetModel; - env.setLastReturnValue(null); - if (!func.isFunction()) { - throw new _MiscTemplateException(env, "A macro cannot be called in an expression. (Functions can be.)"); + } else if (targetModel instanceof TemplateFunctionModel) { + TemplateFunctionModel func = (TemplateFunctionModel) targetModel; + + ArgumentArrayLayout arrayLayout = func.getArgumentArrayLayout(); + + // TODO [FM3] This is just temporary, until we support named args. Then the logic in ASTDynamicTopLevelCall + // should be reused. + + int posVarargsLength; + int callArgCnt = arguments.size(); + int predefPosArgCnt = arrayLayout.getPredefinedPositionalArgumentCount(); + int posVarargsIdx = arrayLayout.getPositionalVarargsArgumentIndex(); + if (callArgCnt > predefPosArgCnt) { + if (posVarargsIdx == -1) { + throw new _MiscTemplateException(env, + "Too many arguments; the target ", FTLUtil.getCallableTypeName(func), + " has ", predefPosArgCnt, " arguments."); + } } - Writer prevOut = env.getOut(); - try { - env.setOut(_NullWriter.INSTANCE); - env.invoke(func, null, arguments.items, null, null); - } catch (IOException e) { - // Should not occur - throw new TemplateException("Unexpected exception during function execution", e, env); - } finally { - env.setOut(prevOut); + + List<TemplateModel> callArgList = arguments.getModelList(env); + + TemplateModel[] args = new TemplateModel[arrayLayout.getTotalLength()]; + int callPredefArgCnt = Math.min(callArgCnt, predefPosArgCnt); + for (int argIdx = 0; argIdx < callPredefArgCnt; argIdx++) { + args[argIdx] = callArgList.get(argIdx); } - return env.getLastReturnValue(); + + if (posVarargsIdx != -1) { + TemplateSequenceModel varargsSeq; + posVarargsLength = callArgCnt - predefPosArgCnt; + if (posVarargsLength <= 0) { + varargsSeq = Constants.EMPTY_SEQUENCE; + } else { + NativeSequence nativeSeq = new NativeSequence(posVarargsLength); + varargsSeq = nativeSeq; + for (int posVarargIdx = 0; posVarargIdx < posVarargsLength; posVarargIdx++) { + nativeSeq.add(callArgList.get(predefPosArgCnt + posVarargIdx)); + } + } + args[posVarargsIdx] = varargsSeq; + } + + int namedVarargsArgIdx = arrayLayout.getNamedVarargsArgumentIndex(); + if (namedVarargsArgIdx != -1) { + args[namedVarargsArgIdx] = Constants.EMPTY_HASH; + } + + return func.execute(args, this, env); } else { throw new NonMethodException(target, targetModel, env); } @@ -144,4 +181,50 @@ final class ASTExpMethodCall extends ASTExpression { } } + // ----------------------------------------------------------------------------------------------------------------- + // CallPlace API + + @Override + public boolean hasNestedContent() { + return false; + } + + @Override + public int getNestedContentParameterCount() { + return 0; + } + + @Override + public void executeNestedContent(TemplateModel[] nestedContentArgs, Writer out, Environment env) + throws TemplateException, IOException { + // Do nothing + } + + @Override + public Object getOrCreateCustomData(Object providerIdentity, CommonSupplier<?> supplier) + throws CallPlaceCustomDataInitializationException { + throw new UnsupportedOperationException("Expression call places don't store custom data"); + } + + @Override + public boolean isCustomDataSupported() { + return false; + } + + @Override + public boolean isNestedOutputCacheable() { + return false; + } + + @Override + public int getFirstTargetJavaParameterTypeIndex() { + // TODO [FM3] + return -1; + } + + @Override + public Class<?> getTargetJavaParameterType(int argIndex) { + // TODO [FM3] + return null; + } } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/ASTStaticText.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTStaticText.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTStaticText.java index 1ed4d2d..eceb412 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTStaticText.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTStaticText.java @@ -370,7 +370,7 @@ final class ASTStaticText extends ASTElement { private boolean nonOutputtingType(ASTElement element) { - return (element instanceof ASTDirMacro || + return (element instanceof ASTDirMacroOrFunction || element instanceof ASTDirAssignment || element instanceof ASTDirAssignmentsContainer || element instanceof ASTDirSetting || http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMultipleTypes.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMultipleTypes.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMultipleTypes.java index 467a2f4..74f7c15 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMultipleTypes.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForMultipleTypes.java @@ -312,8 +312,8 @@ class BuiltInsForMultipleTypes { TemplateModel _eval(Environment env) throws TemplateException { TemplateModel tm = target.eval(env); target.assertNonNull(tm, env); - // WRONG: it also had to check ASTDirMacro.isFunction() - return (tm instanceof ASTDirMacro || tm instanceof TemplateDirectiveModel) ? + // WRONG: it also had to check ASTDirMacroOrFunction.isFunction() + return (tm instanceof ASTDirMacroOrFunction || tm instanceof TemplateDirectiveModel) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE; } } @@ -360,8 +360,7 @@ class BuiltInsForMultipleTypes { TemplateModel _eval(Environment env) throws TemplateException { TemplateModel tm = target.eval(env); target.assertNonNull(tm, env); - // WRONG: it also had to check ASTDirMacro.isFunction() - return (tm instanceof ASTDirMacro) ? + return (tm instanceof Environment.TemplateLanguageDirective) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE; } } @@ -430,14 +429,13 @@ class BuiltInsForMultipleTypes { @Override TemplateModel _eval(Environment env) throws TemplateException { TemplateModel tm = target.eval(env); - if (!(tm instanceof ASTDirMacro)) { + if (!(tm instanceof Environment.TemplateLanguageCallable)) { throw new UnexpectedTypeException( target, tm, - "macro or function", new Class[] { ASTDirMacro.class }, + "macro or function", new Class[] { Environment.TemplateLanguageCallable.class }, env); - } else { - return env.getMacroNamespace((ASTDirMacro) tm); } + return ((Environment.TemplateLanguageCallable) tm).getNamespace(); } } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java index d9ad1bf..2d50556 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java @@ -25,7 +25,6 @@ import java.io.Writer; import java.util.List; import org.apache.freemarker.core.model.ArgumentArrayLayout; -import org.apache.freemarker.core.model.CallPlace; import org.apache.freemarker.core.model.ObjectWrapper; import org.apache.freemarker.core.model.TemplateBooleanModel; import org.apache.freemarker.core.model.TemplateDirectiveModel; http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/CallPlace.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/CallPlace.java b/freemarker-core/src/main/java/org/apache/freemarker/core/CallPlace.java new file mode 100644 index 0000000..628c9ec --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/CallPlace.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core; + +import java.io.IOException; +import java.io.Writer; +import java.util.IdentityHashMap; + +import org.apache.freemarker.core.model.TemplateDirectiveModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.util.CommonSupplier; + +/** + * The place (in a template, usually) from where a directive (like a macro) or function was called; + * <b>Do not implement this interface yourself</b>, as new methods may be added any time! Only FreeMarker itself + * should provide implementations. In case you have to call something from outside a template, use + * {@link NonTemplateCallPlace#INSTANCE}. + */ +public interface CallPlace { + + // ------------------------------------------------------------------------------------------------------------- + // Nested content: + + /** + * Tells if there's a non-zero-length nested content. This is {@code false} for {@code <@foo />} or + * {@code <@foo></@foo>} or for calls inside expressions (i.e., for function calls). + */ + boolean hasNestedContent(); + + /** + * The number of nested content parameters in this call (like 2 in {@code <@foo xs; k, v>...</@>}). If you want the + * caller to specify a fixed number of nested content parameters, then this is not interesting for you, and just + * pass an array of that length to {@link #executeNestedContent(TemplateModel[], Writer, Environment)}. If, however, + * you want to allow the caller to declare less parameters, then this is how you know how much parameters you should + * calculate and pass to {@link #executeNestedContent(TemplateModel[], Writer, Environment)}. + */ + int getNestedContentParameterCount(); + + /** + * Executes the nested content; it there's none, it just does nothing. + * + * @param nestedContentArgs + * The nested content parameter values to pass to the nested content (as in {@code <@foo bar; i, j>${i}, + * ${j}</@foo>} there are 2 such parameters, whose value you set here), or {@code null} if there's none. + * This array must be {@link #getNestedContentParameterCount()} long, or else FreeMarker will throw an + * {@link TemplateException} with a descriptive error message that tells to user that they need to declare + * that many nested content parameters as the length of this array. If you want to allow the caller to not + * declare some of the nested content parameters, then you have to make this array shorter according to + * {@link #getNestedContentParameterCount()}. + */ + void executeNestedContent(TemplateModel[] nestedContentArgs, Writer out, Environment env) + throws TemplateException, IOException; + + // ------------------------------------------------------------------------------------------------------------- + // Source code info: + + /** + * The template that contains this call; {@code null} if the call is not from a template (but directly from + * user Java code, for example). + */ + Template getTemplate(); + + /** + * The 1-based column number of the first character of the directive call in the template source code, or -1 if it's + * not known. + */ + int getBeginColumn(); + + /** + * The 1-based line number of the first character of the directive call in the template source code, or -1 if it's + * not known. + */ + int getBeginLine(); + + /** + * The 1-based column number of the last character of the directive call in the template source code, or -1 if it's + * not known. If the directive has an end-tag ({@code </@...>}), then it points to the last character of that. + */ + int getEndColumn(); + + /** + * The 1-based line number of the last character of the directive call in the template source code, or -1 if it's + * not known. If the directive has an end-tag ({@code </@...>}), then it points to the last character of that. + */ + int getEndLine(); + + // ------------------------------------------------------------------------------------------------------------- + // Caching: + + /** + * Returns the custom data, or if that's {@code null}, then it creates and stores it in an atomic operation then + * returns it. This method is thread-safe, however, it doesn't ensure thread safe (like synchronized) access to the + * custom data itself. Be sure that the custom data only depends on things that get their final value during + * template parsing, not on runtime settings. + * <p> + * This method will block other calls while the {@code supplier} is executing, thus, the object will be + * <em>usually</em> created only once, even if multiple threads request the value when it's still {@code null}. It + * doesn't stand though when {@code providerIdentity} mismatches occur (see later). Furthermore, then it's also + * possible that multiple objects created by the same {@link CommonSupplier} will be in use on the same time, + * because of directive executions already running in parallel, and because of memory synchronization delays + * (hardware dependent) between the threads. + * + * @param providerIdentity + * This is usually the class of the {@link TemplateDirectiveModel} that creates (and uses) the custom data, + * or if you are using your own class for the custom data object (as opposed to a class from some more + * generic API), then that class. This is needed as the same call place might calls different directives + * depending on runtime conditions, and so it must be ensured that these directives won't accidentally read + * each other's custom data, ending up with class cast exceptions or worse. In the current implementation, + * if there's a {@code providerIdentity} mismatch (means, the {@code providerIdentity} object used when the + * custom data was last set isn't the exactly same object as the one provided with the parameter now), the + * previous custom data will be just ignored as if it was {@code null}. So if multiple directives that use + * the custom data feature use the same call place, the caching of the custom data can be inefficient, as + * they will keep overwriting each other's custom data. (In a more generic implementation the {@code + * providerIdentity} would be a key in a {@link IdentityHashMap}, but then this feature would be slower, + * while {@code providerIdentity} mismatches aren't occurring in most applications.) + * @param supplier + * Called when the custom data wasn't yet set, to invoke its initial value. If this parameter is {@code + * null} and the custom data wasn't set yet, then {@code null} will be returned. The returned value of + * {@link CommonSupplier#get()} can be any kind of object, but can't be {@code null}. + * + * @return The current custom data object, or possibly {@code null} if there was no {@link CommonSupplier} provided. + * + * @throws CallPlaceCustomDataInitializationException + * If the {@link CommonSupplier} had to be invoked but failed. + * @throws UnsupportedOperationException + * If this call place doesn't support storing custom date; see {@link #isCustomDataSupported()}. + */ + Object getOrCreateCustomData(Object providerIdentity, CommonSupplier<?> supplier) + throws CallPlaceCustomDataInitializationException; + + /** + * Tells if this call place supports storing custom data. As of this writing, only top-level (i.e., outside + * expression) directive calls do. + */ + boolean isCustomDataSupported(); + + /** + * Tells if the output of the nested content can be safely cached, as it only depends on the template content (not + * on variable values and such) and has no side-effects (other than writing to the output). Examples of cases that + * give {@code false}: {@code <@foo>Name: } <tt>${name}</tt>{@code</@foo>}, {@code <@foo>Name: <#if + * condition>bar</#if></@foo>}. Examples of cases that give {@code true}: {@code <@foo>Name: Joe</@foo>}, {@code + * <@foo />}. Note that we get {@code true} for no nested content, because that's equivalent to 0-length nested + * content. + * <p> + * This method returns a pessimistic result. For example, if it sees a custom directive call, it can't know what it + * does, so it will assume that it's not cacheable. + */ + boolean isNestedOutputCacheable(); + + // ------------------------------------------------------------------------------------------------------------- + // Overloaded method selection: + + /** + * The index of the first item in the argument array passed to {@code execute} that has this information. + * Used solely for speed optimization (to minimize the number of + * {@link #getTargetJavaParameterType(int)} calls). + * + * @return -1 if no parameter has type hint + */ + int getFirstTargetJavaParameterTypeIndex(); + + /** + * The type of the parameter in the target Java method; used for overloaded Java method selection. This optional + * information is specified by the template author in the source code (the syntax is not yet decided when I write + * this). + * + * @param argIndex + * The index of the argument in the argument array + * + * @return The desired Java type or {@code null} if this information wasn't specified in the template. + * + * @throws IndexOutOfBoundsException + * Might be thrown if {@code argIndex} is an invalid index according the number of arguments on the call + * site. Some implementations may just return {@code null} in that case though. + */ + Class<?> getTargetJavaParameterType(int argIndex); + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/CallPlaceCustomDataInitializationException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/CallPlaceCustomDataInitializationException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/CallPlaceCustomDataInitializationException.java index 0033afd..9c4d22e 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/CallPlaceCustomDataInitializationException.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/CallPlaceCustomDataInitializationException.java @@ -19,13 +19,12 @@ package org.apache.freemarker.core; -import org.apache.freemarker.core.model.CallPlace; import org.apache.freemarker.core.util.CommonSupplier; /** * Thrown by {@link CallPlace#getOrCreateCustomData(Object, CommonSupplier)} */ -public class CallPlaceCustomDataInitializationException extends Exception { +public class CallPlaceCustomDataInitializationException extends RuntimeException { public CallPlaceCustomDataInitializationException(String message, Throwable cause) { super(message, cause); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java index af798b4..25ea39d 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java @@ -43,10 +43,13 @@ import java.util.Set; import java.util.TimeZone; import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.model.ArgumentArrayLayout; import org.apache.freemarker.core.model.ObjectWrapper; +import org.apache.freemarker.core.model.TemplateCallableModel; import org.apache.freemarker.core.model.TemplateCollectionModel; import org.apache.freemarker.core.model.TemplateDateModel; import org.apache.freemarker.core.model.TemplateDirectiveModel; +import org.apache.freemarker.core.model.TemplateFunctionModel; import org.apache.freemarker.core.model.TemplateHashModel; import org.apache.freemarker.core.model.TemplateHashModelEx; import org.apache.freemarker.core.model.TemplateModel; @@ -164,7 +167,7 @@ public final class Environment extends MutableProcessingConfiguration<Environmen private Collator cachedCollator; private Writer out; - private ASTDirMacro.Context currentMacroContext; + private ASTDirMacroOrFunction.Context currentMacroContext; private LocalContextStack localContextStack; private final Template mainTemplate; private final Namespace mainNamespace; @@ -175,7 +178,6 @@ public final class Environment extends MutableProcessingConfiguration<Environmen private Throwable lastThrowable; private TemplateModel lastReturnValue; - private HashMap macroToNamespaceLookup = new HashMap(); private TemplateNodeModel currentVisitorNode; private TemplateSequenceModel nodeNamespaces; @@ -517,28 +519,25 @@ public final class Environment extends MutableProcessingConfiguration<Environmen } /** - * Used for {@code #nested}. + * Used to execute the nested content of a macro call during a macro execution. It's not enough to simply call + * {@link CallPlace#executeNestedContent(TemplateModel[], Writer, Environment)} in such case, because an executing + * macro modifies the template language environment for its purposes, and the nested content expects that to be + * like before the macro invocation. So things has to be temporarily restored. */ - void invokeNestedContent(ASTDirNested.Context bodyCtx) throws TemplateException, IOException { - ASTDirMacro.Context invokingMacroContext = getCurrentMacroContext(); - LocalContextStack prevLocalContextStack = localContextStack; - ASTElement[] nestedContentBuffer = invokingMacroContext.nestedContentBuffer; - if (nestedContentBuffer != null) { + void executeNestedContentOfMacro(TemplateModel[] nestedContentParamValues) + throws TemplateException, IOException { + ASTDirMacroOrFunction.Context invokingMacroContext = getCurrentMacroContext(); + CallPlace callPlace = invokingMacroContext.callPlace; + if (callPlace.hasNestedContent()) { currentMacroContext = invokingMacroContext.prevMacroContext; currentNamespace = invokingMacroContext.nestedContentNamespace; - + LocalContextStack prevLocalContextStack = localContextStack; localContextStack = invokingMacroContext.prevLocalContextStack; - if (invokingMacroContext.nestedContentParameterNames != null) { - pushLocalContext(bodyCtx); - } try { - visit(nestedContentBuffer); + callPlace.executeNestedContent(nestedContentParamValues, out, this); } finally { - if (invokingMacroContext.nestedContentParameterNames != null) { - popLocalContext(); - } currentMacroContext = invokingMacroContext; - currentNamespace = getMacroNamespace(invokingMacroContext.getMacro()); + currentNamespace = invokingMacroContext.callable.namespace; localContextStack = prevLocalContextStack; } } @@ -580,18 +579,20 @@ public final class Environment extends MutableProcessingConfiguration<Environmen nodeNamespaces = namespaces; } try { - TemplateModel macroOrDirective = getNodeProcessor(node); - if (macroOrDirective instanceof ASTDirMacro) { - invoke((ASTDirMacro) macroOrDirective, null, null, null, null); - } else if (macroOrDirective instanceof TemplateDirectiveModel) { - ((TemplateDirectiveModel) macroOrDirective).execute( - null, null /* TODO [FM3][CF] */, out, this); - } else { + TemplateDirectiveModel nodeProcessor = getNodeProcessor(node); + if (nodeProcessor != null) { + _TemplateCallableModelUtils.executeWith0Arguments( + nodeProcessor, NonTemplateCallPlace.INSTANCE, out, this); + } else if (nodeProcessor == null) { String nodeType = node.getNodeType(); if (nodeType != null) { + // TODO [FM3] We are supposed to be o.a.f.dom unaware in the core, plus these types can mean + // something else with another wrapper. So we should encode the default behavior into the + // // TemplateNodeModel somehow. + // If the node's type is 'text', we just output it. if ((nodeType.equals("text") && node instanceof TemplateScalarModel)) { - out.write(((TemplateScalarModel) node).getAsString()); + out.write(((TemplateScalarModel) node).getAsString()); // TODO [FM3] Escaping? } else if (nodeType.equals("document")) { recurse(node, namespaces); } @@ -637,134 +638,23 @@ public final class Environment extends MutableProcessingConfiguration<Environmen } void fallback() throws TemplateException, IOException { - TemplateModel macroOrDirective = getNodeProcessor(currentNodeName, currentNodeNS, nodeNamespaceIndex); - if (macroOrDirective instanceof ASTDirMacro) { - invoke((ASTDirMacro) macroOrDirective, null, null, null, null); - } else if (macroOrDirective instanceof TemplateDirectiveModel) { - ((TemplateDirectiveModel) macroOrDirective).execute( - null, null /* TODO [FM3][CF] */, out, this); - } - } - - /** - * Calls the macro or function with the given arguments and nested block. - */ - void invoke(ASTDirMacro macro, - Map<String, ASTExpression> namedArgs, List<ASTExpression> positionalArgs, - List<String> bodyParameterNames, ASTElement[] childBuffer) throws TemplateException, IOException { - if (macro == ASTDirMacro.DO_NOTHING_MACRO) { - return; - } - - pushElement(macro); - try { - final ASTDirMacro.Context macroCtx = macro.new Context(this, childBuffer, bodyParameterNames); - setMacroContextLocalsFromArguments(macroCtx, macro, namedArgs, positionalArgs); - - final ASTDirMacro.Context prevMacroCtx = currentMacroContext; - currentMacroContext = macroCtx; - - final LocalContextStack prevLocalContextStack = localContextStack; - localContextStack = null; - - final Namespace prevNamespace = currentNamespace; - currentNamespace = (Namespace) macroToNamespaceLookup.get(macro); - - try { - macroCtx.sanityCheck(this); - visit(macro.getChildBuffer()); - } catch (ASTDirReturn.Return re) { - // Not an error, just a <#return> - } catch (TemplateException te) { - handleTemplateException(te); - } finally { - currentMacroContext = prevMacroCtx; - localContextStack = prevLocalContextStack; - currentNamespace = prevNamespace; - } - } finally { - popElement(); - } - } - - /** - * Sets the local variables corresponding to the macro call arguments in the macro context. - */ - private void setMacroContextLocalsFromArguments( - final ASTDirMacro.Context macroCtx, - final ASTDirMacro macro, - final Map<String, ASTExpression> namedArgs, final List<ASTExpression> positionalArgs) throws TemplateException { - String catchAllParamName = macro.getCatchAll(); - if (namedArgs != null) { - final NativeHashEx2 catchAllParamValue; - if (catchAllParamName != null) { - catchAllParamValue = new NativeHashEx2(); - macroCtx.setLocalVar(catchAllParamName, catchAllParamValue); - } else { - catchAllParamValue = null; - } - - for (Map.Entry<String, ASTExpression> argNameAndValExp : namedArgs.entrySet()) { - final String argName = argNameAndValExp.getKey(); - final boolean isArgNameDeclared = macro.hasArgNamed(argName); - if (isArgNameDeclared || catchAllParamName != null) { - ASTExpression argValueExp = argNameAndValExp.getValue(); - TemplateModel argValue = argValueExp.eval(this); - if (isArgNameDeclared) { - macroCtx.setLocalVar(argName, argValue); - } else { - catchAllParamValue.put(argName, argValue); - } - } else { - throw new _MiscTemplateException(this, - (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()), - " has no parameter with name ", new _DelayedJQuote(argName), "."); - } - } - } else if (positionalArgs != null) { - final NativeSequence catchAllParamValue; - if (catchAllParamName != null) { - catchAllParamValue = new NativeSequence(8); - macroCtx.setLocalVar(catchAllParamName, catchAllParamValue); - } else { - catchAllParamValue = null; - } - - String[] argNames = macro.getArgumentNamesInternal(); - final int argsCnt = positionalArgs.size(); - if (argNames.length < argsCnt && catchAllParamName == null) { - throw new _MiscTemplateException(this, - (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()), - " only accepts ", new _DelayedToString(argNames.length), " parameters, but got ", - new _DelayedToString(argsCnt), "."); - } - for (int i = 0; i < argsCnt; i++) { - ASTExpression argValueExp = positionalArgs.get(i); - TemplateModel argValue = argValueExp.eval(this); - try { - if (i < argNames.length) { - String argName = argNames[i]; - macroCtx.setLocalVar(argName, argValue); - } else { - catchAllParamValue.add(argValue); - } - } catch (RuntimeException re) { - throw new _MiscTemplateException(re, this); - } - } + TemplateDirectiveModel nodeProcessor = getNodeProcessor(currentNodeName, currentNodeNS, nodeNamespaceIndex); + if (nodeProcessor != null) { + _TemplateCallableModelUtils.executeWith0Arguments( + nodeProcessor, NonTemplateCallPlace.INSTANCE, out, this); } } /** * Defines the given macro in the current namespace (doesn't call it). */ - void visitMacroDef(ASTDirMacro macro) { - macroToNamespaceLookup.put(macro, currentNamespace); - currentNamespace.put(macro.getName(), macro); - } - - Namespace getMacroNamespace(ASTDirMacro macro) { - return (Namespace) macroToNamespaceLookup.get(macro); + void visitMacroOrFunctionDefinition(ASTDirMacroOrFunction definition) { + Namespace currentNamespace = this.getCurrentNamespace(); + currentNamespace.put( + definition.getName(), + definition.isFunction() + ? new TemplateLanguageFunction(definition, currentNamespace) + : new TemplateLanguageDirective(definition, currentNamespace)); } void recurse(TemplateNodeModel node, TemplateSequenceModel namespaces) @@ -786,7 +676,7 @@ public final class Environment extends MutableProcessingConfiguration<Environmen } } - ASTDirMacro.Context getCurrentMacroContext() { + ASTDirMacroOrFunction.Context getCurrentMacroContext() { return currentMacroContext; } @@ -937,7 +827,7 @@ public final class Environment extends MutableProcessingConfiguration<Environmen * Tells if the same concrete time zone is used for SQL date-only and time-only values as for other * date/time/date-time values. */ - boolean isSQLDateAndTimeTimeZoneSameAsNormal() { + private boolean isSQLDateAndTimeTimeZoneSameAsNormal() { if (cachedSQLDateAndTimeTimeZoneSameAsNormal == null) { cachedSQLDateAndTimeTimeZoneSameAsNormal = Boolean.valueOf( getSQLDateAndTimeTimeZone() == null @@ -1959,7 +1849,7 @@ public final class Environment extends MutableProcessingConfiguration<Environmen * (Note that the misnomer is kept for backward compatibility: nested content parameters are not local variables * according to our terminology.) */ - // TODO [FM3] Don't return nested content params anymore (see JavaDoc)? But, LocalContext does return them... + // TODO [FM3] Don't return nested content params anymore (see JavaDoc) public TemplateModel getLocalVariable(String name) throws TemplateModelException { if (localContextStack != null) { for (int i = localContextStack.size() - 1; i >= 0; i--) { @@ -2227,7 +2117,7 @@ public final class Environment extends MutableProcessingConfiguration<Environmen sb.append(MessageUtil.shorten(stackEl.getDescription(), 40)); sb.append(" ["); - ASTDirMacro enclosingMacro = getEnclosingMacro(stackEl); + ASTDirMacroOrFunction enclosingMacro = getEnclosingMacro(stackEl); if (enclosingMacro != null) { sb.append(MessageUtil.formatLocationForEvaluationError( enclosingMacro, stackEl.beginLine, stackEl.beginColumn)); @@ -2238,9 +2128,9 @@ public final class Environment extends MutableProcessingConfiguration<Environmen sb.append("]"); } - static private ASTDirMacro getEnclosingMacro(ASTElement stackEl) { + static private ASTDirMacroOrFunction getEnclosingMacro(ASTElement stackEl) { while (stackEl != null) { - if (stackEl instanceof ASTDirMacro) return (ASTDirMacro) stackEl; + if (stackEl instanceof ASTDirMacroOrFunction) return (ASTDirMacroOrFunction) stackEl; stackEl = stackEl.getParent(); } return null; @@ -2420,12 +2310,12 @@ public final class Environment extends MutableProcessingConfiguration<Environmen currentVisitorNode = node; } - TemplateModel getNodeProcessor(TemplateNodeModel node) throws TemplateException { + TemplateDirectiveModel getNodeProcessor(TemplateNodeModel node) throws TemplateException { String nodeName = node.getNodeName(); if (nodeName == null) { - throw new _MiscTemplateException(this, "Node name is null."); + throw new _MiscTemplateException(this, "Node name was null."); } - TemplateModel result = getNodeProcessor(nodeName, node.getNodeNamespace(), 0); + TemplateDirectiveModel result = getNodeProcessor(nodeName, node.getNodeNamespace(), 0); if (result == null) { String type = node.getNodeType(); @@ -2445,9 +2335,9 @@ public final class Environment extends MutableProcessingConfiguration<Environmen return result; } - private TemplateModel getNodeProcessor(final String nodeName, final String nsURI, int startIndex) + private TemplateDirectiveModel getNodeProcessor(final String nodeName, final String nsURI, int startIndex) throws TemplateException { - TemplateModel result = null; + TemplateDirectiveModel result = null; int i; for (i = startIndex; i < nodeNamespaces.size(); i++) { Namespace ns = null; @@ -2470,11 +2360,12 @@ public final class Environment extends MutableProcessingConfiguration<Environmen return result; } - private TemplateModel getNodeProcessor(Namespace ns, String localName, String nsURI) throws TemplateException { + private TemplateDirectiveModel getNodeProcessor(Namespace ns, String localName, String nsURI) + throws TemplateException { TemplateModel result = null; if (nsURI == null) { result = ns.get(localName); - if (!(result instanceof ASTDirMacro) && !(result instanceof TemplateDirectiveModel)) { + if (!(result instanceof TemplateDirectiveModel)) { result = null; } } else { @@ -2487,31 +2378,31 @@ public final class Environment extends MutableProcessingConfiguration<Environmen } if (prefix.length() > 0) { result = ns.get(prefix + ":" + localName); - if (!(result instanceof ASTDirMacro) && !(result instanceof TemplateDirectiveModel)) { + if (!(result instanceof TemplateDirectiveModel)) { result = null; } } else { - if (nsURI.length() == 0) { + if (nsURI.isEmpty()) { result = ns.get(Template.NO_NS_PREFIX + ":" + localName); - if (!(result instanceof ASTDirMacro) && !(result instanceof TemplateDirectiveModel)) { + if (!(result instanceof TemplateDirectiveModel)) { result = null; } } if (nsURI.equals(template.getDefaultNS())) { result = ns.get(Template.DEFAULT_NAMESPACE_PREFIX + ":" + localName); - if (!(result instanceof ASTDirMacro) && !(result instanceof TemplateDirectiveModel)) { + if (!(result instanceof TemplateDirectiveModel)) { result = null; } } if (result == null) { result = ns.get(localName); - if (!(result instanceof ASTDirMacro) && !(result instanceof TemplateDirectiveModel)) { + if (!(result instanceof TemplateDirectiveModel)) { result = null; } } } } - return result; + return (TemplateDirectiveModel) result; } /** @@ -2759,7 +2650,7 @@ public final class Environment extends MutableProcessingConfiguration<Environmen void importMacros(Template template) { for (Object macro : template.getMacros().values()) { - visitMacroDef((ASTDirMacro) macro); + visitMacroOrFunctionDefinition((ASTDirMacroOrFunction) macro); } } @@ -3034,4 +2925,158 @@ public final class Environment extends MutableProcessingConfiguration<Environmen return res; } + /** + * Superclass of {@link TemplateCallableModel}-s implemented in the template language. + */ + abstract class TemplateLanguageCallable implements TemplateCallableModel { + private final ASTDirMacroOrFunction callableDefinition; + private final Namespace namespace; + + public TemplateLanguageCallable(ASTDirMacroOrFunction callableDefinition, Namespace namespace) { + this.callableDefinition = callableDefinition; + this.namespace = namespace; + } + + protected void genericExecute(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env) + throws TemplateException, IOException { + pushElement(callableDefinition); + try { + final ASTDirMacroOrFunction.Context macroCtx = callableDefinition.new Context( + this, callPlace, Environment.this); + + final ASTDirMacroOrFunction.Context prevMacroCtx = currentMacroContext; + currentMacroContext = macroCtx; + + final LocalContextStack prevLocalContextStack = localContextStack; + localContextStack = null; + + final Namespace prevNamespace = currentNamespace; + currentNamespace = namespace; + + try { + // Note: Default expressions are evaluated here, so namespace, stack, etc. must be already set + setLocalsFromArguments(macroCtx, args); + + visit(callableDefinition.getChildBuffer(), out); + } catch (ASTDirReturn.Return re) { + // Not an error, just a <#return> + } catch (TemplateException te) { + handleTemplateException(te); + } finally { + currentMacroContext = prevMacroCtx; + localContextStack = prevLocalContextStack; + currentNamespace = prevNamespace; + } + } finally { + popElement(); + } + } + + private void setLocalsFromArguments(ASTDirMacroOrFunction.Context macroCtx, TemplateModel[] args) + throws TemplateException { + ASTDirMacroOrFunction.ParameterDefinition[] paramDefsByArgIdx = + callableDefinition.getParameterDefinitionByArgumentArrayIndex(); + for (int argIdx = 0; argIdx < args.length; argIdx++) { + TemplateModel arg = args[argIdx]; + ASTDirMacroOrFunction.ParameterDefinition paramDef = paramDefsByArgIdx[argIdx]; + if (arg == null) { // TODO [FM3] FM2 doesn't differentiate omitted and null, but FM3 will. + ASTExpression defaultExp = paramDef.getDefaultExpression(); + if (defaultExp != null) { + arg = defaultExp.eval(Environment.this); + if (arg == null) { + throw InvalidReferenceException.getInstance(defaultExp, Environment.this); + } + } else { + // TODO [FM3] Had to give different messages depending on if the argument was omitted, or if + // it was null, but this will be fixed with the null related refactoring. + throw new _MiscTemplateException(Environment.this, + new _ErrorDescriptionBuilder( + "When calling macro ", new _DelayedJQuote(callableDefinition.getName()), + ", required parameter ", new _DelayedJQuote(paramDef.getName()), + (argIdx < getArgumentArrayLayout().getPredefinedPositionalArgumentCount() + ? new Object[] { " (parameter #", (argIdx + 1), ")" } + : ""), + " was either not specified, or had null/missing value.") + .tip("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 on the caller side with the \"!\" operator, like " + + "paramValue!defaultValue.") + .tip("If the parameter was omitted on the caller side, and 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>." + ) + ); + } + } + macroCtx.setLocalVar(paramDef.getName(), arg); + } + } + + @Override + public ArgumentArrayLayout getArgumentArrayLayout() { + return callableDefinition.getArgumentArrayLayout(); + } + + ASTDirMacroOrFunction getCallableDefinition() { + return callableDefinition; + } + + Namespace getNamespace() { + return namespace; + } + } + + /** + * {@link TemplateDirectiveModel} implemented in the template language (such as with the {@code #macro} directive). + * This is the value that {@code #macro} creates on runtime, not the {@code #macro} directive itself. + */ + final class TemplateLanguageDirective extends TemplateLanguageCallable implements TemplateDirectiveModel { + + public TemplateLanguageDirective(ASTDirMacroOrFunction macroDef, Namespace namespace) { + super(macroDef, namespace); + } + + @Override + public void execute(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env) + throws IOException, TemplateException { + if (getCallableDefinition() == ASTDirMacroOrFunction.PASS_MACRO) { + return; + } + genericExecute(args, callPlace, out, env); + } + + @Override + public boolean isNestedContentSupported() { + // TODO [FM3] We should detect if #nested is called anywhere (also maybe something like + // `<#macro m{supportsNested[=true|false]}>`) should be added. + return true; + } + + } + + /** + * {@link TemplateFunctionModel} implemented in the template language (such as with the {@code #function} + * directive). This is the value that {@code #function} creates on runtime, not the {@code #macro} directive itself. + */ + final class TemplateLanguageFunction extends TemplateLanguageCallable implements TemplateFunctionModel { + + public TemplateLanguageFunction(ASTDirMacroOrFunction callableDefinition, Namespace namespace) { + super(callableDefinition, namespace); + } + + @Override + public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env) + throws TemplateException { + env.setLastReturnValue(null); + try { + genericExecute(args, callPlace, _NullWriter.INSTANCE, env); + } catch (IOException e) { + // Should not occur + throw new TemplateException("Unexpected exception during function execution", e, env); + } + return env.getLastReturnValue(); + } + } + } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContext.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContext.java b/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContext.java index 42c5455..59d867d 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContext.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContext.java @@ -26,7 +26,7 @@ import org.apache.freemarker.core.model.TemplateModelException; /** * An interface that represents a local context. This is used as the abstraction for - * the context of a ASTDirMacro invocation, a loop, or the nested block call from within + * the context of a ASTDirMacroOrFunction invocation, a loop, or the nested block call from within * a macro. */ public interface LocalContext { http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java index 8820f08..a1858b1 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java @@ -71,7 +71,7 @@ class MessageUtil { return formatLocation("at", template, line, column); } - static String formatLocationForEvaluationError(ASTDirMacro macro, int line, int column) { + static String formatLocationForEvaluationError(ASTDirMacroOrFunction macro, int line, int column) { Template t = macro.getTemplate(); return formatLocation("at", t != null ? t.getSourceOrLookupName() : null, macro.getName(), macro.isFunction(), line, column); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/NonDirectiveException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonDirectiveException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonDirectiveException.java new file mode 100644 index 0000000..2053361 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonDirectiveException.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core; + +import org.apache.freemarker.core.model.TemplateDirectiveModel; +import org.apache.freemarker.core.model.TemplateModel; + +/** + * Indicates that a {@link TemplateDirectiveModel} was expected, but the value had a different type. + */ +class NonDirectiveException extends UnexpectedTypeException { + + private static final Class[] EXPECTED_TYPES = new Class[] { + TemplateDirectiveModel.class, ASTDirMacroOrFunction.class }; + + public NonDirectiveException(Environment env) { + super(env, "Expecting user-defined directive, transform or macro value here"); + } + + public NonDirectiveException(String description, Environment env) { + super(env, description); + } + + NonDirectiveException(Environment env, _ErrorDescriptionBuilder description) { + super(env, description); + } + + NonDirectiveException( + ASTExpression blamed, TemplateModel model, Environment env) + throws InvalidReferenceException { + super(blamed, model, "user-defined directive, transform or macro", EXPECTED_TYPES, env); + } + + NonDirectiveException( + ASTExpression blamed, TemplateModel model, String tip, + Environment env) + throws InvalidReferenceException { + super(blamed, model, "user-defined directive, transform or macro", EXPECTED_TYPES, tip, env); + } + + NonDirectiveException( + ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException { + super(blamed, model, "user-defined directive, transform or macro", EXPECTED_TYPES, tips, env); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/NonTemplateCallPlace.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonTemplateCallPlace.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonTemplateCallPlace.java new file mode 100644 index 0000000..917ccbf --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonTemplateCallPlace.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core; + +import java.io.IOException; +import java.io.Writer; + +import org.apache.freemarker.core.model.TemplateDirectiveModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.util.CommonSupplier; + +/** + * {@link CallPlace} used when a {@link TemplateDirectiveModel} is called from outside a template (such as from user + * Java code). + */ +public final class NonTemplateCallPlace implements CallPlace { + + public static final NonTemplateCallPlace INSTANCE = new NonTemplateCallPlace(); + + private final int firstTargetJavaParameterTypeIndex; + private final Class<?>[] targetJavaParameterTypes; + + private NonTemplateCallPlace() { + this(-1, null); + } + + /** + * @param firstTargetJavaParameterTypeIndex + * See {@link CallPlace#getFirstTargetJavaParameterTypeIndex()} + * @param targetJavaParameterTypes + * The target Java types of the arguments of the invocation, starting with the target Java type of the + * argument at index {@code firstTargetJavaParameterTypeIndex}, and usually ending with the target type of + * the last argument that has a non-{@code null} target type (although having {@code null}-s at the end is + * legal too). For example, using Java-like call syntax, if the call is like + * {@code m(a, b, (Foo) c, d, (Bar) e, f)}, then {@code firstTargetJavaParameterTypeIndex} is 2, + * and the array will be {@code new Class<?>[] { } [ Foo.class, null, Bar.class ]}. + */ + public NonTemplateCallPlace(int firstTargetJavaParameterTypeIndex, Class<?>[] targetJavaParameterTypes) { + this.firstTargetJavaParameterTypeIndex = firstTargetJavaParameterTypeIndex; + this.targetJavaParameterTypes = targetJavaParameterTypes; + } + + /** + * Always returns {@code false}. + */ + @Override + public boolean hasNestedContent() { + return false; + } + + /** + * Always returns {@code 0}. + */ + @Override + public int getNestedContentParameterCount() { + return 0; + } + + /** + * Does nothing. + */ + @Override + public void executeNestedContent(TemplateModel[] nestedContentArgs, Writer out, Environment env) + throws TemplateException, IOException { + // Do nothing + } + + /** + * Always returns {@code -1}. + */ + @Override + public Template getTemplate() { + return null; + } + + /** + * Always returns {@code -1}. + */ + @Override + public int getBeginColumn() { + return -1; + } + + /** + * Always returns {@code -1}. + */ + @Override + public int getBeginLine() { + return -1; + } + + /** + * Always returns {@code -1}. + */ + @Override + public int getEndColumn() { + return -1; + } + + /** + * Always returns {@code -1}. + */ + @Override + public int getEndLine() { + return -1; + } + + /** + * Always throws {@link UnsupportedOperationException}. + */ + @Override + public Object getOrCreateCustomData(Object providerIdentity, CommonSupplier<?> supplier) + throws CallPlaceCustomDataInitializationException { + throw new UnsupportedOperationException("Non-template call place doesn't support custom data storage"); + } + + /** + * Always returns {@code false}. + */ + @Override + public boolean isCustomDataSupported() { + return false; + } + + /** + * Always returns {@code false}. + */ + @Override + public boolean isNestedOutputCacheable() { + return false; + } + + /** + * Always returns {@code -1}. + */ + @Override + public int getFirstTargetJavaParameterTypeIndex() { + return firstTargetJavaParameterTypeIndex; + } + + /** + * Always returns {@code null}. + */ + @Override + public Class<?> getTargetJavaParameterType(int argIndex) { + int idx = argIndex - firstTargetJavaParameterTypeIndex; + return idx >= 0 || idx < targetJavaParameterTypes.length ? targetJavaParameterTypes[idx] : null; + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java deleted file mode 100644 index 547d688..0000000 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.freemarker.core; - -import org.apache.freemarker.core.model.TemplateDirectiveModel; -import org.apache.freemarker.core.model.TemplateModel; - -// TODO [FM3][CF] Review and rename this when TDM2 and TFM are in place -/** - * Indicates that a {@link TemplateDirectiveModel} or {@link ASTDirMacro} value was - * expected, but the value had a different type. - */ -class NonUserDefinedDirectiveLikeException extends UnexpectedTypeException { - - private static final Class[] EXPECTED_TYPES = new Class[] { - TemplateDirectiveModel.class, ASTDirMacro.class }; - - public NonUserDefinedDirectiveLikeException(Environment env) { - super(env, "Expecting user-defined directive, transform or macro value here"); - } - - public NonUserDefinedDirectiveLikeException(String description, Environment env) { - super(env, description); - } - - NonUserDefinedDirectiveLikeException(Environment env, _ErrorDescriptionBuilder description) { - super(env, description); - } - - NonUserDefinedDirectiveLikeException( - ASTExpression blamed, TemplateModel model, Environment env) - throws InvalidReferenceException { - super(blamed, model, "user-defined directive, transform or macro", EXPECTED_TYPES, env); - } - - NonUserDefinedDirectiveLikeException( - ASTExpression blamed, TemplateModel model, String tip, - Environment env) - throws InvalidReferenceException { - super(blamed, model, "user-defined directive, transform or macro", EXPECTED_TYPES, tip, env); - } - - NonUserDefinedDirectiveLikeException( - ASTExpression blamed, TemplateModel model, String[] tips, Environment env) throws InvalidReferenceException { - super(blamed, model, "user-defined directive, transform or macro", EXPECTED_TYPES, tips, env); - } - -} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/ParameterRole.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ParameterRole.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ParameterRole.java index ccae7b7..b9ce370 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/ParameterRole.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ParameterRole.java @@ -54,15 +54,11 @@ final class ParameterRole { static final ParameterRole TEMPLATE_NAME = new ParameterRole("template name"); static final ParameterRole IGNORE_MISSING_PARAMETER = new ParameterRole("\"ignore_missing\" parameter"); static final ParameterRole PARAMETER_NAME = new ParameterRole("parameter name"); - static final ParameterRole PARAMETER_DEFAULT = new ParameterRole("parameter default"); - static final ParameterRole CATCH_ALL_PARAMETER_NAME = new ParameterRole("catch-all parameter name"); + static final ParameterRole PARAMETER_DEFINITION = new ParameterRole("parameter definition"); static final ParameterRole ARGUMENT_NAME = new ParameterRole("argument name"); static final ParameterRole ARGUMENT_VALUE = new ParameterRole("argument value"); static final ParameterRole CONTENT = new ParameterRole("content"); - static final ParameterRole EMBEDDED_TEMPLATE = new ParameterRole("embedded template"); static final ParameterRole VALUE_PART = new ParameterRole("value part"); - static final ParameterRole MINIMUM_DECIMALS = new ParameterRole("minimum decimals"); - static final ParameterRole MAXIMUM_DECIMALS = new ParameterRole("maximum decimals"); static final ParameterRole NODE = new ParameterRole("node"); static final ParameterRole CALLEE = new ParameterRole("callee"); static final ParameterRole MESSAGE = new ParameterRole("message"); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java index 5ea4517..fde11d9 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java @@ -735,7 +735,7 @@ public class Template implements ProcessingConfiguration, CustomStateScope { out.write(rootElement != null ? rootElement.getCanonicalForm() : "Unfinished template"); } - void addMacro(ASTDirMacro macro) { + void addMacro(ASTDirMacroOrFunction macro) { macros.put(macro.getName(), macro); } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateCallableModelUtils.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateCallableModelUtils.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateCallableModelUtils.java deleted file mode 100644 index 8313d53..0000000 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateCallableModelUtils.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.apache.freemarker.core; - -import org.apache.freemarker.core.model.TemplateModel; -import org.apache.freemarker.core.model.TemplateNumberModel; - -public class TemplateCallableModelUtils { - - // TODO [FM3][CF] Add this to the other exception classes too - public static TemplateNumberModel castArgumentToNumber(TemplateModel[] args, int argIndex, boolean allowNull, - Environment env) throws TemplateException { - return getTemplateNumberModel(args[argIndex], argIndex, allowNull, env); - } - - // TODO [FM3][CF] Add this to the other exception classes too - private static TemplateNumberModel getTemplateNumberModel(TemplateModel argValue, int argIndex, boolean allowNull, - Environment env) throws TemplateException { - if (argValue instanceof TemplateNumberModel) { - return (TemplateNumberModel) argValue; - } - if (argValue == null) { - if (allowNull) { - return null; - } - throw new _MiscTemplateException(env, - "The ", new _DelayedOrdinal(argIndex + 1), " argument can't be null."); - } - throw new NonNumericalException(argIndex, argValue, null, env); - } - - // TODO [FM3][CF] Add this to the other exception classes too - public static TemplateNumberModel castArgumentToNumber(TemplateModel argValue, String argName, boolean allowNull, - Environment env) throws TemplateException { - if (argValue instanceof TemplateNumberModel) { - return (TemplateNumberModel) argValue; - } - if (argValue == null) { - if (allowNull) { - return null; - } - throw new _MiscTemplateException(env, - "The ", new _DelayedJQuote(argName), " argument can't be null."); - } - throw new NonNumericalException(argName, argValue, null, env); - } - -} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java index 71db276..a1b60bf 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java @@ -21,7 +21,6 @@ package org.apache.freemarker.core; import java.io.IOException; -import org.apache.freemarker.core.model.CallPlace; import org.apache.freemarker.core.model.TemplateDateModel; /** http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/_CoreAPI.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_CoreAPI.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_CoreAPI.java index b575901..c904afa 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/_CoreAPI.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_CoreAPI.java @@ -61,14 +61,12 @@ public final class _CoreAPI { } } - // [FM3] Should become unnecessary as custom directive classes are reworked - public static boolean isMacroOrFunction(TemplateModel m) { - return m instanceof ASTDirMacro; + public static boolean isMacro(Class<? extends TemplateModel> cl) { + return Environment.TemplateLanguageDirective.class.isAssignableFrom(cl); } - // [FM3] Should become unnecessary as custom directive classes are reworked - public static boolean isFunction(TemplateModel m) { - return m instanceof ASTDirMacro && ((ASTDirMacro) m).isFunction(); + public static boolean isFunction(Class<? extends TemplateModel> cl) { + return Environment.TemplateLanguageFunction.class.isAssignableFrom(cl); } public static void checkVersionNotNullAndSupported(Version incompatibleImprovements) { http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/_ErrorDescriptionBuilder.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_ErrorDescriptionBuilder.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_ErrorDescriptionBuilder.java index c548487..13cadcd 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/_ErrorDescriptionBuilder.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_ErrorDescriptionBuilder.java @@ -211,6 +211,8 @@ public class _ErrorDescriptionBuilder { for (Object partObj : parts) { if (partObj instanceof Object[]) { appendParts(sb, (Object[]) partObj); + } else if (partObj instanceof Class<?>) { + sb.append(((Class<?>) partObj).getName()); } else { String partStr; partStr = tryToString(partObj); @@ -219,12 +221,13 @@ public class _ErrorDescriptionBuilder { } if (template != null) { + // Translate tag syntax of the part looks like an FTL tag if (partStr.length() > 4 && partStr.charAt(0) == '<' && ( (partStr.charAt(1) == '#' || partStr.charAt(1) == '@') || (partStr.charAt(1) == '/') && (partStr.charAt(2) == '#' || partStr.charAt(2) == '@') - ) + ) && partStr.charAt(partStr.length() - 1) == '>') { if (template.getActualTagSyntax() == TagSyntax.SQUARE_BRACKET) { sb.append('['); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3cacd9ed/freemarker-core/src/main/java/org/apache/freemarker/core/_TemplateCallableModelUtils.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_TemplateCallableModelUtils.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_TemplateCallableModelUtils.java new file mode 100644 index 0000000..277dc1a --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_TemplateCallableModelUtils.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core; + +import java.io.IOException; +import java.io.Writer; + +import org.apache.freemarker.core.model.ArgumentArrayLayout; +import org.apache.freemarker.core.model.Constants; +import org.apache.freemarker.core.model.TemplateCallableModel; +import org.apache.freemarker.core.model.TemplateDirectiveModel; +import org.apache.freemarker.core.model.TemplateFunctionModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateNumberModel; + +/** + * For internal use only; don't depend on this, there's no backward compatibility guarantee at all! + */ +// TODO [FM3] Most functionality here should be made public on some way +public final class _TemplateCallableModelUtils { + + private _TemplateCallableModelUtils() { + // + } + + public static void executeWith0Arguments( + TemplateDirectiveModel directive, CallPlace callPlace, Writer out, Environment env) + throws IOException, TemplateException { + directive.execute(getArgumentArrayWithNoArguments(directive), callPlace, out, env); + } + + public static TemplateModel executeWith0Arguments( + TemplateFunctionModel function, CallPlace callPlace, Environment env) + throws TemplateException { + return function.execute(getArgumentArrayWithNoArguments(function), callPlace, env); + } + + private static TemplateModel[] getArgumentArrayWithNoArguments(TemplateCallableModel callable) { + ArgumentArrayLayout argsLayout = callable.getArgumentArrayLayout(); + int totalLength = argsLayout.getTotalLength(); + if (totalLength == 0) { + return Constants.EMPTY_TEMPLATE_MODEL_ARRAY; + } else { + TemplateModel[] args = new TemplateModel[totalLength]; + + int positionalVarargsArgumentIndex = argsLayout.getPositionalVarargsArgumentIndex(); + if (positionalVarargsArgumentIndex != -1) { + args[positionalVarargsArgumentIndex] = Constants.EMPTY_SEQUENCE; + } + + int namedVarargsArgumentIndex = argsLayout.getNamedVarargsArgumentIndex(); + if (namedVarargsArgumentIndex != -1) { + args[namedVarargsArgumentIndex] = Constants.EMPTY_SEQUENCE; + } + + return args; + } + } + + public static TemplateNumberModel castArgumentToNumber(TemplateModel[] args, int argIndex, boolean allowNull, + Environment env) throws TemplateException { + return getTemplateNumberModel(args[argIndex], argIndex, allowNull, env); + } + + private static TemplateNumberModel getTemplateNumberModel(TemplateModel argValue, int argIndex, boolean allowNull, + Environment env) throws TemplateException { + if (argValue instanceof TemplateNumberModel) { + return (TemplateNumberModel) argValue; + } + if (argValue == null) { + if (allowNull) { + return null; + } + throw new _MiscTemplateException(env, + "The ", new _DelayedOrdinal(argIndex + 1), " argument can't be null."); + } + throw new NonNumericalException(argIndex, argValue, null, env); + } + + public static TemplateNumberModel castArgumentToNumber(TemplateModel argValue, String argName, boolean allowNull, + Environment env) throws TemplateException { + if (argValue instanceof TemplateNumberModel) { + return (TemplateNumberModel) argValue; + } + if (argValue == null) { + if (allowNull) { + return null; + } + throw new _MiscTemplateException(env, + "The ", new _DelayedJQuote(argName), " argument can't be null."); + } + throw new NonNumericalException(argName, argValue, null, env); + } + +}
