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.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 }