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 724746 2008-12-09 15:20:58Z 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             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 if (value.toString() == null)
474         {
475             return false;
476         }
477         else
478             return true;
479     }
480 
481     /**
482      * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter)
483      */
484     public Object value(InternalContextAdapter context)
485         throws MethodInvocationException
486     {
487         return (computableReference ? execute(null, context) : null);
488     }
489 
490     /**
491      *  Sets the value of a complex reference (something like $foo.bar)
492      *  Currently used by ASTSetReference()
493      *
494      *  @see ASTSetDirective
495      *
496      *  @param context context object containing this reference
497      *  @param value Object to set as value
498      *  @return true if successful, false otherwise
499      * @throws MethodInvocationException
500      */
501     public boolean setValue( InternalContextAdapter context, Object value)
502       throws MethodInvocationException
503     {
504         if (jjtGetNumChildren() == 0)
505         {
506             context.put(rootString, value);
507             return true;
508         }
509 
510         /*
511          *  The rootOfIntrospection is the object we will
512          *  retrieve from the Context. This is the base
513          *  object we will apply reflection to.
514          */
515 
516         Object result = getVariableValue(context, rootString);
517 
518         if (result == null)
519         {
520             String msg = "reference set is not a valid reference at "
521                     + Log.formatFileString(uberInfo);
522             log.error(msg);
523             return false;
524         }
525 
526         /*
527          * How many child nodes do we have?
528          */
529 
530         for (int i = 0; i < numChildren - 1; i++)
531         {
532             result = jjtGetChild(i).execute(result, context);
533 
534             if (result == null)
535             {
536                 if (strictRef)
537                 {
538                     String name = jjtGetChild(i+1).getFirstToken().image;
539                     throw new MethodInvocationException("Attempted to access '"  
540                         + name + "' on a null value", null, name, uberInfo.getTemplateName(),
541                         jjtGetChild(i+1).getLine(), jjtGetChild(i+1).getColumn());
542                 }            
543               
544                 String msg = "reference set is not a valid reference at "
545                     + Log.formatFileString(uberInfo);
546                 log.error(msg);
547 
548                 return false;
549             }
550         }
551 
552         /*
553          *  We support two ways of setting the value in a #set($ref.foo = $value ) :
554          *  1) ref.setFoo( value )
555          *  2) ref,put("foo", value ) to parallel the get() map introspection
556          */
557 
558         try
559         {
560             VelPropertySet vs =
561                     rsvc.getUberspect().getPropertySet(result, identifier,
562                             value, uberInfo);
563 
564             if (vs == null)
565             {
566                 if (strictRef)
567                 {
568                     throw new MethodInvocationException("Object '" + result.getClass().getName() +
569                        "' does not contain property '" + identifier + "'", null, identifier,
570                        uberInfo.getTemplateName(), uberInfo.getLine(), uberInfo.getColumn());
571                 }
572                 else
573                 {
574                   return false;
575                 }
576             }
577 
578             vs.invoke(result, value);
579         }
580         catch(InvocationTargetException ite)
581         {
582             /*
583              *  this is possible
584              */
585 
586             throw  new MethodInvocationException(
587                 "ASTReference : Invocation of method '"
588                 + identifier + "' in  " + result.getClass()
589                 + " threw exception "
590                 + ite.getTargetException().toString(),
591                ite.getTargetException(), identifier, getTemplateName(), this.getLine(), this.getColumn());
592         }
593         /**
594          * pass through application level runtime exceptions
595          */
596         catch( RuntimeException e )
597         {
598             throw e;
599         }
600         catch(Exception e)
601         {
602             /*
603              *  maybe a security exception?
604              */
605             String msg = "ASTReference setValue() : exception : " + e
606                           + " template at " + Log.formatFileString(uberInfo);
607             log.error(msg, e);
608             throw new VelocityException(msg, e);
609          }
610 
611         return true;
612     }
613 
614     private String getRoot()
615     {
616         Token t = getFirstToken();
617 
618         /*
619          *  we have a special case where something like
620          *  $(\\)*!, where the user want's to see something
621          *  like $!blargh in the output, but the ! prevents it from showing.
622          *  I think that at this point, this isn't a reference.
623          */
624 
625         /* so, see if we have "\\!" */
626 
627         int slashbang = t.image.indexOf("\\!");
628 
629         if (slashbang != -1)
630         {
631             /*
632              *  lets do all the work here.  I would argue that if this occurrs,
633              *  it's not a reference at all, so preceeding \ characters in front
634              *  of the $ are just schmoo.  So we just do the escape processing
635              *  trick (even | odd) and move on.  This kind of breaks the rule
636              *  pattern of $ and # but '!' really tosses a wrench into things.
637              */
638 
639              /*
640               *  count the escapes : even # -> not escaped, odd -> escaped
641               */
642 
643             int i = 0;
644             int len = t.image.length();
645 
646             i = t.image.indexOf('$');
647 
648             if (i == -1)
649             {
650                 /* yikes! */
651                 log.error("ASTReference.getRoot() : internal error : "
652                             + "no $ found for slashbang.");
653                 computableReference = false;
654                 nullString = t.image;
655                 return nullString;
656             }
657 
658             while (i < len && t.image.charAt(i) != '\\')
659             {
660                 i++;
661             }
662 
663             /*  ok, i is the first \ char */
664 
665             int start = i;
666             int count = 0;
667 
668             while (i < len && t.image.charAt(i++) == '\\')
669             {
670                 count++;
671             }
672 
673             /*
674              *  now construct the output string.  We really don't care about
675              *  leading  slashes as this is not a reference.  It's quasi-schmoo
676              */
677 
678             nullString = t.image.substring(0,start); // prefix up to the first
679             nullString += t.image.substring(start, start + count-1 ); // get the slashes
680             nullString += t.image.substring(start+count); // and the rest, including the
681 
682             /*
683              *  this isn't a valid reference, so lets short circuit the value
684              *  and set calcs
685              */
686 
687             computableReference = false;
688 
689             return nullString;
690         }
691 
692         /*
693          *  we need to see if this reference is escaped.  if so
694          *  we will clean off the leading \'s and let the
695          *  regular behavior determine if we should output this
696          *  as \$foo or $foo later on in render(). Lazyness..
697          */
698 
699         escaped = false;
700 
701         if (t.image.startsWith("\\"))
702         {
703             /*
704              *  count the escapes : even # -> not escaped, odd -> escaped
705              */
706 
707             int i = 0;
708             int len = t.image.length();
709 
710             while (i < len && t.image.charAt(i) == '\\')
711             {
712                 i++;
713             }
714 
715             if ((i % 2) != 0)
716                 escaped = true;
717 
718             if (i > 0)
719                 escPrefix = t.image.substring(0, i / 2 );
720 
721             t.image = t.image.substring(i);
722         }
723 
724         /*
725          *  Look for preceeding stuff like '#' and '$'
726          *  and snip it off, except for the
727          *  last $
728          */
729 
730         int loc1 = t.image.lastIndexOf('$');
731 
732         /*
733          *  if we have extra stuff, loc > 0
734          *  ex. '#$foo' so attach that to
735          *  the prefix.
736          */
737         if (loc1 > 0)
738         {
739             morePrefix = morePrefix + t.image.substring(0, loc1);
740             t.image = t.image.substring(loc1);
741         }
742 
743         /*
744          *  Now it should be clean. Get the literal in case this reference
745          *  isn't backed by the context at runtime, and then figure out what
746          *  we are working with.
747          */
748 
749         // FIXME: this is the key to render nulls as literals, we need to look at context(refname+".literal") 
750         nullString = literal();
751 
752         if (t.image.startsWith("$!"))
753         {
754             referenceType = QUIET_REFERENCE;
755 
756             /*
757              *  only if we aren't escaped do we want to null the output
758              */
759 
760             if (!escaped)
761                 nullString = "";
762 
763             if (t.image.startsWith("$!{"))
764             {
765                 /*
766                  *  ex : $!{provider.Title}
767                  */
768 
769                 return t.next.image;
770             }
771             else
772             {
773                 /*
774                  *  ex : $!provider.Title
775                  */
776 
777                 return t.image.substring(2);
778             }
779         }
780         else if (t.image.equals("${"))
781         {
782             /*
783              *  ex : ${provider.Title}
784              */
785 
786             referenceType = FORMAL_REFERENCE;
787             return t.next.image;
788         }
789         else if (t.image.startsWith("$"))
790         {
791             /*
792              *  just nip off the '$' so we have
793              *  the root
794              */
795 
796             referenceType = NORMAL_REFERENCE;
797             return t.image.substring(1);
798         }
799         else
800         {
801             /*
802              * this is a 'RUNT', which can happen in certain circumstances where
803              *  the parser is fooled into believeing that an IDENTIFIER is a real
804              *  reference.  Another 'dreaded' MORE hack :).
805              */
806             referenceType = RUNT;
807             return t.image;
808         }
809 
810     }
811 
812     /**
813      * @param context
814      * @param variable
815      * @return The evaluated value of the variable.
816      * @throws MethodInvocationException
817      */
818     public Object getVariableValue(Context context, String variable) throws MethodInvocationException
819     {
820         Object obj = null;
821         try
822         {
823             obj = context.get(variable);
824         }
825         catch(RuntimeException e)
826         {
827             log.error("Exception calling reference $" + variable + " at "
828                       + Log.formatFileString(uberInfo));
829             throw e;
830         }
831         
832         if (strictRef && obj == null)
833         {
834           if (!context.containsKey(variable))
835           {
836               log.error("Variable $" + variable + " has not been set at "
837                         + Log.formatFileString(uberInfo));
838               throw new MethodInvocationException("Variable $" + variable +
839                   " has not been set", null, identifier,
840                   uberInfo.getTemplateName(), uberInfo.getLine(), uberInfo.getColumn());            
841           }
842         }
843         return obj;        
844     }
845 
846 
847     /**
848      *  Routine to allow the literal representation to be
849      *  externally overridden.  Used now in the VM system
850      *  to override a reference in a VM tree with the
851      *  literal of the calling arg to make it work nicely
852      *  when calling arg is null.  It seems a bit much, but
853      *  does keep things consistant.
854      *
855      *  Note, you can only set the literal once...
856      *
857      *  @param literal String to render to when null
858      */
859     public void setLiteral(String literal)
860     {
861         /*
862          * do only once
863          */
864 
865         if( this.literal == null)
866             this.literal = literal;
867     }
868 
869     /**
870      *  Override of the SimpleNode method literal()
871      *  Returns the literal representation of the
872      *  node.  Should be something like
873      *  $<token>.
874      * @return A literal string.
875      */
876     public String literal()
877     {
878         if (literal != null)
879             return literal;
880         
881         // this value could be cached in this.literal but it increases memory usage
882         return super.literal();
883     }
884 }