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 730988 2009-01-03 14:15:56Z byron $
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             mie.setReferenceName(rootString);
305             throw mie;
306         }
307     }
308 
309     /**
310      *  gets the value of the reference and outputs it to the
311      *  writer.
312      *
313      *  @param context  context of data to use in getting value
314      *  @param writer   writer to render to
315      * @return True if rendering was successful.
316      * @throws IOException
317      * @throws MethodInvocationException
318      */
319     public boolean render(InternalContextAdapter context, Writer writer) throws IOException,
320             MethodInvocationException
321     {
322         if (referenceType == RUNT)
323         {
324             if (context.getAllowRendering())
325             {
326                 writer.write(rootString);
327             }
328 
329             return true;
330         }
331 
332         Object value = execute(null, context);
333 
334         String localNullString = null;
335 
336         /*
337          * if this reference is escaped (\$foo) then we want to do one of two things : 1) if this is
338          * a reference in the context, then we want to print $foo 2) if not, then \$foo (its
339          * considered schmoo, not VTL)
340          */
341 
342         if (escaped)
343         {
344             localNullString = getNullString(context);
345             
346             if (value == null)
347             {
348                 if (context.getAllowRendering())
349                 {
350                     writer.write(escPrefix);
351                     writer.write("\\");
352                     writer.write(localNullString);
353                 }
354             }
355             else
356             {
357                 if (context.getAllowRendering())
358                 {
359                     writer.write(escPrefix);
360                     writer.write(localNullString);
361                 }
362             }
363             return true;
364         }
365 
366         /*
367          * the normal processing
368          * 
369          * if we have an event cartridge, get a new value object
370          */
371 
372         value = EventHandlerUtil.referenceInsert(rsvc, context, literal(), value);
373 
374         String toString = null;
375         if (value != null)
376         {
377 
378             if(value instanceof Renderable && ((Renderable)value).render(context,writer))
379             {
380                 return true;
381             }
382 
383             toString = value.toString();
384         }
385 
386         if (value == null || toString == null)
387         {
388             /*
389              * write prefix twice, because it's schmoo, so the \ don't escape each other...
390              */
391 
392             if (context.getAllowRendering())
393             {
394                 localNullString = getNullString(context);
395 
396                 writer.write(escPrefix);
397                 writer.write(escPrefix);
398                 writer.write(morePrefix);
399                 writer.write(localNullString);
400             }
401 
402             if (logOnNull && referenceType != QUIET_REFERENCE && log.isDebugEnabled())
403             {
404                 log.debug("Null reference [template '" + getTemplateName()
405                         + "', line " + this.getLine() + ", column " + this.getColumn() + "] : "
406                         + this.literal() + " cannot be resolved.");
407             }
408             return true;
409         }
410         else
411         {
412             /*
413              * non-null processing
414              */
415 
416             if (context.getAllowRendering())
417             {
418                 writer.write(escPrefix);
419                 writer.write(morePrefix);
420                 writer.write(toString);
421             }
422 
423             return true;
424         }
425     }
426 
427     /**
428      * This method helps to implement the "render literal if null" functionality.
429      * 
430      * VelocimacroProxy saves references to macro arguments (AST nodes) so that if we have a macro
431      * #foobar($a $b) then there is key "$a.literal" which points to the literal presentation of the
432      * argument provided to variable $a. If the value of $a is null, we render the string that was
433      * provided as the argument.
434      * 
435      * @param context
436      * @return
437      */
438     private String getNullString(InternalContextAdapter context)
439     {
440         Object callingArgument = context.get(".literal." + nullString);
441 
442         if (callingArgument != null)
443             return ((Node) callingArgument).literal();
444         else
445             return nullString;
446     }
447 
448     /**
449      *   Computes boolean value of this reference
450      *   Returns the actual value of reference return type
451      *   boolean, and 'true' if value is not null
452      *
453      *   @param context context to compute value with
454      * @return True if evaluation was ok.
455      * @throws MethodInvocationException
456      */
457     public boolean evaluate(InternalContextAdapter context)
458         throws MethodInvocationException
459     {
460         Object value = execute(null, context);
461 
462         if (value == null)
463         {
464             return false;
465         }
466         else if (value instanceof Boolean)
467         {
468             if (((Boolean) value).booleanValue())
469                 return true;
470             else
471                 return false;
472         }        
473         else
474         {
475             try
476             {
477                 return value.toString() != null;
478             }
479             catch(Exception e)
480             {
481                 throw new VelocityException("Reference evaluation threw an exception at " 
482                     + Log.formatFileString(this), e);
483             }
484         }
485     }
486 
487     /**
488      * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter)
489      */
490     public Object value(InternalContextAdapter context)
491         throws MethodInvocationException
492     {
493         return (computableReference ? execute(null, context) : null);
494     }
495 
496     /**
497      *  Sets the value of a complex reference (something like $foo.bar)
498      *  Currently used by ASTSetReference()
499      *
500      *  @see ASTSetDirective
501      *
502      *  @param context context object containing this reference
503      *  @param value Object to set as value
504      *  @return true if successful, false otherwise
505      * @throws MethodInvocationException
506      */
507     public boolean setValue( InternalContextAdapter context, Object value)
508       throws MethodInvocationException
509     {
510         if (jjtGetNumChildren() == 0)
511         {
512             context.put(rootString, value);
513             return true;
514         }
515 
516         /*
517          *  The rootOfIntrospection is the object we will
518          *  retrieve from the Context. This is the base
519          *  object we will apply reflection to.
520          */
521 
522         Object result = getVariableValue(context, rootString);
523 
524         if (result == null)
525         {
526             String msg = "reference set is not a valid reference at "
527                     + Log.formatFileString(uberInfo);
528             log.error(msg);
529             return false;
530         }
531 
532         /*
533          * How many child nodes do we have?
534          */
535 
536         for (int i = 0; i < numChildren - 1; i++)
537         {
538             result = jjtGetChild(i).execute(result, context);
539 
540             if (result == null)
541             {
542                 if (strictRef)
543                 {
544                     String name = jjtGetChild(i+1).getFirstToken().image;
545                     throw new MethodInvocationException("Attempted to access '"  
546                         + name + "' on a null value", null, name, uberInfo.getTemplateName(),
547                         jjtGetChild(i+1).getLine(), jjtGetChild(i+1).getColumn());
548                 }            
549               
550                 String msg = "reference set is not a valid reference at "
551                     + Log.formatFileString(uberInfo);
552                 log.error(msg);
553 
554                 return false;
555             }
556         }
557 
558         /*
559          *  We support two ways of setting the value in a #set($ref.foo = $value ) :
560          *  1) ref.setFoo( value )
561          *  2) ref,put("foo", value ) to parallel the get() map introspection
562          */
563 
564         try
565         {
566             VelPropertySet vs =
567                     rsvc.getUberspect().getPropertySet(result, identifier,
568                             value, uberInfo);
569 
570             if (vs == null)
571             {
572                 if (strictRef)
573                 {
574                     throw new MethodInvocationException("Object '" + result.getClass().getName() +
575                        "' does not contain property '" + identifier + "'", null, identifier,
576                        uberInfo.getTemplateName(), uberInfo.getLine(), uberInfo.getColumn());
577                 }
578                 else
579                 {
580                   return false;
581                 }
582             }
583 
584             vs.invoke(result, value);
585         }
586         catch(InvocationTargetException ite)
587         {
588             /*
589              *  this is possible
590              */
591 
592             throw  new MethodInvocationException(
593                 "ASTReference : Invocation of method '"
594                 + identifier + "' in  " + result.getClass()
595                 + " threw exception "
596                 + ite.getTargetException().toString(),
597                ite.getTargetException(), identifier, getTemplateName(), this.getLine(), this.getColumn());
598         }
599         /**
600          * pass through application level runtime exceptions
601          */
602         catch( RuntimeException e )
603         {
604             throw e;
605         }
606         catch(Exception e)
607         {
608             /*
609              *  maybe a security exception?
610              */
611             String msg = "ASTReference setValue() : exception : " + e
612                           + " template at " + Log.formatFileString(uberInfo);
613             log.error(msg, e);
614             throw new VelocityException(msg, e);
615          }
616 
617         return true;
618     }
619 
620     private String getRoot()
621     {
622         Token t = getFirstToken();
623 
624         /*
625          *  we have a special case where something like
626          *  $(\\)*!, where the user want's to see something
627          *  like $!blargh in the output, but the ! prevents it from showing.
628          *  I think that at this point, this isn't a reference.
629          */
630 
631         /* so, see if we have "\\!" */
632 
633         int slashbang = t.image.indexOf("\\!");
634 
635         if (slashbang != -1)
636         {
637             /*
638              *  lets do all the work here.  I would argue that if this occurrs,
639              *  it's not a reference at all, so preceeding \ characters in front
640              *  of the $ are just schmoo.  So we just do the escape processing
641              *  trick (even | odd) and move on.  This kind of breaks the rule
642              *  pattern of $ and # but '!' really tosses a wrench into things.
643              */
644 
645              /*
646               *  count the escapes : even # -> not escaped, odd -> escaped
647               */
648 
649             int i = 0;
650             int len = t.image.length();
651 
652             i = t.image.indexOf('$');
653 
654             if (i == -1)
655             {
656                 /* yikes! */
657                 log.error("ASTReference.getRoot() : internal error : "
658                             + "no $ found for slashbang.");
659                 computableReference = false;
660                 nullString = t.image;
661                 return nullString;
662             }
663 
664             while (i < len && t.image.charAt(i) != '\\')
665             {
666                 i++;
667             }
668 
669             /*  ok, i is the first \ char */
670 
671             int start = i;
672             int count = 0;
673 
674             while (i < len && t.image.charAt(i++) == '\\')
675             {
676                 count++;
677             }
678 
679             /*
680              *  now construct the output string.  We really don't care about
681              *  leading  slashes as this is not a reference.  It's quasi-schmoo
682              */
683 
684             nullString = t.image.substring(0,start); // prefix up to the first
685             nullString += t.image.substring(start, start + count-1 ); // get the slashes
686             nullString += t.image.substring(start+count); // and the rest, including the
687 
688             /*
689              *  this isn't a valid reference, so lets short circuit the value
690              *  and set calcs
691              */
692 
693             computableReference = false;
694 
695             return nullString;
696         }
697 
698         /*
699          *  we need to see if this reference is escaped.  if so
700          *  we will clean off the leading \'s and let the
701          *  regular behavior determine if we should output this
702          *  as \$foo or $foo later on in render(). Lazyness..
703          */
704 
705         escaped = false;
706 
707         if (t.image.startsWith("\\"))
708         {
709             /*
710              *  count the escapes : even # -> not escaped, odd -> escaped
711              */
712 
713             int i = 0;
714             int len = t.image.length();
715 
716             while (i < len && t.image.charAt(i) == '\\')
717             {
718                 i++;
719             }
720 
721             if ((i % 2) != 0)
722                 escaped = true;
723 
724             if (i > 0)
725                 escPrefix = t.image.substring(0, i / 2 );
726 
727             t.image = t.image.substring(i);
728         }
729 
730         /*
731          *  Look for preceeding stuff like '#' and '$'
732          *  and snip it off, except for the
733          *  last $
734          */
735 
736         int loc1 = t.image.lastIndexOf('$');
737 
738         /*
739          *  if we have extra stuff, loc > 0
740          *  ex. '#$foo' so attach that to
741          *  the prefix.
742          */
743         if (loc1 > 0)
744         {
745             morePrefix = morePrefix + t.image.substring(0, loc1);
746             t.image = t.image.substring(loc1);
747         }
748 
749         /*
750          *  Now it should be clean. Get the literal in case this reference
751          *  isn't backed by the context at runtime, and then figure out what
752          *  we are working with.
753          */
754 
755         // FIXME: this is the key to render nulls as literals, we need to look at context(refname+".literal") 
756         nullString = literal();
757 
758         if (t.image.startsWith("$!"))
759         {
760             referenceType = QUIET_REFERENCE;
761 
762             /*
763              *  only if we aren't escaped do we want to null the output
764              */
765 
766             if (!escaped)
767                 nullString = "";
768 
769             if (t.image.startsWith("$!{"))
770             {
771                 /*
772                  *  ex : $!{provider.Title}
773                  */
774 
775                 return t.next.image;
776             }
777             else
778             {
779                 /*
780                  *  ex : $!provider.Title
781                  */
782 
783                 return t.image.substring(2);
784             }
785         }
786         else if (t.image.equals("${"))
787         {
788             /*
789              *  ex : ${provider.Title}
790              */
791 
792             referenceType = FORMAL_REFERENCE;
793             return t.next.image;
794         }
795         else if (t.image.startsWith("$"))
796         {
797             /*
798              *  just nip off the '$' so we have
799              *  the root
800              */
801 
802             referenceType = NORMAL_REFERENCE;
803             return t.image.substring(1);
804         }
805         else
806         {
807             /*
808              * this is a 'RUNT', which can happen in certain circumstances where
809              *  the parser is fooled into believeing that an IDENTIFIER is a real
810              *  reference.  Another 'dreaded' MORE hack :).
811              */
812             referenceType = RUNT;
813             return t.image;
814         }
815 
816     }
817 
818     /**
819      * @param context
820      * @param variable
821      * @return The evaluated value of the variable.
822      * @throws MethodInvocationException
823      */
824     public Object getVariableValue(Context context, String variable) throws MethodInvocationException
825     {
826         Object obj = null;
827         try
828         {
829             obj = context.get(variable);
830         }
831         catch(RuntimeException e)
832         {
833             log.error("Exception calling reference $" + variable + " at "
834                       + Log.formatFileString(uberInfo));
835             throw e;
836         }
837         
838         if (strictRef && obj == null)
839         {
840           if (!context.containsKey(variable))
841           {
842               log.error("Variable $" + variable + " has not been set at "
843                         + Log.formatFileString(uberInfo));
844               throw new MethodInvocationException("Variable $" + variable +
845                   " has not been set", null, identifier,
846                   uberInfo.getTemplateName(), uberInfo.getLine(), uberInfo.getColumn());            
847           }
848         }
849         return obj;        
850     }
851 
852 
853     /**
854      *  Routine to allow the literal representation to be
855      *  externally overridden.  Used now in the VM system
856      *  to override a reference in a VM tree with the
857      *  literal of the calling arg to make it work nicely
858      *  when calling arg is null.  It seems a bit much, but
859      *  does keep things consistant.
860      *
861      *  Note, you can only set the literal once...
862      *
863      *  @param literal String to render to when null
864      */
865     public void setLiteral(String literal)
866     {
867         /*
868          * do only once
869          */
870 
871         if( this.literal == null)
872             this.literal = literal;
873     }
874 
875     /**
876      *  Override of the SimpleNode method literal()
877      *  Returns the literal representation of the
878      *  node.  Should be something like
879      *  $<token>.
880      * @return A literal string.
881      */
882     public String literal()
883     {
884         if (literal != null)
885             return literal;
886         
887         // this value could be cached in this.literal but it increases memory usage
888         return super.literal();
889     }
890 }