| 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.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.io.Filter; |
| import org.apache.velocity.runtime.Renderable; |
| import org.apache.velocity.runtime.RuntimeConstants; |
| import org.apache.velocity.runtime.directive.Block.Reference; |
| import org.apache.velocity.runtime.parser.LogContext; |
| import org.apache.velocity.runtime.parser.Parser; |
| import org.apache.velocity.runtime.parser.Token; |
| import org.apache.velocity.util.ClassUtils; |
| import org.apache.velocity.util.DuckType; |
| import org.apache.velocity.util.StringUtils; |
| import org.apache.velocity.util.introspection.Info; |
| import org.apache.velocity.util.introspection.VelMethod; |
| import org.apache.velocity.util.introspection.VelPropertySet; |
| |
| import java.io.IOException; |
| import java.io.Writer; |
| import java.lang.reflect.InvocationTargetException; |
| |
| /** |
| * This class is responsible for handling the references in |
| * VTL ($foo). |
| * |
| * 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> |
| * @author <a href="mailto:Christoph.Reck@dlr.de">Christoph Reck</a> |
| * @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a> |
| * @version $Id$ |
| */ |
| public class ASTReference extends SimpleNode |
| { |
| /* Reference types */ |
| private static final int NORMAL_REFERENCE = 1; |
| private static final int FORMAL_REFERENCE = 2; |
| private static final int QUIET_REFERENCE = 3; |
| private static final int RUNT = 4; |
| |
| private int referenceType; |
| private String nullString; |
| private String rootString; |
| private boolean escaped = false; |
| private boolean computableReference = true; |
| private boolean logOnNull = true; |
| private boolean lookupAlternateLiteral = false; |
| private String escPrefix = ""; |
| private String morePrefix = ""; |
| private String identifier = ""; |
| |
| private boolean checkEmpty; |
| |
| private String literal = null; |
| |
| /** |
| * Indicates if we are running in strict reference mode. |
| */ |
| public boolean strictRef = false; |
| |
| /** |
| * non null Indicates if we are setting an index reference e.g, $foo[2], which basically |
| * means that the last syntax of the reference are brackets. |
| */ |
| private ASTIndex astIndex = null; |
| |
| /** |
| * non null Indicates that an alternate value has been provided |
| */ |
| private ASTExpression astAlternateValue = null; |
| |
| /** |
| * Indicates if we are using modified escape behavior in strict mode. |
| * mainly we allow \$abc -> to render as $abc |
| */ |
| public boolean strictEscape = false; |
| |
| private int numChildren = 0; |
| |
| /** |
| * Whether to trigger an event for invalid quiet references |
| * @since 2.2 |
| */ |
| private boolean warnInvalidQuietReferences = false; |
| |
| /** |
| * Whether to trigger an event for invalid null references, that is when a value |
| * is present in the context or parent object but is null |
| * @since 2.2 |
| */ |
| private boolean warnInvalidNullReferences = false; |
| |
| /** |
| * Whether to trigger an event for invalid tested references - as in #if($foo) |
| * @since 2.2 |
| */ |
| private boolean warnInvalidTestedReferences = false; |
| |
| protected Info uberInfo; |
| |
| /** |
| * @param id |
| */ |
| public ASTReference(int id) |
| { |
| super(id); |
| } |
| |
| /** |
| * @param p |
| * @param id |
| */ |
| public ASTReference(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) |
| */ |
| public Object jjtAccept(ParserVisitor visitor, Object data) |
| { |
| return visitor.visit(this, data); |
| } |
| |
| /** |
| * @see org.apache.velocity.runtime.parser.node.SimpleNode#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object) |
| */ |
| public Object init(InternalContextAdapter context, Object data) |
| throws TemplateInitException |
| { |
| super.init(context, data); |
| |
| strictEscape = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT_ESCAPE, false); |
| strictRef = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false); |
| lookupAlternateLiteral = rsvc.getBoolean(RuntimeConstants.VM_PRESERVE_ARGUMENTS_LITERALS, false); |
| |
| /* |
| * the only thing we can do in init() is getRoot() |
| * as that is template based, not context based, |
| * so it's thread- and context-safe |
| */ |
| |
| rootString = rsvc.useStringInterning() ? getRoot().intern() : getRoot(); |
| |
| numChildren = jjtGetNumChildren(); |
| |
| // This is an expensive call, so get it now. |
| literal = literal(); |
| |
| /* |
| * and if appropriate... |
| */ |
| if (numChildren > 0 ) |
| { |
| Node lastNode = jjtGetChild(numChildren-1); |
| if (lastNode instanceof ASTIndex) |
| { |
| /* |
| * only used in SetValue, where alternate value is forbidden |
| */ |
| astIndex = (ASTIndex) lastNode; |
| } |
| else if (lastNode instanceof ASTExpression) |
| { |
| astAlternateValue = (ASTExpression) lastNode; |
| --numChildren; |
| } |
| else |
| { |
| identifier = lastNode.getFirstTokenImage(); |
| } |
| } |
| |
| |
| /* |
| * make an uberinfo - saves new's later on |
| */ |
| uberInfo = new Info(getTemplateName(), getLine(),getColumn()); |
| |
| /* |
| * track whether we log invalid references |
| */ |
| logOnNull = |
| rsvc.getBoolean(RuntimeConstants.RUNTIME_LOG_REFERENCE_LOG_INVALID, true); |
| |
| /* |
| * whether to check for emptiness when evaluatingnumChildren |
| */ |
| checkEmpty = |
| rsvc.getBoolean(RuntimeConstants.CHECK_EMPTY_OBJECTS, true); |
| |
| /* invalid references special cases */ |
| |
| warnInvalidQuietReferences = |
| rsvc.getBoolean(RuntimeConstants.EVENTHANDLER_INVALIDREFERENCES_QUIET, false); |
| warnInvalidNullReferences = |
| rsvc.getBoolean(RuntimeConstants.EVENTHANDLER_INVALIDREFERENCES_NULL, false); |
| warnInvalidTestedReferences = |
| rsvc.getBoolean(RuntimeConstants.EVENTHANDLER_INVALIDREFERENCES_TESTED, false); |
| |
| |
| /** |
| * In the case we are referencing a variable with #if($foo) or |
| * #if( ! $foo) then we allow variables to be undefined and we |
| * set strictRef to false so that if the variable is undefined |
| * an exception is not thrown. |
| */ |
| if (strictRef && numChildren == 0) |
| { |
| logOnNull = false; // Strict mode allows nulls |
| |
| Node node = this.jjtGetParent(); |
| if (node instanceof ASTNotNode // #if( ! $foo) |
| || node instanceof ASTExpression // #if( $foo ) |
| || node instanceof ASTOrNode // #if( $foo || ... |
| || node instanceof ASTAndNode) // #if( $foo && ... |
| { |
| // Now scan up tree to see if we are in an If statement |
| while (node != null) |
| { |
| if (node instanceof ASTIfStatement) |
| { |
| strictRef = false; |
| break; |
| } |
| node = node.jjtGetParent(); |
| } |
| } |
| } |
| saveTokenImages(); |
| cleanupParserAndTokens(); |
| |
| return data; |
| } |
| |
| /** |
| * Returns the 'root string', the reference key |
| * @return the root string. |
| */ |
| public String getRootString() |
| { |
| return rootString; |
| } |
| |
| /** |
| * gets an Object that 'is' the value of the reference |
| * |
| * @param o Object parameter, unused per se, but non-null by convention inside an #if/#elseif evaluation |
| * @param context context used to generate value |
| * @return The execution result. |
| * @throws MethodInvocationException |
| */ |
| public Object execute(Object o, InternalContextAdapter context) |
| throws MethodInvocationException |
| { |
| try |
| { |
| rsvc.getLogContext().pushLogContext(this, uberInfo); |
| |
| /* |
| * The only case where 'o' is not null is when this method is called by evaluate(). |
| * Its value is not used, but it is a convention meant to allow statements like |
| * #if($invalidReference) *not* to trigger an invalid reference event. |
| * Statements like #if($invalidReference.prop) should *still* trigger an invalid reference event. |
| * Statements like #if($validReference.invalidProp) should not. |
| */ |
| boolean onlyTestingReference = (o != null); |
| |
| if (referenceType == RUNT) |
| return null; |
| |
| /* |
| * get the root object from the context |
| */ |
| |
| Object result = getRootVariableValue(context); |
| |
| /* a reference which has been provided an alternate value |
| * is *knowingly* potentially null and should be accepted |
| * in strict mode (except if the alternate value is null) |
| */ |
| if (astAlternateValue != null && (result == null || !DuckType.asBoolean(result, false))) |
| { |
| result = astAlternateValue.value(context); |
| } |
| |
| if (result == null && !strictRef) |
| { |
| /* |
| * do not trigger an invalid reference if the reference is present, but with a null value |
| * don't either for a quiet reference or inside an #if/#elseif evaluation context |
| */ |
| if ((referenceType != QUIET_REFERENCE || warnInvalidQuietReferences) && |
| (numChildren > 0 || |
| (!context.containsKey(rootString) || warnInvalidNullReferences) && |
| (!onlyTestingReference || warnInvalidTestedReferences))) |
| { |
| result = EventHandlerUtil.invalidGetMethod(rsvc, context, |
| "$" + rootString, null, null, uberInfo); |
| } |
| |
| return result; |
| } |
| |
| /* |
| * Iteratively work 'down' (it's flat...) the reference |
| * to get the value, but check to make sure that |
| * every result along the path is valid. For example: |
| * |
| * $hashtable.Customer.Name |
| * |
| * The $hashtable may be valid, but there is no key |
| * 'Customer' in the hashtable so we want to stop |
| * when we find a null value and return the null |
| * so the error gets logged. |
| */ |
| |
| try |
| { |
| Object previousResult = result; |
| int failedChild = -1; |
| |
| for (int i = 0; i < numChildren; i++) |
| { |
| if (strictRef && result == null) |
| { |
| /** |
| * At this point we know that an attempt is about to be made |
| * to call a method or property on a null value. |
| */ |
| String name = jjtGetChild(i).getFirstTokenImage(); |
| throw new VelocityException("Attempted to access '" |
| + name + "' on a null value at " |
| + StringUtils.formatFileString(uberInfo.getTemplateName(), |
| + jjtGetChild(i).getLine(), jjtGetChild(i).getColumn())); |
| } |
| previousResult = result; |
| result = jjtGetChild(i).execute(result,context); |
| if (astAlternateValue != null && (result == null || !DuckType.asBoolean(result, checkEmpty))) |
| { |
| result = astAlternateValue.value(context); |
| } |
| if (result == null && !strictRef) // If strict and null then well catch this |
| // next time through the loop |
| { |
| failedChild = i; |
| break; |
| } |
| } |
| |
| if (result == null) |
| { |
| if (failedChild == -1) |
| { |
| /* |
| * do not trigger an invalid reference if the reference is present, but with a null value |
| * don't either for a quiet reference, |
| * or inside an #if/#elseif evaluation context when there's no child |
| */ |
| if ((!context.containsKey(rootString) || warnInvalidNullReferences) && |
| (referenceType != QUIET_REFERENCE || warnInvalidQuietReferences) && |
| (!onlyTestingReference || warnInvalidTestedReferences || numChildren > 0)) |
| { |
| result = EventHandlerUtil.invalidGetMethod(rsvc, context, |
| "$" + rootString, previousResult, null, uberInfo); |
| } |
| } |
| else |
| { |
| Node child = jjtGetChild(failedChild); |
| // do not call bad reference handler if the getter is present |
| // (it means the getter has been called and returned null) |
| // do not either for a quiet reference or if the *last* child failed while testing the reference |
| Object getter = context.icacheGet(child); |
| if ((getter == null || warnInvalidNullReferences) && |
| (referenceType != QUIET_REFERENCE || warnInvalidQuietReferences) && |
| (!onlyTestingReference || warnInvalidTestedReferences || failedChild < numChildren - 1)) |
| { |
| StringBuilder name = new StringBuilder("$").append(rootString); |
| for (int i = 0; i <= failedChild; i++) |
| { |
| Node node = jjtGetChild(i); |
| if (node instanceof ASTMethod) |
| { |
| name.append(".").append(((ASTMethod) node).getMethodName()).append("()"); |
| } |
| else |
| { |
| name.append(".").append(node.getFirstTokenImage()); |
| } |
| } |
| |
| if (child instanceof ASTMethod) |
| { |
| String methodName = ((ASTMethod) jjtGetChild(failedChild)).getMethodName(); |
| result = EventHandlerUtil.invalidMethod(rsvc, context, |
| name.toString(), previousResult, methodName, uberInfo); |
| } |
| else |
| { |
| String property = jjtGetChild(failedChild).getFirstTokenImage(); |
| result = EventHandlerUtil.invalidGetMethod(rsvc, context, |
| name.toString(), previousResult, property, uberInfo); |
| } |
| } |
| } |
| } |
| |
| return result; |
| } |
| catch(MethodInvocationException mie) |
| { |
| mie.setReferenceName(rootString); |
| throw mie; |
| } |
| } |
| finally |
| { |
| rsvc.getLogContext().popLogContext(); |
| } |
| } |
| |
| /** |
| * gets the value of the reference and outputs it to the |
| * writer. |
| * |
| * @param context context of data to use in getting value |
| * @param writer writer to render to |
| * @return True if rendering was successful. |
| * @throws IOException |
| * @throws MethodInvocationException |
| */ |
| public boolean render(InternalContextAdapter context, Writer writer) throws IOException, |
| MethodInvocationException |
| { |
| try |
| { |
| rsvc.getLogContext().pushLogContext(this, uberInfo); |
| |
| if (referenceType == RUNT) |
| { |
| writer.write(literal); |
| return true; |
| } |
| |
| Object value = null; |
| if (escaped && strictEscape) |
| { |
| /** |
| * If we are in strict mode and the variable is escaped, then don't bother to |
| * retrieve the value since we won't use it. And if the var is not defined |
| * it will throw an exception. Set value to TRUE to fall through below with |
| * simply printing $foo, and not \$foo |
| */ |
| value = Boolean.TRUE; |
| } |
| else |
| { |
| value = execute(null, context); |
| } |
| |
| String localNullString = null; |
| |
| /* |
| * if this reference is escaped (\$foo) then we want to do one of two things: 1) if this is |
| * a reference in the context, then we want to print $foo 2) if not, then \$foo (its |
| * considered schmoo, not VTL) |
| */ |
| |
| if (escaped) |
| { |
| localNullString = getNullString(context); |
| |
| if (value == null) |
| { |
| writer.write(escPrefix); |
| writer.write("\\"); |
| writer.write(localNullString); |
| } |
| else |
| { |
| writer.write(escPrefix); |
| writer.write(localNullString); |
| } |
| return true; |
| } |
| |
| /* |
| * the normal processing |
| * |
| * if we have an event cartridge, get a new value object |
| */ |
| |
| value = EventHandlerUtil.referenceInsert(rsvc, context, literal, value); |
| |
| String toString = null; |
| if (value != null) |
| { |
| if (value instanceof Renderable) |
| { |
| Renderable renderable = (Renderable)value; |
| try |
| { |
| writer.write(escPrefix); |
| writer.write(morePrefix); |
| if (renderable.render(context,writer)) |
| { |
| return true; |
| } |
| } |
| catch(RuntimeException e) |
| { |
| // We commonly get here when an error occurs within a block reference. |
| // We want to log where the reference is at so that a developer can easily |
| // know where the offending call is located. This can be seen |
| // as another element of the error stack we report to log. |
| log.error("Exception rendering " |
| + ((renderable instanceof Reference)? "block ":"Renderable ") |
| + rootString + " at " + StringUtils.formatFileString(this)); |
| throw e; |
| } |
| } |
| |
| toString = DuckType.asString(value); |
| } |
| |
| if (value == null || toString == null) |
| { |
| if (strictRef) |
| { |
| if (referenceType != QUIET_REFERENCE) |
| { |
| log.error("Prepend the reference with '$!' e.g., $!{}" + |
| " if you want Velocity to ignore the reference when it evaluates to null", |
| literal().substring(1)); |
| if (value == null) |
| { |
| throw new VelocityException("Reference " + literal() |
| + " evaluated to null when attempting to render at " |
| + StringUtils.formatFileString(this)); |
| } |
| else // toString == null |
| { |
| // This will probably rarely happen, but when it does we want to |
| // inform the user that toString == null so they don't pull there |
| // hair out wondering why Velocity thinks the value is null. |
| throw new VelocityException("Reference " + literal() |
| + " evaluated to object " + value.getClass().getName() |
| + " whose toString() method returned null at " |
| + StringUtils.formatFileString(this)); |
| } |
| } |
| return true; |
| } |
| |
| /* |
| * write prefix twice, because it's schmoo, so the \ don't escape each |
| * other... |
| */ |
| localNullString = getNullString(context); |
| if (!strictEscape) |
| { |
| // If in strict escape mode then we only print escape once. |
| // Yea, I know.. brittle stuff |
| writer.write(escPrefix); |
| } |
| writer.write(escPrefix); |
| writer.write(morePrefix); |
| writer.write(localNullString); |
| |
| if (logOnNull && referenceType != QUIET_REFERENCE) |
| { |
| log.debug("Null reference [template '{}', line {}, column {}]: {} cannot be resolved.", |
| getTemplateName(), this.getLine(), this.getColumn(), this.literal()); |
| } |
| return true; |
| } |
| else |
| { |
| /* |
| * non-null processing |
| */ |
| writer.write(escPrefix); |
| writer.write(morePrefix); |
| if (writer instanceof Filter) |
| { |
| ((Filter)writer).writeReference(toString); |
| } |
| else |
| { |
| writer.write(toString); |
| } |
| |
| return true; |
| } |
| } |
| finally |
| { |
| rsvc.getLogContext().popLogContext(); |
| } |
| } |
| |
| /** |
| * This method helps to implement the "render literal if null" functionality. |
| * |
| * VelocimacroProxy saves references to macro arguments (AST nodes) so that if we have a macro |
| * #foobar($a $b) then there is key "$a.literal" which points to the literal presentation of the |
| * argument provided to variable $a. If the value of $a is null, we render the string that was |
| * provided as the argument. |
| * |
| * @param context |
| * @return |
| */ |
| private String getNullString(InternalContextAdapter context) |
| { |
| String ret = nullString; |
| |
| if (lookupAlternateLiteral) |
| { |
| Node callingArgument = (Node)context.get(".literal." + nullString); |
| if (callingArgument != null) |
| { |
| ret = ((Node) callingArgument).literal(); |
| } |
| } |
| return ret; |
| } |
| |
| /** |
| * Computes boolean value of this reference |
| * Returns the actual value of reference return type |
| * boolean, and 'true' if value is not null |
| * |
| * @param context context to compute value with |
| * @return True if evaluation was ok. |
| * @throws MethodInvocationException |
| */ |
| public boolean evaluate(InternalContextAdapter context) |
| throws MethodInvocationException |
| { |
| Object value = execute(this, context); // non-null object as first parameter by convention for 'evaluate' |
| if (value == null) |
| { |
| return false; |
| } |
| try |
| { |
| rsvc.getLogContext().pushLogContext(this, uberInfo); |
| return DuckType.asBoolean(value, checkEmpty); |
| } |
| catch(Exception e) |
| { |
| throw new VelocityException("Reference evaluation threw an exception at " |
| + StringUtils.formatFileString(this), e); |
| } |
| finally |
| { |
| rsvc.getLogContext().popLogContext(); |
| } |
| } |
| |
| /** |
| * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter) |
| */ |
| public Object value(InternalContextAdapter context) |
| throws MethodInvocationException |
| { |
| return (computableReference ? execute(null, context) : null); |
| } |
| |
| |
| /** |
| * Utility class to handle nulls when printing a class type |
| * @param clazz |
| * @return class name, or the string "null" |
| */ |
| public static String printClass(Class clazz) |
| { |
| return clazz == null ? "null" : clazz.getName(); |
| } |
| |
| |
| /** |
| * Sets the value of a complex reference (something like $foo.bar) |
| * Currently used by ASTSetReference() |
| * |
| * @see ASTSetDirective |
| * |
| * @param context context object containing this reference |
| * @param value Object to set as value |
| * @return true if successful, false otherwise |
| * @throws MethodInvocationException |
| */ |
| public boolean setValue(InternalContextAdapter context, Object value) |
| throws MethodInvocationException |
| { |
| try |
| { |
| rsvc.getLogContext().pushLogContext(this, uberInfo); |
| |
| if (astAlternateValue != null) |
| { |
| log.error("reference set cannot have a default value {}", |
| StringUtils.formatFileString(uberInfo)); |
| return false; |
| } |
| |
| if (numChildren == 0) |
| { |
| context.put(rootString, value); |
| return true; |
| } |
| |
| /* |
| * The rootOfIntrospection is the object we will |
| * retrieve from the Context. This is the base |
| * object we will apply reflection to. |
| */ |
| |
| Object result = getRootVariableValue(context); |
| |
| if (result == null) |
| { |
| log.error("reference set is not a valid reference at {}", |
| StringUtils.formatFileString(uberInfo)); |
| return false; |
| } |
| |
| /* |
| * How many child nodes do we have? |
| */ |
| |
| for (int i = 0; i < numChildren - 1; i++) |
| { |
| result = jjtGetChild(i).execute(result, context); |
| |
| if (result == null) |
| { |
| if (strictRef) |
| { |
| String name = jjtGetChild(i+1).getFirstTokenImage(); |
| throw new MethodInvocationException("Attempted to access '" |
| + name + "' on a null value", null, name, uberInfo.getTemplateName(), |
| jjtGetChild(i+1).getLine(), jjtGetChild(i+1).getColumn()); |
| } |
| |
| log.error("reference set is not a valid reference at {}", |
| StringUtils.formatFileString(uberInfo)); |
| return false; |
| } |
| } |
| |
| if (astIndex != null) |
| { |
| // If astIndex is not null then we are actually setting an index reference, |
| // something of the form $foo[1] =, or in general any reference that ends with |
| // the brackets. This means that we need to call a more general method |
| // of the form set(Integer, <something>), or put(Object, <something), where |
| // the first parameter is the index value and the second is the LHS of the set. |
| |
| Object argument = astIndex.jjtGetChild(0).value(context); |
| // If negative, turn -1 into (size - 1) |
| argument = ASTIndex.adjMinusIndexArg(argument, result, context, astIndex); |
| Object [] params = {argument, value}; |
| Class[] paramClasses = {params[0] == null ? null : params[0].getClass(), |
| params[1] == null ? null : params[1].getClass()}; |
| |
| String methodName = "set"; |
| VelMethod method = ClassUtils.getMethod(methodName, params, paramClasses, |
| result, context, astIndex, false); |
| |
| if (method == null) |
| { |
| // If we can't find a 'set' method, lets try 'put', This warrents a little |
| // investigation performance wise... if the user is using the hash |
| // form $foo["blaa"], then it may be expensive to first try and fail on 'set' |
| // then go to 'put'? The problem is that getMethod will try the cache, then |
| // perform introspection on 'result' for 'set' |
| methodName = "put"; |
| method = ClassUtils.getMethod(methodName, params, paramClasses, |
| result, context, astIndex, false); |
| } |
| |
| if (method == null) |
| { |
| // couldn't find set or put method, so bail |
| if (strictRef) |
| { |
| throw new VelocityException( |
| "Found neither a 'set' or 'put' method with param types '(" |
| + printClass(paramClasses[0]) + "," + printClass(paramClasses[1]) |
| + ")' on class '" + result.getClass().getName() |
| + "' at " + StringUtils.formatFileString(astIndex)); |
| } |
| return false; |
| } |
| |
| try |
| { |
| method.invoke(result, params); |
| } |
| catch(RuntimeException e) |
| { |
| // Kludge since invoke throws Exception, pass up Runtimes |
| throw e; |
| } |
| catch(Exception e) |
| { |
| throw new MethodInvocationException( |
| "Exception calling method '" |
| + methodName + "(" |
| + printClass(paramClasses[0]) + "," + printClass(paramClasses[1]) |
| + ")' in " + result.getClass(), |
| e.getCause(), identifier, astIndex.getTemplateName(), astIndex.getLine(), |
| astIndex.getColumn()); |
| } |
| |
| return true; |
| } |
| |
| |
| /* |
| * We support two ways of setting the value in a #set($ref.foo = $value ): |
| * 1) ref.setFoo( value ) |
| * 2) ref,put("foo", value ) to parallel the get() map introspection |
| */ |
| |
| try |
| { |
| VelPropertySet vs = |
| rsvc.getUberspect().getPropertySet(result, identifier, |
| value, uberInfo); |
| |
| if (vs == null) |
| { |
| if (strictRef) |
| { |
| throw new MethodInvocationException("Object '" + result.getClass().getName() + |
| "' does not contain property '" + identifier + "'", null, identifier, |
| uberInfo.getTemplateName(), uberInfo.getLine(), uberInfo.getColumn()); |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| vs.invoke(result, value); |
| } |
| catch(InvocationTargetException ite) |
| { |
| /* |
| * this is possible |
| */ |
| |
| throw new MethodInvocationException( |
| "ASTReference: Invocation of method '" |
| + identifier + "' in " + result.getClass() |
| + " threw exception " |
| + ite.getTargetException().toString(), |
| ite.getTargetException(), identifier, getTemplateName(), this.getLine(), this.getColumn()); |
| } |
| /** |
| * pass through application level runtime exceptions |
| */ |
| catch( RuntimeException e ) |
| { |
| throw e; |
| } |
| catch(Exception e) |
| { |
| /* |
| * maybe a security exception? |
| */ |
| String msg = "ASTReference setValue(): exception: " + e |
| + " template at " + StringUtils.formatFileString(uberInfo); |
| log.error(msg, e); |
| throw new VelocityException(msg, e); |
| } |
| |
| return true; |
| } |
| finally |
| { |
| rsvc.getLogContext().popLogContext(); |
| } |
| } |
| |
| private String getRoot() |
| { |
| Token t = getFirstToken(); |
| |
| /* |
| * we have a special case where something like |
| * $(\\)*!, where the user want's to see something |
| * like $!blargh in the output, but the ! prevents it from showing. |
| * I think that at this point, this isn't a reference. |
| */ |
| |
| /* so, see if we have "\\!" */ |
| |
| int slashbang = t.image.indexOf("\\!"); |
| |
| if (slashbang != -1) |
| { |
| if (strictEscape) |
| { |
| // If we are in strict escape mode, then we consider this type of |
| // pattern a non-reference, and we print it out as schmoo... |
| nullString = literal(); |
| escaped = true; |
| return nullString; |
| } |
| |
| /* |
| * lets do all the work here. I would argue that if this occurs, |
| * it's not a reference at all, so preceding \ characters in front |
| * of the $ are just schmoo. So we just do the escape processing |
| * trick (even | odd) and move on. This kind of breaks the rule |
| * pattern of $ and # but '!' really tosses a wrench into things. |
| */ |
| |
| /* |
| * count the escapes: even # -> not escaped, odd -> escaped |
| */ |
| |
| int i = 0; |
| int len = t.image.length(); |
| |
| i = t.image.indexOf('$'); |
| |
| if (i == -1) |
| { |
| /* yikes! */ |
| log.error("ASTReference.getRoot(): internal error: " |
| + "no $ found for slashbang."); |
| computableReference = false; |
| nullString = t.image; |
| return nullString; |
| } |
| |
| while (i < len && t.image.charAt(i) != '\\') |
| { |
| i++; |
| } |
| |
| /* ok, i is the first \ char */ |
| |
| int start = i; |
| int count = 0; |
| |
| while (i < len && t.image.charAt(i++) == '\\') |
| { |
| count++; |
| } |
| |
| /* |
| * now construct the output string. We really don't care about |
| * leading slashes as this is not a reference. It's quasi-schmoo |
| */ |
| |
| nullString = t.image.substring(0,start); // prefix up to the first |
| nullString += t.image.substring(start, start + count-1 ); // get the slashes |
| nullString += t.image.substring(start+count); // and the rest, including the |
| |
| /* |
| * this isn't a valid reference, so lets short circuit the value |
| * and set calcs |
| */ |
| |
| computableReference = false; |
| |
| return nullString; |
| } |
| |
| /* |
| * we need to see if this reference is escaped. if so |
| * we will clean off the leading \'s and let the |
| * regular behavior determine if we should output this |
| * as \$foo or $foo later on in render(). Laziness.. |
| */ |
| |
| escaped = false; |
| |
| if (t.image.startsWith("\\")) |
| { |
| /* |
| * count the escapes: even # -> not escaped, odd -> escaped |
| */ |
| |
| int i = 0; |
| int len = t.image.length(); |
| |
| while (i < len && t.image.charAt(i) == '\\') |
| { |
| i++; |
| } |
| |
| if ((i % 2) != 0) |
| escaped = true; |
| |
| if (i > 0) |
| escPrefix = t.image.substring(0, i / 2 ); |
| |
| t.image = t.image.substring(i); |
| } |
| |
| /* |
| * Look for preceding stuff like '#' and '$' |
| * and snip it off, except for the |
| * last $ |
| */ |
| |
| int loc1 = t.image.lastIndexOf('$'); |
| |
| /* |
| * if we have extra stuff, loc > 0 |
| * ex. '#$foo' so attach that to |
| * the prefix. |
| */ |
| if (loc1 > 0) |
| { |
| morePrefix = morePrefix + t.image.substring(0, loc1); |
| t.image = t.image.substring(loc1); |
| } |
| |
| /* |
| * Now it should be clean. Get the literal in case this reference |
| * isn't backed by the context at runtime, and then figure out what |
| * we are working with. |
| */ |
| |
| // FIXME: this is the key to render nulls as literals, we need to look at context(refname+".literal") |
| nullString = literal(); |
| |
| if (t.image.startsWith("$!")) |
| { |
| referenceType = QUIET_REFERENCE; |
| |
| /* |
| * only if we aren't escaped do we want to null the output |
| */ |
| |
| if (!escaped) |
| nullString = ""; |
| |
| if (t.image.startsWith("$!{")) |
| { |
| /* |
| * ex: $!{provider.Title} |
| */ |
| |
| return t.next.image; |
| } |
| else |
| { |
| /* |
| * ex: $!provider.Title |
| */ |
| |
| return t.image.substring(2); |
| } |
| } |
| else if (t.image.equals("${")) |
| { |
| /* |
| * ex: ${provider.Title} |
| */ |
| |
| referenceType = FORMAL_REFERENCE; |
| return t.next.image; |
| } |
| else if (t.image.startsWith("$")) |
| { |
| /* |
| * just nip off the '$' so we have |
| * the root |
| */ |
| |
| referenceType = NORMAL_REFERENCE; |
| return t.image.substring(1); |
| } |
| else |
| { |
| /* |
| * this is a 'RUNT', which can happen in certain circumstances where |
| * the parser is fooled into believing that an IDENTIFIER is a real |
| * reference. Another 'dreaded' MORE hack :). |
| */ |
| referenceType = RUNT; |
| return t.image; |
| } |
| |
| } |
| |
| /** |
| * @param context |
| * @return The evaluated value of the variable. |
| * @throws MethodInvocationException |
| */ |
| public Object getRootVariableValue(InternalContextAdapter context) |
| { |
| Object obj = null; |
| try |
| { |
| obj = context.get(rootString); |
| } |
| catch(RuntimeException e) |
| { |
| log.error("Exception calling reference ${} at {}", |
| rootString, StringUtils.formatFileString(uberInfo)); |
| throw e; |
| } |
| |
| if (obj == null && strictRef && astAlternateValue == null) |
| { |
| if (!context.containsKey(rootString)) |
| { |
| log.error("Variable ${} has not been set at {}", |
| rootString, StringUtils.formatFileString(uberInfo)); |
| throw new MethodInvocationException("Variable $" + rootString + |
| " has not been set", null, identifier, |
| uberInfo.getTemplateName(), uberInfo.getLine(), uberInfo.getColumn()); |
| } |
| } |
| return obj; |
| } |
| } |