1 package org.apache.velocity.runtime.parser.node;
2
3 /*
4 * Licensed to the Apache Software Foundation (ASF) under one or more
5 * contributor license agreements. See the NOTICE file distributed with this
6 * work for additional information regarding copyright ownership. The ASF
7 * licenses this file to you under the Apache License, Version 2.0 (the
8 * "License"); you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16 * License for the specific language governing permissions and limitations under
17 * the License.
18 */
19
20 import java.io.IOException;
21 import java.io.StringReader;
22 import java.io.StringWriter;
23
24 import org.apache.commons.lang.text.StrBuilder;
25 import org.apache.velocity.context.InternalContextAdapter;
26 import org.apache.velocity.exception.TemplateInitException;
27 import org.apache.velocity.exception.VelocityException;
28 import org.apache.velocity.runtime.RuntimeConstants;
29 import org.apache.velocity.runtime.log.Log;
30 import org.apache.velocity.runtime.parser.ParseException;
31 import org.apache.velocity.runtime.parser.Parser;
32 import org.apache.velocity.runtime.parser.Token;
33
34 /**
35 * ASTStringLiteral support. Will interpolate!
36 *
37 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
38 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
39 * @version $Id: ASTStringLiteral.java 1032134 2010-11-06 20:19:39Z cbrisson $
40 */
41 public class ASTStringLiteral extends SimpleNode
42 {
43 /* cache the value of the interpolation switch */
44 private boolean interpolate = true;
45
46 private SimpleNode nodeTree = null;
47
48 private String image = "";
49
50 private String interpolateimage = "";
51
52 /** true if the string contains a line comment (##) */
53 private boolean containsLineComment;
54
55 /**
56 * @param id
57 */
58 public ASTStringLiteral(int id)
59 {
60 super(id);
61 }
62
63 /**
64 * @param p
65 * @param id
66 */
67 public ASTStringLiteral(Parser p, int id)
68 {
69 super(p, id);
70 }
71
72 /**
73 * init : we don't have to do much. Init the tree (there shouldn't be one)
74 * and then see if interpolation is turned on.
75 *
76 * @param context
77 * @param data
78 * @return Init result.
79 * @throws TemplateInitException
80 */
81 public Object init(InternalContextAdapter context, Object data)
82 throws TemplateInitException
83 {
84 /*
85 * simple habit... we prollie don't have an AST beneath us
86 */
87
88 super.init(context, data);
89
90 /*
91 * the stringlit is set at template parse time, so we can do this here
92 * for now. if things change and we can somehow create stringlits at
93 * runtime, this must move to the runtime execution path
94 *
95 * so, only if interpolation is turned on AND it starts with a " AND it
96 * has a directive or reference, then we can interpolate. Otherwise,
97 * don't bother.
98 */
99
100 interpolate = rsvc.getBoolean(
101 RuntimeConstants.INTERPOLATE_STRINGLITERALS, true)
102 && getFirstToken().image.startsWith("\"")
103 && ((getFirstToken().image.indexOf('$') != -1) || (getFirstToken().image
104 .indexOf('#') != -1));
105
106 /*
107 * get the contents of the string, minus the '/" at each end
108 */
109 String img = getFirstToken().image;
110
111 image = img.substring(1, img.length() - 1);
112
113 if (img.startsWith("\""))
114 {
115 image = unescape(image);
116 }
117 if (img.charAt(0) == '"' || img.charAt(0) == '\'' )
118 {
119 // replace double-double quotes like "" with a single double quote "
120 // replace double single quotes '' with a single quote '
121 image = replaceQuotes(image, img.charAt(0));
122 }
123
124 /**
125 * note. A kludge on a kludge. The first part, Geir calls this the
126 * dreaded <MORE> kludge. Basically, the use of the <MORE> token eats
127 * the last character of an interpolated string. EXCEPT when a line
128 * comment (##) is in the string this isn't an issue.
129 *
130 * So, to solve this we look for a line comment. If it isn't found we
131 * add a space here and remove it later.
132 */
133
134 /**
135 * Note - this should really use a regexp to look for [^\]## but
136 * apparently escaping of line comments isn't working right now anyway.
137 */
138 containsLineComment = (image.indexOf("##") != -1);
139
140 /*
141 * if appropriate, tack a space on the end (dreaded <MORE> kludge)
142 */
143
144 if (!containsLineComment)
145 {
146 interpolateimage = image + " ";
147 }
148 else
149 {
150 interpolateimage = image;
151 }
152
153 if (interpolate)
154 {
155 /*
156 * now parse and init the nodeTree
157 */
158 StringReader br = new StringReader(interpolateimage);
159
160 /*
161 * it's possible to not have an initialization context - or we don't
162 * want to trust the caller - so have a fallback value if so
163 *
164 * Also, do *not* dump the VM namespace for this template
165 */
166
167 String templateName =
168 (context != null) ? context.getCurrentTemplateName() : "StringLiteral";
169 try
170 {
171 nodeTree = rsvc.parse(br, templateName, false);
172 }
173 catch (ParseException e)
174 {
175 String msg = "Failed to parse String literal at "+
176 Log.formatFileString(templateName, getLine(), getColumn());
177 throw new TemplateInitException(msg, e, templateName, getColumn(), getLine());
178 }
179
180 adjTokenLineNums(nodeTree);
181
182 /*
183 * init with context. It won't modify anything
184 */
185
186 nodeTree.init(context, rsvc);
187 }
188
189 return data;
190 }
191
192 /**
193 * Adjust all the line and column numbers that comprise a node so that they
194 * are corrected for the string literals position within the template file.
195 * This is neccessary if an exception is thrown while processing the node so
196 * that the line and column position reported reflects the error position
197 * within the template and not just relative to the error position within
198 * the string literal.
199 */
200 public void adjTokenLineNums(Node node)
201 {
202 Token tok = node.getFirstToken();
203 // Test against null is probably not neccessary, but just being safe
204 while(tok != null && tok != node.getLastToken())
205 {
206 // If tok is on the first line, then the actual column is
207 // offset by the template column.
208
209 if (tok.beginLine == 1)
210 tok.beginColumn += getColumn();
211
212 if (tok.endLine == 1)
213 tok.endColumn += getColumn();
214
215 tok.beginLine += getLine()- 1;
216 tok.endLine += getLine() - 1;
217 tok = tok.next;
218 }
219 }
220
221 /**
222 * Replaces double double-quotes with a single double quote ("" to ").
223 * Replaces double single quotes with a single quote ('' to ').
224 *
225 * @param s StringLiteral without the surrounding quotes
226 * @param literalQuoteChar char that starts the StringLiteral (" or ')
227 */
228 private String replaceQuotes(String s, char literalQuoteChar)
229 {
230 if( (literalQuoteChar == '"' && s.indexOf("\"") == -1) ||
231 (literalQuoteChar == '\'' && s.indexOf("'") == -1) )
232 {
233 return s;
234 }
235
236 StrBuilder result = new StrBuilder(s.length());
237 char prev = ' ';
238 for(int i = 0, is = s.length(); i < is; i++)
239 {
240 char c = s.charAt(i);
241 result.append(c);
242
243 if( i + 1 < is )
244 {
245 char next = s.charAt(i + 1);
246 // '""' -> "", "''" -> ''
247 // thus it is not necessary to double quotes if the "surrounding" quotes
248 // of the StringLiteral are different. See VELOCITY-785
249 if( (literalQuoteChar == '"' && (next == '"' && c == '"')) ||
250 (literalQuoteChar == '\'' && (next == '\'' && c == '\'')) )
251 {
252 i++;
253 }
254 }
255 }
256 return result.toString();
257 }
258
259 /**
260 * @since 1.6
261 */
262 public static String unescape(final String string)
263 {
264 int u = string.indexOf("\\u");
265 if (u < 0) return string;
266
267 StrBuilder result = new StrBuilder();
268
269 int lastCopied = 0;
270
271 for (;;)
272 {
273 result.append(string.substring(lastCopied, u));
274
275 /* we don't worry about an exception here,
276 * because the lexer checked that string is correct */
277 char c = (char) Integer.parseInt(string.substring(u + 2, u + 6), 16);
278 result.append(c);
279
280 lastCopied = u + 6;
281
282 u = string.indexOf("\\u", lastCopied);
283 if (u < 0)
284 {
285 result.append(string.substring(lastCopied));
286 return result.toString();
287 }
288 }
289 }
290
291
292 /**
293 * @see org.apache.velocity.runtime.parser.node.SimpleNode#jjtAccept(org.apache.velocity.runtime.parser.node.ParserVisitor,
294 * java.lang.Object)
295 */
296 public Object jjtAccept(ParserVisitor visitor, Object data)
297 {
298 return visitor.visit(this, data);
299 }
300
301 /**
302 * Check to see if this is an interpolated string.
303 * @return true if this is constant (not an interpolated string)
304 * @since 1.6
305 */
306 public boolean isConstant()
307 {
308 return !interpolate;
309 }
310
311 /**
312 * renders the value of the string literal If the properties allow, and the
313 * string literal contains a $ or a # the literal is rendered against the
314 * context Otherwise, the stringlit is returned.
315 *
316 * @param context
317 * @return result of the rendering.
318 */
319 public Object value(InternalContextAdapter context)
320 {
321 if (interpolate)
322 {
323 try
324 {
325 /*
326 * now render against the real context
327 */
328
329 StringWriter writer = new StringWriter();
330 nodeTree.render(context, writer);
331
332 /*
333 * and return the result as a String
334 */
335
336 String ret = writer.toString();
337
338 /*
339 * if appropriate, remove the space from the end (dreaded <MORE>
340 * kludge part deux)
341 */
342 if (!containsLineComment && ret.length() > 0)
343 {
344 return ret.substring(0, ret.length() - 1);
345 }
346 else
347 {
348 return ret;
349 }
350 }
351
352 /**
353 * pass through application level runtime exceptions
354 */
355 catch (RuntimeException e)
356 {
357 throw e;
358 }
359
360 catch (IOException e)
361 {
362 String msg = "Error in interpolating string literal";
363 log.error(msg, e);
364 throw new VelocityException(msg, e);
365 }
366
367 }
368
369 /*
370 * ok, either not allowed to interpolate, there wasn't a ref or
371 * directive, or we failed, so just output the literal
372 */
373
374 return image;
375 }
376 }