PageRenderTime 56ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 1ms

/subprojects/groovy-templates/src/main/groovy/groovy/text/StreamingTemplateEngine.java

https://github.com/groovy/groovy-core
Java | 973 lines | 493 code | 93 blank | 387 comment | 111 complexity | ab2ae6889e1fd64f4bb1b8de05445536 MD5 | raw file
  1. /**
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. 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,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. package groovy.text;
  20. import groovy.lang.*;
  21. import org.codehaus.groovy.control.CompilationFailedException;
  22. import org.codehaus.groovy.control.ErrorCollector;
  23. import org.codehaus.groovy.control.MultipleCompilationErrorsException;
  24. import org.codehaus.groovy.control.messages.Message;
  25. import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
  26. import org.codehaus.groovy.runtime.StackTraceUtils;
  27. import org.codehaus.groovy.syntax.SyntaxException;
  28. import java.io.IOException;
  29. import java.io.LineNumberReader;
  30. import java.io.Reader;
  31. import java.io.StringReader;
  32. import java.security.AccessController;
  33. import java.security.PrivilegedAction;
  34. import java.util.ArrayList;
  35. import java.util.List;
  36. import java.util.Map;
  37. /**
  38. * Processes template source files substituting variables and expressions into
  39. * placeholders in a template source text to produce the desired output using a
  40. * closure based approach. This engine has equivalent functionality to the
  41. * {@link groovy.text.SimpleTemplateEngine} but creates the template using
  42. * writable closures making it more scalable for large templates.
  43. * <p>
  44. * Specifically this template engine can handle strings larger than 64k which
  45. * still causes problems for the other groovy template engines.
  46. * </p>
  47. * <p>
  48. * The template engine uses JSP style &lt;% %&gt; script and &lt;%= %&gt;
  49. * expression syntax or GString style expressions. The variable
  50. * '<code>out</code>' is bound to the writer that the template is being written
  51. * to.
  52. * </p>
  53. * Frequently, the template source will be in a file but here is a simple
  54. * example providing the template as a string:
  55. * <pre>
  56. *
  57. * def binding = [
  58. * firstname : "Grace",
  59. * lastname : "Hopper",
  60. * accepted : true,
  61. * title : 'Groovy for COBOL programmers'
  62. * ]
  63. * def text = '''\
  64. * Dear <% out.print firstname %> ${lastname},
  65. *
  66. * We <% if (accepted) out.print 'are pleased' else out.print 'regret' %> \
  67. * to inform you that your paper entitled
  68. * '$title' was ${ accepted ? 'accepted' : 'rejected' }.
  69. *
  70. * The conference committee.
  71. * '''
  72. *
  73. * def template = new groovy.text.StreamingTemplateEngine().createTemplate(text)
  74. *
  75. * print template.make(binding)
  76. * </pre>
  77. *
  78. * This example uses a mix of the JSP style and GString style
  79. * placeholders but you can typically use just one style if you wish. Running
  80. * this example will produce this output:
  81. *
  82. * <pre>
  83. *
  84. * Dear Grace Hopper,
  85. *
  86. * We are pleased to inform you that your paper entitled
  87. * 'Groovy for COBOL programmers' was accepted.
  88. *
  89. * The conference committee.
  90. * </pre>
  91. * <br />
  92. * <h3>StreamingTemplateEngine as a servlet engine</h3>
  93. * The template engine can also be used as the engine for
  94. * {@link groovy.servlet.TemplateServlet} by placing the following in your
  95. * <code>web.xml</code> file (plus a corresponding servlet-mapping element):
  96. * <pre>
  97. *
  98. * &lt;servlet&gt;
  99. * &lt;servlet-name&gt;StreamingTemplate&lt;/servlet-name&gt;
  100. * &lt;servlet-class&gt;groovy.servlet.TemplateServlet&lt;/servlet-class&gt;
  101. * &lt;init-param&gt;
  102. * &lt;param-name&gt;template.engine&lt;/param-name&gt;
  103. * &lt;param-value&gt;groovy.text.StreamingTemplateEngine&lt;/param-value&gt;
  104. * &lt;/init-param&gt;
  105. * &lt;/servlet&gt;
  106. * </pre> In this case, your template source file should be HTML with the
  107. * appropriate embedded placeholders.
  108. *
  109. * <h3>Debugging Template Code</h3>
  110. * <p>The template engine makes an effort to throw descriptive exceptions with
  111. * context lines, ie:
  112. * <pre>
  113. * groovy.text.TemplateExecutionException: Template parse error at line 4:
  114. * 3: We <% if (accepted) out.print 'are pleased' else out.print 'regret' %> to inform you that your paper entitled
  115. * --> 4: '$txitle' was ${ accepted ? 'accepted' : 'rejected' }.
  116. * 5:
  117. * at test.run(test.groovy:18)
  118. *
  119. * Caused by: groovy.lang.MissingPropertyException: No such property: txitle for class: groovy.tmp.templates.StreamingTemplateScript1
  120. * ... 1 more
  121. * </pre>
  122. * and sanitize the exceptions to make things readable.
  123. * </p>
  124. * <p>When the exceptions are not enough, it might sometimes be useful to view the actual script source
  125. * generated by the template engine. This would conceptually be equivalent to viewing the
  126. * .java file generated for a jsp page. The source is not currently very readable and
  127. * until we get a built in groovy code pretty printer, we will probably continue to opt for compactness
  128. * rather than readability.</p>
  129. *
  130. * <p>With that being said, viewing the source might still have some value. For this reason the script
  131. * source is accessible via the template.scriptSource property, i.e.:
  132. * <pre>
  133. * println template.scriptSource
  134. * </pre>
  135. * In the above example.
  136. * </p>
  137. *
  138. * @author mbjarland@gmail.com
  139. * @author Matias Bjarland
  140. */
  141. public class StreamingTemplateEngine extends TemplateEngine {
  142. private static final String TEMPLATE_SCRIPT_PREFIX = "StreamingTemplateScript";
  143. private final ClassLoader parentLoader;
  144. private static int counter = 1;
  145. /**
  146. * Create a streaming template engine instance using the default class loader
  147. */
  148. public StreamingTemplateEngine() {
  149. this(StreamingTemplate.class.getClassLoader());
  150. }
  151. /**
  152. * Create a streaming template engine instance using a custom class loader
  153. *
  154. * <p>The custom loader is used when parsing the template code</p>
  155. *
  156. * @param parentLoader The class loader to use when parsing the template code.
  157. */
  158. public StreamingTemplateEngine(ClassLoader parentLoader) {
  159. this.parentLoader = parentLoader;
  160. }
  161. /**
  162. * <p>Creates a template instance using the template source from the provided Reader.</p>
  163. *
  164. * <p>The template can be applied repeatedly on different bindings to produce custom
  165. * output.</p>
  166. *
  167. *
  168. * <strong>Technical detail</strong><br />
  169. * Under the hood the returned template is represented as a four argument
  170. * closure where the three first arguments are {@link groovy.lang.Closure#curry curried} in
  171. * while generating the template. <br />
  172. * <br />
  173. * In essence we start with a closure on the form:
  174. *
  175. * <pre>
  176. * { parentClass, stringSectionList, binding, out ->
  177. * //code generated by parsing the template data
  178. * } *
  179. * </pre>
  180. *
  181. * , we then curry in the parentClass and stringSectionList arguments so that the StreamingTemplate
  182. * instance returned from 'createTemplate' internally contains a template closure on the form:
  183. *
  184. * <pre>
  185. * { binding, out ->
  186. * //code generated by parsing the template data
  187. * } *
  188. * </pre>
  189. *
  190. * Calling template.make(binding), curries in the 'binding' argument:
  191. *
  192. * <pre>
  193. * public Writable make(final Map map) {
  194. * final Closure template = this.template.curry(new Object[]{map});
  195. * return (Writable) template;
  196. * }
  197. * </pre>
  198. *
  199. * which only leaves the 'out' argument unbound. The only method on the {@link groovy.lang.Writable writable} interface is
  200. * {@link groovy.lang.Writable#writeTo writeTo(Writer out)} so groovy rules about casting a closure to a one-method-interface
  201. * apply and the above works. I.e. we return the now one argument closure as the Writable
  202. * which can be serialized to System.out, a file, etc according to the Writable interface contract.
  203. * </p>
  204. * @see groovy.text.TemplateEngine#createTemplate(java.io.Reader)
  205. */
  206. @Override
  207. public Template createTemplate(final Reader reader) throws CompilationFailedException, ClassNotFoundException, IOException {
  208. return new StreamingTemplate(reader, parentLoader);
  209. }
  210. /**
  211. * The class used to implement the Template interface for the StreamingTemplateEngine
  212. *
  213. */
  214. private static class StreamingTemplate implements Template {
  215. /**
  216. * The 'header' we use for the resulting groovy script source.
  217. */
  218. private static final String SCRIPT_HEAD
  219. = "package groovy.tmp.templates;"
  220. + "def getTemplate() { "
  221. //the below params are:
  222. // _p - parent class, for handling exceptions
  223. // _s - sections, string sections list
  224. // _b - binding map
  225. // out - out stream
  226. //the three first parameters will be curried in as we move along
  227. + "return { _p, _s, _b, out -> "
  228. + "int _i = 0;"
  229. + "try {"
  230. + "delegate = new Binding(_b);";
  231. /**
  232. * The 'footer' we use for the resulting groovy script source
  233. */
  234. private static final String SCRIPT_TAIL
  235. = "} catch (Throwable e) { "
  236. + "_p.error(_i, _s, e);"
  237. + "}"
  238. + "}.asWritable()"
  239. + "}";
  240. private StringBuilder templateSource;
  241. // we use a hard index instead of incrementing the _i variable due to previous
  242. // bug where the increment was not executed when hitting non-executed if branch
  243. private int index = 0;
  244. final Closure template;
  245. String scriptSource;
  246. private static class FinishedReadingException extends Exception {}
  247. //WE USE THIS AS REUSABLE
  248. //CHECKSTYLE.OFF: ConstantNameCheck - special case with a reusable exception
  249. private static final FinishedReadingException finishedReadingException;
  250. //CHECKSTYLE.ON: ConstantNameCheck
  251. public static final StackTraceElement[] EMPTY_STACKTRACE = new StackTraceElement[0];
  252. static {
  253. finishedReadingException = new FinishedReadingException();
  254. finishedReadingException.setStackTrace(EMPTY_STACKTRACE);
  255. }
  256. private static final class Position {
  257. //CHECKSTYLE.OFF: VisibilityModifierCheck - special case, direct access for performance
  258. public int row;
  259. public int column;
  260. //CHECKSTYLE.ON: VisibilityModifierCheck
  261. private Position(int row, int column) {
  262. this.row = row;
  263. this.column = column;
  264. }
  265. private Position(Position p) {
  266. set(p);
  267. }
  268. private void set(Position p) {
  269. this.row = p.row;
  270. this.column = p.column;
  271. }
  272. public String toString() {
  273. return row + ":" + column;
  274. }
  275. }
  276. /**
  277. * A StringSection represent a section in the template source
  278. * with only string data (i.e. no branching, GString references, etc).
  279. * As an example, the following template string:
  280. *
  281. * <pre>
  282. * Alice why is a $bird like a writing desk
  283. * </pre>
  284. *
  285. * Would produce a string section "Alice why is a " followed by
  286. * a dollar identifier expression followed by another string
  287. * section " like a writing desk".
  288. */
  289. private static final class StringSection {
  290. StringBuilder data;
  291. Position firstSourcePosition;
  292. Position lastSourcePosition;
  293. Position lastTargetPosition;
  294. private StringSection(Position firstSourcePosition) {
  295. this.data = new StringBuilder();
  296. this.firstSourcePosition = new Position(firstSourcePosition);
  297. }
  298. @Override
  299. public String toString() {
  300. return data.toString();
  301. }
  302. }
  303. /**
  304. * Called to handle the ending of a string section.
  305. *
  306. * @param sections The list of string sections. The current section gets added to this section.
  307. * @param currentSection The current string section.
  308. * @param templateExpressions Template expressions
  309. * @param lastSourcePosition The last read position in the source template stream.
  310. * @param targetPosition The last written to position in the target script stream.
  311. */
  312. private void finishStringSection(List<StringSection> sections, StringSection currentSection,
  313. StringBuilder templateExpressions,
  314. Position lastSourcePosition, Position targetPosition) {
  315. //when we get exceptions from the parseXXX methods in the main loop, we might try to
  316. //re-finish a section
  317. if (currentSection.lastSourcePosition != null) {
  318. return;
  319. }
  320. currentSection.lastSourcePosition = new Position(lastSourcePosition);
  321. sections.add(currentSection);
  322. append(templateExpressions, targetPosition, "out<<_s[_i=" + index++ + "];");
  323. currentSection.lastTargetPosition = new Position(targetPosition.row, targetPosition.column);
  324. }
  325. public void error(int index, List<StringSection> sections, Throwable e) throws Throwable {
  326. int i = Math.max(0, index);
  327. StringSection precedingSection = sections.get(i);
  328. int traceLine = -1;
  329. for (StackTraceElement element : e.getStackTrace()) {
  330. if (element.getClassName().contains(TEMPLATE_SCRIPT_PREFIX)) {
  331. traceLine = element.getLineNumber();
  332. break;
  333. }
  334. }
  335. if (traceLine != -1) {
  336. int actualLine = precedingSection.lastSourcePosition.row + traceLine - 1;
  337. String message = "Template execution error at line " + actualLine + ":\n" + getErrorContext(actualLine);
  338. TemplateExecutionException unsanitized = new TemplateExecutionException(actualLine, message, StackTraceUtils.sanitize(e));
  339. throw StackTraceUtils.sanitize(unsanitized);
  340. } else {
  341. throw e;
  342. }
  343. }
  344. private int getLinesInSource() throws IOException {
  345. int result = 0;
  346. LineNumberReader reader = null;
  347. try {
  348. reader = new LineNumberReader(new StringReader(templateSource.toString()));
  349. reader.skip(Long.MAX_VALUE);
  350. result = reader.getLineNumber();
  351. } finally {
  352. if (reader != null) {
  353. reader.close();
  354. }
  355. }
  356. return result;
  357. }
  358. private String getErrorContext(int actualLine) throws IOException {
  359. int minLine = Math.max(0, actualLine -1);
  360. int maxLine = Math.min(getLinesInSource(), actualLine + 1);
  361. LineNumberReader r = new LineNumberReader(new StringReader(templateSource.toString()));
  362. int lineNr;
  363. StringBuilder result = new StringBuilder();
  364. while ((lineNr = r.getLineNumber()+1) <= maxLine) {
  365. String line = r.readLine();
  366. if (lineNr < minLine) continue;
  367. String nr = Integer.toString(lineNr);
  368. if (lineNr == actualLine) {
  369. nr = " --> " + nr;
  370. }
  371. result.append(padLeft(nr, 10));
  372. result.append(": ");
  373. result.append(line);
  374. result.append('\n');
  375. }
  376. return result.toString();
  377. }
  378. private String padLeft(String s, int len) {
  379. StringBuilder b = new StringBuilder(s);
  380. while (b.length() < len) b.insert(0, " ");
  381. return b.toString();
  382. }
  383. /**
  384. * Turn the template into a writable Closure. When executed the closure
  385. * evaluates all the code embedded in the template and then writes a
  386. * GString containing the fixed and variable items to the writer passed
  387. * as a parameter
  388. * <p/>
  389. * For example:
  390. * <pre>
  391. * '<%= "test" %> of expr and <% test = 1 %>${test} script.'
  392. * </pre>
  393. * would compile into:
  394. * <pre>
  395. * { out -> out << "${"test"} of expr and "; test = 1 ; out << "${test} script."}.asWritable()
  396. * </pre>
  397. * @param source A reader into the template source data
  398. * @param parentLoader A class loader we use
  399. * @throws CompilationFailedException
  400. * @throws ClassNotFoundException
  401. * @throws IOException
  402. */
  403. StreamingTemplate(final Reader source, final ClassLoader parentLoader) throws CompilationFailedException, ClassNotFoundException, IOException {
  404. final StringBuilder target = new StringBuilder();
  405. List<StringSection> sections = new ArrayList<StringSection>();
  406. Position sourcePosition = new Position(1, 1);
  407. Position targetPosition = new Position(1, 1);
  408. Position lastSourcePosition = new Position(1, 1);
  409. StringSection currentSection = new StringSection(sourcePosition);
  410. templateSource = new StringBuilder();
  411. //we use the lookAhead to make sure that a template file ending in say "abcdef\\"
  412. //will give a result of "abcdef\\" even though we have special handling for \\
  413. StringBuilder lookAhead = new StringBuilder(10);
  414. append(target, targetPosition, SCRIPT_HEAD);
  415. try {
  416. int skipRead = -1;
  417. //noinspection InfiniteLoopStatement
  418. while (true) {
  419. lastSourcePosition.set(sourcePosition);
  420. int c = (skipRead != -1) ? skipRead : read(source, sourcePosition, lookAhead);
  421. skipRead = -1;
  422. if (c == '\\') {
  423. handleEscaping(source, sourcePosition, currentSection, lookAhead);
  424. continue;
  425. } else if (c == '<') {
  426. c = read(source, sourcePosition, lookAhead);
  427. if (c == '%') {
  428. c = read(source, sourcePosition);
  429. clear(lookAhead);
  430. if (c == '=') {
  431. finishStringSection(sections, currentSection, target, lastSourcePosition, targetPosition);
  432. parseExpression(source, target, sourcePosition, targetPosition);
  433. currentSection = new StringSection(sourcePosition);
  434. continue;
  435. } else {
  436. finishStringSection(sections, currentSection, target, lastSourcePosition, targetPosition);
  437. parseSection(c, source, target, sourcePosition, targetPosition);
  438. currentSection = new StringSection(sourcePosition);
  439. continue;
  440. }
  441. } else {
  442. currentSection.data.append('<');
  443. }
  444. } else if (c == '$') {
  445. c = read(source, sourcePosition);
  446. clear(lookAhead);
  447. if (c == '{') {
  448. finishStringSection(sections, currentSection, target, lastSourcePosition, targetPosition);
  449. parseDollarCurlyIdentifier(source, target, sourcePosition, targetPosition);
  450. currentSection = new StringSection(sourcePosition);
  451. continue;
  452. } else if (Character.isJavaIdentifierStart(c)) {
  453. finishStringSection(sections, currentSection, target, lastSourcePosition, targetPosition);
  454. skipRead = parseDollarIdentifier(c, source, target, sourcePosition, targetPosition);
  455. currentSection = new StringSection(sourcePosition);
  456. continue;
  457. } else {
  458. currentSection.data.append('$');
  459. }
  460. }
  461. currentSection.data.append((char) c);
  462. clear(lookAhead);
  463. }
  464. } catch (FinishedReadingException e) {
  465. if (lookAhead.length() > 0) {
  466. currentSection.data.append(lookAhead);
  467. }
  468. //Ignored here, just used for exiting the read loop. Yeah I know we don't like
  469. //empty catch blocks or expected behavior trowing exceptions, but this just cleaned out the code
  470. //_so_ much that I thought it worth it...this once -Matias Bjarland 20100126
  471. }
  472. finishStringSection(sections, currentSection, target, sourcePosition, targetPosition);
  473. append(target, targetPosition, SCRIPT_TAIL);
  474. scriptSource = target.toString();
  475. this.template = createTemplateClosure(sections, parentLoader, target);
  476. }
  477. private static void clear(StringBuilder lookAhead) {
  478. lookAhead.delete(0, lookAhead.length());
  479. }
  480. private void handleEscaping(final Reader source,
  481. final Position sourcePosition,
  482. final StringSection currentSection,
  483. final StringBuilder lookAhead) throws IOException, FinishedReadingException {
  484. //if we get here, we just read in a back-slash from the source, now figure out what to do with it
  485. int c = read(source, sourcePosition, lookAhead);
  486. /*
  487. The _only_ special escaping this template engine allows is to escape the sequences:
  488. ${ and <% and potential slashes in front of these. Escaping in any other sections of the
  489. source string is ignored. The following is a source -> result mapping of a few values, assume a
  490. binding of [alice: 'rabbit'].
  491. Note: we don't do java escaping of slashes in the below
  492. example, i.e. the source string is what you would see in a text editor when looking at your template
  493. file:
  494. source string result
  495. 'bob' -> 'bob'
  496. '\bob' -> '\bob'
  497. '\\bob' -> '\\bob'
  498. '${alice}' -> 'rabbit'
  499. '\${alice}' -> '${alice}'
  500. '\\${alice}' -> '\rabbit'
  501. '\\$bob' -> '\\$bob'
  502. '\\' -> '\\'
  503. '\\\' -> '\\\'
  504. '%<= alice %>' -> 'rabbit'
  505. '\%<= alice %>' -> '%<= alice %>'
  506. */
  507. if (c == '\\') {
  508. //this means we have received a double backslash sequence
  509. //if this is followed by ${ or <% we output one backslash
  510. //and interpret the following sequences with groovy, if followed by anything
  511. //else we output the two backslashes and continue as usual
  512. source.mark(3);
  513. int d = read(source, sourcePosition, lookAhead);
  514. c = read(source, sourcePosition, lookAhead);
  515. clear(lookAhead);
  516. if ((d == '$' && c == '{') ||
  517. (d == '<' && c == '%')) {
  518. source.reset();
  519. currentSection.data.append('\\');
  520. return;
  521. } else {
  522. currentSection.data.append('\\');
  523. currentSection.data.append('\\');
  524. currentSection.data.append((char) d);
  525. }
  526. } else if (c == '$') {
  527. c = read(source, sourcePosition, lookAhead);
  528. if (c == '{') {
  529. currentSection.data.append('$');
  530. } else {
  531. currentSection.data.append('\\');
  532. currentSection.data.append('$');
  533. }
  534. } else if (c == '<') {
  535. c = read(source, sourcePosition, lookAhead);
  536. if (c == '%') {
  537. currentSection.data.append('<');
  538. } else {
  539. currentSection.data.append('\\');
  540. currentSection.data.append('<');
  541. }
  542. } else {
  543. currentSection.data.append('\\');
  544. }
  545. currentSection.data.append((char) c);
  546. clear(lookAhead);
  547. }
  548. private Closure createTemplateClosure(List<StringSection> sections, final ClassLoader parentLoader, StringBuilder target) throws ClassNotFoundException {
  549. final GroovyClassLoader loader = AccessController.doPrivileged(new PrivilegedAction<GroovyClassLoader>() {
  550. public GroovyClassLoader run() {
  551. return new GroovyClassLoader(parentLoader);
  552. }
  553. });
  554. final Class groovyClass;
  555. try {
  556. groovyClass = loader.parseClass(new GroovyCodeSource(target.toString(), TEMPLATE_SCRIPT_PREFIX + counter++ + ".groovy", "x"));
  557. } catch (MultipleCompilationErrorsException e) {
  558. throw mangleMultipleCompilationErrorsException(e, sections);
  559. } catch (Exception e) {
  560. throw new GroovyRuntimeException("Failed to parse template script (your template may contain an error or be trying to use expressions not currently supported): " + e.getMessage());
  561. }
  562. Closure result;
  563. try {
  564. final GroovyObject object = (GroovyObject) groovyClass.newInstance();
  565. Closure chicken = (Closure) object.invokeMethod("getTemplate", null);
  566. //bind the two first parameters of the generated closure to this class and the sections list
  567. result = chicken.curry(this, sections);
  568. } catch (InstantiationException e) {
  569. throw new ClassNotFoundException(e.getMessage());
  570. } catch (IllegalAccessException e) {
  571. throw new ClassNotFoundException(e.getMessage());
  572. }
  573. return result;
  574. }
  575. /**
  576. * Parses a non curly dollar preceded identifier of the type
  577. * '$bird' in the following template example:
  578. *
  579. * <pre>
  580. * Alice why is a $bird like a writing desk
  581. * </pre>
  582. *
  583. * which would produce the following template data:
  584. *
  585. * <pre>
  586. * out << "Alice why is a ";
  587. * out << bird;
  588. * out << " like a writing desk";
  589. * </pre>
  590. *
  591. * This method is given the 'b' in 'bird' in argument c, checks if it is a valid
  592. * java identifier start (we assume groovy did not mangle the java
  593. * identifier rules). If so it proceeds to parse characters from the input
  594. * until it encounters a non-java-identifier character. At that point
  595. *
  596. * @param c The first letter of the potential identifier, 'b' in the above example
  597. * @param reader The reader reading from the template source
  598. * @param target The target groovy script source we write to
  599. * @param sourcePosition The reader position in the source stream
  600. * @param targetPosition The writer position in the target stream
  601. * @return true if a valid dollar preceded identifier was found, false otherwise. More
  602. * specifically, returns true if the first character after the dollar sign is
  603. * a valid java identifier. Note that the dollar curly syntax is handled by
  604. * another method.
  605. *
  606. * @throws IOException
  607. * @throws FinishedReadingException If we encountered the end of the source stream.
  608. */
  609. private int parseDollarIdentifier(int c ,
  610. final Reader reader,
  611. final StringBuilder target,
  612. final Position sourcePosition,
  613. final Position targetPosition) throws IOException, FinishedReadingException {
  614. append(target, targetPosition, "out<<");
  615. append(target, targetPosition, (char) c);
  616. while (true) {
  617. c = read(reader, sourcePosition);
  618. if (!Character.isJavaIdentifierPart(c) || c == '$') {
  619. break;
  620. }
  621. append(target, targetPosition, (char) c);
  622. }
  623. append(target, targetPosition, ";");
  624. return c;
  625. }
  626. /**
  627. * Parses a dollar curly preceded identifier of the type
  628. * '${bird}' in the following template example:
  629. *
  630. * <pre>
  631. * Alice why is a ${bird} like a writing desk
  632. * </pre>
  633. *
  634. * which would produce the following template data:
  635. *
  636. * <pre>
  637. * out << "Alice why is a ";
  638. * out << """${bird}""";
  639. * out << " like a writing desk";
  640. * </pre>
  641. *
  642. * This method is given the 'b' in 'bird' in argument c, checks if it is a valid
  643. * java identifier start (we assume groovy did not mangle the java
  644. * identifier rules). If so it proceeds to parse characters from the input
  645. * until it encounters a non-java-identifier character. At that point
  646. *
  647. * @param reader The reader reading from the template source
  648. * @param target The target groovy script source we write to
  649. * @param sourcePosition The reader position in the source stream
  650. * @param targetPosition The writer position in the target stream
  651. * @throws IOException
  652. * @throws FinishedReadingException
  653. */
  654. private void parseDollarCurlyIdentifier(final Reader reader,
  655. final StringBuilder target,
  656. final Position sourcePosition,
  657. final Position targetPosition) throws IOException, FinishedReadingException {
  658. append(target, targetPosition, "out<<\"\"\"${");
  659. while (true) {
  660. int c = read(reader, sourcePosition);
  661. append(target, targetPosition, (char) c);
  662. if (c == '}') break;
  663. }
  664. append(target, targetPosition, "\"\"\";");
  665. }
  666. /**
  667. * Parse a <% .... %> section if we are writing a GString close and
  668. * append ';' then write the section as a statement
  669. */
  670. private void parseSection(final int pendingC,
  671. final Reader reader,
  672. final StringBuilder target,
  673. final Position sourcePosition,
  674. final Position targetPosition) throws IOException, FinishedReadingException {
  675. //the below is a quirk, we do this so that every non-string-section is prefixed by
  676. //the same number of characters (the others have "out<<\"\"\"${"), this allows us to
  677. //figure out the exception row and column later on
  678. append(target, targetPosition, " ");
  679. append(target, targetPosition, (char) pendingC);
  680. while (true) {
  681. int c = read(reader, sourcePosition);
  682. if (c == '%') {
  683. c = read(reader, sourcePosition);
  684. if (c == '>') break;
  685. append(target, targetPosition, '%');
  686. }
  687. append(target, targetPosition, (char) c);
  688. }
  689. append(target, targetPosition, ';');
  690. }
  691. /**
  692. * Parse a <%= .... %> expression
  693. */
  694. private void parseExpression(final Reader reader,
  695. final StringBuilder target,
  696. final Position sourcePosition,
  697. final Position targetPosition) throws IOException, FinishedReadingException {
  698. append(target, targetPosition, "out<<\"\"\"${");
  699. while (true) {
  700. int c = read(reader, sourcePosition);
  701. if (c == '%') {
  702. c = read(reader, sourcePosition);
  703. if (c == '>') break;
  704. append(target, targetPosition, '%');
  705. }
  706. append(target, targetPosition, (char) c);
  707. }
  708. append(target, targetPosition, "}\"\"\";");
  709. }
  710. @Override
  711. public Writable make() {
  712. return make(null);
  713. }
  714. @Override
  715. public Writable make(final Map map) {
  716. //we don't need a template.clone here as curry calls clone under the hood
  717. final Closure template = this.template.curry(new Object[]{map});
  718. return (Writable) template;
  719. }
  720. /*
  721. * Create groovy assertion style error message for template error. Example:
  722. *
  723. * Error parsing expression on line 71 column 15, message: no such property jboss for for class DUMMY
  724. * templatedata${jboss}templateddatatemplateddata
  725. * ^------^
  726. * |
  727. * syntax error
  728. */
  729. private RuntimeException mangleMultipleCompilationErrorsException(MultipleCompilationErrorsException e, List<StringSection> sections) {
  730. RuntimeException result = e;
  731. ErrorCollector collector = e.getErrorCollector();
  732. @SuppressWarnings({"unchecked"})
  733. List<Message> errors = (List<Message>) collector.getErrors();
  734. if (errors.size() > 0) {
  735. Message firstMessage = errors.get(0);
  736. if (firstMessage instanceof SyntaxErrorMessage) {
  737. @SuppressWarnings({"ThrowableResultOfMethodCallIgnored"})
  738. SyntaxException syntaxException = ((SyntaxErrorMessage) firstMessage).getCause();
  739. Position errorPosition = new Position(syntaxException.getLine(), syntaxException.getStartColumn());
  740. //find the string section which precedes the row/col of the thrown exception
  741. StringSection precedingSection = findPrecedingSection(errorPosition, sections);
  742. //and now use the string section to mangle the line numbers so that they refer to the
  743. //appropriate line in the source template data
  744. if (precedingSection != null) {
  745. //if the error was thrown on the same row as where the last string section
  746. //ended, fix column value
  747. offsetPositionFromSection(errorPosition, precedingSection);
  748. //the below being true indicates that we had an unterminated ${ or <% sequence and
  749. //the column is thus meaningless, we reset it to where the %{ or <% starts to at
  750. //least give the user a sporting chance
  751. if (sections.get(sections.size() - 1) == precedingSection) {
  752. errorPosition.column = precedingSection.lastSourcePosition.column;
  753. }
  754. String message = mangleExceptionMessage(e.getMessage(), errorPosition);
  755. result = new TemplateParseException(message, e, errorPosition.row, errorPosition.column);
  756. }
  757. }
  758. }
  759. return result;
  760. }
  761. private String mangleExceptionMessage(String original, Position p) {
  762. String result = original;
  763. int index = result.indexOf("@ line ");
  764. if (index != -1) {
  765. result = result.substring(0, index);
  766. }
  767. int count = 0;
  768. index = 0;
  769. for (char c : result.toCharArray()) {
  770. if (c == ':') {
  771. count++;
  772. if (count == 3) {
  773. result = result.substring(index + 2);
  774. break;
  775. }
  776. }
  777. index++;
  778. }
  779. String msg = "Template parse error '" + result + "' at line " + p.row + ", column " + p.column;
  780. try {
  781. msg += "\n" + getErrorContext(p.row);
  782. } catch (IOException e) {
  783. //we opt for not doing anthing here...we just do not get context if
  784. //this happens
  785. }
  786. return msg;
  787. }
  788. private void offsetPositionFromSection(Position p, StringSection s) {
  789. if (p.row == s.lastTargetPosition.row) {
  790. //The number 8 below represents the number of characters in the header of a non-string-section such as
  791. //<% ... %>. A section like this is represented in the target script as:
  792. //out<<"""......."""
  793. //12345678
  794. p.column -= s.lastTargetPosition.column + 8;
  795. p.column += s.lastSourcePosition.column;
  796. }
  797. p.row += s.lastSourcePosition.row - 1;
  798. }
  799. private StringSection findPrecedingSection(Position p, List<StringSection> sections) {
  800. StringSection result = null;
  801. for (StringSection s : sections) {
  802. if (s.lastTargetPosition.row > p.row
  803. || (s.lastTargetPosition.row == p.row && s.lastTargetPosition.column > p.column)) {
  804. break;
  805. }
  806. result = s;
  807. }
  808. return result;
  809. }
  810. private void append(final StringBuilder target, Position targetPosition, char c) {
  811. if (c == '\n') {
  812. targetPosition.row++;
  813. targetPosition.column = 1;
  814. } else {
  815. targetPosition.column++;
  816. }
  817. target.append(c);
  818. }
  819. private void append(final StringBuilder target, Position targetPosition, String s) {
  820. int len = s.length();
  821. for (int i = 0; i < len; i++) {
  822. append(target, targetPosition, s.charAt(i));
  823. }
  824. }
  825. private int read(final Reader reader, Position position, StringBuilder lookAhead) throws IOException, FinishedReadingException {
  826. int c = read(reader, position);
  827. lookAhead.append((char) c);
  828. return c;
  829. }
  830. // SEE BELOW
  831. boolean useLastRead = false;
  832. private int lastRead = -1;
  833. /* All \r\n sequences are treated as a single \n. By doing this we
  834. * produce the same output as the GStringTemplateEngine. Otherwise, some
  835. * of our output is on a newline when it should not be.
  836. *
  837. * Instead of using a pushback reader, we just keep a private instance
  838. * variable 'lastRead'.
  839. */
  840. private int read(final Reader reader, Position position) throws IOException, FinishedReadingException {
  841. int c;
  842. if (useLastRead) {
  843. // use last one if we stored a character
  844. c = lastRead;
  845. // reset last
  846. useLastRead = false;
  847. lastRead = -1;
  848. } else {
  849. c = read(reader);
  850. if (c == '\r') {
  851. // IF CRLF JUST KEEP LF
  852. c = read(reader);
  853. if (c != '\n') {
  854. // ELSE keep original char
  855. // and pushback the one we just read
  856. lastRead = c;
  857. useLastRead = true;
  858. c = '\r';
  859. }
  860. }
  861. }
  862. if (c == -1) {
  863. throw finishedReadingException;
  864. }
  865. if (c == '\n') {
  866. position.row++;
  867. position.column = 1;
  868. } else {
  869. position.column++;
  870. }
  871. return c;
  872. }
  873. private int read(final Reader reader) throws IOException {
  874. int c = reader.read();
  875. templateSource.append((char) c);
  876. return c;
  877. }
  878. }
  879. }