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