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