aboutsummaryrefslogtreecommitdiff
path: root/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMethod.java
diff options
context:
space:
mode:
Diffstat (limited to 'velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMethod.java')
-rw-r--r--velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMethod.java459
1 files changed, 459 insertions, 0 deletions
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMethod.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMethod.java
new file mode 100644
index 00000000..5696b6bb
--- /dev/null
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/parser/node/ASTMethod.java
@@ -0,0 +1,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();
+ }
+
+
+}