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