• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 -&gt; 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