aboutsummaryrefslogtreecommitdiff
path: root/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMethod.java
blob: 5696b6bb759b12b019a8ab11ac83b6c2d30cbc84 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
package org.apache.velocity.runtime.parser.node;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.commons.lang3.StringUtils;
import org.apache.velocity.app.event.EventHandlerUtil;
import org.apache.velocity.context.InternalContextAdapter;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.TemplateInitException;
import org.apache.velocity.exception.VelocityException;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.directive.StopCommand;
import org.apache.velocity.runtime.parser.Parser;
import org.apache.velocity.util.ClassUtils;
import org.apache.velocity.util.introspection.Info;
import org.apache.velocity.util.introspection.IntrospectionCacheData;
import org.apache.velocity.util.introspection.VelMethod;

import java.lang.reflect.InvocationTargetException;

/**
 *  ASTMethod.java
 *
 *  Method support for references :  $foo.method()
 *
 *  NOTE :
 *
 *  introspection is now done at render time.
 *
 *  Please look at the Parser.jjt file which is
 *  what controls the generation of this class.
 *
 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
 * @version $Id$
 */
public class ASTMethod extends SimpleNode
{
    /**
     * An empty immutable <code>Class</code> array.
     */
    private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];

    private String methodName = "";
    private int paramCount = 0;
    private boolean logOnInvalid = true;

    protected Info uberInfo;

    /**
     * Indicates if we are running in strict reference mode.
     */
    protected boolean strictRef = false;

    /**
     * @param id
     */
    public ASTMethod(int id)
    {
        super(id);
    }

    /**
     * @param p
     * @param id
     */
    public ASTMethod(Parser p, int id)
    {
        super(p, id);
    }

    /**
     * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
     */
    @Override
    public Object jjtAccept(ParserVisitor visitor, Object data)
    {
        return visitor.visit(this, data);
    }

    /**
     *  simple init - init our subtree and get what we can from
     *  the AST
     * @param context
     * @param data
     * @return The init result
     * @throws TemplateInitException
     */
    @Override
    public Object init(InternalContextAdapter context, Object data)
        throws TemplateInitException
    {
        super.init(  context, data );

        /*
         * make an uberinfo - saves new's later on
         */

        uberInfo = new Info(getTemplateName(),
                getLine(),getColumn());
        /*
         *  this is about all we can do
         */

        methodName = getFirstToken().image;
        paramCount = jjtGetNumChildren() - 1;

        strictRef = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false);
        logOnInvalid = rsvc.getBoolean(RuntimeConstants.RUNTIME_LOG_METHOD_CALL_LOG_INVALID, true);

        cleanupParserAndTokens();

        return data;
    }

    /**
     *  invokes the method.  Returns null if a problem, the
     *  actual return if the method returns something, or
     *  an empty string "" if the method returns void
     * @param o
     * @param context
     * @return Result or null.
     * @throws MethodInvocationException
     */
    @Override
    public Object execute(Object o, InternalContextAdapter context)
        throws MethodInvocationException
    {
        try
        {
            rsvc.getLogContext().pushLogContext(this, uberInfo);

            /*
             *  new strategy (strategery!) for introspection. Since we want
             *  to be thread- as well as context-safe, we *must* do it now,
             *  at execution time.  There can be no in-node caching,
             *  but if we are careful, we can do it in the context.
             */
            Object [] params = new Object[paramCount];

              /*
               * sadly, we do need recalc the values of the args, as this can
               * change from visit to visit
               */
            final Class<?>[] paramClasses =
                paramCount > 0 ? new Class[paramCount] : EMPTY_CLASS_ARRAY;

            for (int j = 0; j < paramCount; j++)
            {
                params[j] = jjtGetChild(j + 1).value(context);
                if (params[j] != null)
                {
                    paramClasses[j] = params[j].getClass();
                }
            }

            VelMethod method = ClassUtils.getMethod(methodName, params, paramClasses,
                o, context, this, strictRef);

            // warn if method wasn't found (if strictRef is true, then ClassUtils did throw an exception)
            if (o != null && method == null && logOnInvalid)
            {
                StringBuilder plist = new StringBuilder();
                for (int i = 0; i < params.length; i++)
                {
                    Class<?> param = paramClasses[i];
                    plist.append(param == null ? "null" : param.getName());
                    if (i < params.length - 1)
                        plist.append(", ");
                }
                log.debug("Object '{}' does not contain method {}({}) (or several ambiguous methods) at {}[line {}, column {}]", o.getClass().getName(), methodName, plist, getTemplateName(), getLine(), getColumn());
            }

            /*
             * The parent class (typically ASTReference) uses the icache entry
             * under 'this' key to distinguish a valid null result from a non-existent method.
             * So update this dummy cache value if necessary.
             */
            IntrospectionCacheData prevICD = context.icacheGet(this);
            if (method == null)
            {
                if (prevICD != null)
                {
                    context.icachePut(this, null);
                }
                return null;
            }
            else if (prevICD == null)
            {
                context.icachePut(this, new IntrospectionCacheData()); // no need to fill in its members
            }

            try
            {
                /*
                 *  get the returned object.  It may be null, and that is
                 *  valid for something declared with a void return type.
                 *  Since the caller is expecting something to be returned,
                 *  as long as things are peachy, we can return an empty
                 *  String so ASTReference() correctly figures out that
                 *  all is well.
                 */

                Object obj = method.invoke(o, params);

                if (obj == null)
                {
                    if( method.getReturnType() == Void.TYPE)
                    {
                        return "";
                    }
                }

                return obj;
            }
            catch( InvocationTargetException ite )
            {
                return handleInvocationException(o, context, ite.getTargetException());
            }

            /* Can also be thrown by method invocation */
            catch( IllegalArgumentException t )
            {
                return handleInvocationException(o, context, t);
            }

            /*
             * pass through application level runtime exceptions
             */
            catch( RuntimeException e )
            {
                throw e;
            }
            catch( Exception e )
            {
                String msg = "ASTMethod.execute() : exception invoking method '"
                             + methodName + "' in " + o.getClass();
                log.error(msg, e);
                throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace());
            }
        }
        finally
        {
            rsvc.getLogContext().popLogContext();
        }
    }

    private Object handleInvocationException(Object o, InternalContextAdapter context, Throwable t)
    {
        /*
         * Errors should not be wrapped
         */
        if (t instanceof Error)
        {
            throw (Error)t;
        }
        /*
         * We let StopCommands go up to the directive they are for/from
         */
        else if (t instanceof StopCommand)
        {
            throw (StopCommand)t;
        }

        /*
         *  In the event that the invocation of the method
         *  itself throws an exception, we want to catch that
         *  wrap it, and throw.  We don't log here as we want to figure
         *  out which reference threw the exception, so do that
         *  above
         */
        else if (t instanceof Exception)
        {
            try
            {
                return EventHandlerUtil.methodException( rsvc, context, o.getClass(), methodName, (Exception) t, uberInfo );
            }

            /*
             * If the event handler throws an exception, then wrap it
             * in a MethodInvocationException.  Don't pass through RuntimeExceptions like other
             * similar catchall code blocks.
             */
            catch( Exception e )
            {
                throw new MethodInvocationException(
                    "Invocation of method '"
                    + methodName + "' in  " + o.getClass()
                    + " threw exception "
                    + e.toString(),
                    e, rsvc.getLogContext().getStackTrace(), methodName, getTemplateName(), this.getLine(), this.getColumn());
            }
        }

        /*
         *  let non-Exception Throwables go...
         */
        else
        {
            /*
             * no event cartridge to override. Just throw
             */

            throw new MethodInvocationException(
            "Invocation of method '"
            + methodName + "' in  " + o.getClass()
            + " threw exception "
            + t.toString(),
            t, rsvc.getLogContext().getStackTrace(), methodName, getTemplateName(), this.getLine(), this.getColumn());
        }
    }

    /**
     * Internal class used as key for method cache.  Combines
     * ASTMethod fields with array of parameter classes.  Has
     * public access (and complete constructor) for unit test
     * purposes.
     * @since 1.5
     */
    public static class MethodCacheKey
    {
        /**
         * method name
         */
        private final String methodName;

        /**
         * parameters classes
         */
        private final Class<?>[] params;

        /**
         * whether the target object is of Class type
         * (meaning we're searching either for methods
         * of Class, or for static methods of the class
         * this Class objects refers to)
         * @since 2.2
         */
        private boolean classObject;

        public MethodCacheKey(String methodName, Class<?>[] params, boolean classObject)
        {
            /*
             * Should never be initialized with nulls, but to be safe we refuse
             * to accept them.
             */
            this.methodName = (methodName != null) ? methodName : StringUtils.EMPTY;
            this.params = (params != null) ? params : EMPTY_CLASS_ARRAY;
            this.classObject = classObject;
        }

        /**
         * @see java.lang.Object#equals(java.lang.Object)
         */
        public boolean equals(Object o)
        {
            /*
             * note we skip the null test for methodName and params
             * due to the earlier test in the constructor
             */
            if (o instanceof MethodCacheKey)
            {
                final MethodCacheKey other = (MethodCacheKey) o;
                if (params.length == other.params.length &&
                        methodName.equals(other.methodName) &&
                            classObject == other.classObject)
                {
                    for (int i = 0; i < params.length; ++i)
                    {
                        if (params[i] == null)
                        {
                            if (params[i] != other.params[i])
                            {
                                return false;
                            }
                        }
                        else if (!params[i].equals(other.params[i]))
                        {
                            return false;
                        }
                    }
                    return true;
                }
            }
            return false;
        }


        /**
         * @see java.lang.Object#hashCode()
         */
        public int hashCode()
        {
            int result = 17;

            /*
             * note we skip the null test for methodName and params
             * due to the earlier test in the constructor
             */
            for (Class<?> param : params)
            {
                if (param != null)
                {
                    result = result * 37 + param.hashCode();
                }
            }

            result = result * 37 + methodName.hashCode();

            return result;
        }
    }

    /**
     * @return Returns the methodName.
     * @since 1.5
     */
    public String getMethodName()
    {
        return methodName;
    }

    /**
     * Returns the string ".<i>method_name</i>(...)". Arguments literals are not rendered. This method is only
     * used for displaying the VTL stacktrace when a rendering error is encountered when runtime.log.track_location is true.
     * @return
     */
    @Override
    public String literal()
    {
        if (literal != null)
        {
            return literal;
        }
        StringBuilder builder = new StringBuilder();
        builder.append('.').append(getMethodName()).append("(...)");

        return literal = builder.toString();
    }


}