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 }