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