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