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