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 org.apache.velocity.app.event.EventHandlerUtil; 23 import org.apache.velocity.context.InternalContextAdapter; 24 import org.apache.velocity.exception.MethodInvocationException; 25 import org.apache.velocity.exception.TemplateInitException; 26 import org.apache.velocity.exception.VelocityException; 27 import org.apache.velocity.io.Filter; 28 import org.apache.velocity.runtime.Renderable; 29 import org.apache.velocity.runtime.RuntimeConstants; 30 import org.apache.velocity.runtime.directive.Block.Reference; 31 import org.apache.velocity.runtime.parser.Parser; 32 import org.apache.velocity.runtime.parser.Token; 33 import org.apache.velocity.util.ClassUtils; 34 import org.apache.velocity.util.DuckType; 35 import org.apache.velocity.util.StringUtils; 36 import org.apache.velocity.util.introspection.Info; 37 import org.apache.velocity.util.introspection.VelMethod; 38 import org.apache.velocity.util.introspection.VelPropertySet; 39 40 import java.io.IOException; 41 import java.io.Writer; 42 import java.lang.reflect.InvocationTargetException; 43 import java.util.Deque; 44 45 /** 46 * This class is responsible for handling the references in 47 * VTL ($foo). 48 * 49 * Please look at the Parser.jjt file which is 50 * what controls the generation of this class. 51 * 52 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a> 53 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a> 54 * @author <a href="mailto:Christoph.Reck@dlr.de">Christoph Reck</a> 55 * @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a> 56 * @version $Id$ 57 */ 58 public class ASTReference extends SimpleNode 59 { 60 /* Reference types */ 61 private static final int NORMAL_REFERENCE = 1; 62 private static final int FORMAL_REFERENCE = 2; 63 private static final int QUIET_REFERENCE = 3; 64 private static final int RUNT = 4; 65 66 private int referenceType; 67 private String nullString; 68 private String alternateNullStringKey; 69 private String rootString; 70 private boolean escaped = false; 71 private boolean computableReference = true; 72 private boolean logOnNull = true; 73 private boolean lookupAlternateLiteral = false; 74 private String escPrefix = ""; 75 private String morePrefix = ""; 76 private String identifier = ""; 77 78 private boolean checkEmpty; 79 80 /** 81 * Indicates if we are running in strict reference mode. 82 */ 83 public boolean strictRef = false; 84 85 /** 86 * non null Indicates if we are setting an index reference e.g, $foo[2], which basically 87 * means that the last syntax of the reference are brackets. 88 */ 89 private ASTIndex astIndex = null; 90 91 /** 92 * non null Indicates that an alternate value has been provided 93 */ 94 private ASTExpression astAlternateValue = null; 95 96 /** 97 * Indicates if we are using modified escape behavior in strict mode. 98 * mainly we allow \$abc -> to render as $abc 99 */ 100 public boolean strictEscape = false; 101 102 private int numChildren = 0; 103 104 /** 105 * Whether to trigger an event for invalid quiet references 106 * @since 2.2 107 */ 108 private boolean warnInvalidQuietReferences = false; 109 110 /** 111 * Whether to trigger an event for invalid null references, that is when a value 112 * is present in the context or parent object but is null 113 * @since 2.2 114 */ 115 private boolean warnInvalidNullReferences = false; 116 117 /** 118 * Whether to trigger an event for invalid tested references - as in #if($foo) 119 * @since 2.2 120 */ 121 private boolean warnInvalidTestedReferences = false; 122 123 protected Info uberInfo; 124 125 /** 126 * @param id 127 */ ASTReference(int id)128 public ASTReference(int id) 129 { 130 super(id); 131 } 132 133 /** 134 * @param p 135 * @param id 136 */ ASTReference(Parser p, int id)137 public ASTReference(Parser p, int id) 138 { 139 super(p, id); 140 } 141 142 /** 143 * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object) 144 */ 145 @Override jjtAccept(ParserVisitor visitor, Object data)146 public Object jjtAccept(ParserVisitor visitor, Object data) 147 { 148 return visitor.visit(this, data); 149 } 150 151 /** 152 * @see org.apache.velocity.runtime.parser.node.SimpleNode#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object) 153 */ 154 @Override init(InternalContextAdapter context, Object data)155 public Object init(InternalContextAdapter context, Object data) 156 throws TemplateInitException 157 { 158 super.init(context, data); 159 160 strictEscape = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT_ESCAPE, false); 161 strictRef = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false); 162 lookupAlternateLiteral = rsvc.getBoolean(RuntimeConstants.VM_ENABLE_BC_MODE, false); 163 164 /* 165 * the only thing we can do in init() is getRoot() 166 * as that is template based, not context based, 167 * so it's thread- and context-safe 168 */ 169 170 rootString = rsvc.useStringInterning() ? getRoot().intern() : getRoot(); 171 if (lookupAlternateLiteral) 172 { 173 /* cache alternate null tring key */ 174 alternateNullStringKey = ".literal." + nullString; 175 } 176 177 numChildren = jjtGetNumChildren(); 178 179 // This is an expensive call, so get it now. 180 literal(); 181 182 /* 183 * and if appropriate... 184 */ 185 if (numChildren > 0 ) 186 { 187 Node lastNode = jjtGetChild(numChildren-1); 188 if (lastNode instanceof ASTIndex) 189 { 190 /* 191 * only used in SetValue, where alternate value is forbidden 192 */ 193 astIndex = (ASTIndex) lastNode; 194 } 195 else if (lastNode instanceof ASTExpression) 196 { 197 astAlternateValue = (ASTExpression) lastNode; 198 --numChildren; 199 } 200 else 201 { 202 identifier = lastNode.getFirstTokenImage(); 203 } 204 } 205 206 207 /* 208 * make an uberinfo - saves new's later on 209 */ 210 uberInfo = new Info(getTemplateName(), getLine(),getColumn()); 211 212 /* 213 * track whether we log invalid references 214 */ 215 logOnNull = 216 rsvc.getBoolean(RuntimeConstants.RUNTIME_LOG_REFERENCE_LOG_INVALID, true); 217 218 /* 219 * whether to check for emptiness when evaluatingnumChildren 220 */ 221 checkEmpty = 222 rsvc.getBoolean(RuntimeConstants.CHECK_EMPTY_OBJECTS, true); 223 224 /* invalid references special cases */ 225 226 warnInvalidQuietReferences = 227 rsvc.getBoolean(RuntimeConstants.EVENTHANDLER_INVALIDREFERENCES_QUIET, false); 228 warnInvalidNullReferences = 229 rsvc.getBoolean(RuntimeConstants.EVENTHANDLER_INVALIDREFERENCES_NULL, false); 230 warnInvalidTestedReferences = 231 rsvc.getBoolean(RuntimeConstants.EVENTHANDLER_INVALIDREFERENCES_TESTED, false); 232 233 234 /* 235 * In the case we are referencing a variable with #if($foo) or 236 * #if( ! $foo) then we allow variables to be undefined and we 237 * set strictRef to false so that if the variable is undefined 238 * an exception is not thrown. 239 */ 240 if (strictRef && numChildren == 0) 241 { 242 logOnNull = false; // Strict mode allows nulls 243 244 Node node = this.jjtGetParent(); 245 if (node instanceof ASTNotNode // #if( ! $foo) 246 || node instanceof ASTExpression // #if( $foo ) 247 || node instanceof ASTOrNode // #if( $foo || ... 248 || node instanceof ASTAndNode) // #if( $foo && ... 249 { 250 // Now scan up tree to see if we are in an If statement 251 while (node != null) 252 { 253 if (node instanceof ASTIfStatement) 254 { 255 strictRef = false; 256 break; 257 } 258 node = node.jjtGetParent(); 259 } 260 } 261 } 262 saveTokenImages(); 263 cleanupParserAndTokens(); 264 265 return data; 266 } 267 268 /** 269 * Returns the 'root string', the reference key 270 * @return the root string. 271 */ getRootString()272 public String getRootString() 273 { 274 return rootString; 275 } 276 277 /** 278 * gets an Object that 'is' the value of the reference 279 * 280 * @param o Object parameter, unused per se, but non-null by convention inside an #if/#elseif evaluation 281 * @param context context used to generate value 282 * @return The execution result. 283 * @throws MethodInvocationException 284 */ 285 @Override execute(Object o, InternalContextAdapter context)286 public Object execute(Object o, InternalContextAdapter context) 287 throws MethodInvocationException 288 { 289 try 290 { 291 rsvc.getLogContext().pushLogContext(this, uberInfo); 292 293 /* 294 * The only case where 'o' is not null is when this method is called by evaluate(). 295 * Its value is not used, but it is a convention meant to allow statements like 296 * #if($invalidReference) *not* to trigger an invalid reference event. 297 * Statements like #if($invalidReference.prop) should *still* trigger an invalid reference event. 298 * Statements like #if($validReference.invalidProp) should not. 299 */ 300 boolean onlyTestingReference = (o != null); 301 302 if (referenceType == RUNT) 303 return null; 304 305 /* 306 * get the root object from the context 307 */ 308 309 Object result = getRootVariableValue(context); 310 311 if (result == null && !strictRef) 312 { 313 /* 314 * do not trigger an invalid reference if the reference is present, but with a null value 315 * don't either for a quiet reference or inside an #if/#elseif evaluation context 316 */ 317 if ((referenceType != QUIET_REFERENCE || warnInvalidQuietReferences) && 318 (numChildren > 0 || 319 (!context.containsKey(rootString) || warnInvalidNullReferences) && 320 (!onlyTestingReference || warnInvalidTestedReferences))) 321 { 322 result = EventHandlerUtil.invalidGetMethod(rsvc, context, 323 rsvc.getParserConfiguration().getDollarChar() + rootString, null, null, uberInfo); 324 } 325 326 if (astAlternateValue != null && (!DuckType.asBoolean(result, true))) 327 { 328 result = astAlternateValue.value(context); 329 } 330 331 return result; 332 } 333 334 /* 335 * Iteratively work 'down' (it's flat...) the reference 336 * to get the value, but check to make sure that 337 * every result along the path is valid. For example: 338 * 339 * $hashtable.Customer.Name 340 * 341 * The $hashtable may be valid, but there is no key 342 * 'Customer' in the hashtable so we want to stop 343 * when we find a null value and return the null 344 * so the error gets logged. 345 */ 346 347 try 348 { 349 Object previousResult = result; 350 int failedChild = -1; 351 352 for (int i = 0; i < numChildren; i++) 353 { 354 if (strictRef && result == null) 355 { 356 /* 357 * At this point we know that an attempt is about to be made 358 * to call a method or property on a null value. 359 */ 360 String name = jjtGetChild(i).getFirstTokenImage(); 361 throw new VelocityException("Attempted to access '" 362 + name + "' on a null value at " 363 + StringUtils.formatFileString(uberInfo.getTemplateName(), 364 + jjtGetChild(i).getLine(), jjtGetChild(i).getColumn()), 365 null, rsvc.getLogContext().getStackTrace()); 366 } 367 previousResult = result; 368 result = jjtGetChild(i).execute(result,context); 369 if (result == null && !strictRef) // If strict and null then well catch this 370 // next time through the loop 371 { 372 failedChild = i; 373 break; 374 } 375 } 376 377 if (result == null) 378 { 379 if (failedChild == -1) 380 { 381 /* 382 * do not trigger an invalid reference if the reference is present, but with a null value 383 * don't either for a quiet reference, 384 * or inside an #if/#elseif evaluation context when there's no child 385 */ 386 if ((!context.containsKey(rootString) || warnInvalidNullReferences) && 387 (referenceType != QUIET_REFERENCE || warnInvalidQuietReferences) && 388 (!onlyTestingReference || warnInvalidTestedReferences || numChildren > 0)) 389 { 390 result = EventHandlerUtil.invalidGetMethod(rsvc, context, 391 rsvc.getParserConfiguration().getDollarChar() + rootString, previousResult, null, uberInfo); 392 } 393 } 394 else 395 { 396 Node child = jjtGetChild(failedChild); 397 // do not call bad reference handler if the getter is present 398 // (it means the getter has been called and returned null) 399 // do not either for a quiet reference or if the *last* child failed while testing the reference 400 Object getter = context.icacheGet(child); 401 if ((getter == null || warnInvalidNullReferences) && 402 (referenceType != QUIET_REFERENCE || warnInvalidQuietReferences) && 403 (!onlyTestingReference || warnInvalidTestedReferences || failedChild < numChildren - 1)) 404 { 405 StringBuilder name = new StringBuilder(String.valueOf(rsvc.getParserConfiguration().getDollarChar())).append(rootString); 406 for (int i = 0; i <= failedChild; i++) 407 { 408 Node node = jjtGetChild(i); 409 if (node instanceof ASTMethod) 410 { 411 name.append(".").append(((ASTMethod) node).getMethodName()).append("()"); 412 } 413 else 414 { 415 name.append(".").append(node.getFirstTokenImage()); 416 } 417 } 418 419 if (child instanceof ASTMethod) 420 { 421 String methodName = ((ASTMethod) jjtGetChild(failedChild)).getMethodName(); 422 result = EventHandlerUtil.invalidMethod(rsvc, context, 423 name.toString(), previousResult, methodName, uberInfo); 424 } 425 else 426 { 427 String property = jjtGetChild(failedChild).getFirstTokenImage(); 428 result = EventHandlerUtil.invalidGetMethod(rsvc, context, 429 name.toString(), previousResult, property, uberInfo); 430 } 431 } 432 } 433 } 434 435 // Check alternate value at the end of the evaluation 436 if (astAlternateValue != null && (!DuckType.asBoolean(result, true))) 437 { 438 result = astAlternateValue.value(context); 439 } 440 441 return result; 442 } 443 catch(MethodInvocationException mie) 444 { 445 mie.setReferenceName(rootString); 446 throw mie; 447 } 448 } 449 finally 450 { 451 rsvc.getLogContext().popLogContext(); 452 } 453 } 454 455 /** 456 * gets the value of the reference and outputs it to the 457 * writer. 458 * 459 * @param context context of data to use in getting value 460 * @param writer writer to render to 461 * @return True if rendering was successful. 462 * @throws IOException 463 * @throws MethodInvocationException 464 */ 465 @Override render(InternalContextAdapter context, Writer writer)466 public boolean render(InternalContextAdapter context, Writer writer) throws IOException, 467 MethodInvocationException 468 { 469 try 470 { 471 rsvc.getLogContext().pushLogContext(this, uberInfo); 472 473 if (referenceType == RUNT) 474 { 475 writer.write(literal); 476 return true; 477 } 478 479 Object value = null; 480 if (escaped && strictEscape) 481 { 482 /* 483 * If we are in strict mode and the variable is escaped, then don't bother to 484 * retrieve the value since we won't use it. And if the var is not defined 485 * it will throw an exception. Set value to TRUE to fall through below with 486 * simply printing $foo, and not \$foo 487 */ 488 value = Boolean.TRUE; 489 } 490 else 491 { 492 value = execute(null, context); 493 } 494 495 String localNullString = null; 496 497 /* 498 * if this reference is escaped (\$foo) then we want to do one of two things: 1) if this is 499 * a reference in the context, then we want to print $foo 2) if not, then \$foo (its 500 * considered schmoo, not VTL) 501 */ 502 503 if (escaped) 504 { 505 localNullString = getNullString(context); 506 507 if (value == null) 508 { 509 writer.write(escPrefix); 510 writer.write("\\"); 511 writer.write(localNullString); 512 } 513 else 514 { 515 writer.write(escPrefix); 516 writer.write(localNullString); 517 } 518 return true; 519 } 520 521 /* 522 * the normal processing 523 * 524 * if we have an event cartridge, get a new value object 525 */ 526 527 value = EventHandlerUtil.referenceInsert(rsvc, context, literal, value); 528 529 String toString = null; 530 if (value != null) 531 { 532 if (value instanceof Renderable) 533 { 534 Renderable renderable = (Renderable)value; 535 try 536 { 537 writer.write(escPrefix); 538 writer.write(morePrefix); 539 if (renderable.render(context,writer)) 540 { 541 return true; 542 } 543 } 544 catch(RuntimeException e) 545 { 546 // We commonly get here when an error occurs within a block reference. 547 // We want to log where the reference is at so that a developer can easily 548 // know where the offending call is located. This can be seen 549 // as another element of the error stack we report to log. 550 log.error("Exception rendering " 551 + ((renderable instanceof Reference)? "block ":"Renderable ") 552 + rootString + " at " + StringUtils.formatFileString(this)); 553 throw e; 554 } 555 } 556 557 toString = DuckType.asString(value); 558 } 559 560 if (value == null || toString == null) 561 { 562 if (strictRef) 563 { 564 if (referenceType != QUIET_REFERENCE) 565 { 566 log.error("Prepend the reference with '$!' e.g., $!{}" + 567 " if you want Velocity to ignore the reference when it evaluates to null", 568 literal().substring(1)); 569 if (value == null) 570 { 571 throw new VelocityException("Reference " + literal() 572 + " evaluated to null when attempting to render at " 573 + StringUtils.formatFileString(this) 574 , null, rsvc.getLogContext().getStackTrace()); 575 } 576 else // toString == null 577 { 578 // This will probably rarely happen, but when it does we want to 579 // inform the user that toString == null so they don't pull there 580 // hair out wondering why Velocity thinks the value is null. 581 throw new VelocityException("Reference " + literal() 582 + " evaluated to object " + value.getClass().getName() 583 + " whose toString() method returned null at " 584 + StringUtils.formatFileString(this) 585 , null, rsvc.getLogContext().getStackTrace()); 586 } 587 } 588 return true; 589 } 590 591 /* 592 * write prefix twice, because it's schmoo, so the \ don't escape each 593 * other... 594 */ 595 localNullString = getNullString(context); 596 if (!strictEscape) 597 { 598 // If in strict escape mode then we only print escape once. 599 // Yea, I know.. brittle stuff 600 writer.write(escPrefix); 601 } 602 writer.write(escPrefix); 603 writer.write(morePrefix); 604 writer.write(localNullString); 605 606 if (logOnNull && referenceType != QUIET_REFERENCE) 607 { 608 log.debug("Null reference [template '{}', line {}, column {}]: {} cannot be resolved.", 609 getTemplateName(), this.getLine(), this.getColumn(), this.literal()); 610 } 611 } 612 else 613 { 614 /* 615 * non-null processing 616 */ 617 writer.write(escPrefix); 618 writer.write(morePrefix); 619 if (writer instanceof Filter) 620 { 621 ((Filter)writer).writeReference(toString); 622 } 623 else 624 { 625 writer.write(toString); 626 } 627 628 } 629 return true; 630 } 631 finally 632 { 633 rsvc.getLogContext().popLogContext(); 634 } 635 } 636 637 /** 638 * This method helps to implement the "render literal if null" functionality. 639 * 640 * VelocimacroProxy saves references to macro arguments (AST nodes) so that if we have a macro 641 * #foobar($a $b) then there is key "$a.literal" which points to the literal presentation of the 642 * argument provided to variable $a. If the value of $a is null, we render the string that was 643 * provided as the argument. 644 * 645 * @param context 646 * @return 647 */ getNullString(InternalContextAdapter context)648 private String getNullString(InternalContextAdapter context) 649 { 650 String ret = nullString; 651 652 if (lookupAlternateLiteral) 653 { 654 Deque<String> alternateLiteralsStack = (Deque<String>)context.get(alternateNullStringKey); 655 if (alternateLiteralsStack != null && alternateLiteralsStack.size() > 0) 656 { 657 ret = alternateLiteralsStack.peekFirst(); 658 } 659 } 660 return ret; 661 } 662 663 /** 664 * Computes boolean value of this reference 665 * Returns the actual value of reference return type 666 * boolean, and 'true' if value is not null 667 * 668 * @param context context to compute value with 669 * @return True if evaluation was ok. 670 * @throws MethodInvocationException 671 */ 672 @Override evaluate(InternalContextAdapter context)673 public boolean evaluate(InternalContextAdapter context) 674 throws MethodInvocationException 675 { 676 Object value = execute(this, context); // non-null object as first parameter by convention for 'evaluate' 677 if (value == null) 678 { 679 return false; 680 } 681 try 682 { 683 rsvc.getLogContext().pushLogContext(this, uberInfo); 684 return DuckType.asBoolean(value, checkEmpty); 685 } 686 catch(Exception e) 687 { 688 throw new VelocityException("Reference evaluation threw an exception at " 689 + StringUtils.formatFileString(this), e, rsvc.getLogContext().getStackTrace()); 690 } 691 finally 692 { 693 rsvc.getLogContext().popLogContext(); 694 } 695 } 696 697 /** 698 * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter) 699 */ 700 @Override value(InternalContextAdapter context)701 public Object value(InternalContextAdapter context) 702 throws MethodInvocationException 703 { 704 return (computableReference ? execute(null, context) : null); 705 } 706 707 708 /** 709 * Utility class to handle nulls when printing a class type 710 * @param clazz 711 * @return class name, or the string "null" 712 */ printClass(Class<?> clazz)713 public static String printClass(Class<?> clazz) 714 { 715 return clazz == null ? "null" : clazz.getName(); 716 } 717 718 719 /** 720 * Sets the value of a complex reference (something like $foo.bar) 721 * Currently used by ASTSetReference() 722 * 723 * @see ASTSetDirective 724 * 725 * @param context context object containing this reference 726 * @param value Object to set as value 727 * @return true if successful, false otherwise 728 * @throws MethodInvocationException 729 */ setValue(InternalContextAdapter context, Object value)730 public boolean setValue(InternalContextAdapter context, Object value) 731 throws MethodInvocationException 732 { 733 try 734 { 735 rsvc.getLogContext().pushLogContext(this, uberInfo); 736 737 if (astAlternateValue != null) 738 { 739 log.error("reference set cannot have a default value {}", 740 StringUtils.formatFileString(uberInfo)); 741 return false; 742 } 743 744 if (numChildren == 0) 745 { 746 context.put(rootString, value); 747 return true; 748 } 749 750 /* 751 * The rootOfIntrospection is the object we will 752 * retrieve from the Context. This is the base 753 * object we will apply reflection to. 754 */ 755 756 Object result = getRootVariableValue(context); 757 758 if (result == null) 759 { 760 log.error("reference set is not a valid reference at {}", 761 StringUtils.formatFileString(uberInfo)); 762 return false; 763 } 764 765 /* 766 * How many child nodes do we have? 767 */ 768 769 for (int i = 0; i < numChildren - 1; i++) 770 { 771 result = jjtGetChild(i).execute(result, context); 772 773 if (result == null) 774 { 775 if (strictRef) 776 { 777 String name = jjtGetChild(i+1).getFirstTokenImage(); 778 throw new MethodInvocationException("Attempted to access '" 779 + name + "' on a null value", null, rsvc.getLogContext().getStackTrace(), name, uberInfo.getTemplateName(), 780 jjtGetChild(i+1).getLine(), jjtGetChild(i+1).getColumn()); 781 } 782 783 log.error("reference set is not a valid reference at {}", 784 StringUtils.formatFileString(uberInfo)); 785 return false; 786 } 787 } 788 789 if (astIndex != null) 790 { 791 // If astIndex is not null then we are actually setting an index reference, 792 // something of the form $foo[1] =, or in general any reference that ends with 793 // the brackets. This means that we need to call a more general method 794 // of the form set(Integer, <something>), or put(Object, <something), where 795 // the first parameter is the index value and the second is the LHS of the set. 796 797 Object argument = astIndex.jjtGetChild(0).value(context); 798 // If negative, turn -1 into (size - 1) 799 argument = ASTIndex.adjMinusIndexArg(argument, result, context, astIndex); 800 Object [] params = {argument, value}; 801 Class<?>[] paramClasses = {params[0] == null ? null : params[0].getClass(), 802 params[1] == null ? null : params[1].getClass()}; 803 804 String methodName = "set"; 805 VelMethod method = ClassUtils.getMethod(methodName, params, paramClasses, 806 result, context, astIndex, false); 807 808 if (method == null) 809 { 810 // If we can't find a 'set' method, lets try 'put', This warrents a little 811 // investigation performance wise... if the user is using the hash 812 // form $foo["blaa"], then it may be expensive to first try and fail on 'set' 813 // then go to 'put'? The problem is that getMethod will try the cache, then 814 // perform introspection on 'result' for 'set' 815 methodName = "put"; 816 method = ClassUtils.getMethod(methodName, params, paramClasses, 817 result, context, astIndex, false); 818 } 819 820 if (method == null) 821 { 822 // couldn't find set or put method, so bail 823 if (strictRef) 824 { 825 throw new VelocityException( 826 "Found neither a 'set' or 'put' method with param types '(" 827 + printClass(paramClasses[0]) + "," + printClass(paramClasses[1]) 828 + ")' on class '" + result.getClass().getName() 829 + "' at " + StringUtils.formatFileString(astIndex) 830 , null, rsvc.getLogContext().getStackTrace()); 831 } 832 return false; 833 } 834 835 try 836 { 837 method.invoke(result, params); 838 } 839 catch(RuntimeException e) 840 { 841 // Kludge since invoke throws Exception, pass up Runtimes 842 throw e; 843 } 844 catch(Exception e) 845 { 846 throw new MethodInvocationException( 847 "Exception calling method '" 848 + methodName + "(" 849 + printClass(paramClasses[0]) + "," + printClass(paramClasses[1]) 850 + ")' in " + result.getClass(), 851 e.getCause(), rsvc.getLogContext().getStackTrace(), identifier, astIndex.getTemplateName(), astIndex.getLine(), 852 astIndex.getColumn()); 853 } 854 855 return true; 856 } 857 858 859 /* 860 * We support two ways of setting the value in a #set($ref.foo = $value ): 861 * 1) ref.setFoo( value ) 862 * 2) ref,put("foo", value ) to parallel the get() map introspection 863 */ 864 865 try 866 { 867 VelPropertySet vs = 868 rsvc.getUberspect().getPropertySet(result, identifier, 869 value, uberInfo); 870 871 if (vs == null) 872 { 873 if (strictRef) 874 { 875 throw new MethodInvocationException("Object '" + result.getClass().getName() + 876 "' does not contain property '" + identifier + "'", null, rsvc.getLogContext().getStackTrace(), identifier, 877 uberInfo.getTemplateName(), uberInfo.getLine(), uberInfo.getColumn()); 878 } 879 else 880 { 881 return false; 882 } 883 } 884 885 vs.invoke(result, value); 886 } 887 catch(InvocationTargetException ite) 888 { 889 /* 890 * this is possible 891 */ 892 893 throw new MethodInvocationException( 894 "ASTReference: Invocation of method '" 895 + identifier + "' in " + result.getClass() 896 + " threw exception " 897 + ite.getTargetException().toString(), 898 ite.getTargetException(), rsvc.getLogContext().getStackTrace(), identifier, getTemplateName(), this.getLine(), this.getColumn()); 899 } 900 /* 901 * pass through application level runtime exceptions 902 */ 903 catch( RuntimeException e ) 904 { 905 throw e; 906 } 907 catch(Exception e) 908 { 909 /* 910 * maybe a security exception? 911 */ 912 String msg = "ASTReference setValue(): exception: " + e 913 + " template at " + StringUtils.formatFileString(uberInfo); 914 log.error(msg, e); 915 throw new VelocityException(msg, e, rsvc.getLogContext().getStackTrace()); 916 } 917 918 return true; 919 } 920 finally 921 { 922 rsvc.getLogContext().popLogContext(); 923 } 924 } 925 getRoot()926 private String getRoot() 927 { 928 Token t = getFirstToken(); 929 930 /* 931 * we have a special case where something like 932 * $(\\)*!, where the user want's to see something 933 * like $!blargh in the output, but the ! prevents it from showing. 934 * I think that at this point, this isn't a reference. 935 */ 936 937 /* so, see if we have "\\!" */ 938 939 int slashbang = t.image.indexOf("\\!"); 940 941 if (slashbang != -1) 942 { 943 if (strictEscape) 944 { 945 // If we are in strict escape mode, then we consider this type of 946 // pattern a non-reference, and we print it out as schmoo... 947 nullString = literal(); 948 escaped = true; 949 return nullString; 950 } 951 952 /* 953 * lets do all the work here. I would argue that if this occurs, 954 * it's not a reference at all, so preceding \ characters in front 955 * of the $ are just schmoo. So we just do the escape processing 956 * trick (even | odd) and move on. This kind of breaks the rule 957 * pattern of $ and # but '!' really tosses a wrench into things. 958 */ 959 960 /* 961 * count the escapes: even # -> not escaped, odd -> escaped 962 */ 963 964 int i = 0; 965 int len = t.image.length(); 966 967 i = t.image.indexOf(rsvc.getParserConfiguration().getDollarChar()); 968 969 if (i == -1) 970 { 971 /* yikes! */ 972 log.error("ASTReference.getRoot(): internal error: " 973 + "no $ found for slashbang."); 974 computableReference = false; 975 nullString = t.image; 976 return nullString; 977 } 978 979 while (i < len && t.image.charAt(i) != '\\') 980 { 981 i++; 982 } 983 984 /* ok, i is the first \ char */ 985 986 int start = i; 987 int count = 0; 988 989 while (i < len && t.image.charAt(i++) == '\\') 990 { 991 count++; 992 } 993 994 /* 995 * now construct the output string. We really don't care about 996 * leading slashes as this is not a reference. It's quasi-schmoo 997 */ 998 999 nullString = t.image.substring(0,start); // prefix up to the first 1000 nullString += t.image.substring(start, start + count-1 ); // get the slashes 1001 nullString += t.image.substring(start+count); // and the rest, including the 1002 1003 /* 1004 * this isn't a valid reference, so lets short circuit the value 1005 * and set calcs 1006 */ 1007 1008 computableReference = false; 1009 1010 return nullString; 1011 } 1012 1013 /* 1014 * we need to see if this reference is escaped. if so 1015 * we will clean off the leading \'s and let the 1016 * regular behavior determine if we should output this 1017 * as \$foo or $foo later on in render(). Laziness.. 1018 */ 1019 1020 escaped = false; 1021 1022 if (t.image.startsWith("\\")) 1023 { 1024 /* 1025 * count the escapes: even # -> not escaped, odd -> escaped 1026 */ 1027 1028 int i = 0; 1029 int len = t.image.length(); 1030 1031 while (i < len && t.image.charAt(i) == '\\') 1032 { 1033 i++; 1034 } 1035 1036 if ((i % 2) != 0) 1037 escaped = true; 1038 1039 if (i > 0) 1040 escPrefix = t.image.substring(0, i / 2 ); 1041 1042 t.image = t.image.substring(i); 1043 } 1044 1045 /* 1046 * Look for preceding stuff like '#' and '$' 1047 * and snip it off, except for the 1048 * last $ 1049 */ 1050 1051 int loc1 = t.image.lastIndexOf(rsvc.getParserConfiguration().getDollarChar()); 1052 1053 /* 1054 * if we have extra stuff, loc > 0 1055 * ex. '#$foo' so attach that to 1056 * the prefix. 1057 */ 1058 if (loc1 > 0) 1059 { 1060 morePrefix = morePrefix + t.image.substring(0, loc1); 1061 t.image = t.image.substring(loc1); 1062 } 1063 1064 /* 1065 * Now it should be clean. Get the literal in case this reference 1066 * isn't backed by the context at runtime, and then figure out what 1067 * we are working with. 1068 */ 1069 1070 // FIXME: this is the key to render nulls as literals, we need to look at context(refname+".literal") 1071 nullString = literal(); 1072 1073 if (t.image.startsWith("$!")) 1074 { 1075 referenceType = QUIET_REFERENCE; 1076 1077 /* 1078 * only if we aren't escaped do we want to null the output 1079 */ 1080 1081 if (!escaped) 1082 nullString = ""; 1083 1084 if (t.image.startsWith("$!{")) 1085 { 1086 /* 1087 * ex: $!{provider.Title} 1088 */ 1089 1090 return t.next.image; 1091 } 1092 else 1093 { 1094 /* 1095 * ex: $!provider.Title 1096 */ 1097 1098 return t.image.substring(2); 1099 } 1100 } 1101 else if (t.image.equals("${")) 1102 { 1103 /* 1104 * ex: ${provider.Title} 1105 */ 1106 1107 referenceType = FORMAL_REFERENCE; 1108 return t.next.image; 1109 } 1110 else if (t.image.startsWith("$")) 1111 { 1112 /* 1113 * just nip off the '$' so we have 1114 * the root 1115 */ 1116 1117 referenceType = NORMAL_REFERENCE; 1118 return t.image.substring(1); 1119 } 1120 else 1121 { 1122 /* 1123 * this is a 'RUNT', which can happen in certain circumstances where 1124 * the parser is fooled into believing that an IDENTIFIER is a real 1125 * reference. Another 'dreaded' MORE hack :). 1126 */ 1127 referenceType = RUNT; 1128 return t.image; 1129 } 1130 1131 } 1132 1133 /** 1134 * @param context 1135 * @return The evaluated value of the variable. 1136 * @throws MethodInvocationException 1137 */ getRootVariableValue(InternalContextAdapter context)1138 public Object getRootVariableValue(InternalContextAdapter context) 1139 { 1140 Object obj = null; 1141 try 1142 { 1143 obj = context.get(rootString); 1144 } 1145 catch(RuntimeException e) 1146 { 1147 log.error("Exception calling reference ${} at {}", 1148 rootString, StringUtils.formatFileString(uberInfo)); 1149 throw e; 1150 } 1151 1152 if (obj == null && strictRef && astAlternateValue == null) 1153 { 1154 if (!context.containsKey(rootString)) 1155 { 1156 log.error("Variable ${} has not been set at {}", 1157 rootString, StringUtils.formatFileString(uberInfo)); 1158 throw new MethodInvocationException("Variable $" + rootString + 1159 " has not been set", null, rsvc.getLogContext().getStackTrace(), identifier, 1160 uberInfo.getTemplateName(), uberInfo.getLine(), uberInfo.getColumn()); 1161 } 1162 } 1163 return obj; 1164 } 1165 } 1166