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