1 package org.apache.velocity.runtime.parser.node;
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.lang.reflect.InvocationTargetException;
23
24 import org.apache.commons.lang.ArrayUtils;
25 import org.apache.commons.lang.StringUtils;
26 import org.apache.velocity.app.event.EventHandlerUtil;
27 import org.apache.velocity.context.InternalContextAdapter;
28 import org.apache.velocity.exception.MethodInvocationException;
29 import org.apache.velocity.exception.TemplateInitException;
30 import org.apache.velocity.runtime.parser.Parser;
31 import org.apache.velocity.util.introspection.Info;
32 import org.apache.velocity.util.introspection.IntrospectionCacheData;
33 import org.apache.velocity.util.introspection.VelMethod;
34
35 /**
36 * ASTMethod.java
37 *
38 * Method support for references : $foo.method()
39 *
40 * NOTE :
41 *
42 * introspection is now done at render time.
43 *
44 * Please look at the Parser.jjt file which is
45 * what controls the generation of this class.
46 *
47 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
48 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
49 * @version $Id: ASTMethod.java 517553 2007-03-13 06:09:58Z wglass $
50 */
51 public class ASTMethod extends SimpleNode
52 {
53 private String methodName = "";
54 private int paramCount = 0;
55
56 protected Info uberInfo;
57
58 /**
59 * @param id
60 */
61 public ASTMethod(int id)
62 {
63 super(id);
64 }
65
66 /**
67 * @param p
68 * @param id
69 */
70 public ASTMethod(Parser p, int id)
71 {
72 super(p, id);
73 }
74
75 /**
76 * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
77 */
78 public Object jjtAccept(ParserVisitor visitor, Object data)
79 {
80 return visitor.visit(this, data);
81 }
82
83 /**
84 * simple init - init our subtree and get what we can from
85 * the AST
86 * @param context
87 * @param data
88 * @return The init result
89 * @throws TemplateInitException
90 */
91 public Object init( InternalContextAdapter context, Object data)
92 throws TemplateInitException
93 {
94 super.init( context, data );
95
96 /*
97 * make an uberinfo - saves new's later on
98 */
99
100 uberInfo = new Info(context.getCurrentTemplateName(),
101 getLine(),getColumn());
102 /*
103 * this is about all we can do
104 */
105
106 methodName = getFirstToken().image;
107 paramCount = jjtGetNumChildren() - 1;
108
109 return data;
110 }
111
112 /**
113 * invokes the method. Returns null if a problem, the
114 * actual return if the method returns something, or
115 * an empty string "" if the method returns void
116 * @param o
117 * @param context
118 * @return Result or null.
119 * @throws MethodInvocationException
120 */
121 public Object execute(Object o, InternalContextAdapter context)
122 throws MethodInvocationException
123 {
124 /*
125 * new strategy (strategery!) for introspection. Since we want
126 * to be thread- as well as context-safe, we *must* do it now,
127 * at execution time. There can be no in-node caching,
128 * but if we are careful, we can do it in the context.
129 */
130
131 VelMethod method = null;
132
133 Object [] params = new Object[paramCount];
134
135 try
136 {
137 /*
138 * sadly, we do need recalc the values of the args, as this can
139 * change from visit to visit
140 */
141
142 final Class[] paramClasses = paramCount > 0 ? new Class[paramCount] : ArrayUtils.EMPTY_CLASS_ARRAY;
143
144 for (int j = 0; j < paramCount; j++)
145 {
146 params[j] = jjtGetChild(j + 1).value(context);
147
148 if (params[j] != null)
149 {
150 paramClasses[j] = params[j].getClass();
151 }
152 }
153
154 /*
155 * check the cache
156 */
157
158 MethodCacheKey mck = new MethodCacheKey(methodName, paramClasses);
159 IntrospectionCacheData icd = context.icacheGet( mck );
160
161 /*
162 * like ASTIdentifier, if we have cache information, and the
163 * Class of Object o is the same as that in the cache, we are
164 * safe.
165 */
166
167 if ( icd != null && (o != null && icd.contextData == o.getClass()) )
168 {
169
170 /*
171 * get the method from the cache
172 */
173
174 method = (VelMethod) icd.thingy;
175 }
176 else
177 {
178 /*
179 * otherwise, do the introspection, and then
180 * cache it
181 */
182
183 method = rsvc.getUberspect().getMethod(o, methodName, params, new Info(context.getCurrentTemplateName(), getLine(), getColumn()));
184
185 if ((method != null) && (o != null))
186 {
187 icd = new IntrospectionCacheData();
188 icd.contextData = o.getClass();
189 icd.thingy = method;
190
191 context.icachePut( mck, icd );
192 }
193 }
194
195 /*
196 * if we still haven't gotten the method, either we are calling
197 * a method that doesn't exist (which is fine...) or I screwed
198 * it up.
199 */
200
201 if (method == null)
202 {
203 return null;
204 }
205 }
206 catch( MethodInvocationException mie )
207 {
208 /*
209 * this can come from the doIntrospection(), as the arg values
210 * are evaluated to find the right method signature. We just
211 * want to propogate it here, not do anything fancy
212 */
213
214 throw mie;
215 }
216 /**
217 * pass through application level runtime exceptions
218 */
219 catch( RuntimeException e )
220 {
221 throw e;
222 }
223 catch( Exception e )
224 {
225 /*
226 * can come from the doIntropection() also, from Introspector
227 */
228
229 log.error("ASTMethod.execute() : exception from introspection", e);
230 return null;
231 }
232
233 try
234 {
235 /*
236 * get the returned object. It may be null, and that is
237 * valid for something declared with a void return type.
238 * Since the caller is expecting something to be returned,
239 * as long as things are peachy, we can return an empty
240 * String so ASTReference() correctly figures out that
241 * all is well.
242 */
243
244 Object obj = method.invoke(o, params);
245
246 if (obj == null)
247 {
248 if( method.getReturnType() == Void.TYPE)
249 {
250 return "";
251 }
252 }
253
254 return obj;
255 }
256 catch( InvocationTargetException ite )
257 {
258 /*
259 * In the event that the invocation of the method
260 * itself throws an exception, we want to catch that
261 * wrap it, and throw. We don't log here as we want to figure
262 * out which reference threw the exception, so do that
263 * above
264 */
265
266 /*
267 * let non-Exception Throwables go...
268 */
269
270 Throwable t = ite.getTargetException();
271 if (t instanceof Exception)
272 {
273 try
274 {
275 return EventHandlerUtil.methodException( rsvc, context, o.getClass(), methodName, (Exception) t );
276 }
277
278 /**
279 * If the event handler throws an exception, then wrap it
280 * in a MethodInvocationException. Don't pass through RuntimeExceptions like other
281 * similar catchall code blocks.
282 */
283 catch( Exception e )
284 {
285 throw new MethodInvocationException(
286 "Invocation of method '"
287 + methodName + "' in " + o.getClass()
288 + " threw exception "
289 + e.toString(),
290 e, methodName, context.getCurrentTemplateName(), this.getLine(), this.getColumn());
291 }
292 }
293 else
294 {
295 /*
296 * no event cartridge to override. Just throw
297 */
298
299 throw new MethodInvocationException(
300 "Invocation of method '"
301 + methodName + "' in " + o.getClass()
302 + " threw exception "
303 + ite.getTargetException().toString(),
304 ite.getTargetException(), methodName, context.getCurrentTemplateName(), this.getLine(), this.getColumn());
305 }
306 }
307 /**
308 * pass through application level runtime exceptions
309 */
310 catch( RuntimeException e )
311 {
312 throw e;
313 }
314 catch( Exception e )
315 {
316 log.error("ASTMethod.execute() : exception invoking method '"
317 + methodName + "' in " + o.getClass(), e);
318 return null;
319 }
320 }
321
322 /**
323 * Internal class used as key for method cache. Combines
324 * ASTMethod fields with array of parameter classes. Has
325 * public access (and complete constructor) for unit test
326 * purposes.
327 */
328 public static class MethodCacheKey
329 {
330 private final String methodName;
331 private final Class[] params;
332
333 public MethodCacheKey(String methodName, Class[] params)
334 {
335 /**
336 * Should never be initialized with nulls, but to be safe we refuse
337 * to accept them.
338 */
339 this.methodName = (methodName != null) ? methodName : StringUtils.EMPTY;
340 this.params = (params != null) ? params : ArrayUtils.EMPTY_CLASS_ARRAY;
341 }
342
343 /**
344 * @see java.lang.Object#equals(java.lang.Object)
345 */
346 public boolean equals(Object o)
347 {
348 /**
349 * note we skip the null test for methodName and params
350 * due to the earlier test in the constructor
351 */
352 if (o instanceof MethodCacheKey)
353 {
354 final MethodCacheKey other = (MethodCacheKey) o;
355 if (params.length == other.params.length &&
356 methodName.equals(other.methodName))
357 {
358 for (int i = 0; i < params.length; ++i)
359 {
360 if (params[i] == null)
361 {
362 if (params[i] != other.params[i])
363 {
364 return false;
365 }
366 }
367 else if (!params[i].equals(other.params[i]))
368 {
369 return false;
370 }
371 }
372 return true;
373 }
374 }
375 return false;
376 }
377
378
379 /**
380 * @see java.lang.Object#hashCode()
381 */
382 public int hashCode()
383 {
384 int result = 17;
385
386 /**
387 * note we skip the null test for methodName and params
388 * due to the earlier test in the constructor
389 */
390 for (int i = 0; i < params.length; ++i)
391 {
392 final Class param = params[i];
393 if (param != null)
394 {
395 result = result * 37 + param.hashCode();
396 }
397 }
398
399 result = result * 37 + methodName.hashCode();
400
401 return result;
402 }
403 }
404
405 /**
406 * @return Returns the methodName.
407 */
408 public String getMethodName()
409 {
410 return methodName;
411 }
412
413
414 }