View Javadoc

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