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