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.Renderable;
33 import org.apache.velocity.runtime.RuntimeConstants;
34 import org.apache.velocity.runtime.directive.Block.Reference;
35 import org.apache.velocity.runtime.log.Log;
36 import org.apache.velocity.runtime.parser.Parser;
37 import org.apache.velocity.runtime.parser.Token;
38 import org.apache.velocity.util.ClassUtils;
39 import org.apache.velocity.util.introspection.Info;
40 import org.apache.velocity.util.introspection.VelMethod;
41 import org.apache.velocity.util.introspection.VelPropertySet;
42
43 /**
44 * This class is responsible for handling the references in
45 * VTL ($foo).
46 *
47 * Please look at the Parser.jjt file which is
48 * what controls the generation of this class.
49 *
50 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
51 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
52 * @author <a href="mailto:Christoph.Reck@dlr.de">Christoph Reck</a>
53 * @author <a href="mailto:kjohnson@transparent.com>Kent Johnson</a>
54 * @version $Id: ASTReference.java 832302 2009-11-03 05:32:31Z wglass $
55 */
56 public class ASTReference extends SimpleNode
57 {
58 /* Reference types */
59 private static final int NORMAL_REFERENCE = 1;
60 private static final int FORMAL_REFERENCE = 2;
61 private static final int QUIET_REFERENCE = 3;
62 private static final int RUNT = 4;
63
64 private int referenceType;
65 private String nullString;
66 private String rootString;
67 private boolean escaped = false;
68 private boolean computableReference = true;
69 private boolean logOnNull = true;
70 private String escPrefix = "";
71 private String morePrefix = "";
72 private String identifier = "";
73
74 private String literal = null;
75
76 /**
77 * Indicates if we are running in strict reference mode.
78 */
79 public boolean strictRef = false;
80
81 /**
82 * non null Indicates if we are setting an index reference e.g, $foo[2], which basically
83 * means that the last syntax of the reference are brackets.
84 */
85 private ASTIndex astIndex = null;
86
87 /**
88 * Indicates if we are using modified escape behavior in strict mode.
89 * mainly we allow \$abc -> to render as $abc
90 */
91 public boolean strictEscape = false;
92
93 /**
94 * Indicates if toString() should be called during condition evaluation just
95 * to ensure it does not return null. Check is unnecessary if all toString()
96 * implementations are known to have non-null return values. Disabling the
97 * check will give a performance improval since toString() may be a complex
98 * operation on large objects.
99 */
100 public boolean toStringNullCheck = true;
101
102 private int numChildren = 0;
103
104 protected Info uberInfo;
105
106 /**
107 * @param id
108 */
109 public ASTReference(int id)
110 {
111 super(id);
112 }
113
114 /**
115 * @param p
116 * @param id
117 */
118 public ASTReference(Parser p, int id)
119 {
120 super(p, id);
121 }
122
123 /**
124 * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor, java.lang.Object)
125 */
126 public Object jjtAccept(ParserVisitor visitor, Object data)
127 {
128 return visitor.visit(this, data);
129 }
130
131 /**
132 * @see org.apache.velocity.runtime.parser.node.SimpleNode#init(org.apache.velocity.context.InternalContextAdapter, java.lang.Object)
133 */
134 public Object init(InternalContextAdapter context, Object data)
135 throws TemplateInitException
136 {
137 super.init(context, data);
138
139 strictEscape = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT_ESCAPE, false);
140 strictRef = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false);
141 toStringNullCheck = rsvc.getBoolean(RuntimeConstants.DIRECTIVE_IF_TOSTRING_NULLCHECK, true);
142
143 /*
144 * the only thing we can do in init() is getRoot()
145 * as that is template based, not context based,
146 * so it's thread- and context-safe
147 */
148
149 rootString = getRoot().intern();
150
151 numChildren = jjtGetNumChildren();
152
153 // This is an expensive call, so get it now.
154 literal = literal();
155
156 /*
157 * and if appropriate...
158 */
159
160 if (numChildren > 0 )
161 {
162 Node lastNode = jjtGetChild(numChildren-1);
163 if (lastNode instanceof ASTIndex)
164 astIndex = (ASTIndex)lastNode;
165 else
166 identifier = lastNode.getFirstToken().image;
167 }
168
169 /*
170 * make an uberinfo - saves new's later on
171 */
172 uberInfo = new Info(getTemplateName(), getLine(),getColumn());
173
174 /*
175 * track whether we log invalid references
176 */
177 logOnNull =
178 rsvc.getBoolean(RuntimeConstants.RUNTIME_LOG_REFERENCE_LOG_INVALID, true);
179
180 /**
181 * In the case we are referencing a variable with #if($foo) or
182 * #if( ! $foo) then we allow variables to be undefined and we
183 * set strictRef to false so that if the variable is undefined
184 * an exception is not thrown.
185 */
186 if (strictRef && numChildren == 0)
187 {
188 logOnNull = false; // Strict mode allows nulls
189
190 Node node = this.jjtGetParent();
191 if (node instanceof ASTNotNode // #if( ! $foo)
192 || node instanceof ASTExpression // #if( $foo )
193 || node instanceof ASTOrNode // #if( $foo || ...
194 || node instanceof ASTAndNode) // #if( $foo && ...
195 {
196 // Now scan up tree to see if we are in an If statement
197 while (node != null)
198 {
199 if (node instanceof ASTIfStatement)
200 {
201 strictRef = false;
202 break;
203 }
204 node = node.jjtGetParent();
205 }
206 }
207 }
208
209 return data;
210 }
211
212 /**
213 * Returns the 'root string', the reference key
214 * @return the root string.
215 */
216 public String getRootString()
217 {
218 return rootString;
219 }
220
221 /**
222 * gets an Object that 'is' the value of the reference
223 *
224 * @param o unused Object parameter
225 * @param context context used to generate value
226 * @return The execution result.
227 * @throws MethodInvocationException
228 */
229 public Object execute(Object o, InternalContextAdapter context)
230 throws MethodInvocationException
231 {
232
233 if (referenceType == RUNT)
234 return null;
235
236 /*
237 * get the root object from the context
238 */
239
240 Object result = getVariableValue(context, rootString);
241
242 if (result == null && !strictRef)
243 {
244 return EventHandlerUtil.invalidGetMethod(rsvc, context,
245 getDollarBang() + rootString, null, null, uberInfo);
246 }
247
248 /*
249 * Iteratively work 'down' (it's flat...) the reference
250 * to get the value, but check to make sure that
251 * every result along the path is valid. For example:
252 *
253 * $hashtable.Customer.Name
254 *
255 * The $hashtable may be valid, but there is no key
256 * 'Customer' in the hashtable so we want to stop
257 * when we find a null value and return the null
258 * so the error gets logged.
259 */
260
261 try
262 {
263 Object previousResult = result;
264 int failedChild = -1;
265 for (int i = 0; i < numChildren; i++)
266 {
267 if (strictRef && result == null)
268 {
269 /**
270 * At this point we know that an attempt is about to be made
271 * to call a method or property on a null value.
272 */
273 String name = jjtGetChild(i).getFirstToken().image;
274 throw new VelocityException("Attempted to access '"
275 + name + "' on a null value at "
276 + Log.formatFileString(uberInfo.getTemplateName(),
277 + jjtGetChild(i).getLine(), jjtGetChild(i).getColumn()));
278 }
279 previousResult = result;
280 result = jjtGetChild(i).execute(result,context);
281 if (result == null && !strictRef) // If strict and null then well catch this
282 // next time through the loop
283 {
284 failedChild = i;
285 break;
286 }
287 }
288
289 if (result == null)
290 {
291 if (failedChild == -1)
292 {
293 result = EventHandlerUtil.invalidGetMethod(rsvc, context,
294 getDollarBang() + rootString, previousResult, null, uberInfo);
295 }
296 else
297 {
298 StringBuffer name = new StringBuffer(getDollarBang()).append(rootString);
299 for (int i = 0; i <= failedChild; i++)
300 {
301 Node node = jjtGetChild(i);
302 if (node instanceof ASTMethod)
303 {
304 name.append(".").append(((ASTMethod) node).getMethodName()).append("()");
305 }
306 else
307 {
308 name.append(".").append(node.getFirstToken().image);
309 }
310 }
311
312 if (jjtGetChild(failedChild) instanceof ASTMethod)
313 {
314 String methodName = ((ASTMethod) jjtGetChild(failedChild)).getMethodName();
315 result = EventHandlerUtil.invalidMethod(rsvc, context,
316 name.toString(), previousResult, methodName, uberInfo);
317 }
318 else
319 {
320 String property = jjtGetChild(failedChild).getFirstToken().image;
321 result = EventHandlerUtil.invalidGetMethod(rsvc, context,
322 name.toString(), previousResult, property, uberInfo);
323 }
324 }
325
326 }
327
328 return result;
329 }
330 catch(MethodInvocationException mie)
331 {
332 mie.setReferenceName(rootString);
333 throw mie;
334 }
335 }
336
337 /**
338 * gets the value of the reference and outputs it to the
339 * writer.
340 *
341 * @param context context of data to use in getting value
342 * @param writer writer to render to
343 * @return True if rendering was successful.
344 * @throws IOException
345 * @throws MethodInvocationException
346 */
347 public boolean render(InternalContextAdapter context, Writer writer) throws IOException,
348 MethodInvocationException
349 {
350 if (referenceType == RUNT)
351 {
352 writer.write(rootString);
353 return true;
354 }
355
356 Object value = null;
357 if (escaped && strictEscape)
358 {
359 /**
360 * If we are in strict mode and the variable is escaped, then don't bother to
361 * retreive the value since we won't use it. And if the var is not defined
362 * it will throw an exception. Set value to TRUE to fall through below with
363 * simply printing $foo, and not \$foo
364 */
365 value = Boolean.TRUE;
366 }
367 else
368 {
369 value = execute(null, context);
370 }
371
372 String localNullString = null;
373
374 /*
375 * if this reference is escaped (\$foo) then we want to do one of two things : 1) if this is
376 * a reference in the context, then we want to print $foo 2) if not, then \$foo (its
377 * considered schmoo, not VTL)
378 */
379
380 if (escaped)
381 {
382 localNullString = getNullString(context);
383
384 if (value == null)
385 {
386 writer.write(escPrefix);
387 writer.write("\\");
388 writer.write(localNullString);
389 }
390 else
391 {
392 writer.write(escPrefix);
393 writer.write(localNullString);
394 }
395 return true;
396 }
397
398 /*
399 * the normal processing
400 *
401 * if we have an event cartridge, get a new value object
402 */
403
404 value = EventHandlerUtil.referenceInsert(rsvc, context, literal, value);
405
406 String toString = null;
407 if (value != null)
408 {
409 if (value instanceof Renderable)
410 {
411 Renderable renderable = (Renderable)value;
412 try
413 {
414 if (renderable.render(context,writer))
415 return true;
416 }
417 catch(RuntimeException e)
418 {
419 // We commonly get here when an error occurs within a block reference.
420 // We want to log where the reference is at so that a developer can easily
421 // know where the offending call is located. This can be seen
422 // as another element of the error stack we report to log.
423 log.error("Exception rendering "
424 + ((renderable instanceof Reference)? "block ":"Renderable ")
425 + rootString + " at " + Log.formatFileString(this));
426 throw e;
427 }
428 }
429
430 toString = value.toString();
431 }
432
433 if (value == null || toString == null)
434 {
435 if (strictRef)
436 {
437 if (referenceType != QUIET_REFERENCE)
438 {
439 log.error("Prepend the reference with '$!' e.g., $!" + literal().substring(1)
440 + " if you want Velocity to ignore the reference when it evaluates to null");
441 if (value == null)
442 {
443 throw new VelocityException("Reference " + literal()
444 + " evaluated to null when attempting to render at "
445 + Log.formatFileString(this));
446 }
447 else // toString == null
448 {
449 // This will probably rarely happen, but when it does we want to
450 // inform the user that toString == null so they don't pull there
451 // hair out wondering why Velocity thinks the value is null.
452 throw new VelocityException("Reference " + literal()
453 + " evaluated to object " + value.getClass().getName()
454 + " whose toString() method returned null at "
455 + Log.formatFileString(this));
456 }
457 }
458 return true;
459 }
460
461 /*
462 * write prefix twice, because it's schmoo, so the \ don't escape each
463 * other...
464 */
465 localNullString = getNullString(context);
466 if (!strictEscape)
467 {
468 // If in strict escape mode then we only print escape once.
469 // Yea, I know.. brittle stuff
470 writer.write(escPrefix);
471 }
472 writer.write(escPrefix);
473 writer.write(morePrefix);
474 writer.write(localNullString);
475
476 if (logOnNull && referenceType != QUIET_REFERENCE && log.isDebugEnabled())
477 {
478 log.debug("Null reference [template '" + getTemplateName()
479 + "', line " + this.getLine() + ", column " + this.getColumn() + "] : "
480 + this.literal() + " cannot be resolved.");
481 }
482 return true;
483 }
484 else
485 {
486 /*
487 * non-null processing
488 */
489 writer.write(escPrefix);
490 writer.write(morePrefix);
491 writer.write(toString);
492
493 return true;
494 }
495 }
496
497 /**
498 * This method helps to implement the "render literal if null" functionality.
499 *
500 * VelocimacroProxy saves references to macro arguments (AST nodes) so that if we have a macro
501 * #foobar($a $b) then there is key "$a.literal" which points to the literal presentation of the
502 * argument provided to variable $a. If the value of $a is null, we render the string that was
503 * provided as the argument.
504 *
505 * @param context
506 * @return
507 */
508 private String getNullString(InternalContextAdapter context)
509 {
510 Object callingArgument = context.get(".literal." + nullString);
511
512 if (callingArgument != null)
513 return ((Node) callingArgument).literal();
514 else
515 return nullString;
516 }
517
518 /**
519 * Computes boolean value of this reference
520 * Returns the actual value of reference return type
521 * boolean, and 'true' if value is not null
522 *
523 * @param context context to compute value with
524 * @return True if evaluation was ok.
525 * @throws MethodInvocationException
526 */
527 public boolean evaluate(InternalContextAdapter context)
528 throws MethodInvocationException
529 {
530 Object value = execute(null, context);
531
532 if (value == null)
533 {
534 return false;
535 }
536 else if (value instanceof Boolean)
537 {
538 if (((Boolean) value).booleanValue())
539 return true;
540 else
541 return false;
542 }
543 else if (toStringNullCheck)
544 {
545 try
546 {
547 return value.toString() != null;
548 }
549 catch(Exception e)
550 {
551 throw new VelocityException("Reference evaluation threw an exception at "
552 + Log.formatFileString(this), e);
553 }
554 }
555 else
556 {
557 return true;
558 }
559 }
560
561 /**
562 * @see org.apache.velocity.runtime.parser.node.SimpleNode#value(org.apache.velocity.context.InternalContextAdapter)
563 */
564 public Object value(InternalContextAdapter context)
565 throws MethodInvocationException
566 {
567 return (computableReference ? execute(null, context) : null);
568 }
569
570
571 /**
572 * Utility class to handle nulls when printing a class type
573 */
574 public static String printClass(Class clazz)
575 {
576 return clazz == null ? "null" : clazz.getName();
577 }
578
579
580 /**
581 * Sets the value of a complex reference (something like $foo.bar)
582 * Currently used by ASTSetReference()
583 *
584 * @see ASTSetDirective
585 *
586 * @param context context object containing this reference
587 * @param value Object to set as value
588 * @return true if successful, false otherwise
589 * @throws MethodInvocationException
590 */
591 public boolean setValue( InternalContextAdapter context, Object value)
592 throws MethodInvocationException
593 {
594 if (jjtGetNumChildren() == 0)
595 {
596 context.put(rootString, value);
597 return true;
598 }
599
600 /*
601 * The rootOfIntrospection is the object we will
602 * retrieve from the Context. This is the base
603 * object we will apply reflection to.
604 */
605
606 Object result = getVariableValue(context, rootString);
607
608 if (result == null)
609 {
610 String msg = "reference set is not a valid reference at "
611 + Log.formatFileString(uberInfo);
612 log.error(msg);
613 return false;
614 }
615
616 /*
617 * How many child nodes do we have?
618 */
619
620 for (int i = 0; i < numChildren - 1; i++)
621 {
622 result = jjtGetChild(i).execute(result, context);
623
624 if (result == null)
625 {
626 if (strictRef)
627 {
628 String name = jjtGetChild(i+1).getFirstToken().image;
629 throw new MethodInvocationException("Attempted to access '"
630 + name + "' on a null value", null, name, uberInfo.getTemplateName(),
631 jjtGetChild(i+1).getLine(), jjtGetChild(i+1).getColumn());
632 }
633
634 String msg = "reference set is not a valid reference at "
635 + Log.formatFileString(uberInfo);
636 log.error(msg);
637
638 return false;
639 }
640 }
641
642 if (astIndex != null)
643 {
644 // If astIndex is not null then we are actually setting an index reference,
645 // something of the form $foo[1] =, or in general any reference that ends with
646 // the brackets. This means that we need to call a more general method
647 // of the form set(Integer, <something>), or put(Object, <something), where
648 // the first parameter is the index value and the second is the LHS of the set.
649
650 Object argument = astIndex.jjtGetChild(0).value(context);
651 // If negative, turn -1 into (size - 1)
652 argument = ASTIndex.adjMinusIndexArg(argument, result, context, astIndex);
653 Object [] params = {argument, value};
654 Class[] paramClasses = {params[0] == null ? null : params[0].getClass(),
655 params[1] == null ? null : params[1].getClass()};
656
657 String methodName = "set";
658 VelMethod method = ClassUtils.getMethod(methodName, params, paramClasses,
659 result, context, astIndex, false);
660
661 if (method == null)
662 {
663 // If we can't find a 'set' method, lets try 'put', This warrents a little
664 // investigation performance wise... if the user is using the hash
665 // form $foo["blaa"], then it may be expensive to first try and fail on 'set'
666 // then go to 'put'? The problem is that getMethod will try the cache, then
667 // perform introspection on 'result' for 'set'
668 methodName = "put";
669 method = ClassUtils.getMethod(methodName, params, paramClasses,
670 result, context, astIndex, false);
671 }
672
673 if (method == null)
674 {
675 // couldn't find set or put method, so bail
676 if (strictRef)
677 {
678 throw new VelocityException(
679 "Found neither a 'set' or 'put' method with param types '("
680 + printClass(paramClasses[0]) + "," + printClass(paramClasses[1])
681 + ")' on class '" + result.getClass().getName()
682 + "' at " + Log.formatFileString(astIndex));
683 }
684 return false;
685 }
686
687 try
688 {
689 method.invoke(result, params);
690 }
691 catch(RuntimeException e)
692 {
693 // Kludge since invoke throws Exception, pass up Runtimes
694 throw e;
695 }
696 catch(Exception e)
697 {
698 throw new MethodInvocationException(
699 "Exception calling method '"
700 + methodName + "("
701 + printClass(paramClasses[0]) + "," + printClass(paramClasses[1])
702 + ")' in " + result.getClass(),
703 e.getCause(), identifier, astIndex.getTemplateName(), astIndex.getLine(),
704 astIndex.getColumn());
705 }
706
707 return true;
708 }
709
710
711 /*
712 * We support two ways of setting the value in a #set($ref.foo = $value ) :
713 * 1) ref.setFoo( value )
714 * 2) ref,put("foo", value ) to parallel the get() map introspection
715 */
716
717 try
718 {
719 VelPropertySet vs =
720 rsvc.getUberspect().getPropertySet(result, identifier,
721 value, uberInfo);
722
723 if (vs == null)
724 {
725 if (strictRef)
726 {
727 throw new MethodInvocationException("Object '" + result.getClass().getName() +
728 "' does not contain property '" + identifier + "'", null, identifier,
729 uberInfo.getTemplateName(), uberInfo.getLine(), uberInfo.getColumn());
730 }
731 else
732 {
733 return false;
734 }
735 }
736
737 vs.invoke(result, value);
738 }
739 catch(InvocationTargetException ite)
740 {
741 /*
742 * this is possible
743 */
744
745 throw new MethodInvocationException(
746 "ASTReference : Invocation of method '"
747 + identifier + "' in " + result.getClass()
748 + " threw exception "
749 + ite.getTargetException().toString(),
750 ite.getTargetException(), identifier, getTemplateName(), this.getLine(), this.getColumn());
751 }
752 /**
753 * pass through application level runtime exceptions
754 */
755 catch( RuntimeException e )
756 {
757 throw e;
758 }
759 catch(Exception e)
760 {
761 /*
762 * maybe a security exception?
763 */
764 String msg = "ASTReference setValue() : exception : " + e
765 + " template at " + Log.formatFileString(uberInfo);
766 log.error(msg, e);
767 throw new VelocityException(msg, e);
768 }
769
770 return true;
771 }
772
773 private String getRoot()
774 {
775 Token t = getFirstToken();
776
777 /*
778 * we have a special case where something like
779 * $(\\)*!, where the user want's to see something
780 * like $!blargh in the output, but the ! prevents it from showing.
781 * I think that at this point, this isn't a reference.
782 */
783
784 /* so, see if we have "\\!" */
785
786 int slashbang = t.image.indexOf("\\!");
787
788 if (slashbang != -1)
789 {
790 if (strictEscape)
791 {
792 // If we are in strict escape mode, then we consider this type of
793 // pattern a non-reference, and we print it out as schmoo...
794 nullString = literal();
795 escaped = true;
796 return literal();
797 }
798
799 /*
800 * lets do all the work here. I would argue that if this occurrs,
801 * it's not a reference at all, so preceeding \ characters in front
802 * of the $ are just schmoo. So we just do the escape processing
803 * trick (even | odd) and move on. This kind of breaks the rule
804 * pattern of $ and # but '!' really tosses a wrench into things.
805 */
806
807 /*
808 * count the escapes : even # -> not escaped, odd -> escaped
809 */
810
811 int i = 0;
812 int len = t.image.length();
813
814 i = t.image.indexOf('$');
815
816 if (i == -1)
817 {
818 /* yikes! */
819 log.error("ASTReference.getRoot() : internal error : "
820 + "no $ found for slashbang.");
821 computableReference = false;
822 nullString = t.image;
823 return nullString;
824 }
825
826 while (i < len && t.image.charAt(i) != '\\')
827 {
828 i++;
829 }
830
831 /* ok, i is the first \ char */
832
833 int start = i;
834 int count = 0;
835
836 while (i < len && t.image.charAt(i++) == '\\')
837 {
838 count++;
839 }
840
841 /*
842 * now construct the output string. We really don't care about
843 * leading slashes as this is not a reference. It's quasi-schmoo
844 */
845
846 nullString = t.image.substring(0,start); // prefix up to the first
847 nullString += t.image.substring(start, start + count-1 ); // get the slashes
848 nullString += t.image.substring(start+count); // and the rest, including the
849
850 /*
851 * this isn't a valid reference, so lets short circuit the value
852 * and set calcs
853 */
854
855 computableReference = false;
856
857 return nullString;
858 }
859
860 /*
861 * we need to see if this reference is escaped. if so
862 * we will clean off the leading \'s and let the
863 * regular behavior determine if we should output this
864 * as \$foo or $foo later on in render(). Lazyness..
865 */
866
867 escaped = false;
868
869 if (t.image.startsWith("\\"))
870 {
871 /*
872 * count the escapes : even # -> not escaped, odd -> escaped
873 */
874
875 int i = 0;
876 int len = t.image.length();
877
878 while (i < len && t.image.charAt(i) == '\\')
879 {
880 i++;
881 }
882
883 if ((i % 2) != 0)
884 escaped = true;
885
886 if (i > 0)
887 escPrefix = t.image.substring(0, i / 2 );
888
889 t.image = t.image.substring(i);
890 }
891
892 /*
893 * Look for preceeding stuff like '#' and '$'
894 * and snip it off, except for the
895 * last $
896 */
897
898 int loc1 = t.image.lastIndexOf('$');
899
900 /*
901 * if we have extra stuff, loc > 0
902 * ex. '#$foo' so attach that to
903 * the prefix.
904 */
905 if (loc1 > 0)
906 {
907 morePrefix = morePrefix + t.image.substring(0, loc1);
908 t.image = t.image.substring(loc1);
909 }
910
911 /*
912 * Now it should be clean. Get the literal in case this reference
913 * isn't backed by the context at runtime, and then figure out what
914 * we are working with.
915 */
916
917 // FIXME: this is the key to render nulls as literals, we need to look at context(refname+".literal")
918 nullString = literal();
919
920 if (t.image.startsWith("$!"))
921 {
922 referenceType = QUIET_REFERENCE;
923
924 /*
925 * only if we aren't escaped do we want to null the output
926 */
927
928 if (!escaped)
929 nullString = "";
930
931 if (t.image.startsWith("$!{"))
932 {
933 /*
934 * ex : $!{provider.Title}
935 */
936
937 return t.next.image;
938 }
939 else
940 {
941 /*
942 * ex : $!provider.Title
943 */
944
945 return t.image.substring(2);
946 }
947 }
948 else if (t.image.equals("${"))
949 {
950 /*
951 * ex : ${provider.Title}
952 */
953
954 referenceType = FORMAL_REFERENCE;
955 return t.next.image;
956 }
957 else if (t.image.startsWith("$"))
958 {
959 /*
960 * just nip off the '$' so we have
961 * the root
962 */
963
964 referenceType = NORMAL_REFERENCE;
965 return t.image.substring(1);
966 }
967 else
968 {
969 /*
970 * this is a 'RUNT', which can happen in certain circumstances where
971 * the parser is fooled into believeing that an IDENTIFIER is a real
972 * reference. Another 'dreaded' MORE hack :).
973 */
974 referenceType = RUNT;
975 return t.image;
976 }
977
978 }
979
980 /**
981 * @param context
982 * @param variable
983 * @return The evaluated value of the variable.
984 * @throws MethodInvocationException
985 */
986 public Object getVariableValue(Context context, String variable) throws MethodInvocationException
987 {
988 Object obj = null;
989 try
990 {
991 obj = context.get(variable);
992 }
993 catch(RuntimeException e)
994 {
995 log.error("Exception calling reference $" + variable + " at "
996 + Log.formatFileString(uberInfo));
997 throw e;
998 }
999
1000 if (strictRef && obj == null)
1001 {
1002 if (!context.containsKey(variable))
1003 {
1004 log.error("Variable $" + variable + " has not been set at "
1005 + Log.formatFileString(uberInfo));
1006 throw new MethodInvocationException("Variable $" + variable +
1007 " has not been set", null, identifier,
1008 uberInfo.getTemplateName(), uberInfo.getLine(), uberInfo.getColumn());
1009 }
1010 }
1011 return obj;
1012 }
1013
1014 public String getDollarBang()
1015 {
1016 return (referenceType == QUIET_REFERENCE) ? "$!" : "$";
1017 }
1018 }