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