View Javadoc

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