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 java.io.IOException;
23 import java.io.Writer;
24 import java.util.List;
25
26 import org.apache.velocity.context.InternalContextAdapter;
27 import org.apache.velocity.exception.MacroOverflowException;
28 import org.apache.velocity.exception.VelocityException;
29 import org.apache.velocity.runtime.Renderable;
30 import org.apache.velocity.runtime.RuntimeConstants;
31 import org.apache.velocity.runtime.RuntimeServices;
32 import org.apache.velocity.runtime.directive.Macro.MacroArg;
33 import org.apache.velocity.runtime.log.Log;
34 import org.apache.velocity.runtime.parser.node.Node;
35 import org.apache.velocity.runtime.parser.node.SimpleNode;
36
37 /**
38 * VelocimacroProxy.java
39 *
40 * a proxy Directive-derived object to fit with the current directive system
41 *
42 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
43 * @version $Id: VelocimacroProxy.java 754562 2009-03-14 23:59:44Z byron $
44 */
45 public class VelocimacroProxy extends Directive
46 {
47 private String macroName;
48 private List<Macro.MacroArg> macroArgs = null;
49 private String[] literalArgArray = null;
50 private SimpleNode nodeTree = null;
51 private int numMacroArgs = 0;
52 private boolean strictArguments;
53 private int maxCallDepth;
54 private String bodyReference;
55
56 /**
57 * Return name of this Velocimacro.
58 * @return The name of this Velocimacro.
59 */
60 public String getName()
61 {
62 return macroName;
63 }
64
65 /**
66 * Velocimacros are always LINE type directives.
67 * @return The type of this directive.
68 */
69 public int getType()
70 {
71 return LINE;
72 }
73
74 /**
75 * sets the directive name of this VM
76 *
77 * @param name
78 */
79 public void setName(String name)
80 {
81 macroName = name;
82 }
83
84 /**
85 * sets the array of arguments specified in the macro definition
86 * @param args Array of macro arguments, containing the
87 * #macro() arguments and default values. the 0th is the name.
88 */
89 public void setMacroArgs(List<Macro.MacroArg> args)
90 {
91 macroArgs = args;
92
93 // for performance reasons we precache these strings - they are needed in
94 // "render literal if null" functionality
95 literalArgArray = new String[macroArgs.size()];
96 for(int i = 0; i < macroArgs.size(); i++)
97 {
98 literalArgArray[i] = ".literal.$" + macroArgs.get(i);
99 }
100
101 /*
102 * get the arg count from the arg array. remember that the arg array has the macro name as
103 * it's 0th element
104 */
105
106 numMacroArgs = macroArgs.size() - 1;
107 }
108
109 /**
110 * Return the list of macro arguments associated with this macro
111 */
112 public List<Macro.MacroArg> getMacroArgs()
113 {
114 return macroArgs;
115 }
116
117 /**
118 * @param tree
119 */
120 public void setNodeTree(SimpleNode tree)
121 {
122 nodeTree = tree;
123 }
124
125 /**
126 * returns the number of ars needed for this VM
127 *
128 * @return The number of ars needed for this VM
129 */
130 public int getNumArgs()
131 {
132 return numMacroArgs;
133 }
134
135 /**
136 * Initialize members of VelocimacroProxy. called from MacroEntry
137 */
138 public void init(RuntimeServices rs)
139 {
140 rsvc = rs;
141
142 // this is a very expensive call (ExtendedProperties is very slow)
143 strictArguments = rs.getConfiguration().getBoolean(
144 RuntimeConstants.VM_ARGUMENTS_STRICT, false);
145
146 // get the macro call depth limit
147 maxCallDepth = rsvc.getInt(RuntimeConstants.VM_MAX_DEPTH);
148
149 // get name of the reference that refers to AST block passed to block macro call
150 bodyReference = rsvc.getString(RuntimeConstants.VM_BODY_REFERENCE, "bodyContent");
151 }
152
153 public boolean render(InternalContextAdapter context, Writer writer, Node node)
154 throws IOException
155 {
156 return render(context, writer, node, null);
157 }
158
159 /**
160 * Renders the macro using the context.
161 *
162 * @param context Current rendering context
163 * @param writer Writer for output
164 * @param node AST that calls the macro
165 * @param body the macro body
166 * @return true if the directive rendered successfully.
167 * @throws IOException
168 */
169 public boolean render(InternalContextAdapter context, Writer writer,
170 Node node, Renderable body)
171 throws IOException
172 {
173 int callArgNum = node.jjtGetNumChildren();
174
175 // if this macro was invoked by a call directive, we might have a body AST here.
176 Object oldBodyRef = null;
177 if (body != null)
178 {
179 oldBodyRef = context.get(bodyReference);
180 context.put(bodyReference, body);
181 callArgNum--; // Remove the body AST from the arg count
182 }
183
184 // is everything copacetic?
185 checkArgumentCount(node, callArgNum);
186 checkDepth(context);
187
188 // put macro arg values and save the returned old/new value pairs
189 Object[][] values = handleArgValues(context, node, callArgNum);
190 try
191 {
192 // render the velocity macro
193 context.pushCurrentMacroName(macroName);
194 nodeTree.render(context, writer);
195 context.popCurrentMacroName();
196 return true;
197 }
198 catch (RuntimeException e)
199 {
200 throw e;
201 }
202 catch (Exception e)
203 {
204 String msg = "VelocimacroProxy.render() : exception VM = #" + macroName + "()";
205 rsvc.getLog().error(msg, e);
206 throw new VelocityException(msg, e);
207 }
208 finally
209 {
210 // clean up after the args and bodyRef
211 // but only if they weren't overridden inside
212 Object current = context.get(bodyReference);
213 if (current == body)
214 {
215 if (oldBodyRef != null)
216 {
217 context.put(bodyReference, oldBodyRef);
218 }
219 else
220 {
221 context.remove(bodyReference);
222 }
223 }
224
225 for (int i = 1; i < macroArgs.size(); i++)
226 {
227 MacroArg macroArg = macroArgs.get(i);
228 current = context.get(macroArg.name);
229 if (current == values[i-1][1])
230 {
231 Object old = values[i-1][0];
232 if (old != null)
233 {
234 context.put(macroArg.name, old);
235 }
236 else
237 {
238 context.remove(macroArg.name);
239 }
240 }
241 }
242 }
243 }
244
245 /**
246 * Check whether the number of arguments given matches the number defined.
247 */
248 protected void checkArgumentCount(Node node, int callArgNum)
249 {
250 // Check if we have more calling arguments then the macro accepts
251 if (callArgNum > macroArgs.size() - 1)
252 {
253 if (strictArguments)
254 {
255 throw new VelocityException("Provided " + callArgNum + " arguments but macro #"
256 + macroArgs.get(0).name + " accepts at most " + (macroArgs.size()-1)
257 + " at " + Log.formatFileString(node));
258 }
259 else if (rsvc.getLog().isDebugEnabled())
260 {
261 // Backward compatibility logging, Mainly for MacroForwardDefinedTestCase
262 rsvc.getLog().debug("VM #" + macroArgs.get(0).name
263 + ": too many arguments to macro. Wanted " + (macroArgs.size()-1)
264 + " got " + callArgNum);
265 }
266 }
267 }
268
269 /**
270 * check that we aren't already at the max call depth and throws
271 * a MacroOverflowException if we are there.
272 */
273 protected void checkDepth(InternalContextAdapter context)
274 {
275 if (maxCallDepth > 0 && maxCallDepth == context.getCurrentMacroCallDepth())
276 {
277 String templateName = context.getCurrentTemplateName();
278 Object[] stack = context.getMacroNameStack();
279
280 StringBuffer out = new StringBuffer(100)
281 .append("Max calling depth of ").append(maxCallDepth)
282 .append(" was exceeded in macro '").append(macroName)
283 .append("' with Call Stack:");
284 for (int i = 0; i < stack.length; i++)
285 {
286 if (i != 0)
287 {
288 out.append("->");
289 }
290 out.append(stack[i]);
291 }
292 out.append(" at " + Log.formatFileString(this));
293 rsvc.getLog().error(out.toString());
294
295 // clean out the macro stack, since we just broke it
296 while (context.getCurrentMacroCallDepth() > 0)
297 {
298 context.popCurrentMacroName();
299 }
300 throw new MacroOverflowException(out.toString());
301 }
302 }
303
304 /**
305 * Gets the macro argument values and puts them in the context under
306 * the argument names. Store and return an array of old and new values
307 * paired for each argument name, for later cleanup.
308 */
309 protected Object[][] handleArgValues(InternalContextAdapter context,
310 Node node, int callArgNum)
311 {
312 Object[][] values = new Object[macroArgs.size()][2];
313
314 // Move arguments into the macro's context. Start at one to skip macro name
315 for (int i = 1; i < macroArgs.size(); i++)
316 {
317 MacroArg macroArg = macroArgs.get(i);
318 values[i-1][0] = context.get(macroArg.name);
319
320 // put the new value in
321 Object newVal = null;
322 if (i - 1 < callArgNum)
323 {
324 // There's a calling value.
325 newVal = node.jjtGetChild(i - 1).value(context);
326 }
327 else if (macroArg.defaultVal != null)
328 {
329 // We don't have a calling value, but the macro defines a default value
330 newVal = macroArg.defaultVal.value(context);
331 }
332 else if (strictArguments)
333 {
334 // We come to this point if we don't have a calling value, and
335 // there is no default value. Not enough arguments defined.
336 int minArgNum = -1; //start at -1 to skip the macro name
337 // Calculate minimum number of args required for macro
338 for (MacroArg marg : macroArgs)
339 {
340 if (marg.defaultVal == null) minArgNum++;
341 }
342 throw new VelocityException("Need at least " + minArgNum + " argument for macro #"
343 + macroArgs.get(0).name + " but only " + callArgNum + " where provided at "
344 + Log.formatFileString(node));
345 }
346 else
347 {
348 // Backward compatibility logging, Mainly for MacroForwardDefinedTestCase
349 if (rsvc.getLog().isDebugEnabled())
350 {
351 rsvc.getLog().debug("VM #" + macroArgs.get(0).name
352 + ": too few arguments to macro. Wanted " + (macroArgs.size()-1)
353 + " got " + callArgNum);
354 }
355 break;
356 }
357
358 context.put(macroArg.name, newVal);
359 values[i-1][1] = newVal;
360 }
361
362 // return the array of replaced and new values
363 return values;
364 }
365
366 }
367