aboutsummaryrefslogtreecommitdiff
path: root/velocity-engine-core/src/main/java/org/apache/velocity/runtime
diff options
context:
space:
mode:
Diffstat (limited to 'velocity-engine-core/src/main/java/org/apache/velocity/runtime')
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/DeprecatedRuntimeConstants.java285
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/ParserConfiguration.java116
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/ParserPool.java53
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/ParserPoolImpl.java81
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/Renderable.java44
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java499
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeInstance.java2025
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeServices.java498
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeSingleton.java571
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/VelocimacroFactory.java639
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/VelocimacroManager.java355
-rwxr-xr-xvelocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Block.java209
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/BlockMacro.java125
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Break.java126
-rwxr-xr-xvelocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Define.java121
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Directive.java264
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/DirectiveConstants.java35
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Evaluate.java240
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Foreach.java360
-rwxr-xr-xvelocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/ForeachScope.java79
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Include.java319
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/InputBase.java59
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Macro.java294
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/MacroParseException.java206
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Parse.java358
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/RuntimeMacro.java371
-rwxr-xr-xvelocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Scope.java359
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Stop.java115
-rwxr-xr-xvelocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/StopCommand.java89
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/VelocimacroProxy.java459
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/contrib/For.java141
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/CharStream.java128
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/LogContext.java165
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/ParseException.java237
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/Parser.java54
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/TemplateParseException.java245
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/VelocityCharStream.java526
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTAddNode.java90
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTAndNode.java123
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTAssignment.java69
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTBinaryOperator.java47
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTBlock.java158
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTComment.java109
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTComparisonNode.java177
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTDirective.java372
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTDirectiveAssign.java53
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTDivNode.java86
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTEQNode.java96
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTElseIfStatement.java110
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTElseStatement.java87
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTEscape.java90
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTEscapedDirective.java91
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTExpression.java96
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTFalse.java88
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTFloatingPointLiteral.java126
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTGENode.java51
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTGTNode.java51
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIdentifier.java305
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIfStatement.java216
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIncludeStatement.java70
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIndex.java224
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIntegerLiteral.java122
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIntegerRange.java296
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTLENode.java51
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTLTNode.java51
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTLogicalOperator.java30
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMap.java112
-rwxr-xr-xvelocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMathNode.java165
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMethod.java459
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTModNode.java99
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMulNode.java81
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTNENode.java50
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTNegateNode.java94
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTNotNode.java96
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTObjectArray.java103
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTOrNode.java115
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTParameters.java55
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTReference.java1165
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTSetDirective.java304
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTStringLiteral.java364
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTSubtractNode.java67
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTText.java122
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTTextblock.java94
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTTrue.java89
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTVariable.java70
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTWord.java69
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTprocess.java68
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/AbstractExecutor.java83
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/BooleanPropertyExecutor.java123
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/GetExecutor.java120
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/IndentationFixer.java362
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/JJTParserState.java5
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/MapGetExecutor.java105
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/MapSetExecutor.java84
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/MathUtils.java539
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/Node.java232
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/NodeUtils.java162
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ParserTreeConstants.java24
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ParserVisitor.java332
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/PropertyExecutor.java151
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/PublicFieldExecutor.java128
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/PutExecutor.java133
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SetExecutor.java87
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SetPropertyExecutor.java138
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SetPublicFieldExecutor.java149
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SimpleNode.java650
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ContentResource.java105
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/Resource.java320
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCache.java81
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCacheImpl.java168
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceFactory.java59
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManager.java81
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManagerImpl.java607
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ClasspathResourceLoader.java169
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DataSourceResourceLoader.java550
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/FileResourceLoader.java373
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarHolder.java169
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarResourceLoader.java263
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoader.java324
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoaderFactory.java62
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/StringResourceLoader.java438
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/URLResourceLoader.java214
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResource.java108
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepository.java76
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepositoryImpl.java104
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/visitor/BaseVisitor.java494
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/visitor/NodeViewMode.java436
127 files changed, 27034 insertions, 0 deletions
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/DeprecatedRuntimeConstants.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/DeprecatedRuntimeConstants.java
new file mode 100644
index 00000000..3789a76e
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/DeprecatedRuntimeConstants.java
@@ -0,0 +1,285 @@
+package org.apache.velocity.runtime;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * This class gathers deprecated runtime constants
+ *
+ * @author <a href="mailto:claude@renegat.net">Claude Brisson</a>
+ * @version $$
+ */
+
+@Deprecated
+public interface DeprecatedRuntimeConstants
+{
+ /**
+ * Logging of invalid references.
+ * @deprecated see {@link RuntimeConstants#RUNTIME_LOG_REFERENCE_LOG_INVALID}
+ */
+ String OLD_RUNTIME_LOG_REFERENCE_LOG_INVALID = "runtime.log.invalid.references";
+
+ /**
+ * Maximum allowed number of loops.
+ * @deprecated see {@link RuntimeConstants#MAX_NUMBER_LOOPS}
+ */
+ String OLD_MAX_NUMBER_LOOPS = "directive.foreach.maxloops";
+
+ /**
+ * Whether to throw an exception or just skip bad iterables. Default is true.
+ * @since 1.6
+ * @deprecated see {@link RuntimeConstants#SKIP_INVALID_ITERATOR}
+ */
+ String OLD_SKIP_INVALID_ITERATOR = "directive.foreach.skip.invalid";
+
+ /**
+ * An empty object (string, collection) or zero number is false.
+ * @since 2.0
+ * @deprecated see {@link RuntimeConstants#CHECK_EMPTY_OBJECTS}
+ */
+ String OLD_CHECK_EMPTY_OBJECTS = "directive.if.emptycheck";
+
+ /**
+ * Starting tag for error messages triggered by passing a parameter not allowed in the #include directive. Only string literals,
+ * and references are allowed.
+ * @deprecated see {@link RuntimeConstants#ERRORMSG_START}
+ */
+ String OLD_ERRORMSG_START = "directive.include.output.errormsg.start";
+
+ /**
+ * Ending tag for error messages triggered by passing a parameter not allowed in the #include directive. Only string literals,
+ * and references are allowed.
+ * @deprecated see {@link RuntimeConstants#ERRORMSG_END}
+ */
+ String OLD_ERRORMSG_END = "directive.include.output.errormsg.end";
+
+ /**
+ * Maximum recursion depth allowed for the #parse directive.
+ * @deprecated see {@link RuntimeConstants#PARSE_DIRECTIVE_MAXDEPTH}
+ */
+ String OLD_PARSE_DIRECTIVE_MAXDEPTH = "directive.parse.max.depth";
+
+ /**
+ * Maximum recursion depth allowed for the #define directive.
+ * @deprecated see {@link RuntimeConstants#DEFINE_DIRECTIVE_MAXDEPTH}
+ */
+ String OLD_DEFINE_DIRECTIVE_MAXDEPTH = "directive.define.max.depth";
+
+ /**
+ * Vector of custom directives
+ * @deprecated see {@link RuntimeConstants#CUSTOM_DIRECTIVES}
+ */
+ String OLD_CUSTOM_DIRECTIVES = "userdirective";
+
+ /**
+ * The <code>resource.manager.cache.size</code> property specifies the cache upper bound (if relevant).
+ * @deprecated see {@link RuntimeConstants#RESOURCE_MANAGER_DEFAULTCACHE_SIZE}
+ */
+ String OLD_RESOURCE_MANAGER_DEFAULTCACHE_SIZE = "resource.manager.defaultcache.size";
+
+ /**
+ * controls if the finding of a resource is logged.
+ * @deprecated see {@link RuntimeConstants#RESOURCE_MANAGER_LOGWHENFOUND}
+ */
+ String OLD_RESOURCE_MANAGER_LOGWHENFOUND = "resource.manager.logwhenfound";
+
+ /**
+ * Key used to retrieve the names of the resource loaders to be used. In a properties file they may appear as the following:
+ * <p>resource.loader = file,classpath</p>
+ * @deprecated see {@link RuntimeConstants#RESOURCE_LOADERS}
+ */
+ String OLD_RESOURCE_LOADERS = "resource.loader";
+
+ /**
+ * The public handle for setting a path in the FileResourceLoader.
+ * @deprecated see {@link RuntimeConstants#FILE_RESOURCE_LOADER_PATH}
+ */
+ String OLD_FILE_RESOURCE_LOADER_PATH = "file.resource.loader.path";
+
+ /**
+ * The public handle for turning the caching on in the FileResourceLoader.
+ * @deprecated see {@link RuntimeConstants#FILE_RESOURCE_LOADER_CACHE}
+ */
+ String OLD_FILE_RESOURCE_LOADER_CACHE = "file.resource.loader.cache";
+
+ /**
+ * Resource loader modification check interval property suffix
+ */
+ String OLD_RESOURCE_LOADER_CHECK_INTERVAL = "modificationCheckInterval";
+
+ /**
+ * Datasource loader datasource url
+ * @deprecated see {@link RuntimeConstants#DS_RESOURCE_LOADER_DATASOURCE}
+ */
+ String OLD_DS_RESOURCE_LOADER_DATASOURCE = "ds.resource.loader.resource.datasource";
+
+ /**
+ * Datasource loader template key column
+ * @deprecated see {@link RuntimeConstants#DS_RESOURCE_LOADER_KEY_COLUMN}
+ */
+ String OLD_DS_RESOURCE_LOADER_KEY_COLUMN = "ds.resource.loader.resource.keycolumn";
+
+ /**
+ * Datasource loader template content column
+ * @deprecated see {@link RuntimeConstants#DS_RESOURCE_LOADER_TEMPLATE_COLUMN}
+ */
+ String OLD_DS_RESOURCE_LOADER_TEMPLATE_COLUMN = "ds.resource.loader.resource.templatecolumn";
+
+ /**
+ * Datasource loader template timestamp column
+ * @deprecated see {@link RuntimeConstants#DS_RESOURCE_LOADER_TIMESTAMP_COLUMN}
+ */
+ String OLD_DS_RESOURCE_LOADER_TIMESTAMP_COLUMN = "ds.resource.loader.resource.timestampcolumn";
+
+ /**
+ * The default character encoding for the templates. Used by the parser in processing the input streams.
+ * @deprecated see {@link RuntimeConstants#INPUT_ENCODING}
+ */
+ String OLD_INPUT_ENCODING = "input.encoding";
+
+ /**
+ * The <code>eventhandler.referenceinsertion.class</code> property specifies a list of the
+ * {@link org.apache.velocity.app.event.ReferenceInsertionEventHandler} implementations to use.
+ * @deprecated see {@link RuntimeConstants#EVENTHANDLER_REFERENCEINSERTION}
+ */
+ String OLD_EVENTHANDLER_REFERENCEINSERTION = "eventhandler.referenceinsertion.class";
+
+ /**
+ * The <code>eventhandler.methodexception.class</code> property specifies a list of the
+ * {@link org.apache.velocity.app.event.MethodExceptionEventHandler} implementations to use.
+ * @deprecated see {@link RuntimeConstants#EVENTHANDLER_METHODEXCEPTION}
+ */
+ String OLD_EVENTHANDLER_METHODEXCEPTION = "eventhandler.methodexception.class";
+
+ /**
+ * The <code>eventhandler.include.class</code> property specifies a list of the
+ * {@link org.apache.velocity.app.event.IncludeEventHandler} implementations to use.
+ * @deprecated see {@link RuntimeConstants#EVENTHANDLER_INCLUDE}
+ */
+ String OLD_EVENTHANDLER_INCLUDE = "eventhandler.include.class";
+
+ /**
+ * The <code>eventhandler.invalidreferences.class</code> property specifies a list of the
+ * {@link org.apache.velocity.app.event.InvalidReferenceEventHandler} implementations to use.
+ * @deprecated see {@link RuntimeConstants#EVENTHANDLER_INVALIDREFERENCES}
+ */
+ String OLD_EVENTHANDLER_INVALIDREFERENCES = "eventhandler.invalidreferences.class";
+
+ /**
+ * Name of local Velocimacro library template.
+ * @deprecated see {@link RuntimeConstants#VM_LIBRARY}
+ */
+ String OLD_VM_LIBRARY = "velocimacro.library";
+
+ /**
+ * Default Velocimacro library template.
+ * @deprecated see {@link RuntimeConstants#VM_LIBRARY_DEFAULT}
+ */
+ String OLD_VM_LIBRARY_DEFAULT = "VM_global_library.vm";
+
+ /**
+ * boolean (true/false) default true: allow inline (in-template) macro definitions.
+ * @deprecated see {@link RuntimeConstants#VM_PERM_ALLOW_INLINE}
+ */
+ String OLD_VM_PERM_ALLOW_INLINE = "velocimacro.permissions.allow.inline";
+
+ /**
+ * boolean (true/false) default false: allow inline (in-template) macro definitions to replace existing.
+ * @deprecated see {@link RuntimeConstants#VM_PERM_ALLOW_INLINE_REPLACE_GLOBAL}
+ */
+ String OLD_VM_PERM_ALLOW_INLINE_REPLACE_GLOBAL = "velocimacro.permissions.allow.inline.to.replace.global";
+
+ /**
+ * Switch for forcing inline macros to be local: default false.
+ * @deprecated see {@link RuntimeConstants#VM_PERM_INLINE_LOCAL}
+ */
+ String OLD_VM_PERM_INLINE_LOCAL = "velocimacro.permissions.allow.inline.local.scope";
+
+ /**
+ * Specify the maximum depth for macro calls
+ * @since 1.6
+ * @deprecated see {@link RuntimeConstants#VM_MAX_DEPTH}
+ */
+ String OLD_VM_MAX_DEPTH = "velocimacro.max.depth";
+
+ /**
+ * Defines name of the reference that can be used to get the AST block passed to block macro calls.
+ * @since 1.7
+ * @deprecated see {@link RuntimeConstants#VM_BODY_REFERENCE}
+ */
+ String OLD_VM_BODY_REFERENCE = "velocimacro.body.reference";
+
+ /**
+ * Properties referenced in the template are required to exist the object
+ * @deprecated see {@link RuntimeConstants#RUNTIME_REFERENCES_STRICT}
+ */
+ String OLD_RUNTIME_REFERENCES_STRICT = "runtime.references.strict";
+
+ /**
+ * Indicates we are going to use modified escape behavior in strict mode
+ * @deprecated see {@link RuntimeConstants#RUNTIME_REFERENCES_STRICT_ESCAPE}
+ */
+ String OLD_RUNTIME_REFERENCES_STRICT_ESCAPE = "runtime.references.strict.escape";
+
+ /**
+ * key name for uberspector. Multiple classnames can be specified,in which case uberspectors will be chained.
+ * @deprecated see {@link RuntimeConstants#UBERSPECT_CLASSNAME}
+ */
+ String OLD_UBERSPECT_CLASSNAME = "runtime.introspector.uberspect";
+
+ /**
+ * key for Conversion Manager class
+ * @deprecated see {@link RuntimeConstants#CONVERSION_HANDLER_INSTANCE}
+ */
+ String OLD_CONVERSION_HANDLER_CLASS = "introspector.conversion_handler.class";
+
+ /**
+ * Switch for the interpolation facility for string literals.
+ * @deprecated see {@link RuntimeConstants#INTERPOLATE_STRINGLITERALS}
+ */
+ String OLD_INTERPOLATE_STRINGLITERALS = "runtime.interpolate.string.literals";
+
+ /**
+ * Switch for ignoring nulls in math equations vs throwing exceptions.
+ * @deprecated see {@link RuntimeConstants#STRICT_MATH}
+ */
+ String OLD_STRICT_MATH = "runtime.strict.math";
+
+ /**
+ * Key upon which a context should be accessible within itself
+ * @deprecated see {@link RuntimeConstants#CONTEXT_AUTOREFERENCE_KEY}
+ */
+ String OLD_CONTEXT_AUTOREFERENCE_KEY = "context.autoreference.key";
+
+ /**
+ * Space gobbling mode
+ * @since 2.0
+ * @deprecated see {@link RuntimeConstants#SPACE_GOBBLING}
+ */
+ String OLD_SPACE_GOBBLING = "space.gobbling";
+
+ /**
+ * When displaying null or invalid non-quiet references, use the argument literal reference
+ * instead of the one in the macro block. Defaults to false.
+ * @since 2.1
+ * @Deprecated since 2.2, see {@link RuntimeConstants#VM_ENABLE_BC_MODE}
+ **/
+ String OLD_VM_ENABLE_BC_MODE = "velocimacro.arguments.preserve_literals";
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/ParserConfiguration.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/ParserConfiguration.java
new file mode 100644
index 00000000..6ec74b4a
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/ParserConfiguration.java
@@ -0,0 +1,116 @@
+package org.apache.velocity.runtime;
+
+/*
+ * 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.
+ */
+
+/**
+ * Class gathering configured replacement characters for a specific parser class.
+ * @since 2.2
+ */
+
+public class ParserConfiguration
+{
+ /**
+ * Configured replacement character for '$'
+ */
+ private char dollar = '$';
+
+ /**
+ * Configured replacement character for '#'
+ */
+ private char hash = '#';
+
+ /**
+ * Configured replacement character for '@'
+ */
+ private char at = '@';
+
+ /**
+ * Configured replacement character for '*'
+ */
+ private char asterisk = '*';
+
+ /**
+ * Getter for '$' configured replacement character
+ * @return configured replacement character for '$'
+ */
+ public char getDollarChar()
+ {
+ return dollar;
+ }
+
+ /**
+ * Setter for '$' configured replacement character
+ */
+ void setDollarChar(char dollar)
+ {
+ this.dollar = dollar;
+ }
+
+ /**
+ * Getter for '#' configured replacement character
+ * @return configured replacement character for '#'
+ */
+ public char getHashChar()
+ {
+ return hash;
+ }
+
+ /**
+ * Setter for '#' configured replacement character
+ */
+ void setHashChar(char hash)
+ {
+ this.hash = hash;
+ }
+
+ /**
+ * Getter for '@' configured replacement character
+ * @return configured replacement character for '@'
+ */
+ public char getAtChar()
+ {
+ return at;
+ }
+
+ /**
+ * Setter for '@' configured replacement character
+ */
+ void setAtChar(char at)
+ {
+ this.at = at;
+ }
+
+ /**
+ * Getter for '*' configured replacement character
+ * @return configured replacement character for '*'
+ */
+ public char getAsteriskChar()
+ {
+ return asterisk;
+ }
+
+ /**
+ * Setter for '*' configured replacement character
+ */
+ void setAsteriskChar(char asterisk)
+ {
+ this.asterisk = asterisk;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/ParserPool.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/ParserPool.java
new file mode 100644
index 00000000..0af4403c
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/ParserPool.java
@@ -0,0 +1,53 @@
+package org.apache.velocity.runtime;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.parser.Parser;
+
+
+/**
+ * Provides instances of parsers as needed. get() will return a new parser if
+ * available. If a parser is acquired from the pool, put() should be called
+ * with that parser to make it available again for reuse.
+ *
+ * @author <a href="mailto:sergek@lokitech.com">Serge Knystautas</a>
+ * @version $Id: RuntimeInstance.java 384374 2006-03-08 23:19:30Z nbubna $
+ * @since 1.5
+ */
+public interface ParserPool
+{
+ /**
+ * Initialize the pool so that it can begin serving parser instances.
+ * @param svc
+ */
+ void initialize(RuntimeServices svc);
+
+ /**
+ * Retrieve an instance of a parser pool.
+ * @return A parser object.
+ */
+ Parser get();
+
+ /**
+ * Return the parser to the pool so that it may be reused.
+ * @param parser
+ */
+ void put(Parser parser);
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/ParserPoolImpl.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/ParserPoolImpl.java
new file mode 100644
index 00000000..3934f2d0
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/ParserPoolImpl.java
@@ -0,0 +1,81 @@
+package org.apache.velocity.runtime;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.parser.CharStream;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.util.SimplePool;
+import org.slf4j.Logger;
+
+/**
+ * This wraps the original parser SimplePool class. It also handles
+ * instantiating ad-hoc parsers if none are available.
+ *
+ * @author <a href="mailto:sergek@lokitech.com">Serge Knystautas</a>
+ * @version $Id: RuntimeInstance.java 384374 2006-03-08 23:19:30Z nbubna $
+ * @since 1.5
+ */
+public class ParserPoolImpl implements ParserPool {
+
+ SimplePool pool = null;
+ int max = RuntimeConstants.NUMBER_OF_PARSERS;
+ Logger log;
+
+ /**
+ * Create the underlying "pool".
+ * @param rsvc
+ */
+ @Override
+ public void initialize(RuntimeServices rsvc)
+ {
+ log = rsvc.getLog("parser");
+ max = rsvc.getInt(RuntimeConstants.PARSER_POOL_SIZE, RuntimeConstants.NUMBER_OF_PARSERS);
+ pool = new SimplePool(max);
+
+ for (int i = 0; i < max; i++)
+ {
+ pool.put(rsvc.createNewParser());
+ }
+
+ log.debug("Created '{}' parsers.", max);
+ }
+
+ /**
+ * Call the wrapped pool. If none are available, it will create a new
+ * temporary one.
+ * @return A parser Object.
+ */
+ @Override
+ public Parser get()
+ {
+ return (Parser) pool.get();
+ }
+
+ /**
+ * Call the wrapped pool.
+ * @param parser
+ */
+ @Override
+ public void put(Parser parser)
+ {
+ parser.ReInit((CharStream) null);
+ pool.put(parser);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/Renderable.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/Renderable.java
new file mode 100644
index 00000000..157ba6a0
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/Renderable.java
@@ -0,0 +1,44 @@
+package org.apache.velocity.runtime;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * This interface characterize objects other than ASTNodes that can be rendered
+ * to a writer using a context.
+ *
+ * @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a>
+ * @version $Id:$
+ * @since 1.6
+ */
+
+public interface Renderable {
+
+ boolean render(InternalContextAdapter context, Writer writer)
+ throws IOException, MethodInvocationException, ParseErrorException, ResourceNotFoundException;
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java
new file mode 100644
index 00000000..89d3c41f
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java
@@ -0,0 +1,499 @@
+package org.apache.velocity.runtime;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * This class defines the keys that are used in the velocity.properties file so that they can be referenced as a constant within
+ * Java code.
+ *
+ * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @version $Id$
+ */
+
+public interface RuntimeConstants extends DeprecatedRuntimeConstants
+{
+ /*
+ * ----------------------------------------------------------------------
+ * These are public constants that are used as handles for the
+ * properties that can be specified in your typical
+ * velocity.properties file.
+ * ----------------------------------------------------------------------
+ */
+
+ /*
+ * ----------------------------------------------------------------------
+ * L O G G I N G C O N F I G U R A T I O N
+ * ----------------------------------------------------------------------
+ */
+
+ /** externally provided logger. */
+ String RUNTIME_LOG_INSTANCE = "runtime.log.instance";
+
+ /** externally provided logger name. */
+ String RUNTIME_LOG_NAME = "runtime.log.name";
+
+ /** Logging of invalid references. */
+ String RUNTIME_LOG_REFERENCE_LOG_INVALID = "runtime.log.log_invalid_references";
+
+ /** Logging of invalid method calls. */
+ String RUNTIME_LOG_METHOD_CALL_LOG_INVALID = "runtime.log.log_invalid_method_calls";
+
+ /** <p>Whether to:</p>
+ * <ul>
+ * <li>populate slf4j's MDC with location in template file</li>
+ * <li>display VTL stack trace on errors</li>
+ * </ul>
+ * @since 2.2
+ */
+ String RUNTIME_LOG_TRACK_LOCATION = "runtime.log.track_location";
+
+ /*
+ * ----------------------------------------------------------------------
+ * D I R E C T I V E C O N F I G U R A T I O N
+ * ----------------------------------------------------------------------
+ * Directive properties are of the form:
+ *
+ * directive.<directive-name>.<property>
+ * ----------------------------------------------------------------------
+ */
+
+ /** Maximum allowed number of loops. */
+ String MAX_NUMBER_LOOPS = "directive.foreach.max_loops";
+
+ /**
+ * Whether to throw an exception or just skip bad iterables. Default is true.
+ * @since 1.6
+ */
+ String SKIP_INVALID_ITERATOR = "directive.foreach.skip_invalid";
+
+ /**
+ * An empty object (string, collection) or zero number is false.
+ * @since 2.0
+ */
+ String CHECK_EMPTY_OBJECTS = "directive.if.empty_check";
+
+ /**
+ * Starting tag for error messages triggered by passing a parameter not allowed in the #include directive. Only string literals,
+ * and references are allowed.
+ * @deprecated if/how errors are displayed is not the concern of the engine, which should throw in all cases
+ */
+ String ERRORMSG_START = "directive.include.output_error_start";
+
+ /**
+ * Ending tag for error messages triggered by passing a parameter not allowed in the #include directive. Only string literals,
+ * and references are allowed.
+ * @deprecated if/how errors are displayed is not the concern of the engine, which should throw in all cases
+ */
+ String ERRORMSG_END = "directive.include.output_error_end";
+
+ /** Maximum recursion depth allowed for the #parse directive. */
+ String PARSE_DIRECTIVE_MAXDEPTH = "directive.parse.max_depth";
+
+ /** Maximum recursion depth allowed for the #define directive. */
+ String DEFINE_DIRECTIVE_MAXDEPTH = "directive.define.max_depth";
+
+ /**
+ * Used to suppress various scope control objects (property suffix).
+ * @since 1.7
+ * @deprecated use <code>context.scope_control.&lt;scope_name&gt; = true/false</code>
+ * @see #CONTEXT_SCOPE_CONTROL
+ */
+ @Deprecated
+ String PROVIDE_SCOPE_CONTROL = "provide.scope.control";
+
+ /**
+ * Used to enable or disable a scope control (false by default):
+ * <code>context.scope_control.&lt;scope_name&gt; = true/false</code>
+ * where <i>scope_name</i> is one of: <code>template, evaluate, foreach, macro, define</code>
+ * or the name of a body macro.
+ * @since 2.1
+ */
+ String CONTEXT_SCOPE_CONTROL = "context.scope_control";
+
+ /**
+ * Vector of custom directives
+ */
+ String CUSTOM_DIRECTIVES = "runtime.custom_directives";
+
+ /*
+ * ----------------------------------------------------------------------
+ * R E S O U R C E M A N A G E R C O N F I G U R A T I O N
+ * ----------------------------------------------------------------------
+ */
+
+ /**
+ * The <code>resource.manager.instance</code> property specifies an existing instance of a
+ * {@link org.apache.velocity.runtime.resource.ResourceManager} implementation to use
+ */
+ String RESOURCE_MANAGER_INSTANCE = "resource.manager.instance";
+
+ /**
+ * The <code>resource.manager.class</code> property specifies the name of the
+ * {@link org.apache.velocity.runtime.resource.ResourceManager} implementation to use.
+ */
+ String RESOURCE_MANAGER_CLASS = "resource.manager.class";
+
+ /**
+ * The <code>resource.manager.cache.class</code> property specifies the name of the
+ * {@link org.apache.velocity.runtime.resource.ResourceCache} implementation to use.
+ */
+ String RESOURCE_MANAGER_CACHE_CLASS = "resource.manager.cache.class";
+
+ /** The <code>resource.manager.cache.size</code> property specifies the cache upper bound (if relevant). */
+ String RESOURCE_MANAGER_DEFAULTCACHE_SIZE = "resource.manager.cache.default_size";
+
+ /*
+ * ----------------------------------------------------------------------
+ * R E S O U R C E L O A D E R C O N F I G U R A T I O N
+ * ----------------------------------------------------------------------
+ */
+
+ /** controls if the finding of a resource is logged. */
+ String RESOURCE_MANAGER_LOGWHENFOUND = "resource.manager.log_when_found";
+
+ /**
+ * Key used to retrieve the names of the resource loaders to be used. In a properties file they may appear as the following:
+ *
+ * <p>resource.loaders = file,classpath</p>
+ */
+ String RESOURCE_LOADERS = "resource.loaders";
+
+ /**
+ * Key prefix for a specific resource loader properties
+ *
+ * <p>resource.loader.file.path = ...</p>
+ */
+ String RESOURCE_LOADER = "resource.loader";
+
+ /** The public handle for setting paths in the FileResourceLoader.
+ * (this constant is used by test cases only)
+ */
+ String FILE_RESOURCE_LOADER_PATH = "resource.loader.file.path";
+
+ /** The public handle for turning the caching on in the FileResourceLoader. */
+ String FILE_RESOURCE_LOADER_CACHE = "resource.loader.file.cache";
+
+ /**
+ * Resource loader class property suffix
+ */
+ String RESOURCE_LOADER_CLASS = "class";
+
+ /**
+ * Resource loader instance property suffix
+ */
+ String RESOURCE_LOADER_INSTANCE = "instance";
+
+ /**
+ * Resource loader cache property suffix
+ */
+ String RESOURCE_LOADER_CACHE = "cache";
+
+ /**
+ * File resource loader paths property suffix
+ */
+ String RESOURCE_LOADER_PATHS = "path";
+
+ /**
+ * Resource loader modification check interval property suffix
+ */
+ String RESOURCE_LOADER_CHECK_INTERVAL = "modification_check_interval";
+
+ /**
+ * Datasource loader datasource url
+ */
+ String DS_RESOURCE_LOADER_DATASOURCE = "resource.loader.ds.resource.datasource_url";
+
+ /**
+ * Datasource loader templates table
+ */
+ String DS_RESOURCE_LOADER_TABLE = "resource.loader.ds.resource.table";
+
+ /**
+ * Datasource loader template key column
+ */
+ String DS_RESOURCE_LOADER_KEY_COLUMN = "resource.loader.ds.resource.key_column";
+
+ /**
+ * Datasource loader template content column
+ */
+ String DS_RESOURCE_LOADER_TEMPLATE_COLUMN = "resource.loader.ds.resource.template_column";
+
+ /**
+ * Datasource loader template timestamp column
+ */
+ String DS_RESOURCE_LOADER_TIMESTAMP_COLUMN = "resource.loader.ds.resource.timestamp_column";
+
+ /** The default character encoding for the templates. Used by the parser in processing the input streams. */
+ String INPUT_ENCODING = "resource.default_encoding";
+
+ /** Default Encoding is UTF-8. */
+ String ENCODING_DEFAULT = "UTF-8";
+
+ /*
+ * ----------------------------------------------------------------------
+ * E V E N T H A N D L E R C O N F I G U R A T I O N
+ * ----------------------------------------------------------------------
+ */
+
+ /**
+ * The <code>event_handler.reference_insertion.class</code> property specifies a list of the
+ * {@link org.apache.velocity.app.event.ReferenceInsertionEventHandler} implementations to use.
+ */
+ String EVENTHANDLER_REFERENCEINSERTION = "event_handler.reference_insertion.class";
+
+ /**
+ * The <code>event_handler.method_exception.class</code> property specifies a list of the
+ * {@link org.apache.velocity.app.event.MethodExceptionEventHandler} implementations to use.
+ */
+ String EVENTHANDLER_METHODEXCEPTION = "event_handler.method_exception.class";
+
+ /**
+ * The <code>event_handler.include.class</code> property specifies a list of the
+ * {@link org.apache.velocity.app.event.IncludeEventHandler} implementations to use.
+ */
+ String EVENTHANDLER_INCLUDE = "event_handler.include.class";
+
+ /**
+ * The <code>event_handler.invalid_references.class</code> property specifies a list of the
+ * {@link org.apache.velocity.app.event.InvalidReferenceEventHandler} implementations to use.
+ */
+ String EVENTHANDLER_INVALIDREFERENCES = "event_handler.invalid_references.class";
+
+ /**
+ * The <code>event_handler.invalid_references.quiet</code> property specifies if invalid quiet references
+ * (as in <code>$!foo</code>) trigger events (defaults to false).
+ * {@link org.apache.velocity.app.event.InvalidReferenceEventHandler} implementations to use.
+ * @since 2.2
+ */
+ String EVENTHANDLER_INVALIDREFERENCES_QUIET = "event_handler.invalid_references.quiet";
+
+ /**
+ * The <code>event_handler.invalid_references.null</code> property specifies if invalid null references
+ * (aka the value is present in the context or parent object but is null or a method returned null)
+ * trigger invalid reference events (defaults to false).
+ * {@link org.apache.velocity.app.event.InvalidReferenceEventHandler} implementations to use.
+ * @since 2.2
+ */
+ String EVENTHANDLER_INVALIDREFERENCES_NULL = "event_handler.invalid_references.null";
+
+ /**
+ * The <code>event_handler.invalid_references.tested</code> property specifies if invalid tested references
+ * (as in <code>#if($foo)</code> ) trigger invalid reference events (defaults to false).
+ * {@link org.apache.velocity.app.event.InvalidReferenceEventHandler} implementations to use.
+ * @since 2.2
+ */
+ String EVENTHANDLER_INVALIDREFERENCES_TESTED = "event_handler.invalid_references.tested";
+
+ /*
+ * ----------------------------------------------------------------------
+ * V E L O C I M A C R O C O N F I G U R A T I O N
+ * ----------------------------------------------------------------------
+ */
+
+ /** Filename of local Velocimacro library template. */
+ String VM_LIBRARY = "velocimacro.library.path";
+
+ /** Default Velocimacro library template. */
+ String VM_LIBRARY_DEFAULT = "velocimacros.vtl";
+
+ /** switch for autoloading library-sourced VMs (for development). */
+ String VM_LIBRARY_AUTORELOAD = "velocimacro.library.autoreload";
+
+ /** boolean (true/false) default true: allow inline (in-template) macro definitions. */
+ String VM_PERM_ALLOW_INLINE = "velocimacro.inline.allow";
+
+ /** boolean (true/false) default false: allow inline (in-template) macro definitions to replace existing. */
+ String VM_PERM_ALLOW_INLINE_REPLACE_GLOBAL = "velocimacro.inline.replace_global";
+
+ /** Switch for forcing inline macros to be local: default false. */
+ String VM_PERM_INLINE_LOCAL = "velocimacro.inline.local_scope";
+
+ /** if true, throw an exception for wrong number of arguments **/
+ String VM_ARGUMENTS_STRICT = "velocimacro.arguments.strict";
+
+ /**
+ * This flag enable the 1.7 backward compatible mode for velocimacros (defaults to false):
+ * <ul>
+ * <li>
+ * preserve argument literals: when displaying null or invalid non-quiet references,
+ * use the argument literal reference instead of the one in the macro block. Defaults to false.
+ * </li>
+ * <li>
+ * use global values for missing arguments: when calling a macro with fewer arguments than declared,
+ * if those arguments don't have an explicit default value in the macro definition, default values will
+ * be looked for in the global context
+ * </li>
+ * </ul>
+ * @since 2.2
+ */
+ String VM_ENABLE_BC_MODE = "velocimacro.enable_bc_mode";
+
+ /**
+ * Specify the maximum depth for macro calls
+ * @since 1.6
+ */
+ String VM_MAX_DEPTH = "velocimacro.max_depth";
+
+ /**
+ * Defines name of the reference that can be used to get the AST block passed to block macro calls.
+ * @since 1.7
+ */
+ String VM_BODY_REFERENCE = "velocimacro.body_reference";
+
+ /**
+ * <p>Switch for VM blather: default true. Unused since 2.0.</p>
+ * @deprecated since 2.1
+ */
+ @Deprecated
+ String VM_MESSAGES_ON = "velocimacro.messages.on";
+
+ /*
+ * ----------------------------------------------------------------------
+ * S T I C T M O D E B E H A V I O U R
+ * ----------------------------------------------------------------------
+ */
+
+ /**
+ * Properties referenced in the template are required to exist the object
+ */
+ String RUNTIME_REFERENCES_STRICT = "runtime.strict_mode.enable";
+
+ /**
+ * Indicates we are going to use modified escape behavior in strict mode
+ */
+ String RUNTIME_REFERENCES_STRICT_ESCAPE = "runtime.strict_mode.escape";
+
+ /*
+ * ----------------------------------------------------------------------
+ * I N T R O S P E C T I O N C O N F I G U R A T I O N
+ * ----------------------------------------------------------------------
+ */
+
+ /** key name for uberspector. Multiple classnames can be specified,in which case uberspectors will be chained. */
+ String UBERSPECT_CLASSNAME = "introspector.uberspect.class";
+
+ /** A comma separated list of packages to restrict access to in the SecureIntrospector. */
+ String INTROSPECTOR_RESTRICT_PACKAGES = "introspector.restrict.packages";
+
+ /** A comma separated list of classes to restrict access to in the SecureIntrospector. */
+ String INTROSPECTOR_RESTRICT_CLASSES = "introspector.restrict.classes";
+
+ /** key for Conversion Manager class */
+ String CONVERSION_HANDLER_CLASS = "introspector.conversion_handler.class";
+
+ /** key for Conversion Manager instance */
+ String CONVERSION_HANDLER_INSTANCE = "introspector.conversion_handler.instance";
+
+ /*
+ * ----------------------------------------------------------------------
+ * P A R S E R C O N F I G U R A T I O N
+ * ----------------------------------------------------------------------
+ */
+
+ /**
+ * Property specifying the parser class to use
+ * @since 2.2
+ */
+ String PARSER_CLASS = "parser.class";
+
+ /**
+ * Default parser class
+ * @since 2.2
+ */
+ String DEFAULT_PARSER_CLASS = "org.apache.velocity.runtime.parser.StandardParser";
+
+ /**
+ * The <code>parser.pool.class</code> property specifies the name of the {@link org.apache.velocity.util.SimplePool}
+ * implementation to use.
+ */
+ String PARSER_POOL_CLASS = "parser.pool.class";
+
+ /**
+ * @see #NUMBER_OF_PARSERS
+ */
+ String PARSER_POOL_SIZE = "parser.pool.size";
+
+ /**
+ * Allow hyphen in identifiers (backward compatibility option)
+ * @since 2.1
+ */
+ String PARSER_HYPHEN_ALLOWED = "parser.allow_hyphen_in_identifiers";
+
+ /*
+ * ----------------------------------------------------------------------
+ * G E N E R A L R U N T I M E C O N F I G U R A T I O N
+ * ----------------------------------------------------------------------
+ */
+
+ /** Whether to use string interning. */
+ String RUNTIME_STRING_INTERNING = "runtime.string_interning";
+
+ /** Switch for the interpolation facility for string literals. */
+ String INTERPOLATE_STRINGLITERALS = "runtime.interpolate_string_literals";
+
+ /** Switch for ignoring nulls in math equations vs throwing exceptions. */
+ String STRICT_MATH = "runtime.strict_math";
+
+ /** Key upon which a context should be accessible within itself */
+ String CONTEXT_AUTOREFERENCE_KEY = "context.self_reference_key";
+
+ /**
+ * Space gobbling mode
+ * @since 2.0
+ */
+ String SPACE_GOBBLING = "parser.space_gobbling";
+
+ /**
+ * Space gobbling modes
+ * @since 2.0
+ */
+ enum SpaceGobbling
+ {
+ NONE, BC, LINES, STRUCTURED
+ }
+
+ /*
+ * ----------------------------------------------------------------------
+ * These constants are used internally by the Velocity runtime i.e.
+ * the constants listed below are strictly used in the Runtime
+ * class itself.
+ * ----------------------------------------------------------------------
+ */
+
+ /** Default Runtime properties. */
+ String DEFAULT_RUNTIME_PROPERTIES = "org/apache/velocity/runtime/defaults/velocity.properties";
+
+ /** Default Runtime properties. */
+ String DEFAULT_RUNTIME_DIRECTIVES = "org/apache/velocity/runtime/defaults/directive.properties";
+
+ /** externally provided logger name. */
+ String DEFAULT_RUNTIME_LOG_NAME = "org.apache.velocity";
+
+ /** token used to identify the loader internally. */
+ String RESOURCE_LOADER_IDENTIFIER = "_RESOURCE_LOADER_IDENTIFIER_";
+
+ /**
+ * The default number of parser instances to create. Configurable via the parameter named by the {@link #PARSER_POOL_SIZE}
+ * constant.
+ */
+ int NUMBER_OF_PARSERS = 20;
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeInstance.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeInstance.java
new file mode 100644
index 00000000..3d77055d
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeInstance.java
@@ -0,0 +1,2025 @@
+package org.apache.velocity.runtime;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+import org.apache.velocity.app.event.EventCartridge;
+import org.apache.velocity.app.event.EventHandler;
+import org.apache.velocity.app.event.IncludeEventHandler;
+import org.apache.velocity.app.event.InvalidReferenceEventHandler;
+import org.apache.velocity.app.event.MethodExceptionEventHandler;
+import org.apache.velocity.app.event.ReferenceInsertionEventHandler;
+import org.apache.velocity.context.Context;
+import org.apache.velocity.context.InternalContextAdapterImpl;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.directive.Directive;
+import org.apache.velocity.runtime.directive.Macro;
+import org.apache.velocity.runtime.directive.Scope;
+import org.apache.velocity.runtime.directive.StopCommand;
+import org.apache.velocity.runtime.parser.LogContext;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.runtime.parser.node.Node;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+import org.apache.velocity.runtime.resource.ContentResource;
+import org.apache.velocity.runtime.resource.ResourceManager;
+import org.apache.velocity.util.ClassUtils;
+import org.apache.velocity.util.ExtProperties;
+import org.apache.velocity.util.RuntimeServicesAware;
+import org.apache.velocity.util.introspection.ChainableUberspector;
+import org.apache.velocity.util.introspection.LinkingUberspector;
+import org.apache.velocity.util.introspection.Uberspect;
+
+import org.apache.commons.lang3.StringUtils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.Writer;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * <p>This is the Runtime system for Velocity. It is the
+ * single access point for all functionality in Velocity.
+ * It adheres to the mediator pattern and is the only
+ * structure that developers need to be familiar with
+ * in order to get Velocity to perform.</p>
+ *
+ * <p>The Runtime will also cooperate with external
+ * systems, which can make all needed setProperty() calls
+ * before calling init().</p>
+ * <pre>
+ * -----------------------------------------------------------------------
+ * N O T E S O N R U N T I M E I N I T I A L I Z A T I O N
+ * -----------------------------------------------------------------------
+ * init()
+ *
+ * If init() is called by itself the RuntimeInstance will initialize
+ * with a set of default values.
+ * -----------------------------------------------------------------------
+ * init(String/Properties)
+ *
+ * In this case the default velocity properties are layed down
+ * first to provide a solid base, then any properties provided
+ * in the given properties object will override the corresponding
+ * default property.
+ * -----------------------------------------------------------------------
+ * </pre>
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:jlb@houseofdistraction.com">Jeff Bowden</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magusson Jr.</a>
+ * @version $Id$
+ */
+public class RuntimeInstance implements RuntimeConstants, RuntimeServices
+{
+ /**
+ * VelocimacroFactory object to manage VMs
+ */
+ private VelocimacroFactory vmFactory = null;
+
+ /**
+ * The Runtime logger. The default instance is the "org.apache.velocity" logger.
+ */
+ private Logger log = LoggerFactory.getLogger(DEFAULT_RUNTIME_LOG_NAME);
+
+ /**
+ * The Runtime parser pool
+ */
+ private ParserPool parserPool;
+
+ /**
+ * Indicate whether the Runtime is in the midst of initialization.
+ */
+ private boolean initializing = false;
+
+ /**
+ * Indicate whether the Runtime has been fully initialized.
+ */
+ private volatile boolean initialized = false;
+
+ /**
+ * These are the properties that are laid down over top
+ * of the default properties when requested.
+ */
+ private ExtProperties overridingProperties = null;
+
+ /**
+ * This is a hashtable of initialized directives.
+ * The directives that populate this hashtable are
+ * taken from the RUNTIME_DEFAULT_DIRECTIVES
+ * property file.
+ */
+ private Map<String, Directive> runtimeDirectives = new Hashtable<>();
+ /**
+ * Copy of the actual runtimeDirectives that is shared between
+ * parsers. Whenever directives are updated, the synchronized
+ * runtimeDirectives is first updated and then an unsynchronized
+ * copy of it is passed to parsers.
+ */
+ private Map<String, Directive> runtimeDirectivesShared;
+
+ /**
+ * Object that houses the configuration options for
+ * the velocity runtime. The ExtProperties object allows
+ * the convenient retrieval of a subset of properties.
+ * For example all the properties for a resource loader
+ * can be retrieved from the main ExtProperties object
+ * using something like the following:
+ *
+ * ExtProperties loaderConfiguration =
+ * configuration.subset(loaderID);
+ *
+ * And a configuration is a lot more convenient to deal
+ * with then conventional properties objects, or Maps.
+ */
+ private ExtProperties configuration = new ExtProperties();
+
+ private ResourceManager resourceManager = null;
+
+ /**
+ * This stores the engine-wide set of event handlers. Event handlers for
+ * each specific merge are stored in the context.
+ */
+ private EventCartridge eventCartridge = null;
+
+ /**
+ * Whether to use string interning
+ */
+ private boolean stringInterning = false;
+
+ /**
+ * Scope name for evaluate(...) calls.
+ */
+ private String evaluateScopeName = "evaluate";
+
+ /**
+ * Scope names for which to provide scope control objects in the context
+ */
+ private Set<String> enabledScopeControls = new HashSet<>();
+
+ /**
+ * Opaque reference to something specified by the
+ * application for use in application supplied/specified
+ * pluggable components
+ */
+ private Map<Object, Object> applicationAttributes = null;
+
+ /**
+ * Uberspector
+ */
+ private Uberspect uberSpect;
+
+ /**
+ * Default encoding
+ */
+ private String defaultEncoding;
+
+ /**
+ * Space gobbling mode
+ */
+ private SpaceGobbling spaceGobbling;
+
+ /**
+ * Whether hyphen is allowed in identifiers
+ */
+ private boolean hyphenAllowedInIdentifiers;
+
+ /**
+ * The LogContext object used to track location in templates
+ */
+ private LogContext logContext;
+
+ /**
+ * Configured parser class
+ * @since 2.2
+ */
+ private Constructor<? extends Parser> parserConstructor;
+
+ /**
+ * Configured replacement characters in parser grammar
+ * @since 2.2
+ */
+ private ParserConfiguration parserConfiguration;
+
+ /**
+ * Creates a new RuntimeInstance object.
+ */
+ public RuntimeInstance()
+ {
+ reset();
+ }
+
+ /**
+ * This is the primary initialization method in the Velocity
+ * Runtime. The systems that are setup/initialized here are
+ * as follows:
+ *
+ * <ul>
+ * <li>Logging System</li>
+ * <li>ResourceManager</li>
+ * <li>EventHandler</li>
+ * <li>Parser Pool</li>
+ * <li>Global Cache</li>
+ * <li>Static Content Include System</li>
+ * <li>Velocimacro System</li>
+ * </ul>
+ */
+ @Override
+ public synchronized void init()
+ {
+ if (!initialized && !initializing)
+ {
+ try
+ {
+ log.debug("Initializing Velocity, Calling init()...");
+ initializing = true;
+
+ log.trace("*****************************");
+ log.debug("Starting Apache Velocity v" + VelocityEngineVersion.VERSION);
+ log.trace("RuntimeInstance initializing.");
+
+ initializeProperties();
+ initializeSelfProperties();
+ initializeLog();
+ initializeResourceManager();
+ initializeDirectives();
+ initializeEventHandlers();
+ initializeParserPool();
+
+ initializeIntrospection();
+ initializeScopeSettings();
+ /*
+ * initialize the VM Factory. It will use the properties
+ * accessible from Runtime, so keep this here at the end.
+ */
+ vmFactory.initVelocimacro();
+
+ log.trace("RuntimeInstance successfully initialized.");
+
+ initialized = true;
+ initializing = false;
+ }
+ catch(RuntimeException re)
+ {
+ // initialization failed at some point... try to reset everything
+ try
+ {
+ reset();
+ }
+ catch(RuntimeException re2) {} // prefer throwing the original exception
+ throw re;
+ }
+ finally
+ {
+ initializing = false;
+ }
+ }
+ }
+
+ /**
+ * Resets the instance, so Velocity can be re-initialized again.
+ *
+ * @since 2.0.0
+ */
+ public synchronized void reset()
+ {
+ this.configuration = new ExtProperties();
+ this.defaultEncoding = null;
+ this.evaluateScopeName = "evaluate";
+ this.eventCartridge = null;
+ this.initialized = false;
+ this.initializing = false;
+ this.overridingProperties = null;
+ this.parserPool = null;
+ this.enabledScopeControls.clear();
+ this.resourceManager = null;
+ this.runtimeDirectives = new Hashtable<>();
+ this.runtimeDirectivesShared = null;
+ this.uberSpect = null;
+ this.stringInterning = false;
+ this.parserConfiguration = new ParserConfiguration();
+
+ /*
+ * create a VM factory, introspector, and application attributes
+ */
+ vmFactory = new VelocimacroFactory( this );
+
+ /*
+ * and a store for the application attributes
+ */
+ applicationAttributes = new HashMap<>();
+ }
+
+ /**
+ * Returns true if the RuntimeInstance has been successfully initialized.
+ * @return True if the RuntimeInstance has been successfully initialized.
+ * @since 1.5
+ */
+ @Override
+ public boolean isInitialized()
+ {
+ return initialized;
+ }
+
+ /**
+ * Init or die! (with some log help, of course)
+ */
+ private void requireInitialization()
+ {
+ if (!initialized)
+ {
+ try
+ {
+ init();
+ }
+ catch (Exception e)
+ {
+ log.error("Could not auto-initialize Velocity", e);
+ throw new RuntimeException("Velocity could not be initialized!", e);
+ }
+ }
+ }
+
+ /**
+ * Initialize runtime internal properties
+ */
+ private void initializeSelfProperties()
+ {
+ /* initialize string interning (defaults to false) */
+ stringInterning = getBoolean(RUNTIME_STRING_INTERNING, true);
+
+ /* initialize indentation mode (defaults to 'lines') */
+ String im = getString(SPACE_GOBBLING, "lines");
+ try
+ {
+ spaceGobbling = SpaceGobbling.valueOf(im.toUpperCase(Locale.ROOT));
+ }
+ catch (NoSuchElementException nse)
+ {
+ spaceGobbling = SpaceGobbling.LINES;
+ }
+
+ /* init parser behavior */
+ hyphenAllowedInIdentifiers = getBoolean(PARSER_HYPHEN_ALLOWED, false);
+ }
+
+ private char getConfiguredCharacter(String configKey, char defaultChar)
+ {
+ String configuredChar = getString(configKey);
+ if (configuredChar != null)
+ {
+ if (configuredChar.length() != 1)
+ {
+ throw new IllegalArgumentException(String.format("value of '%s' must be a single character string, but is '%s'", configKey, configuredChar));
+ }
+ return configuredChar.charAt(0);
+ }
+ return defaultChar;
+ }
+
+ /**
+ * Gets the classname for the Uberspect introspection package and
+ * instantiates an instance.
+ */
+ private void initializeIntrospection()
+ {
+ String[] uberspectors = configuration.getStringArray(RuntimeConstants.UBERSPECT_CLASSNAME);
+ for (String rm : uberspectors)
+ {
+ Object o = null;
+
+ try
+ {
+ o = ClassUtils.getNewInstance(rm);
+ }
+ catch (ClassNotFoundException cnfe)
+ {
+ String err = "The specified class for Uberspect (" + rm
+ + ") does not exist or is not accessible to the current classloader.";
+ log.error(err);
+ throw new VelocityException(err, cnfe);
+ }
+ catch (InstantiationException ie)
+ {
+ throw new VelocityException("Could not instantiate class '" + rm + "'", ie);
+ }
+ catch (IllegalAccessException ae)
+ {
+ throw new VelocityException("Cannot access class '" + rm + "'", ae);
+ }
+
+ if (!(o instanceof Uberspect))
+ {
+ String err = "The specified class for Uberspect ("
+ + rm + ") does not implement " + Uberspect.class.getName()
+ + "; Velocity is not initialized correctly.";
+
+ log.error(err);
+ throw new VelocityException(err);
+ }
+
+ Uberspect u = (Uberspect) o;
+
+ if (u instanceof RuntimeServicesAware)
+ {
+ ((RuntimeServicesAware) u).setRuntimeServices(this);
+ }
+
+ if (uberSpect == null)
+ {
+ uberSpect = u;
+ } else
+ {
+ if (u instanceof ChainableUberspector)
+ {
+ ((ChainableUberspector) u).wrap(uberSpect);
+ uberSpect = u;
+ } else
+ {
+ uberSpect = new LinkingUberspector(uberSpect, u);
+ }
+ }
+ }
+
+ if(uberSpect != null)
+ {
+ uberSpect.init();
+ }
+ else
+ {
+ /*
+ * someone screwed up. Lets not fool around...
+ */
+
+ String err = "It appears that no class was specified as the"
+ + " Uberspect. Please ensure that all configuration"
+ + " information is correct.";
+
+ log.error(err);
+ throw new VelocityException(err);
+ }
+ }
+
+ /**
+ * Initializes the Velocity Runtime with properties file.
+ * The properties file may be in the file system proper,
+ * or the properties file may be in the classpath.
+ */
+ private void setDefaultProperties()
+ {
+ InputStream inputStream = null;
+ try
+ {
+ inputStream = getClass().getClassLoader()
+ .getResourceAsStream(DEFAULT_RUNTIME_PROPERTIES);
+
+ if (inputStream == null)
+ throw new IOException("Resource not found: " + DEFAULT_RUNTIME_PROPERTIES);
+
+ configuration.load( inputStream );
+
+ /* populate 'defaultEncoding' member */
+ defaultEncoding = getString(INPUT_ENCODING, ENCODING_DEFAULT);
+
+ log.debug("Default Properties resource: {}", DEFAULT_RUNTIME_PROPERTIES);
+ }
+ catch (IOException ioe)
+ {
+ String msg = "Cannot get Velocity Runtime default properties!";
+ log.error(msg, ioe);
+ throw new RuntimeException(msg, ioe);
+ }
+ finally
+ {
+ try
+ {
+ if (inputStream != null)
+ {
+ inputStream.close();
+ }
+ }
+ catch (IOException ioe)
+ {
+ String msg = "Cannot close Velocity Runtime default properties!";
+ log.error(msg, ioe);
+ throw new RuntimeException(msg, ioe);
+ }
+ }
+ }
+
+ /**
+ * Allows an external system to set a property in
+ * the Velocity Runtime.
+ *
+ * @param key property key
+ * @param value property value
+ */
+ @Override
+ public void setProperty(String key, Object value)
+ {
+ if (overridingProperties == null)
+ {
+ overridingProperties = new ExtProperties();
+ }
+
+ overridingProperties.setProperty(key, value);
+ }
+
+
+ /**
+ * Add all properties contained in the file fileName to the RuntimeInstance properties
+ * @param fileName
+ */
+ public void setProperties(String fileName)
+ {
+ ExtProperties props = null;
+ try
+ {
+ props = new ExtProperties(fileName);
+ }
+ catch (IOException e)
+ {
+ throw new VelocityException("Error reading properties from '"
+ + fileName + "'", e);
+ }
+
+ Enumeration<String> en = props.keys();
+ while (en.hasMoreElements())
+ {
+ String key = en.nextElement();
+ setProperty(key, props.get(key));
+ }
+ }
+
+
+ /**
+ * Add all the properties in props to the RuntimeInstance properties
+ * @param props
+ */
+ public void setProperties(Properties props)
+ {
+ Enumeration en = props.keys();
+ while (en.hasMoreElements())
+ {
+ String key = en.nextElement().toString();
+ setProperty(key, props.get(key));
+ }
+ }
+
+ /**
+ * Allow an external system to set an ExtProperties
+ * object to use.
+ *
+ * @param configuration
+ * @since 2.0
+ */
+ @Override
+ public void setConfiguration(ExtProperties configuration)
+ {
+ if (overridingProperties == null)
+ {
+ overridingProperties = configuration;
+ }
+ else
+ {
+ // Avoid possible ConcurrentModificationException
+ if (overridingProperties != configuration)
+ {
+ overridingProperties.combine(configuration);
+ }
+ }
+ }
+
+ /**
+ * Add a property to the configuration. If it already
+ * exists then the value stated here will be added
+ * to the configuration entry. For example, if
+ *
+ * resource.loader = file
+ *
+ * is already present in the configuration and you
+ *
+ * addProperty("resource.loader", "classpath")
+ *
+ * Then you will end up with a Vector like the
+ * following:
+ *
+ * ["file", "classpath"]
+ *
+ * @param key
+ * @param value
+ */
+ @Override
+ public void addProperty(String key, Object value)
+ {
+ if (overridingProperties == null)
+ {
+ overridingProperties = new ExtProperties();
+ }
+
+ overridingProperties.addProperty(key, value);
+ }
+
+ /**
+ * Clear the values pertaining to a particular
+ * property.
+ *
+ * @param key of property to clear
+ */
+ @Override
+ public void clearProperty(String key)
+ {
+ if (overridingProperties != null)
+ {
+ overridingProperties.clearProperty(key);
+ }
+ }
+
+ /**
+ * Allows an external caller to get a property. The calling
+ * routine is required to know the type, as this routine
+ * will return an Object, as that is what properties can be.
+ *
+ * @param key property to return
+ * @return Value of the property or null if it does not exist.
+ */
+ @Override
+ public Object getProperty(String key)
+ {
+ Object o = null;
+
+ /*
+ * Before initialization, check the user-entered properties first.
+ */
+ if (!initialized && overridingProperties != null)
+ {
+ o = overridingProperties.get(key);
+ }
+
+ /*
+ * After initialization, configuration will hold all properties.
+ */
+ if (o == null)
+ {
+ o = configuration.getProperty(key);
+ }
+ if (o instanceof String)
+ {
+ return StringUtils.trim((String) o);
+ }
+ else
+ {
+ return o;
+ }
+ }
+
+ /**
+ * Initialize Velocity properties, if the default
+ * properties have not been laid down first then
+ * do so. Then proceed to process any overriding
+ * properties. Laying down the default properties
+ * gives a much greater chance of having a
+ * working system.
+ */
+ private void initializeProperties()
+ {
+ /*
+ * Always lay down the default properties first as
+ * to provide a solid base.
+ */
+ if ( !configuration.isInitialized() )
+ {
+ setDefaultProperties();
+ }
+
+ if( overridingProperties != null )
+ {
+ configuration.combine(overridingProperties);
+
+ /* reinitialize defaultEncoding in case it is overridden */
+ defaultEncoding = getString(INPUT_ENCODING, ENCODING_DEFAULT);
+ }
+ }
+
+ /**
+ * Initialize the Velocity Runtime with a Properties
+ * object.
+ *
+ * @param p Velocity properties for initialization
+ */
+ @Override
+ public void init(Properties p)
+ {
+ setConfiguration(ExtProperties.convertProperties(p));
+ init();
+ }
+
+ /**
+ * Initialize the Velocity Runtime with a
+ * properties file path.
+ *
+ * @param configurationFile
+ */
+ @Override
+ public void init(String configurationFile)
+ {
+ setProperties(configurationFile);
+ init();
+ }
+
+ private void initializeResourceManager()
+ {
+ /*
+ * Which resource manager?
+ */
+ Object inst = getProperty(RuntimeConstants.RESOURCE_MANAGER_INSTANCE);
+ String rm = getString(RuntimeConstants.RESOURCE_MANAGER_CLASS);
+
+ if (inst != null)
+ {
+ if (ResourceManager.class.isAssignableFrom(inst.getClass()))
+ {
+ resourceManager = (ResourceManager)inst;
+ resourceManager.initialize(this);
+ }
+ else
+ {
+ String msg = inst.getClass().getName() + " object set as resource.manager.instance is not a valid org.apache.velocity.runtime.resource.ResourceManager.";
+ log.error(msg);
+ throw new VelocityException(msg);
+ }
+ }
+ else if (rm != null && rm.length() > 0)
+ {
+ /*
+ * if something was specified, then make one.
+ * if that isn't a ResourceManager, consider
+ * this a huge error and throw
+ */
+
+ Object o = null;
+
+ try
+ {
+ o = ClassUtils.getNewInstance( rm );
+ }
+ catch (ClassNotFoundException cnfe )
+ {
+ String err = "The specified class for ResourceManager (" + rm
+ + ") does not exist or is not accessible to the current classloader.";
+ log.error(err);
+ throw new VelocityException(err, cnfe);
+ }
+ catch (InstantiationException ie)
+ {
+ throw new VelocityException("Could not instantiate class '" + rm + "'", ie);
+ }
+ catch (IllegalAccessException ae)
+ {
+ throw new VelocityException("Cannot access class '" + rm + "'", ae);
+ }
+
+ if (!(o instanceof ResourceManager))
+ {
+ String err = "The specified class for ResourceManager (" + rm
+ + ") does not implement " + ResourceManager.class.getName()
+ + "; Velocity is not initialized correctly.";
+
+ log.error(err);
+ throw new VelocityException(err);
+ }
+
+ resourceManager = (ResourceManager) o;
+ resourceManager.initialize(this);
+ setProperty(RESOURCE_MANAGER_INSTANCE, resourceManager);
+ }
+ else
+ {
+ /*
+ * someone screwed up. Lets not fool around...
+ */
+
+ String err = "It appears that no class or instance was specified as the"
+ + " ResourceManager. Please ensure that all configuration"
+ + " information is correct.";
+
+ log.error(err);
+ throw new VelocityException( err );
+ }
+ }
+
+ private void initializeEventHandlers()
+ {
+
+ eventCartridge = new EventCartridge();
+ eventCartridge.setRuntimeServices(this);
+
+ /*
+ * For each type of event handler, get the class name, instantiate it, and store it.
+ */
+
+ String[] referenceinsertion = configuration.getStringArray(RuntimeConstants.EVENTHANDLER_REFERENCEINSERTION);
+ if ( referenceinsertion != null )
+ {
+ for (String aReferenceinsertion : referenceinsertion)
+ {
+ EventHandler ev = initializeSpecificEventHandler(aReferenceinsertion, RuntimeConstants.EVENTHANDLER_REFERENCEINSERTION, ReferenceInsertionEventHandler.class);
+ if (ev != null)
+ eventCartridge.addReferenceInsertionEventHandler((ReferenceInsertionEventHandler) ev);
+ }
+ }
+
+ String[] methodexception = configuration.getStringArray(RuntimeConstants.EVENTHANDLER_METHODEXCEPTION);
+ if ( methodexception != null )
+ {
+ for (String aMethodexception : methodexception)
+ {
+ EventHandler ev = initializeSpecificEventHandler(aMethodexception, RuntimeConstants.EVENTHANDLER_METHODEXCEPTION, MethodExceptionEventHandler.class);
+ if (ev != null)
+ eventCartridge.addMethodExceptionHandler((MethodExceptionEventHandler) ev);
+ }
+ }
+
+ String[] includeHandler = configuration.getStringArray(RuntimeConstants.EVENTHANDLER_INCLUDE);
+ if ( includeHandler != null )
+ {
+ for (String anIncludeHandler : includeHandler)
+ {
+ EventHandler ev = initializeSpecificEventHandler(anIncludeHandler, RuntimeConstants.EVENTHANDLER_INCLUDE, IncludeEventHandler.class);
+ if (ev != null)
+ eventCartridge.addIncludeEventHandler((IncludeEventHandler) ev);
+ }
+ }
+
+ String[] invalidReferenceSet = configuration.getStringArray(RuntimeConstants.EVENTHANDLER_INVALIDREFERENCES);
+ if ( invalidReferenceSet != null )
+ {
+ for (String anInvalidReferenceSet : invalidReferenceSet)
+ {
+ EventHandler ev = initializeSpecificEventHandler(anInvalidReferenceSet, RuntimeConstants.EVENTHANDLER_INVALIDREFERENCES, InvalidReferenceEventHandler.class);
+ if (ev != null)
+ {
+ eventCartridge.addInvalidReferenceEventHandler((InvalidReferenceEventHandler) ev);
+ }
+ }
+ }
+
+
+ }
+
+ private EventHandler initializeSpecificEventHandler(String classname, String paramName, Class<?> EventHandlerInterface)
+ {
+ if ( classname != null && classname.length() > 0)
+ {
+ Object o = null;
+ try
+ {
+ o = ClassUtils.getNewInstance(classname);
+ }
+ catch (ClassNotFoundException cnfe )
+ {
+ String err = "The specified class for "
+ + paramName + " (" + classname
+ + ") does not exist or is not accessible to the current classloader.";
+ log.error(err);
+ throw new VelocityException(err, cnfe);
+ }
+ catch (InstantiationException ie)
+ {
+ throw new VelocityException("Could not instantiate class '" + classname + "'", ie);
+ }
+ catch (IllegalAccessException ae)
+ {
+ throw new VelocityException("Cannot access class '" + classname + "'", ae);
+ }
+
+ if (!EventHandlerInterface.isAssignableFrom(EventHandlerInterface))
+ {
+ String err = "The specified class for " + paramName + " ("
+ + classname + ") does not implement "
+ + EventHandlerInterface.getName()
+ + "; Velocity is not initialized correctly.";
+
+ log.error(err);
+ throw new VelocityException(err);
+ }
+
+ EventHandler ev = (EventHandler) o;
+ if ( ev instanceof RuntimeServicesAware )
+ ((RuntimeServicesAware) ev).setRuntimeServices(this);
+ return ev;
+
+ } else
+ return null;
+ }
+
+ /**
+ * Initialize the Velocity logging system.
+ */
+ private void initializeLog()
+ {
+ // if we were provided a specific logger or logger name, let's use it
+ try
+ {
+ /* If a Logger instance was set as a configuration
+ * value, use that. This is any class the user specifies.
+ */
+ Object o = getProperty(RuntimeConstants.RUNTIME_LOG_INSTANCE);
+ if (o != null)
+ {
+ // check for a Logger
+ if (Logger.class.isAssignableFrom(o.getClass()))
+ {
+ //looks ok
+ log = (Logger)o;
+ }
+ else
+ {
+ String msg = o.getClass().getName() + " object set as runtime.log.instance is not a valid org.slf4j.Logger implementation.";
+ log.error(msg);
+ throw new VelocityException(msg);
+ }
+ }
+ else
+ {
+ /* otherwise, see if a logger name was specified.
+ */
+ o = getProperty(RuntimeConstants.RUNTIME_LOG_NAME);
+ if (o != null)
+ {
+ if (o instanceof String)
+ {
+ log = LoggerFactory.getLogger((String)o);
+ }
+ else
+ {
+ String msg = o.getClass().getName() + " object set as runtime.log.name is not a valid string.";
+ log.error(msg);
+ throw new VelocityException(msg);
+ }
+ }
+ }
+ /* else keep our default Velocity logger
+ */
+
+ /* Initialize LogContext */
+ boolean trackLocation = getBoolean(RUNTIME_LOG_TRACK_LOCATION, false);
+ logContext = new LogContext(trackLocation);
+ }
+ catch (Exception e)
+ {
+ throw new VelocityException("Error initializing log: " + e.getMessage(), e);
+ }
+ }
+
+
+ /**
+ * This methods initializes all the directives
+ * that are used by the Velocity Runtime. The
+ * directives to be initialized are listed in
+ * the RUNTIME_DEFAULT_DIRECTIVES properties
+ * file.
+ */
+ private void initializeDirectives()
+ {
+ Properties directiveProperties = new Properties();
+
+ /*
+ * Grab the properties file with the list of directives
+ * that we should initialize.
+ */
+
+ InputStream inputStream = null;
+
+ try
+ {
+ inputStream = getClass().getResourceAsStream('/' + DEFAULT_RUNTIME_DIRECTIVES);
+
+ if (inputStream == null)
+ {
+ throw new VelocityException("Error loading directive.properties! " +
+ "Something is very wrong if these properties " +
+ "aren't being located. Either your Velocity " +
+ "distribution is incomplete or your Velocity " +
+ "jar file is corrupted!");
+ }
+
+ directiveProperties.load(inputStream);
+
+ }
+ catch (IOException ioe)
+ {
+ String msg = "Error while loading directive properties!";
+ log.error(msg, ioe);
+ throw new RuntimeException(msg, ioe);
+ }
+ finally
+ {
+ try
+ {
+ if (inputStream != null)
+ {
+ inputStream.close();
+ }
+ }
+ catch (IOException ioe)
+ {
+ String msg = "Cannot close directive properties!";
+ log.error(msg, ioe);
+ throw new RuntimeException(msg, ioe);
+ }
+ }
+
+
+ /*
+ * Grab all the values of the properties. These
+ * are all class names for example:
+ *
+ * org.apache.velocity.runtime.directive.Foreach
+ */
+ Enumeration directiveClasses = directiveProperties.elements();
+
+ while (directiveClasses.hasMoreElements())
+ {
+ String directiveClass = (String) directiveClasses.nextElement();
+ loadDirective(directiveClass);
+ log.debug("Loaded System Directive: {}", directiveClass);
+ }
+
+ /*
+ * now the user's directives
+ */
+
+ String[] userdirective = configuration.getStringArray(CUSTOM_DIRECTIVES);
+
+ for (String anUserdirective : userdirective)
+ {
+ loadDirective(anUserdirective);
+ log.debug("Loaded User Directive: {}", anUserdirective);
+ }
+
+ }
+
+ /**
+ * Programatically add a directive.
+ * @param directive
+ */
+ public synchronized void addDirective(Directive directive)
+ {
+ runtimeDirectives.put(directive.getName(), directive);
+ updateSharedDirectivesMap();
+ }
+
+ /**
+ * Retrieve a previously instantiated directive.
+ * @param name name of the directive
+ * @return the {@link Directive} for that name
+ */
+ @Override
+ public Directive getDirective(String name)
+ {
+ return runtimeDirectivesShared.get(name);
+ }
+
+ /**
+ * Remove a directive.
+ * @param name name of the directive.
+ */
+ public synchronized void removeDirective(String name)
+ {
+ runtimeDirectives.remove(name);
+ updateSharedDirectivesMap();
+ }
+
+ /**
+ * Makes an unsynchronized copy of the directives map
+ * that is used for Directive lookups by all parsers.
+ *
+ * This follows Copy-on-Write pattern. The cost of creating
+ * a new map is acceptable since directives are typically
+ * set and modified only during Velocity setup phase.
+ */
+ private void updateSharedDirectivesMap()
+ {
+ runtimeDirectivesShared = new HashMap<>(runtimeDirectives);
+ }
+
+ /**
+ * instantiates and loads the directive with some basic checks
+ *
+ * @param directiveClass classname of directive to load
+ */
+ public void loadDirective(String directiveClass)
+ {
+ try
+ {
+ Object o = ClassUtils.getNewInstance( directiveClass );
+
+ if (o instanceof Directive)
+ {
+ Directive directive = (Directive) o;
+ addDirective(directive);
+ }
+ else
+ {
+ String msg = directiveClass + " does not implement "
+ + Directive.class.getName() + "; it cannot be loaded.";
+ log.error(msg);
+ throw new VelocityException(msg);
+ }
+ }
+ // The ugly threesome: ClassNotFoundException,
+ // IllegalAccessException, InstantiationException.
+ // Ignore Findbugs complaint for now.
+ catch (Exception e)
+ {
+ String msg = "Failed to load Directive: " + directiveClass;
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ }
+
+
+ /**
+ * Initializes the Velocity parser pool.
+ */
+ private void initializeParserPool()
+ {
+ /*
+ * First initialize parser class. If it's not valid or not found, it will generate an error
+ * later on in this method when parser creation is tester.
+ */
+ String parserClassName = getString(PARSER_CLASS, DEFAULT_PARSER_CLASS);
+ Class<? extends Parser> parserClass;
+ try
+ {
+ parserClass = (Class<? extends Parser>)ClassUtils.getClass(parserClassName);
+ }
+ catch (ClassNotFoundException cnfe)
+ {
+ throw new VelocityException("parser class not found: " + parserClassName, cnfe);
+ }
+ try
+ {
+ parserConstructor = parserClass.getConstructor(RuntimeServices.class);
+ }
+ catch (NoSuchMethodException nsme)
+ {
+ throw new VelocityException("parser class must provide a constructor taking a RuntimeServices argument", nsme);
+ }
+
+ /*
+ * Which parser pool?
+ */
+ String pp = getString(RuntimeConstants.PARSER_POOL_CLASS);
+
+ if (pp != null && pp.length() > 0)
+ {
+ /*
+ * if something was specified, then make one.
+ * if that isn't a ParserPool, consider
+ * this a huge error and throw
+ */
+
+ Object o = null;
+
+ try
+ {
+ o = ClassUtils.getNewInstance( pp );
+ }
+ catch (ClassNotFoundException cnfe )
+ {
+ String err = "The specified class for ParserPool ("
+ + pp
+ + ") does not exist (or is not accessible to the current classloader.";
+ log.error(err);
+ throw new VelocityException(err, cnfe);
+ }
+ catch (InstantiationException ie)
+ {
+ throw new VelocityException("Could not instantiate class '" + pp + "'", ie);
+ }
+ catch (IllegalAccessException ae)
+ {
+ throw new VelocityException("Cannot access class '" + pp + "'", ae);
+ }
+
+ if (!(o instanceof ParserPool))
+ {
+ String err = "The specified class for ParserPool ("
+ + pp + ") does not implement " + ParserPool.class
+ + " Velocity not initialized correctly.";
+
+ log.error(err);
+ throw new VelocityException(err);
+ }
+
+ parserPool = (ParserPool) o;
+
+ parserPool.initialize(this);
+
+ /*
+ * test parser creation and use generated parser to fill up customized characters
+ */
+ Parser parser = parserPool.get();
+ parserConfiguration = new ParserConfiguration();
+ parserConfiguration.setDollarChar(parser.dollar());
+ parserConfiguration.setHashChar(parser.hash());
+ parserConfiguration.setAtChar(parser.at());
+ parserConfiguration.setAsteriskChar(parser.asterisk());
+ parserPool.put(parser);
+ }
+ else
+ {
+ /*
+ * someone screwed up. Lets not fool around...
+ */
+
+ String err = "It appears that no class was specified as the"
+ + " ParserPool. Please ensure that all configuration"
+ + " information is correct.";
+
+ log.error(err);
+ throw new VelocityException( err );
+ }
+
+ }
+
+ /**
+ * Returns a JavaCC generated Parser.
+ *
+ * @return Parser javacc generated parser
+ */
+ @Override
+ public Parser createNewParser()
+ {
+ requireInitialization();
+ try
+ {
+ return parserConstructor.newInstance(this);
+ }
+ catch (IllegalAccessException | InstantiationException | InvocationTargetException e)
+ {
+ throw new VelocityException("could not build new parser class", e);
+ }
+ }
+
+ /**
+ * Parse the input and return the root of
+ * AST node structure.
+ * <br><br>
+ * In the event that it runs out of parsers in the
+ * pool, it will create and let them be GC'd
+ * dynamically, logging that it has to do that. This
+ * is considered an exceptional condition. It is
+ * expected that the user will set the
+ * PARSER_POOL_SIZE property appropriately for their
+ * application. We will revisit this.
+ *
+ * @param reader Reader retrieved by a resource loader
+ * @param template template being parsed
+ * @return A root node representing the template as an AST tree.
+ * @throws ParseException When the template could not be parsed.
+ */
+ @Override
+ public SimpleNode parse(Reader reader, Template template)
+ throws ParseException
+ {
+ requireInitialization();
+
+ Parser parser = parserPool.get();
+ boolean keepParser = true;
+ if (parser == null)
+ {
+ /*
+ * if we couldn't get a parser from the pool make one and log it.
+ */
+ log.info("Runtime: ran out of parsers. Creating a new one. "
+ + " Please increment the parser.pool.size property."
+ + " The current value is too small.");
+ parser = createNewParser();
+ keepParser = false;
+ }
+
+ try
+ {
+ return parser.parse(reader, template);
+ }
+ finally
+ {
+ if (keepParser)
+ {
+ /* drop the parser Template reference to allow garbage collection */
+ parser.resetCurrentTemplate();
+ parserPool.put(parser);
+ }
+
+ }
+ }
+
+ private void initializeScopeSettings()
+ {
+ ExtProperties scopes = configuration.subset(CONTEXT_SCOPE_CONTROL);
+ if (scopes != null)
+ {
+ Iterator<String> scopeIterator = scopes.getKeys();
+ while (scopeIterator.hasNext())
+ {
+ String scope = scopeIterator.next();
+ boolean enabled = scopes.getBoolean(scope);
+ if (enabled) enabledScopeControls.add(scope);
+ }
+ }
+ }
+
+ /**
+ * Renders the input string using the context into the output writer.
+ * To be used when a template is dynamically constructed, or want to use
+ * Velocity as a token replacer.
+ * <br>
+ * Note! Macros defined in evaluate() calls are not persisted in memory so next evaluate() call
+ * does not know about macros defined during previous calls.
+ *
+ * @param context context to use in rendering input string
+ * @param out Writer in which to render the output
+ * @param logTag string to be used as the template name for log
+ * messages in case of error
+ * @param instring input string containing the VTL to be rendered
+ *
+ * @return true if successful, false otherwise. If false, see
+ * Velocity runtime log
+ * @throws ParseErrorException The template could not be parsed.
+ * @throws MethodInvocationException A method on a context object could not be invoked.
+ * @throws ResourceNotFoundException A referenced resource could not be loaded.
+ * @since Velocity 1.6
+ */
+ @Override
+ public boolean evaluate(Context context, Writer out,
+ String logTag, String instring)
+ {
+ return evaluate(context, out, logTag, new StringReader(instring));
+ }
+
+ /**
+ * Renders the input reader using the context into the output writer.
+ * To be used when a template is dynamically constructed, or want to
+ * use Velocity as a token replacer.
+ * <br>
+ * Note! Macros defined in evaluate() calls are not persisted in memory so next evaluate() call
+ * does not know about macros defined during previous calls.
+ *
+ * @param context context to use in rendering input string
+ * @param writer Writer in which to render the output
+ * @param logTag string to be used as the template name for log messages
+ * in case of error
+ * @param reader Reader containing the VTL to be rendered
+ *
+ * @return true if successful, false otherwise. If false, see
+ * Velocity runtime log
+ * @throws ParseErrorException The template could not be parsed.
+ * @throws MethodInvocationException A method on a context object could not be invoked.
+ * @throws ResourceNotFoundException A referenced resource could not be loaded.
+ * @since Velocity 1.6
+ */
+ @Override
+ public boolean evaluate(Context context, Writer writer,
+ String logTag, Reader reader)
+ {
+ if (logTag == null)
+ {
+ throw new NullPointerException("logTag (i.e. template name) cannot be null, you must provide an identifier for the content being evaluated");
+ }
+
+ SimpleNode nodeTree = null;
+ Template t = new Template();
+ t.setName(logTag);
+ try
+ {
+ nodeTree = parse(reader, t);
+ }
+ catch (ParseException pex)
+ {
+ throw new ParseErrorException(pex, null);
+ }
+ catch (TemplateInitException pex)
+ {
+ throw new ParseErrorException(pex, null);
+ }
+
+ if (nodeTree == null)
+ {
+ return false;
+ }
+ else
+ {
+ return render(context, writer, logTag, nodeTree);
+ }
+ }
+
+
+ /**
+ * Initializes and renders the AST {@link SimpleNode} using the context
+ * into the output writer.
+ *
+ * @param context context to use in rendering input string
+ * @param writer Writer in which to render the output
+ * @param logTag string to be used as the template name for log messages
+ * in case of error
+ * @param nodeTree SimpleNode which is the root of the AST to be rendered
+ *
+ * @return true if successful, false otherwise. If false, see
+ * Velocity runtime log for errors
+ * @throws ParseErrorException The template could not be parsed.
+ * @throws MethodInvocationException A method on a context object could not be invoked.
+ * @throws ResourceNotFoundException A referenced resource could not be loaded.
+ * @since Velocity 1.6
+ */
+ public boolean render(Context context, Writer writer,
+ String logTag, SimpleNode nodeTree)
+ {
+ /*
+ * we want to init then render
+ */
+ InternalContextAdapterImpl ica =
+ new InternalContextAdapterImpl(context);
+
+ ica.pushCurrentTemplateName(logTag);
+
+ try
+ {
+ try
+ {
+ nodeTree.init(ica, this);
+ }
+ catch (TemplateInitException pex)
+ {
+ throw new ParseErrorException(pex, null);
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch(RuntimeException e)
+ {
+ throw e;
+ }
+ catch(Exception e)
+ {
+ String msg = "RuntimeInstance.render(): init exception for tag = "+logTag;
+ log.error(msg, e);
+ throw new VelocityException(msg, e, getLogContext().getStackTrace());
+ }
+
+ try
+ {
+ if (isScopeControlEnabled(evaluateScopeName))
+ {
+ Object previous = ica.get(evaluateScopeName);
+ context.put(evaluateScopeName, new Scope(this, previous));
+ }
+ /*
+ * optionally put the context in itself if asked so
+ */
+ String self = getString(CONTEXT_AUTOREFERENCE_KEY);
+ if (self != null) context.put(self, context);
+ nodeTree.render(ica, writer);
+ }
+ catch (StopCommand stop)
+ {
+ if (!stop.isFor(this))
+ {
+ throw stop;
+ }
+ else
+ {
+ log.debug(stop.getMessage());
+ }
+ }
+ catch (IOException e)
+ {
+ throw new VelocityException("IO Error in writer: " + e.getMessage(), e, getLogContext().getStackTrace());
+ }
+ }
+ finally
+ {
+ ica.popCurrentTemplateName();
+ if (isScopeControlEnabled(evaluateScopeName))
+ {
+ Object obj = ica.get(evaluateScopeName);
+ if (obj instanceof Scope)
+ {
+ Scope scope = (Scope)obj;
+ if (scope.getParent() != null)
+ {
+ ica.put(evaluateScopeName, scope.getParent());
+ }
+ else if (scope.getReplaced() != null)
+ {
+ ica.put(evaluateScopeName, scope.getReplaced());
+ }
+ else
+ {
+ ica.remove(evaluateScopeName);
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Invokes a currently registered Velocimacro with the params provided
+ * and places the rendered stream into the writer.
+ * <br>
+ * Note: currently only accepts args to the VM if they are in the context.
+ * <br>
+ * Note: only macros in the global context can be called. This method doesn't find macros defined by
+ * templates during previous mergeTemplate calls if Velocity.VM_PERM_INLINE_LOCAL has been enabled.
+ *
+ * @param vmName name of Velocimacro to call
+ * @param logTag string to be used for template name in case of error. if null,
+ * the vmName will be used
+ * @param params keys for args used to invoke Velocimacro, in java format
+ * rather than VTL (eg "foo" or "bar" rather than "$foo" or "$bar")
+ * @param context Context object containing data/objects used for rendering.
+ * @param writer Writer for output stream
+ * @return true if Velocimacro exists and successfully invoked, false otherwise.
+ * @since 1.6
+ */
+ @Override
+ public boolean invokeVelocimacro(final String vmName, String logTag,
+ String[] params, final Context context,
+ final Writer writer)
+ {
+ /* check necessary parameters */
+ if (vmName == null || context == null || writer == null)
+ {
+ String msg = "RuntimeInstance.invokeVelocimacro(): invalid call: vmName, context, and writer must not be null";
+ log.error(msg);
+ throw new NullPointerException(msg);
+ }
+
+ /* handle easily corrected parameters */
+ if (logTag == null)
+ {
+ logTag = vmName;
+ }
+ if (params == null)
+ {
+ params = new String[0];
+ }
+
+ /* does the VM exist? (only global scope is scanned so this doesn't find inline macros in templates) */
+ if (!isVelocimacro(vmName, null))
+ {
+ String msg = "RuntimeInstance.invokeVelocimacro(): VM '" + vmName
+ + "' is not registered.";
+ log.error(msg);
+ throw new VelocityException(msg, null, getLogContext().getStackTrace());
+ }
+
+ /* now just create the VM call, and use evaluate */
+ StringBuilder template = new StringBuilder(String.valueOf(parserConfiguration.getHashChar()));
+ template.append(vmName);
+ template.append("(");
+ for (String param : params)
+ {
+ template.append(" $");
+ template.append(param);
+ }
+ template.append(" )");
+
+ return evaluate(context, writer, logTag, template.toString());
+ }
+
+ /**
+ * Retrieves and caches the configured default encoding
+ * for better performance. (VELOCITY-606)
+ */
+ private String getDefaultEncoding()
+ {
+ return defaultEncoding;
+ }
+
+ /**
+ * Returns a <code>Template</code> from the resource manager.
+ * This method assumes that the character encoding of the
+ * template is set by the <code>resource.default_encoding</code>
+ * property. The default is UTF-8.
+ *
+ * @param name The file name of the desired template.
+ * @return The template.
+ * @throws ResourceNotFoundException if template not found
+ * from any available source.
+ * @throws ParseErrorException if template cannot be parsed due
+ * to syntax (or other) error.
+ */
+ @Override
+ public Template getTemplate(String name)
+ throws ResourceNotFoundException, ParseErrorException
+ {
+ return getTemplate(name, null);
+ }
+
+ /**
+ * Returns a <code>Template</code> from the resource manager
+ *
+ * @param name The name of the desired template.
+ * @param encoding Character encoding of the template
+ * @return The template.
+ * @throws ResourceNotFoundException if template not found
+ * from any available source.
+ * @throws ParseErrorException if template cannot be parsed due
+ * to syntax (or other) error.
+ */
+ @Override
+ public Template getTemplate(String name, String encoding)
+ throws ResourceNotFoundException, ParseErrorException
+ {
+ requireInitialization();
+ if (encoding == null) encoding = getDefaultEncoding();
+ return (Template)
+ resourceManager.getResource(name,
+ ResourceManager.RESOURCE_TEMPLATE, encoding);
+ }
+
+ /**
+ * Returns a static content resource from the
+ * resource manager. Uses the current value
+ * if INPUT_ENCODING as the character encoding.
+ *
+ * @param name Name of content resource to get
+ * @return parsed ContentResource object ready for use
+ * @throws ResourceNotFoundException if template not found
+ * from any available source.
+ * @throws ParseErrorException When the template could not be parsed.
+ */
+ @Override
+ public ContentResource getContent(String name)
+ throws ResourceNotFoundException, ParseErrorException
+ {
+ /*
+ * the encoding is irrelvant as we don't do any converstion
+ * the bytestream should be dumped to the output stream
+ */
+
+ return getContent(name, getDefaultEncoding());
+ }
+
+ /**
+ * Returns a static content resource from the
+ * resource manager.
+ *
+ * @param name Name of content resource to get
+ * @param encoding Character encoding to use
+ * @return parsed ContentResource object ready for use
+ * @throws ResourceNotFoundException if template not found
+ * from any available source.
+ * @throws ParseErrorException When the template could not be parsed.
+ */
+ @Override
+ public ContentResource getContent(String name, String encoding)
+ throws ResourceNotFoundException, ParseErrorException
+ {
+ requireInitialization();
+
+ return (ContentResource)
+ resourceManager.getResource(name,
+ ResourceManager.RESOURCE_CONTENT, encoding);
+ }
+
+
+ /**
+ * Determines if a template exists and returns name of the loader that
+ * provides it. This is a slightly less hokey way to support
+ * the Velocity.resourceExists() utility method, which was broken
+ * when per-template encoding was introduced. We can revisit this.
+ *
+ * @param resourceName Name of template or content resource
+ * @return class name of loader than can provide it
+ */
+ @Override
+ public String getLoaderNameForResource(String resourceName)
+ {
+ requireInitialization();
+
+ return resourceManager.getLoaderNameForResource(resourceName);
+ }
+
+ /**
+ * Returns the configured logger.
+ *
+ * @return A Logger instance
+ * @since 1.5
+ */
+ @Override
+ public Logger getLog()
+ {
+ return log;
+ }
+
+ /**
+ * Get a logger for the specified child namespace.
+ * If a logger was configured using the runtime.log.instance configuration property, returns this instance.
+ * Otherwise, uses SLF4J LoggerFactory on baseNamespace '.' childNamespace.
+ * @param childNamespace
+ * @return child namespace logger
+ */
+ @Override
+ public Logger getLog(String childNamespace)
+ {
+ Logger log = (Logger)getProperty(RuntimeConstants.RUNTIME_LOG_INSTANCE);
+ if (log == null)
+ {
+ String loggerName = getString(RUNTIME_LOG_NAME, DEFAULT_RUNTIME_LOG_NAME) + "." + childNamespace;
+ log = LoggerFactory.getLogger(loggerName);
+ }
+ return log;
+ }
+
+ /**
+ * Get the LogContext object used to tack locations in templates.
+ * @return LogContext object
+ * @since 2.2
+ */
+ @Override
+ public LogContext getLogContext()
+ {
+ return logContext;
+ }
+
+ /**
+ * String property accessor method with default to hide the
+ * configuration implementation.
+ *
+ * @param key property key
+ * @param defaultValue default value to return if key not
+ * found in resource manager.
+ * @return value of key or default
+ */
+ @Override
+ public String getString(String key, String defaultValue)
+ {
+ return configuration.getString(key, defaultValue);
+ }
+
+ /**
+ * Returns the appropriate VelocimacroProxy object if vmName
+ * is a valid current Velocimacro.
+ *
+ * @param vmName Name of velocimacro requested
+ * @param renderingTemplate Template we are currently rendering. This
+ * information is needed when VM_PERM_ALLOW_INLINE_REPLACE_GLOBAL setting is true
+ * and template contains a macro with the same name as the global macro library.
+ * @param template Template which acts as the host for the macro
+ *
+ * @return VelocimacroProxy
+ */
+ @Override
+ public Directive getVelocimacro(String vmName, Template renderingTemplate, Template template)
+ {
+ return vmFactory.getVelocimacro(vmName, renderingTemplate, template);
+ }
+
+ /**
+ * Adds a new Velocimacro. Usually called by Macro only while parsing.
+ *
+ * @param name Name of velocimacro
+ * @param macro root AST node of the parsed macro
+ * @param macroArgs Array of macro arguments, containing the
+ * #macro() arguments and default values. the 0th is the name.
+ * @param definingTemplate Template containing the source of the macro
+ *
+ * @return boolean True if added, false if rejected for some
+ * reason (either parameters or permission settings)
+ */
+ @Override
+ public boolean addVelocimacro(String name,
+ Node macro,
+ List<Macro.MacroArg> macroArgs,
+ Template definingTemplate)
+ {
+ return vmFactory.addVelocimacro(stringInterning ? name.intern() : name, macro, macroArgs, definingTemplate);
+ }
+
+ /**
+ * Checks to see if a VM exists
+ *
+ * @param vmName Name of the Velocimacro.
+ * @param template Template on which to look for the Macro.
+ * @return True if VM by that name exists, false if not
+ */
+ @Override
+ public boolean isVelocimacro(String vmName, Template template)
+ {
+ return vmFactory.isVelocimacro(stringInterning ? vmName.intern() : vmName, template);
+ }
+
+ /* --------------------------------------------------------------------
+ * R U N T I M E A C C E S S O R M E T H O D S
+ * --------------------------------------------------------------------
+ * These are the getXXX() methods that are a simple wrapper
+ * around the configuration object. This is an attempt
+ * to make a the Velocity Runtime the single access point
+ * for all things Velocity, and allow the Runtime to
+ * adhere as closely as possible the the Mediator pattern
+ * which is the ultimate goal.
+ * --------------------------------------------------------------------
+ */
+
+ /**
+ * String property accessor method to hide the configuration implementation
+ * @param key property key
+ * @return value of key or null
+ */
+ @Override
+ public String getString(String key)
+ {
+ return StringUtils.trim(configuration.getString(key));
+ }
+
+ /**
+ * Int property accessor method to hide the configuration implementation.
+ *
+ * @param key Property key
+ * @return value
+ */
+ @Override
+ public int getInt(String key)
+ {
+ return configuration.getInt(key);
+ }
+
+ /**
+ * Int property accessor method to hide the configuration implementation.
+ *
+ * @param key property key
+ * @param defaultValue The default value.
+ * @return value
+ */
+ @Override
+ public int getInt(String key, int defaultValue)
+ {
+ return configuration.getInt(key, defaultValue);
+ }
+
+ /**
+ * Boolean property accessor method to hide the configuration implementation.
+ *
+ * @param key property key
+ * @param def The default value if property not found.
+ * @return value of key or default value
+ */
+ @Override
+ public boolean getBoolean(String key, boolean def)
+ {
+ return configuration.getBoolean(key, def);
+ }
+
+ /**
+ * Return the velocity runtime configuration object.
+ *
+ * @return Configuration object which houses the Velocity runtime
+ * properties.
+ */
+ @Override
+ public ExtProperties getConfiguration()
+ {
+ return configuration;
+ }
+
+ /**
+ * Returns the event handlers for the application.
+ * @return The event handlers for the application.
+ * @since 1.5
+ */
+ @Override
+ public EventCartridge getApplicationEventCartridge()
+ {
+ return eventCartridge;
+ }
+
+
+ /**
+ * Gets the application attribute for the given key
+ *
+ * @param key
+ * @return The application attribute for the given key.
+ */
+ @Override
+ public Object getApplicationAttribute(Object key)
+ {
+ return applicationAttributes.get(key);
+ }
+
+ /**
+ * Sets the application attribute for the given key
+ *
+ * @param key
+ * @param o The new application attribute.
+ * @return The old value of this attribute or null if it hasn't been set before.
+ */
+ @Override
+ public Object setApplicationAttribute(Object key, Object o)
+ {
+ return applicationAttributes.put(key, o);
+ }
+
+ /**
+ * Returns the Uberspect object for this Instance.
+ *
+ * @return The Uberspect object for this Instance.
+ */
+ @Override
+ public Uberspect getUberspect()
+ {
+ return uberSpect;
+ }
+
+ /**
+ * Whether to use string interning
+ *
+ * @return boolean
+ */
+ @Override
+ public boolean useStringInterning()
+ {
+ return stringInterning;
+ }
+
+ /**
+ * get space gobbling mode
+ * @return indentation mode
+ */
+ @Override
+ public SpaceGobbling getSpaceGobbling()
+ {
+ return spaceGobbling;
+ }
+
+ /**
+ * get whether hyphens are allowed in identifiers
+ * @return configured boolean flag
+ */
+ @Override
+ public boolean isHyphenAllowedInIdentifiers()
+ {
+ return hyphenAllowedInIdentifiers;
+ }
+
+ /**
+ * Get whether to provide a scope control object for this scope
+ * @param scopeName
+ * @return scope control enabled
+ * @since 2.1
+ */
+ @Override
+ public boolean isScopeControlEnabled(String scopeName)
+ {
+ return enabledScopeControls.contains(scopeName);
+ }
+
+ @Override
+ public ParserConfiguration getParserConfiguration()
+ {
+ return parserConfiguration;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeServices.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeServices.java
new file mode 100644
index 00000000..eec0e335
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeServices.java
@@ -0,0 +1,498 @@
+package org.apache.velocity.runtime;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+import org.apache.velocity.app.event.EventCartridge;
+import org.apache.velocity.context.Context;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.RuntimeConstants.SpaceGobbling;
+import org.apache.velocity.runtime.directive.Directive;
+import org.apache.velocity.runtime.directive.Macro;
+import org.apache.velocity.runtime.parser.LogContext;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.runtime.parser.node.Node;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+import org.apache.velocity.runtime.resource.ContentResource;
+import org.apache.velocity.util.ExtProperties;
+import org.apache.velocity.util.introspection.Uberspect;
+import org.slf4j.Logger;
+
+import java.io.Reader;
+import java.io.Writer;
+import java.util.List;
+import java.util.Properties;
+
+
+/**
+ * Interface for internal runtime services that are needed by the
+ * various components w/in Velocity. This was taken from the old
+ * Runtime singleton, and anything not necessary was removed.
+ *
+ * Currently implemented by RuntimeInstance.
+ *
+ * @author <a href="mailto:geirm@optonline.net">Geir Magusson Jr.</a>
+ * @version $Id$
+ */
+public interface RuntimeServices
+{
+
+ /**
+ * This is the primary initialization method in the Velocity
+ * Runtime. The systems that are setup/initialized here are
+ * as follows:
+ *
+ * <ul>
+ * <li>Logging System</li>
+ * <li>ResourceManager</li>
+ * <li>Parser Pool</li>
+ * <li>Global Cache</li>
+ * <li>Static Content Include System</li>
+ * <li>Velocimacro System</li>
+ * </ul>
+ */
+ void init();
+
+ /**
+ * Allows an external system to set a property in
+ * the Velocity Runtime.
+ *
+ * @param key property key
+ * @param value property value
+ */
+ void setProperty(String key, Object value);
+
+ /**
+ * Allow an external system to set an ExtProperties
+ * object to use.
+ *
+ * @param configuration
+ * @since 2.0
+ */
+ void setConfiguration(ExtProperties configuration);
+
+ /**
+ * Add a property to the configuration. If it already
+ * exists then the value stated here will be added
+ * to the configuration entry. For example, if
+ *
+ * resource.loader = file
+ *
+ * is already present in the configuration and you
+ *
+ * addProperty("resource.loader", "classpath")
+ *
+ * Then you will end up with a Vector like the
+ * following:
+ *
+ * ["file", "classpath"]
+ *
+ * @param key
+ * @param value
+ */
+ void addProperty(String key, Object value);
+
+ /**
+ * Clear the values pertaining to a particular
+ * property.
+ *
+ * @param key of property to clear
+ */
+ void clearProperty(String key);
+
+ /**
+ * Allows an external caller to get a property. The calling
+ * routine is required to know the type, as this routine
+ * will return an Object, as that is what properties can be.
+ *
+ * @param key property to return
+ * @return The value.
+ */
+ Object getProperty(String key);
+
+ /**
+ * Initialize the Velocity Runtime with a Properties
+ * object.
+ *
+ * @param p
+ */
+ void init(Properties p);
+
+ /**
+ * Initialize the Velocity Runtime with the name of
+ * ExtProperties object.
+ *
+ * @param configurationFile
+ */
+ void init(String configurationFile);
+
+ /**
+ * Parse the input and return the root of
+ * AST node structure.
+ * <br><br>
+ * In the event that it runs out of parsers in the
+ * pool, it will create and let them be GC'd
+ * dynamically, logging that it has to do that. This
+ * is considered an exceptional condition. It is
+ * expected that the user will set the
+ * PARSER_POOL_SIZE property appropriately for their
+ * application. We will revisit this.
+ *
+ * @param reader inputstream retrieved by a resource loader
+ * @param template template being parsed
+ * @return The AST representing the template.
+ * @throws ParseException
+ */
+ SimpleNode parse(Reader reader, Template template)
+ throws ParseException;
+
+ /**
+ * Renders the input string using the context into the output writer.
+ * To be used when a template is dynamically constructed, or want to use
+ * Velocity as a token replacer.
+ *
+ * @param context context to use in rendering input string
+ * @param out Writer in which to render the output
+ * @param logTag string to be used as the template name for log
+ * messages in case of error
+ * @param instring input string containing the VTL to be rendered
+ *
+ * @return true if successful, false otherwise. If false, see
+ * Velocity runtime log
+ * @throws ParseErrorException The template could not be parsed.
+ * @throws MethodInvocationException A method on a context object could not be invoked.
+ * @throws ResourceNotFoundException A referenced resource could not be loaded.
+ * @since Velocity 1.6
+ */
+ boolean evaluate(Context context, Writer out,
+ String logTag, String instring);
+
+ /**
+ * Renders the input reader using the context into the output writer.
+ * To be used when a template is dynamically constructed, or want to
+ * use Velocity as a token replacer.
+ *
+ * @param context context to use in rendering input string
+ * @param writer Writer in which to render the output
+ * @param logTag string to be used as the template name for log messages
+ * in case of error
+ * @param reader Reader containing the VTL to be rendered
+ *
+ * @return true if successful, false otherwise. If false, see
+ * Velocity runtime log
+ * @throws ParseErrorException The template could not be parsed.
+ * @throws MethodInvocationException A method on a context object could not be invoked.
+ * @throws ResourceNotFoundException A referenced resource could not be loaded.
+ * @since Velocity 1.6
+ */
+ boolean evaluate(Context context, Writer writer,
+ String logTag, Reader reader);
+
+ /**
+ * Invokes a currently registered Velocimacro with the params provided
+ * and places the rendered stream into the writer.
+ * <br>
+ * Note : currently only accepts args to the VM if they are in the context.
+ *
+ * @param vmName name of Velocimacro to call
+ * @param logTag string to be used for template name in case of error. if null,
+ * the vmName will be used
+ * @param params keys for args used to invoke Velocimacro, in java format
+ * rather than VTL (eg "foo" or "bar" rather than "$foo" or "$bar")
+ * @param context Context object containing data/objects used for rendering.
+ * @param writer Writer for output stream
+ * @return true if Velocimacro exists and successfully invoked, false otherwise.
+ * @since 1.6
+ */
+ boolean invokeVelocimacro(final String vmName, String logTag,
+ String[] params, final Context context,
+ final Writer writer);
+
+ /**
+ * Returns a <code>Template</code> from the resource manager.
+ * This method assumes that the character encoding of the
+ * template is set by the <code>input.encoding</code>
+ * property. The default is UTF-8.
+ *
+ * @param name The file name of the desired template.
+ * @return The template.
+ * @throws ResourceNotFoundException if template not found
+ * from any available source.
+ * @throws ParseErrorException if template cannot be parsed due
+ * to syntax (or other) error.
+ */
+ Template getTemplate(String name)
+ throws ResourceNotFoundException, ParseErrorException;
+
+ /**
+ * Returns a <code>Template</code> from the resource manager
+ *
+ * @param name The name of the desired template.
+ * @param encoding Character encoding of the template
+ * @return The template.
+ * @throws ResourceNotFoundException if template not found
+ * from any available source.
+ * @throws ParseErrorException if template cannot be parsed due
+ * to syntax (or other) error.
+ */
+ Template getTemplate(String name, String encoding)
+ throws ResourceNotFoundException, ParseErrorException;
+
+ /**
+ * Returns a static content resource from the
+ * resource manager. Uses the current value
+ * if INPUT_ENCODING as the character encoding.
+ *
+ * @param name Name of content resource to get
+ * @return parsed ContentResource object ready for use
+ * @throws ResourceNotFoundException if template not found
+ * from any available source.
+ * @throws ParseErrorException
+ */
+ ContentResource getContent(String name)
+ throws ResourceNotFoundException, ParseErrorException;
+
+ /**
+ * Returns a static content resource from the
+ * resource manager.
+ *
+ * @param name Name of content resource to get
+ * @param encoding Character encoding to use
+ * @return parsed ContentResource object ready for use
+ * @throws ResourceNotFoundException if template not found
+ * from any available source.
+ * @throws ParseErrorException
+ */
+ ContentResource getContent(String name, String encoding)
+ throws ResourceNotFoundException, ParseErrorException;
+
+ /**
+ * Determines is a template exists, and returns name of the loader that
+ * provides it. This is a slightly less hokey way to support
+ * the Velocity.templateExists() utility method, which was broken
+ * when per-template encoding was introduced. We can revisit this.
+ *
+ * @param resourceName Name of template or content resource
+ * @return class name of loader than can provide it
+ */
+ String getLoaderNameForResource(String resourceName);
+
+ /**
+ * String property accessor method with default to hide the
+ * configuration implementation.
+ *
+ * @param key property key
+ * @param defaultValue default value to return if key not
+ * found in resource manager.
+ * @return String value of key or default
+ */
+ String getString(String key, String defaultValue);
+
+ /**
+ * Returns the appropriate VelocimacroProxy object if strVMname
+ * is a valid current Velocimacro.
+ *
+ * @param vmName Name of velocimacro requested
+ * @param renderingTemplate Template we are currently rendering. This
+ * information is needed when VM_PERM_ALLOW_INLINE_REPLACE_GLOBAL setting is true
+ * and template contains a macro with the same name as the global macro library.
+ * @param template current template
+ *
+ * @return VelocimacroProxy
+ */
+ Directive getVelocimacro(String vmName, Template renderingTemplate, Template template);
+
+ /**
+ * Adds a new Velocimacro. Usually called by Macro only while parsing.
+ *
+ * @param name Name of velocimacro
+ * @param macro root AST node of the parsed macro
+ * @param macroArgs Array of macro arguments, containing the
+ * #macro() arguments and default values. the 0th is the name.
+ * @param definingTemplate template containing macro definition
+ *
+ * @return boolean True if added, false if rejected for some
+ * reason (either parameters or permission settings)
+ */
+ boolean addVelocimacro(String name,
+ Node macro,
+ List<Macro.MacroArg> macroArgs,
+ Template definingTemplate);
+
+
+ /**
+ * Checks to see if a VM exists
+ *
+ * @param vmName Name of velocimacro
+ * @param template Template "namespace"
+ * @return boolean True if VM by that name exists, false if not
+ */
+ boolean isVelocimacro(String vmName, Template template);
+
+ /**
+ * String property accessor method to hide the configuration implementation
+ * @param key property key
+ * @return value of key or null
+ */
+ String getString(String key);
+
+ /**
+ * Int property accessor method to hide the configuration implementation.
+ *
+ * @param key property key
+ * @return int value
+ */
+ int getInt(String key);
+
+ /**
+ * Int property accessor method to hide the configuration implementation.
+ *
+ * @param key property key
+ * @param defaultValue default value
+ * @return int value
+ */
+ int getInt(String key, int defaultValue);
+
+ /**
+ * Boolean property accessor method to hide the configuration implementation.
+ *
+ * @param key property key
+ * @param def default default value if property not found
+ * @return boolean value of key or default value
+ */
+ boolean getBoolean(String key, boolean def);
+
+ /**
+ * Return the velocity runtime configuration object.
+ *
+ * @return ExtProperties configuration object which houses
+ * the velocity runtime properties.
+ */
+ ExtProperties getConfiguration();
+
+ /**
+ * Return the specified application attribute.
+ *
+ * @param key The name of the attribute to retrieve.
+ * @return The value of the attribute.
+ */
+ Object getApplicationAttribute(Object key);
+
+ /**
+ * Set the specified application attribute.
+ *
+ * @param key The name of the attribute to set.
+ * @param value The attribute value to set.
+ * @return the displaced attribute value
+ */
+ Object setApplicationAttribute(Object key, Object value);
+
+ /**
+ * Returns the configured class introspection/reflection
+ * implementation.
+ * @return The current Uberspect object.
+ */
+ Uberspect getUberspect();
+
+ /**
+ * Returns the configured logger.
+ * @return A Logger object.
+ */
+ Logger getLog();
+
+ /**
+ * Get a logger for the specified child namespace.
+ * If a logger was configured using the runtime.log.instance configuration property, returns this instance.
+ * Otherwise, uses SLF4J LoggerFactory on baseNamespace + childNamespace.
+ * @param childNamespace
+ * @return child name space logger
+ */
+ Logger getLog(String childNamespace);
+
+ /**
+ * Get the LogContext object used to tack locations in templates.
+ * @return LogContext object
+ * @since 2.2
+ */
+ LogContext getLogContext();
+
+ /**
+ * Returns the event handlers for the application.
+ * @return The event handlers for the application.
+ */
+ EventCartridge getApplicationEventCartridge();
+
+ /**
+ * Returns true if the RuntimeInstance has been successfully initialized.
+ * @return True if the RuntimeInstance has been successfully initialized.
+ */
+ boolean isInitialized();
+
+ /**
+ * Create a new parser instance.
+ * @return A new parser instance.
+ */
+ Parser createNewParser();
+
+ /**
+ * Retrieve a previously instantiated directive.
+ * @param name name of the directive
+ * @return the directive with that name, if any
+ * @since 1.6
+ */
+ Directive getDirective(String name);
+
+ /**
+ * Check whether the engine uses string interning
+ * @return true if string interning is active
+ */
+ boolean useStringInterning();
+
+ /**
+ * get space gobbling mode
+ * @return space gobbling mode
+ */
+ SpaceGobbling getSpaceGobbling();
+
+ /**
+ * Get whether hyphens are allowed in identifiers
+ * @return configured boolean flag
+ * @since 2.1
+ */
+ boolean isHyphenAllowedInIdentifiers();
+
+ /**
+ * Get whether to provide a scope control object for this scope
+ * @param scopeName
+ * @return scope control enabled
+ * @since 2.1
+ */
+ boolean isScopeControlEnabled(String scopeName);
+
+ /**
+ * Get the replacement characters configured for this runtime service's parser
+ * @return configured replacement characters
+ * @since 2.2
+ */
+ ParserConfiguration getParserConfiguration();
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeSingleton.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeSingleton.java
new file mode 100644
index 00000000..bc36c26f
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeSingleton.java
@@ -0,0 +1,571 @@
+package org.apache.velocity.runtime;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+import org.apache.velocity.app.event.EventCartridge;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.directive.Directive;
+import org.apache.velocity.runtime.directive.Macro;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.node.Node;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+import org.apache.velocity.runtime.resource.ContentResource;
+import org.apache.velocity.util.ExtProperties;
+import org.apache.velocity.util.introspection.Uberspect;
+import org.slf4j.Logger;
+
+import java.io.Reader;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * <p>This is the Runtime system for Velocity. It is the
+ * single access point for all functionality in Velocity.
+ * It adheres to the mediator pattern and is the only
+ * structure that developers need to be familiar with
+ * in order to get Velocity to perform.</p>
+ *
+ * <p>The Runtime will also cooperate with external
+ * systems, which can make all needed setProperty() calls
+ * before calling init().</p>
+ * <pre>
+ * -----------------------------------------------------------------------
+ * N O T E S O N R U N T I M E I N I T I A L I Z A T I O N
+ * -----------------------------------------------------------------------
+ * RuntimeSingleton.init()
+ *
+ * If Runtime.init() is called by itself the Runtime will
+ * initialize with a set of default values.
+ * -----------------------------------------------------------------------
+ * RuntimeSingleton.init(String/Properties)
+ *
+ * In this case the default velocity properties are laid down
+ * first to provide a solid base, then any properties provided
+ * in the given properties object will override the corresponding
+ * default property.
+ * -----------------------------------------------------------------------
+ * </pre>
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:jlb@houseofdistraction.com">Jeff Bowden</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magusson Jr.</a>
+ * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
+ *
+ * @see org.apache.velocity.runtime.RuntimeInstance
+ *
+ * @version $Id$
+ */
+public class RuntimeSingleton implements RuntimeConstants
+{
+ private static RuntimeInstance ri = new RuntimeInstance();
+
+ /**
+ * This is the primary initialization method in the Velocity
+ * Runtime. The systems that are setup/initialized here are
+ * as follows:
+ *
+ * <ul>
+ * <li>Logging System</li>
+ * <li>ResourceManager</li>
+ * <li>Event Handlers</li>
+ * <li>Parser Pool</li>
+ * <li>Global Cache</li>
+ * <li>Static Content Include System</li>
+ * <li>Velocimacro System</li>
+ * </ul>
+ * @see RuntimeInstance#init()
+ */
+ public synchronized static void init()
+ {
+ ri.init();
+ }
+
+ /**
+ * Resets the instance, so Velocity can be re-initialized again.
+ *
+ * @since 2.0.0
+ */
+ public synchronized static void reset()
+ {
+ ri.reset();
+ }
+
+ /**
+ * Returns true if the RuntimeInstance has been successfully initialized.
+ * @return True if the RuntimeInstance has been successfully initialized.
+ * @see RuntimeInstance#isInitialized()
+ * @since 1.5
+ */
+ public static boolean isInitialized()
+ {
+ return ri.isInitialized();
+ }
+
+ /**
+ * Returns the RuntimeServices Instance used by this wrapper.
+ *
+ * @return The RuntimeServices Instance used by this wrapper.
+ */
+ public static RuntimeServices getRuntimeServices()
+ {
+ return ri;
+ }
+
+
+ /**
+ * Allows an external system to set a property in
+ * the Velocity Runtime.
+ *
+ * @param key property key
+ * @param value property value
+ * @see RuntimeInstance#setProperty(String, Object)
+ */
+ public static void setProperty(String key, Object value)
+ {
+ ri.setProperty(key, value);
+ }
+
+ /**
+ * Allow an external system to set a Properties
+ * object to use.
+ *
+ * @param configuration
+ * @see RuntimeInstance#setProperties(Properties)
+ */
+ public static void setProperties(Properties configuration)
+ {
+ ri.setProperties(configuration);
+ }
+
+ /**
+ * Set an entire configuration at once from a named properties file
+ *
+ * @param propsFilename properties filename
+ * @since 2.1
+ */
+ public static void setProperties(String propsFilename)
+ {
+ ri.setProperties(propsFilename);
+ }
+
+ /**
+ * Add a property to the configuration. If it already
+ * exists then the value stated here will be added
+ * to the configuration entry. For example, if
+ *
+ * resource.loader = file
+ *
+ * is already present in the configuration and you
+ *
+ * addProperty("resource.loader", "classpath")
+ *
+ * Then you will end up with a Vector like the
+ * following:
+ *
+ * ["file", "classpath"]
+ *
+ * @param key
+ * @param value
+ * @see RuntimeInstance#addProperty(String, Object)
+ */
+ public static void addProperty(String key, Object value)
+ {
+ ri.addProperty(key, value);
+ }
+
+ /**
+ * Clear the values pertaining to a particular
+ * property.
+ *
+ * @param key of property to clear
+ * @see RuntimeInstance#clearProperty(String)
+ */
+ public static void clearProperty(String key)
+ {
+ ri.clearProperty( key );
+ }
+
+ /**
+ * Allows an external caller to get a property. The calling
+ * routine is required to know the type, as this routine
+ * will return an Object, as that is what properties can be.
+ *
+ * @param key property to return
+ * @return Value of the property or null if it does not exist.
+ * @see RuntimeInstance#getProperty(String)
+ */
+ public static Object getProperty( String key )
+ {
+ return ri.getProperty( key );
+ }
+
+ /**
+ * Initialize the Velocity Runtime with a Properties
+ * object.
+ *
+ * @param p
+ * @see RuntimeInstance#init(Properties)
+ */
+ public static void init(Properties p)
+ {
+ ri.init(p);
+ }
+
+ /**
+ * Initialize the Velocity Runtime with the name of
+ * a properties file.
+ *
+ * @param configurationFile
+ * @see RuntimeInstance#init(String)
+ */
+ public static void init(String configurationFile)
+ {
+ ri.init( configurationFile );
+ }
+
+ /**
+ * Parse the input and return the root of
+ * AST node structure.
+ * <br><br>
+ * In the event that it runs out of parsers in the
+ * pool, it will create and let them be GC'd
+ * dynamically, logging that it has to do that. This
+ * is considered an exceptional condition. It is
+ * expected that the user will set the
+ * PARSER_POOL_SIZE property appropriately for their
+ * application. We will revisit this.
+ *
+ * @param reader Reader retrieved by a resource loader
+ * @param template Template being parsed
+ * @return A root node representing the template as an AST tree.
+ * @throws ParseException When the template could not be parsed.
+ * @see RuntimeInstance#parse(Reader, Template)
+ */
+ public static SimpleNode parse( Reader reader, Template template )
+ throws ParseException
+ {
+ return ri.parse(reader, template);
+ }
+
+ /**
+ * Returns a <code>Template</code> from the resource manager.
+ * This method assumes that the character encoding of the
+ * template is set by the <code>resource.default_encoding</code>
+ * property. The default is UTF-8.
+ *
+ * @param name The file name of the desired template.
+ * @return The template.
+ * @throws ResourceNotFoundException if template not found
+ * from any available source.
+ * @throws ParseErrorException if template cannot be parsed due
+ * to syntax (or other) error.
+ * @see RuntimeInstance#getTemplate(String)
+ */
+ public static Template getTemplate(String name)
+ throws ResourceNotFoundException, ParseErrorException
+ {
+ return ri.getTemplate(name);
+ }
+
+ /**
+ * Returns a <code>Template</code> from the resource manager
+ *
+ * @param name The name of the desired template.
+ * @param encoding Character encoding of the template
+ * @return The template.
+ * @throws ResourceNotFoundException if template not found
+ * from any available source.
+ * @throws ParseErrorException if template cannot be parsed due
+ * to syntax (or other) error.
+ * @see RuntimeInstance#getTemplate(String, String)
+ */
+ public static Template getTemplate(String name, String encoding)
+ throws ResourceNotFoundException, ParseErrorException
+ {
+ return ri.getTemplate(name, encoding);
+ }
+
+ /**
+ * Returns a static content resource from the
+ * resource manager. Uses the current value
+ * if INPUT_ENCODING as the character encoding.
+ *
+ * @param name Name of content resource to get
+ * @return parsed ContentResource object ready for use
+ * @throws ResourceNotFoundException if template not found
+ * from any available source.
+ * @throws ParseErrorException When the template could not be parsed.
+ * @see RuntimeInstance#getContent(String)
+ */
+ public static ContentResource getContent(String name)
+ throws ResourceNotFoundException, ParseErrorException
+ {
+ return ri.getContent(name);
+ }
+
+ /**
+ * Returns a static content resource from the
+ * resource manager.
+ *
+ * @param name Name of content resource to get
+ * @param encoding Character encoding to use
+ * @return parsed ContentResource object ready for use
+ * @throws ResourceNotFoundException if template not found
+ * from any available source.
+ * @throws ParseErrorException When the template could not be parsed.
+ * @see RuntimeInstance#getContent(String, String)
+ */
+ public static ContentResource getContent( String name, String encoding )
+ throws ResourceNotFoundException, ParseErrorException
+ {
+ return ri.getContent(name, encoding);
+ }
+
+
+ /**
+ * Determines is a template exists, and returns name of the loader that
+ * provides it. This is a slightly less hokey way to support
+ * the Velocity.templateExists() utility method, which was broken
+ * when per-template encoding was introduced. We can revisit this.
+ *
+ * @param resourceName Name of template or content resource
+ * @return class name of loader than can provide it
+ * @see RuntimeInstance#getLoaderNameForResource(String)
+ */
+ public static String getLoaderNameForResource( String resourceName )
+ {
+ return ri.getLoaderNameForResource(resourceName);
+ }
+
+
+ /**
+ * Returns the configured logger.
+ *
+ * @return A Logger instance
+ * @see RuntimeInstance#getLog()
+ * @since 1.5
+ */
+ public static Logger getLog()
+ {
+ return ri.getLog();
+ }
+
+ /**
+ * String property accessor method with default to hide the
+ * configuration implementation.
+ *
+ * @param key property key
+ * @param defaultValue default value to return if key not
+ * found in resource manager.
+ * @return value of key or default
+ * @see RuntimeInstance#getString(String, String)
+ */
+ public static String getString( String key, String defaultValue)
+ {
+ return ri.getString(key, defaultValue);
+ }
+
+ /**
+ * Returns the appropriate VelocimacroProxy object if strVMname
+ * is a valid current Velocimacro.
+ *
+ * @param vmName Name of velocimacro requested
+ * @param renderingTemplate Template we are currently rendering. This
+ * information is needed when VM_PERM_ALLOW_INLINE_REPLACE_GLOBAL setting is true
+ * and template contains a macro with the same name as the global macro library.
+ * @param template current template
+ *
+ * @return VelocimacroProxy
+ */
+ public static Directive getVelocimacro(String vmName, Template renderingTemplate, Template template)
+ {
+ return ri.getVelocimacro(vmName, renderingTemplate, template);
+ }
+
+ /**
+ * Adds a new Velocimacro. Usually called by Macro only while parsing.
+ *
+ * @param name Name of a new velocimacro.
+ * @param macro root AST node of the parsed macro
+ * @param macroArgs Array of macro arguments, containing the
+ * #macro() arguments and default values. the 0th is the name.
+ * @param definingTemplate Template containing the definition of the macro.
+ * @return true for success
+ */
+ public static boolean addVelocimacro(String name, Node macro,
+ List<Macro.MacroArg> macroArgs, Template definingTemplate)
+ {
+ return ri.addVelocimacro(name, macro, macroArgs, definingTemplate);
+ }
+
+ /**
+ * Checks to see if a VM exists
+ *
+ * @param vmName Name of the Velocimacro.
+ * @param template Template on which to look for the Macro.
+ * @return True if VM by that name exists, false if not
+ */
+ public static boolean isVelocimacro(String vmName, Template template)
+ {
+ return ri.isVelocimacro(vmName, template);
+ }
+
+ /* --------------------------------------------------------------------
+ * R U N T I M E A C C E S S O R M E T H O D S
+ * --------------------------------------------------------------------
+ * These are the getXXX() methods that are a simple wrapper
+ * around the configuration object. This is an attempt
+ * to make a the Velocity Runtime the single access point
+ * for all things Velocity, and allow the Runtime to
+ * adhere as closely as possible the the Mediator pattern
+ * which is the ultimate goal.
+ * --------------------------------------------------------------------
+ */
+
+ /**
+ * String property accessor method to hide the configuration implementation
+ * @param key property key
+ * @return value of key or null
+ * @see RuntimeInstance#getString(String)
+ */
+ public static String getString(String key)
+ {
+ return ri.getString( key );
+ }
+
+ /**
+ * Int property accessor method to hide the configuration implementation.
+ *
+ * @param key Property key
+ * @return value
+ * @see RuntimeInstance#getInt(String)
+ */
+ public static int getInt( String key )
+ {
+ return ri.getInt(key);
+ }
+
+ /**
+ * Int property accessor method to hide the configuration implementation.
+ *
+ * @param key property key
+ * @param defaultValue The default value.
+ * @return value
+ * @see RuntimeInstance#getInt(String, int)
+ */
+ public static int getInt( String key, int defaultValue )
+ {
+ return ri.getInt( key, defaultValue );
+ }
+
+ /**
+ * Boolean property accessor method to hide the configuration implementation.
+ *
+ * @param key property key
+ * @param def The default value if property not found.
+ * @return value of key or default value
+ * @see RuntimeInstance#getBoolean(String, boolean)
+ */
+ public static boolean getBoolean( String key, boolean def )
+ {
+ return ri.getBoolean(key, def);
+ }
+
+ /**
+ * Directly set the ExtProperties configuration object
+ * @param configuration
+ * @see RuntimeInstance#setConfiguration(ExtProperties)
+ */
+ public static void setConfiguration(ExtProperties configuration)
+ {
+ ri.setConfiguration(configuration);
+ }
+
+ /**
+ * Return the velocity runtime configuration object.
+ *
+ * @return ExtProperties configuration object which houses
+ * the velocity runtime properties.
+ * @see RuntimeInstance#getConfiguration()
+ */
+ public static ExtProperties getConfiguration()
+ {
+ return ri.getConfiguration();
+ }
+
+ /**
+ * Returns the event handlers for the application.
+ * @return The event handlers for the application.
+ * @see RuntimeInstance#getApplicationEventCartridge()
+ * @since 1.5
+ */
+ public EventCartridge getEventCartridge()
+ {
+ return ri.getApplicationEventCartridge();
+ }
+
+ /**
+ * Gets the application attribute for the given key
+ *
+ * @see org.apache.velocity.runtime.RuntimeServices#getApplicationAttribute(Object)
+ * @param key
+ * @return The application attribute for the given key.
+ * @see RuntimeInstance#getApplicationAttribute(Object)
+ */
+ public static Object getApplicationAttribute(Object key)
+ {
+ return ri.getApplicationAttribute(key);
+ }
+
+ /**
+ * Returns the Uberspect object for this Instance.
+ *
+ * @return The Uberspect object for this Instance.
+ * @see org.apache.velocity.runtime.RuntimeServices#getUberspect()
+ * @see RuntimeInstance#getUberspect()
+ */
+ public static Uberspect getUberspect()
+ {
+ return ri.getUberspect();
+ }
+
+
+ /**
+ * Remove a directive.
+ *
+ * @param name name of the directive.
+ */
+ public static void removeDirective(String name)
+ {
+ ri.removeDirective(name);
+ }
+
+ /**
+ * Instantiates and loads the directive with some basic checks.
+ *
+ * @param directiveClass classname of directive to load
+ */
+ public static void loadDirective(String directiveClass)
+ {
+ ri.loadDirective(directiveClass);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/VelocimacroFactory.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/VelocimacroFactory.java
new file mode 100644
index 00000000..b7961a5d
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/VelocimacroFactory.java
@@ -0,0 +1,639 @@
+package org.apache.velocity.runtime;
+
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.Template;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.directive.Directive;
+import org.apache.velocity.runtime.directive.Macro;
+import org.apache.velocity.runtime.directive.VelocimacroProxy;
+import org.apache.velocity.runtime.parser.node.Node;
+
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+/**
+ * VelocimacroFactory.java
+ *
+ * manages the set of VMs in a running Velocity engine.
+ *
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class VelocimacroFactory
+{
+ /**
+ * runtime services for this instance
+ */
+ private final RuntimeServices rsvc;
+
+ /**
+ * the log for this instance
+ */
+ private Logger log = null;
+
+ /**
+ * VMManager: deal with namespace management
+ * and actually keeps all the VM definitions
+ */
+ private VelocimacroManager vmManager = null;
+
+ /**
+ * determines if replacement of global VMs are allowed
+ * controlled by VM_PERM_ALLOW_INLINE_REPLACE_GLOBAL
+ */
+ private boolean replaceAllowed = false;
+
+ /**
+ * controls if new VMs can be added. Set by
+ * VM_PERM_ALLOW_INLINE Note the assumption that only
+ * through inline defs can this happen.
+ * additions through autoloaded VMs is allowed
+ */
+ private boolean addNewAllowed = true;
+
+ /**
+ * sets if template-local namespace in used
+ */
+ private boolean templateLocal = false;
+
+ /**
+ * determines if the libraries are auto-loaded
+ * when they change
+ */
+ private boolean autoReloadLibrary = false;
+
+ /**
+ * vector of the library names
+ */
+ private List<String> macroLibVec = null;
+
+ /**
+ * map of the library Template objects
+ * used for reload determination
+ */
+ private Map<String, Twonk> libModMap;
+
+ /**
+ * C'tor for the VelociMacro factory.
+ *
+ * @param rsvc Reference to a runtime services object.
+ */
+ public VelocimacroFactory(final RuntimeServices rsvc)
+ {
+ this.rsvc = rsvc;
+
+ /*
+ * we always access in a synchronized(), so we
+ * can use an unsynchronized hashmap
+ */
+ libModMap = new HashMap<>();
+ vmManager = new VelocimacroManager(rsvc);
+ }
+
+ /**
+ * initialize the factory - setup all permissions
+ * load all global libraries.
+ */
+ public void initVelocimacro()
+ {
+ /*
+ * maybe I'm just paranoid...
+ */
+ synchronized(this)
+ {
+ log = rsvc.getLog("macro");
+ log.trace("initialization starting.");
+
+ /*
+ * allow replacements while we add the libraries, if exist
+ */
+ setReplacementPermission(true);
+
+ /*
+ * add all library macros to the global namespace
+ */
+
+ vmManager.setNamespaceUsage(false);
+
+ /*
+ * now, if there is a global or local libraries specified, use them.
+ * All we have to do is get the template. The template will be parsed;
+ * VM's are added during the parse phase
+ */
+
+ Object libfiles = rsvc.getProperty(RuntimeConstants.VM_LIBRARY);
+
+ if (libfiles == null)
+ {
+ log.debug("\"{}\" is not set. Trying default library: {}", RuntimeConstants.VM_LIBRARY, RuntimeConstants.VM_LIBRARY_DEFAULT);
+
+ // try the default library.
+ if (rsvc.getLoaderNameForResource(RuntimeConstants.VM_LIBRARY_DEFAULT) != null)
+ {
+ libfiles = RuntimeConstants.VM_LIBRARY_DEFAULT;
+ }
+ else
+ {
+ // try the old default library
+ log.debug("Default library {} not found. Trying old default library: {}", RuntimeConstants.VM_LIBRARY_DEFAULT, DeprecatedRuntimeConstants.OLD_VM_LIBRARY_DEFAULT);
+ if (rsvc.getLoaderNameForResource(RuntimeConstants.OLD_VM_LIBRARY_DEFAULT) != null)
+ {
+ libfiles = RuntimeConstants.OLD_VM_LIBRARY_DEFAULT;
+ }
+ else
+ {
+ log.debug("Old default library {} not found.", DeprecatedRuntimeConstants.OLD_VM_LIBRARY_DEFAULT);
+ }
+ }
+ }
+
+ if(libfiles != null)
+ {
+ macroLibVec = new ArrayList<>();
+ if (libfiles instanceof Vector)
+ {
+ macroLibVec.addAll((Vector<String>)libfiles);
+ }
+ else if (libfiles instanceof String)
+ {
+ macroLibVec.add((String)libfiles);
+ }
+
+ for (String lib : macroLibVec)
+ {
+ /*
+ * only if it's a non-empty string do we bother
+ */
+
+ if (StringUtils.isNotEmpty(lib))
+ {
+ /*
+ * let the VMManager know that the following is coming
+ * from libraries - need to know for auto-load
+ */
+
+ vmManager.setRegisterFromLib(true);
+
+ log.debug("adding VMs from VM library: {}", lib);
+
+ try
+ {
+ Template template = rsvc.getTemplate(lib);
+
+ /*
+ * save the template. This depends on the assumption
+ * that the Template object won't change - currently
+ * this is how the Resource manager works
+ */
+
+ Twonk twonk = new Twonk();
+ twonk.template = template;
+ twonk.modificationTime = template.getLastModified();
+ libModMap.put(lib, twonk);
+ }
+ catch (Exception e)
+ {
+ String msg = "Velocimacro: Error using VM library: " + lib;
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+
+ log.trace("VM library registration complete.");
+
+ vmManager.setRegisterFromLib(false);
+ }
+ }
+ }
+
+ /*
+ * now, the permissions
+ */
+
+
+ /*
+ * allowinline: anything after this will be an inline macro, I think
+ * there is the question if a #include is an inline, and I think so
+ *
+ * default = true
+ */
+ setAddMacroPermission(true);
+
+ if (!rsvc.getBoolean( RuntimeConstants.VM_PERM_ALLOW_INLINE, true))
+ {
+ setAddMacroPermission(false);
+
+ log.debug("allowInline = false: VMs can NOT be defined inline in templates");
+ }
+ else
+ {
+ log.debug("allowInline = true: VMs can be defined inline in templates");
+ }
+
+ /*
+ * allowInlineToReplaceGlobal: allows an inline VM , if allowed at all,
+ * to replace an existing global VM
+ *
+ * default = false
+ */
+ setReplacementPermission(false);
+
+ if (rsvc.getBoolean(
+ RuntimeConstants.VM_PERM_ALLOW_INLINE_REPLACE_GLOBAL, false))
+ {
+ setReplacementPermission(true);
+
+ log.debug("allowInlineToOverride = true: VMs " +
+ "defined inline may replace previous VM definitions");
+ }
+ else
+ {
+ log.debug("allowInlineToOverride = false: VMs " +
+ "defined inline may NOT replace previous VM definitions");
+ }
+
+ /*
+ * now turn on namespace handling as far as permissions allow in the
+ * manager, and also set it here for gating purposes
+ */
+ vmManager.setNamespaceUsage(true);
+
+ /*
+ * template-local inline VM mode: default is off
+ */
+ setTemplateLocalInline(rsvc.getBoolean(
+ RuntimeConstants.VM_PERM_INLINE_LOCAL, false));
+
+ if (getTemplateLocalInline())
+ {
+ log.debug("allowInlineLocal = true: VMs " +
+ "defined inline will be local to their defining template only.");
+ }
+ else
+ {
+ log.debug("allowInlineLocal = false: VMs " +
+ "defined inline will be global in scope if allowed.");
+ }
+
+ vmManager.setTemplateLocalInlineVM(getTemplateLocalInline());
+
+ /*
+ * autoload VM libraries
+ */
+ setAutoload(rsvc.getBoolean(RuntimeConstants.VM_LIBRARY_AUTORELOAD, false));
+
+ if (getAutoload())
+ {
+ log.debug("autoload on: VM system " +
+ "will automatically reload global library macros");
+ }
+ else
+ {
+ log.debug("autoload off: VM system " +
+ "will not automatically reload global library macros");
+ }
+
+ log.trace("Velocimacro: initialization complete.");
+ }
+ }
+
+ /**
+ * Adds a macro to the factory.
+ *
+ * @param name Name of the Macro to add.
+ * @param macroBody root node of the parsed macro AST
+ * @param macroArgs Array of macro arguments, containing the
+ * #macro() arguments and default values. the 0th is the name.
+ * @param definingTemplate template containing the macro definition
+ * @return true if Macro was registered successfully.
+ * @since 1.6
+ */
+ public boolean addVelocimacro(String name, Node macroBody,
+ List<Macro.MacroArg> macroArgs, Template definingTemplate)
+ {
+ // Called by RuntimeInstance.addVelocimacro
+
+ /*
+ * maybe we should throw an exception, maybe just tell
+ * the caller like this...
+ *
+ * I hate this: maybe exceptions are in order here...
+ * They definitely would be if this was only called by directly
+ * by users, but Velocity calls this internally.
+ */
+ if (name == null || macroBody == null || macroArgs == null ||
+ definingTemplate == null)
+ {
+ String msg = "VM '"+name+"' addition rejected: ";
+ if (name == null)
+ {
+ msg += "name";
+ }
+ else if (macroBody == null)
+ {
+ msg += "macroBody";
+ }
+ else if (macroArgs == null)
+ {
+ msg += "macroArgs";
+ }
+ else
+ {
+ msg += "sourceTemplate";
+ }
+ msg += " argument was null";
+ log.error(msg);
+ throw new NullPointerException(msg);
+ }
+
+ /*
+ * see if the current ruleset allows this addition
+ */
+
+ if (!canAddVelocimacro(name, definingTemplate))
+ {
+ return false;
+ }
+
+ synchronized(this)
+ {
+ vmManager.addVM(name, macroBody, macroArgs, definingTemplate, replaceAllowed);
+ }
+ log.debug("added VM {}: source={}", name, definingTemplate);
+ return true;
+ }
+
+
+ /**
+ * determines if a given macro/namespace (name, source) combo is allowed
+ * to be added
+ *
+ * @param name Name of VM to add
+ * @param definingTemplate template containing the source of the macro
+ * @return true if it is allowed to be added, false otherwise
+ */
+ private synchronized boolean canAddVelocimacro(String name, Template definingTemplate)
+ {
+ /*
+ * short circuit and do it if autoloader is on, and the
+ * template is one of the library templates
+ */
+
+ if (autoReloadLibrary && (macroLibVec != null))
+ {
+ if( macroLibVec.contains(definingTemplate.getName()) )
+ return true;
+ }
+
+
+ /*
+ * maybe the rules should be in manager? I dunno. It's to manage
+ * the namespace issues first, are we allowed to add VMs at all?
+ * This trumps all.
+ */
+ if (!addNewAllowed)
+ {
+ log.warn("VM addition rejected: {}: inline VelociMacros not allowed.", name);
+ return false;
+ }
+
+ /*
+ * are they local in scope? Then it is ok to add.
+ */
+ if (!templateLocal)
+ {
+ /*
+ * otherwise, if we have it already in global namespace, and they can't replace
+ * since local templates are not allowed, the global namespace is implied.
+ * remember, we don't know anything about namespace management here, so lets
+ * note do anything fancy like trying to give it the global namespace here
+ *
+ * so if we have it, and we aren't allowed to replace, bail
+ */
+ if (!replaceAllowed && isVelocimacro(name, definingTemplate))
+ {
+ /*
+ * Concurrency fix: the log entry was changed to debug scope because it
+ * causes false alarms when several concurrent threads simultaneously (re)parse
+ * some macro
+ */
+ log.debug("VM addition rejected: {}: inline not allowed to replace existing VM", name);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Tells the world if a given directive string is a Velocimacro
+ * @param vm Name of the Macro.
+ * @param template Source template from which the macro should be loaded.
+ * @return True if the given name is a macro.
+ */
+ public boolean isVelocimacro(String vm, Template template)
+ {
+ // synchronization removed
+ return(vmManager.get(vm, null, template) != null);
+ }
+
+ /**
+ * actual factory: creates a Directive that will
+ * behave correctly wrt getting the framework to
+ * dig out the correct # of args
+ * @param vmName Name of the Macro.
+ * @param renderingTemplate destination template
+ * @param sourceTemplate Source template from which the macro should be loaded.
+ * @return A directive representing the Macro.
+ */
+ public Directive getVelocimacro(String vmName, Template renderingTemplate, Template sourceTemplate)
+ {
+ VelocimacroProxy vp = null;
+
+ vp = vmManager.get(vmName, renderingTemplate, sourceTemplate);
+
+ /*
+ * if this exists, and autoload is on, we need to check where this VM came from
+ */
+
+ if (vp != null && autoReloadLibrary )
+ {
+ synchronized (this)
+ {
+ /*
+ * see if this VM came from a library. Need to pass sourceTemplate in the event
+ * namespaces are set, as it could be masked by local
+ */
+
+ String lib = vmManager.getLibraryName(vmName, sourceTemplate);
+
+ if (lib != null)
+ {
+ try
+ {
+ /*
+ * get the template from our map
+ */
+
+ Twonk tw = libModMap.get(lib);
+
+ if (tw != null)
+ {
+ Template template = tw.template;
+
+ /*
+ * now, compare the last modified time of the resource with the last
+ * modified time of the template if the file has changed, then reload.
+ * Otherwise, we should be ok.
+ */
+
+ long tt = tw.modificationTime;
+ long ft = template.getResourceLoader().getLastModified(template);
+
+ if (ft > tt)
+ {
+ log.debug("auto-reloading VMs from VM library: {}", lib);
+
+ /*
+ * when there are VMs in a library that invoke each other, there are
+ * calls into getVelocimacro() from the init() process of the VM
+ * directive. To stop the infinite loop we save the current time
+ * reported by the resource loader and then be honest when the
+ * reload is complete
+ */
+
+ tw.modificationTime = ft;
+
+ template = rsvc.getTemplate(lib);
+
+ /*
+ * and now we be honest
+ */
+
+ tw.template = template;
+ tw.modificationTime = template.getLastModified();
+
+ /*
+ * note that we don't need to put this twonk
+ * back into the map, as we can just use the
+ * same reference and this block is synchronized
+ */
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ String msg = "Velocimacro: Error using VM library: " + lib;
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+
+ vp = vmManager.get(vmName, sourceTemplate, renderingTemplate);
+ }
+ }
+ }
+
+ return vp;
+ }
+
+ /**
+ * sets permission to have VMs local in scope to their declaring template note that this is
+ * really taken care of in the VMManager class, but we need it here for gating purposes in addVM
+ * eventually, I will slide this all into the manager, maybe.
+ */
+ private void setTemplateLocalInline(boolean b)
+ {
+ templateLocal = b;
+ }
+
+ private boolean getTemplateLocalInline()
+ {
+ return templateLocal;
+ }
+
+ /**
+ * sets the permission to add new macros
+ */
+ private boolean setAddMacroPermission(final boolean addNewAllowed)
+ {
+ boolean b = this.addNewAllowed;
+ this.addNewAllowed = addNewAllowed;
+ return b;
+ }
+
+ /**
+ * sets the permission for allowing addMacro() calls to replace existing VM's
+ */
+ private boolean setReplacementPermission(boolean arg)
+ {
+ boolean b = replaceAllowed;
+ replaceAllowed = arg;
+ vmManager.setInlineReplacesGlobal(arg);
+ return b;
+ }
+
+ /**
+ * set the switch for automatic reloading of
+ * global library-based VMs
+ */
+ private void setAutoload(boolean b)
+ {
+ autoReloadLibrary = b;
+ }
+
+ /**
+ * get the switch for automatic reloading of
+ * global library-based VMs
+ */
+ private boolean getAutoload()
+ {
+ return autoReloadLibrary;
+ }
+
+ /**
+ * small container class to hold the tuple
+ * of a template and modification time.
+ * We keep the modification time so we can
+ * 'override' it on a reload to prevent
+ * recursive reload due to inter-calling
+ * VMs in a library
+ */
+ private static class Twonk
+ {
+ /** Template kept in this container. */
+ public Template template;
+
+ /** modification time of the template. */
+ public long modificationTime;
+ }
+}
+
+
+
+
+
+
+
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/VelocimacroManager.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/VelocimacroManager.java
new file mode 100644
index 00000000..63dc92b9
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/VelocimacroManager.java
@@ -0,0 +1,355 @@
+package org.apache.velocity.runtime;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.directive.Macro;
+import org.apache.velocity.runtime.directive.VelocimacroProxy;
+import org.apache.velocity.runtime.parser.node.Node;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Manages VMs in namespaces. Currently, two namespace modes are
+ * supported:
+ *
+ * <ul>
+ * <li>flat - all allowable VMs are in the global namespace</li>
+ * <li>local - inline VMs are added to it's own template namespace</li>
+ * </ul>
+ *
+ * Thanks to <a href="mailto:JFernandez@viquity.com">Jose Alberto Fernandez</a>
+ * for some ideas incorporated here.
+ *
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:JFernandez@viquity.com">Jose Alberto Fernandez</a>
+ * @version $Id$
+ */
+public class VelocimacroManager
+{
+ private boolean registerFromLib = false;
+
+ /** reference to global namespace hash */
+ private final Map<String, MacroEntry> globalNamespace;
+
+ /** set of names of library templates/namespaces */
+ private final Map<String, Template> libraries = new ConcurrentHashMap<>(17, 0.5f, 20);
+
+ private RuntimeServices rsvc = null;
+
+ /*
+ * big switch for namespaces. If true, then properties control
+ * usage. If false, no.
+ */
+ private boolean namespacesOn = true;
+ private boolean inlineLocalMode = false;
+ private boolean inlineReplacesGlobal = false;
+
+ /**
+ * Adds the global namespace to the hash.
+ */
+ VelocimacroManager(RuntimeServices rsvc)
+ {
+ /*
+ * add the global namespace to the namespace hash. We always have that.
+ */
+
+ globalNamespace = new ConcurrentHashMap<>(101, 0.5f, 20);
+ this.rsvc = rsvc;
+ }
+
+ /**
+ * Adds a VM definition to the cache.
+ *
+ * Called by VelocimacroFactory.addVelociMacro (after parsing and discovery in Macro directive)
+ *
+ * @param vmName Name of the new VelociMacro.
+ * @param macroBody String representation of the macro body.
+ * @param macroArgs Array of macro arguments, containing the
+ * #macro() arguments and default values. the 0th is the name.
+ * @param definingTemplate The template from which this macro has been loaded.
+ * @param canReplaceGlobalMacro whether this macro can replace a global macro
+ * @return Whether everything went okay.
+ */
+ public boolean addVM(final String vmName, final Node macroBody, List<Macro.MacroArg> macroArgs,
+ final Template definingTemplate, boolean canReplaceGlobalMacro)
+ {
+ if (macroBody == null)
+ {
+ // happens only if someone uses this class without the Macro directive
+ // and provides a null value as an argument
+ throw new VelocityException("Null AST for "+vmName+" in " + definingTemplate.getName());
+ }
+
+ MacroEntry me = new MacroEntry(vmName, macroBody, macroArgs, definingTemplate.getName(), rsvc);
+
+ me.setFromLibrary(registerFromLib);
+
+ /*
+ * the client (VMFactory) will signal to us via
+ * registerFromLib that we are in startup mode registering
+ * new VMs from libraries. Therefore, we want to
+ * addto the library map for subsequent auto reloads
+ */
+
+ boolean isLib = true;
+
+ MacroEntry exist = globalNamespace.get(vmName);
+
+ if (registerFromLib)
+ {
+ libraries.put(definingTemplate.getName(), definingTemplate);
+ }
+ else
+ {
+ /*
+ * now, we first want to check to see if this namespace (template)
+ * is actually a library - if so, we need to use the global namespace
+ * we don't have to do this when registering, as namespaces should
+ * be shut off. If not, the default value is true, so we still go
+ * global
+ */
+
+ isLib = libraries.containsKey(definingTemplate.getName());
+ }
+
+ if ( !isLib && usingNamespaces() )
+ {
+ definingTemplate.getMacros().put(vmName, me);
+ }
+ else
+ {
+ /*
+ * otherwise, add to global template. First, check if we
+ * already have it to preserve some of the autoload information
+ */
+
+
+ if (exist != null)
+ {
+ me.setFromLibrary(exist.getFromLibrary());
+ }
+
+ /*
+ * now add it
+ */
+
+ globalNamespace.put(vmName, me);
+
+ }
+ return true;
+ }
+
+ /**
+ * Gets a VelocimacroProxy object by the name / source template duple.
+ *
+ * @param vmName Name of the VelocityMacro to look up.
+ * @param renderingTemplate Template we are currently rendering.
+ * @param template Source Template.
+ * @return A proxy representing the Macro.
+ */
+ public VelocimacroProxy get(final String vmName, final Template renderingTemplate, final Template template)
+ {
+ if( inlineReplacesGlobal && renderingTemplate != null )
+ {
+ /*
+ * if VM_PERM_ALLOW_INLINE_REPLACE_GLOBAL is true (local macros can
+ * override global macros) and we know which template we are rendering at the
+ * moment, check if local namespace contains a macro we are looking for
+ * if so, return it instead of the global one
+ */
+
+ MacroEntry me = (MacroEntry)renderingTemplate.getMacros().get(vmName);
+ if( me != null )
+ {
+ return me.getProxy();
+ }
+ }
+
+ if( usingNamespaces() && template != null )
+ {
+ MacroEntry me = (MacroEntry)template.getMacros().get(vmName);
+ if( template.getMacros().size() > 0 && me != null )
+ {
+ return me.getProxy();
+ }
+ }
+
+ MacroEntry me = globalNamespace.get(vmName);
+
+ if (me != null)
+ {
+ return me.getProxy();
+ }
+
+ return null;
+ }
+
+ /**
+ * public switch to let external user of manager to control namespace
+ * usage indep of properties. That way, for example, at startup the
+ * library files are loaded into global namespace
+ *
+ * @param namespaceOn True if namespaces should be used.
+ */
+ public void setNamespaceUsage(final boolean namespaceOn)
+ {
+ this.namespacesOn = namespaceOn;
+ }
+
+ /**
+ * Should macros registered from Libraries be marked special?
+ * @param registerFromLib True if macros from Libs should be marked.
+ */
+ public void setRegisterFromLib(final boolean registerFromLib)
+ {
+ this.registerFromLib = registerFromLib;
+ }
+
+ /**
+ * Should macros from the same template be inlined?
+ *
+ * @param inlineLocalMode True if macros should be inlined on the same template.
+ */
+ public void setTemplateLocalInlineVM(final boolean inlineLocalMode)
+ {
+ this.inlineLocalMode = inlineLocalMode;
+ }
+
+ /**
+ * determines if currently using namespaces.
+ *
+ * @return true if using namespaces, false if not
+ */
+ private boolean usingNamespaces()
+ {
+ /*
+ * if the big switch turns of namespaces, then ignore the rules
+ */
+
+ if (!namespacesOn)
+ {
+ return false;
+ }
+
+ /*
+ * currently, we only support the local template namespace idea
+ */
+
+ return inlineLocalMode;
+
+ }
+
+ /**
+ * Return the library name for a given macro.
+ * @param vmName Name of the Macro to look up.
+ * @param template Template
+ * @return The name of the library which registered this macro in a namespace.
+ */
+ public String getLibraryName(final String vmName, Template template)
+ {
+ if (usingNamespaces())
+ {
+ /*
+ * if we have this macro defined in this namespace, then
+ * it is masking the global, library-based one, so
+ * just return null
+ */
+ MacroEntry me = (MacroEntry)template.getMacros().get(vmName);
+ if( me != null )
+ return null;
+ }
+
+ MacroEntry me = globalNamespace.get(vmName);
+
+ if (me != null)
+ {
+ return me.getSourceTemplate();
+ }
+
+ return null;
+ }
+
+ /**
+ * @param is
+ * @since 1.6
+ */
+ public void setInlineReplacesGlobal(boolean is)
+ {
+ inlineReplacesGlobal = is;
+ }
+
+
+ /**
+ * wrapper class for holding VM information
+ */
+ private static class MacroEntry
+ {
+ private final String sourceTemplate;
+ private boolean fromLibrary = false;
+ private VelocimacroProxy vp;
+
+ private MacroEntry(final String vmName, final Node macro,
+ List<Macro.MacroArg> macroArgs, final String sourceTemplate,
+ RuntimeServices rsvc)
+ {
+ this.sourceTemplate = sourceTemplate;
+
+ vp = new VelocimacroProxy();
+ vp.init(rsvc);
+ vp.setName(vmName);
+ vp.setMacroArgs(macroArgs);
+ vp.setNodeTree((SimpleNode)macro);
+ vp.setLocation(macro.getLine(), macro.getColumn(), macro.getTemplate());
+ }
+
+ /**
+ * Has the macro been registered from a library.
+ * @param fromLibrary True if the macro was registered from a Library.
+ */
+ public void setFromLibrary(final boolean fromLibrary)
+ {
+ this.fromLibrary = fromLibrary;
+ }
+
+ /**
+ * Returns true if the macro was registered from a library.
+ * @return True if the macro was registered from a library.
+ */
+ public boolean getFromLibrary()
+ {
+ return fromLibrary;
+ }
+
+ public String getSourceTemplate()
+ {
+ return sourceTemplate;
+ }
+
+ VelocimacroProxy getProxy()
+ {
+ return vp;
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Block.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Block.java
new file mode 100755
index 00000000..e2d34a78
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Block.java
@@ -0,0 +1,209 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.Renderable;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.parser.node.Node;
+import org.apache.velocity.util.StringBuilderWriter;
+import org.apache.velocity.util.StringUtils;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Directive that puts an unrendered AST block in the context
+ * under the specified key, postponing rendering until the
+ * reference is used and rendered.
+ *
+ * @author Andrew Tetlaw
+ * @author Nathan Bubna
+ * @author <a href="mailto:wyla@removethis.sci.fi">Jarkko Viinamaki</a>
+ * @since 1.7
+ * @version $Id: Block.java 686842 2008-08-18 18:29:31Z nbubna $
+ */
+public abstract class Block extends Directive
+{
+ protected Node block;
+ protected Logger log;
+ protected int maxDepth;
+ protected String key;
+
+ /**
+ * Return type of this directive.
+ * @return type, DirectiveConstants.BLOCK or DirectiveConstants.LINE
+ */
+ @Override
+ public int getType()
+ {
+ return BLOCK;
+ }
+
+ /**
+ * simple init - get the key
+ * @param rs
+ * @param context
+ * @param node
+ */
+ @Override
+ public void init(RuntimeServices rs, InternalContextAdapter context, Node node)
+ throws TemplateInitException
+ {
+ super.init(rs, context, node);
+
+ log = rsvc.getLog();
+
+ /*
+ * No checking is done. We just grab the last child node and assume
+ * that it's the block!
+ */
+ block = node.jjtGetChild(node.jjtGetNumChildren() - 1);
+ }
+
+ /**
+ * renders block directive
+ * @param context
+ * @param writer
+ * @return success status
+ */
+ public boolean render(InternalContextAdapter context, Writer writer)
+ {
+ preRender(context);
+ try
+ {
+ return block.render(context, writer);
+ }
+ catch (IOException e)
+ {
+ String msg = "Failed to render " + id(context) + " to writer at " +
+ StringUtils.formatFileString(this);
+ log.error(msg, e);
+ throw new RuntimeException(msg, e);
+ }
+ catch (StopCommand stop)
+ {
+ if (!stop.isFor(this))
+ {
+ throw stop;
+ }
+ return true;
+ }
+ finally
+ {
+ postRender(context);
+ }
+ }
+
+ /**
+ * Creates a string identifying the source and location of the block
+ * definition, and the current template being rendered if that is
+ * different.
+ * @param context
+ * @return id string
+ */
+ protected String id(InternalContextAdapter context)
+ {
+ StringBuilder str = new StringBuilder(100)
+ .append("block $").append(key);
+ if (!context.getCurrentTemplateName().equals(getTemplateName()))
+ {
+ str.append(" used in ").append(context.getCurrentTemplateName());
+ }
+ return str.toString();
+ }
+
+ /**
+ * actual class placed in the context, holds the context
+ * being used for the render, as well as the parent (which already holds
+ * everything else we need).
+ */
+ public static class Reference implements Renderable
+ {
+ private InternalContextAdapter context;
+ private Block parent;
+ private int depth;
+
+ /**
+ * @param context
+ * @param parent
+ */
+ public Reference(InternalContextAdapter context, Block parent)
+ {
+ this.context = context;
+ this.parent = parent;
+ }
+
+ /**
+ * Render the AST of this block into the writer using the context.
+ * @param context
+ * @param writer
+ * @return success status
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer)
+ {
+ depth++;
+ if (depth > parent.maxDepth)
+ {
+ /* this is only a debug message, as recursion can
+ * happen in quasi-innocent situations and is relatively
+ * harmless due to how we handle it here.
+ * this is more to help anyone nuts enough to intentionally
+ * use recursive block definitions and having problems
+ * pulling it off properly.
+ */
+ parent.log.debug("Max recursion depth reached for {} at {}", parent.id(context), StringUtils.formatFileString(parent));
+ depth--;
+ return false;
+ }
+ else
+ {
+ parent.render(context, writer);
+ depth--;
+ return true;
+ }
+ }
+
+ /**
+ * Makes #if( $blockRef ) true without rendering, so long as we aren't beyond max depth.
+ * @return reference value as boolean
+ */
+ public boolean getAsBoolean()
+ {
+ return depth <= parent.maxDepth;
+ }
+
+ /**
+ * @return rendered string
+ */
+ public String toString()
+ {
+ Writer writer = new StringBuilderWriter();
+ if (render(context, writer))
+ {
+ return writer.toString();
+ }
+ return null;
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/BlockMacro.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/BlockMacro.java
new file mode 100644
index 00000000..713dc5ce
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/BlockMacro.java
@@ -0,0 +1,125 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.parser.node.Node;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * BlockMacro directive is used to invoke Velocity macros with normal parameters and a macro body.
+ * <p>
+ * The macro can then refer to the passed body AST. This directive can be used as a
+ * "decorator". Body AST can contain any valid Velocity syntax.
+ *
+ * An example:
+ * <pre>
+ * #set($foobar = "yeah!")
+ *
+ * #macro(strong $txt)
+ * &lt;strong&gt;$bodyContent&lt;/strong&gt; $txt
+ * #end
+ *
+ * #@strong($foobar)
+ * &lt;u&gt;This text is underlined and bold&lt;/u&gt;
+ * #end
+ * </pre>
+ * Will print:
+ * <pre>
+ * &lt;strong&gt;&lt;u&gt;This text is underlined and bold&lt;u&gt;&lt;/strong&gt; yeah!
+ * </pre>
+ *
+ * bodyContent reference name is configurable (see velocity.properties).
+ *
+ * @author <a href="mailto:wyla@removethis.sci.fi">Jarkko Viinamaki</a>
+ * @since 1.7
+ * @version $Id$
+ */
+public class BlockMacro extends Block
+{
+ private String name;
+ private RuntimeMacro macro;
+
+ @Override
+ public String getName()
+ {
+ return key;
+ }
+
+ /**
+ * Override to use the macro name, since it is within an
+ * #@myMacro() ... #end block that the scope in question
+ * would be used.
+ */
+ @Override
+ public String getScopeName()
+ {
+ return name;
+ }
+
+ /**
+ * Initializes the directive.
+ *
+ * @param rs
+ * @param macroName
+ * @param context
+ * @param node
+ * @throws TemplateInitException
+ */
+ public void init(RuntimeServices rs, String macroName, InternalContextAdapter context, Node node)
+ throws TemplateInitException
+ {
+ this.name = macroName;
+
+ super.init(rs, context, node);
+
+ // get name of the reference that refers to AST block passed to block macro call
+ key = rsvc.getString(RuntimeConstants.VM_BODY_REFERENCE, "bodyContent");
+
+ // use the macro max depth for bodyContent max depth as well
+ maxDepth = rsvc.getInt(RuntimeConstants.VM_MAX_DEPTH);
+
+ macro = new RuntimeMacro();
+ macro.setLocation(getLine(), getColumn(), getTemplate());
+ macro.init(rsvc, name, context, node);
+ }
+
+ /**
+ * Renders content using the selected macro and the passed AST body.
+ *
+ * @param context
+ * @param writer
+ * @param node
+ * @return True if the directive rendered successfully.
+ * @throws IOException
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer, Node node)
+ throws IOException
+ {
+ return macro.render(context, writer, node, new Reference(context, this));
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Break.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Break.java
new file mode 100644
index 00000000..053afd9e
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Break.java
@@ -0,0 +1,126 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.runtime.parser.node.Node;
+import org.apache.velocity.util.StringUtils;
+
+import java.io.Writer;
+import java.util.ArrayList;
+
+/**
+ * Break directive used for interrupting scopes.
+ *
+ * @author <a href="mailto:wyla@removethis.sci.fi">Jarkko Viinamaki</a>
+ * @author Nathan Bubna
+ * @version $Id$
+ */
+public class Break extends Directive
+{
+ private boolean scoped = false;
+
+ /**
+ * Return name of this directive.
+ * @return The name of this directive.
+ */
+ @Override
+ public String getName()
+ {
+ return "break";
+ }
+
+ /**
+ * Return type of this directive.
+ * @return The type of this directive.
+ */
+ @Override
+ public int getType()
+ {
+ return LINE;
+ }
+
+ /**
+ * Since there is no processing of content,
+ * there is never a need for an internal scope.
+ */
+ @Override
+ public boolean isScopeProvided()
+ {
+ return false;
+ }
+
+ @Override
+ public void init(RuntimeServices rs, InternalContextAdapter context, Node node)
+ {
+ super.init(rs, context, node);
+
+ this.scoped = (node.jjtGetNumChildren() == 1);
+ }
+
+ /**
+ * This directive throws a StopCommand which signals either
+ * the nearest Scope or the specified scope to stop rendering
+ * its content.
+ * @return never, always throws a StopCommand or Exception
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer, Node node)
+ {
+ if (!scoped)
+ {
+ throw new StopCommand();
+ }
+
+ Object argument = node.jjtGetChild(0).value(context);
+ if (argument instanceof Scope)
+ {
+ ((Scope)argument).stop();
+ throw new IllegalStateException("Scope.stop() failed to throw a StopCommand");
+ }
+ else
+ {
+ throw new VelocityException(node.jjtGetChild(0).literal()+
+ " is not a valid " + Scope.class.getName() + " instance at "
+ + StringUtils.formatFileString(this),
+ null,
+ rsvc.getLogContext().getStackTrace());
+ }
+ }
+
+ /**
+ * Called by the parser to validate the argument types
+ */
+ @Override
+ public void checkArgs(ArrayList<Integer> argtypes, Token t, String templateName)
+ throws ParseException
+ {
+ if (argtypes.size() > 1)
+ {
+ throw new MacroParseException("The #break directive takes only a single, optional Scope argument",
+ templateName, t);
+ }
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Define.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Define.java
new file mode 100755
index 00000000..f1e90f6a
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Define.java
@@ -0,0 +1,121 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.runtime.parser.node.Node;
+import org.apache.velocity.runtime.parser.node.ParserTreeConstants;
+import org.apache.velocity.util.StringUtils;
+
+import java.io.Writer;
+import java.util.ArrayList;
+
+/**
+ * Directive that puts an unrendered AST block in the context
+ * under the specified key, postponing rendering until the
+ * reference is used and rendered.
+ *
+ * @author Andrew Tetlaw
+ * @author Nathan Bubna
+ * @version $Id: Define.java 686842 2008-08-18 18:29:31Z nbubna $
+ */
+public class Define extends Block
+{
+ /**
+ * Return name of this directive.
+ */
+ @Override
+ public String getName()
+ {
+ return "define";
+ }
+
+ /**
+ * simple init - get the key
+ */
+ @Override
+ public void init(RuntimeServices rs, InternalContextAdapter context, Node node)
+ throws TemplateInitException
+ {
+ super.init(rs, context, node);
+
+ // the first child is the block name (key), the second child is the block AST body
+ if ( node.jjtGetNumChildren() != 2 )
+ {
+ throw new VelocityException("parameter missing: block name at "
+ + StringUtils.formatFileString(this),
+ null,
+ rsvc.getLogContext().getStackTrace());
+ }
+
+ /*
+ * first token is the name of the block. We don't even check the format,
+ * just assume it looks like this: $block_name. Should we check if it has
+ * a '$' or not?
+ */
+ key = node.jjtGetChild(0).getFirstTokenImage().substring(1);
+
+ /*
+ * default max depth of two is used because intentional recursion is
+ * unlikely and discouraged, so make unintentional ones end fast
+ */
+ maxDepth = rsvc.getInt(RuntimeConstants.DEFINE_DIRECTIVE_MAXDEPTH, 2);
+ }
+
+ /**
+ * directive.render() simply makes an instance of the Block inner class
+ * and places it into the context as indicated.
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer, Node node)
+ {
+ /* put a Block.Reference instance into the context,
+ * using the user-defined key, for later inline rendering.
+ */
+ context.put(key, new Reference(context, this));
+ return true;
+ }
+
+ /**
+ * Called by the parser to validate the argument types
+ */
+ @Override
+ public void checkArgs(ArrayList<Integer> argtypes, Token t, String templateName)
+ throws ParseException
+ {
+ if (argtypes.size() != 1)
+ {
+ throw new MacroParseException("The #define directive requires one argument",
+ templateName, t);
+ }
+
+ if (argtypes.get(0) == ParserTreeConstants.JJTWORD)
+ {
+ throw new MacroParseException("The argument to #define is of the wrong type",
+ templateName, t);
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Directive.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Directive.java
new file mode 100644
index 00000000..9d938f78
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Directive.java
@@ -0,0 +1,264 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.runtime.parser.node.Node;
+
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+
+/**
+ * Base class for all directives used in Velocity.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author Nathan Bubna
+ * @version $Id$
+ */
+public abstract class Directive implements DirectiveConstants, Cloneable
+{
+ private int line = 0;
+ private int column = 0;
+ private boolean provideScope = false;
+ private Template template;
+
+ protected Logger log = null;
+
+ /**
+ *
+ */
+ protected RuntimeServices rsvc = null;
+
+ /**
+ * Return the name of this directive.
+ * @return The name of this directive.
+ */
+ public abstract String getName();
+
+ /**
+ * Get the directive type BLOCK/LINE.
+ * @return The directive type BLOCK/LINE.
+ */
+ public abstract int getType();
+
+ /**
+ * Allows the template location to be set.
+ * @param line
+ * @param column
+ */
+ public void setLocation( int line, int column )
+ {
+ this.line = line;
+ this.column = column;
+ }
+
+ /**
+ * Allows the template location to be set.
+ * @param line
+ * @param column
+ * @param template
+ */
+ public void setLocation(int line, int column, Template template)
+ {
+ setLocation(line, column);
+ this.template = template;
+ }
+
+ /**
+ * returns the template in which this directive appears
+ * @return template
+ */
+ public Template getTemplate()
+ {
+ return template;
+ }
+
+ /**
+ * for log msg purposes
+ * @return The current line for log msg purposes.
+ */
+ public int getLine()
+ {
+ return line;
+ }
+
+ /**
+ * for log msg purposes
+ * @return The current column for log msg purposes.
+ */
+ public int getColumn()
+ {
+ return column;
+ }
+
+ /**
+ * @return The template file name this directive was defined in, or null if not
+ * defined in a file.
+ */
+ public String getTemplateName()
+ {
+ return template.getName();
+ }
+
+ /**
+ * @return the name to be used when a scope control is provided for this
+ * directive.
+ */
+ public String getScopeName()
+ {
+ return getName();
+ }
+
+ /**
+ * @return true if there will be a scope control injected into the context
+ * when rendering this directive.
+ */
+ public boolean isScopeProvided()
+ {
+ return provideScope;
+ }
+
+ /**
+ * How this directive is to be initialized.
+ * @param rs
+ * @param context
+ * @param node
+ * @throws TemplateInitException
+ */
+ public void init( RuntimeServices rs, InternalContextAdapter context,
+ Node node)
+ throws TemplateInitException
+ {
+ rsvc = rs;
+ log = rsvc.getLog("directive." + getName());
+
+ provideScope = rsvc.isScopeControlEnabled(getScopeName());
+ }
+
+ /**
+ * The Parser calls this method during template parsing to check the arguments
+ * types. Be aware that this method is called pre init, so not all data
+ * is available in this method. The default implementation does not peform any
+ * checking. We do this so that Custom directives do not trigger any parse
+ * errors in IDEs.
+ * @param argtypes type, Array of argument types of each argument to the directive
+ * for example ParserTreeConstants.JJTWORD
+ * @param t token of directive
+ * @param templateName the name of the template this directive is referenced in.
+ * @throws ParseException
+ */
+ public void checkArgs(ArrayList<Integer> argtypes, Token t, String templateName)
+ throws ParseException
+ {
+ }
+
+ /**
+ * How this directive is to be rendered
+ * @param context
+ * @param writer
+ * @param node
+ * @return True if the directive rendered successfully.
+ * @throws IOException
+ * @throws ResourceNotFoundException
+ * @throws ParseErrorException
+ * @throws MethodInvocationException
+ */
+ public abstract boolean render( InternalContextAdapter context,
+ Writer writer, Node node )
+ throws IOException, ResourceNotFoundException, ParseErrorException,
+ MethodInvocationException;
+
+
+ /**
+ * This creates and places the scope control for this directive
+ * into the context (if scope provision is turned on).
+ * @param context
+ */
+ protected void preRender(InternalContextAdapter context)
+ {
+ if (isScopeProvided())
+ {
+ String name = getScopeName();
+ Object previous = context.get(name);
+ context.put(name, makeScope(previous));
+ }
+ }
+
+ /**
+ * @param prev
+ * @return scope
+ */
+ protected Scope makeScope(Object prev)
+ {
+ return new Scope(this, prev);
+ }
+
+ /**
+ * This cleans up any scope control for this directive after rendering,
+ * assuming the scope control was turned on.
+ * @param context
+ */
+ protected void postRender(InternalContextAdapter context)
+ {
+ if (isScopeProvided())
+ {
+ String name = getScopeName();
+ Object obj = context.get(name);
+
+ try
+ {
+ Scope scope = (Scope)obj;
+ if (scope.getParent() != null)
+ {
+ context.put(name, scope.getParent());
+ }
+ else if (scope.getReplaced() != null)
+ {
+ context.put(name, scope.getReplaced());
+ }
+ else
+ {
+ context.remove(name);
+ }
+ }
+ catch (ClassCastException cce)
+ {
+ // the user can override the scope with a #set,
+ // since that means they don't care about a replaced value
+ // and obviously aren't too keen on their scope control,
+ // and especially since #set is meant to be handled globally,
+ // we'll assume they know what they're doing and not worry
+ // about replacing anything superseded by this directive's scope
+ }
+ }
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/DirectiveConstants.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/DirectiveConstants.java
new file mode 100644
index 00000000..0836e752
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/DirectiveConstants.java
@@ -0,0 +1,35 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+/**
+ * Base class for all directives used in Velocity.
+ *
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public interface DirectiveConstants
+{
+ /** Block directive indicator */
+ int BLOCK = 1;
+
+ /** Line directive indicator */
+ int LINE = 2;
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Evaluate.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Evaluate.java
new file mode 100644
index 00000000..89ccdb6b
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Evaluate.java
@@ -0,0 +1,240 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.node.Node;
+import org.apache.velocity.runtime.parser.node.ParserTreeConstants;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+import org.apache.velocity.util.introspection.Info;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.Writer;
+
+/**
+ * Evaluates the directive argument as a VTL string, using the existing
+ * context.
+ *
+ * @author <a href="mailto:wglass@apache.org">Will Glass-Husain</a>
+ * @version $Id$
+ * @since 1.6
+ */
+public class Evaluate extends Directive
+{
+
+ /**
+ * Return name of this directive.
+ * @return The name of this directive.
+ */
+ @Override
+ public String getName()
+ {
+ return "evaluate";
+ }
+
+ /**
+ * Return type of this directive.
+ * @return The type of this directive.
+ */
+ @Override
+ public int getType()
+ {
+ return LINE;
+ }
+
+ /**
+ * Initialize and check arguments.
+ * @param rs
+ * @param context
+ * @param node
+ * @throws TemplateInitException
+ */
+ @Override
+ public void init(RuntimeServices rs, InternalContextAdapter context,
+ Node node)
+ throws TemplateInitException
+ {
+ super.init( rs, context, node );
+
+ /*
+ * Check that there is exactly one argument and it is a string or reference.
+ */
+
+ int argCount = node.jjtGetNumChildren();
+ if (argCount == 0)
+ {
+ throw new TemplateInitException(
+ "#" + getName() + "() requires exactly one argument",
+ null,
+ rsvc.getLogContext().getStackTrace(),
+ context.getCurrentTemplateName(),
+ node.getColumn(),
+ node.getLine());
+ }
+ if (argCount > 1)
+ {
+ /*
+ * use line/col of second argument
+ */
+
+ throw new TemplateInitException(
+ "#" + getName() + "() requires exactly one argument",
+ null,
+ rsvc.getLogContext().getStackTrace(),
+ context.getCurrentTemplateName(),
+ node.jjtGetChild(1).getColumn(),
+ node.jjtGetChild(1).getLine());
+ }
+
+ Node childNode = node.jjtGetChild(0);
+ if ( childNode.getType() != ParserTreeConstants.JJTSTRINGLITERAL &&
+ childNode.getType() != ParserTreeConstants.JJTREFERENCE )
+ {
+ throw new TemplateInitException(
+ "#" + getName() + "() argument must be a string literal or reference",
+ null,
+ rsvc.getLogContext().getStackTrace(),
+ context.getCurrentTemplateName(),
+ childNode.getColumn(),
+ childNode.getLine());
+ }
+ }
+
+ /**
+ * Evaluate the argument, convert to a String, and evaluate again
+ * (with the same context).
+ * @param context
+ * @param writer
+ * @param node
+ * @return True if the directive rendered successfully.
+ * @throws IOException
+ * @throws ResourceNotFoundException
+ * @throws ParseErrorException
+ * @throws MethodInvocationException
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer,
+ Node node) throws IOException, ResourceNotFoundException,
+ ParseErrorException, MethodInvocationException
+ {
+
+ /*
+ * Evaluate the string with the current context. We know there is
+ * exactly one argument and it is a string or reference.
+ */
+
+ Object value = node.jjtGetChild(0).value( context );
+ String sourceText;
+ if ( value != null )
+ {
+ sourceText = value.toString();
+ }
+ else
+ {
+ sourceText = "";
+ }
+
+ /*
+ * The new string needs to be parsed since the text has been dynamically generated.
+ */
+ String templateName = context.getCurrentTemplateName();
+ Template template = (Template)context.getCurrentResource();
+ if (template == null)
+ {
+ template = new Template();
+ template.setName(templateName);
+ }
+ SimpleNode nodeTree = null;
+
+ try
+ {
+ nodeTree = rsvc.parse(new StringReader(sourceText), template);
+ }
+ catch (ParseException | TemplateInitException pex)
+ {
+ // use the line/column from the template
+ Info info = new Info( templateName, node.getLine(), node.getColumn() );
+ throw new ParseErrorException( pex.getMessage(), info, rsvc.getLogContext().getStackTrace() );
+ }
+
+ /*
+ * now we want to init and render. Chain the context
+ * to prevent any changes to the current context.
+ */
+
+ if (nodeTree != null)
+ {
+ context.pushCurrentTemplateName(templateName);
+
+ try
+ {
+ try
+ {
+ nodeTree.init(context, rsvc);
+ }
+ catch (TemplateInitException pex)
+ {
+ Info info = new Info( templateName, node.getLine(), node.getColumn() );
+ throw new ParseErrorException( pex.getMessage(), info, rsvc.getLogContext().getStackTrace() );
+ }
+
+ try
+ {
+ preRender(context);
+
+ /*
+ * now render, and let any exceptions fly
+ */
+ nodeTree.render(context, writer);
+ }
+ catch (StopCommand stop)
+ {
+ if (!stop.isFor(this))
+ {
+ throw stop;
+ }
+ }
+ catch (ParseErrorException pex)
+ {
+ // convert any parsing errors to the correct line/col
+ Info info = new Info( templateName, node.getLine(), node.getColumn() );
+ throw new ParseErrorException( pex.getMessage(), info, rsvc.getLogContext().getStackTrace() );
+ }
+ }
+ finally
+ {
+ context.popCurrentTemplateName();
+ postRender(context);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Foreach.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Foreach.java
new file mode 100644
index 00000000..991e097a
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Foreach.java
@@ -0,0 +1,360 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.runtime.parser.node.*;
+import org.apache.velocity.util.StringUtils;
+import org.apache.velocity.util.introspection.Info;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * Foreach directive used for moving through arrays,
+ * or objects that provide an Iterator.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author Daniel Rall
+ * @version $Id$
+ */
+public class Foreach extends Directive
+{
+ /**
+ * Return name of this directive.
+ * @return The name of this directive.
+ */
+ @Override
+ public String getName()
+ {
+ return "foreach";
+ }
+
+ /**
+ * Return type of this directive.
+ * @return The type of this directive.
+ */
+ @Override
+ public int getType()
+ {
+ return BLOCK;
+ }
+
+ /**
+ * The maximum number of times we're allowed to loop.
+ */
+ private int maxNbrLoops;
+
+ /**
+ * Whether or not to throw an Exception if the iterator is null.
+ */
+ private boolean skipInvalidIterator;
+
+ /**
+ * The reference name used to access each
+ * of the elements in the list object. It
+ * is the $item in the following:
+ *
+ * #foreach ($item in $list)
+ *
+ * This can be used class wide because
+ * it is immutable.
+ */
+ private String elementKey;
+
+ /**
+ * immutable, so create in init
+ */
+ protected Info uberInfo;
+
+ /**
+ * simple init - init the tree and get the elementKey from
+ * the AST
+ * @param rs
+ * @param context
+ * @param node
+ * @throws TemplateInitException
+ */
+ @Override
+ public void init(RuntimeServices rs, InternalContextAdapter context, Node node)
+ throws TemplateInitException
+ {
+ super.init(rs, context, node);
+
+ maxNbrLoops = rsvc.getInt(RuntimeConstants.MAX_NUMBER_LOOPS,
+ Integer.MAX_VALUE);
+ if (maxNbrLoops < 1)
+ {
+ maxNbrLoops = Integer.MAX_VALUE;
+ }
+ skipInvalidIterator =
+ rsvc.getBoolean(RuntimeConstants.SKIP_INVALID_ITERATOR, true);
+
+ if (rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false))
+ {
+ // If we are in strict mode then the default for skipInvalidItarator
+ // is true. However, if the property is explicitly set, then honor the setting.
+ skipInvalidIterator = rsvc.getBoolean(RuntimeConstants.SKIP_INVALID_ITERATOR, false);
+ }
+
+ /*
+ * this is really the only thing we can do here as everything
+ * else is context sensitive
+ */
+ SimpleNode sn = (SimpleNode) node.jjtGetChild(0);
+
+ if (sn instanceof ASTReference)
+ {
+ elementKey = ((ASTReference) sn).getRootString();
+ }
+ else
+ {
+ /*
+ * the default, error-prone way which we'll remove
+ */
+ elementKey = sn.getFirstTokenImage().substring(1);
+ }
+
+ /*
+ * make an uberinfo - saves new's later on
+ */
+
+ uberInfo = new Info(this.getTemplateName(),
+ getLine(),getColumn());
+ }
+
+ /**
+ * Extension hook to allow subclasses to control whether loop vars
+ * are set locally or not. So, those in favor of VELOCITY-285, can
+ * make that happen easily by overriding this and having it use
+ * context.localPut(k,v). See VELOCITY-630 for more on this.
+ * @param context
+ * @param key
+ * @param value
+ */
+ protected void put(InternalContextAdapter context, String key, Object value)
+ {
+ context.put(key, value);
+ }
+
+ /**
+ * Retrieve the contextual iterator.
+ * @param iterable
+ * @param node
+ * @return iterator
+ */
+ protected Iterator getIterator(Object iterable, Node node)
+ {
+ Iterator i = null;
+ /*
+ * do our introspection to see what our collection is
+ */
+ if (iterable != null)
+ {
+ try
+ {
+ i = rsvc.getUberspect().getIterator(iterable, uberInfo);
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch (RuntimeException e)
+ {
+ throw e;
+ }
+ catch (Exception ee)
+ {
+ String msg = "Error getting iterator for #foreach parameter "
+ + node.literal() + " at " + StringUtils.formatFileString(node);
+ log.error(msg, ee);
+ throw new VelocityException(msg, ee, rsvc.getLogContext().getStackTrace());
+ }
+
+ if (i == null && !skipInvalidIterator)
+ {
+ String msg = "#foreach parameter " + node.literal() + " at "
+ + StringUtils.formatFileString(node) + " is of type " + iterable.getClass().getName()
+ + " and cannot be iterated by " + rsvc.getUberspect().getClass().getName();
+ log.error(msg);
+ throw new VelocityException(msg, null, rsvc.getLogContext().getStackTrace());
+ }
+ }
+ return i;
+ }
+
+ /**
+ * renders the #foreach() block
+ * @param context
+ * @param writer
+ * @param node
+ * @return True if the directive rendered successfully.
+ * @throws IOException
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer, Node node)
+ throws IOException
+ {
+ // Get the block ast tree which is always the last child ...
+ Node block = node.jjtGetChild(node.jjtGetNumChildren()-1);
+
+ // ... except if there is an #else clause
+ Node elseBlock = null;
+ Node previous = node.jjtGetChild(node.jjtGetNumChildren()-2);
+ if (previous instanceof ASTBlock)
+ {
+ elseBlock = block;
+ block = previous;
+ }
+
+ Node iterableNode = node.jjtGetChild(2);
+ Object iterable = iterableNode.value(context);
+ Iterator i = getIterator(iterable, iterableNode);
+ if (i == null || !i.hasNext())
+ {
+ if (elseBlock != null)
+ {
+ renderBlock(context, writer, elseBlock);
+ }
+ return false;
+ }
+
+ /*
+ * save the element key if there is one
+ */
+ Object o = context.get(elementKey);
+
+ /*
+ * roll our own scope class instead of using preRender(ctx)'s
+ */
+ ForeachScope foreach = null;
+ if (isScopeProvided())
+ {
+ String name = getScopeName();
+ foreach = new ForeachScope(this, context.get(name));
+ context.put(name, foreach);
+ }
+
+ int count = 1;
+ while (count <= maxNbrLoops && i.hasNext())
+ {
+ count++;
+
+ put(context, elementKey, i.next());
+ if (isScopeProvided())
+ {
+ // update the scope control
+ foreach.index++;
+ foreach.hasNext = i.hasNext();
+ }
+
+ try
+ {
+ renderBlock(context, writer, block);
+ }
+ catch (StopCommand stop)
+ {
+ if (stop.isFor(this))
+ {
+ break;
+ }
+ else
+ {
+ // clean up first
+ clean(context, o);
+ throw stop;
+ }
+ }
+ }
+ clean(context, o);
+ /*
+ * closes the iterator if it implements the Closeable interface
+ */
+ if (i instanceof Closeable && i != iterable) /* except if the iterable is the iterator itself */
+ {
+ ((Closeable)i).close();
+ }
+ return true;
+ }
+
+ protected void renderBlock(InternalContextAdapter context, Writer writer, Node block)
+ throws IOException
+ {
+ block.render(context, writer);
+ }
+
+ protected void clean(InternalContextAdapter context, Object o)
+ {
+ /*
+ * restores element key if exists
+ * otherwise just removes
+ */
+ if (o != null)
+ {
+ context.put(elementKey, o);
+ }
+ else
+ {
+ context.remove(elementKey);
+ }
+
+ // clean up after the ForeachScope
+ postRender(context);
+ }
+
+ /**
+ * We do not allow a word token in any other arg position except for the 2nd since
+ * we are looking for the pattern #foreach($foo in $bar).
+ */
+ @Override
+ public void checkArgs(ArrayList<Integer> argtypes, Token t, String templateName)
+ throws ParseException
+ {
+ if (argtypes.size() < 3)
+ {
+ throw new MacroParseException("Too few arguments to the #foreach directive",
+ templateName, t);
+ }
+ else if (argtypes.get(0) != ParserTreeConstants.JJTREFERENCE)
+ {
+ throw new MacroParseException("Expected argument 1 of #foreach to be a reference",
+ templateName, t);
+ }
+ else if (argtypes.get(1) != ParserTreeConstants.JJTWORD)
+ {
+ throw new MacroParseException("Expected word 'in' at argument position 2 in #foreach",
+ templateName, t);
+ }
+ else if (argtypes.get(2) == ParserTreeConstants.JJTWORD)
+ {
+ throw new MacroParseException("Argument 3 of #foreach is of the wrong type",
+ templateName, t);
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/ForeachScope.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/ForeachScope.java
new file mode 100755
index 00000000..d1267552
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/ForeachScope.java
@@ -0,0 +1,79 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * This represents scoping and metadata for #foreach,
+ * adding index, count, hasNext, isFirst and isLast info.
+ *
+ * @author Nathan Bubna
+ * @version $Id$
+ */
+public class ForeachScope extends Scope
+{
+ protected int index = -1;
+ protected boolean hasNext = false;
+
+ public ForeachScope(Object owner, Object replaces)
+ {
+ super(owner, replaces);
+ }
+
+ public int getIndex()
+ {
+ return index;
+ }
+
+ public int getCount()
+ {
+ return index + 1;
+ }
+
+ public boolean hasNext()
+ {
+ return getHasNext();
+ }
+
+ public boolean getHasNext()
+ {
+ return hasNext;
+ }
+
+ public boolean isFirst()
+ {
+ return index < 1;
+ }
+
+ public boolean getFirst()
+ {
+ return isFirst();
+ }
+
+ public boolean isLast()
+ {
+ return !hasNext;
+ }
+
+ public boolean getLast()
+ {
+ return isLast();
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Include.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Include.java
new file mode 100644
index 00000000..1e1abd7b
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Include.java
@@ -0,0 +1,319 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.app.event.EventHandlerUtil;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.parser.node.Node;
+import org.apache.velocity.runtime.parser.node.ParserTreeConstants;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.util.StringUtils;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * <p>Pluggable directive that handles the #include() statement in VTL.
+ * This #include() can take multiple arguments of either
+ * StringLiteral or Reference.</p>
+ *
+ * <p>Notes:</p>
+ * <ol>
+ * <li>For security reasons, the included source material can only come
+ * from somewhere within the template root tree. If you want to include
+ * content from elsewhere on your disk, add extra template roots, or use
+ * a link from somwehere under template root to that content.</li>
+ *
+ * <li>By default, there is no output to the render stream in the event of
+ * a problem. You can override this behavior with two property values :
+ * include.output.errormsg.start
+ * include.output.errormsg.end
+ * If both are defined in velocity.properties, they will be used to
+ * in the render output to bracket the arg string that caused the
+ * problem.
+ * Ex. : if you are working in html then
+ * include.output.errormsg.start=&lt;!-- #include error :
+ * include.output.errormsg.end= --&gt;
+ * might be an excellent way to start...</li>
+ *
+ * <li>As noted above, #include() can take multiple arguments.
+ * Ex : #include('foo.vm' 'bar.vm' $foo)
+ * will include all three if valid to output without any
+ * special separator.</li>
+ * </ol>
+ *
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:kav@kav.dk">Kasper Nielsen</a>
+ * @version $Id$
+ */
+public class Include extends InputBase
+{
+ private String outputMsgStart = "";
+ private String outputMsgEnd = "";
+
+ /**
+ * Return name of this directive.
+ * @return The name of this directive.
+ */
+ @Override
+ public String getName()
+ {
+ return "include";
+ }
+
+ /**
+ * Return type of this directive.
+ * @return The type of this directive.
+ */
+ @Override
+ public int getType()
+ {
+ return LINE;
+ }
+
+ /**
+ * Since there is no processing of content,
+ * there is never a need for an internal scope.
+ */
+ @Override
+ public boolean isScopeProvided()
+ {
+ return false;
+ }
+
+ /**
+ * simple init - init the tree and get the elementKey from
+ * the AST
+ * @param rs
+ * @param context
+ * @param node
+ * @throws TemplateInitException
+ */
+ @Override
+ public void init(RuntimeServices rs, InternalContextAdapter context,
+ Node node)
+ throws TemplateInitException
+ {
+ super.init( rs, context, node );
+
+ /*
+ * get the msg, and add the space so we don't have to
+ * do it each time
+ */
+ outputMsgStart = rsvc.getString(RuntimeConstants.ERRORMSG_START);
+ outputMsgStart = outputMsgStart + " ";
+
+ outputMsgEnd = rsvc.getString(RuntimeConstants.ERRORMSG_END );
+ outputMsgEnd = " " + outputMsgEnd;
+ }
+
+ /**
+ * iterates through the argument list and renders every
+ * argument that is appropriate. Any non appropriate
+ * arguments are logged, but render() continues.
+ * @param context
+ * @param writer
+ * @param node
+ * @return True if the directive rendered successfully.
+ * @throws IOException
+ * @throws MethodInvocationException
+ * @throws ResourceNotFoundException
+ */
+ @Override
+ public boolean render(InternalContextAdapter context,
+ Writer writer, Node node)
+ throws IOException, MethodInvocationException,
+ ResourceNotFoundException
+ {
+ /*
+ * get our arguments and check them
+ */
+
+ int argCount = node.jjtGetNumChildren();
+
+ for( int i = 0; i < argCount; i++)
+ {
+ /*
+ * we only handle StringLiterals and References right now
+ */
+
+ Node n = node.jjtGetChild(i);
+
+ if ( n.getType() == ParserTreeConstants.JJTSTRINGLITERAL ||
+ n.getType() == ParserTreeConstants.JJTREFERENCE )
+ {
+ if (!renderOutput( n, context, writer ))
+ outputErrorToStream( writer, "error with arg " + i
+ + " please see log.");
+ }
+ else
+ {
+ String msg = "invalid #include() argument '"
+ + n.toString() + "' at " + StringUtils.formatFileString(this);
+ log.error(msg);
+ outputErrorToStream( writer, "error with arg " + i
+ + " please see log.");
+ throw new VelocityException(msg, null, rsvc.getLogContext().getStackTrace());
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * does the actual rendering of the included file
+ *
+ * @param node AST argument of type StringLiteral or Reference
+ * @param context valid context so we can render References
+ * @param writer output Writer
+ * @return boolean success or failure. failures are logged
+ * @exception IOException
+ * @exception MethodInvocationException
+ * @exception ResourceNotFoundException
+ */
+ private boolean renderOutput( Node node, InternalContextAdapter context,
+ Writer writer )
+ throws IOException, MethodInvocationException,
+ ResourceNotFoundException
+ {
+ if ( node == null )
+ {
+ log.error("#include() null argument");
+ return false;
+ }
+
+ /*
+ * does it have a value? If you have a null reference, then no.
+ */
+ Object value = node.value( context );
+ if ( value == null)
+ {
+ log.error("#include() null argument");
+ return false;
+ }
+
+ /*
+ * get the path
+ */
+ String sourcearg = value.toString();
+
+ /*
+ * check to see if the argument will be changed by the event handler
+ */
+
+ String arg = EventHandlerUtil.includeEvent( rsvc, context, sourcearg, context.getCurrentTemplateName(), getName() );
+
+ /*
+ * a null return value from the event cartridge indicates we should not
+ * input a resource.
+ */
+ boolean blockinput = false;
+ if (arg == null)
+ blockinput = true;
+
+ Resource resource = null;
+
+ try
+ {
+ if (!blockinput)
+ resource = getResource(arg, getInputEncoding(context));
+ }
+ catch ( ResourceNotFoundException rnfe )
+ {
+ /*
+ * the arg wasn't found. Note it and throw
+ */
+ log.error("#" + getName() + "(): cannot find resource '{}', called at {}",
+ arg, StringUtils.formatFileString(this));
+ throw rnfe;
+ }
+
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ log.error("#" + getName() + "(): arg = '{}', called at {}",
+ arg, StringUtils.formatFileString(this));
+ throw e;
+ }
+ catch (Exception e)
+ {
+ String msg = "#" + getName() + "(): arg = '" + arg +
+ "', called at " + StringUtils.formatFileString(this);
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+
+
+ /*
+ * note - a blocked input is still a successful operation as this is
+ * expected behavior.
+ */
+
+ if ( blockinput )
+ return true;
+
+ else if ( resource == null )
+ return false;
+
+ writer.write((String)resource.getData());
+ return true;
+ }
+
+ /**
+ * Puts a message to the render output stream if ERRORMSG_START / END
+ * are valid property strings. Mainly used for end-user template
+ * debugging.
+ * @param writer
+ * @param msg
+ * @throws IOException
+ * @deprecated if/how errors are displayed is not the concern of the engine, which should throw in all cases
+ */
+ private void outputErrorToStream( Writer writer, String msg )
+ throws IOException
+ {
+ if ( outputMsgStart != null && outputMsgEnd != null)
+ {
+ writer.write(outputMsgStart);
+ writer.write(msg);
+ writer.write(outputMsgEnd);
+ }
+ }
+
+ /**
+ * Find the resource to include
+ * @param path resource path
+ * @param encoding resource encoding
+ * @return found resource
+ * @throws ResourceNotFoundException if resource was not found
+ */
+ protected Resource getResource(String path, String encoding) throws ResourceNotFoundException
+ {
+ return rsvc.getContent(path, encoding);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/InputBase.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/InputBase.java
new file mode 100644
index 00000000..c12626a4
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/InputBase.java
@@ -0,0 +1,59 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.resource.Resource;
+
+/**
+ * Base class for directives which do input operations
+ * (e.g. <code>#include()</code>, <code>#parse()</code>, etc.).
+ *
+ * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
+ * @since 1.4
+ */
+public abstract class InputBase extends Directive
+{
+ /**
+ * Decides the encoding used during input processing of this
+ * directive.
+ *
+ * Get the resource, and assume that we use the encoding of the
+ * current template the 'current resource' can be
+ * <code>null</code> if we are processing a stream....
+ *
+ * @param context The context to derive the default input encoding
+ * from.
+ * @return The encoding to use when processing this directive.
+ */
+ protected String getInputEncoding(InternalContextAdapter context)
+ {
+ Resource current = context.getCurrentResource();
+ if (current != null)
+ {
+ return current.getEncoding();
+ }
+ else
+ {
+ return (String) rsvc.getProperty(RuntimeConstants.INPUT_ENCODING);
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Macro.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Macro.java
new file mode 100644
index 00000000..973c3358
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Macro.java
@@ -0,0 +1,294 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.runtime.parser.node.Node;
+import org.apache.velocity.runtime.parser.node.ParserTreeConstants;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Macro implements the macro definition directive of VTL.
+ *
+ * example:
+ *
+ * #macro( isnull $i )
+ * #if( $i )
+ * $i
+ * #end
+ * #end
+ *
+ * This object is used at parse time to mainly process and register the
+ * macro. It is used inline in the parser when processing a directive.
+ *
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="hps@intermeta.de">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ */
+public class Macro extends Directive
+{
+ private static boolean debugMode = false;
+
+ /**
+ * Return name of this directive.
+ * @return The name of this directive.
+ */
+ @Override
+ public String getName()
+ {
+ return "macro";
+ }
+
+ /**
+ * Return type of this directive.
+ * @return The type of this directive.
+ */
+ @Override
+ public int getType()
+ {
+ return BLOCK;
+ }
+
+ /**
+ * Since this class does no processing of content,
+ * there is never a need for an internal scope.
+ */
+ @Override
+ public boolean isScopeProvided()
+ {
+ return false;
+ }
+
+ /**
+ * render() doesn't do anything in the final output rendering.
+ * There is no output from a #macro() directive.
+ * @param context
+ * @param writer
+ * @param node
+ * @return True if the directive rendered successfully.
+ * @throws IOException
+ */
+ @Override
+ public boolean render(InternalContextAdapter context,
+ Writer writer, Node node)
+ throws IOException
+ {
+ /*
+ * do nothing: We never render. The VelocimacroProxy object does that
+ */
+
+ return true;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.directive.Directive#init(org.apache.velocity.runtime.RuntimeServices, org.apache.velocity.context.InternalContextAdapter, org.apache.velocity.runtime.parser.node.Node)
+ */
+ @Override
+ public void init(RuntimeServices rs, InternalContextAdapter context,
+ Node node)
+ throws TemplateInitException
+ {
+ super.init(rs, context, node);
+
+
+ // Add this macro to the VelocimacroManager now that it has been initialized.
+ List<MacroArg> macroArgs = getArgArray(node, rsvc);
+ int numArgs = node.jjtGetNumChildren();
+ rsvc.addVelocimacro(macroArgs.get(0).name, node.jjtGetChild(numArgs - 1),
+ macroArgs, node.getTemplate());
+ }
+
+ /**
+ * Check the argument types of a macro call, called by the parser to do validation
+ */
+ @Override
+ public void checkArgs(ArrayList<Integer> argtypes, Token t, String templateName)
+ throws ParseException
+ {
+ if (argtypes.size() < 1)
+ {
+ throw new MacroParseException("A macro definition requires at least a macro name"
+ , templateName, t);
+ }
+
+ /*
+ * lets make sure that the first arg is an ASTWord
+ */
+ if(argtypes.get(0) != ParserTreeConstants.JJTWORD)
+ {
+ throw new MacroParseException("Macro argument 1"
+ + " must be a token without surrounding \' or \""
+ , templateName, t);
+ }
+
+
+ // We use this to flag if the default arguments are out of order. such as
+ // #macro($a $b=1 $c). We enforce that all default parameters must be
+ // specified consecutively, and at the end of the argument list.
+ boolean consecutive = false;
+
+ // All arguments other then the first must be either a reference
+ // or a directiveassign followed by a reference in the case a default
+ // value is specified.
+ for (int argPos = 1; argPos < argtypes.size(); argPos++)
+ {
+ if (argtypes.get(argPos) == ParserTreeConstants.JJTDIRECTIVEASSIGN)
+ {
+ // Absorb next argument type since parser enforces that these are in
+ // pairs, and we don't need to check the type of the second
+ // arg because it is done by the parser.
+ argPos++;
+ consecutive = true;
+ }
+ else if (argtypes.get(argPos) != ParserTreeConstants.JJTREFERENCE)
+ {
+ throw new MacroParseException("Macro argument " + (argPos + 1)
+ + " must be a reference", templateName, t);
+ }
+ else if (consecutive)
+ {
+ // We have already found a default parameter e.g.; $x = 2, but
+ // the next parameter was not a reference.
+ throw new MacroParseException("Macro non-default argument follows a default argument at "
+ , templateName, t);
+ }
+ }
+ }
+
+ /**
+ * Creates an array containing the literal text from the macro
+ * argument(s) (including the macro's name as the first arg).
+ *
+ * @param node The parse node from which to grok the argument
+ * list. It's expected to include the block node tree (for the
+ * macro body).
+ * @param rsvc For debugging purposes only.
+ * @return array of arguments
+ */
+ private static List<MacroArg> getArgArray(Node node, RuntimeServices rsvc)
+ {
+ /*
+ * Get the number of arguments for the macro, excluding the
+ * last child node which is the block tree containing the
+ * macro body.
+ */
+ int numArgs = node.jjtGetNumChildren();
+ numArgs--; // avoid the block tree...
+
+ ArrayList<MacroArg> macroArgs = new ArrayList<>();
+
+ for (int i = 0; i < numArgs; i++)
+ {
+ Node curnode = node.jjtGetChild(i);
+ MacroArg macroArg = new MacroArg();
+ if (curnode.getType() == ParserTreeConstants.JJTDIRECTIVEASSIGN)
+ {
+ // This is an argument with a default value
+ macroArg.name = curnode.getFirstTokenImage();
+
+ // Inforced by the parser there will be an argument here.
+ i++;
+ curnode = node.jjtGetChild(i);
+ macroArg.defaultVal = curnode;
+ }
+ else
+ {
+ // An argument without a default value
+ macroArg.name = curnode.getFirstTokenImage();
+ }
+
+ // trim off the leading $ for the args after the macro name.
+ // saves everyone else from having to do it
+ if (i > 0 && macroArg.name.startsWith(String.valueOf(rsvc.getParserConfiguration().getDollarChar())))
+ {
+ macroArg.name = macroArg.name.substring(1);
+ }
+
+ macroArgs.add(macroArg);
+ }
+
+ if (debugMode)
+ {
+ StringBuilder msg = new StringBuilder("Macro.getArgArray(): nbrArgs=");
+ msg.append(numArgs).append(": ");
+ macroToString(msg, macroArgs, rsvc);
+ rsvc.getLog("macro").debug(msg.toString());
+ }
+
+ return macroArgs;
+ }
+
+ /**
+ * MacroArgs holds the information for a single argument in a
+ * macro definition. The arguments for a macro are passed around as a
+ * list of these objects.
+ */
+ public static class MacroArg
+ {
+ /**
+ * Name of the argument with '$' stripped off
+ */
+ public String name = null;
+
+ /**
+ * If the argument was given a default value, then this contains
+ * the base of the AST tree of the value. Otherwise it is null.
+ */
+ public Node defaultVal = null;
+ }
+
+ /**
+ * For debugging purposes. Formats the arguments from
+ * <code>argArray</code> and appends them to <code>buf</code>.
+ *
+ * @param buf A StringBuilder. If null, a new StringBuilder is allocated.
+ * @param macroArgs Array of macro arguments, containing the
+ * #macro() arguments and default values. the 0th is the name.
+ * @return A StringBuilder containing the formatted arguments. If a StringBuilder
+ * has passed in as buf, this method returns it.
+ * @since 1.5
+ */
+ public static StringBuilder macroToString(final StringBuilder buf, List<MacroArg> macroArgs, RuntimeServices rsvc)
+ {
+ StringBuilder ret = (buf == null) ? new StringBuilder() : buf;
+
+ ret.append(rsvc.getParserConfiguration().getHashChar()).append(macroArgs.get(0).name).append("( ");
+ for (MacroArg marg : macroArgs)
+ {
+ ret.append(rsvc.getParserConfiguration().getDollarChar()).append(marg.name);
+ if (marg.defaultVal != null)
+ {
+ ret.append("=").append(marg.defaultVal);
+ }
+ ret.append(' ');
+ }
+ ret.append(" )");
+ return ret;
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/MacroParseException.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/MacroParseException.java
new file mode 100644
index 00000000..0ce9a5f8
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/MacroParseException.java
@@ -0,0 +1,206 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ExtendedParseException;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.util.StringUtils;
+
+/**
+ * Exception to indicate problem happened while constructing #macro()
+ *
+ * For internal use in parser - not to be passed to app level
+ *
+ * @author <a href="mailto:geirm@apache.org">Geir Magnusson Jr.</a>
+ * @author <a href="hps@intermeta.de">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ */
+public class MacroParseException
+ extends ParseException
+ implements ExtendedParseException
+{
+ private final String templateName;
+
+ /**
+ * Version Id for serializable
+ */
+ private static final long serialVersionUID = -4985224672336070689L;
+
+ /**
+ * @param msg
+ * @param templateName
+ * @param currentToken
+ */
+ public MacroParseException(final String msg, final String templateName, final Token currentToken)
+ {
+ super(msg + " at ");
+ this.currentToken = currentToken;
+ this.templateName = templateName;
+ }
+
+ /**
+ * returns the Template name where this exception occured.
+ * @return The Template name where this exception occured.
+ * @since 1.5
+ */
+ @Override
+ public String getTemplateName()
+ {
+ return templateName;
+ }
+
+ /**
+ * returns the line number where this exception occured.
+ * @return The line number where this exception occured.
+ * @since 1.5
+ */
+ @Override
+ public int getLineNumber()
+ {
+ if ((currentToken != null) && (currentToken.next != null))
+ {
+ return currentToken.next.beginLine;
+ }
+ else if (currentToken != null)
+ {
+ return currentToken.beginLine;
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ /**
+ * returns the column number where this exception occured.
+ * @return The column number where this exception occured.
+ * @since 1.5
+ */
+ @Override
+ public int getColumnNumber()
+ {
+ if ((currentToken != null) && (currentToken.next != null))
+ {
+ return currentToken.next.beginColumn;
+ }
+ else if (currentToken != null)
+ {
+ return currentToken.beginColumn;
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ /**
+ * This method has the standard behavior when this object has been
+ * created using the standard constructors. Otherwise, it uses
+ * "currentToken" and "expectedTokenSequences" to generate a parse
+ * error message and returns it. If this object has been created
+ * due to a parse error, and you do not catch it (it gets thrown
+ * from the parser), then this method is called during the printing
+ * of the final stack trace, and hence the correct error message
+ * gets displayed.
+ * @return the current message.
+ * @since 1.5
+ */
+ @Override
+ public String getMessage()
+ {
+ if (!specialConstructor)
+ {
+ StringBuilder sb = new StringBuilder(super.getMessage());
+ appendTemplateInfo(sb);
+ return sb.toString();
+ }
+
+ int maxSize = 0;
+
+ StringBuilder expected = new StringBuilder();
+
+ for (int[] expectedTokenSequence : expectedTokenSequences)
+ {
+ if (maxSize < expectedTokenSequence.length)
+ {
+ maxSize = expectedTokenSequence.length;
+ }
+
+ for (int i : expectedTokenSequence)
+ {
+ expected.append(tokenImage[i]).append(" ");
+ }
+
+ if (expectedTokenSequence[expectedTokenSequence.length - 1] != 0)
+ {
+ expected.append("...");
+ }
+
+ expected.append(eol).append(" ");
+ }
+
+ StringBuilder retval = new StringBuilder("Encountered \"");
+ Token tok = currentToken.next;
+
+ for (int i = 0; i < maxSize; i++)
+ {
+ if (i != 0)
+ {
+ retval.append(" ");
+ }
+
+ if (tok.kind == 0)
+ {
+ retval.append(tokenImage[0]);
+ break;
+ }
+
+ retval.append(add_escapes(tok.image));
+ tok = tok.next;
+ }
+
+ retval.append("\"");
+ appendTemplateInfo(retval);
+
+ if (expectedTokenSequences.length == 1)
+ {
+ retval.append("Was expecting:").append(eol).append(" ");
+ }
+ else
+ {
+ retval.append("Was expecting one of:").append(eol).append(" ");
+ }
+
+ // avoid JDK 1.3 StringBuffer.append(Object o) vs 1.4 StringBuffer.append(StringBuffer sb) gotcha.
+ retval.append(expected.toString());
+ return retval.toString();
+ }
+
+ /**
+ * @param sb
+ * @since 1.5
+ */
+ protected void appendTemplateInfo(final StringBuilder sb)
+ {
+ sb.append(StringUtils.formatFileString(getTemplateName(), getLineNumber(), getColumnNumber()));
+ sb.append(eol);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Parse.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Parse.java
new file mode 100644
index 00000000..06dbf13b
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Parse.java
@@ -0,0 +1,358 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+import org.apache.velocity.app.event.EventHandlerUtil;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.runtime.parser.node.Node;
+import org.apache.velocity.runtime.parser.node.ParserTreeConstants;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+import org.apache.velocity.util.StringUtils;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Pluggable directive that handles the <code>#parse()</code>
+ * statement in VTL.
+ *
+ * <pre>
+ * Notes:
+ * -----
+ * 1) The parsed source material can only come from somewhere in
+ * the TemplateRoot tree for security reasons. There is no way
+ * around this. If you want to include content from elsewhere on
+ * your disk, use a link from somewhere under Template Root to that
+ * content.
+ *
+ * 2) There is a limited parse depth. It is set as a property
+ * "directive.parse.max_depth = 10" by default. This 10 deep
+ * limit is a safety feature to prevent infinite loops.
+ * </pre>
+ *
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:Christoph.Reck@dlr.de">Christoph Reck</a>
+ * @version $Id$
+ */
+public class Parse extends InputBase
+{
+ private int maxDepth;
+
+ /**
+ * Indicates if we are running in strict reference mode.
+ */
+ public boolean strictRef = false;
+
+ /**
+ * Return name of this directive.
+ * @return The name of this directive.
+ */
+ @Override
+ public String getName()
+ {
+ return "parse";
+ }
+
+ /**
+ * Overrides the default to use "template", so that all templates
+ * can use the same scope reference, whether rendered via #parse
+ * or direct merge.
+ */
+ @Override
+ public String getScopeName()
+ {
+ return "template";
+ }
+
+ /**
+ * Return type of this directive.
+ * @return The type of this directive.
+ */
+ @Override
+ public int getType()
+ {
+ return LINE;
+ }
+
+ /**
+ * Init's the #parse directive.
+ * @param rs
+ * @param context
+ * @param node
+ * @throws TemplateInitException
+ */
+ @Override
+ public void init(RuntimeServices rs, InternalContextAdapter context, Node node)
+ throws TemplateInitException
+ {
+ super.init(rs, context, node);
+
+ this.maxDepth = rsvc.getInt(RuntimeConstants.PARSE_DIRECTIVE_MAXDEPTH, 10);
+
+ strictRef = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false);
+ }
+
+ /**
+ * iterates through the argument list and renders every
+ * argument that is appropriate. Any non appropriate
+ * arguments are logged, but render() continues.
+ * @param context
+ * @param writer
+ * @param node
+ * @return True if the directive rendered successfully.
+ * @throws IOException
+ * @throws ResourceNotFoundException
+ * @throws ParseErrorException
+ * @throws MethodInvocationException
+ */
+ @Override
+ public boolean render(InternalContextAdapter context,
+ Writer writer, Node node)
+ throws IOException, ResourceNotFoundException, ParseErrorException,
+ MethodInvocationException
+ {
+ /*
+ * did we get an argument?
+ */
+ if ( node.jjtGetNumChildren() == 0 )
+ {
+ throw new VelocityException("#" + getName() + "(): argument missing at " +
+ StringUtils.formatFileString(this), null, rsvc.getLogContext().getStackTrace());
+ }
+
+ /*
+ * does it have a value? If you have a null reference, then no.
+ */
+ Object value = node.jjtGetChild(0).value( context );
+ if (value == null)
+ {
+ log.debug("#" + getName() + "(): null argument at {}", StringUtils.formatFileString(this));
+ }
+
+ /*
+ * get the path
+ */
+ String sourcearg = value == null ? null : value.toString();
+
+ /*
+ * check to see if the argument will be changed by the event cartridge
+ */
+ String arg = EventHandlerUtil.includeEvent( rsvc, context, sourcearg, context.getCurrentTemplateName(), getName());
+
+ /*
+ * if strict mode and arg was not fixed by event handler, then complain
+ */
+ if (strictRef && value == null && arg == null)
+ {
+ throw new VelocityException("The argument to #" + getName() + " returned null at "
+ + StringUtils.formatFileString(this), null, rsvc.getLogContext().getStackTrace());
+ }
+
+ /*
+ * a null return value from the event cartridge indicates we should not
+ * input a resource.
+ */
+ if (arg == null)
+ {
+ // abort early, but still consider it a successful rendering
+ return true;
+ }
+
+
+ if (maxDepth > 0)
+ {
+ /*
+ * see if we have exceeded the configured depth.
+ */
+ String[] templateStack = context.getTemplateNameStack();
+ if (templateStack.length >= maxDepth)
+ {
+ StringBuilder path = new StringBuilder();
+ for (String aTemplateStack : templateStack)
+ {
+ path.append(" > ").append(aTemplateStack);
+ }
+ log.error("Max recursion depth reached ({}). File stack: {}",
+ templateStack.length, path);
+
+ return false;
+ }
+ }
+
+ /*
+ * now use the Runtime resource loader to get the template
+ */
+
+ Template t = null;
+
+ try
+ {
+ t = getTemplate(arg, getInputEncoding(context));
+ }
+ catch ( ResourceNotFoundException rnfe )
+ {
+ /*
+ * the arg wasn't found. Note it and throw
+ */
+ log.error("#" + getName() + "(): cannot find template '{}', called at {}",
+ arg, StringUtils.formatFileString(this));
+ throw rnfe;
+ }
+ catch ( ParseErrorException pee )
+ {
+ /*
+ * the arg was found, but didn't parse - syntax error
+ * note it and throw
+ */
+ log.error("#" + getName() + "(): syntax error in #" + getName() + "()-ed template '{}', called at {}",
+ arg, StringUtils.formatFileString(this));
+ throw pee;
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ log.error("Exception rendering #" + getName() + "({}) at {}",
+ arg, StringUtils.formatFileString(this));
+ throw e;
+ }
+ catch ( Exception e )
+ {
+ String msg = "Exception rendering #" + getName() + "(" + arg + ") at " +
+ StringUtils.formatFileString(this);
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+
+ /*
+ * Add the template name to the macro libraries list
+ */
+ List<Template> macroLibraries = context.getMacroLibraries();
+
+ /*
+ * if macroLibraries are not set create a new one
+ */
+ if (macroLibraries == null)
+ {
+ macroLibraries = new ArrayList<>();
+ }
+
+ context.setMacroLibraries(macroLibraries);
+
+ /* instead of adding the name of the template, add the Template reference */
+ macroLibraries.add(t);
+
+ /*
+ * and render it
+ */
+ try
+ {
+ preRender(context);
+ context.pushCurrentTemplateName(arg);
+
+ ((SimpleNode) t.getData()).render(context, writer);
+ }
+ catch( StopCommand stop )
+ {
+ if (!stop.isFor(this))
+ {
+ throw stop;
+ }
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ /*
+ * Log #parse errors so the user can track which file called which.
+ */
+ log.error("Exception rendering #" + getName() + "({}) at {}",
+ arg, StringUtils.formatFileString(this));
+ throw e;
+ }
+ catch ( Exception e )
+ {
+ String msg = "Exception rendering #" + getName() + "(" + arg + ") at " +
+ StringUtils.formatFileString(this);
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+ finally
+ {
+ context.popCurrentTemplateName();
+ postRender(context);
+ }
+
+ /*
+ * note - a blocked input is still a successful operation as this is
+ * expected behavior.
+ */
+
+ return true;
+ }
+
+ /**
+ * Called by the parser to validate the argument types
+ */
+ @Override
+ public void checkArgs(ArrayList<Integer> argtypes, Token t, String templateName)
+ throws ParseException
+ {
+ if (argtypes.size() != 1)
+ {
+ throw new MacroParseException("The #" + getName() + " directive requires one argument",
+ templateName, t);
+ }
+
+ if (argtypes.get(0) == ParserTreeConstants.JJTWORD)
+ {
+ throw new MacroParseException("The argument to #" + getName() + " is of the wrong type",
+ templateName, t);
+ }
+ }
+
+ /**
+ * Find the template to render in the appropriate encoding
+ * @param path template path
+ * @param encoding template encoding
+ * @return found template
+ * @throws ResourceNotFoundException if template was not found
+ * @since 2.4
+ */
+ protected Template getTemplate(String path, String encoding) throws ResourceNotFoundException
+ {
+ return rsvc.getTemplate(path, encoding);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/RuntimeMacro.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/RuntimeMacro.java
new file mode 100644
index 00000000..95ddb165
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/RuntimeMacro.java
@@ -0,0 +1,371 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.Renderable;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeConstants.SpaceGobbling;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.runtime.parser.node.ASTDirective;
+import org.apache.velocity.runtime.parser.node.Node;
+import org.apache.velocity.runtime.parser.node.ParserTreeConstants;
+import org.apache.velocity.util.StringUtils;
+
+import org.apache.commons.lang3.Validate;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.List;
+
+/**
+ * This class acts as a proxy for potential macros. When the AST is built
+ * this class is inserted as a placeholder for the macro (whether or not
+ * the macro is actually defined). At render time we check whether there is
+ * a implementation for the macro call. If an implementation cannot be
+ * found the literal text is rendered.
+ * @since 1.6
+ */
+public class RuntimeMacro extends Directive
+{
+ /**
+ * Name of the macro
+ */
+ private String macroName;
+
+ /**
+ * Literal text of the macro
+ */
+ private String literal = null;
+
+ /**
+ * Node of the macro call
+ */
+ private Node node = null;
+
+ /**
+ * Indicates if we are running in strict reference mode.
+ */
+ protected boolean strictRef = false;
+
+ /**
+ * badArgsErrorMsg will be non null if the arguments to this macro
+ * are deamed bad at init time, see the init method. If his is non null, then this macro
+ * cannot be rendered, and if there is an attempt to render we throw an exception
+ * with this as the message.
+ */
+ private String badArgsErrorMsg = null;
+
+ /**
+ * Return name of this Velocimacro.
+ *
+ * @return The name of this Velocimacro.
+ */
+ @Override
+ public String getName()
+ {
+ return macroName;
+ }
+
+ /**
+ * Override to always return "macro". We don't want to use
+ * the macro name here, since when writing VTL that uses the
+ * scope, we are within a #macro call. The macro name will instead
+ * be used as the scope name when defining the body of a BlockMacro.
+ */
+ @Override
+ public String getScopeName()
+ {
+ return "macro";
+ }
+
+ /**
+ * Velocimacros are always LINE
+ * type directives.
+ *
+ * @return The type of this directive.
+ */
+ @Override
+ public int getType()
+ {
+ return LINE;
+ }
+
+
+ /**
+ * Initialize the Runtime macro. At the init time no implementation so we
+ * just save the values to use at the render time.
+ *
+ * @param rs runtime services
+ * @param name macro name
+ * @param context InternalContextAdapter
+ * @param node node containing the macro call
+ */
+ public void init(RuntimeServices rs, String name, InternalContextAdapter context,
+ Node node)
+ {
+ super.init(rs, context, node);
+
+ macroName = Validate.notNull(name);
+ macroName = rsvc.useStringInterning() ? macroName.intern() : macroName;
+ this.node = node;
+
+ /*
+ * Apply strictRef setting only if this really looks like a macro,
+ * so strict mode doesn't balk at things like #E0E0E0 in a template.
+ * compare with ")" is a simple #foo() style macro, comparing to
+ * "#end" is a block style macro. We use starts with because the token
+ * may end with '\n'
+ */
+ // Tokens can be used here since we are in init() and Tokens have not been dropped yet
+ Token t = node.getLastToken();
+ if (t.image.startsWith(")") || t.image.startsWith(rsvc.getParserConfiguration().getHashChar() + "end"))
+ {
+ strictRef = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false);
+ }
+
+ // Validate that none of the arguments are plain words, (VELOCITY-614)
+ // they should be string literals, references, inline maps, or inline lists
+ for (int n=0; n < node.jjtGetNumChildren(); n++)
+ {
+ Node child = node.jjtGetChild(n);
+ if (child.getType() == ParserTreeConstants.JJTWORD)
+ {
+ badArgsErrorMsg = "Invalid arg '" + child.getFirstTokenImage()
+ + "' in macro #" + macroName + " at " + StringUtils.formatFileString(child);
+
+ if (strictRef) // If strict, throw now
+ {
+ /* indicate col/line assuming it starts at 0
+ * this will be corrected one call up */
+ throw new TemplateInitException(badArgsErrorMsg,
+ null,
+ rsvc.getLogContext().getStackTrace(),
+ context.getCurrentTemplateName(), 0, 0);
+ }
+ }
+ }
+ // TODO: Improve this
+ // this is only needed if the macro does not exist during runtime
+ // since tokens are eliminated after this init call, we have to create a cached version of the
+ // literal which is in 99.9% cases waste. However, for regular macro calls (non Block macros)
+ // this doesn't create very long Strings so it's probably acceptable
+ getLiteral();
+ }
+
+ /**
+ * It is probably quite rare that we need to render the macro literal
+ * but since we won't keep the tokens in memory, we need to calculate it
+ * at parsing time.
+ */
+ private String getLiteral()
+ {
+ SpaceGobbling spaceGobbling = rsvc.getSpaceGobbling();
+ ASTDirective directive = (ASTDirective)node;
+
+ String morePrefix = directive.getMorePrefix();
+
+ if (literal == null)
+ {
+ StringBuilder buffer = new StringBuilder();
+ Token t = node.getFirstToken();
+
+ /* avoid outputting twice the prefix and the 'MORE' prefix,
+ * but still display the prefix in the cases where the ASTDirective would hide it */
+ int pos = -1;
+ while (t != null && t != node.getLastToken())
+ {
+ if (pos == -1) pos = t.image.lastIndexOf(rsvc.getParserConfiguration().getHashChar());
+ if (pos != -1)
+ {
+ buffer.append(t.image.substring(pos));
+ pos = 0;
+ }
+ else if (morePrefix.length() == 0 && spaceGobbling.compareTo(SpaceGobbling.LINES) >= 0)
+ {
+ buffer.append(t.image);
+ }
+ t = t.next;
+ }
+
+ if (t != null)
+ {
+ if (pos == -1) pos = t.image.lastIndexOf(rsvc.getParserConfiguration().getHashChar());
+ if (pos != -1)
+ {
+ buffer.append(t.image.substring(pos));
+ }
+ }
+
+ literal = buffer.toString();
+ /* avoid outputting twice the postfix, but still display it in the cases
+ * where the ASTDirective would hide it */
+ String postfix = directive.getPostfix();
+ if ((morePrefix.length() > 0 || spaceGobbling == SpaceGobbling.NONE) && literal.endsWith(postfix))
+ {
+ literal = literal.substring(0, literal.length() - postfix.length());
+ }
+ }
+ return literal;
+ }
+
+
+ /**
+ * Velocimacro implementation is not known at the init time. So look for
+ * a implementation in the macro libraries and if finds one renders it. The
+ * actual rendering is delegated to the VelocimacroProxy object. When
+ * looking for a macro we first loot at the template with has the
+ * macro call then we look at the macro libraries in the order they appear
+ * in the list. If a macro has many definitions above look up will
+ * determine the precedence.
+ *
+ * @param context
+ * @param writer
+ * @param node
+ * @return true if the rendering is successful
+ * @throws IOException
+ * @throws ResourceNotFoundException
+ * @throws ParseErrorException
+ * @throws MethodInvocationException
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer,
+ Node node)
+ throws IOException, ResourceNotFoundException,
+ ParseErrorException, MethodInvocationException
+ {
+ return render(context, writer, node, null);
+ }
+
+ /**
+ * This method is used with BlockMacro when we want to render a macro with a body AST.
+ *
+ * @param context
+ * @param writer
+ * @param node
+ * @param body AST block that was enclosed in the macro body.
+ * @return true if the rendering is successful
+ * @throws IOException
+ * @throws ResourceNotFoundException
+ * @throws ParseErrorException
+ * @throws MethodInvocationException
+ */
+ public boolean render(InternalContextAdapter context, Writer writer,
+ Node node, Renderable body)
+ throws IOException, ResourceNotFoundException,
+ ParseErrorException, MethodInvocationException
+ {
+ VelocimacroProxy vmProxy = null;
+ Template renderingTemplate = (Template)context.getCurrentResource();
+
+ /*
+ * first look in the source template
+ */
+ Object o = rsvc.getVelocimacro(macroName, renderingTemplate, getTemplate());
+
+ if( o != null )
+ {
+ // getVelocimacro can only return a VelocimacroProxy so we don't need the
+ // costly instanceof check
+ vmProxy = (VelocimacroProxy)o;
+ }
+
+ /*
+ * if not found, look in the macro libraries.
+ */
+ if (vmProxy == null)
+ {
+ List<Template> macroLibraries = context.getMacroLibraries();
+ if (macroLibraries != null)
+ {
+ for (int i = macroLibraries.size() - 1; i >= 0; i--)
+ {
+ o = rsvc.getVelocimacro(macroName, renderingTemplate, macroLibraries.get(i));
+
+ // get the first matching macro
+ if (o != null)
+ {
+ vmProxy = (VelocimacroProxy) o;
+ break;
+ }
+ }
+ }
+ }
+
+ if (vmProxy != null)
+ {
+ if (badArgsErrorMsg != null)
+ {
+ throw new TemplateInitException(badArgsErrorMsg,
+ null, rsvc.getLogContext().getStackTrace(),
+ context.getCurrentTemplateName(), node.getColumn(), node.getLine());
+ }
+
+ try
+ {
+ preRender(context);
+ return vmProxy.render(context, writer, node, body);
+ }
+ catch (StopCommand stop)
+ {
+ if (!stop.isFor(this))
+ {
+ throw stop;
+ }
+ return true;
+ }
+ catch (RuntimeException | IOException e)
+ {
+ /*
+ * We catch, the exception here so that we can record in
+ * the logs the template and line number of the macro call
+ * which generate the exception. This information is
+ * especially important for multiple macro call levels.
+ * this is also true for the following catch blocks.
+ */
+ log.error("Exception in macro #{} called at {}",
+ macroName, StringUtils.formatFileString(node));
+ throw e;
+ }
+ finally
+ {
+ postRender(context);
+ }
+ }
+ else if (strictRef)
+ {
+ throw new VelocityException("Macro '#" + macroName + "' is not defined at "
+ + StringUtils.formatFileString(node), null, rsvc.getLogContext().getStackTrace());
+ }
+
+ /*
+ * If we cannot find an implementation write the literal text
+ */
+ writer.write(getLiteral());
+ return true;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Scope.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Scope.java
new file mode 100755
index 00000000..7e7d226e
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Scope.java
@@ -0,0 +1,359 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+
+import java.util.AbstractMap;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This handles context scoping and metadata for directives.
+ *
+ * @author Nathan Bubna
+ * @version $Id$
+ */
+public class Scope extends AbstractMap<Object, Object>
+{
+ private static final String setReturnValue = "";
+ private Map<Object, Object> storage;
+ private Object replaced;
+ private Scope parent;
+ private Info info;
+ protected final Object owner;
+
+ /**
+ * @param owner
+ * @param previous
+ */
+ public Scope(Object owner, Object previous)
+ {
+ this.owner = owner;
+ if (previous != null)
+ {
+ try
+ {
+ this.parent = (Scope)previous;
+ }
+ catch (ClassCastException cce)
+ {
+ this.replaced = previous;
+ }
+ }
+ }
+
+ private Map<Object, Object> getStorage()
+ {
+ if (storage == null)
+ {
+ storage = new HashMap<>();
+ }
+ return storage;
+ }
+
+ /**
+ * @return entry set
+ */
+ @Override
+ public Set<Map.Entry<Object, Object>> entrySet()
+ {
+ return getStorage().entrySet();
+ }
+
+ /**
+ * getter
+ * @param key
+ * @return found value
+ */
+ @Override
+ public Object get(Object key)
+ {
+ Object o = super.get(key);
+ if (o == null && parent != null && !containsKey(key))
+ {
+ return parent.get(key);
+ }
+ return o;
+ }
+
+ /**
+ * setter
+ * @param key
+ * @param value
+ * @return previous value
+ */
+ @Override
+ public Object put(Object key, Object value)
+ {
+ return getStorage().put(key, value);
+ }
+
+ /**
+ * Convenience method to call put(key,val) in a template
+ * without worrying about what is returned/rendered by the call.
+ * This should ALWAYS return an empty string.
+ * @param key
+ * @param value
+ * @return empty string
+ */
+ public String set(Object key, Object value)
+ {
+ put(key, value);
+ return setReturnValue;
+ }
+
+ /**
+ * Allows #stop to easily trigger the proper StopCommand for this scope.
+ */
+ protected void stop()
+ {
+ throw new StopCommand(owner);
+ }
+
+ /**
+ * Returns the number of control arguments of this type
+ * that are stacked up. This is the distance between this
+ * instance and the topmost instance, plus one. This value
+ * will never be negative or zero.
+ * @return depth
+ */
+ protected int getDepth()
+ {
+ if (parent == null)
+ {
+ return 1;
+ }
+ return parent.getDepth() + 1;
+ }
+
+ /**
+ * Returns the topmost parent control reference, retrieved
+ * by simple recursion on {@link #getParent}.
+ * @return top-most scope
+ */
+ public Scope getTopmost()
+ {
+ if (parent == null)
+ {
+ return this;
+ }
+ return parent.getTopmost();
+ }
+
+ /**
+ * Returns the parent control reference overridden by the placement
+ * of this instance in the context.
+ * @return parent scope
+ */
+ public Scope getParent()
+ {
+ return parent;
+ }
+
+ /**
+ * Returns the user's context reference overridden by the placement
+ * of this instance in the context. If there was none (as is hoped),
+ * then this will return null. This never returns parent controls;
+ * those are returned by {@link #getParent}.
+ * @return replaced reference value, or null
+ */
+ public Object getReplaced()
+ {
+ if (replaced == null && parent != null)
+ {
+ return parent.getReplaced();
+ }
+ return replaced;
+ }
+
+ /**
+ * Returns info about the current scope for debugging purposes.
+ * @return template debugging infos
+ */
+ public Info getInfo()
+ {
+ if (info == null)
+ {
+ info = new Info(this, owner);
+ }
+ return info;
+ }
+
+ /**
+ * Class to encapsulate and provide access to info about
+ * the current scope for debugging.
+ */
+ public static class Info
+ {
+ private Scope scope;
+ private Directive directive;
+ private Template template;
+
+ /**
+ * c'tor
+ * @param scope
+ * @param owner
+ */
+ public Info(Scope scope, Object owner)
+ {
+ if (owner instanceof Directive)
+ {
+ directive = (Directive)owner;
+ }
+ if (owner instanceof Template)
+ {
+ template = (Template)owner;
+ }
+ this.scope = scope;
+ }
+
+ /**
+ * name getter
+ * @return name
+ */
+ public String getName()
+ {
+ if (directive != null)
+ {
+ return directive.getName();
+ }
+ if (template != null)
+ {
+ return template.getName();
+ }
+ return null;
+ }
+
+ /**
+ * type getter
+ * @return scope type
+ */
+ public String getType()
+ {
+ if (directive != null)
+ {
+ switch (directive.getType())
+ {
+ case Directive.BLOCK:
+ return "block";
+ case Directive.LINE:
+ return "line";
+ }
+ }
+ if (template != null)
+ {
+ return template.getEncoding();
+ }
+ return null;
+ }
+
+ /**
+ * current depth
+ * @return depth
+ */
+ public int getDepth()
+ {
+ return scope.getDepth();
+ }
+
+ /**
+ * template name getter
+ * @return template name
+ */
+ public String getTemplate()
+ {
+ if (directive != null)
+ {
+ return directive.getTemplateName();
+ }
+ if (template != null)
+ {
+ return template.getName();
+ }
+ return null;
+ }
+
+ /**
+ * line getter
+ * @return line number
+ */
+ public int getLine()
+ {
+ if (directive != null)
+ {
+ return directive.getLine();
+ }
+ return 0;
+ }
+
+ /**
+ * column getter
+ * @return column number
+ */
+ public int getColumn()
+ {
+ if (directive != null)
+ {
+ return directive.getColumn();
+ }
+ return 0;
+ }
+
+ /**
+ * string representation getter
+ * @return string representation
+ */
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ if (directive != null)
+ {
+ sb.append('#'); // parser characters substitution is not handled here
+ }
+ sb.append(getName());
+ sb.append("[type:").append(getType());
+ int depth = getDepth();
+ if (depth > 1)
+ {
+ sb.append(" depth:").append(depth);
+ }
+ if (template == null)
+ {
+ String vtl = getTemplate();
+ sb.append(" template:");
+ if (!vtl.contains(" "))
+ {
+ sb.append(vtl);
+ }
+ else
+ {
+ sb.append('"').append(vtl).append('"');
+ }
+ sb.append(" line:").append(getLine());
+ sb.append(" column:").append(getColumn());
+ }
+ sb.append(']');
+ return sb.toString();
+ }
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Stop.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Stop.java
new file mode 100644
index 00000000..c2caf388
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Stop.java
@@ -0,0 +1,115 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.runtime.parser.node.Node;
+
+import java.io.Writer;
+import java.util.ArrayList;
+
+/**
+ * This class implements the #stop directive which allows
+ * a user to stop the merging and rendering process. The #stop directive
+ * will accept a single message argument with info about the reason for
+ * stopping.
+ */
+public class Stop extends Directive
+{
+ private static final StopCommand STOP_ALL = new StopCommand("StopCommand to exit merging") {
+ @Override
+ public synchronized Throwable fillInStackTrace() {
+ return this;
+ }
+ };
+
+ private boolean hasMessage = false;
+
+ /**
+ * Return name of this directive.
+ * @return The name of this directive.
+ */
+ @Override
+ public String getName()
+ {
+ return "stop";
+ }
+
+ /**
+ * Return type of this directive.
+ * @return The type of this directive.
+ */
+ @Override
+ public int getType()
+ {
+ return LINE;
+ }
+
+ /**
+ * Since there is no processing of content,
+ * there is never a need for an internal scope.
+ */
+ @Override
+ public boolean isScopeProvided()
+ {
+ return false;
+ }
+
+ @Override
+ public void init(RuntimeServices rs, InternalContextAdapter context, Node node)
+ {
+ super.init(rs, context, node);
+
+ hasMessage = (node.jjtGetNumChildren() == 1);
+ }
+
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer, Node node)
+ {
+ if (!hasMessage)
+ {
+ throw STOP_ALL;
+ }
+
+ Object argument = node.jjtGetChild(0).value(context);
+
+ // stop all and use specified message
+ throw new StopCommand(String.valueOf(argument));
+ }
+
+ /**
+ * Called by the parser to check the argument types
+ */
+ @Override
+ public void checkArgs(ArrayList<Integer> argtypes, Token t, String templateName)
+ throws ParseException
+ {
+ int kids = argtypes.size();
+ if (kids > 1)
+ {
+ throw new MacroParseException("The #stop directive only accepts a single message parameter",
+ templateName, t);
+ }
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/StopCommand.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/StopCommand.java
new file mode 100755
index 00000000..37768a57
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/StopCommand.java
@@ -0,0 +1,89 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+import org.apache.velocity.runtime.RuntimeInstance;
+
+/**
+ * Stop command for directive Control objects. In an ideal JDK,
+ * this would be able to extend a RuntimeThrowable class, but we
+ * don't have that. So to avoid the interface changes needed by
+ * extending Throwable and the potential errant catches were we
+ * to extend RuntimeException, we'll have to extend Error,
+ * despite the fact that this is never an error.
+ *
+ * @author Nathan Bubna
+ * @version $Id$
+ */
+public class StopCommand extends Error
+{
+ private static final long serialVersionUID = 2577683435802825964L;
+ private Object stopMe;
+ private boolean nearest = false;
+
+ public StopCommand()
+ {
+ this.nearest = true;
+ }
+
+ public StopCommand(String message)
+ {
+ super(message);
+ }
+
+ public StopCommand(Object stopMe)
+ {
+ this.stopMe = stopMe;
+ }
+
+ @Override
+ public String getMessage()
+ {
+ if (stopMe != null)
+ {
+ // only create a useful message if requested (which is unlikely)
+ return "StopCommand: "+stopMe;
+ }
+ else
+ {
+ return "StopCommand: "+super.getMessage();
+ }
+ }
+
+ public boolean isFor(Object that)
+ {
+ if (nearest) // if we're stopping at the first chance
+ {
+ // save that for message
+ stopMe = that;
+ return true;
+ }
+ else if (stopMe != null) // if we have a specified stopping point
+ {
+ return (that == stopMe);
+ }
+ else // only stop for the top :)
+ {
+ return (that instanceof Template ||
+ that instanceof RuntimeInstance);
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/VelocimacroProxy.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/VelocimacroProxy.java
new file mode 100644
index 00000000..d3b8ff5c
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/VelocimacroProxy.java
@@ -0,0 +1,459 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MacroOverflowException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.Renderable;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.directive.Macro.MacroArg;
+import org.apache.velocity.runtime.parser.node.ASTReference;
+import org.apache.velocity.runtime.parser.node.ASTStringLiteral;
+import org.apache.velocity.runtime.parser.node.Node;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+import org.apache.velocity.util.StringUtils;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Deque;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * VelocimacroProxy.java
+ *
+ * a proxy Directive-derived object to fit with the current directive system
+ *
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class VelocimacroProxy extends Directive
+{
+ private String macroName;
+ private List<MacroArg> macroArgs = null;
+ private String[] literalArgArray = null;
+ private SimpleNode nodeTree = null;
+ private int numMacroArgs = 0;
+ private boolean strictArguments;
+ private int maxCallDepth;
+ private String bodyReference;
+ private boolean enableBCmode;
+
+ private static final Object NULL_VALUE_MARKER = new Object();
+
+ /**
+ * Return name of this Velocimacro.
+ * @return The name of this Velocimacro.
+ */
+ @Override
+ public String getName()
+ {
+ return macroName;
+ }
+
+ /**
+ * Velocimacros are always LINE type directives.
+ * @return The type of this directive.
+ */
+ @Override
+ public int getType()
+ {
+ return LINE;
+ }
+
+ /**
+ * sets the directive name of this VM
+ *
+ * @param name
+ */
+ public void setName(String name)
+ {
+ macroName = name;
+ }
+
+ /**
+ * sets the array of arguments specified in the macro definition
+ * @param args Array of macro arguments, containing the
+ * #macro() arguments and default values. the 0th is the name.
+ */
+ public void setMacroArgs(List<Macro.MacroArg> args)
+ {
+ macroArgs = args;
+
+ // for performance reasons we precache these strings - they are needed in
+ // "render literal if null" functionality
+ if (enableBCmode)
+ {
+ literalArgArray = new String[macroArgs.size()];
+ for (int i = 0; i < macroArgs.size(); i++)
+ {
+ literalArgArray[i] = ".literal.$" + macroArgs.get(i).name;
+ }
+ }
+
+ /*
+ * get the arg count from the arg array. remember that the arg array has the macro name as
+ * it's 0th element
+ */
+
+ numMacroArgs = macroArgs.size() - 1;
+ }
+
+ /**
+ * Return the list of macro arguments associated with this macro
+ * @return macro arguments
+ */
+ public List<Macro.MacroArg> getMacroArgs()
+ {
+ return macroArgs;
+ }
+
+ /**
+ * @param tree
+ */
+ public void setNodeTree(SimpleNode tree)
+ {
+ nodeTree = tree;
+ }
+
+ /**
+ * returns the number of ars needed for this VM
+ * @return The number of ars needed for this VM
+ */
+ public int getNumArgs()
+ {
+ return numMacroArgs;
+ }
+
+ /**
+ * Initialize members of VelocimacroProxy. called from MacroEntry
+ * @param rs runtime services
+ */
+ public void init(RuntimeServices rs)
+ {
+ rsvc = rs;
+ log = rs.getLog("macro");
+
+ strictArguments = rsvc.getBoolean(
+ RuntimeConstants.VM_ARGUMENTS_STRICT, false);
+
+ // get the macro call depth limit
+ maxCallDepth = rsvc.getInt(RuntimeConstants.VM_MAX_DEPTH);
+
+ // get name of the reference that refers to AST block passed to block macro call
+ bodyReference = rsvc.getString(RuntimeConstants.VM_BODY_REFERENCE, "bodyContent");
+
+ enableBCmode = rsvc.getBoolean(RuntimeConstants.VM_ENABLE_BC_MODE, false);
+ }
+
+ /**
+ * Render the macro AST node
+ * @param context
+ * @param writer
+ * @param node
+ * @return success status
+ * @throws IOException
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer, Node node)
+ throws IOException
+ {
+ return render(context, writer, node, null);
+ }
+
+ /**
+ * Renders the macro using the context.
+ *
+ * @param context Current rendering context
+ * @param writer Writer for output
+ * @param node AST that calls the macro
+ * @param body the macro body
+ * @return true if the directive rendered successfully.
+ * @throws IOException
+ */
+ public boolean render(InternalContextAdapter context, Writer writer,
+ Node node, Renderable body)
+ throws IOException
+ {
+ int callArgNum = node.jjtGetNumChildren();
+
+ // if this macro was invoked by a call directive, we might have a body AST here.
+ Object oldBodyRef = null;
+ if (body != null)
+ {
+ oldBodyRef = context.get(bodyReference);
+ context.put(bodyReference, body);
+ callArgNum--; // Remove the body AST from the arg count
+ }
+
+ // is everything copacetic?
+ checkArgumentCount(node, callArgNum);
+ checkDepth(context);
+
+ // put macro arg values and save the returned old/new value pairs
+ Object[] values = handleArgValues(context, node, callArgNum);
+ try
+ {
+ // render the velocity macro
+ context.pushCurrentMacroName(macroName);
+ nodeTree.render(context, writer);
+ }
+ catch (RuntimeException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ String msg = "VelocimacroProxy.render() : exception VM = #" + macroName + "()";
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+ finally
+ {
+ // if MacroOverflowException was thrown then it already empties the stack
+ // for everything else - e.g. other exceptions - we clean up after ourself
+ if (context.getCurrentMacroCallDepth() > 0)
+ context.popCurrentMacroName();
+
+ // clean up after the args and bodyRef
+ // but only if they weren't overridden inside
+ Object current = context.get(bodyReference);
+ if (current == body)
+ {
+ if (oldBodyRef != null)
+ {
+ context.put(bodyReference, oldBodyRef);
+ }
+ else
+ {
+ context.remove(bodyReference);
+ }
+ }
+
+ for (int i = 1; i < macroArgs.size(); i++)
+ {
+ MacroArg macroArg = macroArgs.get(i);
+ current = context.get(macroArg.name);
+ Object given = values[(i-1) * 2 + 1];
+ Object old = values[(i-1) * 2];
+ if (current == given || current == null && given == NULL_VALUE_MARKER)
+ {
+ if (old == null)
+ {
+ context.remove(macroArg.name);
+ }
+ else if (old == NULL_VALUE_MARKER)
+ {
+ context.put(macroArg.name, null);
+ }
+ else
+ {
+ context.put(macroArg.name, old);
+ }
+ }
+
+ if (enableBCmode)
+ {
+ /* allow for nested calls */
+ Deque<String> literalsStack = (Deque<String>)context.get(literalArgArray[i]);
+ if (literalsStack != null) /* shouldn't be null */
+ {
+ literalsStack.removeFirst();
+ if (literalsStack.size() == 0)
+ {
+ context.remove(literalArgArray[i]);
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Check whether the number of arguments given matches the number defined.
+ * @param node
+ * @param callArgNum
+ */
+ protected void checkArgumentCount(Node node, int callArgNum)
+ {
+ // Check if we have more calling arguments then the macro accepts
+ if (callArgNum > macroArgs.size() - 1)
+ {
+ if (strictArguments)
+ {
+ throw new VelocityException("Provided " + callArgNum + " arguments but macro #"
+ + macroArgs.get(0).name + " accepts at most " + (macroArgs.size()-1)
+ + " at " + StringUtils.formatFileString(node), null, rsvc.getLogContext().getStackTrace());
+ }
+ // Backward compatibility logging, Mainly for MacroForwardDefinedTestCase
+ log.debug("VM #{}: too many arguments to macro. Wanted {} got {}",
+ macroArgs.get(0).name, macroArgs.size() - 1, callArgNum);
+ }
+ }
+
+ /**
+ * check that we aren't already at the max call depth and throws
+ * a MacroOverflowException if we are there.
+ * @param context
+ */
+ protected void checkDepth(InternalContextAdapter context)
+ {
+ if (maxCallDepth > 0 && maxCallDepth == context.getCurrentMacroCallDepth())
+ {
+ String[] stack = context.getMacroNameStack();
+
+ StringBuilder out = new StringBuilder(100)
+ .append("Max calling depth of ").append(maxCallDepth)
+ .append(" was exceeded in macro '").append(macroName)
+ .append("' with Call Stack:");
+ for (int i = 0; i < stack.length; i++)
+ {
+ if (i != 0)
+ {
+ out.append("->");
+ }
+ out.append(stack[i]);
+ }
+ out.append(" at ").append(StringUtils.formatFileString(this));
+ log.error(out.toString());
+
+ // clean out the macro stack, since we just broke it
+ while (context.getCurrentMacroCallDepth() > 0)
+ {
+ context.popCurrentMacroName();
+ }
+ throw new MacroOverflowException(out.toString(), null, rsvc.getLogContext().getStackTrace());
+ }
+ }
+
+ /**
+ * Gets the macro argument values and puts them in the context under
+ * the argument names. Store and return an array of old and new values
+ * paired for each argument name, for later cleanup. Also, put literal
+ * representations of arguments which evaluate to null in the context.
+ * @param context
+ * @param node
+ * @param callArgNum
+ * @return macro arguments values
+ */
+ protected Object[] handleArgValues(InternalContextAdapter context,
+ Node node, int callArgNum)
+ {
+ // Changed two dimensional array to single dimensional to optimize memory lookups
+ Object[] values = new Object[macroArgs.size() * 2];
+
+ boolean warnedMissingArguments = false;
+
+ // Move arguments into the macro's context. Start at one to skip macro name
+ for (int i = 1; i < macroArgs.size(); i++)
+ {
+ MacroArg macroArg = macroArgs.get(i);
+ Object oldVal = context.get(macroArg.name);
+ values[(i-1) * 2] =
+ oldVal == null
+ ? context.containsKey(macroArg.name) ? NULL_VALUE_MARKER : null
+ : oldVal;
+
+ // put the new value in
+ Object newVal = null;
+ Node argNode = null;
+ if (i - 1 < callArgNum)
+ {
+ // There's a calling value.
+ argNode = node.jjtGetChild(i - 1);
+ newVal = argNode.value(context);
+ }
+ else if (macroArg.defaultVal != null)
+ {
+ // We don't have a calling value, but the macro defines a default value
+ newVal = macroArg.defaultVal.value(context);
+ }
+ else if (strictArguments)
+ {
+ // We come to this point if we don't have a calling value, and
+ // there is no default value. Not enough arguments defined.
+ int minArgNum = -1; //start at -1 to skip the macro name
+ // Calculate minimum number of args required for macro
+ for (MacroArg marg : macroArgs)
+ {
+ if (marg.defaultVal == null) minArgNum++;
+ }
+ throw new VelocityException("Need at least " + minArgNum + " argument for macro #"
+ + macroArgs.get(0).name + " but only " + callArgNum + " where provided at "
+ + StringUtils.formatFileString(node), null, rsvc.getLogContext().getStackTrace());
+ }
+ else
+ {
+ if (!warnedMissingArguments)
+ {
+ // Backward compatibility logging, Mainly for MacroForwardDefinedTestCase
+ log.debug("VM #{}: too few arguments to macro. Wanted {} got {}",
+ macroArgs.get(0).name, macroArgs.size() - 1, callArgNum);
+ warnedMissingArguments = true;
+ }
+ if (enableBCmode)
+ {
+ // use the global context value as default
+ newVal = oldVal;
+ }
+ }
+
+ values[(i-1) * 2 + 1] = newVal;
+
+ /* when enableBCmode is true, we still store the actual reference passed to the macro
+ even if the value is not null, because *if* the argument is set to null *during* the macro rendering
+ we still expect the passed argument literal to be displayed to be fully backward compatible. */
+ if (enableBCmode && /* newVal == null && */ argNode != null)
+ {
+ /* allow nested macro calls for B.C. */
+ Deque<String> literalsStack = (Deque<String>)context.get(literalArgArray[i]);
+ if (literalsStack == null)
+ {
+ literalsStack = new LinkedList<>();
+ context.put(literalArgArray[i], literalsStack);
+ }
+ /* Reflects the strange 1.7 behavor... */
+ if (argNode != null && (argNode instanceof ASTReference || argNode instanceof ASTStringLiteral))
+ {
+ literalsStack.addFirst(argNode.literal());
+ }
+ else
+ {
+ literalsStack.addFirst('$' + macroArg.name);
+ }
+ }
+ }
+
+ // Now really put the values in the context
+ for (int i = 1; i < macroArgs.size(); i++)
+ {
+ MacroArg macroArg = macroArgs.get(i);
+ Object value = values[(i-1) * 2 + 1];
+ context.put(macroArg.name, value);
+ }
+
+ // return the array of replaced and new values
+ return values;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/contrib/For.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/contrib/For.java
new file mode 100644
index 00000000..aaefae9b
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/contrib/For.java
@@ -0,0 +1,141 @@
+package org.apache.velocity.runtime.directive.contrib;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.directive.Foreach;
+import org.apache.velocity.runtime.directive.MacroParseException;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.runtime.parser.node.ASTReference;
+import org.apache.velocity.runtime.parser.node.Node;
+import org.apache.velocity.runtime.parser.node.ParserTreeConstants;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+
+/**
+ * The #for directive provides the behavior of the #foreach directive but also
+ * provides an 'index' keyword that allows the user to define an optional index variable
+ * that tracks the loop iterations. e.g.; #for($user in $users index $i).
+ * As $user iterates through $users the index reference $i will be equal to
+ * 0, 1, 2, etc..
+ * @see org.apache.velocity.runtime.directive.Foreach
+ */
+public class For extends Foreach
+{
+ protected String counterName;
+ protected int counterInitialValue;
+
+ @Override
+ public String getName()
+ {
+ return "for";
+ }
+
+ @Override
+ public int getType()
+ {
+ return BLOCK;
+ }
+
+ @Override
+ public void init(RuntimeServices rs, InternalContextAdapter context, Node node)
+ throws TemplateInitException
+ {
+ super.init(rs, context, node);
+ // If we have more then 3 argument then the user has specified an
+ // index value, i.e.; #foreach($a in $b index $c)
+ if (node.jjtGetNumChildren() > 4)
+ {
+ // The index variable name is at position 4
+ counterName = ((ASTReference) node.jjtGetChild(4)).getRootString();
+ // The count value always starts at 0 when using an index.
+ counterInitialValue = 0;
+ }
+ }
+
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer, Node node)
+ throws IOException
+ {
+ Object c = context.get(counterName);
+ context.put(counterName, counterInitialValue);
+ try
+ {
+ return super.render(context, writer, node);
+ }
+ finally
+ {
+ if (c != null)
+ {
+ context.put(counterName, c);
+ }
+ else
+ {
+ context.remove(counterName);
+ }
+ }
+ }
+
+ @Override
+ protected void renderBlock(InternalContextAdapter context, Writer writer, Node node)
+ throws IOException
+ {
+ Object count = context.get(counterName);
+ if (count instanceof Number)
+ {
+ context.put(counterName, ((Number)count).intValue() + 1);
+ }
+ super.renderBlock(context, writer, node);
+ }
+
+ /**
+ * We do not allow a word token in any other arg position except for the 2nd
+ * since we are looking for the pattern #foreach($foo in $bar).
+ */
+ @Override
+ public void checkArgs(ArrayList<Integer> argtypes, Token t,
+ String templateName) throws ParseException
+ {
+ super.checkArgs(argtypes, t, templateName);
+
+ // If #foreach is defining an index variable make sure it has the 'index
+ // $var' combo.
+ if (argtypes.size() > 3)
+ {
+ if (argtypes.get(3) != ParserTreeConstants.JJTWORD)
+ {
+ throw new MacroParseException(
+ "Expected word 'index' at argument position 4 in #foreach",
+ templateName, t);
+ }
+ else if (argtypes.size() == 4
+ || argtypes.get(4) != ParserTreeConstants.JJTREFERENCE)
+ {
+ throw new MacroParseException(
+ "Expected a reference after 'index' in #foreach", templateName, t);
+ }
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/CharStream.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/CharStream.java
new file mode 100644
index 00000000..9e7ee389
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/CharStream.java
@@ -0,0 +1,128 @@
+package org.apache.velocity.runtime.parser;
+
+/*
+ * 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.
+ */
+/* Generated By:JavaCC: Do not edit this line. CharStream.java Version 2.1 */
+
+/**
+ * This interface describes a character stream that maintains line and
+ * column number positions of the characters. It also has the capability
+ * to backup the stream to some extent. An implementation of this
+ * interface is used in the TokenManager implementation generated by
+ * JavaCCParser.
+ *
+ * All the methods except backup can be implemented in any fashion. backup
+ * needs to be implemented correctly for the correct operation of the lexer.
+ * Rest of the methods are all used to get information like line number,
+ * column number and the String that constitutes a token and are not used
+ * by the lexer. Hence their implementation won't affect the generated lexer's
+ * operation.
+ */
+
+public interface CharStream
+{
+ /**
+ * Returns the next character from the selected input. The method
+ * of selecting the input is the responsibility of the class
+ * implementing this interface. Can throw any java.io.IOException.
+ * @return read char
+ * @throws java.io.IOException
+ */
+ char readChar() throws java.io.IOException;
+
+ /**
+ * Returns the column number of the last character for current token (being
+ * matched after the last call to BeginTOken).
+ * @return ending column number
+ */
+ int getEndColumn();
+
+ /**
+ * Returns the line number of the last character for current token (being
+ * matched after the last call to BeginTOken).
+ * @return ending line number
+ */
+ int getEndLine();
+
+ /**
+ * Returns the column number of the first character for current token (being
+ * matched after the last call to BeginTOken).
+ * @return starting column number
+ */
+ int getBeginColumn();
+
+ /**
+ * Returns the line number of the first character for current token (being
+ * matched after the last call to BeginTOken).
+ * @return starting line number
+ */
+ int getBeginLine();
+
+ /**
+ * Backs up the input stream by amount steps. Lexer calls this method if it
+ * had already read some characters, but could not use them to match a
+ * (longer) token. So, they will be used again as the prefix of the next
+ * token and it is the implemetation's responsibility to do this right.
+ * @param amount
+ */
+ void backup(int amount);
+
+ /**
+ * Returns the next character that marks the beginning of the next token.
+ * All characters must remain in the buffer between two successive calls
+ * to this method to implement backup correctly.
+ * @return next token start char
+ * @throws java.io.IOException
+ */
+ char BeginToken() throws java.io.IOException;
+
+ /**
+ * Returns a string made up of characters from the marked token beginning
+ * to the current buffer position. Implementations have the choice of returning
+ * anything that they want to. For example, for efficiency, one might decide
+ * to just return null, which is a valid implementation.
+ * @return token image
+ */
+ String GetImage();
+
+ /**
+ * <p>Returns an array of characters that make up the suffix of length 'len' for
+ * the currently matched token. This is used to build up the matched string
+ * for use in actions in the case of MORE. A simple and inefficient
+ * implementation of this is as follows :</p>
+ * <pre><code>
+ * {
+ * String t = GetImage();
+ * return t.substring(t.length() - len, t.length()).toCharArray();
+ * }
+ * </code></pre>
+ * @param len suffix len
+ * @return suffix
+ */
+ char[] GetSuffix(int len);
+
+ /**
+ * The lexer calls this function to indicate that it is done with the stream
+ * and hence implementations can free any resources held by this class.
+ * Again, the body of this function can be just empty and it will not
+ * affect the lexer's operation.
+ */
+ void Done();
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/LogContext.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/LogContext.java
new file mode 100644
index 00000000..c6738cd5
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/LogContext.java
@@ -0,0 +1,165 @@
+package org.apache.velocity.runtime.parser;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+import org.apache.velocity.util.introspection.Info;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * <p>Track location in template files during rendering by populating the slf4j MDC tags <code>file</code>, <code>line</code> and <code>column</code>.</p>
+ * <p>An MDC-aware logger can then use this info to display the template location in the message</p>
+ * <p>For instance with webapp-slf4j-logger, it's enough to use <code>%file</code>, <code>%line</code> and <code>%column</code> in the logger format string.</p>
+ * <p>Since this feature can have a performance impact, it has to be enabled in <code>velocity.properties</code> using:</p>
+ * <pre><code>runtime.log.track_location = true</code></pre>
+ * <p>(typically in a development environment)</p>
+ *
+ * @author Claude Brisson
+ * @version $Id:$
+ * @since 2.2
+ */
+
+public class LogContext
+{
+ protected static Logger logger = LoggerFactory.getLogger("rendering");
+
+ public static final String MDC_FILE = "file";
+ public static final String MDC_LINE = "line";
+ public static final String MDC_COLUMN = "column";
+
+ private boolean trackLocation;
+
+ public LogContext(boolean trackLocation)
+ {
+ this.trackLocation = trackLocation;
+ }
+
+ private static ThreadLocal<Deque<StackElement>> contextStack = new ThreadLocal<Deque<StackElement>>()
+ {
+ @Override
+ public Deque<StackElement> initialValue()
+ {
+ return new ArrayDeque<>();
+ }
+ };
+
+ private static class StackElement
+ {
+ protected StackElement(SimpleNode src, Info info)
+ {
+ this.src = src;
+ this.info = info;
+ }
+
+ protected SimpleNode src;
+ protected int count = 1;
+ protected Info info;
+ }
+
+ public void pushLogContext(SimpleNode src, Info info)
+ {
+ if (!trackLocation)
+ {
+ return;
+ }
+ Deque<StackElement> stack = contextStack.get();
+ StackElement last = stack.peek();
+ if (last != null && last.src == src)
+ {
+ ++last.count;
+ }
+ else
+ {
+ stack.push(new StackElement(src, info));
+ setLogContext(info);
+ }
+ }
+
+ public void popLogContext()
+ {
+ if (!trackLocation)
+ {
+ return;
+ }
+ Deque<StackElement> stack = contextStack.get();
+ StackElement last = stack.peek();
+ if (last == null)
+ {
+ logger.error("log context is already empty");
+ return;
+ }
+ if (--last.count == 0)
+ {
+ stack.pop();
+ last = stack.peek();
+ if (last == null)
+ {
+ clearLogContext();
+ }
+ else
+ {
+ setLogContext(last.info);
+ }
+ }
+ }
+
+ private void setLogContext(Info info)
+ {
+ MDC.put(MDC_FILE, info.getTemplateName());
+ MDC.put(MDC_LINE, String.valueOf(info.getLine()));
+ MDC.put(MDC_COLUMN, String.valueOf(info.getColumn()));
+ }
+
+ private void clearLogContext()
+ {
+ MDC.remove(MDC_FILE);
+ MDC.remove(MDC_LINE);
+ MDC.remove(MDC_COLUMN);
+ }
+
+ private static final String STACKTRACE_LINE = " %s at %s[line %d, column %d]";
+
+ public String[] getStackTrace()
+ {
+ if (!trackLocation)
+ {
+ return null;
+ }
+ Deque<StackElement> stack = contextStack.get();
+ List<String> levels = new ArrayList<>();
+ for (StackElement level : stack)
+ {
+ String line = String.format(STACKTRACE_LINE,
+ level.src.literal(),
+ level.info.getTemplateName(),
+ level.info.getLine(),
+ level.info.getColumn());
+ levels.add(line);
+ }
+ return levels.size() > 0 ? levels.toArray(new String[levels.size()]) : null;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/ParseException.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/ParseException.java
new file mode 100644
index 00000000..4a8d5769
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/ParseException.java
@@ -0,0 +1,237 @@
+package org.apache.velocity.runtime.parser;
+
+/*
+ * 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.
+ */
+/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */
+
+/**
+ * This exception is thrown when parse errors are encountered.
+ * It is intended to be caught and typically will be rethrown
+ * as a ParseErrorException.
+ *
+ * <p>You can explicitly create objects of this exception type by
+ * calling the method generateParseException in the generated
+ * parser.
+ *
+ * You can modify this class to customize your error reporting
+ * mechanisms so long as you retain the public fields.
+ */
+public class ParseException extends Exception {
+
+ private static final long serialVersionUID = -309603325673449381L;
+
+ /**
+ * <p>This constructor is used by the method "generateParseException"
+ * in the generated parser. Calling this constructor generates
+ * a new object of this type with the fields "currentToken",
+ * "expectedTokenSequences", and "tokenImage" set. The boolean
+ * flag "specialConstructor" is also set to true to indicate that
+ * this constructor was used to create this object.
+ * This constructor calls its super class with the empty string
+ * to force the "toString" method of parent class "Throwable" to
+ * print the error message in the form:</p>
+ * <pre>
+ * ParseException: &lt;result of getMessage&gt;
+ * </pre>
+ * @param currentTokenVal
+ * @param expectedTokenSequencesVal
+ * @param tokenImageVal
+ */
+ public ParseException(Token currentTokenVal,
+ int[][] expectedTokenSequencesVal,
+ String[] tokenImageVal
+ )
+ {
+ super("");
+ specialConstructor = true;
+ currentToken = currentTokenVal;
+ expectedTokenSequences = expectedTokenSequencesVal;
+ tokenImage = tokenImageVal;
+ }
+
+ /**
+ * The following constructors are for use by you for whatever
+ * purpose you can think of. Constructing the exception in this
+ * manner makes the exception behave in the normal way - i.e., as
+ * documented in the class "Throwable". The fields "errorToken",
+ * "expectedTokenSequences", and "tokenImage" do not contain
+ * relevant information. The JavaCC generated code does not use
+ * these constructors.
+ */
+ public ParseException() {
+ super();
+ specialConstructor = false;
+ }
+
+ /**
+ * The following constructors are for use by you for whatever
+ * purpose you can think of. Constructing the exception in this
+ * manner makes the exception behave in the normal way - i.e., as
+ * documented in the class "Throwable". The fields "errorToken",
+ * "expectedTokenSequences", and "tokenImage" do not contain
+ * relevant information. The JavaCC generated code does not use
+ * these constructors.
+ * @param message
+ */
+ public ParseException(String message) {
+ super(message);
+ specialConstructor = false;
+ }
+
+ /**
+ * This variable determines which constructor was used to create
+ * this object and thereby affects the semantics of the
+ * "getMessage" method (see below).
+ */
+ protected boolean specialConstructor;
+
+ /**
+ * This is the last token that has been consumed successfully. If
+ * this object has been created due to a parse error, the token
+ * followng this token will (therefore) be the first error token.
+ */
+ public Token currentToken;
+
+ /**
+ * Each entry in this array is an array of integers. Each array
+ * of integers represents a sequence of tokens (by their ordinal
+ * values) that is expected at this point of the parse.
+ */
+ public int[][] expectedTokenSequences;
+
+ /**
+ * This is a reference to the "tokenImage" array of the generated
+ * parser within which the parse error occurred. This array is
+ * defined in the generated ...Constants interface.
+ */
+ public String[] tokenImage;
+
+ /**
+ * This method has the standard behavior when this object has been
+ * created using the standard constructors. Otherwise, it uses
+ * "currentToken" and "expectedTokenSequences" to generate a parse
+ * error message and returns it. If this object has been created
+ * due to a parse error, and you do not catch it (it gets thrown
+ * from the parser), then this method is called during the printing
+ * of the final stack trace, and hence the correct error message
+ * gets displayed.
+ * @return message
+ */
+ @Override
+ public String getMessage() {
+ if (!specialConstructor) {
+ return super.getMessage();
+ }
+ String expected = "";
+ int maxSize = 0;
+ for (int[] expectedTokenSequence : expectedTokenSequences)
+ {
+ if (maxSize < expectedTokenSequence.length)
+ {
+ maxSize = expectedTokenSequence.length;
+ }
+ for (int i : expectedTokenSequence)
+ {
+ expected += tokenImage[i] + " ";
+ }
+ if (expectedTokenSequence[expectedTokenSequence.length - 1] != 0)
+ {
+ expected += "...";
+ }
+ expected += eol + " ";
+ }
+ String retval = "Encountered \"";
+ Token tok = currentToken.next;
+ for (int i = 0; i < maxSize; i++) {
+ if (i != 0) retval += " ";
+ if (tok.kind == 0) {
+ retval += tokenImage[0];
+ break;
+ }
+ retval += add_escapes(tok.image);
+ tok = tok.next;
+ }
+ retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn;
+ retval += "." + eol;
+ if (expectedTokenSequences.length == 1) {
+ retval += "Was expecting:" + eol + " ";
+ } else {
+ retval += "Was expecting one of:" + eol + " ";
+ }
+ retval += expected;
+ return retval;
+ }
+
+ /**
+ * The end of line string for this machine.
+ */
+ protected String eol = System.lineSeparator();
+
+ /**
+ * Used to convert raw characters to their escaped version
+ * when these raw version cannot be used as part of an ASCII
+ * string literal.
+ * @param str raw characters
+ * @return escaped string
+ */
+ protected String add_escapes(String str) {
+ StringBuilder retval = new StringBuilder();
+ char ch;
+ for (int i = 0; i < str.length(); i++) {
+ switch (str.charAt(i))
+ {
+ case 0 :
+ continue;
+ case '\b':
+ retval.append("\\b");
+ continue;
+ case '\t':
+ retval.append("\\t");
+ continue;
+ case '\n':
+ retval.append("\\n");
+ continue;
+ case '\f':
+ retval.append("\\f");
+ continue;
+ case '\r':
+ retval.append("\\r");
+ continue;
+ case '\"':
+ retval.append("\\\"");
+ continue;
+ case '\'':
+ retval.append("\\\'");
+ continue;
+ case '\\':
+ retval.append("\\\\");
+ continue;
+ default:
+ if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
+ String s = "0000" + Integer.toString(ch, 16);
+ retval.append("\\u").append(s.substring(s.length() - 4, s.length()));
+ } else {
+ retval.append(ch);
+ }
+ }
+ }
+ return retval.toString();
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/Parser.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/Parser.java
new file mode 100644
index 00000000..73bc0e9b
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/Parser.java
@@ -0,0 +1,54 @@
+package org.apache.velocity.runtime.parser;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.directive.Directive;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+
+import java.io.Reader;
+
+public interface Parser
+{
+ RuntimeServices getRuntimeServices();
+ SimpleNode parse(Reader reader, Template template) throws ParseException;
+ void resetCurrentTemplate();
+ Template getCurrentTemplate();
+ Token getToken(int index);
+ boolean isDirective(String macro);
+ Directive getDirective(String directive);
+ void ReInit(CharStream stream);
+
+ char dollar();
+ char hash();
+ char at();
+ char asterisk();
+
+ default String lineComment()
+ {
+ return String.valueOf(hash()) + hash();
+ }
+
+ default String blockComment()
+ {
+ return String.valueOf(hash()) + asterisk();
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/TemplateParseException.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/TemplateParseException.java
new file mode 100644
index 00000000..acb20a97
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/TemplateParseException.java
@@ -0,0 +1,245 @@
+package org.apache.velocity.runtime.parser;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ExtendedParseException;
+import org.apache.velocity.util.StringUtils;
+
+/**
+ * This is an extension of the ParseException, which also takes a
+ * template name.
+ *
+ * @see org.apache.velocity.runtime.parser.ParseException
+ *
+ * @author <a href="hps@intermeta.de">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ * @since 1.5
+ */
+public class TemplateParseException
+ extends ParseException
+ implements ExtendedParseException
+{
+ private static final long serialVersionUID = -3146323135623083918L;
+
+ /**
+ * This is the name of the template which contains the parsing error, or
+ * null if not defined.
+ */
+ private final String templateName;
+
+ /**
+ * This constructor is used to add a template name
+ * to info cribbed from a ParseException generated in the parser.
+ * @param currentTokenVal
+ * @param expectedTokenSequencesVal
+ * @param tokenImageVal
+ * @param templateNameVal
+ */
+ public TemplateParseException(Token currentTokenVal, int [][] expectedTokenSequencesVal, String [] tokenImageVal,
+ String templateNameVal)
+ {
+ super(currentTokenVal, expectedTokenSequencesVal, tokenImageVal);
+ this.templateName = templateNameVal;
+ }
+
+ /**
+ * <p>This constructor is used by the method "generateParseException"
+ * in the generated parser. Calling this constructor generates
+ * a new object of this type with the fields "currentToken",
+ * "expectedTokenSequences", and "tokenImage" set. The boolean
+ * flag "specialConstructor" is also set to true to indicate that
+ * this constructor was used to create this object.
+ * This constructor calls its super class with the empty string
+ * to force the "toString" method of parent class "Throwable" to
+ * print the error message in the form:</p>
+ * <pre>
+ * ParseException: &lt;result of getMessage&gt;
+ * </pre>
+ * @param currentTokenVal
+ * @param expectedTokenSequencesVal
+ * @param tokenImageVal
+ */
+ public TemplateParseException(Token currentTokenVal, int [][] expectedTokenSequencesVal, String [] tokenImageVal)
+ {
+ super(currentTokenVal, expectedTokenSequencesVal, tokenImageVal);
+ templateName = "*unset*";
+ }
+
+ /**
+ * The following constructors are for use by you for whatever
+ * purpose you can think of. Constructing the exception in this
+ * manner makes the exception behave in the normal way - i.e., as
+ * documented in the class "Throwable". The fields "errorToken",
+ * "expectedTokenSequences", and "tokenImage" do not contain
+ * relevant information. The JavaCC generated code does not use
+ * these constructors.
+ */
+ public TemplateParseException()
+ {
+ super();
+ templateName = "*unset*";
+ }
+
+ /**
+ * Creates a new TemplateParseException object.
+ *
+ * @param message TODO: DOCUMENT ME!
+ */
+ public TemplateParseException(String message)
+ {
+ super(message);
+ templateName = "*unset*";
+ }
+
+ /**
+ * returns the Template name where this exception occurred.
+ * @return The Template name where this exception occurred.
+ */
+ @Override
+ public String getTemplateName()
+ {
+ return templateName;
+ }
+
+ /**
+ * returns the line number where this exception occurred.
+ * @return The line number where this exception occurred.
+ */
+ @Override
+ public int getLineNumber()
+ {
+ if ((currentToken != null) && (currentToken.next != null))
+ {
+ return currentToken.next.beginLine;
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ /**
+ * returns the column number where this exception occurred.
+ * @return The column number where this exception occurred.
+ */
+ @Override
+ public int getColumnNumber()
+ {
+ if ((currentToken != null) && (currentToken.next != null))
+ {
+ return currentToken.next.beginColumn;
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ /**
+ * This method has the standard behavior when this object has been
+ * created using the standard constructors. Otherwise, it uses
+ * "currentToken" and "expectedTokenSequences" to generate a parse
+ * error message and returns it. If this object has been created
+ * due to a parse error, and you do not catch it (it gets thrown
+ * from the parser), then this method is called during the printing
+ * of the final stack trace, and hence the correct error message
+ * gets displayed.
+ * @return The error message.
+ */
+ @Override
+ public String getMessage()
+ {
+ if (!specialConstructor)
+ {
+ StringBuilder sb = new StringBuilder(super.getMessage());
+ appendTemplateInfo(sb);
+ return sb.toString();
+ }
+
+ int maxSize = 0;
+
+ StringBuilder expected = new StringBuilder();
+
+ for (int[] expectedTokenSequence : expectedTokenSequences)
+ {
+ if (maxSize < expectedTokenSequence.length)
+ {
+ maxSize = expectedTokenSequence.length;
+ }
+
+ for (int i : expectedTokenSequence)
+ {
+ expected.append(tokenImage[i]).append(" ");
+ }
+
+ if (expectedTokenSequence[expectedTokenSequence.length - 1] != 0)
+ {
+ expected.append("...");
+ }
+
+ expected.append(eol).append(" ");
+ }
+
+ StringBuilder retval = new StringBuilder("Encountered \"");
+ Token tok = currentToken.next;
+
+ for (int i = 0; i < maxSize; i++)
+ {
+ if (i != 0)
+ {
+ retval.append(" ");
+ }
+
+ if (tok.kind == 0)
+ {
+ retval.append(tokenImage[0]);
+ break;
+ }
+
+ retval.append(add_escapes(tok.image));
+ tok = tok.next;
+ }
+
+ retval.append("\" at ");
+ appendTemplateInfo(retval);
+
+ if (expectedTokenSequences.length == 1)
+ {
+ retval.append("Was expecting:").append(eol).append(" ");
+ }
+ else
+ {
+ retval.append("Was expecting one of:").append(eol).append(" ");
+ }
+
+ // avoid JDK 1.3 StringBuffer.append(Object o) vs 1.4 StringBuffer.append(StringBuffer sb) gotcha.
+ retval.append(expected.toString());
+ return retval.toString();
+ }
+
+ /**
+ * @param sb
+ */
+ protected void appendTemplateInfo(final StringBuilder sb)
+ {
+ sb.append(StringUtils.formatFileString(getTemplateName(), getLineNumber(), getColumnNumber()));
+ sb.append(eol);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/VelocityCharStream.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/VelocityCharStream.java
new file mode 100644
index 00000000..c5133749
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/VelocityCharStream.java
@@ -0,0 +1,526 @@
+package org.apache.velocity.runtime.parser;
+
+/*
+ * 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.
+ */
+
+/**
+ * <p>An implementation of interface CharStream, where the stream is assumed to
+ * contain only ASCII characters (without unicode processing).</p>
+ *
+ * <p>NOTE : This class was originally an ASCII_CharStream autogenerated
+ * by Javacc. It was then modified via changing class name with appropriate
+ * fixes for CTORS, and mods to readChar().</p>
+ *
+ * <p>This is safe because we *always* use Reader with this class, and never a
+ * InputStream. This guarantees that we have a correct stream of 16-bit
+ * chars - all encoding transformations have been done elsewhere, so we
+ * believe that there is no risk in doing this. Time will tell :)</p>
+ *
+ */
+public final class VelocityCharStream
+implements CharStream
+{
+ public static final boolean staticFlag = false;
+ int bufsize;
+ private int nextBufExpand;
+ int available;
+ int tokenBegin;
+
+ public int bufpos = -1;
+ private int bufline[];
+ private int bufcolumn[];
+
+ private int column = 0;
+ private int line = 1;
+
+ private boolean prevCharIsCR = false;
+ private boolean prevCharIsLF = false;
+
+ private java.io.Reader inputStream;
+
+ private char[] buffer;
+ private int maxNextCharInd = 0;
+ private int inBuf = 0;
+
+ /* CB - to properly handle EOF *inside* javacc lexer,
+ * we send a 'file separator' ascii char *just before* EOF
+ * (see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text)
+ */
+ private boolean beforeEOF = false;
+ private static char END_OF_FILE = '\u001C';
+
+ private void ExpandBuff(boolean wrapAround)
+ {
+ char[] newbuffer = new char[bufsize + nextBufExpand];
+ int newbufline[] = new int[bufsize + nextBufExpand];
+ int newbufcolumn[] = new int[bufsize + nextBufExpand];
+
+ try
+ {
+ if (wrapAround)
+ {
+ System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin);
+ System.arraycopy(buffer, 0, newbuffer,
+ bufsize - tokenBegin, bufpos);
+ buffer = newbuffer;
+
+ System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin);
+ System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos);
+ bufline = newbufline;
+
+ System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin);
+ System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos);
+ bufcolumn = newbufcolumn;
+
+ maxNextCharInd = (bufpos += (bufsize - tokenBegin));
+ }
+ else
+ {
+ System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin);
+ buffer = newbuffer;
+
+ System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin);
+ bufline = newbufline;
+
+ System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin);
+ bufcolumn = newbufcolumn;
+
+ maxNextCharInd = (bufpos -= tokenBegin);
+ }
+ }
+ catch (Throwable t)
+ {
+ throw new Error(t.getMessage());
+ }
+
+
+ bufsize += nextBufExpand;
+ nextBufExpand = bufsize;
+ available = bufsize;
+ tokenBegin = 0;
+ }
+
+ private void FillBuff() throws java.io.IOException
+ {
+ if (maxNextCharInd == available)
+ {
+ if (available == bufsize)
+ {
+ if (tokenBegin > nextBufExpand)
+ {
+ bufpos = maxNextCharInd = 0;
+ available = tokenBegin;
+ }
+ else if (tokenBegin < 0)
+ {
+ bufpos = maxNextCharInd = 0;
+ }
+ else
+ {
+ ExpandBuff(false);
+ }
+ }
+ else if (available > tokenBegin)
+ {
+ available = bufsize;
+ }
+ else if ((tokenBegin - available) < nextBufExpand)
+ {
+ ExpandBuff(true);
+ }
+ else
+ {
+ available = tokenBegin;
+ }
+ }
+
+ int i;
+ try
+ {
+ if ((i = inputStream.read(buffer, maxNextCharInd,
+ available - maxNextCharInd)) == -1)
+ {
+ if (beforeEOF)
+ {
+ inputStream.close();
+ throw new java.io.IOException();
+ }
+ buffer[maxNextCharInd++] = END_OF_FILE;
+ beforeEOF = true;
+ }
+ else
+ {
+ maxNextCharInd += i;
+ }
+ }
+ catch(java.io.IOException e)
+ {
+ --bufpos;
+ backup(0);
+ if (tokenBegin == -1)
+ {
+ tokenBegin = bufpos;
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.CharStream#BeginToken()
+ */
+ @Override
+ public final char BeginToken() throws java.io.IOException
+ {
+ tokenBegin = -1;
+ char c = readChar();
+ tokenBegin = bufpos;
+
+ return c;
+ }
+
+ private void UpdateLineColumn(char c)
+ {
+ column++;
+
+ if (prevCharIsLF)
+ {
+ prevCharIsLF = false;
+ line += (column = 1);
+ }
+ else if (prevCharIsCR)
+ {
+ prevCharIsCR = false;
+ if (c == '\n')
+ {
+ prevCharIsLF = true;
+ }
+ else
+ {
+ line += (column = 1);
+ }
+ }
+
+ switch (c)
+ {
+ case '\r' :
+ prevCharIsCR = true;
+ break;
+ case '\n' :
+ prevCharIsLF = true;
+ break;
+ case '\t' :
+ column--;
+ column += (8 - (column & 07));
+ break;
+ default :
+ break;
+ }
+
+ bufline[bufpos] = line;
+ bufcolumn[bufpos] = column;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.CharStream#readChar()
+ */
+ @Override
+ public final char readChar() throws java.io.IOException
+ {
+ if (inBuf > 0)
+ {
+ --inBuf;
+
+ /*
+ * was : return (char)((char)0xff & buffer[(bufpos == bufsize - 1) ? (bufpos = 0) : ++bufpos]);
+ */
+ return buffer[(bufpos == bufsize - 1) ? (bufpos = 0) : ++bufpos];
+ }
+
+ if (++bufpos >= maxNextCharInd)
+ {
+ FillBuff();
+ }
+
+ /*
+ * was : char c = (char)((char)0xff & buffer[bufpos]);
+ */
+ char c = buffer[bufpos];
+
+ UpdateLineColumn(c);
+ return (c);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.CharStream#getEndColumn()
+ */
+ @Override
+ public final int getEndColumn()
+ {
+ return bufcolumn[bufpos];
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.CharStream#getEndLine()
+ */
+ @Override
+ public final int getEndLine()
+ {
+ return bufline[bufpos];
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.CharStream#getBeginColumn()
+ */
+ @Override
+ public final int getBeginColumn()
+ {
+ return bufcolumn[tokenBegin];
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.CharStream#getBeginLine()
+ */
+ @Override
+ public final int getBeginLine()
+ {
+ return bufline[tokenBegin];
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.CharStream#backup(int)
+ */
+ @Override
+ public final void backup(int amount)
+ {
+
+ inBuf += amount;
+ if ((bufpos -= amount) < 0)
+ bufpos += bufsize;
+ }
+
+ /**
+ * @param dstream
+ * @param startline
+ * @param startcolumn
+ * @param buffersize
+ */
+ public VelocityCharStream(java.io.Reader dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ inputStream = dstream;
+ line = startline;
+ column = startcolumn - 1;
+
+ available = bufsize = nextBufExpand = buffersize;
+ buffer = new char[buffersize];
+ bufline = new int[buffersize];
+ bufcolumn = new int[buffersize];
+ }
+
+ /**
+ * @param dstream
+ * @param startline
+ * @param startcolumn
+ */
+ public VelocityCharStream(java.io.Reader dstream, int startline,
+ int startcolumn)
+ {
+ this(dstream, startline, startcolumn, 4096);
+ }
+ /**
+ * @param dstream
+ * @param startline
+ * @param startcolumn
+ * @param buffersize
+ */
+ public void ReInit(java.io.Reader dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ inputStream = dstream;
+ line = startline;
+ column = startcolumn - 1;
+
+ if (buffer == null || buffersize != buffer.length)
+ {
+ available = bufsize = nextBufExpand = buffersize;
+ buffer = new char[buffersize];
+ bufline = new int[buffersize];
+ bufcolumn = new int[buffersize];
+ }
+ prevCharIsLF = prevCharIsCR = false;
+ tokenBegin = inBuf = maxNextCharInd = 0;
+ bufpos = -1;
+ beforeEOF = false;
+ }
+
+ /**
+ * @param dstream
+ * @param startline
+ * @param startcolumn
+ */
+ public void ReInit(java.io.Reader dstream, int startline,
+ int startcolumn)
+ {
+ ReInit(dstream, startline, startcolumn, 4096);
+ }
+ /**
+ * @param dstream
+ * @param startline
+ * @param startcolumn
+ * @param buffersize
+ */
+ public VelocityCharStream(java.io.InputStream dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize);
+ }
+
+ /**
+ * @param dstream
+ * @param startline
+ * @param startcolumn
+ */
+ public VelocityCharStream(java.io.InputStream dstream, int startline,
+ int startcolumn)
+ {
+ this(dstream, startline, startcolumn, 4096);
+ }
+
+ /**
+ * @param dstream
+ * @param startline
+ * @param startcolumn
+ * @param buffersize
+ */
+ public void ReInit(java.io.InputStream dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize);
+ }
+ /**
+ * @param dstream
+ * @param startline
+ * @param startcolumn
+ */
+ public void ReInit(java.io.InputStream dstream, int startline,
+ int startcolumn)
+ {
+ ReInit(dstream, startline, startcolumn, 4096);
+ }
+ /**
+ * @see org.apache.velocity.runtime.parser.CharStream#GetImage()
+ */
+ @Override
+ public final String GetImage()
+ {
+ if (bufpos >= tokenBegin)
+ {
+ return new String(buffer, tokenBegin, bufpos - tokenBegin + 1);
+ }
+ else
+ {
+ return new String(buffer, tokenBegin, bufsize - tokenBegin) +
+ new String(buffer, 0, bufpos + 1);
+ }
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.CharStream#GetSuffix(int)
+ */
+ @Override
+ public final char[] GetSuffix(int len)
+ {
+ char[] ret = new char[len];
+
+ if ((bufpos + 1) >= len)
+ {
+ System.arraycopy(buffer, bufpos - len + 1, ret, 0, len);
+ }
+ else
+ {
+ System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0,
+ len - bufpos - 1);
+ System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1);
+ }
+
+ return ret;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.CharStream#Done()
+ */
+ @Override
+ public void Done()
+ {
+ buffer = null;
+ bufline = null;
+ bufcolumn = null;
+ }
+
+ /**
+ * Method to adjust line and column numbers for the start of a token.<BR>
+ * @param newLine
+ * @param newCol
+ */
+ public void adjustBeginLineColumn(int newLine, int newCol)
+ {
+ int start = tokenBegin;
+ int len;
+
+ if (bufpos >= tokenBegin)
+ {
+ len = bufpos - tokenBegin + inBuf + 1;
+ }
+ else
+ {
+ len = bufsize - tokenBegin + bufpos + 1 + inBuf;
+ }
+
+ int i = 0, j = 0, k = 0;
+ int nextColDiff = 0, columnDiff = 0;
+
+ while (i < len &&
+ bufline[j = start % bufsize] == bufline[k = ++start % bufsize])
+ {
+ bufline[j] = newLine;
+ nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j];
+ bufcolumn[j] = newCol + columnDiff;
+ columnDiff = nextColDiff;
+ i++;
+ }
+
+ if (i < len)
+ {
+ bufline[j] = newLine++;
+ bufcolumn[j] = newCol + columnDiff;
+
+ while (i++ < len)
+ {
+ if (bufline[j = start % bufsize] != bufline[++start % bufsize])
+ bufline[j] = newLine++;
+ else
+ bufline[j] = newLine;
+ }
+ }
+
+ line = bufline[j];
+ column = bufcolumn[j];
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTAddNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTAddNode.java
new file mode 100644
index 00000000..119c4865
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTAddNode.java
@@ -0,0 +1,90 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.util.DuckType;
+
+/**
+ * Handles number addition of nodes.<br><br>
+ *
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @author <a href="mailto:pero@antaramusic.de">Peter Romianowski</a>
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ASTAddNode extends ASTMathNode
+{
+ /**
+ * @param id
+ */
+ public ASTAddNode(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTAddNode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ @Override
+ protected Object handleSpecial(Object left, Object right, InternalContextAdapter context)
+ {
+ // check for strings, but don't coerce
+ String lstr = DuckType.asString(left, false);
+ String rstr = DuckType.asString(right, false);
+ if (lstr != null || rstr != null)
+ {
+ if (lstr == null)
+ {
+ lstr = left != null ? left.toString() : jjtGetChild(0).literal();
+ }
+ else if (rstr == null)
+ {
+ rstr = right != null ? right.toString() : jjtGetChild(1).literal();
+ }
+ return lstr.concat(rstr);
+ }
+ return null;
+ }
+
+ @Override
+ public String getLiteralOperator()
+ {
+ return "+";
+ }
+
+ @Override
+ public Number perform(Number left, Number right, InternalContextAdapter context)
+ {
+ return MathUtils.add(left, right);
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTAndNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTAndNode.java
new file mode 100644
index 00000000..2338d40f
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTAndNode.java
@@ -0,0 +1,123 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ASTAndNode extends ASTLogicalOperator
+{
+ /**
+ * @param id
+ */
+ public ASTAndNode(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTAndNode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ @Override
+ public String getLiteralOperator()
+ {
+ return "&&";
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * Returns the value of the expression.
+ * Since the value of the expression is simply the boolean
+ * result of evaluate(), lets return that.
+ * @param context
+ * @return The value of the expression.
+ * @throws MethodInvocationException
+ */
+ @Override
+ public Object value(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ return evaluate(context) ? Boolean.TRUE : Boolean.FALSE;
+ }
+
+ /**
+ * logical and :
+ * <pre>
+ * null &amp;&amp; right = false
+ * left &amp;&amp; null = false
+ * null &amp;&amp; null = false
+ * </pre>
+ * @param context
+ * @return True if both sides are true.
+ * @throws MethodInvocationException
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ Node left = jjtGetChild(0);
+ Node right = jjtGetChild(1);
+
+ /*
+ * null == false
+ */
+ if (left == null || right == null)
+ {
+ return false;
+ }
+
+ /*
+ * short circuit the test. Don't eval the RHS if the LHS is false
+ */
+
+ if( left.evaluate( context ) )
+ {
+ if ( right.evaluate( context ) )
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTAssignment.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTAssignment.java
new file mode 100644
index 00000000..2cc2973d
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTAssignment.java
@@ -0,0 +1,69 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ *
+ */
+public class ASTAssignment extends SimpleNode
+{
+ /**
+ * @param id
+ */
+ public ASTAssignment(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTAssignment(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTBinaryOperator.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTBinaryOperator.java
new file mode 100644
index 00000000..8b776eaf
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTBinaryOperator.java
@@ -0,0 +1,47 @@
+package org.apache.velocity.runtime.parser.node;
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+public abstract class ASTBinaryOperator extends SimpleNode
+{
+ public ASTBinaryOperator(int id)
+ {
+ super(id);
+ }
+
+ public ASTBinaryOperator(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+ /**
+ * get the string representing the mathematical operator
+ * @return operator string
+ */
+ public abstract String getLiteralOperator();
+
+ @Override
+ public String literal()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append(jjtGetChild(0).literal());
+ builder.append(' ');
+ builder.append(getLiteralOperator());
+ builder.append(' ');
+ return builder.toString();
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTBlock.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTBlock.java
new file mode 100644
index 00000000..c8358ed5
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTBlock.java
@@ -0,0 +1,158 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeConstants.SpaceGobbling;
+import org.apache.velocity.runtime.parser.Parser;
+
+import java.io.IOException;
+import java.io.Writer;
+
+
+/**
+ *
+ */
+public class ASTBlock extends SimpleNode
+{
+ private String prefix = "";
+ private String postfix = "";
+
+ // used during parsing
+ public boolean endsWithNewline = false;
+
+ /*
+ * '#' and '$' prefix characters eaten by javacc MORE mode, prefixing the '#' ending the block
+ */
+ private String morePostfix = "";
+
+ /**
+ * @param id
+ */
+ public ASTBlock(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTBlock(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+ /**
+ * set indentation prefix
+ * @param prefix
+ */
+ public void setPrefix(String prefix)
+ {
+ this.prefix = prefix;
+ }
+
+ /**
+ * get indentation prefix
+ * @return indentation prefix
+ */
+ public String getPrefix()
+ {
+ return prefix;
+ }
+
+ public void setMorePostfix(String morePosffix)
+ {
+ this.morePostfix = morePosffix;
+ }
+
+ /**
+ * set indentation postfix
+ * @param postfix
+ */
+ public void setPostfix(String postfix)
+ {
+ this.postfix = postfix;
+ }
+
+ /**
+ * get indentation postfix
+ * @return indentation prefix
+ */
+ public String getPostfix()
+ {
+ return postfix;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#render(org.apache.velocity.context.InternalContextAdapter, java.io.Writer)
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer)
+ throws IOException, MethodInvocationException,
+ ResourceNotFoundException, ParseErrorException
+ {
+ SpaceGobbling spaceGobbling = rsvc.getSpaceGobbling();
+
+ if (spaceGobbling == SpaceGobbling.NONE)
+ {
+ writer.write(prefix);
+ }
+
+ int i, k = jjtGetNumChildren();
+
+ for (i = 0; i < k; i++)
+ jjtGetChild(i).render(context, writer);
+
+ if (morePostfix.length() > 0 || spaceGobbling.compareTo(SpaceGobbling.LINES) < 0)
+ {
+ writer.write(postfix);
+ }
+
+ writer.write(morePostfix);
+
+ return true;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTComment.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTComment.java
new file mode 100644
index 00000000..b2d3ba2a
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTComment.java
@@ -0,0 +1,109 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.runtime.parser.Token;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Represents all comments...
+ *
+ * @author <a href="mailto:geirm@apache.org">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ASTComment extends SimpleNode
+{
+ private static final char[] ZILCH = "".toCharArray();
+
+ private char[] carr;
+
+ /**
+ * @param id
+ */
+ public ASTComment(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTComment(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * We need to make sure we catch any of the dreaded MORE tokens.
+ * @param context
+ * @param data
+ * @return The data object.
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data)
+ {
+ Token t = getFirstToken();
+
+ int loc1 = t.image.indexOf(parser.lineComment());
+ int loc2 = t.image.indexOf(parser.blockComment());
+
+ if (loc1 == -1 && loc2 == -1)
+ {
+ carr = ZILCH;
+ }
+ else
+ {
+ carr = t.image.substring(0, (loc1 == -1) ? loc2 : loc1).toCharArray();
+ }
+
+ cleanupParserAndTokens();
+
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#render(org.apache.velocity.context.InternalContextAdapter, java.io.Writer)
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer)
+ throws IOException, MethodInvocationException, ParseErrorException, ResourceNotFoundException
+ {
+ writer.write(carr);
+ return true;
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTComparisonNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTComparisonNode.java
new file mode 100644
index 00000000..721aa23d
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTComparisonNode.java
@@ -0,0 +1,177 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.util.DuckType;
+import org.apache.velocity.util.StringUtils;
+
+/**
+ * Numeric comparison support<br><br>
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @author <a href="mailto:pero@antaramusic.de">Peter Romianowski</a>
+ * @author Nathan Bubna
+ */
+public abstract class ASTComparisonNode extends ASTBinaryOperator
+{
+ /**
+ * @param id
+ */
+ public ASTComparisonNode(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTComparisonNode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#evaluate(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context) throws MethodInvocationException
+ {
+ Object left = jjtGetChild(0).value(context);
+ Object right = jjtGetChild(1).value(context);
+
+ if (left == null || right == null)
+ {
+ return compareNull(left, right);
+ }
+ Boolean result = compareNumbers(left, right);
+ if (result == null)
+ {
+ result = compareNonNumber(left, right);
+ }
+ return result;
+ }
+
+ /**
+ * Always false by default, != and == subclasses must override this.
+ * @param left
+ * @param right
+ * @return comparison result
+ */
+ public boolean compareNull(Object left, Object right)
+ {
+ // if either side is null, log and bail
+ String msg = (left == null ? "Left" : "Right")
+ + " side ("
+ + jjtGetChild( (left == null? 0 : 1) ).literal()
+ + ") of comparison operation has null value at "
+ + StringUtils.formatFileString(this);
+ if (rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false))
+ {
+ throw new VelocityException(msg, null, rsvc.getLogContext().getStackTrace());
+ }
+ log.error(msg);
+ return false;
+ }
+
+ /**
+ * compare numbers
+ * @param left
+ * @param right
+ * @return comparison result
+ */
+ public Boolean compareNumbers(Object left, Object right)
+ {
+ try
+ {
+ left = DuckType.asNumber(left);
+ }
+ catch (NumberFormatException nfe) {}
+ try
+ {
+ right = DuckType.asNumber(right);
+ }
+ catch (NumberFormatException nfe) {}
+
+ // only compare Numbers
+ if (left instanceof Number && right instanceof Number)
+ {
+ return numberTest(MathUtils.compare((Number)left, (Number)right));
+ }
+ return null;
+ }
+
+ /**
+ * get the string representing the mathematical operator
+ * @return operator string
+ */
+ @Override
+ public abstract String getLiteralOperator();
+
+ /**
+ * performs the actual comparison
+ * @param compareResult
+ * @return comparison result
+ */
+ public abstract boolean numberTest(int compareResult);
+
+ public boolean compareNonNumber(Object left, Object right)
+ {
+ // by default, log and bail
+ String msg = (right instanceof Number ? "Left" : "Right")
+ + " side of comparison operation is not a number at "
+ + StringUtils.formatFileString(this);
+ if (rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false))
+ {
+ throw new VelocityException(msg, null, rsvc.getLogContext().getStackTrace());
+ }
+ log.error(msg);
+ return false;
+ }
+
+ private String getLiteral(boolean left)
+ {
+ return jjtGetChild(left ? 0 : 1).literal();
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public Object value(InternalContextAdapter context) throws MethodInvocationException
+ {
+ return evaluate(context);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTDirective.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTDirective.java
new file mode 100644
index 00000000..925edf45
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTDirective.java
@@ -0,0 +1,372 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants.SpaceGobbling;
+import org.apache.velocity.runtime.directive.BlockMacro;
+import org.apache.velocity.runtime.directive.Directive;
+import org.apache.velocity.runtime.directive.RuntimeMacro;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.runtime.parser.StandardParserConstants;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.util.introspection.Info;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * This class is responsible for handling the pluggable
+ * directives in VTL.
+ *
+ * For example : #foreach()
+ *
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:kav@kav.dk">Kasper Nielsen</a>
+ * @version $Id$
+ */
+public class ASTDirective extends SimpleNode
+{
+ private Directive directive = null;
+ private String directiveName = "";
+ private boolean isDirective;
+ private boolean isInitialized;
+
+ private String prefix = "";
+ private String postfix = "";
+
+ /*
+ * '#' and '$' prefix characters eaten by javacc MORE mode
+ */
+ private String morePrefix = "";
+
+ /**
+ * @param id
+ */
+ public ASTDirective(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTDirective(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public synchronized Object init(InternalContextAdapter context, Object data)
+ throws TemplateInitException
+ {
+ Token t;
+
+ /* method is synchronized to avoid concurrent directive initialization **/
+
+ if (!isInitialized)
+ {
+ super.init( context, data );
+
+ /*
+ * handle '$' and '#' chars prefix
+ */
+ t = getFirstToken();
+ int pos = -1;
+ while (t != null && (pos = t.image.lastIndexOf(rsvc.getParserConfiguration().getHashChar())) == -1)
+ {
+ t = t.next;
+ }
+ if (t != null && pos > 0)
+ {
+ morePrefix = t.image.substring(0, pos);
+ }
+
+ /*
+ * only do things that are not context dependent
+ */
+
+ if (parser.isDirective( directiveName ))
+ {
+ isDirective = true;
+
+ try
+ {
+ directive = parser.getDirective( directiveName )
+ .getClass().newInstance();
+ }
+ catch (InstantiationException | IllegalAccessException e)
+ {
+ throw new VelocityException(
+ "Couldn't initialize directive of class " +
+ parser.getDirective(directiveName).getClass().getName(),
+ e, rsvc.getLogContext().getStackTrace());
+ }
+
+ t = getFirstToken();
+ if (t.kind == StandardParserConstants.WHITESPACE) t = t.next;
+ directive.setLocation(t.beginLine, t.beginColumn, getTemplate());
+ directive.init(rsvc, context, this);
+ }
+ else if( directiveName.startsWith(String.valueOf(rsvc.getParserConfiguration().getAtChar())) )
+ {
+ if( this.jjtGetNumChildren() > 0 )
+ {
+ // block macro call (normal macro call but has AST body)
+ directiveName = directiveName.substring(1);
+
+ directive = new BlockMacro();
+ directive.setLocation(getLine(), getColumn(), getTemplate());
+
+ try
+ {
+ ((BlockMacro)directive).init( rsvc, directiveName, context, this );
+ }
+ catch (TemplateInitException die)
+ {
+ throw new TemplateInitException(die.getMessage(),
+ (ParseException) die.getCause(),
+ rsvc.getLogContext().getStackTrace(),
+ die.getTemplateName(),
+ die.getColumnNumber() + getColumn(),
+ die.getLineNumber() + getLine());
+ }
+ isDirective = true;
+ }
+ else
+ {
+ // this is a fake block macro call without a body. e.g. #@foo
+ // just render as it is
+ isDirective = false;
+ }
+ }
+ else
+ {
+ /*
+ Create a new RuntimeMacro
+ */
+ directive = new RuntimeMacro();
+ directive.setLocation(getLine(), getColumn(), getTemplate());
+
+ /*
+ Initialize it
+ */
+ try
+ {
+ ((RuntimeMacro)directive).init( rsvc, directiveName, context, this );
+ }
+
+ /*
+ correct the line/column number if an exception is caught
+ */
+ catch (TemplateInitException die)
+ {
+ throw new TemplateInitException(die.getMessage(),
+ (ParseException) die.getCause(),
+ rsvc.getLogContext().getStackTrace(),
+ die.getTemplateName(),
+ die.getColumnNumber() + getColumn(),
+ die.getLineNumber() + getLine());
+ }
+ isDirective = true;
+
+ }
+
+ isInitialized = true;
+
+ saveTokenImages();
+ cleanupParserAndTokens();
+ }
+
+ if (morePrefix.length() == 0 && rsvc.getSpaceGobbling() == SpaceGobbling.STRUCTURED && isInitialized && isDirective && directive.getType() == Directive.BLOCK)
+ {
+ NodeUtils.fixIndentation(this, prefix);
+ }
+
+ return data;
+ }
+
+ /**
+ * set indentation prefix
+ * @param prefix
+ */
+ public void setPrefix(String prefix)
+ {
+ this.prefix = prefix;
+ }
+
+ /**
+ * get indentation prefix
+ * @return indentation prefix
+ */
+ public String getPrefix()
+ {
+ return prefix;
+ }
+
+ /**
+ * set indentation postfix
+ * @param postfix
+ */
+ public void setPostfix(String postfix)
+ {
+ this.postfix = postfix;
+ }
+
+ /**
+ * get indentation postfix
+ * @return indentation prefix
+ */
+ public String getPostfix()
+ {
+ return postfix;
+ }
+
+ /**
+ * more prefix getter
+ * @return more prefix
+ */
+ public String getMorePrefix()
+ {
+ return morePrefix;
+ }
+
+ public int getDirectiveType()
+ {
+ return directive.getType();
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#render(org.apache.velocity.context.InternalContextAdapter, java.io.Writer)
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer)
+ throws IOException,MethodInvocationException, ResourceNotFoundException, ParseErrorException
+ {
+ SpaceGobbling spaceGobbling = rsvc.getSpaceGobbling();
+ /*
+ * normal processing
+ */
+
+ if (isDirective)
+ {
+ if (morePrefix.length() > 0 || spaceGobbling.compareTo(SpaceGobbling.LINES) < 0)
+ {
+ writer.write(prefix);
+ }
+
+ writer.write(morePrefix);
+
+ try
+ {
+ rsvc.getLogContext().pushLogContext(this, new Info(getTemplateName(), getLine(), getColumn()));
+ directive.render(context, writer, this);
+ }
+ finally
+ {
+ rsvc.getLogContext().popLogContext();
+ }
+
+ if (morePrefix.length() > 0 || spaceGobbling == SpaceGobbling.NONE)
+ {
+ writer.write(postfix);
+ }
+ }
+ else
+ {
+ writer.write(prefix);
+ writer.write(morePrefix);
+ writer.write(rsvc.getParserConfiguration().getHashChar());
+ writer.write(directiveName);
+ writer.write(postfix);
+ }
+
+ return true;
+ }
+
+ /**
+ * Sets the directive name. Used by the parser. This keeps us from having to
+ * dig it out of the token stream and gives the parse the change to override.
+ * @param str
+ */
+ public void setDirectiveName( String str )
+ {
+ directiveName = str;
+ }
+
+ /**
+ * Gets the name of this directive.
+ * @return The name of this directive.
+ */
+ public String getDirectiveName()
+ {
+ return directiveName;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "ASTDirective [" + super.toString() + ", directiveName="
+ + directiveName + "]";
+ }
+
+ /**
+ * Returns the string "#<i>directive_name</i>(...)". Arguments literals are not rendered. This method is only
+ * used for displaying the VTL stacktrace when a rendering error is encountered when runtime.log.track_location is true.
+ * @return
+ */
+ @Override
+ public String literal()
+ {
+ if (literal != null)
+ {
+ return literal;
+ }
+ StringBuilder builder = new StringBuilder();
+ builder.append('#').append(getDirectiveName()).append("(...)");
+
+ return literal = builder.toString();
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTDirectiveAssign.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTDirectiveAssign.java
new file mode 100644
index 00000000..66f74266
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTDirectiveAssign.java
@@ -0,0 +1,53 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+public class ASTDirectiveAssign extends SimpleNode
+{
+
+ public ASTDirectiveAssign(int i)
+ {
+ super(i);
+ }
+
+ public ASTDirectiveAssign(Parser p, int i)
+ {
+ super(p, i);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ saveTokenImages();
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTDivNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTDivNode.java
new file mode 100644
index 00000000..e070a78a
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTDivNode.java
@@ -0,0 +1,86 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MathException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ * Handles number division of nodes<br><br>
+ *
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @author <a href="mailto:pero@antaramusic.de">Peter Romianowski</a>
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ASTDivNode extends ASTMathNode
+{
+ /**
+ * @param id
+ */
+ public ASTDivNode(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTDivNode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ @Override
+ public String getLiteralOperator()
+ {
+ return "/";
+ }
+
+ @Override
+ public Number perform(Number left, Number right, InternalContextAdapter context)
+ {
+ /*
+ * check for divide by 0
+ */
+ if (MathUtils.isZero(right))
+ {
+ String msg = "Right side of division operation is zero. Must be non-zero. "
+ + getLocation(context);
+ if (strictMode)
+ {
+ log.error(msg);
+ throw new MathException(msg, rsvc.getLogContext().getStackTrace());
+ }
+ else
+ {
+ log.debug(msg);
+ return null;
+ }
+ }
+ return MathUtils.divide(left, right);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTEQNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTEQNode.java
new file mode 100644
index 00000000..5a2411bb
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTEQNode.java
@@ -0,0 +1,96 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.util.DuckType;
+
+/**
+ * Handles <code>arg1 == arg2</code>
+ *
+ * This operator requires that the LHS and RHS are both of the
+ * same Class, both numbers or both coerce-able to strings.
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @author <a href="mailto:pero@antaramusic.de">Peter Romianowski</a>
+ * @author Nathan Bubna
+ */
+public class ASTEQNode extends ASTComparisonNode
+{
+ public ASTEQNode(int id)
+ {
+ super(id);
+ }
+
+ public ASTEQNode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ @Override
+ public boolean compareNull(Object left, Object right)
+ {
+ // at least one is null, see if other is null or acts as a null
+ return left == right || DuckType.asNull(left == null ? right : left);
+ }
+
+ @Override
+ public String getLiteralOperator()
+ {
+ return "==";
+ }
+
+ @Override
+ public boolean numberTest(int compareResult)
+ {
+ return compareResult == 0;
+ }
+
+ @Override
+ public boolean compareNonNumber(Object left, Object right)
+ {
+ /*
+ * if both are not null, then assume that if one class
+ * is a subclass of the other that we should use the equals operator
+ */
+ if (left.getClass().isAssignableFrom(right.getClass()) ||
+ right.getClass().isAssignableFrom(left.getClass()))
+ {
+ return left.equals(right);
+ }
+
+ // coerce to string, remember getAsString() methods may return null
+ left = DuckType.asString(left);
+ right = DuckType.asString(right);
+ if (left == right)
+ {
+ return true;
+ }
+ else if (left == null || right == null)
+ {
+ return false;
+ }
+ else
+ {
+ return left.equals(right);
+ }
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTElseIfStatement.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTElseIfStatement.java
new file mode 100644
index 00000000..78334c33
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTElseIfStatement.java
@@ -0,0 +1,110 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * This class is responsible for handling the ElseIf VTL control statement.
+ *
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+*/
+public class ASTElseIfStatement extends SimpleNode
+{
+ /**
+ * @param id
+ */
+ public ASTElseIfStatement(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTElseIfStatement(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * An ASTElseStatement is true if the expression
+ * it contains evaluates to true. Expressions know
+ * how to evaluate themselves, so we do that
+ * here and return the value back to ASTIfStatement
+ * where this node was originally asked to evaluate
+ * itself.
+ * @param context
+ * @return True if all children are true.
+ * @throws MethodInvocationException
+ */
+ @Override
+ public boolean evaluate (InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ return jjtGetChild(0).evaluate(context);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#render(org.apache.velocity.context.InternalContextAdapter, java.io.Writer)
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer)
+ throws IOException,MethodInvocationException,
+ ResourceNotFoundException, ParseErrorException
+ {
+ return jjtGetChild(1).render( context, writer );
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTElseStatement.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTElseStatement.java
new file mode 100644
index 00000000..378501cd
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTElseStatement.java
@@ -0,0 +1,87 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ * This class is responsible for handling the Else VTL control statement.
+ *
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ASTElseStatement extends SimpleNode
+{
+ /**
+ * @param id
+ */
+ public ASTElseStatement(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTElseStatement(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * An ASTElseStatement always evaluates to
+ * true. Basically behaves like an #if(true).
+ * @param context
+ * @return Always true.
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ {
+ return true;
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTEscape.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTEscape.java
new file mode 100644
index 00000000..67539ec0
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTEscape.java
@@ -0,0 +1,90 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.runtime.parser.Parser;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * This class is responsible for handling Escapes
+ * in VTL.
+ *
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ASTEscape extends SimpleNode
+{
+ /** Used by the parser */
+ public String val;
+ private char[] ctext;
+
+ /**
+ * @param id
+ */
+ public ASTEscape(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTEscape(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data)
+ {
+ ctext = val.toCharArray();
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#render(org.apache.velocity.context.InternalContextAdapter, java.io.Writer)
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer)
+ throws IOException
+ {
+ writer.write(ctext);
+ return true;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTEscapedDirective.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTEscapedDirective.java
new file mode 100644
index 00000000..3a7ba3a7
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTEscapedDirective.java
@@ -0,0 +1,91 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * This class is responsible for handling EscapedDirectives
+ * in VTL.
+ *
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ASTEscapedDirective extends SimpleNode
+{
+ /**
+ * @param id
+ */
+ public ASTEscapedDirective(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTEscapedDirective(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#render(org.apache.velocity.context.InternalContextAdapter, java.io.Writer)
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer)
+ throws IOException
+ {
+ writer.write(firstImage);
+ return true;
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ saveTokenImages();
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTExpression.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTExpression.java
new file mode 100644
index 00000000..6b11698a
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTExpression.java
@@ -0,0 +1,96 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ *
+ */
+public class ASTExpression extends SimpleNode
+{
+ /**
+ * @param id
+ */
+ public ASTExpression(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTExpression(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#evaluate(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ return jjtGetChild(0).evaluate(context);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public Object value(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ return jjtGetChild(0).value(context);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ saveTokenImages();
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+ @Override
+ public String literal()
+ {
+ return jjtGetChild(0).literal();
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTFalse.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTFalse.java
new file mode 100644
index 00000000..aed9d6f5
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTFalse.java
@@ -0,0 +1,88 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ *
+ */
+public class ASTFalse extends SimpleNode
+{
+ private static Boolean value = Boolean.FALSE;
+
+ /**
+ * @param id
+ */
+ public ASTFalse(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTFalse(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#evaluate(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ {
+ return false;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public Object value(InternalContextAdapter context)
+ {
+ return value;
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTFloatingPointLiteral.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTFloatingPointLiteral.java
new file mode 100644
index 00000000..1f13c30a
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTFloatingPointLiteral.java
@@ -0,0 +1,126 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.parser.Parser;
+
+import java.math.BigDecimal;
+
+
+/**
+ * Handles floating point numbers. The value will be either a Double
+ * or a BigDecimal.
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @since 1.5
+ */
+public class ASTFloatingPointLiteral extends SimpleNode
+{
+
+ // This may be of type Double or BigDecimal
+ private Number value = null;
+
+ /**
+ * @param id
+ */
+ public ASTFloatingPointLiteral(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTFloatingPointLiteral(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * Initialization method - doesn't do much but do the object
+ * creation. We only need to do it once.
+ * @param context
+ * @param data
+ * @return The data object.
+ * @throws TemplateInitException
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data)
+ throws TemplateInitException
+ {
+ /*
+ * init the tree correctly
+ */
+
+ super.init( context, data );
+
+ /*
+ * Determine the size of the item and make it a Double or BigDecimal as appropriate.
+ */
+ String str = getFirstToken().image;
+ try
+ {
+ value = Double.valueOf( str );
+
+ } catch ( NumberFormatException E1 )
+ {
+
+ // if there's still an Exception it will propqgate out
+ value = new BigDecimal( str );
+
+ }
+ cleanupParserAndTokens();
+
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public Object value(InternalContextAdapter context)
+ {
+ return value;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#evaluate(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ {
+ return !rsvc.getBoolean(RuntimeConstants.CHECK_EMPTY_OBJECTS, true) || !MathUtils.isZero(value);
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTGENode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTGENode.java
new file mode 100644
index 00000000..62be6c76
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTGENode.java
@@ -0,0 +1,51 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ * Handles arg1 &gt;= arg2<br><br>
+ */
+public class ASTGENode extends ASTComparisonNode
+{
+ public ASTGENode(int id)
+ {
+ super(id);
+ }
+
+ public ASTGENode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ @Override
+ public String getLiteralOperator()
+ {
+ return ">=";
+ }
+
+ @Override
+ public boolean numberTest(int compareResult)
+ {
+ return compareResult >= 0;
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTGTNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTGTNode.java
new file mode 100644
index 00000000..2eddc6f7
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTGTNode.java
@@ -0,0 +1,51 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ * Handles arg1 &gt; arg2<br><br>
+ */
+public class ASTGTNode extends ASTComparisonNode
+{
+ public ASTGTNode(int id)
+ {
+ super(id);
+ }
+
+ public ASTGTNode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ @Override
+ public String getLiteralOperator()
+ {
+ return ">";
+ }
+
+ @Override
+ public boolean numberTest(int compareResult)
+ {
+ return compareResult == 1;
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIdentifier.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIdentifier.java
new file mode 100644
index 00000000..3cb762a4
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIdentifier.java
@@ -0,0 +1,305 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.app.event.EventHandlerUtil;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.util.introspection.Info;
+import org.apache.velocity.util.introspection.IntrospectionCacheData;
+import org.apache.velocity.util.introspection.VelPropertyGet;
+
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * ASTIdentifier.java
+ *
+ * Method support for identifiers : $foo
+ *
+ * mainly used by ASTReference
+ *
+ * Introspection is now moved to 'just in time' or at render / execution
+ * time. There are many reasons why this has to be done, but the
+ * primary two are thread safety, to remove any context-derived
+ * information from class member variables.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ASTIdentifier extends SimpleNode
+{
+ private String identifier = "";
+
+ /**
+ * This is really immutable after the init, so keep one for this node
+ */
+ protected Info uberInfo;
+
+ /**
+ * Indicates if we are running in strict reference mode.
+ */
+ protected boolean strictRef = false;
+
+ /**
+ * @param id
+ */
+ public ASTIdentifier(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTIdentifier(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * Identifier getter
+ * @return identifier
+ */
+ public String getIdentifier()
+ {
+ return identifier;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * simple init - don't do anything that is context specific.
+ * just get what we need from the AST, which is static.
+ * @param context
+ * @param data
+ * @return The data object.
+ * @throws TemplateInitException
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data)
+ throws TemplateInitException
+ {
+ super.init(context, data);
+
+ identifier = rsvc.useStringInterning() ? getFirstToken().image.intern() : getFirstToken().image;
+
+ uberInfo = new Info(getTemplateName(), getLine(), getColumn());
+
+ strictRef = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false);
+
+ saveTokenImages();
+ cleanupParserAndTokens();
+
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#execute(java.lang.Object, org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public Object execute(Object o, InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ try
+ {
+ rsvc.getLogContext().pushLogContext(this, uberInfo);
+
+ VelPropertyGet vg = null;
+
+ try
+ {
+ /*
+ * first, see if we have this information cached.
+ */
+
+ IntrospectionCacheData icd = context.icacheGet(this);
+ Class<?> clazz = o instanceof Class<?> ? (Class<?>)o : o.getClass();
+
+ /*
+ * if we have the cache data and the class of the object we are
+ * invoked with is the same as that in the cache, then we must
+ * be all right. The last 'variable' is the method name, and
+ * that is fixed in the template :)
+ */
+
+ if ( icd != null && (icd.contextData == clazz) )
+ {
+ vg = (VelPropertyGet) icd.thingy;
+ }
+ else
+ {
+ /*
+ * otherwise, do the introspection, and cache it. Use the
+ * uberspector
+ */
+
+ vg = rsvc.getUberspect().getPropertyGet(o, identifier, uberInfo);
+
+ if (vg != null && vg.isCacheable())
+ {
+ icd = new IntrospectionCacheData();
+ icd.contextData = clazz;
+ icd.thingy = vg;
+ context.icachePut(this,icd);
+ }
+ }
+ }
+
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ throw e;
+ }
+ catch(Exception e)
+ {
+ String msg = "ASTIdentifier.execute() : identifier = "+identifier;
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+
+ /*
+ * we have no getter... punt...
+ */
+
+ if (vg == null)
+ {
+ if (strictRef)
+ {
+ throw new MethodInvocationException("Object '" + o.getClass().getName() +
+ "' does not contain property '" + identifier + "'",
+ null, rsvc.getLogContext().getStackTrace(), identifier,
+ uberInfo.getTemplateName(), uberInfo.getLine(), uberInfo.getColumn());
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /*
+ * now try and execute. If we get a MIE, throw that
+ * as the app wants to get these. If not, log and punt.
+ */
+ try
+ {
+ return vg.invoke(o);
+ }
+ catch(InvocationTargetException ite)
+ {
+ /*
+ * if we have an event cartridge, see if it wants to veto
+ * also, let non-Exception Throwables go...
+ */
+
+ Throwable t = ite.getTargetException();
+ if (t instanceof Exception)
+ {
+ try
+ {
+ return EventHandlerUtil.methodException(rsvc, context, o.getClass(), vg.getMethodName(),
+ (Exception) t, uberInfo);
+ }
+
+ /*
+ * If the event handler throws an exception, then wrap it
+ * in a MethodInvocationException.
+ */
+ catch( Exception e )
+ {
+ throw new MethodInvocationException(
+ "Invocation of method '" + vg.getMethodName() + "'"
+ + " in " + o.getClass()
+ + " threw exception "
+ + ite.getTargetException().toString(),
+ ite.getTargetException(), rsvc.getLogContext().getStackTrace(), vg.getMethodName(), getTemplateName(), this.getLine(), this.getColumn());
+ }
+ }
+ else
+ {
+ /*
+ * no event cartridge to override. Just throw
+ */
+
+ throw new MethodInvocationException(
+ "Invocation of method '" + vg.getMethodName() + "'"
+ + " in " + o.getClass()
+ + " threw exception "
+ + ite.getTargetException().toString(),
+ ite.getTargetException(), rsvc.getLogContext().getStackTrace(), vg.getMethodName(), getTemplateName(), this.getLine(), this.getColumn());
+
+
+ }
+ }
+ catch(IllegalArgumentException iae)
+ {
+ return null;
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ throw e;
+ }
+ catch(Exception e)
+ {
+ String msg = "ASTIdentifier() : exception invoking method "
+ + "for identifier '" + identifier + "' in "
+ + o.getClass();
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+ }
+ finally
+ {
+ rsvc.getLogContext().popLogContext();
+ }
+ }
+
+ /**
+ * Returns the string ".<i>identifier</i>". This method is only used for displaying the VTL stacktrace
+ * when a rendering error is encountered when runtime.log.track_location is true.
+ * @return
+ */
+ @Override
+ public String literal()
+ {
+ if (literal != null)
+ {
+ return literal;
+ }
+ return literal = '.' + getIdentifier();
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIfStatement.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIfStatement.java
new file mode 100644
index 00000000..b1b1bb66
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIfStatement.java
@@ -0,0 +1,216 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeConstants.SpaceGobbling;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.runtime.parser.Token;
+
+import java.io.IOException;
+import java.io.Writer;
+
+
+/**
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ASTIfStatement extends SimpleNode
+{
+ private String prefix = "";
+ private String postfix = "";
+
+ /*
+ * '#' and '$' prefix characters eaten by javacc MORE mode
+ */
+ private String morePrefix = "";
+
+ /**
+ * @param id
+ */
+ public ASTIfStatement(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTIfStatement(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+
+ /*
+ * handle '$' and '#' chars prefix
+ */
+ Token t = getFirstToken();
+ int pos = -1;
+ while (t != null && (pos = t.image.lastIndexOf(rsvc.getParserConfiguration().getHashChar())) == -1)
+ {
+ t = t.next;
+ }
+ if (t != null && pos > 0)
+ {
+ morePrefix = t.image.substring(0, pos);
+ }
+
+ /* handle structured space gobbling */
+ if (rsvc.getSpaceGobbling() == SpaceGobbling.STRUCTURED && postfix.length() > 0)
+ {
+ NodeUtils.fixIndentation(this, prefix);
+ }
+
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+ /**
+ * set indentation prefix
+ * @param prefix
+ */
+ public void setPrefix(String prefix)
+ {
+ this.prefix = prefix;
+ }
+
+ /**
+ * get indentation prefix
+ * @return prefix
+ */
+ public String getPrefix()
+ {
+ return prefix;
+ }
+
+ /**
+ * set indentation postfix
+ * @param postfix
+ */
+ public void setPostfix(String postfix)
+ {
+ this.postfix = postfix;
+ }
+
+ /**
+ * get indentation postfix
+ * @return postfix
+ */
+ public String getPostfix()
+ {
+ return postfix;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#render(org.apache.velocity.context.InternalContextAdapter, java.io.Writer)
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer)
+ throws IOException,MethodInvocationException,
+ ResourceNotFoundException, ParseErrorException
+ {
+ SpaceGobbling spaceGobbling = rsvc.getSpaceGobbling();
+
+ if (morePrefix.length() > 0 || spaceGobbling.compareTo(SpaceGobbling.LINES) < 0)
+ {
+ writer.write(prefix);
+ }
+
+ writer.write(morePrefix);
+
+ /*
+ * Check if the #if(expression) construct evaluates to true:
+ */
+ if (jjtGetChild(0).evaluate(context))
+ {
+ jjtGetChild(1).render(context, writer);
+ }
+ else
+ {
+ int totalNodes = jjtGetNumChildren();
+
+ /*
+ * Now check the remaining nodes left in the
+ * if construct. The nodes are either elseif
+ * nodes or else nodes. Each of these node
+ * types knows how to evaluate themselves. If
+ * a node evaluates to true then the node will
+ * render itself and this method will return
+ * as there is nothing left to do.
+ */
+ for (int i = 2; i < totalNodes; i++)
+ {
+ if (jjtGetChild(i).evaluate(context))
+ {
+ jjtGetChild(i).render(context, writer);
+ break;
+ }
+ }
+ }
+
+ if (morePrefix.length() > 0 || spaceGobbling == SpaceGobbling.NONE)
+ {
+ writer.write(postfix);
+ }
+
+ /*
+ * This is reached without rendering anything (other than potential suffix/prefix) when an ASTIfStatement
+ * consists of an if/elseif sequence where none of the nodes evaluate to true.
+ */
+
+ return true;
+ }
+
+ /**
+ * @param context
+ * @param visitor
+ */
+ public void process( InternalContextAdapter context, ParserVisitor visitor)
+ {
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIncludeStatement.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIncludeStatement.java
new file mode 100644
index 00000000..37f88b2b
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIncludeStatement.java
@@ -0,0 +1,70 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ *
+ */
+public class ASTIncludeStatement extends SimpleNode
+{
+ /**
+ * @param id
+ */
+ public ASTIncludeStatement(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTIncludeStatement(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIndex.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIndex.java
new file mode 100644
index 00000000..ff8576fe
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIndex.java
@@ -0,0 +1,224 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.util.ClassUtils;
+import org.apache.velocity.util.StringUtils;
+import org.apache.velocity.util.introspection.VelMethod;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * This node is responsible for the bracket notation at the end of
+ * a reference, e.g., $foo[1]
+ */
+
+public class ASTIndex extends SimpleNode
+{
+ private static final String methodName = "get";
+
+ /**
+ * Indicates if we are running in strict reference mode.
+ */
+ protected boolean strictRef = false;
+
+ /**
+ * @param i
+ */
+ public ASTIndex(int i)
+ {
+ super(i);
+ }
+
+ /**
+ * @param p
+ * @param i
+ */
+ public ASTIndex(Parser p, int i)
+ {
+ super(p, i);
+ }
+
+ /**
+ * @param context
+ * @param data
+ * @return data
+ * @throws TemplateInitException
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data)
+ throws TemplateInitException
+ {
+ super.init(context, data);
+ strictRef = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false);
+ cleanupParserAndTokens();
+ return data;
+ }
+
+ private final static Object[] noParams = {};
+ private final static Class<?>[] noTypes = {};
+
+ /**
+ * If argument is an Integer and negative, then return (o.size() - argument).
+ * Otherwise return the original argument. We use this to calculate the true
+ * index of a negative index e.g., $foo[-1]. If no size() method is found on the
+ * 'o' object, then we throw an VelocityException.
+ * @param argument
+ * @param o
+ * @param context Used to access the method cache.
+ * @param node ASTNode used for error reporting.
+ * @return found object
+ */
+ public static Object adjMinusIndexArg(Object argument, Object o,
+ InternalContextAdapter context, SimpleNode node)
+ {
+ if (argument instanceof Integer && (Integer) argument < 0)
+ {
+ // The index value is a negative number, $foo[-1], so we want to actually
+ // Index [size - value], so try and call the size method.
+ VelMethod method = ClassUtils.getMethod("size", noParams, noTypes,
+ o, context, node, false);
+ if (method == null)
+ {
+ // The object doesn't have a size method, so there is no notion of "at the end"
+ throw new VelocityException(
+ "A 'size()' method required for negative value "
+ + (Integer) argument + " does not exist for class '"
+ + o.getClass().getName() + "' at " + StringUtils.formatFileString(node),
+ null, node.getRuntimeServices().getLogContext().getStackTrace());
+ }
+
+ Object size = null;
+ try
+ {
+ size = method.invoke(o, noParams);
+ }
+ catch (Exception e)
+ {
+ throw new VelocityException("Error trying to calls the 'size()' method on '"
+ + o.getClass().getName() + "' at " + StringUtils.formatFileString(node), e,
+ node.getRuntimeServices().getLogContext().getStackTrace());
+ }
+
+ int sizeint = 0;
+ try
+ {
+ sizeint = (Integer) size;
+ }
+ catch (ClassCastException e)
+ {
+ // If size() doesn't return an Integer we want to report a pretty error
+ throw new VelocityException("Method 'size()' on class '"
+ + o.getClass().getName() + "' returned '" + size.getClass().getName()
+ + "' when Integer was expected at " + StringUtils.formatFileString(node),
+ null, node.getRuntimeServices().getLogContext().getStackTrace());
+ }
+
+ argument = sizeint + (Integer) argument;
+ }
+
+ // Nothing to do, return the original argument
+ return argument;
+ }
+
+ /**
+ * @param o
+ * @param context
+ * @return object value
+ * @throws MethodInvocationException
+ */
+ @Override
+ public Object execute(Object o, InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ Object argument = jjtGetChild(0).value(context);
+ // If negative, turn -1 into size - 1
+ argument = adjMinusIndexArg(argument, o, context, this);
+ Object [] params = {argument};
+ Class<?>[] paramClasses = {argument == null ? null : argument.getClass()};
+
+ VelMethod method = ClassUtils.getMethod(methodName, params, paramClasses,
+ o, context, this, strictRef);
+
+ if (method == null) return null;
+
+ try
+ {
+ /*
+ * get the returned object. It may be null, and that is
+ * valid for something declared with a void return type.
+ * Since the caller is expecting something to be returned,
+ * as long as things are peachy, we can return an empty
+ * String so ASTReference() correctly figures out that
+ * all is well.
+ */
+ Object obj = method.invoke(o, params);
+
+ if (obj == null)
+ {
+ if( method.getReturnType() == Void.TYPE)
+ {
+ return "";
+ }
+ }
+
+ return obj;
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ throw e;
+ }
+ catch( Exception e )
+ {
+ String msg = "Error invoking method 'get("
+ + (argument == null ? "null" : argument.getClass().getName())
+ + ")' in " + o.getClass().getName()
+ + " at " + StringUtils.formatFileString(this);
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIntegerLiteral.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIntegerLiteral.java
new file mode 100644
index 00000000..56e2c3ba
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIntegerLiteral.java
@@ -0,0 +1,122 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.parser.Parser;
+
+import java.math.BigInteger;
+
+/**
+ * Handles integer numbers. The value will be either an Integer, a Long, or a BigInteger.
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @since 1.5
+ */
+public class ASTIntegerLiteral extends SimpleNode
+{
+
+ // This may be of type Integer, Long or BigInteger
+ private Number value = null;
+
+ /**
+ * @param id
+ */
+ public ASTIntegerLiteral(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTIntegerLiteral(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data)
+ throws TemplateInitException
+ {
+ /*
+ * init the tree correctly
+ */
+
+ super.init( context, data );
+
+ /*
+ * Determine the size of the item and make it an Integer, Long, or BigInteger as appropriate.
+ */
+ String str = getFirstToken().image;
+ try
+ {
+ value = Integer.valueOf( str );
+ }
+ catch ( NumberFormatException E1 )
+ {
+ try
+ {
+ value = Long.valueOf( str );
+ }
+ catch ( NumberFormatException E2 )
+ {
+ // if there's still an Exception it will propagate out
+ value = new BigInteger( str );
+ }
+ }
+ cleanupParserAndTokens();
+
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public Object value(InternalContextAdapter context)
+ {
+ return value;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#evaluate(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ {
+ return !rsvc.getBoolean(RuntimeConstants.CHECK_EMPTY_OBJECTS, true) || !MathUtils.isZero(value);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIntegerRange.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIntegerRange.java
new file mode 100644
index 00000000..5135ff92
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTIntegerRange.java
@@ -0,0 +1,296 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.util.DuckType;
+import org.apache.velocity.util.StringUtils;
+
+import java.util.AbstractList;
+import java.util.Iterator;
+import java.util.ListIterator;
+
+/**
+ * handles the range 'operator' [ n .. m ]
+ *
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ */
+public class ASTIntegerRange extends SimpleNode
+{
+ /**
+ * @param id
+ */
+ public ASTIntegerRange(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTIntegerRange(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ public static class IntegerRange extends AbstractList<Integer>
+ {
+ public class RangeIterator implements ListIterator<Integer>
+ {
+ private int value;
+
+ public RangeIterator()
+ {
+ value = left - delta;
+ }
+
+ public RangeIterator(int startIndex)
+ {
+ value = left + (startIndex - 1) * delta;
+ }
+
+ @Override
+ public Integer next()
+ {
+ value += delta;
+ return value;
+ }
+
+ @Override
+ public boolean hasPrevious()
+ {
+ return value != left - delta;
+ }
+
+ @Override
+ public Integer previous()
+ {
+ value -= delta;
+ return value;
+ }
+
+ @Override
+ public int nextIndex()
+ {
+ return (value + delta - left) * delta;
+ }
+
+ @Override
+ public int previousIndex()
+ {
+ return (value - delta - left) * delta;
+ }
+
+ @Override
+ public void remove()
+ {
+ throw new UnsupportedOperationException("integer range is read only");
+ }
+
+ @Override
+ public void set(Integer integer)
+ {
+ throw new UnsupportedOperationException("integer range is read only");
+ }
+
+ @Override
+ public void add(Integer integer)
+ {
+ throw new UnsupportedOperationException("integer range is read only");
+ }
+
+ @Override
+ public boolean hasNext()
+ {
+ return value != right;
+ }
+ }
+
+ private int left;
+ private int right;
+ private int delta;
+
+ public IntegerRange(int left, int right, int delta)
+ {
+ this.left = left;
+ this.right = right;
+ this.delta = delta;
+ }
+
+ @Override
+ public Iterator<Integer> iterator()
+ {
+ return new RangeIterator();
+ }
+
+ @Override
+ public Integer get(int index)
+ {
+ int ret = left + delta * index;
+ if (delta > 0 && ret > right || delta < 0 && ret < right)
+ {
+ throw new IndexOutOfBoundsException();
+ }
+ return ret;
+ }
+
+ @Override
+ public int indexOf(Object o)
+ {
+ int v = DuckType.asNumber(o).intValue();
+ v -= left;
+ v *= delta;
+ return v >= 0 && v < size() ? v : -1;
+ }
+
+ @Override
+ public int lastIndexOf(Object o)
+ {
+ return indexOf(o);
+ }
+
+ @Override
+ public ListIterator<Integer> listIterator()
+ {
+ return new RangeIterator();
+ }
+
+ @Override
+ public ListIterator<Integer> listIterator(int index)
+ {
+ return new RangeIterator(index);
+ }
+
+ @Override
+ public int size()
+ {
+ return Math.abs(right - left) + 1;
+ }
+ }
+
+ /**
+ * does the real work. Creates an Vector of Integers with the
+ * right value range
+ *
+ * @param context app context used if Left or Right of .. is a ref
+ * @return Object array of Integers
+ * @throws MethodInvocationException
+ */
+ @Override
+ public Object value(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ /*
+ * get the two range ends
+ */
+
+ Object left = jjtGetChild(0).value( context );
+ Object right = jjtGetChild(1).value( context );
+
+ /*
+ * if either is null, lets log and bail
+ */
+
+ if (left == null || right == null)
+ {
+ log.error((left == null ? "Left" : "Right")
+ + " side of range operator [n..m] has null value."
+ + " Operation not possible. "
+ + StringUtils.formatFileString(this));
+ return null;
+ }
+
+ /*
+ * if not a Number, try to convert
+ */
+
+ try
+ {
+ left = DuckType.asNumber(left);
+ }
+ catch (NumberFormatException nfe) {}
+
+ try
+ {
+ right = DuckType.asNumber(right);
+ }
+ catch (NumberFormatException nfe) {}
+
+ /*
+ * if still not a Number, nothing we can do
+ */
+
+ if ( !( left instanceof Number ) || !( right instanceof Number ))
+ {
+ log.error((!(left instanceof Number) ? "Left" : "Right")
+ + " side of range operator is not convertible to a Number. "
+ + StringUtils.formatFileString(this));
+ return null;
+ }
+
+ /*
+ * get the two integer values of the ends of the range
+ */
+
+ int l = ((Number) left).intValue() ;
+ int r = ((Number) right).intValue();
+
+ /*
+ * Determine whether the increment is positive or negative.
+ */
+
+ int delta = ( l >= r ) ? -1 : 1;
+
+ /*
+ * Return the corresponding integer range
+ */
+
+ return new IntegerRange(l, r, delta);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTLENode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTLENode.java
new file mode 100644
index 00000000..ff3c2e8e
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTLENode.java
@@ -0,0 +1,51 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ * Handles arg1 &lt;= arg2<br><br>
+ */
+public class ASTLENode extends ASTComparisonNode
+{
+ public ASTLENode(int id)
+ {
+ super(id);
+ }
+
+ public ASTLENode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ @Override
+ public String getLiteralOperator()
+ {
+ return "<=";
+ }
+
+ @Override
+ public boolean numberTest(int compareResult)
+ {
+ return compareResult <= 0;
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTLTNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTLTNode.java
new file mode 100644
index 00000000..a4a43465
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTLTNode.java
@@ -0,0 +1,51 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ * Handles arg1 &lt; arg2<br><br>
+ */
+public class ASTLTNode extends ASTComparisonNode
+{
+ public ASTLTNode(int id)
+ {
+ super(id);
+ }
+
+ public ASTLTNode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ @Override
+ public String getLiteralOperator()
+ {
+ return "<";
+ }
+
+ @Override
+ public boolean numberTest(int compareResult)
+ {
+ return compareResult == -1;
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTLogicalOperator.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTLogicalOperator.java
new file mode 100644
index 00000000..60180460
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTLogicalOperator.java
@@ -0,0 +1,30 @@
+package org.apache.velocity.runtime.parser.node;
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+public abstract class ASTLogicalOperator extends ASTBinaryOperator
+{
+ public ASTLogicalOperator(int id)
+ {
+ super(id);
+ }
+
+ public ASTLogicalOperator(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMap.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMap.java
new file mode 100644
index 00000000..c259b2fb
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMap.java
@@ -0,0 +1,112 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.parser.Parser;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * AST Node for creating a map / dictionary.
+ *
+ * This class was originally generated from Parser.jjt.
+ *
+ * @version $Id$
+ * @since 1.5
+ */
+public class ASTMap extends SimpleNode
+{
+ /**
+ * @param id
+ */
+ public ASTMap(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTMap(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public Object value(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ int size = jjtGetNumChildren();
+
+ Map<Object, Object> objectMap = new LinkedHashMap<>();
+
+ for (int i = 0; i < size; i += 2)
+ {
+ SimpleNode keyNode = (SimpleNode) jjtGetChild(i);
+ SimpleNode valueNode = (SimpleNode) jjtGetChild(i+1);
+
+ Object key = (keyNode == null ? null : keyNode.value(context));
+ Object value = (valueNode == null ? null : valueNode.value(context));
+
+ objectMap.put(key, value);
+ }
+
+ return objectMap;
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#evaluate(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ {
+ return !rsvc.getBoolean(RuntimeConstants.CHECK_EMPTY_OBJECTS, true) || children != null && children.length > 0;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMathNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMathNode.java
new file mode 100755
index 00000000..e298bc0f
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMathNode.java
@@ -0,0 +1,165 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MathException;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.util.DuckType;
+
+/**
+ * Helps handle math<br><br>
+ *
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @author <a href="mailto:pero@antaramusic.de">Peter Romianowski</a>
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author Nathan Bubna
+ * @version $Id: ASTMathNode.java 517553 2007-03-13 06:09:58Z wglass $
+ */
+public abstract class ASTMathNode extends ASTBinaryOperator
+{
+ protected boolean strictMode = false;
+
+ public ASTMathNode(int id)
+ {
+ super(id);
+ }
+
+ public ASTMathNode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ super.init(context, data);
+ strictMode = rsvc.getBoolean(RuntimeConstants.STRICT_MATH, false);
+ cleanupParserAndTokens();
+ return data;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * gets the two args and performs the operation on them
+ *
+ * @param context
+ * @return result or null
+ * @throws MethodInvocationException
+ */
+ @Override
+ public Object value(InternalContextAdapter context) throws MethodInvocationException
+ {
+ Object left = jjtGetChild(0).value(context);
+ Object right = jjtGetChild(1).value(context);
+
+ /*
+ * should we do anything special here?
+ */
+ Object special = handleSpecial(left, right, context);
+ if (special != null)
+ {
+ return special;
+ }
+
+ // coerce to Number type, if possible
+ try
+ {
+ left = DuckType.asNumber(left);
+ }
+ catch (NumberFormatException nfe) {}
+ try
+ {
+ right = DuckType.asNumber(right);
+ }
+ catch (NumberFormatException nfe) {}
+
+ /*
+ * if not a Number, not much we can do
+ */
+ if (!(left instanceof Number) || !(right instanceof Number))
+ {
+ boolean wrongright = (left instanceof Number);
+ boolean wrongtype = wrongright ? right != null : left != null;
+ String msg = (wrongright ? "Right" : "Left")
+ + " side of math operation ("
+ + jjtGetChild(wrongright ? 1 : 0).literal() + ") "
+ + (wrongtype ? "is not a Number. " : "has a null value. ")
+ + getLocation(context);
+ if (strictMode)
+ {
+ log.error(msg);
+ throw new MathException(msg, rsvc.getLogContext().getStackTrace());
+ }
+ else
+ {
+ log.debug(msg);
+ return null;
+ }
+ }
+
+ return perform((Number)left, (Number)right, context);
+ }
+
+ /**
+ * Extension hook to allow special behavior by subclasses
+ * If this method returns a non-null value, that is returned,
+ * rather than the result of the math operation.
+ * @param left
+ * @param right
+ * @param context
+ * @return special value
+ * @see ASTAddNode#handleSpecial
+ */
+ protected Object handleSpecial(Object left, Object right, InternalContextAdapter context)
+ {
+ // do nothing, this is an extension hook
+ return null;
+ }
+
+ /**
+ * Performs the math operation represented by this node.
+ * @param left
+ * @param right
+ * @param context
+ * @return computed value
+ * @see ASTAddNode#perform(Number, Number, InternalContextAdapter)
+ */
+ public abstract Number perform(Number left, Number right, InternalContextAdapter context);
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMethod.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMethod.java
new file mode 100644
index 00000000..5696b6bb
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMethod.java
@@ -0,0 +1,459 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.app.event.EventHandlerUtil;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.directive.StopCommand;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.util.ClassUtils;
+import org.apache.velocity.util.introspection.Info;
+import org.apache.velocity.util.introspection.IntrospectionCacheData;
+import org.apache.velocity.util.introspection.VelMethod;
+
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * ASTMethod.java
+ *
+ * Method support for references : $foo.method()
+ *
+ * NOTE :
+ *
+ * introspection is now done at render time.
+ *
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ASTMethod extends SimpleNode
+{
+ /**
+ * An empty immutable <code>Class</code> array.
+ */
+ private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
+
+ private String methodName = "";
+ private int paramCount = 0;
+ private boolean logOnInvalid = true;
+
+ protected Info uberInfo;
+
+ /**
+ * Indicates if we are running in strict reference mode.
+ */
+ protected boolean strictRef = false;
+
+ /**
+ * @param id
+ */
+ public ASTMethod(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTMethod(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * simple init - init our subtree and get what we can from
+ * the AST
+ * @param context
+ * @param data
+ * @return The init result
+ * @throws TemplateInitException
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data)
+ throws TemplateInitException
+ {
+ super.init( context, data );
+
+ /*
+ * make an uberinfo - saves new's later on
+ */
+
+ uberInfo = new Info(getTemplateName(),
+ getLine(),getColumn());
+ /*
+ * this is about all we can do
+ */
+
+ methodName = getFirstToken().image;
+ paramCount = jjtGetNumChildren() - 1;
+
+ strictRef = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false);
+ logOnInvalid = rsvc.getBoolean(RuntimeConstants.RUNTIME_LOG_METHOD_CALL_LOG_INVALID, true);
+
+ cleanupParserAndTokens();
+
+ return data;
+ }
+
+ /**
+ * invokes the method. Returns null if a problem, the
+ * actual return if the method returns something, or
+ * an empty string "" if the method returns void
+ * @param o
+ * @param context
+ * @return Result or null.
+ * @throws MethodInvocationException
+ */
+ @Override
+ public Object execute(Object o, InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ try
+ {
+ rsvc.getLogContext().pushLogContext(this, uberInfo);
+
+ /*
+ * new strategy (strategery!) for introspection. Since we want
+ * to be thread- as well as context-safe, we *must* do it now,
+ * at execution time. There can be no in-node caching,
+ * but if we are careful, we can do it in the context.
+ */
+ Object [] params = new Object[paramCount];
+
+ /*
+ * sadly, we do need recalc the values of the args, as this can
+ * change from visit to visit
+ */
+ final Class<?>[] paramClasses =
+ paramCount > 0 ? new Class[paramCount] : EMPTY_CLASS_ARRAY;
+
+ for (int j = 0; j < paramCount; j++)
+ {
+ params[j] = jjtGetChild(j + 1).value(context);
+ if (params[j] != null)
+ {
+ paramClasses[j] = params[j].getClass();
+ }
+ }
+
+ VelMethod method = ClassUtils.getMethod(methodName, params, paramClasses,
+ o, context, this, strictRef);
+
+ // warn if method wasn't found (if strictRef is true, then ClassUtils did throw an exception)
+ if (o != null && method == null && logOnInvalid)
+ {
+ StringBuilder plist = new StringBuilder();
+ for (int i = 0; i < params.length; i++)
+ {
+ Class<?> param = paramClasses[i];
+ plist.append(param == null ? "null" : param.getName());
+ if (i < params.length - 1)
+ plist.append(", ");
+ }
+ log.debug("Object '{}' does not contain method {}({}) (or several ambiguous methods) at {}[line {}, column {}]", o.getClass().getName(), methodName, plist, getTemplateName(), getLine(), getColumn());
+ }
+
+ /*
+ * The parent class (typically ASTReference) uses the icache entry
+ * under 'this' key to distinguish a valid null result from a non-existent method.
+ * So update this dummy cache value if necessary.
+ */
+ IntrospectionCacheData prevICD = context.icacheGet(this);
+ if (method == null)
+ {
+ if (prevICD != null)
+ {
+ context.icachePut(this, null);
+ }
+ return null;
+ }
+ else if (prevICD == null)
+ {
+ context.icachePut(this, new IntrospectionCacheData()); // no need to fill in its members
+ }
+
+ try
+ {
+ /*
+ * get the returned object. It may be null, and that is
+ * valid for something declared with a void return type.
+ * Since the caller is expecting something to be returned,
+ * as long as things are peachy, we can return an empty
+ * String so ASTReference() correctly figures out that
+ * all is well.
+ */
+
+ Object obj = method.invoke(o, params);
+
+ if (obj == null)
+ {
+ if( method.getReturnType() == Void.TYPE)
+ {
+ return "";
+ }
+ }
+
+ return obj;
+ }
+ catch( InvocationTargetException ite )
+ {
+ return handleInvocationException(o, context, ite.getTargetException());
+ }
+
+ /* Can also be thrown by method invocation */
+ catch( IllegalArgumentException t )
+ {
+ return handleInvocationException(o, context, t);
+ }
+
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ throw e;
+ }
+ catch( Exception e )
+ {
+ String msg = "ASTMethod.execute() : exception invoking method '"
+ + methodName + "' in " + o.getClass();
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+ }
+ finally
+ {
+ rsvc.getLogContext().popLogContext();
+ }
+ }
+
+ private Object handleInvocationException(Object o, InternalContextAdapter context, Throwable t)
+ {
+ /*
+ * Errors should not be wrapped
+ */
+ if (t instanceof Error)
+ {
+ throw (Error)t;
+ }
+ /*
+ * We let StopCommands go up to the directive they are for/from
+ */
+ else if (t instanceof StopCommand)
+ {
+ throw (StopCommand)t;
+ }
+
+ /*
+ * In the event that the invocation of the method
+ * itself throws an exception, we want to catch that
+ * wrap it, and throw. We don't log here as we want to figure
+ * out which reference threw the exception, so do that
+ * above
+ */
+ else if (t instanceof Exception)
+ {
+ try
+ {
+ return EventHandlerUtil.methodException( rsvc, context, o.getClass(), methodName, (Exception) t, uberInfo );
+ }
+
+ /*
+ * If the event handler throws an exception, then wrap it
+ * in a MethodInvocationException. Don't pass through RuntimeExceptions like other
+ * similar catchall code blocks.
+ */
+ catch( Exception e )
+ {
+ throw new MethodInvocationException(
+ "Invocation of method '"
+ + methodName + "' in " + o.getClass()
+ + " threw exception "
+ + e.toString(),
+ e, rsvc.getLogContext().getStackTrace(), methodName, getTemplateName(), this.getLine(), this.getColumn());
+ }
+ }
+
+ /*
+ * let non-Exception Throwables go...
+ */
+ else
+ {
+ /*
+ * no event cartridge to override. Just throw
+ */
+
+ throw new MethodInvocationException(
+ "Invocation of method '"
+ + methodName + "' in " + o.getClass()
+ + " threw exception "
+ + t.toString(),
+ t, rsvc.getLogContext().getStackTrace(), methodName, getTemplateName(), this.getLine(), this.getColumn());
+ }
+ }
+
+ /**
+ * Internal class used as key for method cache. Combines
+ * ASTMethod fields with array of parameter classes. Has
+ * public access (and complete constructor) for unit test
+ * purposes.
+ * @since 1.5
+ */
+ public static class MethodCacheKey
+ {
+ /**
+ * method name
+ */
+ private final String methodName;
+
+ /**
+ * parameters classes
+ */
+ private final Class<?>[] params;
+
+ /**
+ * whether the target object is of Class type
+ * (meaning we're searching either for methods
+ * of Class, or for static methods of the class
+ * this Class objects refers to)
+ * @since 2.2
+ */
+ private boolean classObject;
+
+ public MethodCacheKey(String methodName, Class<?>[] params, boolean classObject)
+ {
+ /*
+ * Should never be initialized with nulls, but to be safe we refuse
+ * to accept them.
+ */
+ this.methodName = (methodName != null) ? methodName : StringUtils.EMPTY;
+ this.params = (params != null) ? params : EMPTY_CLASS_ARRAY;
+ this.classObject = classObject;
+ }
+
+ /**
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ public boolean equals(Object o)
+ {
+ /*
+ * note we skip the null test for methodName and params
+ * due to the earlier test in the constructor
+ */
+ if (o instanceof MethodCacheKey)
+ {
+ final MethodCacheKey other = (MethodCacheKey) o;
+ if (params.length == other.params.length &&
+ methodName.equals(other.methodName) &&
+ classObject == other.classObject)
+ {
+ for (int i = 0; i < params.length; ++i)
+ {
+ if (params[i] == null)
+ {
+ if (params[i] != other.params[i])
+ {
+ return false;
+ }
+ }
+ else if (!params[i].equals(other.params[i]))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * @see java.lang.Object#hashCode()
+ */
+ public int hashCode()
+ {
+ int result = 17;
+
+ /*
+ * note we skip the null test for methodName and params
+ * due to the earlier test in the constructor
+ */
+ for (Class<?> param : params)
+ {
+ if (param != null)
+ {
+ result = result * 37 + param.hashCode();
+ }
+ }
+
+ result = result * 37 + methodName.hashCode();
+
+ return result;
+ }
+ }
+
+ /**
+ * @return Returns the methodName.
+ * @since 1.5
+ */
+ public String getMethodName()
+ {
+ return methodName;
+ }
+
+ /**
+ * Returns the string ".<i>method_name</i>(...)". Arguments literals are not rendered. This method is only
+ * used for displaying the VTL stacktrace when a rendering error is encountered when runtime.log.track_location is true.
+ * @return
+ */
+ @Override
+ public String literal()
+ {
+ if (literal != null)
+ {
+ return literal;
+ }
+ StringBuilder builder = new StringBuilder();
+ builder.append('.').append(getMethodName()).append("(...)");
+
+ return literal = builder.toString();
+ }
+
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTModNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTModNode.java
new file mode 100644
index 00000000..d9de7911
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTModNode.java
@@ -0,0 +1,99 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MathException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ * Handles modulus division<br><br>
+ *
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @author <a href="mailto:pero@antaramusic.de">Peter Romianowski</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ASTModNode extends ASTMathNode
+{
+ /**
+ * @param id
+ */
+ public ASTModNode(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTModNode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ @Override
+ public Number perform(Number left, Number right, InternalContextAdapter context)
+ {
+ /*
+ * check for divide / modulo by 0
+ */
+ if (MathUtils.isZero(right))
+ {
+ String msg = "Right side of modulus operation is zero. Must be non-zero. "
+ + getLocation(context);
+ if (strictMode)
+ {
+ log.error(msg);
+ throw new MathException(msg, rsvc.getLogContext().getStackTrace());
+ }
+ else
+ {
+ log.debug(msg);
+ return null;
+ }
+ }
+ return MathUtils.modulo(left, right);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+ @Override
+ public String getLiteralOperator()
+ {
+ return "%";
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMulNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMulNode.java
new file mode 100644
index 00000000..67bad83e
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMulNode.java
@@ -0,0 +1,81 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ * Handles multiplication<br><br>
+ *
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @author <a href="mailto:pero@antaramusic.de">Peter Romianowski</a>
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ASTMulNode extends ASTMathNode
+{
+ /**
+ * @param id
+ */
+ public ASTMulNode(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTMulNode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ @Override
+ public Number perform(Number left, Number right, InternalContextAdapter context)
+ {
+ return MathUtils.multiply(left, right);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+ @Override
+ public String getLiteralOperator()
+ {
+ return "*";
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTNENode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTNENode.java
new file mode 100644
index 00000000..3f78f5ab
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTNENode.java
@@ -0,0 +1,50 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ * Handles <code>arg1 != arg2</code> by negating evaluation of ASTEQNode
+ */
+public class ASTNENode extends ASTEQNode
+{
+ public ASTNENode(int id)
+ {
+ super(id);
+ }
+
+ public ASTNENode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context) throws MethodInvocationException
+ {
+ return !super.evaluate(context);
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTNegateNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTNegateNode.java
new file mode 100644
index 00000000..842a2925
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTNegateNode.java
@@ -0,0 +1,94 @@
+package org.apache.velocity.runtime.parser.node;
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MathException;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.util.DuckType;
+
+public class ASTNegateNode extends SimpleNode
+{
+ protected boolean strictMode = false;
+
+ public ASTNegateNode(int i)
+ {
+ super(i);
+ }
+
+ public ASTNegateNode(Parser p, int i)
+ {
+ super(p, i);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ super.init(context, data);
+ /* save a literal image now (needed in case of error) */
+ strictMode = rsvc.getBoolean(RuntimeConstants.STRICT_MATH, false);
+ cleanupParserAndTokens();
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#evaluate(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ return jjtGetChild(0).evaluate(context);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public Object value(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ Object value = jjtGetChild(0).value( context );
+ try
+ {
+ value = DuckType.asNumber(value);
+ }
+ catch (NumberFormatException nfe) {}
+ if (!(value instanceof Number))
+ {
+ String msg = "Argument of unary negate (" +
+ jjtGetChild(0).literal() +
+ ") " +
+ (value == null ? "has a null value." : "is not a Number.");
+ if (strictMode)
+ {
+ log.error(msg);
+ throw new MathException(msg, rsvc.getLogContext().getStackTrace());
+ }
+ else
+ {
+ log.debug(msg);
+ return null;
+ }
+ }
+ return MathUtils.negate((Number) value);
+ }
+ @Override
+ public String literal()
+ {
+ return "-" + jjtGetChild(0).literal();
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTNotNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTNotNode.java
new file mode 100644
index 00000000..310a3536
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTNotNode.java
@@ -0,0 +1,96 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ *
+ */
+public class ASTNotNode extends SimpleNode
+{
+ /**
+ * @param id
+ */
+ public ASTNotNode(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTNotNode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#evaluate(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ return !jjtGetChild(0).evaluate(context);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public Object value(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ return (jjtGetChild(0).evaluate( context ) ? Boolean.FALSE : Boolean.TRUE) ;
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+ @Override
+ public String literal()
+ {
+ return "!" + jjtGetChild(0).literal();
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTObjectArray.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTObjectArray.java
new file mode 100644
index 00000000..16d1b236
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTObjectArray.java
@@ -0,0 +1,103 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.parser.Parser;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ */
+public class ASTObjectArray extends SimpleNode
+{
+ /**
+ * @param id
+ */
+ public ASTObjectArray(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTObjectArray(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public Object value(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ int size = jjtGetNumChildren();
+
+ // since we know the amount of elements, initialize arraylist with proper size
+ List<Object> objectArray = new ArrayList<>(size);
+
+ for (int i = 0; i < size; i++)
+ {
+ objectArray.add(jjtGetChild(i).value(context));
+ }
+
+ return objectArray;
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#evaluate(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ {
+ return !rsvc.getBoolean(RuntimeConstants.CHECK_EMPTY_OBJECTS, true) || children != null && children.length > 0;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTOrNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTOrNode.java
new file mode 100644
index 00000000..c019e452
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTOrNode.java
@@ -0,0 +1,115 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+*/
+public class ASTOrNode extends ASTLogicalOperator
+{
+ /**
+ * @param id
+ */
+ public ASTOrNode(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTOrNode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ @Override
+ public String getLiteralOperator()
+ {
+ return "||";
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * Returns the value of the expression.
+ * Since the value of the expression is simply the boolean
+ * result of evaluate(), lets return that.
+ * @param context
+ * @return The Expression value.
+ * @throws MethodInvocationException
+ */
+ @Override
+ public Object value(InternalContextAdapter context )
+ throws MethodInvocationException
+ {
+ return evaluate(context) ? Boolean.TRUE : Boolean.FALSE;
+ }
+
+ /**
+ * the logical or :
+ * <pre>
+ * left || null -&gt; left
+ * null || right -&gt; right
+ * null || null -&gt; false
+ * left || right -&gt; left || right
+ * </pre>
+ * @param context
+ * @return The evaluation result.
+ * @throws MethodInvocationException
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ Node left = jjtGetChild(0);
+ Node right = jjtGetChild(1);
+
+ /*
+ * if the left is not null and true, then true
+ */
+
+ if (left != null && left.evaluate( context ) )
+ return true;
+
+ /*
+ * same for right
+ */
+
+ return right != null && right.evaluate(context);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTParameters.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTParameters.java
new file mode 100644
index 00000000..1c004286
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTParameters.java
@@ -0,0 +1,55 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.parser.Parser;
+
+
+/**
+ *
+ */
+public class ASTParameters extends SimpleNode
+{
+ /**
+ * @param id
+ */
+ public ASTParameters(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTParameters(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTReference.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTReference.java
new file mode 100644
index 00000000..d4ddda9d
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTReference.java
@@ -0,0 +1,1165 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.app.event.EventHandlerUtil;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.io.Filter;
+import org.apache.velocity.runtime.Renderable;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.directive.Block.Reference;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.util.ClassUtils;
+import org.apache.velocity.util.DuckType;
+import org.apache.velocity.util.StringUtils;
+import org.apache.velocity.util.introspection.Info;
+import org.apache.velocity.util.introspection.VelMethod;
+import org.apache.velocity.util.introspection.VelPropertySet;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Deque;
+
+/**
+ * This class is responsible for handling the references in
+ * VTL ($foo).
+ *
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:Christoph.Reck@dlr.de">Christoph Reck</a>
+ * @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a>
+ * @version $Id$
+*/
+public class ASTReference extends SimpleNode
+{
+ /* Reference types */
+ private static final int NORMAL_REFERENCE = 1;
+ private static final int FORMAL_REFERENCE = 2;
+ private static final int QUIET_REFERENCE = 3;
+ private static final int RUNT = 4;
+
+ private int referenceType;
+ private String nullString;
+ private String alternateNullStringKey;
+ private String rootString;
+ private boolean escaped = false;
+ private boolean computableReference = true;
+ private boolean logOnNull = true;
+ private boolean lookupAlternateLiteral = false;
+ private String escPrefix = "";
+ private String morePrefix = "";
+ private String identifier = "";
+
+ private boolean checkEmpty;
+
+ /**
+ * Indicates if we are running in strict reference mode.
+ */
+ public boolean strictRef = false;
+
+ /**
+ * non null Indicates if we are setting an index reference e.g, $foo[2], which basically
+ * means that the last syntax of the reference are brackets.
+ */
+ private ASTIndex astIndex = null;
+
+ /**
+ * non null Indicates that an alternate value has been provided
+ */
+ private ASTExpression astAlternateValue = null;
+
+ /**
+ * Indicates if we are using modified escape behavior in strict mode.
+ * mainly we allow \$abc -&gt; to render as $abc
+ */
+ public boolean strictEscape = false;
+
+ private int numChildren = 0;
+
+ /**
+ * Whether to trigger an event for invalid quiet references
+ * @since 2.2
+ */
+ private boolean warnInvalidQuietReferences = false;
+
+ /**
+ * Whether to trigger an event for invalid null references, that is when a value
+ * is present in the context or parent object but is null
+ * @since 2.2
+ */
+ private boolean warnInvalidNullReferences = false;
+
+ /**
+ * Whether to trigger an event for invalid tested references - as in #if($foo)
+ * @since 2.2
+ */
+ private boolean warnInvalidTestedReferences = false;
+
+ protected Info uberInfo;
+
+ /**
+ * @param id
+ */
+ public ASTReference(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTReference(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data)
+ throws TemplateInitException
+ {
+ super.init(context, data);
+
+ strictEscape = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT_ESCAPE, false);
+ strictRef = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false);
+ lookupAlternateLiteral = rsvc.getBoolean(RuntimeConstants.VM_ENABLE_BC_MODE, false);
+
+ /*
+ * the only thing we can do in init() is getRoot()
+ * as that is template based, not context based,
+ * so it's thread- and context-safe
+ */
+
+ rootString = rsvc.useStringInterning() ? getRoot().intern() : getRoot();
+ if (lookupAlternateLiteral)
+ {
+ /* cache alternate null tring key */
+ alternateNullStringKey = ".literal." + nullString;
+ }
+
+ numChildren = jjtGetNumChildren();
+
+ // This is an expensive call, so get it now.
+ literal();
+
+ /*
+ * and if appropriate...
+ */
+ if (numChildren > 0 )
+ {
+ Node lastNode = jjtGetChild(numChildren-1);
+ if (lastNode instanceof ASTIndex)
+ {
+ /*
+ * only used in SetValue, where alternate value is forbidden
+ */
+ astIndex = (ASTIndex) lastNode;
+ }
+ else if (lastNode instanceof ASTExpression)
+ {
+ astAlternateValue = (ASTExpression) lastNode;
+ --numChildren;
+ }
+ else
+ {
+ identifier = lastNode.getFirstTokenImage();
+ }
+ }
+
+
+ /*
+ * make an uberinfo - saves new's later on
+ */
+ uberInfo = new Info(getTemplateName(), getLine(),getColumn());
+
+ /*
+ * track whether we log invalid references
+ */
+ logOnNull =
+ rsvc.getBoolean(RuntimeConstants.RUNTIME_LOG_REFERENCE_LOG_INVALID, true);
+
+ /*
+ * whether to check for emptiness when evaluatingnumChildren
+ */
+ checkEmpty =
+ rsvc.getBoolean(RuntimeConstants.CHECK_EMPTY_OBJECTS, true);
+
+ /* invalid references special cases */
+
+ warnInvalidQuietReferences =
+ rsvc.getBoolean(RuntimeConstants.EVENTHANDLER_INVALIDREFERENCES_QUIET, false);
+ warnInvalidNullReferences =
+ rsvc.getBoolean(RuntimeConstants.EVENTHANDLER_INVALIDREFERENCES_NULL, false);
+ warnInvalidTestedReferences =
+ rsvc.getBoolean(RuntimeConstants.EVENTHANDLER_INVALIDREFERENCES_TESTED, false);
+
+
+ /*
+ * In the case we are referencing a variable with #if($foo) or
+ * #if( ! $foo) then we allow variables to be undefined and we
+ * set strictRef to false so that if the variable is undefined
+ * an exception is not thrown.
+ */
+ if (strictRef && numChildren == 0)
+ {
+ logOnNull = false; // Strict mode allows nulls
+
+ Node node = this.jjtGetParent();
+ if (node instanceof ASTNotNode // #if( ! $foo)
+ || node instanceof ASTExpression // #if( $foo )
+ || node instanceof ASTOrNode // #if( $foo || ...
+ || node instanceof ASTAndNode) // #if( $foo && ...
+ {
+ // Now scan up tree to see if we are in an If statement
+ while (node != null)
+ {
+ if (node instanceof ASTIfStatement)
+ {
+ strictRef = false;
+ break;
+ }
+ node = node.jjtGetParent();
+ }
+ }
+ }
+ saveTokenImages();
+ cleanupParserAndTokens();
+
+ return data;
+ }
+
+ /**
+ * Returns the 'root string', the reference key
+ * @return the root string.
+ */
+ public String getRootString()
+ {
+ return rootString;
+ }
+
+ /**
+ * gets an Object that 'is' the value of the reference
+ *
+ * @param o Object parameter, unused per se, but non-null by convention inside an #if/#elseif evaluation
+ * @param context context used to generate value
+ * @return The execution result.
+ * @throws MethodInvocationException
+ */
+ @Override
+ public Object execute(Object o, InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ try
+ {
+ rsvc.getLogContext().pushLogContext(this, uberInfo);
+
+ /*
+ * The only case where 'o' is not null is when this method is called by evaluate().
+ * Its value is not used, but it is a convention meant to allow statements like
+ * #if($invalidReference) *not* to trigger an invalid reference event.
+ * Statements like #if($invalidReference.prop) should *still* trigger an invalid reference event.
+ * Statements like #if($validReference.invalidProp) should not.
+ */
+ boolean onlyTestingReference = (o != null);
+
+ if (referenceType == RUNT)
+ return null;
+
+ /*
+ * get the root object from the context
+ */
+
+ Object result = getRootVariableValue(context);
+
+ if (result == null && !strictRef)
+ {
+ /*
+ * do not trigger an invalid reference if the reference is present, but with a null value
+ * don't either for a quiet reference or inside an #if/#elseif evaluation context
+ */
+ if ((referenceType != QUIET_REFERENCE || warnInvalidQuietReferences) &&
+ (numChildren > 0 ||
+ (!context.containsKey(rootString) || warnInvalidNullReferences) &&
+ (!onlyTestingReference || warnInvalidTestedReferences)))
+ {
+ result = EventHandlerUtil.invalidGetMethod(rsvc, context,
+ rsvc.getParserConfiguration().getDollarChar() + rootString, null, null, uberInfo);
+ }
+
+ if (astAlternateValue != null && (!DuckType.asBoolean(result, true)))
+ {
+ result = astAlternateValue.value(context);
+ }
+
+ return result;
+ }
+
+ /*
+ * Iteratively work 'down' (it's flat...) the reference
+ * to get the value, but check to make sure that
+ * every result along the path is valid. For example:
+ *
+ * $hashtable.Customer.Name
+ *
+ * The $hashtable may be valid, but there is no key
+ * 'Customer' in the hashtable so we want to stop
+ * when we find a null value and return the null
+ * so the error gets logged.
+ */
+
+ try
+ {
+ Object previousResult = result;
+ int failedChild = -1;
+
+ for (int i = 0; i < numChildren; i++)
+ {
+ if (strictRef && result == null)
+ {
+ /*
+ * At this point we know that an attempt is about to be made
+ * to call a method or property on a null value.
+ */
+ String name = jjtGetChild(i).getFirstTokenImage();
+ throw new VelocityException("Attempted to access '"
+ + name + "' on a null value at "
+ + StringUtils.formatFileString(uberInfo.getTemplateName(),
+ + jjtGetChild(i).getLine(), jjtGetChild(i).getColumn()),
+ null, rsvc.getLogContext().getStackTrace());
+ }
+ previousResult = result;
+ result = jjtGetChild(i).execute(result,context);
+ if (result == null && !strictRef) // If strict and null then well catch this
+ // next time through the loop
+ {
+ failedChild = i;
+ break;
+ }
+ }
+
+ if (result == null)
+ {
+ if (failedChild == -1)
+ {
+ /*
+ * do not trigger an invalid reference if the reference is present, but with a null value
+ * don't either for a quiet reference,
+ * or inside an #if/#elseif evaluation context when there's no child
+ */
+ if ((!context.containsKey(rootString) || warnInvalidNullReferences) &&
+ (referenceType != QUIET_REFERENCE || warnInvalidQuietReferences) &&
+ (!onlyTestingReference || warnInvalidTestedReferences || numChildren > 0))
+ {
+ result = EventHandlerUtil.invalidGetMethod(rsvc, context,
+ rsvc.getParserConfiguration().getDollarChar() + rootString, previousResult, null, uberInfo);
+ }
+ }
+ else
+ {
+ Node child = jjtGetChild(failedChild);
+ // do not call bad reference handler if the getter is present
+ // (it means the getter has been called and returned null)
+ // do not either for a quiet reference or if the *last* child failed while testing the reference
+ Object getter = context.icacheGet(child);
+ if ((getter == null || warnInvalidNullReferences) &&
+ (referenceType != QUIET_REFERENCE || warnInvalidQuietReferences) &&
+ (!onlyTestingReference || warnInvalidTestedReferences || failedChild < numChildren - 1))
+ {
+ StringBuilder name = new StringBuilder(String.valueOf(rsvc.getParserConfiguration().getDollarChar())).append(rootString);
+ for (int i = 0; i <= failedChild; i++)
+ {
+ Node node = jjtGetChild(i);
+ if (node instanceof ASTMethod)
+ {
+ name.append(".").append(((ASTMethod) node).getMethodName()).append("()");
+ }
+ else
+ {
+ name.append(".").append(node.getFirstTokenImage());
+ }
+ }
+
+ if (child instanceof ASTMethod)
+ {
+ String methodName = ((ASTMethod) jjtGetChild(failedChild)).getMethodName();
+ result = EventHandlerUtil.invalidMethod(rsvc, context,
+ name.toString(), previousResult, methodName, uberInfo);
+ }
+ else
+ {
+ String property = jjtGetChild(failedChild).getFirstTokenImage();
+ result = EventHandlerUtil.invalidGetMethod(rsvc, context,
+ name.toString(), previousResult, property, uberInfo);
+ }
+ }
+ }
+ }
+
+ // Check alternate value at the end of the evaluation
+ if (astAlternateValue != null && (!DuckType.asBoolean(result, true)))
+ {
+ result = astAlternateValue.value(context);
+ }
+
+ return result;
+ }
+ catch(MethodInvocationException mie)
+ {
+ mie.setReferenceName(rootString);
+ throw mie;
+ }
+ }
+ finally
+ {
+ rsvc.getLogContext().popLogContext();
+ }
+ }
+
+ /**
+ * gets the value of the reference and outputs it to the
+ * writer.
+ *
+ * @param context context of data to use in getting value
+ * @param writer writer to render to
+ * @return True if rendering was successful.
+ * @throws IOException
+ * @throws MethodInvocationException
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer) throws IOException,
+ MethodInvocationException
+ {
+ try
+ {
+ rsvc.getLogContext().pushLogContext(this, uberInfo);
+
+ if (referenceType == RUNT)
+ {
+ writer.write(literal);
+ return true;
+ }
+
+ Object value = null;
+ if (escaped && strictEscape)
+ {
+ /*
+ * If we are in strict mode and the variable is escaped, then don't bother to
+ * retrieve the value since we won't use it. And if the var is not defined
+ * it will throw an exception. Set value to TRUE to fall through below with
+ * simply printing $foo, and not \$foo
+ */
+ value = Boolean.TRUE;
+ }
+ else
+ {
+ value = execute(null, context);
+ }
+
+ String localNullString = null;
+
+ /*
+ * if this reference is escaped (\$foo) then we want to do one of two things: 1) if this is
+ * a reference in the context, then we want to print $foo 2) if not, then \$foo (its
+ * considered schmoo, not VTL)
+ */
+
+ if (escaped)
+ {
+ localNullString = getNullString(context);
+
+ if (value == null)
+ {
+ writer.write(escPrefix);
+ writer.write("\\");
+ writer.write(localNullString);
+ }
+ else
+ {
+ writer.write(escPrefix);
+ writer.write(localNullString);
+ }
+ return true;
+ }
+
+ /*
+ * the normal processing
+ *
+ * if we have an event cartridge, get a new value object
+ */
+
+ value = EventHandlerUtil.referenceInsert(rsvc, context, literal, value);
+
+ String toString = null;
+ if (value != null)
+ {
+ if (value instanceof Renderable)
+ {
+ Renderable renderable = (Renderable)value;
+ try
+ {
+ writer.write(escPrefix);
+ writer.write(morePrefix);
+ if (renderable.render(context,writer))
+ {
+ return true;
+ }
+ }
+ catch(RuntimeException e)
+ {
+ // We commonly get here when an error occurs within a block reference.
+ // We want to log where the reference is at so that a developer can easily
+ // know where the offending call is located. This can be seen
+ // as another element of the error stack we report to log.
+ log.error("Exception rendering "
+ + ((renderable instanceof Reference)? "block ":"Renderable ")
+ + rootString + " at " + StringUtils.formatFileString(this));
+ throw e;
+ }
+ }
+
+ toString = DuckType.asString(value);
+ }
+
+ if (value == null || toString == null)
+ {
+ if (strictRef)
+ {
+ if (referenceType != QUIET_REFERENCE)
+ {
+ log.error("Prepend the reference with '$!' e.g., $!{}" +
+ " if you want Velocity to ignore the reference when it evaluates to null",
+ literal().substring(1));
+ if (value == null)
+ {
+ throw new VelocityException("Reference " + literal()
+ + " evaluated to null when attempting to render at "
+ + StringUtils.formatFileString(this)
+ , null, rsvc.getLogContext().getStackTrace());
+ }
+ else // toString == null
+ {
+ // This will probably rarely happen, but when it does we want to
+ // inform the user that toString == null so they don't pull there
+ // hair out wondering why Velocity thinks the value is null.
+ throw new VelocityException("Reference " + literal()
+ + " evaluated to object " + value.getClass().getName()
+ + " whose toString() method returned null at "
+ + StringUtils.formatFileString(this)
+ , null, rsvc.getLogContext().getStackTrace());
+ }
+ }
+ return true;
+ }
+
+ /*
+ * write prefix twice, because it's schmoo, so the \ don't escape each
+ * other...
+ */
+ localNullString = getNullString(context);
+ if (!strictEscape)
+ {
+ // If in strict escape mode then we only print escape once.
+ // Yea, I know.. brittle stuff
+ writer.write(escPrefix);
+ }
+ writer.write(escPrefix);
+ writer.write(morePrefix);
+ writer.write(localNullString);
+
+ if (logOnNull && referenceType != QUIET_REFERENCE)
+ {
+ log.debug("Null reference [template '{}', line {}, column {}]: {} cannot be resolved.",
+ getTemplateName(), this.getLine(), this.getColumn(), this.literal());
+ }
+ }
+ else
+ {
+ /*
+ * non-null processing
+ */
+ writer.write(escPrefix);
+ writer.write(morePrefix);
+ if (writer instanceof Filter)
+ {
+ ((Filter)writer).writeReference(toString);
+ }
+ else
+ {
+ writer.write(toString);
+ }
+
+ }
+ return true;
+ }
+ finally
+ {
+ rsvc.getLogContext().popLogContext();
+ }
+ }
+
+ /**
+ * This method helps to implement the "render literal if null" functionality.
+ *
+ * VelocimacroProxy saves references to macro arguments (AST nodes) so that if we have a macro
+ * #foobar($a $b) then there is key "$a.literal" which points to the literal presentation of the
+ * argument provided to variable $a. If the value of $a is null, we render the string that was
+ * provided as the argument.
+ *
+ * @param context
+ * @return
+ */
+ private String getNullString(InternalContextAdapter context)
+ {
+ String ret = nullString;
+
+ if (lookupAlternateLiteral)
+ {
+ Deque<String> alternateLiteralsStack = (Deque<String>)context.get(alternateNullStringKey);
+ if (alternateLiteralsStack != null && alternateLiteralsStack.size() > 0)
+ {
+ ret = alternateLiteralsStack.peekFirst();
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Computes boolean value of this reference
+ * Returns the actual value of reference return type
+ * boolean, and 'true' if value is not null
+ *
+ * @param context context to compute value with
+ * @return True if evaluation was ok.
+ * @throws MethodInvocationException
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ Object value = execute(this, context); // non-null object as first parameter by convention for 'evaluate'
+ if (value == null)
+ {
+ return false;
+ }
+ try
+ {
+ rsvc.getLogContext().pushLogContext(this, uberInfo);
+ return DuckType.asBoolean(value, checkEmpty);
+ }
+ catch(Exception e)
+ {
+ throw new VelocityException("Reference evaluation threw an exception at "
+ + StringUtils.formatFileString(this), e, rsvc.getLogContext().getStackTrace());
+ }
+ finally
+ {
+ rsvc.getLogContext().popLogContext();
+ }
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public Object value(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ return (computableReference ? execute(null, context) : null);
+ }
+
+
+ /**
+ * Utility class to handle nulls when printing a class type
+ * @param clazz
+ * @return class name, or the string "null"
+ */
+ public static String printClass(Class<?> clazz)
+ {
+ return clazz == null ? "null" : clazz.getName();
+ }
+
+
+ /**
+ * Sets the value of a complex reference (something like $foo.bar)
+ * Currently used by ASTSetReference()
+ *
+ * @see ASTSetDirective
+ *
+ * @param context context object containing this reference
+ * @param value Object to set as value
+ * @return true if successful, false otherwise
+ * @throws MethodInvocationException
+ */
+ public boolean setValue(InternalContextAdapter context, Object value)
+ throws MethodInvocationException
+ {
+ try
+ {
+ rsvc.getLogContext().pushLogContext(this, uberInfo);
+
+ if (astAlternateValue != null)
+ {
+ log.error("reference set cannot have a default value {}",
+ StringUtils.formatFileString(uberInfo));
+ return false;
+ }
+
+ if (numChildren == 0)
+ {
+ context.put(rootString, value);
+ return true;
+ }
+
+ /*
+ * The rootOfIntrospection is the object we will
+ * retrieve from the Context. This is the base
+ * object we will apply reflection to.
+ */
+
+ Object result = getRootVariableValue(context);
+
+ if (result == null)
+ {
+ log.error("reference set is not a valid reference at {}",
+ StringUtils.formatFileString(uberInfo));
+ return false;
+ }
+
+ /*
+ * How many child nodes do we have?
+ */
+
+ for (int i = 0; i < numChildren - 1; i++)
+ {
+ result = jjtGetChild(i).execute(result, context);
+
+ if (result == null)
+ {
+ if (strictRef)
+ {
+ String name = jjtGetChild(i+1).getFirstTokenImage();
+ throw new MethodInvocationException("Attempted to access '"
+ + name + "' on a null value", null, rsvc.getLogContext().getStackTrace(), name, uberInfo.getTemplateName(),
+ jjtGetChild(i+1).getLine(), jjtGetChild(i+1).getColumn());
+ }
+
+ log.error("reference set is not a valid reference at {}",
+ StringUtils.formatFileString(uberInfo));
+ return false;
+ }
+ }
+
+ if (astIndex != null)
+ {
+ // If astIndex is not null then we are actually setting an index reference,
+ // something of the form $foo[1] =, or in general any reference that ends with
+ // the brackets. This means that we need to call a more general method
+ // of the form set(Integer, <something>), or put(Object, <something), where
+ // the first parameter is the index value and the second is the LHS of the set.
+
+ Object argument = astIndex.jjtGetChild(0).value(context);
+ // If negative, turn -1 into (size - 1)
+ argument = ASTIndex.adjMinusIndexArg(argument, result, context, astIndex);
+ Object [] params = {argument, value};
+ Class<?>[] paramClasses = {params[0] == null ? null : params[0].getClass(),
+ params[1] == null ? null : params[1].getClass()};
+
+ String methodName = "set";
+ VelMethod method = ClassUtils.getMethod(methodName, params, paramClasses,
+ result, context, astIndex, false);
+
+ if (method == null)
+ {
+ // If we can't find a 'set' method, lets try 'put', This warrents a little
+ // investigation performance wise... if the user is using the hash
+ // form $foo["blaa"], then it may be expensive to first try and fail on 'set'
+ // then go to 'put'? The problem is that getMethod will try the cache, then
+ // perform introspection on 'result' for 'set'
+ methodName = "put";
+ method = ClassUtils.getMethod(methodName, params, paramClasses,
+ result, context, astIndex, false);
+ }
+
+ if (method == null)
+ {
+ // couldn't find set or put method, so bail
+ if (strictRef)
+ {
+ throw new VelocityException(
+ "Found neither a 'set' or 'put' method with param types '("
+ + printClass(paramClasses[0]) + "," + printClass(paramClasses[1])
+ + ")' on class '" + result.getClass().getName()
+ + "' at " + StringUtils.formatFileString(astIndex)
+ , null, rsvc.getLogContext().getStackTrace());
+ }
+ return false;
+ }
+
+ try
+ {
+ method.invoke(result, params);
+ }
+ catch(RuntimeException e)
+ {
+ // Kludge since invoke throws Exception, pass up Runtimes
+ throw e;
+ }
+ catch(Exception e)
+ {
+ throw new MethodInvocationException(
+ "Exception calling method '"
+ + methodName + "("
+ + printClass(paramClasses[0]) + "," + printClass(paramClasses[1])
+ + ")' in " + result.getClass(),
+ e.getCause(), rsvc.getLogContext().getStackTrace(), identifier, astIndex.getTemplateName(), astIndex.getLine(),
+ astIndex.getColumn());
+ }
+
+ return true;
+ }
+
+
+ /*
+ * We support two ways of setting the value in a #set($ref.foo = $value ):
+ * 1) ref.setFoo( value )
+ * 2) ref,put("foo", value ) to parallel the get() map introspection
+ */
+
+ try
+ {
+ VelPropertySet vs =
+ rsvc.getUberspect().getPropertySet(result, identifier,
+ value, uberInfo);
+
+ if (vs == null)
+ {
+ if (strictRef)
+ {
+ throw new MethodInvocationException("Object '" + result.getClass().getName() +
+ "' does not contain property '" + identifier + "'", null, rsvc.getLogContext().getStackTrace(), identifier,
+ uberInfo.getTemplateName(), uberInfo.getLine(), uberInfo.getColumn());
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ vs.invoke(result, value);
+ }
+ catch(InvocationTargetException ite)
+ {
+ /*
+ * this is possible
+ */
+
+ throw new MethodInvocationException(
+ "ASTReference: Invocation of method '"
+ + identifier + "' in " + result.getClass()
+ + " threw exception "
+ + ite.getTargetException().toString(),
+ ite.getTargetException(), rsvc.getLogContext().getStackTrace(), identifier, getTemplateName(), this.getLine(), this.getColumn());
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ throw e;
+ }
+ catch(Exception e)
+ {
+ /*
+ * maybe a security exception?
+ */
+ String msg = "ASTReference setValue(): exception: " + e
+ + " template at " + StringUtils.formatFileString(uberInfo);
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+
+ return true;
+ }
+ finally
+ {
+ rsvc.getLogContext().popLogContext();
+ }
+ }
+
+ private String getRoot()
+ {
+ Token t = getFirstToken();
+
+ /*
+ * we have a special case where something like
+ * $(\\)*!, where the user want's to see something
+ * like $!blargh in the output, but the ! prevents it from showing.
+ * I think that at this point, this isn't a reference.
+ */
+
+ /* so, see if we have "\\!" */
+
+ int slashbang = t.image.indexOf("\\!");
+
+ if (slashbang != -1)
+ {
+ if (strictEscape)
+ {
+ // If we are in strict escape mode, then we consider this type of
+ // pattern a non-reference, and we print it out as schmoo...
+ nullString = literal();
+ escaped = true;
+ return nullString;
+ }
+
+ /*
+ * lets do all the work here. I would argue that if this occurs,
+ * it's not a reference at all, so preceding \ characters in front
+ * of the $ are just schmoo. So we just do the escape processing
+ * trick (even | odd) and move on. This kind of breaks the rule
+ * pattern of $ and # but '!' really tosses a wrench into things.
+ */
+
+ /*
+ * count the escapes: even # -> not escaped, odd -> escaped
+ */
+
+ int i = 0;
+ int len = t.image.length();
+
+ i = t.image.indexOf(rsvc.getParserConfiguration().getDollarChar());
+
+ if (i == -1)
+ {
+ /* yikes! */
+ log.error("ASTReference.getRoot(): internal error: "
+ + "no $ found for slashbang.");
+ computableReference = false;
+ nullString = t.image;
+ return nullString;
+ }
+
+ while (i < len && t.image.charAt(i) != '\\')
+ {
+ i++;
+ }
+
+ /* ok, i is the first \ char */
+
+ int start = i;
+ int count = 0;
+
+ while (i < len && t.image.charAt(i++) == '\\')
+ {
+ count++;
+ }
+
+ /*
+ * now construct the output string. We really don't care about
+ * leading slashes as this is not a reference. It's quasi-schmoo
+ */
+
+ nullString = t.image.substring(0,start); // prefix up to the first
+ nullString += t.image.substring(start, start + count-1 ); // get the slashes
+ nullString += t.image.substring(start+count); // and the rest, including the
+
+ /*
+ * this isn't a valid reference, so lets short circuit the value
+ * and set calcs
+ */
+
+ computableReference = false;
+
+ return nullString;
+ }
+
+ /*
+ * we need to see if this reference is escaped. if so
+ * we will clean off the leading \'s and let the
+ * regular behavior determine if we should output this
+ * as \$foo or $foo later on in render(). Laziness..
+ */
+
+ escaped = false;
+
+ if (t.image.startsWith("\\"))
+ {
+ /*
+ * count the escapes: even # -> not escaped, odd -> escaped
+ */
+
+ int i = 0;
+ int len = t.image.length();
+
+ while (i < len && t.image.charAt(i) == '\\')
+ {
+ i++;
+ }
+
+ if ((i % 2) != 0)
+ escaped = true;
+
+ if (i > 0)
+ escPrefix = t.image.substring(0, i / 2 );
+
+ t.image = t.image.substring(i);
+ }
+
+ /*
+ * Look for preceding stuff like '#' and '$'
+ * and snip it off, except for the
+ * last $
+ */
+
+ int loc1 = t.image.lastIndexOf(rsvc.getParserConfiguration().getDollarChar());
+
+ /*
+ * if we have extra stuff, loc > 0
+ * ex. '#$foo' so attach that to
+ * the prefix.
+ */
+ if (loc1 > 0)
+ {
+ morePrefix = morePrefix + t.image.substring(0, loc1);
+ t.image = t.image.substring(loc1);
+ }
+
+ /*
+ * Now it should be clean. Get the literal in case this reference
+ * isn't backed by the context at runtime, and then figure out what
+ * we are working with.
+ */
+
+ // FIXME: this is the key to render nulls as literals, we need to look at context(refname+".literal")
+ nullString = literal();
+
+ if (t.image.startsWith("$!"))
+ {
+ referenceType = QUIET_REFERENCE;
+
+ /*
+ * only if we aren't escaped do we want to null the output
+ */
+
+ if (!escaped)
+ nullString = "";
+
+ if (t.image.startsWith("$!{"))
+ {
+ /*
+ * ex: $!{provider.Title}
+ */
+
+ return t.next.image;
+ }
+ else
+ {
+ /*
+ * ex: $!provider.Title
+ */
+
+ return t.image.substring(2);
+ }
+ }
+ else if (t.image.equals("${"))
+ {
+ /*
+ * ex: ${provider.Title}
+ */
+
+ referenceType = FORMAL_REFERENCE;
+ return t.next.image;
+ }
+ else if (t.image.startsWith("$"))
+ {
+ /*
+ * just nip off the '$' so we have
+ * the root
+ */
+
+ referenceType = NORMAL_REFERENCE;
+ return t.image.substring(1);
+ }
+ else
+ {
+ /*
+ * this is a 'RUNT', which can happen in certain circumstances where
+ * the parser is fooled into believing that an IDENTIFIER is a real
+ * reference. Another 'dreaded' MORE hack :).
+ */
+ referenceType = RUNT;
+ return t.image;
+ }
+
+ }
+
+ /**
+ * @param context
+ * @return The evaluated value of the variable.
+ * @throws MethodInvocationException
+ */
+ public Object getRootVariableValue(InternalContextAdapter context)
+ {
+ Object obj = null;
+ try
+ {
+ obj = context.get(rootString);
+ }
+ catch(RuntimeException e)
+ {
+ log.error("Exception calling reference ${} at {}",
+ rootString, StringUtils.formatFileString(uberInfo));
+ throw e;
+ }
+
+ if (obj == null && strictRef && astAlternateValue == null)
+ {
+ if (!context.containsKey(rootString))
+ {
+ log.error("Variable ${} has not been set at {}",
+ rootString, StringUtils.formatFileString(uberInfo));
+ throw new MethodInvocationException("Variable $" + rootString +
+ " has not been set", null, rsvc.getLogContext().getStackTrace(), identifier,
+ uberInfo.getTemplateName(), uberInfo.getLine(), uberInfo.getColumn());
+ }
+ }
+ return obj;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTSetDirective.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTSetDirective.java
new file mode 100644
index 00000000..0cf4d566
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTSetDirective.java
@@ -0,0 +1,304 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.app.event.EventHandlerUtil;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeConstants.SpaceGobbling;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.util.introspection.Info;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Node for the #set directive
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ASTSetDirective extends SimpleNode
+{
+ private String leftReference = "";
+ private Node right = null;
+ private ASTReference left = null;
+ private boolean isInitialized;
+ private String prefix = "";
+ private String postfix = "";
+
+ /*
+ * '#' and '$' prefix characters eaten by javacc MORE mode
+ */
+ private String morePrefix = "";
+
+
+ /**
+ * This is really immutable after the init, so keep one for this node
+ */
+ protected Info uberInfo;
+
+ /**
+ * Indicates if we are running in strict reference mode.
+ */
+ protected boolean strictRef = false;
+
+ /**
+ * @param id
+ */
+ public ASTSetDirective(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTSetDirective(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * simple init. We can get the RHS and LHS as the the tree structure is static
+ * @param context
+ * @param data
+ * @return Init result.
+ * @throws TemplateInitException
+ */
+ @Override
+ public synchronized Object init(InternalContextAdapter context, Object data)
+ throws TemplateInitException
+ {
+ /* This method is synchronized to prevent double initialization or initialization while rendering */
+
+ if (!isInitialized)
+ {
+ /*
+ * init the tree correctly
+ */
+
+ super.init( context, data );
+
+ /*
+ * handle '$' and '#' chars prefix
+ */
+ Token t = getFirstToken();
+ int pos = -1;
+ while (t != null && (pos = t.image.lastIndexOf(rsvc.getParserConfiguration().getHashChar())) == -1)
+ {
+ t = t.next;
+ }
+ if (t != null && pos > 0)
+ {
+ morePrefix = t.image.substring(0, pos);
+ }
+
+
+ uberInfo = new Info(getTemplateName(),
+ getLine(), getColumn());
+
+ right = getRightHandSide();
+ left = getLeftHandSide();
+
+ strictRef = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false);
+
+ /*
+ * grab this now. No need to redo each time
+ */
+ leftReference = left.firstImage.substring(1);
+
+ /* handle backward compatible space gobbling if asked so */
+ if (rsvc.getSpaceGobbling() == SpaceGobbling.BC)
+ {
+ Node previousNode = null;
+ for (int brother = 0; brother < parent.jjtGetNumChildren(); ++brother)
+ {
+ Node node = parent.jjtGetChild(brother);
+ if (node == this) break;
+ previousNode = node;
+ }
+ if (previousNode == null) prefix = "";
+ else if (previousNode instanceof ASTText)
+ {
+ ASTText text = (ASTText)previousNode;
+ if (text.getCtext().matches("[ \t]*"))
+ {
+ text.setCtext("");
+ }
+ }
+ else prefix = "";
+ }
+
+ isInitialized = true;
+
+ cleanupParserAndTokens();
+ }
+
+ return data;
+ }
+
+ /**
+ * set indentation prefix
+ * @param prefix
+ */
+ public void setPrefix(String prefix)
+ {
+ this.prefix = prefix;
+ }
+
+ /**
+ * get indentation prefix
+ * @return indentation prefix
+ */
+ public String getPrefix()
+ {
+ return prefix;
+ }
+
+ /**
+ * set indentation postfix
+ * @param postfix
+ */
+ public void setPostfix(String postfix)
+ {
+ this.postfix = postfix;
+ }
+
+ /**
+ * get indentation postfix
+ * @return indentation prefix
+ */
+ public String getPostfix()
+ {
+ return postfix;
+ }
+
+ /**
+ * puts the value of the RHS into the context under the key of the LHS
+ * @param context
+ * @param writer
+ * @return True if rendering was sucessful.
+ * @throws IOException
+ * @throws MethodInvocationException
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer)
+ throws IOException, MethodInvocationException
+ {
+ try
+ {
+ rsvc.getLogContext().pushLogContext(this, uberInfo);
+
+ SpaceGobbling spaceGobbling = rsvc.getSpaceGobbling();
+
+ /* Velocity 1.x space gobbling for #set is rather wacky:
+ prefix is eaten *only* if previous token is not a text node.
+ We handle this by appropriately emptying the prefix in BC mode.
+ */
+
+ if (morePrefix.length() > 0 || spaceGobbling.compareTo(SpaceGobbling.LINES) < 0)
+ {
+ writer.write(prefix);
+ }
+
+ writer.write(morePrefix);
+
+ /*
+ * get the RHS node, and its value
+ */
+
+ Object value = right.value(context);
+
+ if ( value == null && !strictRef)
+ {
+ String rightReference = null;
+ if (right instanceof ASTExpression)
+ {
+ rightReference = ((ASTExpression) right).lastImage;
+ }
+ EventHandlerUtil.invalidSetMethod(rsvc, context, leftReference, rightReference, uberInfo);
+ }
+
+ if (morePrefix.length() > 0 || spaceGobbling == SpaceGobbling.NONE)
+ {
+ writer.write(postfix);
+ }
+
+ return left.setValue(context, value);
+ }
+ finally
+ {
+ rsvc.getLogContext().popLogContext();
+ StringBuilder builder;
+ }
+ }
+
+ /**
+ * Returns the string "#set($<i>reference</i> = ...)". RHS is not rendered. This method is only
+ * used for displaying the VTL stacktrace when a rendering error is encountered when runtime.log.track_location is true.
+ * @return
+ */
+ @Override
+ public String literal()
+ {
+ if (literal != null)
+ {
+ return literal;
+ }
+ StringBuilder builder = new StringBuilder();
+ builder.append("#set(").append(left.literal()).append(" = ...)");
+ return literal = builder.toString();
+ }
+
+ /**
+ * returns the ASTReference that is the LHS of the set statement
+ *
+ * @return left hand side of #set statement
+ */
+ private ASTReference getLeftHandSide()
+ {
+ return (ASTReference) jjtGetChild(0);
+ }
+
+ /**
+ * returns the RHS Node of the set statement
+ *
+ * @return right hand side of #set statement
+ */
+ private Node getRightHandSide()
+ {
+ return jjtGetChild(1);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTStringLiteral.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTStringLiteral.java
new file mode 100644
index 00000000..44134727
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTStringLiteral.java
@@ -0,0 +1,364 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.util.StringBuilderWriter;
+import org.apache.velocity.util.StringUtils;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.Writer;
+
+/**
+ * ASTStringLiteral support. Will interpolate!
+ *
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @version $Id$
+ */
+public class ASTStringLiteral extends SimpleNode
+{
+ /* cache the value of the interpolation switch */
+ private boolean interpolate = true;
+
+ private SimpleNode nodeTree = null;
+
+ private String image = "";
+
+ /**
+ * @param id
+ */
+ public ASTStringLiteral(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTStringLiteral(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * init : we don't have to do much. Init the tree (there shouldn't be one)
+ * and then see if interpolation is turned on.
+ *
+ * @param context
+ * @param data
+ * @return Init result.
+ * @throws TemplateInitException
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data)
+ throws TemplateInitException
+ {
+ /*
+ * simple habit... we prollie don't have an AST beneath us
+ */
+
+ super.init(context, data);
+
+ /*
+ * the stringlit is set at template parse time, so we can do this here
+ * for now. if things change and we can somehow create stringlits at
+ * runtime, this must move to the runtime execution path
+ *
+ * so, only if interpolation is turned on AND it starts with a " AND it
+ * has a directive or reference, then we can interpolate. Otherwise,
+ * don't bother.
+ */
+
+ interpolate = rsvc.getBoolean(
+ RuntimeConstants.INTERPOLATE_STRINGLITERALS, true)
+ && getFirstToken().image.startsWith("\"")
+ && ((getFirstToken().image.indexOf(rsvc.getParserConfiguration().getDollarChar()) != -1) || (getFirstToken().image
+ .indexOf(rsvc.getParserConfiguration().getHashChar()) != -1));
+
+ /*
+ * get the contents of the string, minus the '/" at each end
+ */
+ String img = getFirstToken().image;
+
+ /*
+ the literal string *should* contain enclosing quotes
+ */
+ literal = img;
+
+ image = img.substring(1, img.length() - 1);
+
+ if (img.startsWith("\""))
+ {
+ image = unescape(image);
+ }
+ if (img.charAt(0) == '"' || img.charAt(0) == '\'' )
+ {
+ // replace double-double quotes like "" with a single double quote "
+ // replace double single quotes '' with a single quote '
+ image = replaceQuotes(image, img.charAt(0));
+ }
+
+ if (interpolate)
+ {
+ /*
+ * parse and init the nodeTree
+ */
+ StringReader br = new StringReader(image);
+
+ /*
+ * it's possible to not have an initialization context - or we don't
+ * want to trust the caller - so have a fallback value if so
+ *
+ * Also, do *not* dump the VM namespace for this template
+ */
+
+ Template template = null;
+ if (context != null)
+ {
+ template = (Template)context.getCurrentResource();
+ }
+ if (template == null)
+ {
+ template = new Template();
+ template.setName("StringLiteral");
+ }
+ try
+ {
+ nodeTree = rsvc.parse(br, template);
+ }
+ catch (ParseException e)
+ {
+ String msg = "Failed to parse String literal at "+
+ StringUtils.formatFileString(template.getName(), getLine(), getColumn());
+ throw new TemplateInitException(msg, e, rsvc.getLogContext().getStackTrace(), template.getName(), getColumn(), getLine());
+ }
+
+ adjTokenLineNums(nodeTree);
+
+ /*
+ * init with context. It won't modify anything
+ */
+
+ nodeTree.init(context, rsvc);
+ }
+
+ cleanupParserAndTokens();
+
+ return data;
+ }
+
+ /**
+ * Adjust all the line and column numbers that comprise a node so that they
+ * are corrected for the string literals position within the template file.
+ * This is necessary if an exception is thrown while processing the node so
+ * that the line and column position reported reflects the error position
+ * within the template and not just relative to the error position within
+ * the string literal.
+ * @param node
+ */
+ public void adjTokenLineNums(Node node)
+ {
+ Token tok = node.getFirstToken();
+ // Test against null is probably not necessary, but just being safe
+ while(tok != null && tok != node.getLastToken())
+ {
+ // If tok is on the first line, then the actual column is
+ // offset by the template column.
+
+ if (tok.beginLine == 1)
+ tok.beginColumn += getColumn();
+
+ if (tok.endLine == 1)
+ tok.endColumn += getColumn();
+
+ tok.beginLine += getLine()- 1;
+ tok.endLine += getLine() - 1;
+ tok = tok.next;
+ }
+ }
+
+ /**
+ * Replaces double double-quotes with a single double quote ("" to ").
+ * Replaces double single quotes with a single quote ('' to ').
+ *
+ * @param s StringLiteral without the surrounding quotes
+ * @param literalQuoteChar char that starts the StringLiteral (" or ')
+ */
+ private String replaceQuotes(String s, char literalQuoteChar)
+ {
+ if( (literalQuoteChar == '"' && !s.contains("\"")) ||
+ (literalQuoteChar == '\'' && !s.contains("'")) )
+ {
+ return s;
+ }
+
+ StringBuilder result = new StringBuilder(s.length());
+ for(int i = 0, is = s.length(); i < is; i++)
+ {
+ char c = s.charAt(i);
+ result.append(c);
+
+ if( i + 1 < is )
+ {
+ char next = s.charAt(i + 1);
+ // '""' -> "", "''" -> ''
+ // thus it is not necessary to double quotes if the "surrounding" quotes
+ // of the StringLiteral are different. See VELOCITY-785
+ if( (literalQuoteChar == '"' && (next == '"' && c == '"')) ||
+ (literalQuoteChar == '\'' && (next == '\'' && c == '\'')) )
+ {
+ i++;
+ }
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * @param string
+ * @return unescaped string
+ * @since 1.6
+ */
+ public static String unescape(final String string)
+ {
+ int u = string.indexOf("\\u");
+ if (u < 0) return string;
+
+ StringBuilder result = new StringBuilder();
+
+ int lastCopied = 0;
+
+ for (;;)
+ {
+ result.append(string.substring(lastCopied, u));
+
+ /* we don't worry about an exception here,
+ * because the lexer checked that string is correct */
+ char c = (char) Integer.parseInt(string.substring(u + 2, u + 6), 16);
+ result.append(c);
+
+ lastCopied = u + 6;
+
+ u = string.indexOf("\\u", lastCopied);
+ if (u < 0)
+ {
+ result.append(string.substring(lastCopied));
+ return result.toString();
+ }
+ }
+ }
+
+
+ /**
+ * @param visitor
+ * @param data
+ * @return rendered object
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor,
+ * java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * Check to see if this is an interpolated string.
+ * @return true if this is constant (not an interpolated string)
+ * @since 1.6
+ */
+ public boolean isConstant()
+ {
+ return !interpolate;
+ }
+
+ /**
+ * renders the value of the string literal If the properties allow, and the
+ * string literal contains a $ or a # the literal is rendered against the
+ * context Otherwise, the stringlit is returned.
+ *
+ * @param context
+ * @return result of the rendering.
+ */
+ @Override
+ public Object value(InternalContextAdapter context)
+ {
+ if (interpolate)
+ {
+ try
+ {
+ /*
+ * now render against the real context
+ */
+
+ Writer writer = new StringBuilderWriter();
+ nodeTree.render(context, writer);
+
+ /*
+ * and return the result as a String
+ */
+
+ return writer.toString();
+ }
+
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch (RuntimeException e)
+ {
+ throw e;
+ }
+
+ catch (IOException e)
+ {
+ String msg = "Error in interpolating string literal";
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+
+ }
+
+ /*
+ * ok, either not allowed to interpolate, there wasn't a ref or
+ * directive, or we failed, so just output the literal
+ */
+
+ return image;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#evaluate(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ {
+ String str = (String)value(context);
+ return str != null && (!rsvc.getBoolean(RuntimeConstants.CHECK_EMPTY_OBJECTS, true) || !str.isEmpty());
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTSubtractNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTSubtractNode.java
new file mode 100644
index 00000000..25642c13
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTSubtractNode.java
@@ -0,0 +1,67 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ * Handles subtraction of nodes (in #set() )<br><br>
+ *
+ * Please look at the Parser.jjt file which is
+ * what controls the generation of this class.
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @author <a href="mailto:pero@antaramusic.de">Peter Romianowski</a>
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ASTSubtractNode extends ASTMathNode
+{
+ /**
+ * @param id
+ */
+ public ASTSubtractNode(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTSubtractNode(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ @Override
+ public String getLiteralOperator()
+ {
+ return "-";
+ }
+
+ @Override
+ public Number perform(Number left, Number right, InternalContextAdapter context)
+ {
+ return MathUtils.subtract(left, right);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTText.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTText.java
new file mode 100644
index 00000000..0f4be56f
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTText.java
@@ -0,0 +1,122 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.runtime.parser.Token;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ *
+ */
+public class ASTText extends SimpleNode
+{
+ private String ctext;
+
+ /**
+ * @param id
+ */
+ public ASTText(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTText(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * text getter
+ * @return ctext
+ */
+ public String getCtext()
+ {
+ return ctext;
+ }
+
+ /**
+ * text setter
+ * @param ctext
+ */
+ public void setCtext(String ctext)
+ {
+ this.ctext = ctext;
+ }
+
+ /**
+ * text getter
+ * @return literal representation
+ */
+ @Override
+ public String literal()
+ {
+ return ctext;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data)
+ throws TemplateInitException
+ {
+ StringBuilder builder = new StringBuilder();
+ Token t = getFirstToken();
+ for (; t != getLastToken(); t = t.next)
+ {
+ builder.append(NodeUtils.tokenLiteral(parser, t));
+ }
+ builder.append(NodeUtils.tokenLiteral(parser, t));
+ ctext = builder.toString();
+
+ cleanupParserAndTokens();
+
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#render(org.apache.velocity.context.InternalContextAdapter, java.io.Writer)
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer)
+ throws IOException
+ {
+ writer.write(ctext);
+ return true;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTTextblock.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTTextblock.java
new file mode 100644
index 00000000..d133b292
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTTextblock.java
@@ -0,0 +1,94 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.runtime.parser.Token;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * This node holds the "Textblock" data which should not be interpreted by Velocity.
+ *
+ * Textblocks are marked in Velocity with #[[content here]]# notation. Velocity
+ * will output everything between the markers and does not attempt to parse it in any way.
+ */
+public class ASTTextblock extends SimpleNode
+{
+ public final String START;
+ public final String END;
+ private char[] ctext;
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTTextblock(Parser p, int id)
+ {
+ super(p, id);
+ START = parser.hash() + "[[";
+ END = "]]" + parser.hash();
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data)
+ throws TemplateInitException
+ {
+ Token t = getFirstToken();
+
+ String text = t.image;
+
+ // t.image is in format: #[[ <string> ]]#
+ // we must strip away the hash tags
+ text = text.substring(START.length(), text.length() - END.length());
+
+ ctext = text.toCharArray();
+
+ cleanupParserAndTokens();
+
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#render(org.apache.velocity.context.InternalContextAdapter, java.io.Writer)
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer)
+ throws IOException
+ {
+ writer.write(ctext);
+ return true;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTTrue.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTTrue.java
new file mode 100644
index 00000000..7763ed60
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTTrue.java
@@ -0,0 +1,89 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ *
+ */
+public class ASTTrue extends SimpleNode
+{
+ private static Boolean value = Boolean.TRUE;
+
+ /**
+ * @param id
+ */
+ public ASTTrue(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTTrue(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#evaluate(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ {
+ return true;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public Object value(InternalContextAdapter context)
+ {
+ return value;
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTVariable.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTVariable.java
new file mode 100644
index 00000000..6b104667
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTVariable.java
@@ -0,0 +1,70 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+
+/**
+ *
+ */
+public class ASTVariable extends SimpleNode
+{
+ /**
+ * @param id
+ */
+ public ASTVariable(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTVariable(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTWord.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTWord.java
new file mode 100644
index 00000000..9493babf
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTWord.java
@@ -0,0 +1,69 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ *
+ */
+public class ASTWord extends SimpleNode
+{
+ /**
+ * @param id
+ */
+ public ASTWord(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTWord(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ saveTokenImages();
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTprocess.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTprocess.java
new file mode 100644
index 00000000..e9c67039
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTprocess.java
@@ -0,0 +1,68 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.parser.Parser;
+
+/**
+ *
+ */
+public class ASTprocess extends SimpleNode
+{
+ /**
+ * @param id
+ */
+ public ASTprocess(int id)
+ {
+ super(id);
+ }
+
+ /**
+ * @param p
+ * @param id
+ */
+ public ASTprocess(Parser p, int id)
+ {
+ super(p, id);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ Object obj = super.init(context, data);
+ cleanupParserAndTokens(); // drop reference to Parser and all JavaCC Tokens
+ return obj;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/AbstractExecutor.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/AbstractExecutor.java
new file mode 100644
index 00000000..d29b6cfe
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/AbstractExecutor.java
@@ -0,0 +1,83 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.slf4j.Logger;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Abstract class that is used to execute an arbitrary
+ * method that is in introspected. This is the superclass
+ * for the GetExecutor and PropertyExecutor.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@apache.org">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public abstract class AbstractExecutor
+{
+ /** */
+ protected Logger log = null;
+
+ /**
+ * Method to be executed.
+ */
+ private Method method = null;
+
+ /**
+ * Execute method against context.
+ * @param o
+ * @return The resulting object.
+ * @throws IllegalAccessException
+ * @throws InvocationTargetException
+ */
+ public abstract Object execute(Object o)
+ throws IllegalAccessException, InvocationTargetException;
+
+ /**
+ * Tell whether the executor is alive by looking
+ * at the value of the method.
+ *
+ * @return True if executor is alive.
+ */
+ public boolean isAlive()
+ {
+ return (method != null);
+ }
+
+ /**
+ * @return The current method.
+ */
+ public Method getMethod()
+ {
+ return method;
+ }
+
+ /**
+ * @param method
+ * @since 1.5
+ */
+ protected void setMethod(final Method method)
+ {
+ this.method = method;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/BooleanPropertyExecutor.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/BooleanPropertyExecutor.java
new file mode 100644
index 00000000..1c834f8c
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/BooleanPropertyExecutor.java
@@ -0,0 +1,123 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.util.introspection.Introspector;
+import org.slf4j.Logger;
+
+/**
+ * Handles discovery and valuation of a
+ * boolean object property, of the
+ * form public boolean is&lt;property&gt; when executed.
+ *
+ * We do this separately as to preserve the current
+ * quasi-broken semantics of get&lt;as is property&gt;
+ * get&lt;flip 1st char&gt; get("property") and now followed
+ * by is&lt;Property&gt;
+ *
+ * @author <a href="geirm@apache.org">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class BooleanPropertyExecutor extends PropertyExecutor
+{
+ /**
+ * @param log
+ * @param introspector
+ * @param clazz
+ * @param property
+ * @since 1.5
+ */
+ public BooleanPropertyExecutor(final Logger log, final Introspector introspector,
+ final Class<?> clazz, final String property)
+ {
+ this(log, introspector, clazz, property, false);
+ }
+
+ /**
+ * @param log
+ * @param introspector
+ * @param clazz
+ * @param property
+ * @param wrapArray
+ * @since 1.5
+ */
+ public BooleanPropertyExecutor(final Logger log, final Introspector introspector,
+ final Class<?> clazz, final String property, final boolean wrapArray)
+ {
+ super(log, introspector, clazz, property, wrapArray);
+ }
+
+ @Override
+ protected void discover(final Class<?> clazz, final String property)
+ {
+ try
+ {
+ Object [] params = {};
+
+ StringBuilder sb = new StringBuilder("is");
+ sb.append(property);
+
+ setMethod(getIntrospector().getMethod(clazz, sb.toString(), params));
+
+ if (!isAlive())
+ {
+ /*
+ * now the convenience, flip the 1st character
+ */
+
+ char c = sb.charAt(2);
+
+ if (Character.isLowerCase(c))
+ {
+ sb.setCharAt(2, Character.toUpperCase(c));
+ }
+ else
+ {
+ sb.setCharAt(2, Character.toLowerCase(c));
+ }
+
+ setMethod(getIntrospector().getMethod(clazz, sb.toString(), params));
+ }
+
+ if (isAlive())
+ {
+ if( getMethod().getReturnType() != Boolean.TYPE &&
+ getMethod().getReturnType() != Boolean.class )
+ {
+ setMethod(null);
+ }
+ }
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ throw e;
+ }
+ catch(Exception e)
+ {
+ String msg = "Exception while looking for boolean property getter for '" + property;
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/GetExecutor.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/GetExecutor.java
new file mode 100644
index 00000000..e08d2c69
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/GetExecutor.java
@@ -0,0 +1,120 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.util.introspection.Introspector;
+import org.slf4j.Logger;
+
+import java.lang.reflect.InvocationTargetException;
+
+
+/**
+ * Executor that simply tries to execute a get(key)
+ * operation. This will try to find a get(key) method
+ * for any type of object, not just objects that
+ * implement the Map interface as was previously
+ * the case.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @version $Id$
+ */
+public class GetExecutor extends AbstractExecutor
+{
+ private final Introspector introspector;
+
+ // This is still threadsafe because this object is only read except in the C'tor.
+ private Object [] params = {};
+
+ /**
+ * @param log
+ * @param introspector
+ * @param clazz
+ * @param property
+ * @since 1.5
+ */
+ public GetExecutor(final Logger log, final Introspector introspector,
+ final Class<?> clazz, final String property)
+ {
+ this.log = log;
+ this.introspector = introspector;
+
+ // If you passed in null as property, we don't use the value
+ // for parameter lookup. Instead we just look for get() without
+ // any parameters.
+ //
+ // In any other case, the following condition will set up an array
+ // for looking up get(String) on the class.
+
+ if (property != null)
+ {
+ this.params = new Object[] { property };
+ }
+ discover(clazz);
+ }
+
+ /**
+ * @param clazz
+ * @since 1.5
+ */
+ protected void discover(final Class<?> clazz)
+ {
+ try
+ {
+ setMethod(introspector.getMethod(clazz, "get", params));
+ /* get(Number) or get(integral) are NOT admissible,
+ * as the key is a string
+ */
+ if (getMethod() != null)
+ {
+ Class<?> [] args = getMethod().getParameterTypes();
+ if (args.length == 1 && (args[0].isPrimitive() || Number.class.isAssignableFrom(args[0])))
+ {
+ setMethod(null);
+ }
+ }
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ throw e;
+ }
+ catch(Exception e)
+ {
+ String msg = "Exception while looking for get('" + params[0] + "') method";
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ }
+
+ /**
+ * @param o
+ * @return value
+ * @see org.apache.velocity.runtime.parser.node.AbstractExecutor#execute(java.lang.Object)
+ */
+ @Override
+ public Object execute(final Object o)
+ throws IllegalAccessException, InvocationTargetException
+ {
+ return isAlive() ? getMethod().invoke(o, params) : null;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/IndentationFixer.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/IndentationFixer.java
new file mode 100644
index 00000000..907aaecb
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/IndentationFixer.java
@@ -0,0 +1,362 @@
+package org.apache.velocity.runtime.parser.node;
+
+import org.apache.velocity.runtime.directive.Directive;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class to fix indentation in structured mode.
+ */
+
+public class IndentationFixer implements ParserVisitor
+{
+ protected String parentIndentation = null;
+ protected String extraIndentation = null;
+ protected Pattern fix = null;
+
+ protected void fillExtraIndentation(String prefix)
+ {
+ Pattern captureExtraIndentation = Pattern.compile("^" + parentIndentation + "(\\s+)");
+ Matcher matcher = captureExtraIndentation.matcher(prefix);
+ if (matcher.find())
+ {
+ extraIndentation = matcher.group(1);
+ fix = Pattern.compile("^" + parentIndentation + extraIndentation, Pattern.MULTILINE);
+ }
+ else
+ {
+ extraIndentation = "";
+ }
+ }
+
+ public IndentationFixer(String parentIndentation)
+ {
+ this.parentIndentation = parentIndentation;
+ }
+
+ @Override
+ public Object visit(SimpleNode node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTprocess node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTText node, Object data)
+ {
+ String text = node.getCtext();
+ if (extraIndentation == null)
+ {
+ fillExtraIndentation(text);
+ }
+ if (extraIndentation.length() > 0)
+ {
+ Matcher matcher = fix.matcher(text);
+ node.setCtext(matcher.replaceAll(parentIndentation));
+ }
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTEscapedDirective node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTEscape node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTComment node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTTextblock node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTFloatingPointLiteral node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTIntegerLiteral node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTStringLiteral node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTIdentifier node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTWord node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTDirectiveAssign node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTDirective node, Object data)
+ {
+ String prefix = node.getPrefix();
+ if (prefix.length() > 0)
+ {
+ if (extraIndentation == null)
+ {
+ fillExtraIndentation(prefix);
+ }
+ if (extraIndentation.length() > 0)
+ {
+ Matcher matcher = fix.matcher(prefix);
+ node.setPrefix(matcher.replaceAll(parentIndentation));
+ if (node.getDirectiveType() == Directive.BLOCK)
+ {
+ node.childrenAccept(this, null);
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTBlock node, Object data)
+ {
+ String prefix = node.getPrefix();
+ if (prefix.length() > 0)
+ {
+ node.childrenAccept(this, null);
+ }
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTMap node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTObjectArray node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTIntegerRange node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTMethod node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTIndex node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTReference node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTTrue node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTFalse node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTIfStatement node, Object data)
+ {
+ String prefix = node.getPrefix();
+ if (prefix.length() > 0)
+ {
+ if (extraIndentation == null)
+ {
+ fillExtraIndentation(prefix);
+ }
+ if (extraIndentation.length() > 0)
+ {
+ Matcher matcher = fix.matcher(prefix);
+ node.setPrefix(matcher.replaceAll(parentIndentation));
+ node.childrenAccept(this, null);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTElseStatement node, Object data)
+ {
+ if (extraIndentation != null && extraIndentation.length() > 0)
+ {
+ node.childrenAccept(this, null);
+ }
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTElseIfStatement node, Object data)
+ {
+ if (extraIndentation != null && extraIndentation.length() > 0)
+ {
+ node.childrenAccept(this, null);
+ }
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTSetDirective node, Object data)
+ {
+ String prefix = node.getPrefix();
+ if (prefix.length() > 0)
+ {
+ if (extraIndentation == null)
+ {
+ fillExtraIndentation(prefix);
+ }
+ if (extraIndentation.length() > 0)
+ {
+ Matcher matcher = fix.matcher(prefix);
+ node.setPrefix(matcher.replaceAll(parentIndentation));
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTExpression node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTAssignment node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTOrNode node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTAndNode node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTEQNode node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTNENode node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTLTNode node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTGTNode node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTLENode node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTGENode node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTAddNode node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTSubtractNode node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTMulNode node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTDivNode node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTModNode node, Object data)
+ {
+ return null;
+ }
+
+ @Override
+ public Object visit(ASTNotNode node, Object data)
+ {
+ return null;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/JJTParserState.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/JJTParserState.java
new file mode 100644
index 00000000..9f84bfb0
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/JJTParserState.java
@@ -0,0 +1,5 @@
+package org.apache.velocity.runtime.parser.node;
+
+public class JJTParserState extends JJTStandardParserState
+{
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/MapGetExecutor.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/MapGetExecutor.java
new file mode 100644
index 00000000..2c9aaea5
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/MapGetExecutor.java
@@ -0,0 +1,105 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.slf4j.Logger;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+
+/**
+ * GetExecutor that is smart about Maps. If it detects one, it does not
+ * use Reflection but a cast to access the getter.
+ *
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ * @since 1.5
+ */
+public class MapGetExecutor
+ extends AbstractExecutor
+{
+ private final String property;
+ private final boolean isAlive;
+
+ public MapGetExecutor(final Logger log, final Object object, final String property)
+ {
+ this.log = log;
+ this.property = property;
+ isAlive = discover(object);
+ }
+
+ @Override
+ public Method getMethod()
+ {
+ if (isAlive())
+ {
+ return MapGetMethod.instance();
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isAlive()
+ {
+ return isAlive;
+ }
+
+ protected boolean discover (final Object object)
+ {
+ if (object instanceof Map)
+ {
+ if (property != null)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Object execute(final Object o)
+ {
+ return ((Map) o).get(property);
+ }
+
+ private static final class MapGetMethod
+ {
+ private static final Method instance;
+
+ static
+ {
+ try
+ {
+ instance = Map.class.getMethod("get", Object.class);
+ }
+ catch (final NoSuchMethodException mapGetMethodMissingError)
+ {
+ throw new Error(mapGetMethodMissingError);
+ }
+ }
+
+ private MapGetMethod() { }
+
+ static Method instance()
+ {
+ return instance;
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/MapSetExecutor.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/MapSetExecutor.java
new file mode 100644
index 00000000..3dd3301a
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/MapSetExecutor.java
@@ -0,0 +1,84 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.VelocityException;
+import org.slf4j.Logger;
+
+import java.util.Map;
+
+/**
+ * SetExecutor that is smart about Maps. If it detects one, it does not
+ * use Reflection but a cast to access the setter.
+ *
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ * @since 1.5
+ */
+public class MapSetExecutor
+ extends SetExecutor
+{
+ private final String property;
+
+ public MapSetExecutor(final Logger log, final Class clazz, final String property)
+ {
+ this.log = log;
+ this.property = property;
+ discover(clazz);
+ }
+
+ protected void discover (final Class<?> clazz)
+ {
+ Class<?> [] interfaces = clazz.getInterfaces();
+ for (Class<?> anInterface : interfaces)
+ {
+ if (anInterface.equals(Map.class))
+ {
+ try
+ {
+ if (property != null)
+ {
+ setMethod(Map.class.getMethod("put", Object.class, Object.class));
+ }
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch (RuntimeException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ String msg = "Exception while looking for put('" + property + "') method";
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ break;
+ }
+ }
+ }
+
+ @Override
+ public Object execute(final Object o, final Object arg)
+ {
+ return ((Map) o).put(property, arg);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/MathUtils.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/MathUtils.java
new file mode 100644
index 00000000..52cedccb
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/MathUtils.java
@@ -0,0 +1,539 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Utility-class for all arithmetic-operations.<br><br>
+ *
+ * All operations (+ - / *) return a Number which type is the type of the bigger argument.<br>
+ * Example:<br>
+ * <code>add ( new Integer(10), new Integer(1))</code> will return an <code>Integer</code>-Object with the value 11<br>
+ * <code>add ( new Long(10), new Integer(1))</code> will return an <code>Long</code>-Object with the value 11<br>
+ * <code>add ( new Integer(10), new Float(1))</code> will return an <code>Float</code>-Object with the value 11<br><br>
+ *
+ * Overflow checking:<br>
+ * For integral values (byte, short, int) there is an implicit overflow correction (the next "bigger"
+ * type will be returned). For example, if you call <code>add (new Integer (Integer.MAX_VALUE), 1)</code> a
+ * <code>Long</code>-object will be returned with the correct value of <code>Integer.MAX_VALUE+1</code>.<br>
+ * In addition to that the methods <code>multiply</code>,<code>add</code> and <code>substract</code> implement overflow
+ * checks for <code>long</code>-values. That means that if an overflow occurs while working with long values a BigInteger
+ * will be returned.<br>
+ * For all other operations and types (such as Float and Double) there is no overflow checking.
+ *
+ * @author <a href="mailto:pero@antaramusic.de">Peter Romianowski</a>
+ * @since 1.5
+ */
+public abstract class MathUtils
+{
+
+ /**
+ * A BigDecimal representing the number 0
+ */
+ protected static final BigDecimal DECIMAL_ZERO = new BigDecimal ( BigInteger.ZERO );
+
+ /**
+ * The constants are used to determine in which context we have to calculate.
+ */
+ protected static final int BASE_LONG = 0;
+ protected static final int BASE_FLOAT = 1;
+ protected static final int BASE_DOUBLE = 2;
+ protected static final int BASE_BIGINTEGER = 3;
+ protected static final int BASE_BIGDECIMAL = 4;
+
+ /**
+ * The <code>Class</code>-object is key, the maximum-value is the value
+ */
+ private static final Map<Class<? extends Number>, BigDecimal> ints = new HashMap<>();
+ static
+ {
+ ints.put (Byte.class, BigDecimal.valueOf (Byte.MAX_VALUE));
+ ints.put (Short.class, BigDecimal.valueOf (Short.MAX_VALUE));
+ ints.put (Integer.class, BigDecimal.valueOf (Integer.MAX_VALUE));
+ ints.put (Long.class, BigDecimal.valueOf (Long.MAX_VALUE));
+ ints.put (BigInteger.class, BigDecimal.valueOf (-1));
+ }
+
+ /**
+ * The "size" of the number-types - ascending.
+ */
+ private static final List<Class<? extends Number>> typesBySize = new ArrayList<>();
+ static
+ {
+ typesBySize.add (Byte.class);
+ typesBySize.add (Short.class);
+ typesBySize.add (Integer.class);
+ typesBySize.add (Long.class);
+ typesBySize.add (Float.class);
+ typesBySize.add (Double.class);
+ }
+
+ /**
+ * Convert the given Number to a BigDecimal
+ * @param n
+ * @return The number as BigDecimal
+ */
+ public static BigDecimal toBigDecimal (Number n)
+ {
+
+ if (n instanceof BigDecimal)
+ {
+ return (BigDecimal)n;
+ }
+
+ if (n instanceof BigInteger)
+ {
+ return new BigDecimal ( (BigInteger)n );
+ }
+
+ return BigDecimal.valueOf(n.doubleValue());
+
+ }
+
+ /**
+ * Convert the given Number to a BigInteger
+ * @param n
+ * @return The number as BigInteger
+ */
+ public static BigInteger toBigInteger (Number n)
+ {
+
+ if (n instanceof BigInteger)
+ {
+ return (BigInteger)n;
+ }
+
+ return BigInteger.valueOf (n.longValue());
+
+ }
+
+ /**
+ * Compare the given Number to 0.
+ * @param n
+ * @return True if number is 0.
+ */
+ public static boolean isZero (Number n)
+ {
+ if (isInteger( n ) )
+ {
+ if (n instanceof BigInteger)
+ {
+ return ((BigInteger)n).compareTo (BigInteger.ZERO) == 0;
+ }
+ return n.doubleValue() == 0;
+ }
+ if (n instanceof Float)
+ {
+ return n.floatValue() == 0f;
+ }
+ if (n instanceof Double)
+ {
+ return n.doubleValue() == 0d;
+ }
+ return toBigDecimal( n ).compareTo( DECIMAL_ZERO) == 0;
+ }
+
+ /**
+ * Test, whether the given object is an integer value
+ * (Byte, Short, Integer, Long, BigInteger)
+ * @param n
+ * @return True if n is an integer.
+ */
+ public static boolean isInteger (Number n)
+ {
+ return ints.containsKey (n.getClass());
+ }
+
+ /**
+ * Wrap the given primitive into the given class if the value is in the
+ * range of the destination type. If not the next bigger type will be chosen.
+ * @param value
+ * @param type
+ * @return Number object representing the primitive.
+ */
+ public static Number wrapPrimitive (long value, Class<?> type)
+ {
+ if (type == Byte.class)
+ {
+ if (value > Byte.MAX_VALUE || value < Byte.MIN_VALUE)
+ {
+ type = Short.class;
+ }
+ else
+ {
+ return (byte) value;
+ }
+ }
+ if (type == Short.class)
+ {
+ if (value > Short.MAX_VALUE || value < Short.MIN_VALUE)
+ {
+ type = Integer.class;
+ }
+ else
+ {
+ return (short) value;
+ }
+ }
+ if (type == Integer.class)
+ {
+ if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE)
+ {
+ type = Long.class;
+ }
+ else
+ {
+ return (int) value;
+ }
+ }
+ if (type == Long.class)
+ {
+ return value;
+ }
+ return BigInteger.valueOf(value);
+ }
+
+ /**
+ * Wrap the result in the object of the bigger type.
+ *
+ * @param value result of operation (as a long) - used to check size
+ * @param op1 first operand of binary operation
+ * @param op2 second operand of binary operation
+ * @return Number object of appropriate size to fit the value and operators
+ */
+ private static Number wrapPrimitive (long value, Number op1, Number op2)
+ {
+ if ( typesBySize.indexOf( op1.getClass()) > typesBySize.indexOf( op2.getClass()))
+ {
+ return wrapPrimitive(value, op1.getClass());
+ }
+ return wrapPrimitive(value, op2.getClass());
+ }
+
+ /**
+ * Find the common Number-type to be used in calculations.
+ *
+ * @param op1 first operand of binary operation
+ * @param op2 second operand of binary operation
+ * @return constant indicating type of Number to use in calculations
+ */
+ private static int findCalculationBase (Number op1, Number op2)
+ {
+
+ boolean op1Int = isInteger(op1);
+ boolean op2Int = isInteger(op2);
+
+ if ( (op1 instanceof BigDecimal || op2 instanceof BigDecimal) ||
+ ( (!op1Int || !op2Int) && (op1 instanceof BigInteger || op2 instanceof BigInteger)) )
+ {
+ return BASE_BIGDECIMAL;
+ }
+
+ if (op1Int && op2Int) {
+ if (op1 instanceof BigInteger || op2 instanceof BigInteger)
+ {
+ return BASE_BIGINTEGER;
+ }
+ return BASE_LONG;
+ }
+
+ if ((op1 instanceof Double) || (op2 instanceof Double))
+ {
+ return BASE_DOUBLE;
+ }
+ return BASE_FLOAT;
+ }
+
+ /**
+ * Find the Number-type to be used for a single number
+ *
+ * @param op operand
+ * @return constant indicating type of Number to use in calculations
+ */
+ public static int findCalculationBase(Number op)
+ {
+ if (isInteger(op))
+ {
+ if (op instanceof BigInteger)
+ {
+ return BASE_BIGINTEGER;
+ }
+ return BASE_LONG;
+ } else if (op instanceof BigDecimal)
+ {
+ return BASE_BIGDECIMAL;
+ } else if (op instanceof Double)
+ {
+ return BASE_DOUBLE;
+ }
+ return BASE_FLOAT;
+ }
+
+ /**
+ * Add two numbers and return the correct value / type.
+ * Overflow detection is done for integer values (byte, short, int, long) only!
+ * @param op1
+ * @param op2
+ * @return Addition result.
+ */
+ public static Number add (Number op1, Number op2)
+ {
+
+ int calcBase = findCalculationBase( op1, op2);
+ switch (calcBase)
+ {
+ case BASE_BIGINTEGER:
+ return toBigInteger( op1 ).add( toBigInteger( op2 ));
+ case BASE_LONG:
+ long l1 = op1.longValue();
+ long l2 = op2.longValue();
+ long result = l1+l2;
+
+ // Overflow check
+ if ((result ^ l1) < 0 && (result ^ l2) < 0)
+ {
+ return toBigInteger( op1).add( toBigInteger( op2));
+ }
+ return wrapPrimitive( result, op1, op2);
+ case BASE_FLOAT:
+ return op1.floatValue() + op2.floatValue();
+ case BASE_DOUBLE:
+ return op1.doubleValue() + op2.doubleValue();
+
+ // Default is BigDecimal operation
+ default:
+ return toBigDecimal( op1 ).add( toBigDecimal( op2 ));
+ }
+ }
+
+ /**
+ * Subtract two numbers and return the correct value / type.
+ * Overflow detection is done for integer values (byte, short, int, long) only!
+ * @param op1
+ * @param op2
+ * @return Subtraction result.
+ */
+ public static Number subtract (Number op1, Number op2) {
+
+ int calcBase = findCalculationBase( op1, op2);
+ switch (calcBase) {
+ case BASE_BIGINTEGER:
+ return toBigInteger( op1 ).subtract( toBigInteger( op2 ));
+ case BASE_LONG:
+ long l1 = op1.longValue();
+ long l2 = op2.longValue();
+ long result = l1-l2;
+
+ // Overflow check
+ if ((result ^ l1) < 0 && (result ^ ~l2) < 0) {
+ return toBigInteger( op1).subtract( toBigInteger( op2));
+ }
+ return wrapPrimitive( result, op1, op2);
+ case BASE_FLOAT:
+ return op1.floatValue() - op2.floatValue();
+ case BASE_DOUBLE:
+ return op1.doubleValue() - op2.doubleValue();
+
+ // Default is BigDecimal operation
+ default:
+ return toBigDecimal( op1 ).subtract( toBigDecimal( op2 ));
+ }
+ }
+
+ /**
+ * Multiply two numbers and return the correct value / type.
+ * Overflow detection is done for integer values (byte, short, int, long) only!
+ * @param op1
+ * @param op2
+ * @return Multiplication result.
+ */
+ public static Number multiply (Number op1, Number op2) {
+
+ int calcBase = findCalculationBase( op1, op2);
+ switch (calcBase) {
+ case BASE_BIGINTEGER:
+ return toBigInteger( op1 ).multiply( toBigInteger( op2 ));
+ case BASE_LONG:
+ long l1 = op1.longValue();
+ long l2 = op2.longValue();
+ long result = l1*l2;
+
+ // Overflow detection
+ if ((l2 != 0) && (result / l2 != l1)) {
+ return toBigInteger( op1).multiply( toBigInteger( op2));
+ }
+ return wrapPrimitive( result, op1, op2);
+ case BASE_FLOAT:
+ return op1.floatValue() * op2.floatValue();
+ case BASE_DOUBLE:
+ return op1.doubleValue() * op2.doubleValue();
+
+ // Default is BigDecimal operation
+ default:
+ return toBigDecimal( op1 ).multiply( toBigDecimal( op2 ));
+ }
+ }
+
+ /**
+ * Divide two numbers. The result will be returned as Integer-type if and only if
+ * both sides of the division operator are Integer-types. Otherwise a Float, Double,
+ * or BigDecimal will be returned.
+ * @param op1
+ * @param op2
+ * @return Division result.
+ */
+ public static Number divide (Number op1, Number op2) {
+
+ int calcBase = findCalculationBase( op1, op2);
+ switch (calcBase) {
+ case BASE_BIGINTEGER:
+ BigInteger b1 = toBigInteger( op1 );
+ BigInteger b2 = toBigInteger( op2 );
+ return b1.divide( b2);
+
+ case BASE_LONG:
+ long l1 = op1.longValue();
+ long l2 = op2.longValue();
+ return wrapPrimitive( l1 / l2, op1, op2);
+
+ case BASE_FLOAT:
+ return op1.floatValue() / op2.floatValue();
+ case BASE_DOUBLE:
+ return op1.doubleValue() / op2.doubleValue();
+
+ // Default is BigDecimal operation
+ default:
+ return toBigDecimal( op1 ).divide( toBigDecimal( op2 ), BigDecimal.ROUND_HALF_DOWN);
+ }
+ }
+
+ /**
+ * Modulo two numbers.
+ * @param op1
+ * @param op2
+ * @return Modulo result.
+ *
+ * @throws ArithmeticException If at least one parameter is a BigDecimal
+ */
+ public static Number modulo (Number op1, Number op2) throws ArithmeticException {
+
+ int calcBase = findCalculationBase( op1, op2);
+ switch (calcBase) {
+ case BASE_BIGINTEGER:
+ return toBigInteger( op1 ).mod( toBigInteger( op2 ));
+ case BASE_LONG:
+ return wrapPrimitive( op1.longValue() % op2.longValue(), op1, op2);
+ case BASE_FLOAT:
+ return op1.floatValue() % op2.floatValue();
+ case BASE_DOUBLE:
+ return op1.doubleValue() % op2.doubleValue();
+
+ // Default is BigDecimal operation
+ default:
+ throw new ArithmeticException( "Cannot calculate the modulo of BigDecimals.");
+ }
+ }
+
+ /**
+ * Compare two numbers.
+ * @param op1
+ * @param op2
+ * @return 1 if n1 &gt; n2, -1 if n1 &lt; n2 and 0 if equal.
+ */
+ public static int compare (Number op1, Number op2) {
+
+ int calcBase = findCalculationBase( op1, op2);
+ switch (calcBase) {
+ case BASE_BIGINTEGER:
+ return toBigInteger( op1 ).compareTo( toBigInteger( op2 ));
+ case BASE_LONG:
+ long l1 = op1.longValue();
+ long l2 = op2.longValue();
+ if (l1 < l2) {
+ return -1;
+ }
+ if (l1 > l2) {
+ return 1;
+ }
+ return 0;
+ case BASE_FLOAT:
+ float f1 = op1.floatValue();
+ float f2 = op2.floatValue();
+ if (f1 < f2) {
+ return -1;
+ }
+ if (f1 > f2) {
+ return 1;
+ }
+ return 0;
+ case BASE_DOUBLE:
+ double d1 = op1.doubleValue();
+ double d2 = op2.doubleValue();
+ if (d1 < d2) {
+ return -1;
+ }
+ if (d1 > d2) {
+ return 1;
+ }
+ return 0;
+
+ // Default is BigDecimal operation
+ default:
+ return toBigDecimal( op1 ).compareTo( toBigDecimal ( op2 ));
+ }
+ }
+
+ /**
+ * Negate a number
+ * @param op n
+ * @return -n (unary negation of n)
+ */
+ public static Number negate(Number op)
+ {
+ int calcBase = findCalculationBase( op);
+ switch (calcBase) {
+ case BASE_BIGINTEGER:
+ return toBigInteger(op).negate();
+ case BASE_LONG:
+ long l = op.longValue();
+ /* overflow check */
+ if (l == Long.MIN_VALUE)
+ {
+ return toBigInteger(l).negate();
+ }
+ return wrapPrimitive(-l, op.getClass());
+ case BASE_FLOAT:
+ float f = op.floatValue();
+ return -f;
+ case BASE_DOUBLE:
+ double d = op.doubleValue();
+ return -d;
+ // Default is BigDecimal operation
+ default:
+ return toBigDecimal(op).negate();
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/Node.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/Node.java
new file mode 100644
index 00000000..ff867a15
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/Node.java
@@ -0,0 +1,232 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.Renderable;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.runtime.parser.Token;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * This file describes the interface between the Velocity code
+ * and the JavaCC generated code.
+ *
+ * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ */
+
+public interface Node extends Renderable
+{
+ /** This method is called after the node has been made the current
+ * node. It indicates that child nodes can now be added to it. */
+ void jjtOpen();
+
+ /** This method is called after all the child nodes have been
+ added.
+ */
+ void jjtClose();
+
+ /**
+ * This pair of methods are used to inform the node of its
+ * parent.
+ * @param n
+ *
+ */
+ void jjtSetParent(Node n);
+
+ /**
+ * @return The node parent.
+ */
+ Node jjtGetParent();
+
+ /**
+ * This method tells the node to add its argument to the node's
+ * list of children.
+ * @param n
+ * @param i
+ */
+ void jjtAddChild(Node n, int i);
+
+ /**
+ * This method returns a child node. The children are numbered
+ * from zero, left to right.
+ * @param i
+ * @return A child node.
+ */
+ Node jjtGetChild(int i);
+
+ /**
+ * Return the number of children the node has.
+ * @return The number of children of this node.
+ */
+ int jjtGetNumChildren();
+
+ /**
+ * @param visitor
+ * @param data
+ * @return The Node execution result object.
+ */
+ Object jjtAccept(ParserVisitor visitor, Object data);
+
+ /*
+ * ========================================================================
+ *
+ * The following methods are not generated automatically be the Parser but
+ * added manually to be used by Velocity.
+ *
+ * ========================================================================
+ */
+
+ /**
+ * @see #jjtAccept(ParserVisitor, Object)
+ * @param visitor
+ * @param data
+ * @return The node execution result.
+ */
+ Object childrenAccept(ParserVisitor visitor, Object data);
+
+ /**
+ * @return The first token.
+ */
+ Token getFirstToken();
+ /**
+ * @return The last token.
+ */
+ Token getLastToken();
+ /**
+ * @return The NodeType.
+ */
+ int getType();
+
+ /**
+ * @param context
+ * @param data
+ * @return The init result.
+ * @throws TemplateInitException
+ */
+ Object init(InternalContextAdapter context, Object data) throws TemplateInitException;
+
+ /**
+ * @param context
+ * @return The evaluation result.
+ * @throws MethodInvocationException
+ */
+ boolean evaluate(InternalContextAdapter context)
+ throws MethodInvocationException;
+
+ /**
+ * @param context
+ * @return The node value.
+ * @throws MethodInvocationException
+ */
+ Object value(InternalContextAdapter context)
+ throws MethodInvocationException;
+
+ /**
+ * @param context
+ * @param writer
+ * @return True if the node rendered successfully.
+ * @throws IOException
+ * @throws MethodInvocationException
+ * @throws ParseErrorException
+ * @throws ResourceNotFoundException
+ */
+ @Override
+ boolean render(InternalContextAdapter context, Writer writer)
+ throws IOException,MethodInvocationException, ParseErrorException, ResourceNotFoundException;
+
+ /**
+ * @param o
+ * @param context
+ * @return The execution result.
+ * @throws MethodInvocationException
+ */
+ Object execute(Object o, InternalContextAdapter context)
+ throws MethodInvocationException;
+
+ /**
+ * @param info
+ */
+ void setInfo(int info);
+
+ /**
+ * @return The current node info.
+ */
+ int getInfo();
+
+ /**
+ * @return A literal.
+ */
+ String literal();
+
+ /**
+ * Mark the node as invalid.
+ */
+ void setInvalid();
+
+ /**
+ * @return True if the node is invalid.
+ */
+ boolean isInvalid();
+
+ /**
+ * @return The current line position.
+ */
+ int getLine();
+
+ /**
+ * @return The current column position.
+ */
+ int getColumn();
+
+ /**
+ * @return the file name of the template
+ */
+ String getTemplateName();
+
+ /**
+ * @return cached image (String) of the first Token for this Node returned by the Parser
+ */
+ String getFirstTokenImage();
+
+ /**
+ * @return cached image (String) of the last Token for this Node returned by the Parser
+ */
+ String getLastTokenImage();
+
+ /**
+ * @return the template this node belongs to
+ */
+ Template getTemplate();
+
+ /**
+ * @return the parser which generated this node
+ * @since 2.2
+ */
+ Parser getParser();
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/NodeUtils.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/NodeUtils.java
new file mode 100644
index 00000000..84b735bd
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/NodeUtils.java
@@ -0,0 +1,162 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.runtime.parser.StandardParserConstants;
+import org.apache.velocity.runtime.parser.Token;
+
+/**
+ * Utilities for dealing with the AST node structure.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class NodeUtils
+{
+ /**
+ * Collect all the &lt;SPECIAL_TOKEN&gt;s that
+ * are carried along with a token. Special
+ * tokens do not participate in parsing but
+ * can still trigger certain lexical actions.
+ * In some cases you may want to retrieve these
+ * special tokens, this is simply a way to
+ * extract them.
+ * @param t the Token
+ * @return StrBuilder with the special tokens.
+ * @since 2.0.0
+ */
+ public static StringBuilder getSpecialText(Parser parser, Token t)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ Token tmp_t = t.specialToken;
+
+ while (tmp_t.specialToken != null)
+ {
+ tmp_t = tmp_t.specialToken;
+ }
+
+ while (tmp_t != null)
+ {
+ String st = tmp_t.image;
+
+ for(int i = 0, is = st.length(); i < is; i++)
+ {
+ char c = st.charAt(i);
+
+ if ( c == parser.hash() || c == parser.dollar() )
+ {
+ sb.append( c );
+ }
+
+ /*
+ * more dreaded MORE hack :)
+ *
+ * looking for ("\\")*"$" sequences
+ */
+
+ if ( c == '\\')
+ {
+ boolean ok = true;
+ boolean term = false;
+
+ int j = i;
+ for( ok = true; ok && j < is; j++)
+ {
+ char cc = st.charAt( j );
+
+ if (cc == '\\')
+ {
+ /*
+ * if we see a \, keep going
+ */
+ continue;
+ }
+ else if( cc == parser.dollar() )
+ {
+ /*
+ * a $ ends it correctly
+ */
+ term = true;
+ ok = false;
+ }
+ else
+ {
+ /*
+ * nah...
+ */
+ ok = false;
+ }
+ }
+
+ if (term)
+ {
+ String foo = st.substring( i, j );
+ sb.append( foo );
+ i = j;
+ }
+ }
+ }
+
+ tmp_t = tmp_t.next;
+ }
+ return sb;
+ }
+
+ /**
+ * complete node literal
+ * @param t
+ * @return A node literal.
+ */
+ public static String tokenLiteral( Parser parser, Token t )
+ {
+ // Look at kind of token and return "" when it's a block comment
+ if (t.kind == StandardParserConstants.MULTI_LINE_COMMENT)
+ {
+ return "";
+ }
+ else if (t.specialToken == null || t.specialToken.image.startsWith(parser.lineComment()))
+ {
+ return t.image;
+ }
+ else
+ {
+ StringBuilder special = getSpecialText(parser, t);
+ if (special.length() > 0)
+ {
+ return special.append(t.image).toString();
+ }
+ return t.image;
+ }
+ }
+
+ /**
+ * Fix children indentation in structured space gobbling mode.
+ * @param parent
+ * @param parentIndentation
+ */
+ public static void fixIndentation(SimpleNode parent, String parentIndentation)
+ {
+ IndentationFixer fixer = new IndentationFixer(parentIndentation);
+ parent.childrenAccept(fixer, null);
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ParserTreeConstants.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ParserTreeConstants.java
new file mode 100644
index 00000000..7cc1dc53
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ParserTreeConstants.java
@@ -0,0 +1,24 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+public interface ParserTreeConstants extends StandardParserTreeConstants
+{
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ParserVisitor.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ParserVisitor.java
new file mode 100644
index 00000000..8fe14fe6
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ParserVisitor.java
@@ -0,0 +1,332 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+/**
+ * Interface used in implementation of visitor pattern. Based on
+ * code autogenerated by JavaCC. Formerly found in package
+ * org.apache.velocity.runtime.parser.
+ *
+ * @version $Id$
+ * @since 1.5
+ */
+public interface ParserVisitor
+{
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(SimpleNode node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+
+ Object visit(ASTprocess node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTText node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTEscapedDirective node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTEscape node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTComment node, Object data);
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTTextblock node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTFloatingPointLiteral node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTIntegerLiteral node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTStringLiteral node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTIdentifier node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTWord node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+
+ Object visit(ASTDirectiveAssign node, Object data);
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTDirective node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTBlock node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTMap node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTObjectArray node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTIntegerRange node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTMethod node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTIndex node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTReference node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTTrue node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTFalse node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTIfStatement node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTElseStatement node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTElseIfStatement node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTSetDirective node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTExpression node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTAssignment node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTOrNode node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTAndNode node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTEQNode node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTNENode node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTLTNode node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTGTNode node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTLENode node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTGENode node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTAddNode node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTSubtractNode node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTMulNode node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTDivNode node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTModNode node, Object data);
+
+ /**
+ * @param node
+ * @param data
+ * @return The object rendered by this node.
+ */
+ Object visit(ASTNotNode node, Object data);
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/PropertyExecutor.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/PropertyExecutor.java
new file mode 100644
index 00000000..7763724c
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/PropertyExecutor.java
@@ -0,0 +1,151 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.util.ArrayListWrapper;
+import org.apache.velocity.util.introspection.Introspector;
+import org.slf4j.Logger;
+
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Returned the value of object property when executed.
+ */
+public class PropertyExecutor extends AbstractExecutor
+{
+ private final Introspector introspector;
+ private final boolean wrapArray;
+
+ /**
+ * @param log
+ * @param introspector
+ * @param clazz
+ * @param property
+ * @since 1.5
+ */
+ public PropertyExecutor(final Logger log, final Introspector introspector,
+ final Class<?> clazz, final String property)
+ {
+ this(log, introspector, clazz, property, false);
+ }
+
+ /**
+ * @param log
+ * @param introspector
+ * @param clazz
+ * @param property
+ * @param wrapArray
+ * @since 1.5
+ */
+ public PropertyExecutor(final Logger log, final Introspector introspector,
+ final Class<?> clazz, final String property, final boolean wrapArray)
+ {
+ this.log = log;
+ this.introspector = introspector;
+ this.wrapArray = wrapArray;
+
+ // Don't allow passing in the empty string or null because
+ // it will either fail with a StringIndexOutOfBounds error
+ // or the introspector will get confused.
+ if (StringUtils.isNotEmpty(property))
+ {
+ discover(clazz, property);
+ }
+ }
+
+ /**
+ * @return The current introspector.
+ * @since 1.5
+ */
+ protected Introspector getIntrospector()
+ {
+ return this.introspector;
+ }
+
+ /**
+ * @param clazz
+ * @param property
+ */
+ protected void discover(final Class<?> clazz, final String property)
+ {
+ /*
+ * this is gross and linear, but it keeps it straightforward.
+ */
+
+ try
+ {
+ Object [] params = {};
+
+ StringBuilder sb = new StringBuilder("get");
+ sb.append(property);
+
+ setMethod(introspector.getMethod(clazz, sb.toString(), params));
+
+ if (!isAlive())
+ {
+ /*
+ * now the convenience, flip the 1st character
+ */
+
+ char c = sb.charAt(3);
+
+ if (Character.isLowerCase(c))
+ {
+ sb.setCharAt(3, Character.toUpperCase(c));
+ }
+ else
+ {
+ sb.setCharAt(3, Character.toLowerCase(c));
+ }
+
+ setMethod(introspector.getMethod(clazz, sb.toString(), params));
+ }
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ throw e;
+ }
+ catch(Exception e)
+ {
+ String msg = "Exception while looking for property getter for '" + property;
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.AbstractExecutor#execute(java.lang.Object)
+ */
+ @Override
+ public Object execute(Object o)
+ throws IllegalAccessException, InvocationTargetException
+ {
+ if (wrapArray)
+ {
+ o = new ArrayListWrapper(o);
+ }
+ return isAlive() ? getMethod().invoke(o, ((Object []) null)) : null;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/PublicFieldExecutor.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/PublicFieldExecutor.java
new file mode 100644
index 00000000..643f9817
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/PublicFieldExecutor.java
@@ -0,0 +1,128 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.util.introspection.Introspector;
+import org.slf4j.Logger;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Returns the value of a public field when executed.
+ */
+public class PublicFieldExecutor extends AbstractExecutor
+{
+ private final Introspector introspector;
+
+ /**
+ * Field to be accessed
+ */
+ private Field field = null;
+
+ /**
+ * @param log
+ * @param introspector
+ * @param clazz
+ * @param property
+ * @since 1.5
+ */
+ public PublicFieldExecutor(final Logger log, final Introspector introspector,
+ final Class<?> clazz, final String property)
+ {
+ this.log = log;
+ this.introspector = introspector;
+
+ // Don't allow passing in the empty string or null because
+ // it will either fail with a StringIndexOutOfBounds error
+ // or the introspector will get confused.
+ if (StringUtils.isNotEmpty(property))
+ {
+ discover(clazz, property);
+ }
+ }
+
+ @Override
+ public boolean isAlive() {
+ return getField() != null;
+ }
+
+ /**
+ * @return The current field.
+ */
+ public Field getField()
+ {
+ return field;
+ }
+
+ /**
+ * @param field
+ */
+ protected void setField(final Field field)
+ {
+ this.field = field;
+ }
+
+ /**
+ * @return The current introspector.
+ * @since 1.5
+ */
+ protected Introspector getIntrospector()
+ {
+ return this.introspector;
+ }
+
+ /**
+ * @param clazz
+ * @param property
+ */
+ protected void discover(final Class<?> clazz, final String property)
+ {
+ try
+ {
+ setField(introspector.getField(clazz, property));
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ throw e;
+ }
+ catch(Exception e)
+ {
+ String msg = "Exception while looking for public field '" + property;
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.AbstractExecutor#execute(java.lang.Object)
+ */
+ @Override
+ public Object execute(Object o)
+ throws IllegalAccessException, InvocationTargetException
+ {
+ return isAlive() ? getField().get(o) : null;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/PutExecutor.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/PutExecutor.java
new file mode 100644
index 00000000..09072615
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/PutExecutor.java
@@ -0,0 +1,133 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.util.introspection.Introspector;
+import org.slf4j.Logger;
+
+import java.lang.reflect.InvocationTargetException;
+
+
+/**
+ * Executor that simply tries to execute a put(key, value)
+ * operation. This will try to find a put(key) method
+ * for any type of object, not just objects that
+ * implement the Map interface as was previously
+ * the case.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ * @since 1.5
+ */
+public class PutExecutor extends SetExecutor
+{
+ private final Introspector introspector;
+ private final String property;
+
+ /**
+ * @param log
+ * @param introspector
+ * @param clazz
+ * @param arg
+ * @param property
+ */
+ public PutExecutor(final Logger log, final Introspector introspector,
+ final Class<?> clazz, final Object arg, final String property)
+ {
+ this.log = log;
+ this.introspector = introspector;
+ this.property = property;
+
+ discover(clazz, arg);
+ }
+
+ /**
+ * @param clazz
+ * @param arg
+ */
+ protected void discover(final Class<?> clazz, final Object arg)
+ {
+ Object [] params;
+
+ // If you passed in null as property, we don't use the value
+ // for parameter lookup. Instead we just look for put(Object) without
+ // any parameters.
+ //
+ // In any other case, the following condition will set up an array
+ // for looking up put(String, Object) on the class.
+
+ if (property == null)
+ {
+ // The passed in arg object is used by the Cache to look up the method.
+ params = new Object[] { arg };
+ }
+ else
+ {
+ params = new Object[] { property, arg };
+ }
+
+ try
+ {
+ setMethod(introspector.getMethod(clazz, "put", params));
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ throw e;
+ }
+ catch(Exception e)
+ {
+ String msg = "Exception while looking for put('" + params[0] + "') method";
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.SetExecutor#execute(java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public Object execute(final Object o, final Object value)
+ throws IllegalAccessException, InvocationTargetException
+ {
+ Object [] params;
+
+ if (isAlive())
+ {
+ // If property != null, pass in the name for put(key, value). Else just put(value).
+ if (property == null)
+ {
+ params = new Object [] { value };
+ }
+ else
+ {
+ params = new Object [] { property, value };
+ }
+
+ return getMethod().invoke(o, params);
+ }
+
+ return null;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SetExecutor.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SetExecutor.java
new file mode 100644
index 00000000..461c92a8
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SetExecutor.java
@@ -0,0 +1,87 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.slf4j.Logger;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Abstract class that is used to execute an arbitrary
+ * method that is in introspected. This is the superclass
+ * for the PutExecutor and SetPropertyExecutor.
+ *
+ * There really should be a superclass for this and AbstractExecutor (which should
+ * be refactored to GetExecutor) because they differ only in the execute() method.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@apache.org">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ * @since 1.5
+ */
+public abstract class SetExecutor
+{
+ /** Class logger */
+ protected Logger log = null;
+
+ /**
+ * Method to be executed.
+ */
+ private Method method = null;
+
+ /**
+ * Execute method against context.
+ * @param o
+ * @param value
+ * @return The result of the invocation.
+ * @throws IllegalAccessException
+ * @throws InvocationTargetException
+ */
+ public abstract Object execute(Object o, Object value)
+ throws IllegalAccessException, InvocationTargetException;
+
+ /**
+ * Tell whether the executor is alive by looking
+ * at the value of the method.
+ * @return True if the executor is alive.
+ */
+ public boolean isAlive()
+ {
+ return (method != null);
+ }
+
+ /**
+ * @return The method to invoke.
+ */
+ public Method getMethod()
+ {
+ return method;
+ }
+
+ /**
+ * @param method
+ */
+ protected void setMethod(final Method method)
+ {
+ this.method = method;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SetPropertyExecutor.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SetPropertyExecutor.java
new file mode 100644
index 00000000..e02ca4e6
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SetPropertyExecutor.java
@@ -0,0 +1,138 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.util.introspection.Introspector;
+import org.slf4j.Logger;
+
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Executor for looking up property names in the passed in class
+ * This will try to find a set&lt;foo&gt;(key, value) method
+ *
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ * @since 1.5
+ */
+public class SetPropertyExecutor
+ extends SetExecutor
+{
+ private final Introspector introspector;
+
+ /**
+ * @param log
+ * @param introspector
+ * @param clazz
+ * @param property
+ * @param arg
+ */
+ public SetPropertyExecutor(final Logger log, final Introspector introspector,
+ final Class<?> clazz, final String property, final Object arg)
+ {
+ this.log = log;
+ this.introspector = introspector;
+
+ // Don't allow passing in the empty string or null because
+ // it will either fail with a StringIndexOutOfBounds error
+ // or the introspector will get confused.
+ if (StringUtils.isNotEmpty(property))
+ {
+ discover(clazz, property, arg);
+ }
+ }
+
+ /**
+ * @return The current introspector.
+ */
+ protected Introspector getIntrospector()
+ {
+ return this.introspector;
+ }
+
+ /**
+ * @param clazz
+ * @param property
+ * @param arg
+ */
+ protected void discover(final Class<?> clazz, final String property, final Object arg)
+ {
+ Object [] params = new Object [] { arg };
+
+ try
+ {
+ StringBuilder sb = new StringBuilder("set");
+ sb.append(property);
+
+ setMethod(introspector.getMethod(clazz, sb.toString(), params));
+
+ if (!isAlive())
+ {
+ /*
+ * now the convenience, flip the 1st character
+ */
+
+ char c = sb.charAt(3);
+
+ if (Character.isLowerCase(c))
+ {
+ sb.setCharAt(3, Character.toUpperCase(c));
+ }
+ else
+ {
+ sb.setCharAt(3, Character.toLowerCase(c));
+ }
+
+ setMethod(introspector.getMethod(clazz, sb.toString(), params));
+ }
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ throw e;
+ }
+ catch(Exception e)
+ {
+ String msg = "Exception while looking for property setter for '" + property;
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ }
+
+ /**
+ * Execute method against context.
+ * @param o
+ * @param value
+ * @return The value of the invocation.
+ * @throws IllegalAccessException
+ * @throws InvocationTargetException
+ */
+ @Override
+ public Object execute(final Object o, final Object value)
+ throws IllegalAccessException, InvocationTargetException
+ {
+ Object [] params = new Object [] { value };
+ return isAlive() ? getMethod().invoke(o, params) : null;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SetPublicFieldExecutor.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SetPublicFieldExecutor.java
new file mode 100644
index 00000000..7dead361
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SetPublicFieldExecutor.java
@@ -0,0 +1,149 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.util.introspection.Introspector;
+import org.slf4j.Logger;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+
+/**
+ * Executor for setting public fields in objects
+ *
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @author <a href="mailto:cdauth@cdauth.eu">Candid Dauth</a>
+ */
+public class SetPublicFieldExecutor
+ extends SetExecutor
+{
+ private final Introspector introspector;
+
+ /**
+ * Field to be accessed
+ */
+ private Field field = null;
+
+ /**
+ * @param log
+ * @param introspector
+ * @param clazz
+ * @param property
+ * @param arg
+ */
+ public SetPublicFieldExecutor(final Logger log, final Introspector introspector,
+ final Class<?> clazz, final String property, final Object arg)
+ {
+ this.log = log;
+ this.introspector = introspector;
+
+ // Don't allow passing in the empty string or null because
+ // it will either fail with a StringIndexOutOfBounds error
+ // or the introspector will get confused.
+ if (StringUtils.isNotEmpty(property))
+ {
+ discover(clazz, property, arg);
+ }
+ }
+
+ @Override
+ public boolean isAlive() {
+ return getField() != null;
+ }
+
+ /**
+ * @return The current field.
+ */
+ public Field getField()
+ {
+ return field;
+ }
+
+ /**
+ * @param field
+ */
+ protected void setField(final Field field)
+ {
+ this.field = field;
+ }
+
+ /**
+ * @return The current introspector.
+ */
+ protected Introspector getIntrospector()
+ {
+ return this.introspector;
+ }
+
+ /**
+ * @param clazz
+ * @param property
+ * @param arg
+ */
+ protected void discover(final Class<?> clazz, final String property, final Object arg)
+ {
+ try
+ {
+ Field field = introspector.getField(clazz, property);
+ if(!Modifier.isFinal(field.getModifiers()))
+ {
+ setField(field);
+ }
+ }
+ /*
+ * pass through application level runtime exceptions
+ */
+ catch( RuntimeException e )
+ {
+ throw e;
+ }
+ catch(Exception e)
+ {
+ String msg = "Exception while looking for public field '" + property;
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ }
+
+ /**
+ * Execute method against context.
+ * @param o
+ * @param value
+ * @return The value of the invocation.
+ * @throws IllegalAccessException
+ * @throws InvocationTargetException
+ */
+ @Override
+ public Object execute(final Object o, final Object value)
+ throws IllegalAccessException, InvocationTargetException
+ {
+ if (isAlive())
+ {
+ Object oldValue = getField().get(o);
+ getField().set(o, value);
+ return oldValue;
+ }
+ else
+ return null;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SimpleNode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SimpleNode.java
new file mode 100644
index 00000000..f85d6926
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/SimpleNode.java
@@ -0,0 +1,650 @@
+package org.apache.velocity.runtime.parser.node;
+
+/*
+ * 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.
+ */
+
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.nio.charset.Charset;
+
+import org.apache.velocity.Template;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.parser.Parser;
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.util.StringUtils;
+
+import org.slf4j.Logger;
+
+/**
+ *
+ */
+public class SimpleNode implements Node, Cloneable
+{
+ /** */
+ protected RuntimeServices rsvc = null;
+
+ /** */
+ protected Logger log = null;
+
+ /** */
+ protected Node parent;
+
+ /** */
+ protected Node[] children;
+
+ /** */
+ protected int id;
+
+ /** */
+ protected Parser parser;
+
+ /** */
+ protected int info;
+
+ /** */
+ public boolean state;
+
+ /** */
+ protected boolean invalid = false;
+
+ /** */
+ protected Token first;
+
+ /** */
+ protected Token last;
+
+ protected Template template;
+
+ /**
+ * For caching the literal value.
+ */
+ protected String literal = null;
+
+ /**
+ * Line number for this Node in the vm source file.
+ */
+
+ protected int line;
+
+ /**
+ * Column number for this Node in the vm source file.
+ */
+ protected int column;
+
+ /**
+ * String image variable of the first Token element that was parsed and connected to this Node.
+ */
+ protected String firstImage;
+
+ /**
+ * String image variable of the last Token element that was parsed and connected to this Node.
+ */
+ protected String lastImage;
+
+ public RuntimeServices getRuntimeServices()
+ {
+ return rsvc;
+ }
+
+ /**
+ * @param i
+ */
+ public SimpleNode(int i)
+ {
+ id = i;
+ }
+
+ /**
+ * @param p
+ * @param i
+ */
+ public SimpleNode(Parser p, int i)
+ {
+ this(i);
+ parser = p;
+ template = parser.getCurrentTemplate();
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#jjtOpen()
+ */
+ @Override
+ public void jjtOpen()
+ {
+ first = parser.getToken(1); // added
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#jjtClose()
+ */
+ @Override
+ public void jjtClose()
+ {
+ last = parser.getToken(0); // added
+ }
+
+ /**
+ * @param t
+ */
+ public void setFirstToken(Token t)
+ {
+ this.first = t;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#getFirstToken()
+ */
+ @Override
+ public Token getFirstToken()
+ {
+ return first;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#getLastToken()
+ */
+ @Override
+ public Token getLastToken()
+ {
+ return last;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#jjtSetParent(org.apache.velocity.runtime.parser.node.Node)
+ */
+ @Override
+ public void jjtSetParent(Node n)
+ {
+ parent = n;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#jjtGetParent()
+ */
+ @Override
+ public Node jjtGetParent()
+ {
+ return parent;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#jjtAddChild(org.apache.velocity.runtime.parser.node.Node, int)
+ */
+ @Override
+ public void jjtAddChild(Node n, int i)
+ {
+ if (children == null)
+ {
+ children = new Node[i + 1];
+ }
+ else if (i >= children.length)
+ {
+ Node c[] = new Node[i + 1];
+ System.arraycopy(children, 0, c, 0, children.length);
+ children = c;
+ }
+ children[i] = n;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#jjtGetChild(int)
+ */
+ @Override
+ public Node jjtGetChild(int i)
+ {
+ return children[i];
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#jjtGetNumChildren()
+ */
+ @Override
+ public int jjtGetNumChildren()
+ {
+ return (children == null) ? 0 : children.length;
+ }
+
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object jjtAccept(ParserVisitor visitor, Object data)
+ {
+ return visitor.visit(this, data);
+ }
+
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#childrenAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
+ */
+ @Override
+ public Object childrenAccept(ParserVisitor visitor, Object data)
+ {
+ if (children != null)
+ {
+ for (Node aChildren : children)
+ {
+ aChildren.jjtAccept(visitor, data);
+ }
+ }
+ return data;
+ }
+
+ /* You can override these two methods in subclasses of SimpleNode to
+ customize the way the node appears when the tree is dumped. If
+ your output uses more than one line you should override
+ toString(String), otherwise overriding toString() is probably all
+ you need to do. */
+ /**
+ * @param prefix display prefix
+ * @return String representation of this node.
+ */
+ public String toString(String prefix)
+ {
+ return prefix + "_" + toString();
+ }
+
+ /**
+ * <p>Dumps nodes tree on System.out.</p>
+ * <p>Override {@link #dump(String, PrintWriter)} if you want to customize
+ * how the node dumps out its children.
+ *
+ * @param prefix
+ */
+ public final void dump(String prefix)
+ {
+ dump(prefix, System.out);
+ }
+
+ /**
+ * <p>Dumps nodes tree on System.out.</p>
+ * <p>Override {@link #dump(String, PrintWriter)} if you want to customize
+ * how the node dumps out its children.
+ *
+ * @param prefix display prefix
+ * @param out output print stream
+ */
+ public final void dump(String prefix, PrintStream out)
+ {
+ Charset charset = null;
+ if (rsvc != null) /* may be null if node isn't yet initialized */
+ {
+ String encoding = rsvc.getString(RuntimeConstants.INPUT_ENCODING);
+ try
+ {
+ charset = Charset.forName(encoding);
+ }
+ catch (Exception e) {}
+ }
+ if (charset == null)
+ {
+ charset = Charset.defaultCharset();
+ }
+ PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, charset));
+ dump(prefix, pw);
+ pw.flush();
+ }
+
+ /**
+ * <p>Dumps nodes tree on System.out.</p>
+ * <p>Override this method if you want to customize how the node dumps
+ * out its children.</p>
+ *
+ * @param prefix display prefix
+ * @param out output print writer
+ */
+ public void dump(String prefix, PrintWriter out)
+ {
+ out.println(toString());
+ if (children != null)
+ {
+ for (int i = 0; i < children.length; ++i)
+ {
+ SimpleNode n = (SimpleNode) children[i];
+ out.print(prefix + " |_");
+ if (n != null)
+ {
+ n.dump(prefix + ( i == children.length - 1 ? " " : " | " ), out);
+ }
+ }
+ }
+ }
+
+ /**
+ * Return a string that tells the current location of this node.
+ * @param context
+ * @return location
+ */
+ protected String getLocation(InternalContextAdapter context)
+ {
+ return StringUtils.formatFileString(this);
+ }
+
+ // All additional methods
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#literal()
+ */
+ @Override
+ public String literal()
+ {
+ if( literal != null )
+ {
+ return literal;
+ }
+
+ // if we have only one string, just return it and avoid
+ // buffer allocation. VELOCITY-606
+ if (first == last)
+ {
+ literal = NodeUtils.tokenLiteral(parser, first);
+ return literal;
+ }
+
+ Token t = first;
+ StringBuilder sb = new StringBuilder(NodeUtils.tokenLiteral(parser, t));
+ while (t != last)
+ {
+ t = t.next;
+ sb.append(NodeUtils.tokenLiteral(parser, t));
+ }
+ literal = sb.toString();
+ return literal;
+ }
+
+ /**
+ * @throws TemplateInitException
+ * @see org.apache.velocity.runtime.parser.node.Node#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
+ */
+ @Override
+ public Object init(InternalContextAdapter context, Object data) throws TemplateInitException
+ {
+ /*
+ * hold onto the RuntimeServices
+ */
+
+ rsvc = (RuntimeServices) data;
+ log = rsvc.getLog("rendering");
+
+ int i, k = jjtGetNumChildren();
+
+ for (i = 0; i < k; i++)
+ {
+ jjtGetChild(i).init( context, data);
+ }
+
+ line = first.beginLine;
+ column = first.beginColumn;
+
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#evaluate(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public boolean evaluate(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ return false;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#value(org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public Object value(InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ return null;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#render(org.apache.velocity.context.InternalContextAdapter, java.io.Writer)
+ */
+ @Override
+ public boolean render(InternalContextAdapter context, Writer writer)
+ throws IOException, MethodInvocationException, ParseErrorException, ResourceNotFoundException
+ {
+ int i, k = jjtGetNumChildren();
+
+ for (i = 0; i < k; i++)
+ jjtGetChild(i).render(context, writer);
+
+ return true;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#execute(java.lang.Object, org.apache.velocity.context.InternalContextAdapter)
+ */
+ @Override
+ public Object execute(Object o, InternalContextAdapter context)
+ throws MethodInvocationException
+ {
+ return null;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#getType()
+ */
+ @Override
+ public int getType()
+ {
+ return id;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#setInfo(int)
+ */
+ @Override
+ public void setInfo(int info)
+ {
+ this.info = info;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#getInfo()
+ */
+ @Override
+ public int getInfo()
+ {
+ return info;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#setInvalid()
+ */
+ @Override
+ public void setInvalid()
+ {
+ invalid = true;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#isInvalid()
+ */
+ @Override
+ public boolean isInvalid()
+ {
+ return invalid;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#getLine()
+ */
+ @Override
+ public int getLine()
+ {
+ return line;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.Node#getColumn()
+ */
+ @Override
+ public int getColumn()
+ {
+ return column;
+ }
+
+ /**
+ * @since 1.5
+ */
+ public String toString()
+ {
+ StringBuilder tokens = new StringBuilder();
+
+ for (Token t = getFirstToken(); t != null; )
+ {
+ tokens.append("[").append(t.image.replace("\n", "\\n")).append("]");
+ if (t.next != null)
+ {
+ if (t.equals(getLastToken()))
+ {
+ break;
+ }
+ else
+ {
+ tokens.append(", ");
+ }
+ }
+ t = t.next;
+ }
+ String tok = tokens.toString();
+ if (tok.length() > 50) tok = tok.substring(0, 50) + "...";
+ return getClass().getSimpleName() + " [id=" + id + ", info=" + info + ", invalid="
+ + invalid
+ + ", tokens=" + tok + "]";
+ }
+
+ @Override
+ public String getTemplateName()
+ {
+ return template.getName();
+ }
+
+ /**
+ * Call before calling cleanupParserAndTokens() if you want to store image of
+ * the first and last token of this node.
+ */
+ public void saveTokenImages()
+ {
+ if( first != null )
+ {
+ this.firstImage = first.image;
+ }
+ if( last != null )
+ {
+ this.lastImage = last.image;
+ }
+ }
+
+ /**
+ * Removes references to Parser and Tokens since they are not needed anymore at this point.
+ *
+ * This allows us to save memory quite a bit.
+ */
+ public void cleanupParserAndTokens()
+ {
+ this.parser = null;
+ this.first = null;
+ this.last = null;
+ }
+
+ /**
+ * @return String image variable of the first Token element that was parsed and connected to this Node.
+ */
+ @Override
+ public String getFirstTokenImage()
+ {
+ return firstImage;
+ }
+
+ /**
+ * @return String image variable of the last Token element that was parsed and connected to this Node.
+ */
+ @Override
+ public String getLastTokenImage()
+ {
+ return lastImage;
+ }
+
+ @Override
+ public Template getTemplate() { return template; }
+
+ /**
+ * @return the parser which created this node
+ * @since 2.2
+ */
+ @Override
+ public Parser getParser()
+ {
+ return parser;
+ }
+
+ /**
+ * Root node deep cloning
+ * @param template owner template
+ * @return cloned node
+ * @throws CloneNotSupportedException
+ * @since 2.4
+ */
+ public Node clone(Template template) throws CloneNotSupportedException
+ {
+ if (parent != null) {
+ throw new IllegalStateException("cannot clone a child node without knowing its parent");
+ }
+ return clone(template, null);
+ }
+
+ /**
+ * Child node deep cloning
+ * @param template owner template
+ * @param parent parent node
+ * @return cloned node
+ * @throws CloneNotSupportedException
+ * @since 2.4
+ */
+ protected Node clone(Template template, Node parent) throws CloneNotSupportedException
+ {
+ SimpleNode clone = (SimpleNode)super.clone();
+ clone.template = template;
+ clone.parent = parent;
+ if (children != null)
+ {
+ clone.children = new SimpleNode[children.length];
+ for (int i = 0; i < children.length; ++i)
+ {
+ clone.children[i] = ((SimpleNode)children[i]).clone(template, clone);
+ }
+ }
+ return clone;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ContentResource.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ContentResource.java
new file mode 100644
index 00000000..67974d1e
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ContentResource.java
@@ -0,0 +1,105 @@
+package org.apache.velocity.runtime.resource;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.util.StringBuilderWriter;
+
+import java.io.BufferedReader;
+import java.io.Writer;
+
+/**
+ * This class represent a general text resource that may have been
+ * retrieved from any number of possible sources.
+ *
+ * Also of interest is Velocity's {@link org.apache.velocity.Template}
+ * <code>Resource</code>.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ContentResource extends Resource
+{
+ /** Default empty constructor */
+ public ContentResource()
+ {
+ super();
+
+ setType(ResourceManager.RESOURCE_CONTENT);
+ }
+
+ /**
+ * Pull in static content and store it.
+ * @return True if everything went ok.
+ *
+ * @exception ResourceNotFoundException Resource could not be
+ * found.
+ */
+ @Override
+ public boolean process()
+ throws ResourceNotFoundException
+ {
+ BufferedReader reader = null;
+
+ try
+ {
+ Writer sw = new StringBuilderWriter();
+
+ reader = new BufferedReader(resourceLoader.getResourceReader(name, encoding));
+
+ char buf[] = new char[1024];
+ int len = 0;
+
+ while ( ( len = reader.read( buf, 0, 1024 )) != -1)
+ sw.write( buf, 0, len );
+
+ setData(sw.toString());
+
+ return true;
+ }
+ catch ( ResourceNotFoundException e )
+ {
+ // Tell the ContentManager to continue to look through any
+ // remaining configured ResourceLoaders.
+ throw e;
+ }
+ catch ( Exception e )
+ {
+ String msg = "Cannot process content resource";
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+ finally
+ {
+ if (reader != null)
+ {
+ try
+ {
+ reader.close();
+ }
+ catch (Exception ignored)
+ {
+ }
+ }
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/Resource.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/Resource.java
new file mode 100644
index 00000000..5922604d
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/Resource.java
@@ -0,0 +1,320 @@
+package org.apache.velocity.runtime.resource;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.resource.loader.ResourceLoader;
+import org.slf4j.Logger;
+
+/**
+ * This class represent a general text resource that
+ * may have been retrieved from any number of possible
+ * sources.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public abstract class Resource implements Cloneable
+{
+ protected RuntimeServices rsvc = null;
+ protected Logger log = null;
+
+ /**
+ * The template loader that initially loaded the input
+ * stream for this template, and knows how to check the
+ * source of the input stream for modification.
+ */
+ protected ResourceLoader resourceLoader;
+
+ /**
+ * The number of milliseconds in a minute, used to calculate the
+ * check interval.
+ */
+ protected static final long MILLIS_PER_SECOND = 1000;
+
+ /**
+ * How often the file modification time is checked (in seconds).
+ */
+ protected long modificationCheckInterval = 0;
+
+ /**
+ * The file modification time (in milliseconds) for the cached template.
+ */
+ protected long lastModified = 0;
+
+ /**
+ * The next time the file modification time will be checked (in
+ * milliseconds).
+ */
+ protected long nextCheck = 0;
+
+ /**
+ * Name of the resource
+ */
+ protected String name;
+
+ /**
+ * Character encoding of this resource
+ */
+ protected String encoding = RuntimeConstants.ENCODING_DEFAULT;
+
+ /**
+ * Resource might require ancillary storage of some kind
+ */
+ protected Object data = null;
+
+ /**
+ * Resource type (RESOURCE_TEMPLATE or RESOURCE_CONTENT)
+ */
+ protected int type;
+
+ /**
+ * Default constructor
+ */
+ public Resource()
+ {
+ }
+
+ /**
+ * @param rs
+ */
+ public void setRuntimeServices( RuntimeServices rs )
+ {
+ rsvc = rs;
+ log = rsvc.getLog("loader");
+ }
+
+ /**
+ * Perform any subsequent processing that might need
+ * to be done by a resource. In the case of a template
+ * the actual parsing of the input stream needs to be
+ * performed.
+ *
+ * @return Whether the resource could be processed successfully.
+ * For a {@link org.apache.velocity.Template} or {@link
+ * org.apache.velocity.runtime.resource.ContentResource}, this
+ * indicates whether the resource could be read.
+ * @exception ResourceNotFoundException Similar in semantics as
+ * returning <code>false</code>.
+ * @throws ParseErrorException
+ */
+ public abstract boolean process()
+ throws ResourceNotFoundException, ParseErrorException;
+
+ /**
+ * @return True if source has been modified.
+ */
+ public boolean isSourceModified()
+ {
+ return resourceLoader.isSourceModified(this);
+ }
+
+ /**
+ * Set the modification check interval.
+ * @param modificationCheckInterval The interval (in seconds).
+ */
+ public void setModificationCheckInterval(long modificationCheckInterval)
+ {
+ this.modificationCheckInterval = modificationCheckInterval;
+ }
+
+ /**
+ * Is it time to check to see if the resource
+ * source has been updated?
+ * @return True if resource must be checked.
+ */
+ public boolean requiresChecking()
+ {
+ /*
+ * short circuit this if modificationCheckInterval == 0
+ * as this means "don't check"
+ */
+
+ if (modificationCheckInterval <= 0 )
+ {
+ return false;
+ }
+
+ /*
+ * see if we need to check now
+ */
+
+ return ( System.currentTimeMillis() >= nextCheck );
+ }
+
+ /**
+ * 'Touch' this template and thereby resetting
+ * the nextCheck field.
+ */
+ public void touch()
+ {
+ nextCheck = System.currentTimeMillis() + ( MILLIS_PER_SECOND * modificationCheckInterval);
+ }
+
+ /**
+ * Set the name of this resource, for example
+ * test.vm.
+ * @param name
+ */
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ /**
+ * Get the name of this template.
+ * @return The name of this template.
+ */
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * set the encoding of this resource
+ * for example, "ISO-8859-1"
+ * @param encoding
+ */
+ public void setEncoding( String encoding )
+ {
+ this.encoding = encoding;
+ }
+
+ /**
+ * get the encoding of this resource
+ * for example, "ISO-8859-1"
+ * @return The encoding of this resource.
+ */
+ public String getEncoding()
+ {
+ return encoding;
+ }
+
+
+ /**
+ * Return the lastModifed time of this
+ * resource.
+ * @return The lastModifed time of this resource.
+ */
+ public long getLastModified()
+ {
+ return lastModified;
+ }
+
+ /**
+ * Set the last modified time for this
+ * resource.
+ * @param lastModified
+ */
+ public void setLastModified(long lastModified)
+ {
+ this.lastModified = lastModified;
+ }
+
+ /**
+ * Return the template loader that pulled
+ * in the template stream
+ * @return The resource loader for this resource.
+ */
+ public ResourceLoader getResourceLoader()
+ {
+ return resourceLoader;
+ }
+
+ /**
+ * Set the template loader for this template. Set
+ * when the Runtime determines where this template
+ * came from the list of possible sources.
+ * @param resourceLoader
+ */
+ public void setResourceLoader(ResourceLoader resourceLoader)
+ {
+ this.resourceLoader = resourceLoader;
+ }
+
+ /**
+ * Set arbitrary data object that might be used
+ * by the resource.
+ * @param data
+ */
+ public void setData(Object data)
+ {
+ this.data = data;
+ }
+
+ /**
+ * Get arbitrary data object that might be used
+ * by the resource.
+ * @return The data object for this resource.
+ */
+ public Object getData()
+ {
+ return data;
+ }
+
+ /**
+ * Sets the type of this Resource (RESOURCE_TEMPLATE or RESOURCE_CONTENT)
+ * @param type RESOURCE_TEMPLATE or RESOURCE_CONTENT
+ * @since 1.6
+ */
+ public void setType(int type)
+ {
+ this.type = type;
+ }
+
+ /**
+ * @return type code of the Resource
+ * @since 1.6
+ */
+ public int getType()
+ {
+ return type;
+ }
+
+ /**
+ * @return cloned resource
+ * @since 2.4
+ */
+ @Override
+ public Object clone() {
+ try
+ {
+ Resource clone = (Resource) super.clone();
+ clone.deepCloneData();
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("cloning not supported");
+ }
+ }
+
+ /**
+ * Deep cloning of resource data
+ * @return cloned data
+ */
+ protected void deepCloneData() throws CloneNotSupportedException
+ {
+ // default implementation does nothing
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCache.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCache.java
new file mode 100644
index 00000000..98570971
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCache.java
@@ -0,0 +1,81 @@
+package org.apache.velocity.runtime.resource;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.RuntimeServices;
+
+import java.util.Iterator;
+
+/**
+ * Interface that defines the shape of a pluggable resource cache
+ * for the included ResourceManager
+ *
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public interface ResourceCache
+{
+ /**
+ * initializes the ResourceCache. Will be
+ * called before any utilization
+ *
+ * @param rs RuntimeServices to use for logging, etc
+ */
+ void initialize(RuntimeServices rs);
+
+ /**
+ * retrieves a Resource from the
+ * cache
+ *
+ * @param resourceKey key for Resource to be retrieved
+ * @return Resource specified or null if not found
+ */
+ Resource get(Object resourceKey);
+
+ /**
+ * stores a Resource in the cache
+ *
+ * @param resourceKey key to associate with the Resource
+ * @param resource Resource to be stored
+ * @return existing Resource stored under this key, or null if none
+ */
+ Resource put(Object resourceKey, Resource resource);
+
+ /**
+ * removes a Resource from the cache
+ *
+ * @param resourceKey resource to be removed
+ * @return stored under key
+ */
+ Resource remove(Object resourceKey);
+
+ /**
+ * Removes all of the resources from this cache.
+ * The cache will be empty after this call returns.
+ * @since 2.0
+ */
+ void clear();
+
+ /**
+ * returns an Iterator of Keys in the cache.
+ * @return An Iterator of Keys in the cache.
+ */
+ Iterator enumerateKeys();
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCacheImpl.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCacheImpl.java
new file mode 100644
index 00000000..7ad03518
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceCacheImpl.java
@@ -0,0 +1,168 @@
+package org.apache.velocity.runtime.resource;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.slf4j.Logger;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Default implementation of the resource cache for the default
+ * ResourceManager. The cache uses a <i>least recently used</i> (LRU)
+ * algorithm, with a maximum size specified via the
+ * <code>resource.manager.cache.size</code> property (identified by the
+ * {@link
+ * org.apache.velocity.runtime.RuntimeConstants#RESOURCE_MANAGER_DEFAULTCACHE_SIZE}
+ * constant). This property get be set to <code>0</code> or less for
+ * a greedy, unbounded cache (the behavior from pre-v1.5).
+ *
+ * @author <a href="mailto:geirm@apache.org">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
+ * @version $Id$
+ */
+public class ResourceCacheImpl implements ResourceCache
+{
+
+ /**
+ * A simple LRU Map based on {@link LinkedHashSet}.
+ *
+ * @param <K> The key type of the map.
+ * @param <V> The value type of the map.
+ */
+ private static class LRUMap<K, V> extends LinkedHashMap<K, V>{
+
+ /**
+ * The serial version uid;
+ */
+ private static final long serialVersionUID = 5889225121697975043L;
+
+ /**
+ * The size of the cache.
+ */
+ private int cacheSize;
+
+ /**
+ * Constructor.
+ *
+ * @param cacheSize The size of the cache. After reaching this size, the
+ * eldest-accessed element will be erased.
+ */
+ public LRUMap(int cacheSize)
+ {
+ this.cacheSize = cacheSize;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean removeEldestEntry(Entry<K, V> eldest)
+ {
+ return size() > cacheSize;
+ }
+ }
+
+ /**
+ * Cache storage, assumed to be thread-safe.
+ */
+ protected Map<Object, Resource> cache = new ConcurrentHashMap<>(512, 0.5f, 30);
+
+ /**
+ * Runtime services, generally initialized by the
+ * <code>initialize()</code> method.
+ */
+ protected RuntimeServices rsvc = null;
+
+ protected Logger log;
+
+ /**
+ * @see org.apache.velocity.runtime.resource.ResourceCache#initialize(org.apache.velocity.runtime.RuntimeServices)
+ */
+ @Override
+ public void initialize(RuntimeServices rs )
+ {
+ rsvc = rs;
+
+ int maxSize =
+ rsvc.getInt(RuntimeConstants.RESOURCE_MANAGER_DEFAULTCACHE_SIZE, 89);
+ if (maxSize > 0)
+ {
+ // Create a whole new Map here to avoid hanging on to a
+ // handle to the unsynch'd LRUMap for our lifetime.
+ Map<Object, Resource> lruCache = Collections.synchronizedMap(new LRUMap<>(maxSize));
+ lruCache.putAll(cache);
+ cache = lruCache;
+ }
+ rsvc.getLog().debug("initialized ({}) with {} cache map.", this.getClass(), cache.getClass());
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.resource.ResourceCache#get(java.lang.Object)
+ */
+ @Override
+ public Resource get(Object key )
+ {
+ return cache.get( key );
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.resource.ResourceCache#put(java.lang.Object, org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public Resource put(Object key, Resource value )
+ {
+ return cache.put( key, value );
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.resource.ResourceCache#remove(java.lang.Object)
+ */
+ @Override
+ public Resource remove(Object key )
+ {
+ return cache.remove( key );
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.resource.ResourceCache#clear()
+ * @since 2.0
+ */
+ @Override
+ public void clear()
+ {
+ cache.clear();
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.resource.ResourceCache#enumerateKeys()
+ */
+ @Override
+ public Iterator<Object> enumerateKeys()
+ {
+ return cache.keySet().iterator();
+ }
+}
+
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceFactory.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceFactory.java
new file mode 100644
index 00000000..e95712aa
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceFactory.java
@@ -0,0 +1,59 @@
+package org.apache.velocity.runtime.resource;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.Template;
+import org.apache.velocity.exception.VelocityException;
+
+/**
+ * Class responsible for instantiating <code>Resource</code> objects,
+ * given name and type.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ResourceFactory
+{
+ /**
+ * @param resourceName
+ * @param resourceType
+ * @return The resource described by name and type.
+ */
+ public static Resource getResource(String resourceName, int resourceType)
+ {
+ Resource resource = null;
+
+ switch (resourceType)
+ {
+ case ResourceManager.RESOURCE_TEMPLATE:
+ resource = new Template();
+ break;
+
+ case ResourceManager.RESOURCE_CONTENT:
+ resource = new ContentResource();
+ break;
+ default:
+ throw new VelocityException("invalide resource type");
+ }
+
+ return resource;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManager.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManager.java
new file mode 100644
index 00000000..a5b64395
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManager.java
@@ -0,0 +1,81 @@
+package org.apache.velocity.runtime.resource;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.RuntimeServices;
+
+/**
+ * Class to manage the text resource for the Velocity
+ * Runtime.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:paulo.gaspar@krankikom.de">Paulo Gaspar</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public interface ResourceManager
+{
+ /**
+ * A template resources.
+ */
+ int RESOURCE_TEMPLATE = 1;
+
+ /**
+ * A static content resource.
+ */
+ int RESOURCE_CONTENT = 2;
+
+ /**
+ * Initialize the ResourceManager.
+ * @param rs
+ */
+ void initialize(RuntimeServices rs);
+
+ /**
+ * Gets the named resource. Returned class type corresponds to specified type
+ * (i.e. <code>Template</code> to <code>RESOURCE_TEMPLATE</code>).
+ *
+ * @param resourceName The name of the resource to retrieve.
+ * @param resourceType The type of resource (<code>RESOURCE_TEMPLATE</code>,
+ * <code>RESOURCE_CONTENT</code>, etc.).
+ * @param encoding The character encoding to use.
+ * @return Resource with the template parsed and ready.
+ * @throws ResourceNotFoundException if template not found
+ * from any available source.
+ * @throws ParseErrorException if template cannot be parsed due
+ * to syntax (or other) error.
+ */
+ Resource getResource(String resourceName, int resourceType, String encoding)
+ throws ResourceNotFoundException, ParseErrorException;
+
+ /**
+ * Determines is a template exists, and returns name of the loader that
+ * provides it. This is a slightly less hokey way to support
+ * the Velocity.templateExists() utility method, which was broken
+ * when per-template encoding was introduced. We can revisit this.
+ *
+ * @param resourceName Name of template or content resource
+ * @return class name of loader than can provide it
+ */
+ String getLoaderNameForResource(String resourceName);
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManagerImpl.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManagerImpl.java
new file mode 100644
index 00000000..82843ac1
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/ResourceManagerImpl.java
@@ -0,0 +1,607 @@
+package org.apache.velocity.runtime.resource;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.resource.loader.ResourceLoader;
+import org.apache.velocity.runtime.resource.loader.ResourceLoaderFactory;
+import org.apache.velocity.util.ClassUtils;
+import org.apache.velocity.util.ExtProperties;
+
+import org.apache.commons.lang3.StringUtils;
+
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Vector;
+
+
+/**
+ * Class to manage the text resource for the Velocity Runtime.
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:paulo.gaspar@krankikom.de">Paulo Gaspar</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ */
+public class ResourceManagerImpl
+ implements ResourceManager
+{
+
+ /** A template resources. */
+ public static final int RESOURCE_TEMPLATE = 1;
+
+ /** A static content resource. */
+ public static final int RESOURCE_CONTENT = 2;
+
+ /** Object implementing ResourceCache to be our resource manager's Resource cache. */
+ protected ResourceCache globalCache = null;
+
+ /** The List of templateLoaders that the Runtime will use to locate the InputStream source of a template. */
+ protected final List<ResourceLoader> resourceLoaders = new ArrayList<>();
+
+ /**
+ * This is a list of the template input stream source initializers, basically properties for a particular template stream
+ * source. The order in this list reflects numbering of the properties i.e.
+ *
+ * <p>resource.loader.&lt;loader-id&gt;.&lt;property&gt; = &lt;value&gt;</p>
+ */
+ private final List<ExtProperties> sourceInitializerList = new ArrayList<>();
+
+ /**
+ * Has this Manager been initialized?
+ */
+ private boolean isInit = false;
+
+ /** switch to turn off log notice when a resource is found for the first time. */
+ private boolean logWhenFound = true;
+
+ /** The internal RuntimeServices object. */
+ protected RuntimeServices rsvc = null;
+
+ /** Logging. */
+ protected Logger log = null;
+
+ /**
+ * Initialize the ResourceManager.
+ *
+ * @param rs The Runtime Services object which is associated with this Resource Manager.
+ */
+ @Override
+ public synchronized void initialize(final RuntimeServices rs)
+ {
+ if (isInit)
+ {
+ log.debug("Re-initialization of ResourceLoader attempted and ignored.");
+ return;
+ }
+
+ ResourceLoader resourceLoader = null;
+
+ rsvc = rs;
+ log = rsvc.getLog("loader");
+
+ log.trace("ResourceManager initializing: {}", this.getClass());
+
+ assembleResourceLoaderInitializers();
+
+ for (ExtProperties configuration : sourceInitializerList)
+ {
+ /*
+ * Resource loader can be loaded either via class name or be passed
+ * in as an instance.
+ */
+
+ String loaderClass = StringUtils.trim(configuration.getString(RuntimeConstants.RESOURCE_LOADER_CLASS));
+ ResourceLoader loaderInstance = (ResourceLoader) configuration.get(RuntimeConstants.RESOURCE_LOADER_INSTANCE);
+
+ if (loaderInstance != null)
+ {
+ resourceLoader = loaderInstance;
+ } else if (loaderClass != null)
+ {
+ resourceLoader = ResourceLoaderFactory.getLoader(rsvc, loaderClass);
+ } else
+ {
+ String msg = "Unable to find 'resource.loader." +
+ configuration.getString(RuntimeConstants.RESOURCE_LOADER_IDENTIFIER) +
+ ".class' specification in configuration." +
+ " This is a critical value. Please adjust configuration.";
+ log.error(msg);
+ throw new VelocityException(msg, null, rsvc.getLogContext().getStackTrace());
+ }
+
+ resourceLoader.commonInit(rsvc, configuration);
+ resourceLoader.init(configuration);
+ resourceLoaders.add(resourceLoader);
+ }
+
+ /*
+ * now see if this is overridden by configuration
+ */
+
+ logWhenFound = rsvc.getBoolean(RuntimeConstants.RESOURCE_MANAGER_LOGWHENFOUND, true);
+
+ /*
+ * now, is a global cache specified?
+ */
+
+ String cacheClassName = rsvc.getString(RuntimeConstants.RESOURCE_MANAGER_CACHE_CLASS);
+
+ Object cacheObject = null;
+
+ if (StringUtils.isNotEmpty(cacheClassName))
+ {
+ try
+ {
+ cacheObject = ClassUtils.getNewInstance(cacheClassName);
+ }
+ catch (ClassNotFoundException cnfe)
+ {
+ String msg = "The specified class for ResourceCache (" + cacheClassName +
+ ") does not exist or is not accessible to the current classloader.";
+ log.error(msg, cnfe);
+ throw new VelocityException(msg, cnfe);
+ }
+ catch (IllegalAccessException ae)
+ {
+ throw new VelocityException("Could not access class '"
+ + cacheClassName + "'", ae);
+ }
+ catch (InstantiationException ie)
+ {
+ throw new VelocityException("Could not instantiate class '"
+ + cacheClassName + "'", ie);
+ }
+
+ if (!(cacheObject instanceof ResourceCache))
+ {
+ String msg = "The specified resource cache class (" + cacheClassName +
+ ") must implement " + ResourceCache.class.getName();
+ log.error(msg);
+ throw new RuntimeException(msg);
+ }
+ }
+
+ /*
+ * if we didn't get through that, just use the default.
+ */
+ if (cacheObject == null)
+ {
+ cacheObject = new ResourceCacheImpl();
+ }
+
+ globalCache = (ResourceCache) cacheObject;
+
+ globalCache.initialize(rsvc);
+
+ isInit = true;
+
+ log.trace("Default ResourceManager initialization complete.");
+ }
+
+ /**
+ * This will produce a List of Hashtables, each hashtable contains the initialization info for a particular resource loader. This
+ * Hashtable will be passed in when initializing the the template loader.
+ */
+ private void assembleResourceLoaderInitializers()
+ {
+ Vector<String> resourceLoaderNames = rsvc.getConfiguration().getVector(RuntimeConstants.RESOURCE_LOADERS);
+
+ for (ListIterator<String> it = resourceLoaderNames.listIterator(); it.hasNext(); )
+ {
+ /*
+ * The loader id might look something like the following:
+ *
+ * resource.loader.file
+ *
+ * The loader id is the prefix used for all properties
+ * pertaining to a particular loader.
+ */
+ String loaderName = StringUtils.trim(it.next());
+ it.set(loaderName);
+ StringBuilder loaderID = new StringBuilder();
+ loaderID.append(RuntimeConstants.RESOURCE_LOADER).append('.').append(loaderName);
+
+ ExtProperties loaderConfiguration =
+ rsvc.getConfiguration().subset(loaderID.toString());
+
+ /*
+ * we can't really count on ExtProperties to give us an empty set
+ */
+ if (loaderConfiguration == null)
+ {
+ log.debug("ResourceManager : No configuration information found "+
+ "for resource loader named '{}' (id is {}). Skipping it...",
+ loaderName, loaderID);
+ continue;
+ }
+
+ /*
+ * add the loader name token to the initializer if we need it
+ * for reference later. We can't count on the user to fill
+ * in the 'name' field
+ */
+
+ loaderConfiguration.setProperty(RuntimeConstants.RESOURCE_LOADER_IDENTIFIER, loaderName);
+
+ /*
+ * Add resources to the list of resource loader
+ * initializers.
+ */
+ sourceInitializerList.add(loaderConfiguration);
+ }
+ }
+
+ /**
+ * Gets the named resource. Returned class type corresponds to specified type (i.e. <code>Template</code> to <code>
+ * RESOURCE_TEMPLATE</code>).
+ *
+ * This method is now unsynchronized which requires that ResourceCache
+ * implementations be thread safe (as the default is).
+ *
+ * @param resourceName The name of the resource to retrieve.
+ * @param resourceType The type of resource (<code>RESOURCE_TEMPLATE</code>, <code>RESOURCE_CONTENT</code>, etc.).
+ * @param encoding The character encoding to use.
+ *
+ * @return Resource with the template parsed and ready.
+ *
+ * @throws ResourceNotFoundException if template not found from any available source.
+ * @throws ParseErrorException if template cannot be parsed due to syntax (or other) error.
+ */
+ @Override
+ public Resource getResource(final String resourceName, final int resourceType, final String encoding)
+ throws ResourceNotFoundException,
+ ParseErrorException
+ {
+ /*
+ * Check to see if the resource was placed in the cache.
+ * If it was placed in the cache then we will use
+ * the cached version of the resource. If not we
+ * will load it.
+ *
+ * Note: the type is included in the key to differentiate ContentResource
+ * (static content from #include) with a Template.
+ */
+
+ String resourceKey = resourceType + resourceName;
+ Resource resource = globalCache.get(resourceKey);
+
+ if (resource != null)
+ {
+ try
+ {
+ // avoids additional method call to refreshResource
+ if (resource.requiresChecking())
+ {
+ /*
+ * both loadResource() and refreshResource() now return
+ * a new Resource instance when they are called
+ * (put in the cache when appropriate) in order to allow
+ * several threads to parse the same template simultaneously.
+ * It is redundant work and will cause more garbage collection but the
+ * benefit is that it allows concurrent parsing and processing
+ * without race conditions when multiple requests try to
+ * refresh/load the same template at the same time.
+ *
+ * Another alternative is to limit template parsing/retrieval
+ * so that only one thread can parse each template at a time
+ * but that creates a scalability bottleneck.
+ *
+ * See VELOCITY-606, VELOCITY-595 and VELOCITY-24
+ */
+ resource = refreshResource(resource, encoding);
+ }
+ }
+ catch (ResourceNotFoundException rnfe)
+ {
+ /*
+ * something exceptional happened to that resource
+ * this could be on purpose,
+ * so clear the cache and try again
+ */
+
+ globalCache.remove(resourceKey);
+
+ return getResource(resourceName, resourceType, encoding);
+ }
+ catch (RuntimeException re)
+ {
+ log.error("ResourceManager.getResource() exception", re);
+ throw re;
+ }
+ }
+ else
+ {
+ try
+ {
+ /*
+ * it's not in the cache, so load it.
+ */
+ resource = loadResource(resourceName, resourceType, encoding);
+
+ if (resource.getResourceLoader().isCachingOn())
+ {
+ globalCache.put(resourceKey, resource);
+ }
+ }
+ catch (ResourceNotFoundException rnfe)
+ {
+ log.error("ResourceManager: unable to find resource '{}' in any resource loader.", resourceName);
+ throw rnfe;
+ }
+ catch (ParseErrorException pee)
+ {
+ log.error("ResourceManager: parse exception: {}", pee.getMessage());
+ throw pee;
+ }
+ catch (RuntimeException re)
+ {
+ log.error("ResourceManager.getResource() load exception", re);
+ throw re;
+ }
+ }
+
+ return resource;
+ }
+
+ /**
+ * Create a new Resource of the specified type.
+ *
+ * @param resourceName The name of the resource to retrieve.
+ * @param resourceType The type of resource (<code>RESOURCE_TEMPLATE</code>, <code>RESOURCE_CONTENT</code>, etc.).
+ * @return new instance of appropriate resource type
+ * @since 1.6
+ */
+ protected Resource createResource(String resourceName, int resourceType)
+ {
+ return ResourceFactory.getResource(resourceName, resourceType);
+ }
+
+ /**
+ * Loads a resource from the current set of resource loaders.
+ *
+ * @param resourceName The name of the resource to retrieve.
+ * @param resourceType The type of resource (<code>RESOURCE_TEMPLATE</code>, <code>RESOURCE_CONTENT</code>, etc.).
+ * @param encoding The character encoding to use.
+ *
+ * @return Resource with the template parsed and ready.
+ *
+ * @throws ResourceNotFoundException if template not found from any available source.
+ * @throws ParseErrorException if template cannot be parsed due to syntax (or other) error.
+ */
+ protected Resource loadResource(String resourceName, int resourceType, String encoding)
+ throws ResourceNotFoundException,
+ ParseErrorException
+ {
+ Resource resource = createResource(resourceName, resourceType);
+ resource.setRuntimeServices(rsvc);
+ resource.setName(resourceName);
+ resource.setEncoding(encoding);
+
+ /*
+ * Now we have to try to find the appropriate
+ * loader for this resource. We have to cycle through
+ * the list of available resource loaders and see
+ * which one gives us a stream that we can use to
+ * make a resource with.
+ */
+
+ long howOldItWas = 0;
+
+ for (ResourceLoader resourceLoader : resourceLoaders)
+ {
+ resource.setResourceLoader(resourceLoader);
+
+ /*
+ * catch the ResourceNotFound exception
+ * as that is ok in our new multi-loader environment
+ */
+
+ try
+ {
+
+ if (resource.process())
+ {
+ /*
+ * FIXME (gmj)
+ * moved in here - technically still
+ * a problem - but the resource needs to be
+ * processed before the loader can figure
+ * it out due to to the new
+ * multi-path support - will revisit and fix
+ */
+
+ if (logWhenFound)
+ {
+ log.debug("ResourceManager: found {} with loader {}",
+ resourceName, resourceLoader.getClassName());
+ }
+
+ howOldItWas = resourceLoader.getLastModified(resource);
+
+ break;
+ }
+ }
+ catch (ResourceNotFoundException rnfe)
+ {
+ /*
+ * that's ok - it's possible to fail in
+ * multi-loader environment
+ */
+ }
+ }
+
+ /*
+ * Return null if we can't find a resource.
+ */
+ if (resource.getData() == null)
+ {
+ throw new ResourceNotFoundException("Unable to find resource '" + resourceName + "'", null, rsvc.getLogContext().getStackTrace());
+ }
+
+ /*
+ * some final cleanup
+ */
+
+ resource.setLastModified(howOldItWas);
+ resource.setModificationCheckInterval(resource.getResourceLoader().getModificationCheckInterval());
+
+ resource.touch();
+
+ return resource;
+ }
+
+ /**
+ * Takes an existing resource, and 'refreshes' it. This generally means that the source of the resource is checked for changes
+ * according to some cache/check algorithm and if the resource changed, then the resource data is reloaded and re-parsed.
+ *
+ * @param resource resource to refresh
+ * @param encoding character encoding of the resource to refresh.
+ * @return resource
+ * @throws ResourceNotFoundException if template not found from current source for this Resource
+ * @throws ParseErrorException if template cannot be parsed due to syntax (or other) error.
+ */
+ protected Resource refreshResource(Resource resource, final String encoding)
+ throws ResourceNotFoundException, ParseErrorException
+ {
+ /*
+ * The resource knows whether it needs to be checked
+ * or not, and the resource's loader can check to
+ * see if the source has been modified. If both
+ * these conditions are true then we must reload
+ * the input stream and parse it to make a new
+ * AST for the resource.
+ */
+
+ String resourceKey = resource.getType() + resource.getName();
+
+ /*
+ * touch() the resource to reset the counters
+ */
+ resource.touch();
+
+ /* check whether this can now be found in a higher priority
+ * resource loader. if so, pass the request off to loadResource.
+ */
+ ResourceLoader loader = resource.getResourceLoader();
+ if (resourceLoaders.size() > 0 && resourceLoaders.indexOf(loader) > 0)
+ {
+ String name = resource.getName();
+ if (loader != getLoaderForResource(name))
+ {
+ resource = loadResource(name, resource.getType(), encoding);
+ if (resource.getResourceLoader().isCachingOn())
+ {
+ globalCache.put(resourceKey, resource);
+ }
+ }
+ }
+
+ if (resource.isSourceModified())
+ {
+ /*
+ * now check encoding info. It's possible that the newly declared
+ * encoding is different than the encoding already in the resource
+ * this strikes me as bad...
+ */
+
+ if (!StringUtils.equals(resource.getEncoding(), encoding))
+ {
+ log.warn("Declared encoding for template '{}' is different on reload. Old = '{}' New = '{}'",
+ resource.getName(), resource.getEncoding(), encoding);
+ resource.setEncoding(encoding);
+ }
+
+ /*
+ * read how old the resource is _before_
+ * processing (=>reading) it
+ */
+ long howOldItWas = loader.getLastModified(resource);
+
+ /*
+ * we create a copy to avoid partially overwriting a
+ * template which may be in use in another thread
+ */
+
+ Resource newResource =
+ ResourceFactory.getResource(resource.getName(), resource.getType());
+
+ newResource.setRuntimeServices(rsvc);
+ newResource.setName(resource.getName());
+ newResource.setEncoding(resource.getEncoding());
+ newResource.setResourceLoader(loader);
+ newResource.setModificationCheckInterval(loader.getModificationCheckInterval());
+
+ newResource.process();
+ newResource.setLastModified(howOldItWas);
+ resource = newResource;
+ resource.touch();
+
+ globalCache.put(resourceKey, newResource);
+ }
+ return resource;
+ }
+
+ /**
+ * Determines if a template exists, and returns name of the loader that provides it. This is a slightly less hokey way to
+ * support the Velocity.templateExists() utility method, which was broken when per-template encoding was introduced. We can
+ * revisit this.
+ *
+ * @param resourceName Name of template or content resource
+ *
+ * @return class name of loader than can provide it
+ */
+ @Override
+ public String getLoaderNameForResource(String resourceName)
+ {
+ ResourceLoader loader = getLoaderForResource(resourceName);
+ if (loader == null)
+ {
+ return null;
+ }
+ return loader.getClass().toString();
+ }
+
+ /**
+ * Returns the first {@link ResourceLoader} in which the specified
+ * resource exists.
+ */
+ private ResourceLoader getLoaderForResource(String resourceName)
+ {
+ for (ResourceLoader loader : resourceLoaders)
+ {
+ if (loader.resourceExists(resourceName))
+ {
+ return loader;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ClasspathResourceLoader.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ClasspathResourceLoader.java
new file mode 100644
index 00000000..9bfd0f14
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ClasspathResourceLoader.java
@@ -0,0 +1,169 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.util.ClassUtils;
+import org.apache.velocity.util.ExtProperties;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+
+/**
+ * ClasspathResourceLoader is a simple loader that will load
+ * templates from the classpath.
+ * <br>
+ * <br>
+ * Will load templates from from multiple instances of
+ * and arbitrary combinations of:
+ * <ul>
+ * <li> jar files
+ * <li> zip files
+ * <li> template directories (any directory containing templates)
+ * </ul>
+ * This is a configuration-free loader, in that there are no
+ * parameters to be specified in the configuration properties,
+ * other than specifying this as the loader to use. For example
+ * the following is all that the loader needs to be functional:
+ * <br>
+ * <pre><code>
+ * resource.loaders = class
+ * resource.loader.class.class =org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
+ * </code></pre>
+ * <br>
+ * <br>
+ * To use, put your template directories, jars
+ * and zip files into the classpath or other mechanisms that make
+ * resources accessible to the classloader.
+ * <br>
+ * <br>
+ * This makes deployment trivial for web applications running in
+ * any Servlet 2.2 compliant servlet runner, such as Tomcat 3.2
+ * and others.
+ * <br>
+ * <br>
+ * For a Servlet Spec v2.2 servlet runner,
+ * just drop the jars of template files into the WEB-INF/lib
+ * directory of your webapp, and you won't have to worry about setting
+ * template paths or altering them with the root of the webapp
+ * before initializing.
+ * <br>
+ * <br>
+ * I have also tried it with a WAR deployment, and that seemed to
+ * work just fine.
+ *
+ * @author <a href="mailto:mailmur@yahoo.com">Aki Nieminen</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public class ClasspathResourceLoader extends ResourceLoader
+{
+
+ /**
+ * This is abstract in the base class, so we need it
+ * @param configuration
+ */
+ @Override
+ public void init(ExtProperties configuration)
+ {
+ log.trace("ClasspathResourceLoader: initialization complete.");
+ }
+
+ /**
+ * Get a Reader so that the Runtime can build a
+ * template with it.
+ *
+ * @param name name of template to get
+ * @param encoding asked encoding
+ * @return InputStream containing the template
+ * @throws ResourceNotFoundException if template not found
+ * in classpath.
+ * @since 2.0
+ */
+ @Override
+ public Reader getResourceReader(String name, String encoding )
+ throws ResourceNotFoundException
+ {
+ Reader result = null;
+
+ if (StringUtils.isEmpty(name))
+ {
+ throw new ResourceNotFoundException ("No template name provided");
+ }
+
+ /*
+ * look for resource in thread classloader first (e.g. WEB-INF\lib in
+ * a servlet container) then fall back to the system classloader.
+ */
+
+ InputStream rawStream = null;
+ try
+ {
+ rawStream = ClassUtils.getResourceAsStream( getClass(), name );
+ if (rawStream != null)
+ {
+ result = buildReader(rawStream, encoding);
+ }
+ }
+ catch( Exception fnfe )
+ {
+ if (rawStream != null)
+ {
+ try
+ {
+ rawStream.close();
+ }
+ catch (IOException ioe) {}
+ }
+ throw new ResourceNotFoundException("ClasspathResourceLoader problem with template: " + name, fnfe, rsvc.getLogContext().getStackTrace() );
+ }
+
+ if (result == null)
+ {
+ String msg = "ClasspathResourceLoader Error: cannot find resource " + name;
+
+ throw new ResourceNotFoundException( msg, null, rsvc.getLogContext().getStackTrace() );
+ }
+
+ return result;
+ }
+
+ /**
+ * @see ResourceLoader#isSourceModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public boolean isSourceModified(Resource resource)
+ {
+ return false;
+ }
+
+ /**
+ * @see ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public long getLastModified(Resource resource)
+ {
+ return 0;
+ }
+}
+
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DataSourceResourceLoader.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DataSourceResourceLoader.java
new file mode 100644
index 00000000..b9cc48cc
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DataSourceResourceLoader.java
@@ -0,0 +1,550 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.util.ExtProperties;
+
+import org.apache.commons.lang3.StringUtils;
+
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.sql.DataSource;
+import java.io.FilterReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+
+/**
+ * <p>This is a simple template file loader that loads templates
+ * from a DataSource instead of plain files.</p>
+ *
+ * <p>It can be configured with a datasource name, a table name,
+ * id column (name), content column (the template body) and a
+ * datetime column (for last modification info).</p>
+ * <br>
+ * <p>Example configuration snippet for velocity.properties:</p>
+ * <br>
+ * <pre><code>
+ * resource.loaders = file, ds
+ *
+ * resource.loader.ds.description = Velocity DataSource Resource Loader <br>
+ * resource.loader.ds.class = org.apache.velocity.runtime.resource.loader.DataSourceResourceLoader <br>
+ * resource.loader.ds.resource.datasource_url = java:comp/env/jdbc/Velocity <br>
+ * resource.loader.ds.resource.table = tb_velocity_template <br>
+ * resource.loader.ds.resource.key_column = id_template <br>
+ * resource.loader.ds.resource.template_column = template_definition <br>
+ * resource.loader.ds.resource.timestamp_column = template_timestamp <br>
+ * resource.loader.ds.cache = false <br>
+ * resource.loader.ds.modification_check_interval = 60 <br>
+ * </code></pre>
+ * <p>Optionally, the developer can instantiate the DataSourceResourceLoader and set the DataSource via code in
+ * a manner similar to the following:</p>
+ * <br>
+ * <pre><code>
+ * DataSourceResourceLoader ds = new DataSourceResourceLoader();
+ * ds.setDataSource(DATASOURCE);
+ * Velocity.setProperty("resource.loader.ds.instance",ds);
+ * </code></pre>
+ * <p> The property <code>resource.loader.ds.class</code> should be left out, otherwise all the other
+ * properties in velocity.properties would remain the same.</p>
+ * <br>
+ * <p>Example WEB-INF/web.xml:</p>
+ * <br>
+ * <pre><code>
+ * &lt;resource-ref&gt;
+ * &lt;description&gt;Velocity template DataSource&lt;/description&gt;
+ * &lt;res-ref-name&gt;jdbc/Velocity&lt;/res-ref-name&gt;
+ * &lt;res-type&gt;javax.sql.DataSource&lt;/res-type&gt;
+ * &lt;res-auth&gt;Container&lt;/res-auth&gt;
+ * &lt;/resource-ref&gt;
+ * </code></pre>
+ * <br>
+ * and Tomcat 4 server.xml file: <br>
+ * <pre><code>
+ * [...]
+ * &lt;Context path="/exampleVelocity" docBase="exampleVelocity" debug="0"&gt;
+ * [...]
+ * &lt;ResourceParams name="jdbc/Velocity"&gt;
+ * &lt;parameter&gt;
+ * &lt;name&gt;driverClassName&lt;/name&gt;
+ * &lt;value&gt;org.hsql.jdbcDriver&lt;/value&gt;
+ * &lt;/parameter&gt;
+ * &lt;parameter&gt;
+ * &lt;name&gt;driverName&lt;/name&gt;
+ * &lt;value&gt;jdbc:HypersonicSQL:database&lt;/value&gt;
+ * &lt;/parameter&gt;
+ * &lt;parameter&gt;
+ * &lt;name&gt;user&lt;/name&gt;
+ * &lt;value&gt;database_username&lt;/value&gt;
+ * &lt;/parameter&gt;
+ * &lt;parameter&gt;
+ * &lt;name&gt;password&lt;/name&gt;
+ * &lt;value&gt;database_password&lt;/value&gt;
+ * &lt;/parameter&gt;
+ * &lt;/ResourceParams&gt;
+ * [...]
+ * &lt;/Context&gt;
+ * [...]
+ * </code></pre>
+ * <br>
+ * <p>Example sql script:</p>
+ * <pre><code>
+ * CREATE TABLE tb_velocity_template (
+ * id_template varchar (40) NOT NULL ,
+ * template_definition text (16) NOT NULL ,
+ * template_timestamp datetime NOT NULL
+ * );
+ * </code></pre>
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @author <a href="mailto:matt@raibledesigns.com">Matt Raible</a>
+ * @author <a href="mailto:david.kinnvall@alertir.com">David Kinnvall</a>
+ * @author <a href="mailto:paulo.gaspar@krankikom.de">Paulo Gaspar</a>
+ * @author <a href="mailto:lachiewicz@plusnet.pl">Sylwester Lachiewicz</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ * @since 1.5
+ */
+public class DataSourceResourceLoader extends ResourceLoader
+{
+ private String dataSourceName;
+ private String tableName;
+ private String keyColumn;
+ private String templateColumn;
+ private String timestampColumn;
+ private InitialContext ctx;
+ private DataSource dataSource;
+
+ /*
+ Keep connection and prepared statements open. It's not just an optimization:
+ For several engines, the connection, and/or the statement, and/or the result set
+ must be kept open for the reader to be valid.
+ */
+ private Connection connection = null;
+ private PreparedStatement templatePrepStatement = null;
+ private PreparedStatement timestampPrepStatement = null;
+
+ private static class SelfCleaningReader extends FilterReader
+ {
+ private ResultSet resultSet;
+
+ public SelfCleaningReader(Reader reader, ResultSet resultSet)
+ {
+ super(reader);
+ this.resultSet = resultSet;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ super.close();
+ try
+ {
+ resultSet.close();
+ }
+ catch (RuntimeException re)
+ {
+ throw re;
+ }
+ catch (Exception e)
+ {
+ // ignore
+ }
+ }
+ }
+
+ /**
+ * @see ResourceLoader#init(org.apache.velocity.util.ExtProperties)
+ */
+ @Override
+ public void init(ExtProperties configuration)
+ {
+ dataSourceName = StringUtils.trim(configuration.getString("datasource_url"));
+ tableName = StringUtils.trim(configuration.getString("resource.table"));
+ keyColumn = StringUtils.trim(configuration.getString("resource.key_column"));
+ templateColumn = StringUtils.trim(configuration.getString("resource.template_column"));
+ timestampColumn = StringUtils.trim(configuration.getString("resource.timestamp_column"));
+
+ if (dataSource != null)
+ {
+ log.debug("DataSourceResourceLoader: using dataSource instance with table \"{}\"", tableName);
+ log.debug("DataSourceResourceLoader: using columns \"{}\", \"{}\" and \"{}\"", keyColumn, templateColumn, timestampColumn);
+
+ log.trace("DataSourceResourceLoader initialized.");
+ }
+ else if (dataSourceName != null)
+ {
+ log.debug("DataSourceResourceLoader: using \"{}\" datasource with table \"{}\"", dataSourceName, tableName);
+ log.debug("DataSourceResourceLoader: using columns \"{}\", \"{}\" and \"{}\"", keyColumn, templateColumn, timestampColumn);
+
+ log.trace("DataSourceResourceLoader initialized.");
+ }
+ else
+ {
+ String msg = "DataSourceResourceLoader not properly initialized. No DataSource was identified.";
+ log.error(msg);
+ throw new RuntimeException(msg);
+ }
+ }
+
+ /**
+ * Set the DataSource used by this resource loader. Call this as an alternative to
+ * specifying the data source name via properties.
+ * @param dataSource The data source for this ResourceLoader.
+ */
+ public void setDataSource(final DataSource dataSource)
+ {
+ this.dataSource = dataSource;
+ }
+
+ /**
+ * @see ResourceLoader#isSourceModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public boolean isSourceModified(final Resource resource)
+ {
+ return (resource.getLastModified() !=
+ readLastModified(resource, "checking timestamp"));
+ }
+
+ /**
+ * @see ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public long getLastModified(final Resource resource)
+ {
+ return readLastModified(resource, "getting timestamp");
+ }
+
+ /**
+ * Get an InputStream so that the Runtime can build a
+ * template with it.
+ *
+ * @param name name of template
+ * @param encoding asked encoding
+ * @return InputStream containing template
+ * @throws ResourceNotFoundException
+ * @since 2.0
+ */
+ @Override
+ public synchronized Reader getResourceReader(final String name, String encoding)
+ throws ResourceNotFoundException
+ {
+ if (StringUtils.isEmpty(name))
+ {
+ throw new ResourceNotFoundException("DataSourceResourceLoader: Template name was empty or null");
+ }
+
+ ResultSet rs = null;
+ try
+ {
+ checkDBConnection();
+ rs = fetchResult(templatePrepStatement, name);
+
+ if (rs.next())
+ {
+ Reader reader = getReader(rs, templateColumn, encoding);
+ if (reader == null)
+ {
+ throw new ResourceNotFoundException("DataSourceResourceLoader: "
+ + "template column for '"
+ + name + "' is null");
+ }
+ return new SelfCleaningReader(reader, rs);
+ }
+ else
+ {
+ throw new ResourceNotFoundException("DataSourceResourceLoader: "
+ + "could not find resource '"
+ + name + "'");
+
+ }
+ }
+ catch (SQLException | NamingException sqle)
+ {
+ String msg = "DataSourceResourceLoader: database problem while getting resource '"
+ + name + "': ";
+
+ log.error(msg, sqle);
+ throw new ResourceNotFoundException(msg);
+ }
+ }
+
+ /**
+ * Fetches the last modification time of the resource
+ *
+ * @param resource Resource object we are finding timestamp of
+ * @param operation string for logging, indicating caller's intention
+ *
+ * @return timestamp as long
+ */
+ private long readLastModified(final Resource resource, final String operation)
+ {
+ long timeStamp = 0;
+
+ /* get the template name from the resource */
+ String name = resource.getName();
+ if (name == null || name.length() == 0)
+ {
+ String msg = "DataSourceResourceLoader: Template name was empty or null";
+ log.error(msg);
+ throw new NullPointerException(msg);
+ }
+ else
+ {
+ ResultSet rs = null;
+
+ try
+ {
+ checkDBConnection();
+ rs = fetchResult(timestampPrepStatement, name);
+
+ if (rs.next())
+ {
+ Timestamp ts = rs.getTimestamp(timestampColumn);
+ timeStamp = ts != null ? ts.getTime() : 0;
+ }
+ else
+ {
+ String msg = "DataSourceResourceLoader: could not find resource "
+ + name + " while " + operation;
+ log.error(msg);
+ throw new ResourceNotFoundException(msg);
+ }
+ }
+ catch (SQLException | NamingException sqle)
+ {
+ String msg = "DataSourceResourceLoader: database problem while "
+ + operation + " of '" + name + "': ";
+
+ log.error(msg, sqle);
+ throw new VelocityException(msg, sqle, rsvc.getLogContext().getStackTrace());
+ }
+ finally
+ {
+ closeResultSet(rs);
+ }
+ }
+ return timeStamp;
+ }
+
+ /**
+ * Gets connection to the datasource specified through the configuration
+ * parameters.
+ *
+ */
+ private void openDBConnection() throws NamingException, SQLException
+ {
+ if (dataSource == null)
+ {
+ if (ctx == null)
+ {
+ ctx = new InitialContext();
+ }
+
+ dataSource = (DataSource) ctx.lookup(dataSourceName);
+ }
+
+ if (connection != null)
+ {
+ closeDBConnection();
+ }
+
+ connection = dataSource.getConnection();
+ templatePrepStatement = prepareStatement(connection, templateColumn, tableName, keyColumn);
+ timestampPrepStatement = prepareStatement(connection, timestampColumn, tableName, keyColumn);
+ }
+
+ /**
+ * Checks the connection is valid
+ *
+ */
+ private void checkDBConnection() throws NamingException, SQLException
+ {
+ if (connection == null || !connection.isValid(0))
+ {
+ openDBConnection();
+ }
+ }
+
+ /**
+ * Close DB connection on finalization
+ *
+ * @throws Throwable
+ */
+ @Override
+ protected void finalize()
+ throws Throwable
+ {
+ closeDBConnection();
+ }
+
+ /**
+ * Closes the prepared statements and the connection to the datasource
+ */
+ private void closeDBConnection()
+ {
+ if (templatePrepStatement != null)
+ {
+ try
+ {
+ templatePrepStatement.close();
+ }
+ catch (RuntimeException re)
+ {
+ throw re;
+ }
+ catch (SQLException e)
+ {
+ // ignore
+ }
+ finally
+ {
+ templatePrepStatement = null;
+ }
+ }
+ if (timestampPrepStatement != null)
+ {
+ try
+ {
+ timestampPrepStatement.close();
+ }
+ catch (RuntimeException re)
+ {
+ throw re;
+ }
+ catch (SQLException e)
+ {
+ // ignore
+ }
+ finally
+ {
+ timestampPrepStatement = null;
+ }
+ }
+ if (connection != null)
+ {
+ try
+ {
+ connection.close();
+ }
+ catch (RuntimeException re)
+ {
+ throw re;
+ }
+ catch (SQLException e)
+ {
+ // ignore
+ }
+ finally
+ {
+ connection = null;
+ }
+ }
+ }
+
+ /**
+ * Closes the result set.
+ */
+ private void closeResultSet(final ResultSet rs)
+ {
+ if (rs != null)
+ {
+ try
+ {
+ rs.close();
+ }
+ catch (RuntimeException re)
+ {
+ throw re;
+ }
+ catch (Exception e)
+ {
+ // ignore
+ }
+ }
+ }
+
+ /**
+ * Creates the following PreparedStatement query :
+ * <br>
+ * SELECT <i>columnNames</i> FROM <i>tableName</i> WHERE <i>keyColumn</i>
+ * = '<i>templateName</i>'
+ * <br>
+ * where <i>keyColumn</i> is a class member set in init()
+ *
+ * @param conn connection to datasource
+ * @param columnNames columns to fetch from datasource
+ * @param tableName table to fetch from
+ * @param keyColumn column whose value should match templateName
+ * @return PreparedStatement
+ * @throws SQLException
+ */
+ protected PreparedStatement prepareStatement(
+ final Connection conn,
+ final String columnNames,
+ final String tableName,
+ final String keyColumn
+ ) throws SQLException
+ {
+ PreparedStatement ps = conn.prepareStatement("SELECT " + columnNames + " FROM "+ tableName + " WHERE " + keyColumn + " = ?");
+ return ps;
+ }
+
+ /**
+ * Fetches the result for a given template name.
+ * Inherit this method if there is any calculation to perform on the template name.
+ *
+ * @param ps target prepared statement
+ * @param templateName input template name
+ * @return result set
+ * @throws SQLException
+ */
+ protected ResultSet fetchResult(
+ final PreparedStatement ps,
+ final String templateName
+ ) throws SQLException
+ {
+ ps.setString(1, templateName);
+ return ps.executeQuery();
+ }
+
+ /**
+ * Gets a reader from a result set's column
+ * @param resultSet
+ * @param column
+ * @param encoding
+ * @return reader
+ * @throws SQLException
+ */
+ protected Reader getReader(ResultSet resultSet, String column, String encoding)
+ throws SQLException
+ {
+ return resultSet.getCharacterStream(column);
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/FileResourceLoader.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/FileResourceLoader.java
new file mode 100644
index 00000000..ba9e5f98
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/FileResourceLoader.java
@@ -0,0 +1,373 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.util.ExtProperties;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+/**
+ * A loader for templates stored on the file system. Treats the template
+ * as relative to the configured root path. If the root path is empty
+ * treats the template name as an absolute path.
+ *
+ * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
+ * @author <a href="mailto:mailmur@yahoo.com">Aki Nieminen</a>
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @version $Id$
+ */
+public class FileResourceLoader extends ResourceLoader
+{
+ /**
+ * The paths to search for templates.
+ */
+ private List<String> paths = new ArrayList<>();
+
+ /**
+ * Used to map the path that a template was found on
+ * so that we can properly check the modification
+ * times of the files. This is synchronizedMap
+ * instance.
+ */
+ private Map<String, String> templatePaths = Collections.synchronizedMap(new HashMap<>());
+
+ /**
+ * @see ResourceLoader#init(org.apache.velocity.util.ExtProperties)
+ */
+ @Override
+ public void init(ExtProperties configuration)
+ {
+ log.trace("FileResourceLoader: initialization starting.");
+
+ paths.addAll( configuration.getVector(RuntimeConstants.RESOURCE_LOADER_PATHS) );
+
+ // trim spaces from all paths
+ for (ListIterator<String> it = paths.listIterator(); it.hasNext(); )
+ {
+ String path = StringUtils.trim(it.next());
+ it.set(path);
+ log.debug("FileResourceLoader: adding path '{}'", path);
+ }
+ log.trace("FileResourceLoader: initialization complete.");
+ }
+
+ /**
+ * Get a Reader so that the Runtime can build a
+ * template with it.
+ *
+ * @param templateName name of template to get
+ * @return Reader containing the template
+ * @throws ResourceNotFoundException if template not found
+ * in the file template path.
+ * @since 2.0
+ */
+ @Override
+ public Reader getResourceReader(String templateName, String encoding)
+ throws ResourceNotFoundException
+ {
+ /*
+ * Make sure we have a valid templateName.
+ */
+ if (org.apache.commons.lang3.StringUtils.isEmpty(templateName))
+ {
+ /*
+ * If we don't get a properly formed templateName then
+ * there's not much we can do. So we'll forget about
+ * trying to search any more paths for the template.
+ */
+ throw new ResourceNotFoundException(
+ "Need to specify a file name or file path!");
+ }
+
+ String template = FilenameUtils.normalize( templateName, true );
+ if ( template == null || template.length() == 0 )
+ {
+ String msg = "File resource error: argument " + template +
+ " contains .. and may be trying to access " +
+ "content outside of template root. Rejected.";
+
+ log.error("FileResourceLoader: {}", msg);
+
+ throw new ResourceNotFoundException ( msg );
+ }
+
+ int size = paths.size();
+ for (String path : paths)
+ {
+ InputStream rawStream = null;
+ Reader reader = null;
+
+ try
+ {
+ rawStream = findTemplate(path, template);
+ if (rawStream != null)
+ {
+ reader = buildReader(rawStream, encoding);
+ }
+ }
+ catch (IOException ioe)
+ {
+ closeQuiet(rawStream);
+ String msg = "Exception while loading Template " + template;
+ log.error(msg, ioe);
+ throw new VelocityException(msg, ioe, rsvc.getLogContext().getStackTrace());
+ }
+ if (reader != null)
+ {
+ /*
+ * Store the path that this template came
+ * from so that we can check its modification
+ * time.
+ */
+ templatePaths.put(templateName, path);
+ return reader;
+ }
+ }
+
+ /*
+ * We have now searched all the paths for
+ * templates and we didn't find anything so
+ * throw an exception.
+ */
+ throw new ResourceNotFoundException("FileResourceLoader: cannot find " + template);
+ }
+
+ /**
+ * Overrides superclass for better performance.
+ * @since 1.6
+ */
+ @Override
+ public boolean resourceExists(String name)
+ {
+ if (name == null)
+ {
+ return false;
+ }
+ name = FilenameUtils.normalize(name);
+ if (name == null || name.length() == 0)
+ {
+ return false;
+ }
+
+ int size = paths.size();
+ for (String path : paths)
+ {
+ try
+ {
+ File file = getFile(path, name);
+ if (file.canRead())
+ {
+ return true;
+ }
+ }
+ catch (Exception ioe)
+ {
+ log.debug("Exception while checking for template {}", name);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Try to find a template given a normalized path.
+ *
+ * @param path a normalized path
+ * @param template name of template to find
+ * @return InputStream input stream that will be parsed
+ *
+ */
+ private InputStream findTemplate(final String path, final String template)
+ throws IOException
+ {
+ try
+ {
+ File file = getFile(path, template);
+
+ if (file.canRead())
+ {
+ FileInputStream fis = null;
+ try
+ {
+ fis = new FileInputStream(file.getAbsolutePath());
+ return fis;
+ }
+ catch (IOException e)
+ {
+ closeQuiet(fis);
+ throw e;
+ }
+ }
+ else
+ {
+ return null;
+ }
+ }
+ catch(FileNotFoundException fnfe)
+ {
+ /*
+ * log and convert to a general Velocity ResourceNotFoundException
+ */
+ return null;
+ }
+ }
+
+ private void closeQuiet(final InputStream is)
+ {
+ if (is != null)
+ {
+ try
+ {
+ is.close();
+ }
+ catch(IOException ioe)
+ {
+ // Ignore
+ }
+ }
+ }
+
+ /**
+ * How to keep track of all the modified times
+ * across the paths. Note that a file might have
+ * appeared in a directory which is earlier in the
+ * path; so we should search the path and see if
+ * the file we find that way is the same as the one
+ * that we have cached.
+ * @param resource
+ * @return True if the source has been modified.
+ */
+ @Override
+ public boolean isSourceModified(Resource resource)
+ {
+ /*
+ * we assume that the file needs to be reloaded;
+ * if we find the original file and it's unchanged,
+ * then we'll flip this.
+ */
+ boolean modified = true;
+
+ String fileName = resource.getName();
+ String path = templatePaths.get(fileName);
+ File currentFile = null;
+
+ for (int i = 0; currentFile == null && i < paths.size(); i++)
+ {
+ String testPath = (String) paths.get(i);
+ File testFile = getFile(testPath, fileName);
+ if (testFile.canRead())
+ {
+ currentFile = testFile;
+ }
+ }
+ File file = getFile(path, fileName);
+ if (currentFile == null || !file.exists())
+ {
+ /*
+ * noop: if the file is missing now (either the cached
+ * file is gone, or the file can no longer be found)
+ * then we leave modified alone (it's set to true); a
+ * reload attempt will be done, which will either use
+ * a new template or fail with an appropriate message
+ * about how the file couldn't be found.
+ */
+ }
+ else if (currentFile.equals(file) && file.canRead())
+ {
+ /*
+ * if only if currentFile is the same as file and
+ * file.lastModified() is the same as
+ * resource.getLastModified(), then we should use the
+ * cached version.
+ */
+ modified = (file.lastModified() != resource.getLastModified());
+ }
+
+ /*
+ * rsvc.debug("isSourceModified for " + fileName + ": " + modified);
+ */
+ return modified;
+ }
+
+ /**
+ * @see ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public long getLastModified(Resource resource)
+ {
+ String path = templatePaths.get(resource.getName());
+ File file = getFile(path, resource.getName());
+
+ if (file.canRead())
+ {
+ return file.lastModified();
+ }
+ else
+ {
+ return 0;
+ }
+ }
+
+
+ /**
+ * Create a File based on either a relative path if given, or absolute path otherwise
+ */
+ private File getFile(String path, String template)
+ {
+
+ File file = null;
+
+ if("".equals(path))
+ {
+ file = new File( template );
+ }
+ else
+ {
+ /*
+ * if a / leads off, then just nip that :)
+ */
+ if (template.startsWith("/"))
+ {
+ template = template.substring(1);
+ }
+
+ file = new File ( path, template );
+ }
+
+ return file;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarHolder.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarHolder.java
new file mode 100644
index 00000000..3ccdc944
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarHolder.java
@@ -0,0 +1,169 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeServices;
+
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+/**
+ * A small wrapper around a Jar
+ *
+ * @author <a href="mailto:daveb@miceda-data.com">Dave Bryson</a>
+ * @version $Id$
+ */
+public class JarHolder
+{
+ private String urlpath = null;
+ private JarFile theJar = null;
+ private JarURLConnection conn = null;
+
+ private Logger log = null;
+
+ /**
+ * @param rs
+ * @param urlpath
+ * @param log
+ */
+ public JarHolder( RuntimeServices rs, String urlpath, Logger log )
+ {
+ this.log = log;
+
+ this.urlpath=urlpath;
+ init();
+
+ log.debug("JarHolder: initialized JAR: {}", urlpath);
+ }
+
+ /**
+ *
+ */
+ public void init()
+ {
+ try
+ {
+ log.debug("JarHolder: attempting to connect to {}", urlpath);
+
+ URL url = new URL( urlpath );
+ conn = (JarURLConnection) url.openConnection();
+ conn.setAllowUserInteraction(false);
+ conn.setDoInput(true);
+ conn.setDoOutput(false);
+ conn.connect();
+ theJar = conn.getJarFile();
+ }
+ catch (IOException ioe)
+ {
+ String msg = "JarHolder: error establishing connection to JAR at \""
+ + urlpath + "\"";
+ log.error(msg, ioe);
+ throw new VelocityException(msg, ioe);
+ }
+ }
+
+ /**
+ *
+ */
+ public void close()
+ {
+ try
+ {
+ theJar.close();
+ }
+ catch ( Exception e )
+ {
+ String msg = "JarHolder: error closing the JAR file";
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ theJar = null;
+ conn = null;
+
+ log.trace("JarHolder: JAR file closed");
+ }
+
+ /**
+ * @param theentry
+ * @return The requested resource.
+ * @throws ResourceNotFoundException
+ */
+ public InputStream getResource( String theentry )
+ throws ResourceNotFoundException {
+ InputStream data = null;
+
+ try
+ {
+ JarEntry entry = theJar.getJarEntry( theentry );
+
+ if ( entry != null )
+ {
+ data = theJar.getInputStream( entry );
+ }
+ }
+ catch(Exception fnfe)
+ {
+ log.error("JarHolder: getResource() error", fnfe);
+ throw new ResourceNotFoundException(fnfe);
+ }
+
+ return data;
+ }
+
+ /**
+ * @return The entries of the jar as a hashtable.
+ */
+ public Map<String, String> getEntries()
+ {
+ Map<String, String> allEntries = new HashMap<>(559);
+
+ Enumeration<JarEntry> all = theJar.entries();
+ while ( all.hasMoreElements() )
+ {
+ JarEntry je = all.nextElement();
+
+ // We don't map plain directory entries
+ if ( !je.isDirectory() )
+ {
+ allEntries.put( je.getName(), this.urlpath );
+ }
+ }
+ return allEntries;
+ }
+
+ /**
+ * @return The URL path of this jar holder.
+ */
+ public String getUrlPath()
+ {
+ return urlpath;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarResourceLoader.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarResourceLoader.java
new file mode 100644
index 00000000..23db3743
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/JarResourceLoader.java
@@ -0,0 +1,263 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.util.ExtProperties;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+/**
+ * <p>
+ * ResourceLoader to load templates from multiple Jar files.
+ * </p>
+ * <p>
+ * The configuration of the JarResourceLoader is straightforward -
+ * You simply add the JarResourceLoader to the configuration via
+ * </p>
+ * <pre><code>
+ * resource.loaders = jar
+ * resource.loader.jar.class = org.apache.velocity.runtime.resource.loader.JarResourceLoader
+ * resource.loader.jar.path = list of JAR &lt;URL&gt;s
+ * </code></pre>
+ *
+ * <p> So for example, if you had a jar file on your local filesystem, you could simply do</p>
+ * <pre><code>
+ * resource.loader.jar.path = jar:file:/opt/myfiles/jar1.jar
+ * </code></pre>
+ * <p> Note that jar specification for the <code>.path</code> configuration property
+ * conforms to the same rules for the java.net.JarUrlConnection class.
+ * </p>
+ *
+ * <p> For a working example, see the unit test case,
+ * org.apache.velocity.test.MultiLoaderTestCase class
+ * </p>
+ *
+ * @author <a href="mailto:mailmur@yahoo.com">Aki Nieminen</a>
+ * @author <a href="mailto:daveb@miceda-data.com">Dave Bryson</a>
+ * @version $Id$
+ */
+public class JarResourceLoader extends ResourceLoader
+{
+ /**
+ * Maps entries to the parent JAR File
+ * Key = the entry *excluding* plain directories
+ * Value = the JAR URL
+ */
+ private Map<String, String> entryDirectory = new HashMap<>(559);
+
+ /**
+ * Maps JAR URLs to the actual JAR
+ * Key = the JAR URL
+ * Value = the JAR
+ */
+ private Map<String, JarHolder> jarfiles = new HashMap<>(89);
+
+ /**
+ * Called by Velocity to initialize the loader
+ * @param configuration
+ */
+ @Override
+ public void init(ExtProperties configuration)
+ {
+ log.trace("JarResourceLoader: initialization starting.");
+
+ List<String> paths = configuration.getList(RuntimeConstants.RESOURCE_LOADER_PATHS);
+
+ if (paths != null)
+ {
+ log.debug("JarResourceLoader # of paths: {}", paths.size() );
+
+ for (ListIterator<String> it = paths.listIterator(); it.hasNext(); )
+ {
+ String jar = StringUtils.trim(it.next());
+ it.set(jar);
+ loadJar(jar);
+ }
+ }
+
+ log.trace("JarResourceLoader: initialization complete.");
+ }
+
+ private void loadJar( String path )
+ {
+ log.debug("JarResourceLoader: trying to load \"{}\"", path);
+
+ // Check path information
+ if ( path == null )
+ {
+ String msg = "JarResourceLoader: can not load JAR - JAR path is null";
+ log.error(msg);
+ throw new RuntimeException(msg);
+ }
+ if ( !path.startsWith("jar:") )
+ {
+ String msg = "JarResourceLoader: JAR path must start with jar: -> see java.net.JarURLConnection for information";
+ log.error(msg);
+ throw new RuntimeException(msg);
+ }
+ if (!path.contains("!/"))
+ {
+ path += "!/";
+ }
+
+ // Close the jar if it's already open
+ // this is useful for a reload
+ closeJar( path );
+
+ // Create a new JarHolder
+ JarHolder temp = new JarHolder( rsvc, path, log );
+ // Add it's entries to the entryCollection
+ addEntries(temp.getEntries());
+ // Add it to the Jar table
+ jarfiles.put(temp.getUrlPath(), temp);
+ }
+
+ /**
+ * Closes a Jar file and set its URLConnection
+ * to null.
+ */
+ private void closeJar( String path )
+ {
+ if ( jarfiles.containsKey(path) )
+ {
+ JarHolder theJar = jarfiles.get(path);
+ theJar.close();
+ }
+ }
+
+ /**
+ * Copy all the entries into the entryDirectory
+ * It will overwrite any duplicate keys.
+ */
+ private void addEntries( Map<String, String> entries )
+ {
+ entryDirectory.putAll( entries );
+ }
+
+ /**
+ * Get a Reader so that the Runtime can build a
+ * template with it.
+ *
+ * @param source name of template to get
+ * @param encoding asked encoding
+ * @return InputStream containing the template
+ * @throws ResourceNotFoundException if template not found
+ * in the file template path.
+ * @since 2.0
+ */
+ @Override
+ public Reader getResourceReader(String source, String encoding )
+ throws ResourceNotFoundException
+ {
+ Reader result = null;
+
+ if (org.apache.commons.lang3.StringUtils.isEmpty(source))
+ {
+ throw new ResourceNotFoundException("Need to have a resource!");
+ }
+
+ String normalizedPath = FilenameUtils.normalize( source, true );
+
+ if ( normalizedPath == null || normalizedPath.length() == 0 )
+ {
+ String msg = "JAR resource error: argument " + normalizedPath +
+ " contains .. and may be trying to access " +
+ "content outside of template root. Rejected.";
+
+ log.error( "JarResourceLoader: {}", msg );
+
+ throw new ResourceNotFoundException ( msg );
+ }
+
+ /*
+ * if a / leads off, then just nip that :)
+ */
+ if ( normalizedPath.startsWith("/") )
+ {
+ normalizedPath = normalizedPath.substring(1);
+ }
+
+ if ( entryDirectory.containsKey( normalizedPath ) )
+ {
+ String jarurl = entryDirectory.get( normalizedPath );
+
+ if ( jarfiles.containsKey( jarurl ) )
+ {
+ JarHolder holder = (JarHolder)jarfiles.get( jarurl );
+ InputStream rawStream = holder.getResource( normalizedPath );
+ try
+ {
+ return buildReader(rawStream, encoding);
+ }
+ catch (Exception e)
+ {
+ if (rawStream != null)
+ {
+ try
+ {
+ rawStream.close();
+ }
+ catch (IOException ioe) {}
+ }
+ String msg = "JAR resource error: Exception while loading " + source;
+ log.error(msg, e);
+ throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
+ }
+ }
+ }
+
+ throw new ResourceNotFoundException( "JarResourceLoader Error: cannot find resource " +
+ source );
+
+ }
+
+ // TODO: SHOULD BE DELEGATED TO THE JARHOLDER
+
+ /**
+ * @see ResourceLoader#isSourceModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public boolean isSourceModified(Resource resource)
+ {
+ return true;
+ }
+
+ /**
+ * @see ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public long getLastModified(Resource resource)
+ {
+ return 0;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoader.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoader.java
new file mode 100644
index 00000000..9a2d5643
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoader.java
@@ -0,0 +1,324 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.io.UnicodeInputStream;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.runtime.resource.ResourceCacheImpl;
+import org.apache.velocity.util.ExtProperties;
+
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * This is abstract class the all text resource loaders should
+ * extend.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a>
+ * @version $Id$
+ */
+public abstract class ResourceLoader
+{
+ /**
+ * Does this loader want templates produced with it
+ * cached in the Runtime.
+ */
+ protected boolean isCachingOn = false;
+
+ /**
+ * This property will be passed on to the templates
+ * that are created with this loader.
+ */
+ protected long modificationCheckInterval = 2;
+
+ /**
+ * Class name for this loader, for logging/debuggin
+ * purposes.
+ */
+ protected String className = null;
+
+ protected RuntimeServices rsvc = null;
+ protected Logger log = null;
+
+ /**
+ * This initialization is used by all resource
+ * loaders and must be called to set up common
+ * properties shared by all resource loaders
+ *
+ * @param rs
+ * @param configuration
+ */
+ public void commonInit(RuntimeServices rs, ExtProperties configuration)
+ {
+ this.rsvc = rs;
+ String loaderName = configuration.getString(RuntimeConstants.RESOURCE_LOADER_IDENTIFIER);
+ log = rsvc.getLog("loader." + (loaderName == null ? this.getClass().getSimpleName() : loaderName));
+
+ /*
+ * these two properties are not required for all loaders.
+ * For example, for ClasspathLoader, what would cache mean?
+ * so adding default values which I think are the safest
+ *
+ * don't cache, and modCheckInterval irrelevant...
+ */
+
+ try
+ {
+ isCachingOn = configuration.getBoolean(RuntimeConstants.RESOURCE_LOADER_CACHE, false);
+ }
+ catch (Exception e)
+ {
+ isCachingOn = false;
+ String msg = "Exception parsing cache setting: " + configuration.getString(RuntimeConstants.RESOURCE_LOADER_CACHE);
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ try
+ {
+ modificationCheckInterval = configuration.getLong(RuntimeConstants.RESOURCE_LOADER_CHECK_INTERVAL, 0);
+ }
+ catch (Exception e)
+ {
+ modificationCheckInterval = 0;
+ String msg = "Exception parsing modificationCheckInterval setting: " + RuntimeConstants.RESOURCE_LOADER_CHECK_INTERVAL;
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+
+ /*
+ * this is a must!
+ */
+ className = ResourceCacheImpl.class.getName();
+ try
+ {
+ className = configuration.getString(RuntimeConstants.RESOURCE_LOADER_CLASS, className);
+ }
+ catch (Exception e)
+ {
+ String msg = "Exception retrieving resource cache class name";
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ }
+
+ /**
+ * Initialize the template loader with a
+ * a resources class.
+ *
+ * @param configuration
+ */
+ public abstract void init(ExtProperties configuration);
+
+ /**
+ * Get the Reader that the Runtime will parse
+ * to create a template.
+ *
+ * @param source
+ * @param encoding
+ * @return The reader for the requested resource.
+ * @throws ResourceNotFoundException
+ * @since 2.0
+ */
+ public abstract Reader getResourceReader(String source, String encoding)
+ throws ResourceNotFoundException;
+
+ /**
+ * Given a template, check to see if the source of InputStream
+ * has been modified.
+ *
+ * @param resource
+ * @return True if the resource has been modified.
+ */
+ public abstract boolean isSourceModified(Resource resource);
+
+ /**
+ * Get the last modified time of the InputStream source
+ * that was used to create the template. We need the template
+ * here because we have to extract the name of the template
+ * in order to locate the InputStream source.
+ *
+ * @param resource
+ * @return Time in millis when the resource has been modified.
+ */
+ public abstract long getLastModified(Resource resource);
+
+ /**
+ * Return the class name of this resource Loader
+ *
+ * @return Class name of the resource loader.
+ */
+ public String getClassName()
+ {
+ return className;
+ }
+
+ /**
+ * Set the caching state. If true, then this loader
+ * would like the Runtime to cache templates that
+ * have been created with InputStreams provided
+ * by this loader.
+ *
+ * @param value
+ */
+ public void setCachingOn(boolean value)
+ {
+ isCachingOn = value;
+ }
+
+ /**
+ * The Runtime uses this to find out whether this
+ * template loader wants the Runtime to cache
+ * templates created with InputStreams provided
+ * by this loader.
+ *
+ * @return True if this resource loader caches.
+ */
+ public boolean isCachingOn()
+ {
+ return isCachingOn;
+ }
+
+ /**
+ * Set the interval at which the InputStream source
+ * should be checked for modifications.
+ *
+ * @param modificationCheckInterval
+ */
+ public void setModificationCheckInterval(long modificationCheckInterval)
+ {
+ this.modificationCheckInterval = modificationCheckInterval;
+ }
+
+ /**
+ * Get the interval at which the InputStream source
+ * should be checked for modifications.
+ *
+ * @return The modification check interval.
+ */
+ public long getModificationCheckInterval()
+ {
+ return modificationCheckInterval;
+ }
+
+ /**
+ * Check whether any given resource exists. This is not really
+ * a very efficient test and it can and should be overridden in the
+ * subclasses extending ResourceLoader2.
+ *
+ * @param resourceName The name of a resource.
+ * @return true if a resource exists and can be accessed.
+ * @since 1.6
+ */
+ public boolean resourceExists(final String resourceName)
+ {
+ Reader reader = null;
+ try
+ {
+ reader = getResourceReader(resourceName, null);
+ }
+ catch (ResourceNotFoundException e)
+ {
+ log.debug("Could not load resource '{}' from ResourceLoader {}",
+ resourceName, this.getClass().getName());
+ }
+ finally
+ {
+ try
+ {
+ if (reader != null)
+ {
+ reader.close();
+ }
+ }
+ catch (Exception e)
+ {
+ String msg = "While closing InputStream for resource '" +
+ resourceName + "' from ResourceLoader " +
+ this.getClass().getName();
+ log.error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ }
+ return (reader != null);
+ }
+
+ /**
+ * Builds a Reader given a raw InputStream and an encoding. Should be use
+ * by every subclass that whishes to accept optional BOMs in resources.
+ * This method does *not* close the given input stream whenever an exception is thrown.
+ *
+ * @param rawStream The raw input stream.
+ * @param encoding The asked encoding.
+ * @return found reader
+ * @throws IOException
+ * @throws UnsupportedEncodingException
+ * @since 2.0
+ */
+ protected Reader buildReader(InputStream rawStream, String encoding)
+ throws IOException
+ {
+ UnicodeInputStream inputStream = new UnicodeInputStream(rawStream);
+ /*
+ * Check encoding
+ */
+ String foundEncoding = inputStream.getEncodingFromStream();
+ if (foundEncoding != null && encoding != null && !UnicodeInputStream.sameEncoding(foundEncoding, encoding))
+ {
+ log.warn("Found BOM encoding '{}' differs from asked encoding: '{}' - using BOM encoding to read resource.", foundEncoding, encoding);
+ encoding = foundEncoding;
+ }
+ if (encoding == null)
+ {
+ if (foundEncoding == null)
+ {
+ encoding = rsvc.getString(RuntimeConstants.INPUT_ENCODING);
+ } else
+ {
+ encoding = foundEncoding;
+ }
+ }
+
+ try
+ {
+ return new InputStreamReader(inputStream, encoding);
+ }
+ catch (UnsupportedEncodingException uee)
+ {
+ try
+ {
+ inputStream.close();
+ }
+ catch (IOException ioe) {}
+ throw uee;
+ }
+ }
+
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoaderFactory.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoaderFactory.java
new file mode 100644
index 00000000..09f1c73e
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/ResourceLoaderFactory.java
@@ -0,0 +1,62 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.util.ClassUtils;
+
+/**
+ * Factory to grab a template loader.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @version $Id$
+ */
+public class ResourceLoaderFactory
+{
+ /**
+ * Gets the loader specified in the configuration file.
+ * @param rs
+ * @param loaderClassName
+ * @return TemplateLoader
+ */
+ public static ResourceLoader getLoader(RuntimeServices rs, String loaderClassName)
+ {
+ ResourceLoader loader = null;
+
+ try
+ {
+ loader = (ResourceLoader) ClassUtils.getNewInstance( loaderClassName );
+
+ rs.getLog().debug("ResourceLoader instantiated: {}", loader.getClass().getName());
+
+ return loader;
+ }
+ // The ugly three strike again: ClassNotFoundException,IllegalAccessException,InstantiationException
+ catch(Exception e)
+ {
+ String msg = "Problem instantiating the template loader: "+loaderClassName+"." + System.lineSeparator() +
+ "Look at your properties file and make sure the" + System.lineSeparator() +
+ "name of the template loader is correct.";
+ rs.getLog().error(msg, e);
+ throw new VelocityException(msg, e);
+ }
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/StringResourceLoader.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/StringResourceLoader.java
new file mode 100644
index 00000000..fd5d8c4a
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/StringResourceLoader.java
@@ -0,0 +1,438 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.runtime.resource.util.StringResource;
+import org.apache.velocity.runtime.resource.util.StringResourceRepository;
+import org.apache.velocity.runtime.resource.util.StringResourceRepositoryImpl;
+import org.apache.velocity.util.ClassUtils;
+import org.apache.velocity.util.ExtProperties;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Resource loader that works with Strings. Users should manually add
+ * resources to the repository that is used by the resource loader instance.
+ *
+ * Below is an example configuration for this loader.
+ * Note that 'repository.class' is not necessary;
+ * if not provided, the factory will fall back on using
+ * {@link StringResourceRepositoryImpl} as the default.
+ * <pre>
+ * resource.loaders = string
+ * resource.loader.string.description = Velocity StringResource loader
+ * resource.loader.string.class = org.apache.velocity.runtime.resource.loader.StringResourceLoader
+ * resource.loader.string.repository.name = MyRepositoryName (optional, to avoid using the default repository)
+ * resource.loader.string.repository.class = org.apache.velocity.runtime.resource.loader.StringResourceRepositoryImpl
+ * </pre>
+ * Resources can be added to the repository like this:
+ * <pre><code>
+ * StringResourceRepository repo = StringResourceLoader.getRepository();
+ *
+ * String myTemplateName = "/some/imaginary/path/hello.vm";
+ * String myTemplate = "Hi, ${username}... this is some template!";
+ * repo.putStringResource(myTemplateName, myTemplate);
+ * </code></pre>
+ *
+ * After this, the templates can be retrieved as usual.
+ * <br>
+ * <p>If there will be multiple StringResourceLoaders used in an application,
+ * you should consider specifying a 'resource.loader.string.repository.name = foo'
+ * property in order to keep you string resources in a non-default repository.
+ * This can help to avoid conflicts between different frameworks or components
+ * that are using StringResourceLoader.
+ * You can then retrieve your named repository like this:
+ * <pre><code>
+ * StringResourceRepository repo = StringResourceLoader.getRepository("foo");
+ * </code></pre>
+ * <p>and add string resources to the repo just as in the previous example.
+ * </p>
+ * <p>If you have concerns about memory leaks or for whatever reason do not wish
+ * to have your string repository stored statically as a class member, then you
+ * should set 'resource.loader.string.repository.static = false' in your properties.
+ * This will tell the resource loader that the string repository should be stored
+ * in the Velocity application attributes. To retrieve the repository, do:</p>
+ * <pre><code>
+ * StringResourceRepository repo = velocityEngine.getApplicationAttribute("foo");
+ * </code></pre>
+ * <p>If you did not specify a name for the repository, then it will be stored under the
+ * class name of the repository implementation class (for which the default is
+ * 'org.apache.velocity.runtime.resource.util.StringResourceRepositoryImpl').
+ * Incidentally, this is also true for the default statically stored repository.
+ * </p>
+ * <p>Whether your repository is stored statically or in Velocity's application
+ * attributes, you can also manually create and set it prior to Velocity
+ * initialization. For a static repository, you can do something like this:
+ * <pre><code>
+ * StringResourceRepository repo = new MyStringResourceRepository();
+ * repo.magicallyAddSomeStringResources();
+ * StringResourceLoader.setRepository("foo", repo);
+ * </code></pre>
+ * <p>Or for a non-static repository:</p>
+ * <pre><code>
+ * StringResourceRepository repo = new MyStringResourceRepository();
+ * repo.magicallyAddSomeStringResources();
+ * velocityEngine.setApplicationAttribute("foo", repo);
+ * </code></pre>
+ * <p>Then, assuming the 'resource.loader.string.repository.name' property is
+ * set to 'some.name', the StringResourceLoader will use that already created
+ * repository, rather than creating a new one.
+ * </p>
+ *
+ * @author <a href="mailto:eelco.hillenius@openedge.nl">Eelco Hillenius</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @author Nathan Bubna
+ * @version $Id$
+ * @since 1.5
+ */
+public class StringResourceLoader extends ResourceLoader
+{
+ /**
+ * Key to determine whether the repository should be set as the static one or not.
+ * @since 1.6
+ */
+ public static final String REPOSITORY_STATIC = "repository.static";
+
+ /**
+ * By default, repositories are stored statically (shared across the VM).
+ * @since 1.6
+ */
+ public static final boolean REPOSITORY_STATIC_DEFAULT = true;
+
+ /** Key to look up the repository implementation class. */
+ public static final String REPOSITORY_CLASS = "repository.class";
+
+ /** The default implementation class. */
+ public static final String REPOSITORY_CLASS_DEFAULT =
+ StringResourceRepositoryImpl.class.getName();
+
+ /**
+ * Key to look up the name for the repository to be used.
+ * @since 1.6
+ */
+ public static final String REPOSITORY_NAME = "repository.name";
+
+ /** The default name for string resource repositories
+ * ('org.apache.velocity.runtime.resource.util.StringResourceRepository').
+ * @since 1.6
+ */
+ public static final String REPOSITORY_NAME_DEFAULT =
+ StringResourceRepository.class.getName();
+
+ /** Key to look up the repository char encoding. */
+ public static final String REPOSITORY_ENCODING = "repository.encoding";
+
+ protected static final Map<String, StringResourceRepository> STATIC_REPOSITORIES =
+ Collections.synchronizedMap(new HashMap<>());
+
+ /**
+ * Returns a reference to the default static repository.
+ * @return default static repository
+ */
+ public static StringResourceRepository getRepository()
+ {
+ return getRepository(REPOSITORY_NAME_DEFAULT);
+ }
+
+ /**
+ * Returns a reference to the repository stored statically under the
+ * specified name.
+ * @param name
+ * @return named repository
+ * @since 1.6
+ */
+ public static StringResourceRepository getRepository(String name)
+ {
+ return (StringResourceRepository)STATIC_REPOSITORIES.get(name);
+ }
+
+ /**
+ * Sets the specified {@link StringResourceRepository} in static storage
+ * under the specified name.
+ * @param name
+ * @param repo
+ * @since 1.6
+ */
+ public static void setRepository(String name, StringResourceRepository repo)
+ {
+ STATIC_REPOSITORIES.put(name, repo);
+ }
+
+ /**
+ * Removes the {@link StringResourceRepository} stored under the specified
+ * name.
+ * @param name
+ * @return removed repository
+ * @since 1.6
+ */
+ public static StringResourceRepository removeRepository(String name)
+ {
+ return (StringResourceRepository)STATIC_REPOSITORIES.remove(name);
+ }
+
+ /**
+ * Removes all statically stored {@link StringResourceRepository}s.
+ * @since 1.6
+ */
+ public static void clearRepositories()
+ {
+ STATIC_REPOSITORIES.clear();
+ }
+
+
+ /**
+ * the repository used internally by this resource loader
+ */
+ protected StringResourceRepository repository;
+
+
+ /**
+ * @param configuration
+ * @see ResourceLoader#init(org.apache.velocity.util.ExtProperties)
+ */
+ @Override
+ public void init(final ExtProperties configuration)
+ {
+ log.trace("StringResourceLoader: initialization starting.");
+
+ // get the repository configuration info
+ String repoClass = configuration.getString(REPOSITORY_CLASS, REPOSITORY_CLASS_DEFAULT);
+ String repoName = configuration.getString(REPOSITORY_NAME, REPOSITORY_NAME_DEFAULT);
+ boolean isStatic = configuration.getBoolean(REPOSITORY_STATIC, REPOSITORY_STATIC_DEFAULT);
+ String encoding = configuration.getString(REPOSITORY_ENCODING);
+
+ // look for an existing repository of that name and isStatic setting
+ if (isStatic)
+ {
+ this.repository = getRepository(repoName);
+ if (repository != null)
+ {
+ log.debug("Loaded repository '{}' from static repo store", repoName);
+ }
+ }
+ else
+ {
+ this.repository = (StringResourceRepository)rsvc.getApplicationAttribute(repoName);
+ if (repository != null)
+ {
+ log.debug("Loaded repository '{}' from application attributes", repoName);
+ }
+ }
+
+ if (this.repository == null)
+ {
+ // since there's no repository under the repo name, create a new one
+ this.repository = createRepository(repoClass, encoding);
+
+ // and store it according to the isStatic setting
+ if (isStatic)
+ {
+ setRepository(repoName, this.repository);
+ }
+ else
+ {
+ rsvc.setApplicationAttribute(repoName, this.repository);
+ }
+ }
+ else
+ {
+ // ok, we already have a repo
+ // warn them if they are trying to change the class of the repository
+ if (!this.repository.getClass().getName().equals(repoClass))
+ {
+ log.debug("Cannot change class of string repository '{}' from {} to {}." +
+ " The change will be ignored.",
+ repoName, this.repository.getClass().getName(), repoClass);
+ }
+
+ // allow them to change the default encoding of the repo
+ if (encoding != null &&
+ !this.repository.getEncoding().equals(encoding))
+ {
+ log.debug("Changing the default encoding of string repository '{}' from {} to {}",
+ repoName, this.repository.getEncoding(), encoding);
+ this.repository.setEncoding(encoding);
+ }
+ }
+
+ log.trace("StringResourceLoader: initialization complete.");
+ }
+
+ /**
+ * @param className
+ * @param encoding
+ * @return created repository
+ * @since 1.6
+ */
+ public StringResourceRepository createRepository(final String className,
+ final String encoding)
+ {
+ log.debug("Creating string repository using class {}...", className);
+
+ StringResourceRepository repo;
+ try
+ {
+ repo = (StringResourceRepository) ClassUtils.getNewInstance(className);
+ }
+ catch (ClassNotFoundException cnfe)
+ {
+ throw new VelocityException("Could not find '" + className + "'", cnfe);
+ }
+ catch (IllegalAccessException iae)
+ {
+ throw new VelocityException("Could not access '" + className + "'", iae);
+ }
+ catch (InstantiationException ie)
+ {
+ throw new VelocityException("Could not instantiate '" + className + "'", ie);
+ }
+
+ if (encoding != null)
+ {
+ repo.setEncoding(encoding);
+ }
+ else
+ {
+ repo.setEncoding(RuntimeConstants.ENCODING_DEFAULT);
+ }
+
+ log.debug("Default repository encoding is {}", repo.getEncoding());
+ return repo;
+ }
+
+ /**
+ * Overrides superclass for better performance.
+ * @param name resource name
+ * @return whether resource exists
+ * @since 1.6
+ */
+ @Override
+ public boolean resourceExists(final String name)
+ {
+ if (name == null)
+ {
+ return false;
+ }
+ return (this.repository.getStringResource(name) != null);
+ }
+
+ /**
+ * Get a reader so that the Runtime can build a
+ * template with it.
+ *
+ * @param name name of template to get.
+ * @param encoding asked encoding
+ * @return Reader containing the template.
+ * @throws ResourceNotFoundException Ff template not found
+ * in the RepositoryFactory.
+ * @since 2.0
+ */
+ @Override
+ public Reader getResourceReader(String name, String encoding)
+ throws ResourceNotFoundException
+ {
+ if (StringUtils.isEmpty(name))
+ {
+ throw new ResourceNotFoundException("No template name provided");
+ }
+
+ StringResource resource = this.repository.getStringResource(name);
+
+ if(resource == null)
+ {
+ throw new ResourceNotFoundException("Could not locate resource '" + name + "'");
+ }
+
+ byte [] byteArray = null;
+ InputStream rawStream = null;
+
+ try
+ {
+ byteArray = resource.getBody().getBytes(resource.getEncoding());
+ rawStream = new ByteArrayInputStream(byteArray);
+ return new InputStreamReader(rawStream, resource.getEncoding());
+ }
+ catch(UnsupportedEncodingException ue)
+ {
+ if (rawStream != null)
+ {
+ try
+ {
+ rawStream.close();
+ }
+ catch (IOException ioe) {}
+ }
+ throw new VelocityException("Could not convert String using encoding " + resource.getEncoding(), ue, rsvc.getLogContext().getStackTrace());
+ }
+ }
+
+ /**
+ * @param resource
+ * @return whether resource was modified
+ * @see ResourceLoader#isSourceModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public boolean isSourceModified(final Resource resource)
+ {
+ StringResource original = null;
+ boolean result = true;
+
+ original = this.repository.getStringResource(resource.getName());
+
+ if (original != null)
+ {
+ result = original.getLastModified() != resource.getLastModified();
+ }
+
+ return result;
+ }
+
+ /**
+ * @param resource
+ * @return last modified timestamp
+ * @see ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource)
+ */
+ @Override
+ public long getLastModified(final Resource resource)
+ {
+ StringResource original = null;
+
+ original = this.repository.getStringResource(resource.getName());
+
+ return (original != null)
+ ? original.getLastModified()
+ : 0;
+ }
+
+}
+
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/URLResourceLoader.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/URLResourceLoader.java
new file mode 100644
index 00000000..81fe33b9
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/URLResourceLoader.java
@@ -0,0 +1,214 @@
+package org.apache.velocity.runtime.resource.loader;
+
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.util.ExtProperties;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This is a simple URL-based loader.
+ *
+ * @author <a href="mailto:geirm@apache.org">Geir Magnusson Jr.</a>
+ * @author <a href="mailto:nbubna@apache.org">Nathan Bubna</a>
+ * @version $Id: URLResourceLoader.java 191743 2005-06-21 23:22:20Z dlr $
+ * @since 1.5
+ */
+public class URLResourceLoader extends ResourceLoader
+{
+ private String[] roots = null;
+ protected Map<String, String> templateRoots = null;
+ private int timeout = -1;
+
+ /**
+ * @param configuration
+ * @see ResourceLoader#init(org.apache.velocity.util.ExtProperties)
+ */
+ @Override
+ public void init(ExtProperties configuration)
+ {
+ log.trace("URLResourceLoader: initialization starting.");
+
+ roots = configuration.getStringArray("root");
+ if (log.isDebugEnabled())
+ {
+ for (String root : roots)
+ {
+ log.debug("URLResourceLoader: adding root '{}'", root);
+ }
+ }
+
+ timeout = configuration.getInt("timeout", -1);
+
+ // init the template paths map
+ templateRoots = new HashMap<>();
+
+ log.trace("URLResourceLoader: initialization complete.");
+ }
+
+ /**
+ * Get a Reader so that the Runtime can build a
+ * template with it.
+ *
+ * @param name name of template to fetch bytestream of
+ * @param encoding asked encoding
+ * @return InputStream containing the template
+ * @throws ResourceNotFoundException if template not found
+ * in the file template path.
+ * @since 2.0
+ */
+ @Override
+ public synchronized Reader getResourceReader(String name, String encoding)
+ throws ResourceNotFoundException
+ {
+ if (StringUtils.isEmpty(name))
+ {
+ throw new ResourceNotFoundException("URLResourceLoader: No template name provided");
+ }
+
+ Reader reader = null;
+ Exception exception = null;
+ for (String root : roots)
+ {
+ InputStream rawStream = null;
+ try
+ {
+ URL u = new URL(root + name);
+ URLConnection conn = u.openConnection();
+ conn.setConnectTimeout(timeout);
+ conn.setReadTimeout(timeout);
+ rawStream = conn.getInputStream();
+ reader = buildReader(rawStream, encoding);
+
+ if (reader != null)
+ {
+ log.debug("URLResourceLoader: Found '{}' at '{}'", name, root);
+
+ // save this root for later re-use
+ templateRoots.put(name, root);
+ break;
+ }
+ }
+ catch (IOException ioe)
+ {
+ if (rawStream != null)
+ {
+ try
+ {
+ rawStream.close();
+ }
+ catch (IOException e)
+ {
+ }
+ }
+ log.debug("URLResourceLoader: Exception when looking for '{}' at '{}'", name, root, ioe);
+
+ // only save the first one for later throwing
+ if (exception == null)
+ {
+ exception = ioe;
+ }
+ }
+ }
+
+ // if we never found the template
+ if (reader == null)
+ {
+ String msg;
+ if (exception == null)
+ {
+ msg = "URLResourceLoader: Resource '" + name + "' not found.";
+ }
+ else
+ {
+ msg = exception.getMessage();
+ }
+ // convert to a general Velocity ResourceNotFoundException
+ throw new ResourceNotFoundException(msg);
+ }
+
+ return reader;
+ }
+
+ /**
+ * Checks to see if a resource has been deleted, moved or modified.
+ *
+ * @param resource Resource The resource to check for modification
+ * @return boolean True if the resource has been modified, moved, or unreachable
+ */
+ @Override
+ public boolean isSourceModified(Resource resource)
+ {
+ long fileLastModified = getLastModified(resource);
+ // if the file is unreachable or otherwise changed
+ return fileLastModified == 0 ||
+ fileLastModified != resource.getLastModified();
+ }
+
+ /**
+ * Checks to see when a resource was last modified
+ *
+ * @param resource Resource the resource to check
+ * @return long The time when the resource was last modified or 0 if the file can't be reached
+ */
+ @Override
+ public long getLastModified(Resource resource)
+ {
+ // get the previously used root
+ String name = resource.getName();
+ String root = templateRoots.get(name);
+
+ try
+ {
+ // get a connection to the URL
+ URL u = new URL(root + name);
+ URLConnection conn = u.openConnection();
+ conn.setConnectTimeout(timeout);
+ conn.setReadTimeout(timeout);
+ return conn.getLastModified();
+ }
+ catch (IOException ioe)
+ {
+ // the file is not reachable at its previous address
+ String msg = "URLResourceLoader: '"+name+"' is no longer reachable at '"+root+"'";
+ log.error(msg, ioe);
+ throw new ResourceNotFoundException(msg, ioe, rsvc.getLogContext().getStackTrace());
+ }
+ }
+
+ /**
+ * Returns the current, custom timeout setting. If negative, there is no custom timeout.
+ * @return timeout
+ * @since 1.6
+ */
+ public int getTimeout()
+ {
+ return timeout;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResource.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResource.java
new file mode 100644
index 00000000..e0887eb6
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResource.java
@@ -0,0 +1,108 @@
+package org.apache.velocity.runtime.resource.util;
+
+/*
+ * 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.
+ */
+
+/**
+ * Wrapper for Strings containing templates, allowing to add additional meta
+ * data like timestamps.
+ *
+ * @author <a href="mailto:eelco.hillenius@openedge.nl">Eelco Hillenius</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ * @since 1.5
+ */
+public final class StringResource
+{
+ /** template body */
+ private String body;
+
+ /** encoding */
+ private String encoding;
+
+ /** last modified ts */
+ private long lastModified;
+
+ /**
+ * convenience constructor; sets body to 'body' and sets lastModified to now
+ * @param body
+ * @param encoding
+ */
+ public StringResource(final String body, final String encoding)
+ {
+ setBody(body);
+ setEncoding(encoding);
+ }
+
+ /**
+ * Sets the template body.
+ * @return String containing the template body.
+ */
+ public String getBody()
+ {
+ return body;
+ }
+
+ /**
+ * Returns the modification date of the template.
+ * @return Modification date in milliseconds.
+ */
+ public long getLastModified()
+ {
+ return lastModified;
+ }
+
+ /**
+ * Sets a new value for the template body.
+ * @param body New body value
+ */
+ public void setBody(final String body)
+ {
+ this.body = body;
+ this.lastModified = System.currentTimeMillis();
+ }
+
+ /**
+ * Changes the last modified parameter.
+ * @param lastModified The modification time in millis.
+ */
+ public void setLastModified(final long lastModified)
+ {
+ this.lastModified = lastModified;
+ }
+
+ /**
+ * Returns the encoding of this String resource.
+ *
+ * @return The encoding of this String resource.
+ */
+ public String getEncoding() {
+ return this.encoding;
+ }
+
+ /**
+ * Sets the encoding of this string resource.
+ *
+ * @param encoding The new encoding of this resource.
+ */
+ public void setEncoding(final String encoding)
+ {
+ this.encoding = encoding;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepository.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepository.java
new file mode 100644
index 00000000..961e72a1
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepository.java
@@ -0,0 +1,76 @@
+package org.apache.velocity.runtime.resource.util;
+
+/*
+ * 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.
+ */
+
+/**
+ * A StringResourceRepository functions as a central repository for Velocity templates
+ * stored in Strings.
+ *
+ * @author <a href="mailto:eelco.hillenius@openedge.nl">Eelco Hillenius</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ * @since 1.5
+ */
+public interface StringResourceRepository
+{
+ /**
+ * get the string resource that is stored with given key
+ * @param name String name to retrieve from the repository.
+ * @return A StringResource containing the template.
+ */
+ StringResource getStringResource(String name);
+
+ /**
+ * add a string resource with given key.
+ * @param name The String name to store the template under.
+ * @param body A String containing a template.
+ */
+ void putStringResource(String name, String body);
+
+ /**
+ * add a string resource with given key.
+ * @param name The String name to store the template under.
+ * @param body A String containing a template.
+ * @param encoding The encoding of this string template
+ * @since 1.6
+ */
+ void putStringResource(String name, String body, String encoding);
+
+ /**
+ * delete a string resource with given key.
+ * @param name The string name to remove from the repository.
+ */
+ void removeStringResource(String name);
+
+ /**
+ * Sets the default encoding of the repository. Encodings can also be stored per
+ * template string. The default implementation does this correctly.
+ *
+ * @param encoding The encoding to use.
+ */
+ void setEncoding(String encoding);
+
+ /**
+ * Returns the current encoding of this repository.
+ *
+ * @return The current encoding of this repository.
+ */
+ String getEncoding();
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepositoryImpl.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepositoryImpl.java
new file mode 100644
index 00000000..fc93602d
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/util/StringResourceRepositoryImpl.java
@@ -0,0 +1,104 @@
+package org.apache.velocity.runtime.resource.util;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.resource.Resource;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Default implementation of StringResourceRepository.
+ * Uses a HashMap for storage
+ *
+ * @author <a href="mailto:eelco.hillenius@openedge.nl">Eelco Hillenius</a>
+ * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
+ * @version $Id$
+ * @since 1.5
+ */
+public class StringResourceRepositoryImpl implements StringResourceRepository
+{
+ /**
+ * mem store
+ */
+ protected Map<String, StringResource> resources = Collections.synchronizedMap(new HashMap<>());
+
+ /**
+ * Current Repository encoding.
+ */
+ private String encoding = RuntimeConstants.ENCODING_DEFAULT;
+
+ /**
+ * @see StringResourceRepository#getStringResource(java.lang.String)
+ */
+ @Override
+ public StringResource getStringResource(final String name)
+ {
+ return resources.get(name);
+ }
+
+ /**
+ * @see StringResourceRepository#putStringResource(java.lang.String, java.lang.String)
+ */
+ @Override
+ public void putStringResource(final String name, final String body)
+ {
+ resources.put(name, new StringResource(body, getEncoding()));
+ }
+
+ /**
+ * @see StringResourceRepository#putStringResource(java.lang.String, java.lang.String, java.lang.String)
+ * @since 1.6
+ */
+ @Override
+ public void putStringResource(final String name, final String body, final String encoding)
+ {
+ resources.put(name, new StringResource(body, encoding));
+ }
+
+ /**
+ * @see StringResourceRepository#removeStringResource(java.lang.String)
+ */
+ @Override
+ public void removeStringResource(final String name)
+ {
+ resources.remove(name);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.resource.util.StringResourceRepository#getEncoding()
+ */
+ @Override
+ public String getEncoding()
+ {
+ return encoding;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.resource.util.StringResourceRepository#setEncoding(java.lang.String)
+ */
+ @Override
+ public void setEncoding(final String encoding)
+ {
+ this.encoding = encoding;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/visitor/BaseVisitor.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/visitor/BaseVisitor.java
new file mode 100644
index 00000000..397a3cd6
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/visitor/BaseVisitor.java
@@ -0,0 +1,494 @@
+package org.apache.velocity.runtime.visitor;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.runtime.parser.node.*;
+
+import java.io.Writer;
+
+/**
+ * This is the base class for all visitors.
+ * For each AST node, this class will provide
+ * a bare-bones method for traversal.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ * @version $Id$
+ */
+public abstract class BaseVisitor implements ParserVisitor
+{
+ /** Context used during traversal */
+ protected InternalContextAdapter context;
+
+ /** Writer used as the output sink */
+ protected Writer writer;
+
+ /**
+ * @param writer
+ */
+ public void setWriter( Writer writer )
+ {
+ this.writer = writer;
+ }
+
+ /**
+ * @param context
+ */
+ public void setContext( InternalContextAdapter context)
+ {
+ this.context = context;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.SimpleNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(SimpleNode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTprocess, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTprocess node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTExpression, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTExpression node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTAssignment, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTAssignment node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTOrNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTOrNode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTAndNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTAndNode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTEQNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTEQNode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTNENode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTNENode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTLTNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTLTNode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTGTNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTGTNode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTLENode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTLENode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTGENode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTGENode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTAddNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTAddNode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTSubtractNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTSubtractNode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTMulNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTMulNode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTDivNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTDivNode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTModNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTModNode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTNotNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTNotNode node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTIntegerLiteral, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTIntegerLiteral node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTFloatingPointLiteral, java.lang.Object)
+ * @since 1.5
+ */
+ @Override
+ public Object visit(ASTFloatingPointLiteral node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTStringLiteral, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTStringLiteral node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTIdentifier, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTIdentifier node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTMethod, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTMethod node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTIndex, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTIndex node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTReference, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTReference node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTTrue, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTTrue node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTFalse, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTFalse node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTBlock, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTBlock node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTText, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTText node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTIfStatement, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTIfStatement node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTElseStatement, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTElseStatement node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTElseIfStatement, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTElseIfStatement node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTComment, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTComment node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTTextblock, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTTextblock node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTObjectArray, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTObjectArray node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTWord, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTWord node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTSetDirective, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTSetDirective node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTDirectiveAssign, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTDirectiveAssign node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTDirective, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTDirective node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTEscapedDirective, java.lang.Object)
+ * @since 1.5
+ */
+ @Override
+ public Object visit(ASTEscapedDirective node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTEscape, java.lang.Object)
+ * @since 1.5
+ */
+ @Override
+ public Object visit(ASTEscape node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTMap, java.lang.Object)
+ * @since 1.5
+ */
+ @Override
+ public Object visit(ASTMap node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.parser.node.ParserVisitor#visit(org.apache.velocity.runtime.parser.node.ASTIntegerRange, java.lang.Object)
+ * @since 1.5
+ */
+ @Override
+ public Object visit(ASTIntegerRange node, Object data)
+ {
+ data = node.childrenAccept(this, data);
+ return data;
+ }
+}
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/visitor/NodeViewMode.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/visitor/NodeViewMode.java
new file mode 100644
index 00000000..baac596d
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/visitor/NodeViewMode.java
@@ -0,0 +1,436 @@
+package org.apache.velocity.runtime.visitor;
+
+/*
+ * 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.
+ */
+
+import org.apache.velocity.runtime.parser.Token;
+import org.apache.velocity.runtime.parser.node.*;
+
+/**
+ * This class is simply a visitor implementation
+ * that traverses the AST, produced by the Velocity
+ * parsing process, and creates a visual structure
+ * of the AST. This is primarily used for
+ * debugging, but it useful for documentation
+ * as well.
+ *
+ * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
+ * @version $Id$
+ */
+public class NodeViewMode extends BaseVisitor
+{
+ private int indent = 0;
+ private boolean showTokens = true;
+
+ /** Indent child nodes to help visually identify
+ * the structure of the AST.
+ */
+ private String indentString()
+ {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < indent; ++i)
+ {
+ sb.append(" ");
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Display the type of nodes and optionally the
+ * first token.
+ */
+ private Object showNode(Node node, Object data)
+ {
+ String tokens = "";
+ String special = "";
+ Token t;
+
+ if (showTokens)
+ {
+ // TODO: Token reference
+ t = node.getFirstToken();
+
+ if (t.specialToken != null && ! t.specialToken.image.startsWith(node.getParser().lineComment()))
+ special = t.specialToken.image;
+
+ tokens = " -> " + special + t.image;
+ }
+
+ System.out.println(indentString() + node + tokens);
+ ++indent;
+ data = node.childrenAccept(this, data);
+ --indent;
+ return data;
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.SimpleNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(SimpleNode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTprocess, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTprocess node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTExpression, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTExpression node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTAssignment, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTAssignment node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTOrNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTOrNode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTAndNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTAndNode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTEQNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTEQNode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTNENode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTNENode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTLTNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTLTNode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTGTNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTGTNode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTLENode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTLENode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTGENode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTGENode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTAddNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTAddNode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTSubtractNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTSubtractNode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTMulNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTMulNode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTDivNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTDivNode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTModNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTModNode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTNotNode, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTNotNode node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTFloatingPointLiteral, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTFloatingPointLiteral node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTIntegerLiteral, java.lang.Object)
+ * @since 1.5
+ */
+ @Override
+ public Object visit(ASTIntegerLiteral node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTStringLiteral, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTStringLiteral node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTIdentifier, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTIdentifier node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTMethod, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTMethod node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTReference, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTReference node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTTrue, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTTrue node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTFalse, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTFalse node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTBlock, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTBlock node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTText, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTText node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTIfStatement, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTIfStatement node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTElseStatement, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTElseStatement node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTElseIfStatement, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTElseIfStatement node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTObjectArray, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTObjectArray node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTDirective, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTDirective node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTWord, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTWord node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTSetDirective, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTSetDirective node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTEscapedDirective, java.lang.Object)
+ * @since 1.5
+ */
+ @Override
+ public Object visit(ASTEscapedDirective node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTEscape, java.lang.Object)
+ * @since 1.5
+ */
+ @Override
+ public Object visit(ASTEscape node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTMap, java.lang.Object)
+ * @since 1.5
+ */
+ @Override
+ public Object visit(ASTMap node, Object data)
+ {
+ return showNode(node,data);
+ }
+
+ /**
+ * @see org.apache.velocity.runtime.visitor.BaseVisitor#visit(org.apache.velocity.runtime.parser.node.ASTIntegerRange, java.lang.Object)
+ */
+ @Override
+ public Object visit(ASTIntegerRange node, Object data)
+ {
+ return showNode(node,data);
+ }
+}