1 package org.apache.velocity.runtime.directive; 2 3 /* 4 * Licensed to the Apache Software Foundation (ASF) under one 5 * or more contributor license agreements. See the NOTICE file 6 * distributed with this work for additional information 7 * regarding copyright ownership. The ASF licenses this file 8 * to you under the Apache License, Version 2.0 (the 9 * "License"); you may not use this file except in compliance 10 * with the License. You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, 15 * software distributed under the License is distributed on an 16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 * KIND, either express or implied. See the License for the 18 * specific language governing permissions and limitations 19 * under the License. 20 */ 21 22 import org.apache.velocity.context.InternalContextAdapter; 23 import org.apache.velocity.exception.MacroOverflowException; 24 import org.apache.velocity.exception.VelocityException; 25 import org.apache.velocity.runtime.Renderable; 26 import org.apache.velocity.runtime.RuntimeConstants; 27 import org.apache.velocity.runtime.RuntimeServices; 28 import org.apache.velocity.runtime.directive.Macro.MacroArg; 29 import org.apache.velocity.runtime.parser.node.ASTReference; 30 import org.apache.velocity.runtime.parser.node.ASTStringLiteral; 31 import org.apache.velocity.runtime.parser.node.Node; 32 import org.apache.velocity.runtime.parser.node.SimpleNode; 33 import org.apache.velocity.util.StringUtils; 34 35 import java.io.IOException; 36 import java.io.Writer; 37 import java.util.Deque; 38 import java.util.LinkedList; 39 import java.util.List; 40 41 /** 42 * VelocimacroProxy.java 43 * 44 * a proxy Directive-derived object to fit with the current directive system 45 * 46 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a> 47 * @version $Id$ 48 */ 49 public class VelocimacroProxy extends Directive 50 { 51 private String macroName; 52 private List<MacroArg> macroArgs = null; 53 private String[] literalArgArray = null; 54 private SimpleNode nodeTree = null; 55 private int numMacroArgs = 0; 56 private boolean strictArguments; 57 private int maxCallDepth; 58 private String bodyReference; 59 private boolean enableBCmode; 60 61 private static final Object NULL_VALUE_MARKER = new Object(); 62 63 /** 64 * Return name of this Velocimacro. 65 * @return The name of this Velocimacro. 66 */ 67 @Override getName()68 public String getName() 69 { 70 return macroName; 71 } 72 73 /** 74 * Velocimacros are always LINE type directives. 75 * @return The type of this directive. 76 */ 77 @Override getType()78 public int getType() 79 { 80 return LINE; 81 } 82 83 /** 84 * sets the directive name of this VM 85 * 86 * @param name 87 */ setName(String name)88 public void setName(String name) 89 { 90 macroName = name; 91 } 92 93 /** 94 * sets the array of arguments specified in the macro definition 95 * @param args Array of macro arguments, containing the 96 * #macro() arguments and default values. the 0th is the name. 97 */ setMacroArgs(List<Macro.MacroArg> args)98 public void setMacroArgs(List<Macro.MacroArg> args) 99 { 100 macroArgs = args; 101 102 // for performance reasons we precache these strings - they are needed in 103 // "render literal if null" functionality 104 if (enableBCmode) 105 { 106 literalArgArray = new String[macroArgs.size()]; 107 for (int i = 0; i < macroArgs.size(); i++) 108 { 109 literalArgArray[i] = ".literal.$" + macroArgs.get(i).name; 110 } 111 } 112 113 /* 114 * get the arg count from the arg array. remember that the arg array has the macro name as 115 * it's 0th element 116 */ 117 118 numMacroArgs = macroArgs.size() - 1; 119 } 120 121 /** 122 * Return the list of macro arguments associated with this macro 123 * @return macro arguments 124 */ getMacroArgs()125 public List<Macro.MacroArg> getMacroArgs() 126 { 127 return macroArgs; 128 } 129 130 /** 131 * @param tree 132 */ setNodeTree(SimpleNode tree)133 public void setNodeTree(SimpleNode tree) 134 { 135 nodeTree = tree; 136 } 137 138 /** 139 * returns the number of ars needed for this VM 140 * @return The number of ars needed for this VM 141 */ getNumArgs()142 public int getNumArgs() 143 { 144 return numMacroArgs; 145 } 146 147 /** 148 * Initialize members of VelocimacroProxy. called from MacroEntry 149 * @param rs runtime services 150 */ init(RuntimeServices rs)151 public void init(RuntimeServices rs) 152 { 153 rsvc = rs; 154 log = rs.getLog("macro"); 155 156 strictArguments = rsvc.getBoolean( 157 RuntimeConstants.VM_ARGUMENTS_STRICT, false); 158 159 // get the macro call depth limit 160 maxCallDepth = rsvc.getInt(RuntimeConstants.VM_MAX_DEPTH); 161 162 // get name of the reference that refers to AST block passed to block macro call 163 bodyReference = rsvc.getString(RuntimeConstants.VM_BODY_REFERENCE, "bodyContent"); 164 165 enableBCmode = rsvc.getBoolean(RuntimeConstants.VM_ENABLE_BC_MODE, false); 166 } 167 168 /** 169 * Render the macro AST node 170 * @param context 171 * @param writer 172 * @param node 173 * @return success status 174 * @throws IOException 175 */ 176 @Override render(InternalContextAdapter context, Writer writer, Node node)177 public boolean render(InternalContextAdapter context, Writer writer, Node node) 178 throws IOException 179 { 180 return render(context, writer, node, null); 181 } 182 183 /** 184 * Renders the macro using the context. 185 * 186 * @param context Current rendering context 187 * @param writer Writer for output 188 * @param node AST that calls the macro 189 * @param body the macro body 190 * @return true if the directive rendered successfully. 191 * @throws IOException 192 */ render(InternalContextAdapter context, Writer writer, Node node, Renderable body)193 public boolean render(InternalContextAdapter context, Writer writer, 194 Node node, Renderable body) 195 throws IOException 196 { 197 int callArgNum = node.jjtGetNumChildren(); 198 199 // if this macro was invoked by a call directive, we might have a body AST here. 200 Object oldBodyRef = null; 201 if (body != null) 202 { 203 oldBodyRef = context.get(bodyReference); 204 context.put(bodyReference, body); 205 callArgNum--; // Remove the body AST from the arg count 206 } 207 208 // is everything copacetic? 209 checkArgumentCount(node, callArgNum); 210 checkDepth(context); 211 212 // put macro arg values and save the returned old/new value pairs 213 Object[] values = handleArgValues(context, node, callArgNum); 214 try 215 { 216 // render the velocity macro 217 context.pushCurrentMacroName(macroName); 218 nodeTree.render(context, writer); 219 } 220 catch (RuntimeException e) 221 { 222 throw e; 223 } 224 catch (Exception e) 225 { 226 String msg = "VelocimacroProxy.render() : exception VM = #" + macroName + "()"; 227 log.error(msg, e); 228 throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace()); 229 } 230 finally 231 { 232 // if MacroOverflowException was thrown then it already empties the stack 233 // for everything else - e.g. other exceptions - we clean up after ourself 234 if (context.getCurrentMacroCallDepth() > 0) 235 context.popCurrentMacroName(); 236 237 // clean up after the args and bodyRef 238 // but only if they weren't overridden inside 239 Object current = context.get(bodyReference); 240 if (current == body) 241 { 242 if (oldBodyRef != null) 243 { 244 context.put(bodyReference, oldBodyRef); 245 } 246 else 247 { 248 context.remove(bodyReference); 249 } 250 } 251 252 for (int i = 1; i < macroArgs.size(); i++) 253 { 254 MacroArg macroArg = macroArgs.get(i); 255 current = context.get(macroArg.name); 256 Object given = values[(i-1) * 2 + 1]; 257 Object old = values[(i-1) * 2]; 258 if (current == given || current == null && given == NULL_VALUE_MARKER) 259 { 260 if (old == null) 261 { 262 context.remove(macroArg.name); 263 } 264 else if (old == NULL_VALUE_MARKER) 265 { 266 context.put(macroArg.name, null); 267 } 268 else 269 { 270 context.put(macroArg.name, old); 271 } 272 } 273 274 if (enableBCmode) 275 { 276 /* allow for nested calls */ 277 Deque<String> literalsStack = (Deque<String>)context.get(literalArgArray[i]); 278 if (literalsStack != null) /* shouldn't be null */ 279 { 280 literalsStack.removeFirst(); 281 if (literalsStack.size() == 0) 282 { 283 context.remove(literalArgArray[i]); 284 } 285 } 286 } 287 } 288 } 289 290 return true; 291 } 292 293 /** 294 * Check whether the number of arguments given matches the number defined. 295 * @param node 296 * @param callArgNum 297 */ checkArgumentCount(Node node, int callArgNum)298 protected void checkArgumentCount(Node node, int callArgNum) 299 { 300 // Check if we have more calling arguments then the macro accepts 301 if (callArgNum > macroArgs.size() - 1) 302 { 303 if (strictArguments) 304 { 305 throw new VelocityException("Provided " + callArgNum + " arguments but macro #" 306 + macroArgs.get(0).name + " accepts at most " + (macroArgs.size()-1) 307 + " at " + StringUtils.formatFileString(node), null, rsvc.getLogContext().getStackTrace()); 308 } 309 // Backward compatibility logging, Mainly for MacroForwardDefinedTestCase 310 log.debug("VM #{}: too many arguments to macro. Wanted {} got {}", 311 macroArgs.get(0).name, macroArgs.size() - 1, callArgNum); 312 } 313 } 314 315 /** 316 * check that we aren't already at the max call depth and throws 317 * a MacroOverflowException if we are there. 318 * @param context 319 */ checkDepth(InternalContextAdapter context)320 protected void checkDepth(InternalContextAdapter context) 321 { 322 if (maxCallDepth > 0 && maxCallDepth == context.getCurrentMacroCallDepth()) 323 { 324 String[] stack = context.getMacroNameStack(); 325 326 StringBuilder out = new StringBuilder(100) 327 .append("Max calling depth of ").append(maxCallDepth) 328 .append(" was exceeded in macro '").append(macroName) 329 .append("' with Call Stack:"); 330 for (int i = 0; i < stack.length; i++) 331 { 332 if (i != 0) 333 { 334 out.append("->"); 335 } 336 out.append(stack[i]); 337 } 338 out.append(" at ").append(StringUtils.formatFileString(this)); 339 log.error(out.toString()); 340 341 // clean out the macro stack, since we just broke it 342 while (context.getCurrentMacroCallDepth() > 0) 343 { 344 context.popCurrentMacroName(); 345 } 346 throw new MacroOverflowException(out.toString(), null, rsvc.getLogContext().getStackTrace()); 347 } 348 } 349 350 /** 351 * Gets the macro argument values and puts them in the context under 352 * the argument names. Store and return an array of old and new values 353 * paired for each argument name, for later cleanup. Also, put literal 354 * representations of arguments which evaluate to null in the context. 355 * @param context 356 * @param node 357 * @param callArgNum 358 * @return macro arguments values 359 */ handleArgValues(InternalContextAdapter context, Node node, int callArgNum)360 protected Object[] handleArgValues(InternalContextAdapter context, 361 Node node, int callArgNum) 362 { 363 // Changed two dimensional array to single dimensional to optimize memory lookups 364 Object[] values = new Object[macroArgs.size() * 2]; 365 366 boolean warnedMissingArguments = false; 367 368 // Move arguments into the macro's context. Start at one to skip macro name 369 for (int i = 1; i < macroArgs.size(); i++) 370 { 371 MacroArg macroArg = macroArgs.get(i); 372 Object oldVal = context.get(macroArg.name); 373 values[(i-1) * 2] = 374 oldVal == null 375 ? context.containsKey(macroArg.name) ? NULL_VALUE_MARKER : null 376 : oldVal; 377 378 // put the new value in 379 Object newVal = null; 380 Node argNode = null; 381 if (i - 1 < callArgNum) 382 { 383 // There's a calling value. 384 argNode = node.jjtGetChild(i - 1); 385 newVal = argNode.value(context); 386 } 387 else if (macroArg.defaultVal != null) 388 { 389 // We don't have a calling value, but the macro defines a default value 390 newVal = macroArg.defaultVal.value(context); 391 } 392 else if (strictArguments) 393 { 394 // We come to this point if we don't have a calling value, and 395 // there is no default value. Not enough arguments defined. 396 int minArgNum = -1; //start at -1 to skip the macro name 397 // Calculate minimum number of args required for macro 398 for (MacroArg marg : macroArgs) 399 { 400 if (marg.defaultVal == null) minArgNum++; 401 } 402 throw new VelocityException("Need at least " + minArgNum + " argument for macro #" 403 + macroArgs.get(0).name + " but only " + callArgNum + " where provided at " 404 + StringUtils.formatFileString(node), null, rsvc.getLogContext().getStackTrace()); 405 } 406 else 407 { 408 if (!warnedMissingArguments) 409 { 410 // Backward compatibility logging, Mainly for MacroForwardDefinedTestCase 411 log.debug("VM #{}: too few arguments to macro. Wanted {} got {}", 412 macroArgs.get(0).name, macroArgs.size() - 1, callArgNum); 413 warnedMissingArguments = true; 414 } 415 if (enableBCmode) 416 { 417 // use the global context value as default 418 newVal = oldVal; 419 } 420 } 421 422 values[(i-1) * 2 + 1] = newVal; 423 424 /* when enableBCmode is true, we still store the actual reference passed to the macro 425 even if the value is not null, because *if* the argument is set to null *during* the macro rendering 426 we still expect the passed argument literal to be displayed to be fully backward compatible. */ 427 if (enableBCmode && /* newVal == null && */ argNode != null) 428 { 429 /* allow nested macro calls for B.C. */ 430 Deque<String> literalsStack = (Deque<String>)context.get(literalArgArray[i]); 431 if (literalsStack == null) 432 { 433 literalsStack = new LinkedList<>(); 434 context.put(literalArgArray[i], literalsStack); 435 } 436 /* Reflects the strange 1.7 behavor... */ 437 if (argNode != null && (argNode instanceof ASTReference || argNode instanceof ASTStringLiteral)) 438 { 439 literalsStack.addFirst(argNode.literal()); 440 } 441 else 442 { 443 literalsStack.addFirst('$' + macroArg.name); 444 } 445 } 446 } 447 448 // Now really put the values in the context 449 for (int i = 1; i < macroArgs.size(); i++) 450 { 451 MacroArg macroArg = macroArgs.get(i); 452 Object value = values[(i-1) * 2 + 1]; 453 context.put(macroArg.name, value); 454 } 455 456 // return the array of replaced and new values 457 return values; 458 } 459 } 460