org.apache.velocity.runtime.directive.Macro 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.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 Geir Magnusson Jr.
* @author Henning P. Schmiedehausen
* @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 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 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 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 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
* argArray
and appends them to buf
.
*
* @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 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;
}
}