blob: db9a552cc1db7ed5ee2f3d12462f5ce2bfc70238 [file] [log] [blame]
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 java.io.IOException;
import java.io.Writer;
import java.util.List;
import org.apache.commons.lang.text.StrBuilder;
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.RuntimeServices;
import org.apache.velocity.runtime.log.Log;
import org.apache.velocity.runtime.parser.ParserTreeConstants;
import org.apache.velocity.runtime.parser.Token;
import org.apache.velocity.runtime.parser.node.Node;
import org.apache.velocity.util.introspection.Info;
/**
* 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;
/**
* Create a RuntimeMacro instance. Macro name and source
* template stored for later use.
*
* @param macroName name of the macro
*/
public RuntimeMacro(String macroName)
{
if (macroName == null)
{
throw new IllegalArgumentException("Null arguments");
}
this.macroName = macroName.intern();
}
/**
* Return name of this Velocimacro.
*
* @return The name of this Velocimacro.
*/
public String getName()
{
return macroName;
}
/**
* Velocimacros are always LINE
* type directives.
*
* @return The type of this directive.
*/
public int getType()
{
return LINE;
}
/**
* Intialize 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 context InternalContextAdapter
* @param node node containing the macro call
*/
public void init(RuntimeServices rs, InternalContextAdapter context,
Node node)
{
super.init(rs, context, node);
rsvc = rs;
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'
*/
Token t = node.getLastToken();
if (t.image.startsWith(")") || t.image.startsWith("#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.getFirstToken().image
+ "' in macro #" + macroName + " at " + Log.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,
context.getCurrentTemplateName(), 0, 0);
}
}
}
}
/**
* It is probably quite rare that we need to render the macro literal
* so do it only on-demand and then cache the value. This tactic helps to
* reduce memory usage a bit.
*/
private String getLiteral()
{
if (literal == null)
{
StrBuilder buffer = new StrBuilder();
Token t = node.getFirstToken();
while (t != null && t != node.getLastToken())
{
buffer.append(t.image);
t = t.next;
}
if (t != null)
{
buffer.append(t.image);
}
literal = buffer.toString();
}
return literal;
}
/**
* Velocimacro implementation is not known at the init time. So look for
* a implementation in the macro libaries 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 lbraries 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
*/
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;
String renderingTemplate = context.getCurrentTemplateName();
/**
* first look in the source template
*/
Object o = rsvc.getVelocimacro(macroName, getTemplateName(), renderingTemplate);
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,
(String)macroLibraries.get(i), renderingTemplate);
// get the first matching macro
if (o != null)
{
vmProxy = (VelocimacroProxy) o;
break;
}
}
}
}
if (vmProxy != null)
{
try
{
// mainly check the number of arguments
vmProxy.checkArgs(context, node, body != null);
}
catch (TemplateInitException die)
{
throw new ParseErrorException(die.getMessage() + " at "
+ Log.formatFileString(node), new Info(node));
}
if (badArgsErrorMsg != null)
{
throw new TemplateInitException(badArgsErrorMsg,
context.getCurrentTemplateName(), node.getColumn(), node.getLine());
}
try
{
return vmProxy.render(context, writer, node, body);
}
catch (RuntimeException 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.
*/
rsvc.getLog().error("Exception in macro #" + macroName + " called at " +
Log.formatFileString(node));
throw e;
}
catch (IOException e)
{
rsvc.getLog().error("Exception in macro #" + macroName + " called at " +
Log.formatFileString(node));
throw e;
}
}
else if (strictRef)
{
throw new VelocityException("Macro '#" + macroName + "' is not defined at "
+ Log.formatFileString(node));
}
/**
* If we cannot find an implementation write the literal text
*/
writer.write(getLiteral());
return true;
}
}