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