org.apache.velocity.runtime.directive.RuntimeMacro Maven / Gradle / Ivy
The newest version!
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 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;
}
}