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