From d68715b3e62b4f1b8ebd7fa051004934a2ba913d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Wed, 27 Dec 2017 16:54:45 -0800 Subject: Initial version. This is forked from the code built in to AutoValue, with the following non-trivial changes: (1) Package changed from com.google.auto.value.processor.escapevelocity to com.google.escapevelocity. (2) New pom.xml. (3) Code rewritten to remove Guava dependency, so no shading or diamond dependency problems. --- CONTRIBUTING.md | 23 + LICENSE | 202 +++++ NOTICE | 6 + README.md | 344 ++++++++ pom.xml | 98 +++ .../escapevelocity/ConstantExpressionNode.java | 41 + .../com/google/escapevelocity/DirectiveNode.java | 193 +++++ .../google/escapevelocity/EvaluationContext.java | 79 ++ .../google/escapevelocity/EvaluationException.java | 32 + .../com/google/escapevelocity/ExpressionNode.java | 186 ++++ .../google/escapevelocity/ImmutableAsciiSet.java | 100 +++ .../com/google/escapevelocity/ImmutableList.java | 93 ++ .../com/google/escapevelocity/ImmutableSet.java | 58 ++ src/main/java/com/google/escapevelocity/Macro.java | 134 +++ src/main/java/com/google/escapevelocity/Node.java | 89 ++ .../com/google/escapevelocity/ParseException.java | 39 + .../java/com/google/escapevelocity/Parser.java | 963 +++++++++++++++++++++ src/main/java/com/google/escapevelocity/README.md | 378 ++++++++ .../com/google/escapevelocity/ReferenceNode.java | 436 ++++++++++ .../java/com/google/escapevelocity/Reparser.java | 288 ++++++ .../java/com/google/escapevelocity/Template.java | 112 +++ .../java/com/google/escapevelocity/TokenNode.java | 167 ++++ .../google/escapevelocity/ImmutableSetTest.java | 43 + .../google/escapevelocity/ReferenceNodeTest.java | 106 +++ .../com/google/escapevelocity/TemplateTest.java | 653 ++++++++++++++ 25 files changed, 4863 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/com/google/escapevelocity/ConstantExpressionNode.java create mode 100644 src/main/java/com/google/escapevelocity/DirectiveNode.java create mode 100644 src/main/java/com/google/escapevelocity/EvaluationContext.java create mode 100644 src/main/java/com/google/escapevelocity/EvaluationException.java create mode 100644 src/main/java/com/google/escapevelocity/ExpressionNode.java create mode 100644 src/main/java/com/google/escapevelocity/ImmutableAsciiSet.java create mode 100644 src/main/java/com/google/escapevelocity/ImmutableList.java create mode 100644 src/main/java/com/google/escapevelocity/ImmutableSet.java create mode 100644 src/main/java/com/google/escapevelocity/Macro.java create mode 100644 src/main/java/com/google/escapevelocity/Node.java create mode 100644 src/main/java/com/google/escapevelocity/ParseException.java create mode 100644 src/main/java/com/google/escapevelocity/Parser.java create mode 100644 src/main/java/com/google/escapevelocity/README.md create mode 100644 src/main/java/com/google/escapevelocity/ReferenceNode.java create mode 100644 src/main/java/com/google/escapevelocity/Reparser.java create mode 100644 src/main/java/com/google/escapevelocity/Template.java create mode 100644 src/main/java/com/google/escapevelocity/TokenNode.java create mode 100644 src/test/java/com/google/escapevelocity/ImmutableSetTest.java create mode 100644 src/test/java/com/google/escapevelocity/ReferenceNodeTest.java create mode 100644 src/test/java/com/google/escapevelocity/TemplateTest.java diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ae319c7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution, +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..1527b34 --- /dev/null +++ b/NOTICE @@ -0,0 +1,6 @@ +Apache Velocity + +Copyright (C) 2000-2007 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/README.md b/README.md new file mode 100644 index 0000000..839c002 --- /dev/null +++ b/README.md @@ -0,0 +1,344 @@ +# EscapeVelocity summary + +EscapeVelocity is a templating engine that can be used from Java. It is a reimplementation of a subset of +functionality from [Apache Velocity](http://velocity.apache.org/). + +This is not an official Google product. + +For a fuller explanation of Velocity's functioning, see its +[User Guide](http://velocity.apache.org/engine/releases/velocity-1.7/user-guide.html) + +If EscapeVelocity successfully produces a result from a template evaluation, that result should be +the exact same string that Velocity produces. If not, that is a bug. + +EscapeVelocity has no facilities for HTML escaping and it is not appropriate for producing +HTML output that might include portions of untrusted input. + + +[TOC] + + +## Motivation + +Velocity has a convenient templating language. It is easy to read, and it has widespread support +from tools such as editors and coding websites. However, *using* Velocity can prove difficult. +Its use to generate Java code in the [AutoValue][AutoValue] annotation processor required many +[workarounds][VelocityHacks]. The way it dynamically loads classes as part of its standard operation +makes it hard to [shade](https://maven.apache.org/plugins/maven-shade-plugin/) it, which in the case +of AutoValue led to interference if Velocity was used elsewhere in a project. + +EscapeVelocity has a simple API that does not involve any class-loading or other sources of +problems. It and its dependencies can be shaded with no difficulty. + +## Loading a template + +The entry point for EscapeVelocity is the `Template` class. To obtain an instance, use +`Template.from(Reader)`. If a template is stored in a file, that file conventionally has the +suffix `.vm` (for Velocity Macros). But since the argument is a `Reader`, you can also load +a template directly from a Java string, using `StringReader`. + +Here's how you might make a `Template` instance from a template file that is packaged as a resource +in the same package as the calling class: + +```java +InputStream in = getClass().getResourceAsStream("foo.vm"); +if (in == null) { + throw new IllegalArgumentException("Could not find resource foo.vm"); +} +Reader reader = new BufferedReader(new InputStreamReader(in)); +Template template = Template.parseFrom(reader); +``` + +## Expanding a template + +Once you have a `Template` object, you can use it to produce a string where the variables in the +template are given the values you provide. You can do this any number of times, specifying the +same or different values each time. + +Suppose you have this template: + +``` +The $language word for $original is $translated. +``` + +You might write this code: + +```java +Map vars = new HashMap<>(); +vars.put("language", "French"); +vars.put("original", "toe"); +vars.put("translated", "orteil"); +String result = template.evaluate(vars); +``` + +The `result` string would then be: `The French word for toe is orteil.` + +## Comments + +The characters `##` introduce a comment. Characters from `##` up to and including the following +newline are omitted from the template. This template has comments: + +``` +Line 1 ## with a comment +Line 2 +``` + +It is the same as this template: +``` +Line 1 Line 2 +``` + +## References + +EscapeVelocity supports most of the reference types described in the +[Velocity User Guide](http://velocity.apache.org/engine/releases/velocity-1.7/user-guide.html#References) + +### Variables + +A variable has an ASCII name that starts with a letter (a-z or A-Z) and where any other characters +are also letters or digits or hyphens (-) or underscores (_). A variable reference can be written +as `$foo` or as `${foo}`. The value of a variable can be of any Java type. If the value `v` of +variable `foo` is not a String then the result of `$foo` in a template will be `String.valueOf(v)`. +Variables must be defined before they are referenced; otherwise an `EvaluationException` will be +thrown. + +Variable names are case-sensitive: `$foo` is not the same variable as `$Foo` or `$FOO`. + +Initially the values of variables come from the Map that is passed to `Template.evaluate`. Those +values can be changed, and new ones defined, using the `#set` directive in the template: + +``` +#set ($foo = "bar") +``` + +Setting a variable affects later references to it in the template, but has no effect on the +`Map` that was passed in or on later template evaluations. + +### Properties + +If a reference looks like `$purchase.Total` then the value of the `$purchase` variable must be a +Java object that has a public method `getTotal()` or `gettotal()`, or a method called `isTotal()` or +`istotal()` that returns `boolean`. The result of `$purchase.Total` is then the result of calling +that method on the `$purchase` object. + +If you want to have a period (`.`) after a variable reference *without* it being a property +reference, you can use braces like this: `${purchase}.Total`. If, after a property reference, you +have a further period, you can put braces around the reference like this: +`${purchase.Total}.nonProperty`. + +### Methods + +If a reference looks like `$purchase.addItem("scones", 23)` then the value of the `$purchase` +variable must be a Java object that has a public method `addItem` with two parameters that match +the given values. Unlike Velocity, EscapeVelocity requires that there be exactly one such method. +It is OK if there are other `addItem` methods provided they are not compatible with the +arguments provided. + +Properties are in fact a special case of methods: instead of writing `$purchase.Total` you could +write `$purchase.getTotal()`. Braces can be used to make the method invocation explicit +(`${purchase.getTotal()}`) or to prevent method invocation (`${purchase}.getTotal()`). + +### Indexing + +If a reference looks like `$indexme[$i]` then the value of the `$indexme` variable must be a Java +object that has a public `get` method that takes one argument that is compatible with the index. +For example, `$indexme` might be a `List` and `$i` might be an integer. Then the reference would +be the result of `List.get(int)` for that list and that integer. Or, `$indexme` might be a `Map`, +and the reference would be the result of `Map.get(Object)` for the object `$i`. In general, +`$indexme[$i]` is equivalent to `$indexme.get($i)`. + +Unlike Velocity, EscapeVelocity does not allow `$indexme` to be a Java array. + +### Undefined references + +If a variable has not been given a value, either by being in the initial Map argument or by being +set in the template, then referencing it will provoke an `EvaluationException`. There is +a special case for `#if`: if you write `#if ($var)` then it is allowed for `$var` not to be defined, +and it is treated as false. + +### Setting properties and indexes: not supported + +Unlke Velocity, EscapeVelocity does not allow `#set` assignments with properties or indexes: + +``` +#set ($data.User = "jon") ## Allowed in Velocity but not in EscapeVelocity +#set ($map["apple"] = "orange") ## Allowed in Velocity but not in EscapeVelocity +``` + +## Expressions + +In certain contexts, such as the `#set` directive we have just seen or certain other directives, +EscapeVelocity can evaluate expressions. An expression can be any of these: + +* A reference, of the kind we have just seen. The value is the value of the reference. +* A string literal enclosed in double quotes, like `"this"`. A string literal must appear on + one line. EscapeVelocity does not support the characters `$` or `\\` in a string literal. +* An integer literal such as `23` or `-100`. EscapeVelocity does not support floating-point + literals. +* A Boolean literal, `true` or `false`. +* Simpler expressions joined together with operators that have the same meaning as in Java: + `!`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `&&`, `||`, `+`, `-`, `*`, `/`, `%`. The operators have the + same precedence as in Java. +* A simpler expression in parentheses, for example `(2 + 3)`. + +Velocity supports string literals with single quotes, like `'this`' and also references within +strings, like `"a $reference in a string"`, but EscapeVelocity does not. + +## Directives + +A directive is introduced by a `#` character followed by a word. We have already seen the `#set` +directive, which sets the value of a variable. The other directives are listed below. + +Directives can be spelled with or without braces, so `#set` or `#{set}`. + +### `#if`/`#elseif`/`#else` + +The `#if` directive selects parts of the template according as a condition is true or false. +The simplest case looks like this: + +``` +#if ($condition) yes #end +``` + +This evaluates to the string ` yes ` if the variable `$condition` is defined and has a true value, +and to the empty string otherwise. It is allowed for `$condition` not to be defined in this case, +and then it is treated as false. + +The expression in `#if` (here `$condition`) is considered true if its value is not null and not +equal to the Boolean value `false`. + +An `#if` directive can also have an `#else` part, for example: + +``` +#if ($condition) yes #else no #end +``` + +This evaluates to the string ` yes ` if the condition is true or the string ` no ` if it is not. + +An `#if` directive can have any number of `#elseif` parts. For example: + +``` +#if ($i == 0) zero #elseif ($i == 1) one #elseif ($i == 2) two #else many #end +``` + +### `#foreach` + +The `#foreach` directive repeats a part of the template once for each value in a list. + +``` +#foreach ($product in $allProducts) + ${product}! +#end +``` + +This will produce one line for each value in the `$allProducts` variable. The value of +`$allProducts` can be a Java `Iterable`, such as a `List` or `Set`; or it can be an object array; +or it can be a Java `Map`. When it is a `Map` the `#foreach` directive loops over every *value* +in the `Map`. + +If `$allProducts` is a `List` containing the strings `oranges` and `lemons` then the result of the +`#foreach` would be this: + +``` + + oranges! + + + lemons! + +``` + +When the `#foreach` completes, the loop variable (`$product` in the example) goes back to whatever +value it had before, or to being undefined if it was undefined before. + +Within the `#foreach`, a special variable `$foreach` is defined, such that you can write +`$foreach.hasNext`, which will be true if there are more values after this one or false if this +is the last value. For example: + +``` +#foreach ($product in $allProducts)${product}#if ($foreach.hasNext), #end#end +``` + +This would produce the output `oranges, lemons` for the list above. (The example is scrunched up +to avoid introducing extraneous spaces, as described in the [section](#spaces) on spaces +below.) + +Velocity gives the `$foreach` variable other properties (`index` and `count`) but EscapeVelocity +does not. + +### Macros + +A macro is a part of the template that can be reused in more than one place, potentially with +different parameters each time. In the simplest case, a macro has no arguments: + +``` +#macro (hello) bonjour #end +``` + +Then the macro can be referenced by writing `#hello()` and the result will be the string ` bonjour ` +inserted at that point. + +Macros can also have parameters: + +``` +#macro (greet $hello $world) $hello, $world! #end +``` + +Then `#greet("bonjour", "monde")` would produce ` bonjour, monde! `. The comma is optional, so +you could also write `#greet("bonjour" "monde")`. + +When a macro completes, the parameters (`$hello` and `$world` in the example) go back to whatever +values they had before, or to being undefined if they were undefined before. + +All macro definitions take effect before the template is evaluated, so you can use a macro at a +point in the template that is before the point where it is defined. This also means that you can't +define a macro conditionally: + +``` +## This doesn't work! +#if ($language == "French") +#macro (hello) bonjour #end +#else +#macro (hello) hello #end +#end +``` + +There is no particular reason to define the same macro more than once, but if you do it is the +first definition that is retained. In the `#if` example just above, the `bonjour` version will +always be used. + +Macros can make templates hard to understand. You may prefer to put the logic in a Java method +rather than a macro, and call the method from the template using `$methods.doSomething("foo")` +or whatever. + +## Spaces + +For the most part, spaces and newlines in the template are preserved exactly in the output. +To avoid unwanted newlines, you may end up using `##` comments. In the `#foreach` example above +we had this: + +``` +#foreach ($product in $allProducts)${product}#if ($foreach.hasNext), #end#end +``` + +That was to avoid introducing unwanted spaces and newlines. A more readable way to achieve the same +result is this: + +``` +#foreach ($product in $allProducts)## +${product}## +#if ($foreach.hasNext), #end## +#end +``` + +Spaces are ignored between the `#` of a directive and the `)` that closes it, so there is no trace +in the output of the spaces in `#foreach ($product in $allProducts)` or `#if ($foreach.hasNext)`. +Spaces are also ignored inside references, such as `$indexme[ $i ]` or `$callme( $i , $j )`. + +If you are concerned about the detailed formatting of the text from the template, you may want to +post-process it. For example, if it is Java code, you could use a formatter such as +[google-java-format](https://github.com/google/google-java-format). Then you shouldn't have to +worry about extraneous spaces. + +[VelocityHacks]: https://github.com/google/auto/blob/ca2384d5ad15a0c761b940384083cf5c50c6e839/value/src/main/java/com/google/auto/value/processor/TemplateVars.java#L54 +[AutoValue]: https://github.com/google/auto/tree/master/value diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a58896d --- /dev/null +++ b/pom.xml @@ -0,0 +1,98 @@ + + + + 4.0.0 + + com.google.escapevelocity + escapevelocity + 0.9-SNAPSHOT + EscapeVelocity + + A reimplementation of a subset of the Apache Velocity templating system. + + + + + + + com.google.guava + guava + 23.5-jre + test + + + + org.apache.velocity + velocity + 1.7 + test + + + com.google.guava + guava-testlib + 23.5-jre + test + + + junit + junit + 4.12 + test + + + com.google.truth + truth + 0.36 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + 1.7 + 1.7 + -Xlint:all + true + true + + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + org.apache.maven.plugins + maven-invoker-plugin + 3.0.1 + + + + diff --git a/src/main/java/com/google/escapevelocity/ConstantExpressionNode.java b/src/main/java/com/google/escapevelocity/ConstantExpressionNode.java new file mode 100644 index 0000000..a4dfe17 --- /dev/null +++ b/src/main/java/com/google/escapevelocity/ConstantExpressionNode.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.escapevelocity; + +/** + * A node in the parse tree representing a constant value. Evaluating the node yields the constant + * value. Instances of this class are used both in expressions, like the {@code 23} in + * {@code #set ($x = 23)}, and for literal text in templates. In the template... + *
{@code
+ * abc#{if}($x == 5)def#{end}xyz
+ * }
+ * ...each of the strings {@code abc}, {@code def}, {@code xyz} is represented by an instance of + * this class that {@linkplain #evaluate evaluates} to that string, and the value {@code 5} is + * represented by an instance of this class that evaluates to the integer 5. + * + * @author emcmanus@google.com (Éamonn McManus) + */ +class ConstantExpressionNode extends ExpressionNode { + private final Object value; + + ConstantExpressionNode(String resourceName, int lineNumber, Object value) { + super(resourceName, lineNumber); + this.value = value; + } + + @Override + Object evaluate(EvaluationContext context) { + return value; + } +} diff --git a/src/main/java/com/google/escapevelocity/DirectiveNode.java b/src/main/java/com/google/escapevelocity/DirectiveNode.java new file mode 100644 index 0000000..cf33f55 --- /dev/null +++ b/src/main/java/com/google/escapevelocity/DirectiveNode.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2015 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.escapevelocity; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; + +/** + * A node in the parse tree that is a directive such as {@code #set ($x = $y)} + * or {@code #if ($x) y #end}. + * + * @author emcmanus@google.com (Éamonn McManus) + */ +abstract class DirectiveNode extends Node { + DirectiveNode(String resourceName, int lineNumber) { + super(resourceName, lineNumber); + } + + /** + * A node in the parse tree representing a {@code #set} construct. Evaluating + * {@code #set ($x = 23)} will set {@code $x} to the value 23. It does not in itself produce + * any text in the output. + * + *

Velocity supports setting values within arrays or collections, with for example + * {@code $set ($x[$i] = $y)}. That is not currently supported here. + */ + static class SetNode extends DirectiveNode { + private final String var; + private final Node expression; + + SetNode(String var, Node expression) { + super(expression.resourceName, expression.lineNumber); + this.var = var; + this.expression = expression; + } + + @Override + Object evaluate(EvaluationContext context) { + context.setVar(var, expression.evaluate(context)); + return ""; + } + } + + /** + * A node in the parse tree representing an {@code #if} construct. All instances of this class + * have a true subtree and a false subtree. For a plain {@code #if (cond) body + * #end}, the false subtree will be empty. For {@code #if (cond1) body1 #elseif (cond2) body2 + * #else body3 #end}, the false subtree will contain a nested {@code IfNode}, as if {@code #else + * #if} had been used instead of {@code #elseif}. + */ + static class IfNode extends DirectiveNode { + private final ExpressionNode condition; + private final Node truePart; + private final Node falsePart; + + IfNode( + String resourceName, + int lineNumber, + ExpressionNode condition, + Node trueNode, + Node falseNode) { + super(resourceName, lineNumber); + this.condition = condition; + this.truePart = trueNode; + this.falsePart = falseNode; + } + + @Override Object evaluate(EvaluationContext context) { + Node branch = condition.isDefinedAndTrue(context) ? truePart : falsePart; + return branch.evaluate(context); + } + } + + /** + * A node in the parse tree representing a {@code #foreach} construct. While evaluating + * {@code #foreach ($x in $things)}, {$code $x} will be set to each element of {@code $things} in + * turn. Once the loop completes, {@code $x} will go back to whatever value it had before, which + * might be undefined. During loop execution, the variable {@code $foreach} is also defined. + * Velocity defines a number of properties in this variable, but here we only support + * {@code $foreach.hasNext}. + */ + static class ForEachNode extends DirectiveNode { + private final String var; + private final ExpressionNode collection; + private final Node body; + + ForEachNode(String resourceName, int lineNumber, String var, ExpressionNode in, Node body) { + super(resourceName, lineNumber); + this.var = var; + this.collection = in; + this.body = body; + } + + @Override + Object evaluate(EvaluationContext context) { + Object collectionValue = collection.evaluate(context); + Iterable iterable; + if (collectionValue instanceof Iterable) { + iterable = (Iterable) collectionValue; + } else if (collectionValue instanceof Object[]) { + iterable = Arrays.asList((Object[]) collectionValue); + } else if (collectionValue instanceof Map) { + iterable = ((Map) collectionValue).values(); + } else { + throw evaluationException("Not iterable: " + collectionValue); + } + Runnable undo = context.setVar(var, null); + StringBuilder sb = new StringBuilder(); + Iterator it = iterable.iterator(); + Runnable undoForEach = context.setVar("foreach", new ForEachVar(it)); + while (it.hasNext()) { + context.setVar(var, it.next()); + sb.append(body.evaluate(context)); + } + undoForEach.run(); + undo.run(); + return sb.toString(); + } + + /** + * This class is the type of the variable {@code $foreach} that is defined within + * {@code #foreach} loops. Its {@link #getHasNext()} method means that we can write + * {@code #if ($foreach.hasNext)}. + */ + private static class ForEachVar { + private final Iterator iterator; + + ForEachVar(Iterator iterator) { + this.iterator = iterator; + } + + public boolean getHasNext() { + return iterator.hasNext(); + } + } + } + + /** + * A node in the parse tree representing a macro call. If the template contains a definition like + * {@code #macro (mymacro $x $y) ... #end}, then a call of that macro looks like + * {@code #mymacro (xvalue yvalue)}. The call is represented by an instance of this class. The + * definition itself does not appear in the parse tree. + * + *

Evaluating a macro involves temporarily setting the parameter variables ({@code $x $y} in + * the example) to thunks representing the argument expressions, evaluating the macro body, and + * restoring any previous values that the parameter variables had. + */ + static class MacroCallNode extends DirectiveNode { + private final String name; + private final ImmutableList thunks; + private Macro macro; + + MacroCallNode( + String resourceName, + int lineNumber, + String name, + ImmutableList argumentNodes) { + super(resourceName, lineNumber); + this.name = name; + this.thunks = argumentNodes; + } + + String name() { + return name; + } + + int argumentCount() { + return thunks.size(); + } + + void setMacro(Macro macro) { + this.macro = macro; + } + + @Override + Object evaluate(EvaluationContext context) { + assert macro != null : "Macro should have been linked: #" + name; + return macro.evaluate(context, thunks); + } + } +} diff --git a/src/main/java/com/google/escapevelocity/EvaluationContext.java b/src/main/java/com/google/escapevelocity/EvaluationContext.java new file mode 100644 index 0000000..43b7868 --- /dev/null +++ b/src/main/java/com/google/escapevelocity/EvaluationContext.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.escapevelocity; + +import java.util.Map; +import java.util.TreeMap; + +/** + * The context of a template evaluation. This consists of the template variables and the template + * macros. The template variables start with the values supplied by the evaluation call, and can + * be changed by {@code #set} directives and during the execution of {@code #foreach} and macro + * calls. The macros are extracted from the template during parsing and never change thereafter. + * + * @author emcmanus@google.com (Éamonn McManus) + */ +interface EvaluationContext { + Object getVar(String var); + + boolean varIsDefined(String var); + + /** + * Sets the given variable to the given value. + * + * @return a Runnable that will restore the variable to the value it had before. If the variable + * was undefined before this method was executed, the Runnable will make it undefined again. + * This allows us to restore the state of {@code $x} after {@code #foreach ($x in ...)}. + */ + Runnable setVar(final String var, Object value); + + class PlainEvaluationContext implements EvaluationContext { + private final Map vars; + + PlainEvaluationContext(Map vars) { + this.vars = new TreeMap(vars); + } + + @Override + public Object getVar(String var) { + return vars.get(var); + } + + @Override + public boolean varIsDefined(String var) { + return vars.containsKey(var); + } + + @Override + public Runnable setVar(final String var, Object value) { + Runnable undo; + if (vars.containsKey(var)) { + final Object oldValue = vars.get(var); + undo = new Runnable() { + @Override public void run() { + vars.put(var, oldValue); + } + }; + } else { + undo = new Runnable() { + @Override public void run() { + vars.remove(var); + } + }; + } + vars.put(var, value); + return undo; + } + } +} diff --git a/src/main/java/com/google/escapevelocity/EvaluationException.java b/src/main/java/com/google/escapevelocity/EvaluationException.java new file mode 100644 index 0000000..67aa15c --- /dev/null +++ b/src/main/java/com/google/escapevelocity/EvaluationException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.escapevelocity; + +/** + * An exception that occurred while evaluating a template, such as an undefined variable reference + * or a division by zero. + * + * @author emcmanus@google.com (Éamonn McManus) + */ +public class EvaluationException extends RuntimeException { + private static final long serialVersionUID = 1; + + EvaluationException(String message) { + super(message); + } + + EvaluationException(String message, Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/google/escapevelocity/ExpressionNode.java b/src/main/java/com/google/escapevelocity/ExpressionNode.java new file mode 100644 index 0000000..4ee29c5 --- /dev/null +++ b/src/main/java/com/google/escapevelocity/ExpressionNode.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2015 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.escapevelocity; + +import com.google.escapevelocity.Parser.Operator; + +/** + * A node in the parse tree representing an expression. Expressions appear inside directives, + * specifically {@code #set}, {@code #if}, {@code #foreach}, and macro calls. Expressions can + * also appear inside indices in references, like {@code $x[$i]}. + * + * @author emcmanus@google.com (Éamonn McManus) + */ +abstract class ExpressionNode extends Node { + ExpressionNode(String resourceName, int lineNumber) { + super(resourceName, lineNumber); + } + + /** + * True if evaluating this expression yields a value that is considered true by Velocity's + * + * rules. A value is false if it is null or equal to Boolean.FALSE. + * Every other value is true. + * + *

Note that the text at the similar link + * here + * states that empty collections and empty strings are also considered false, but that is not + * true. + */ + boolean isTrue(EvaluationContext context) { + Object value = evaluate(context); + if (value instanceof Boolean) { + return (Boolean) value; + } else { + return value != null; + } + } + + /** + * True if this is a defined value and it evaluates to true. This is the same as {@link #isTrue} + * except that it is allowed for this to be undefined variable, in which it evaluates to false. + * The method is overridden for plain references so that undefined is the same as false. + * The reason is to support Velocity's idiom {@code #if ($var)}, where it is not an error + * if {@code $var} is undefined. + */ + boolean isDefinedAndTrue(EvaluationContext context) { + return isTrue(context); + } + + /** + * The integer result of evaluating this expression. + * + * @throws EvaluationException if evaluating the expression produces an exception, or if it + * yields a value that is not an integer. + */ + int intValue(EvaluationContext context) { + Object value = evaluate(context); + if (!(value instanceof Integer)) { + throw evaluationException("Arithemtic is only available on integers, not " + show(value)); + } + return (Integer) value; + } + + /** + * Returns a string representing the given value, for use in error messages. The string + * includes both the value's {@code toString()} and its type. + */ + private static String show(Object value) { + if (value == null) { + return "null"; + } else { + return value + " (a " + value.getClass().getName() + ")"; + } + } + + /** + * Represents all binary expressions. In {@code #set ($a = $b + $c)}, this will be the type + * of the node representing {@code $b + $c}. + */ + static class BinaryExpressionNode extends ExpressionNode { + final ExpressionNode lhs; + final Operator op; + final ExpressionNode rhs; + + BinaryExpressionNode(ExpressionNode lhs, Operator op, ExpressionNode rhs) { + super(lhs.resourceName, lhs.lineNumber); + this.lhs = lhs; + this.op = op; + this.rhs = rhs; + } + + @Override Object evaluate(EvaluationContext context) { + switch (op) { + case OR: + return lhs.isTrue(context) || rhs.isTrue(context); + case AND: + return lhs.isTrue(context) && rhs.isTrue(context); + case EQUAL: + return equal(context); + case NOT_EQUAL: + return !equal(context); + default: // fall out + } + int lhsInt = lhs.intValue(context); + int rhsInt = rhs.intValue(context); + switch (op) { + case LESS: + return lhsInt < rhsInt; + case LESS_OR_EQUAL: + return lhsInt <= rhsInt; + case GREATER: + return lhsInt > rhsInt; + case GREATER_OR_EQUAL: + return lhsInt >= rhsInt; + case PLUS: + return lhsInt + rhsInt; + case MINUS: + return lhsInt - rhsInt; + case TIMES: + return lhsInt * rhsInt; + case DIVIDE: + return lhsInt / rhsInt; + case REMAINDER: + return lhsInt % rhsInt; + default: + throw new AssertionError(op); + } + } + + /** + * Returns true if {@code lhs} and {@code rhs} are equal according to Velocity. + * + *

Velocity's definition + * of equality differs depending on whether the objects being compared are of the same + * class. If so, equality comes from {@code Object.equals} as you would expect. But if they + * are not of the same class, they are considered equal if their {@code toString()} values are + * equal. This means that integer 123 equals long 123L and also string {@code "123"}. It also + * means that equality isn't always transitive. For example, two StringBuilder objects each + * containing {@code "123"} will not compare equal, even though the string {@code "123"} + * compares equal to each of them. + */ + private boolean equal(EvaluationContext context) { + Object lhsValue = lhs.evaluate(context); + Object rhsValue = rhs.evaluate(context); + if (lhsValue == rhsValue) { + return true; + } + if (lhsValue == null || rhsValue == null) { + return false; + } + if (lhsValue.getClass().equals(rhsValue.getClass())) { + return lhsValue.equals(rhsValue); + } + // Funky equals behaviour specified by Velocity. + return lhsValue.toString().equals(rhsValue.toString()); + } + } + + /** + * A node in the parse tree representing an expression like {@code !$a}. + */ + static class NotExpressionNode extends ExpressionNode { + private final ExpressionNode expr; + + NotExpressionNode(ExpressionNode expr) { + super(expr.resourceName, expr.lineNumber); + this.expr = expr; + } + + @Override Object evaluate(EvaluationContext context) { + return !expr.isTrue(context); + } + } +} diff --git a/src/main/java/com/google/escapevelocity/ImmutableAsciiSet.java b/src/main/java/com/google/escapevelocity/ImmutableAsciiSet.java new file mode 100644 index 0000000..96a126c --- /dev/null +++ b/src/main/java/com/google/escapevelocity/ImmutableAsciiSet.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2017 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.escapevelocity; + +import java.util.AbstractSet; +import java.util.BitSet; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * An immutable set of ASCII characters. + * + * @author emcmanus@google.com (Éamonn McManus) + */ +class ImmutableAsciiSet extends AbstractSet { + private final BitSet bits; + + ImmutableAsciiSet(BitSet bits) { + this.bits = bits; + } + + static ImmutableAsciiSet of(char c) { + return ofRange(c, c); + } + + static ImmutableAsciiSet ofRange(char from, char to) { + if (from > to) { + throw new IllegalArgumentException("from > to"); + } + if (to >= 128) { + throw new IllegalArgumentException("Not ASCII"); + } + BitSet bits = new BitSet(); + bits.set(from, to + 1); + return new ImmutableAsciiSet(bits); + } + + ImmutableAsciiSet union(ImmutableAsciiSet that) { + BitSet union = (BitSet) bits.clone(); + union.or(that.bits); + return new ImmutableAsciiSet(union); + } + + @Override + public boolean contains(Object o) { + int i = -1; + if (o instanceof Character) { + i = (Character) o; + } else if (o instanceof Integer) { + i = (Integer) o; + } + return contains(i); + } + + boolean contains(int i) { + if (i < 0) { + return false; + } else { + return bits.get(i); + } + } + + @Override + public Iterator iterator() { + return new Iterator() { + private int index; + + @Override + public boolean hasNext() { + return bits.nextSetBit(index) >= 0; + } + + @Override + public Integer next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + int next = bits.nextSetBit(index); + index = next + 1; + return next; + } + }; + } + + @Override + public int size() { + return bits.cardinality(); + } +} diff --git a/src/main/java/com/google/escapevelocity/ImmutableList.java b/src/main/java/com/google/escapevelocity/ImmutableList.java new file mode 100644 index 0000000..0b903f7 --- /dev/null +++ b/src/main/java/com/google/escapevelocity/ImmutableList.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2017 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.escapevelocity; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + * An immutable list. + * + * @author emcmanus@google.com (Éamonn McManus) + */ +class ImmutableList extends AbstractList { + private static final ImmutableList EMPTY = new ImmutableList<>(new Object[0]); + + private final E[] elements; + + private ImmutableList(E[] elements) { + this.elements = elements; + } + + @Override + public Iterator iterator() { + return Arrays.asList(elements).iterator(); + } + + @Override + public E get(int index) { + if (index < 0 || index >= elements.length) { + throw new IndexOutOfBoundsException(String.valueOf(index)); + } + return elements[index]; + } + + @Override + public int size() { + return elements.length; + } + + static ImmutableList of() { + @SuppressWarnings("unchecked") + ImmutableList empty = (ImmutableList) EMPTY; + return empty; + } + + @SafeVarargs + static ImmutableList of(E... elements) { + return new ImmutableList<>(elements.clone()); + } + + static ImmutableList copyOf(List list) { + @SuppressWarnings("unchecked") + E[] elements = (E[]) new Object[list.size()]; + list.toArray(elements); + return new ImmutableList<>(elements); + } + + static Builder builder() { + return new Builder(); + } + + static class Builder { + private final List list = new ArrayList<>(); + + void add(E element) { + list.add(element); + } + + ImmutableList build() { + if (list.isEmpty()) { + return ImmutableList.of(); + } + @SuppressWarnings("unchecked") + E[] elements = (E[]) new Object[list.size()]; + list.toArray(elements); + return new ImmutableList<>(elements); + } + } +} diff --git a/src/main/java/com/google/escapevelocity/ImmutableSet.java b/src/main/java/com/google/escapevelocity/ImmutableSet.java new file mode 100644 index 0000000..f4e8e9f --- /dev/null +++ b/src/main/java/com/google/escapevelocity/ImmutableSet.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2017 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.escapevelocity; + +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Iterator; + +/** + * An immutable set. This implementation is only suitable for sets with a small number of elements. + * + * @author emcmanus@google.com (Éamonn McManus) + */ +class ImmutableSet extends AbstractSet { + private final E[] elements; + + private ImmutableSet(E[] elements) { + this.elements = elements; + } + + @Override + public Iterator iterator() { + return Arrays.asList(elements).iterator(); + } + + @Override + public int size() { + return elements.length; + } + + @SafeVarargs + static ImmutableSet of(E... elements) { + int len = elements.length; + for (int i = 0; i < len - 1; i++) { + for (int j = len - 1; j > i; j--) { + if (elements[i].equals(elements[j])) { + // We want to exclude elements[j] from the final set. We can do that by copying the + // current last element in place of j (this might be j itself) and then reducing the + // size of the set. + elements[j] = elements[len - 1]; + len--; + } + } + } + return new ImmutableSet<>(Arrays.copyOf(elements, len)); + } +} diff --git a/src/main/java/com/google/escapevelocity/Macro.java b/src/main/java/com/google/escapevelocity/Macro.java new file mode 100644 index 0000000..151ded2 --- /dev/null +++ b/src/main/java/com/google/escapevelocity/Macro.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2015 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.escapevelocity; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + + +/** + * A macro definition. Macros appear in templates using the syntax {@code #macro (m $x $y) ... #end} + * and each one produces an instance of this class. Evaluating a macro involves setting the + * parameters (here {$x $y)} and evaluating the macro body. Macro arguments are call-by-name, which + * means that we need to set each parameter variable to the node in the parse tree that corresponds + * to it, and arrange for that node to be evaluated when the variable is actually referenced. + * + * @author emcmanus@google.com (Éamonn McManus) + */ +class Macro { + private final int definitionLineNumber; + private final String name; + private final ImmutableList parameterNames; + private final Node body; + + Macro(int definitionLineNumber, String name, List parameterNames, Node body) { + this.definitionLineNumber = definitionLineNumber; + this.name = name; + this.parameterNames = ImmutableList.copyOf(parameterNames); + this.body = body; + } + + String name() { + return name; + } + + int parameterCount() { + return parameterNames.size(); + } + + Object evaluate(EvaluationContext context, List thunks) { + try { + assert thunks.size() == parameterNames.size() : "Argument mistmatch for " + name; + Map parameterThunks = new LinkedHashMap<>(); + for (int i = 0; i < parameterNames.size(); i++) { + parameterThunks.put(parameterNames.get(i), thunks.get(i)); + } + EvaluationContext newContext = new MacroEvaluationContext(parameterThunks, context); + return body.evaluate(newContext); + } catch (EvaluationException e) { + EvaluationException newException = new EvaluationException( + "In macro #" + name + " defined on line " + definitionLineNumber + ": " + e.getMessage()); + newException.setStackTrace(e.getStackTrace()); + throw e; + } + } + + /** + * The context for evaluation within macros. This wraps an existing {@code EvaluationContext} + * but intercepts reads of the macro's parameters so that they result in a call-by-name evaluation + * of whatever was passed as the parameter. For example, if you write... + *

{@code
+   * #macro (mymacro $x)
+   * $x $x
+   * #end
+   * #mymacro($foo.bar(23))
+   * }
+ * ...then the {@code #mymacro} call will result in {@code $foo.bar(23)} being evaluated twice, + * once for each time {@code $x} appears. The way this works is that {@code $x} is a thunk. + * Historically a thunk is a piece of code to evaluate an expression in the context where it + * occurs, for call-by-name procedures as in Algol 60. Here, it is not exactly a piece of code, + * but it has the same responsibility. + */ + static class MacroEvaluationContext implements EvaluationContext { + private final Map parameterThunks; + private final EvaluationContext originalEvaluationContext; + + MacroEvaluationContext( + Map parameterThunks, EvaluationContext originalEvaluationContext) { + this.parameterThunks = parameterThunks; + this.originalEvaluationContext = originalEvaluationContext; + } + + @Override + public Object getVar(String var) { + Node thunk = parameterThunks.get(var); + if (thunk == null) { + return originalEvaluationContext.getVar(var); + } else { + // Evaluate the thunk in the context where it appeared, not in this context. Otherwise + // if you pass $x to a parameter called $x you would get an infinite recursion. Likewise + // if you had #macro(mymacro $x $y) and a call #mymacro($y 23), you would expect that $x + // would expand to whatever $y meant at the call site, rather than to the value of the $y + // parameter. + return thunk.evaluate(originalEvaluationContext); + } + } + + @Override + public boolean varIsDefined(String var) { + return parameterThunks.containsKey(var) || originalEvaluationContext.varIsDefined(var); + } + + @Override + public Runnable setVar(final String var, Object value) { + // Copy the behaviour that #set will shadow a macro parameter, even though the Velocity peeps + // seem to agree that that is not good. + final Node thunk = parameterThunks.get(var); + if (thunk == null) { + return originalEvaluationContext.setVar(var, value); + } else { + parameterThunks.remove(var); + final Runnable originalUndo = originalEvaluationContext.setVar(var, value); + return new Runnable() { + @Override + public void run() { + originalUndo.run(); + parameterThunks.put(var, thunk); + } + }; + } + } + } +} diff --git a/src/main/java/com/google/escapevelocity/Node.java b/src/main/java/com/google/escapevelocity/Node.java new file mode 100644 index 0000000..eca745f --- /dev/null +++ b/src/main/java/com/google/escapevelocity/Node.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2015 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.escapevelocity; + +/** + * A node in the parse tree. + * + * @author emcmanus@google.com (Éamonn McManus) + */ +abstract class Node { + final String resourceName; + final int lineNumber; + + Node(String resourceName, int lineNumber) { + this.resourceName = resourceName; + this.lineNumber = lineNumber; + } + + /** + * Returns the result of evaluating this node in the given context. This result may be used as + * part of a further operation, for example evaluating {@code 2 + 3} to 5 in order to set + * {@code $x} to 5 in {@code #set ($x = 2 + 3)}. Or it may be used directly as part of the + * template output, for example evaluating replacing {@code name} by {@code Fred} in + * {@code My name is $name.}. + */ + abstract Object evaluate(EvaluationContext context); + + private String where() { + String where = "In expression on line " + lineNumber; + if (resourceName != null) { + where += " of " + resourceName; + } + return where; + } + + EvaluationException evaluationException(String message) { + return new EvaluationException(where() + ": " + message); + } + + EvaluationException evaluationException(Throwable cause) { + return new EvaluationException(where() + ": " + cause, cause); + } + + /** + * Returns an empty node in the parse tree. This is used for example to represent the trivial + * "else" part of an {@code #if} that does not have an explicit {@code #else}. + */ + static Node emptyNode(String resourceName, int lineNumber) { + return new Cons(resourceName, lineNumber, ImmutableList.of()); + } + + + /** + * Create a new parse tree node that is the concatenation of the given ones. Evaluating the + * new node produces the same string as evaluating each of the given nodes and concatenating the + * result. + */ + static Node cons(String resourceName, int lineNumber, ImmutableList nodes) { + return new Cons(resourceName, lineNumber, nodes); + } + + private static final class Cons extends Node { + private final ImmutableList nodes; + + Cons(String resourceName, int lineNumber, ImmutableList nodes) { + super(resourceName, lineNumber); + this.nodes = nodes; + } + + @Override Object evaluate(EvaluationContext context) { + StringBuilder sb = new StringBuilder(); + for (Node node : nodes) { + sb.append(node.evaluate(context)); + } + return sb.toString(); + } + } +} diff --git a/src/main/java/com/google/escapevelocity/ParseException.java b/src/main/java/com/google/escapevelocity/ParseException.java new file mode 100644 index 0000000..241a192 --- /dev/null +++ b/src/main/java/com/google/escapevelocity/ParseException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.escapevelocity; + +/** + * An exception that occurred while parsing a template. + * + * @author emcmanus@google.com (Éamonn McManus) + */ +public class ParseException extends RuntimeException { + private static final long serialVersionUID = 1; + + ParseException(String message, String resourceName, int lineNumber) { + super(message + ", " + where(resourceName, lineNumber)); + } + + ParseException(String message, String resourceName, int lineNumber, String context) { + super(message + ", " + where(resourceName, lineNumber) + ", at text starting: " + context); + } + + private static String where(String resourceName, int lineNumber) { + if (resourceName == null) { + return "on line " + lineNumber; + } else { + return "on line " + lineNumber + " of " + resourceName; + } + } +} diff --git a/src/main/java/com/google/escapevelocity/Parser.java b/src/main/java/com/google/escapevelocity/Parser.java new file mode 100644 index 0000000..9982be3 --- /dev/null +++ b/src/main/java/com/google/escapevelocity/Parser.java @@ -0,0 +1,963 @@ +/* + * Copyright (C) 2015 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.escapevelocity; + +import com.google.escapevelocity.DirectiveNode.SetNode; +import com.google.escapevelocity.ExpressionNode.BinaryExpressionNode; +import com.google.escapevelocity.ExpressionNode.NotExpressionNode; +import com.google.escapevelocity.ReferenceNode.IndexReferenceNode; +import com.google.escapevelocity.ReferenceNode.MemberReferenceNode; +import com.google.escapevelocity.ReferenceNode.MethodReferenceNode; +import com.google.escapevelocity.ReferenceNode.PlainReferenceNode; +import com.google.escapevelocity.TokenNode.CommentTokenNode; +import com.google.escapevelocity.TokenNode.ElseIfTokenNode; +import com.google.escapevelocity.TokenNode.ElseTokenNode; +import com.google.escapevelocity.TokenNode.EndTokenNode; +import com.google.escapevelocity.TokenNode.EofNode; +import com.google.escapevelocity.TokenNode.ForEachTokenNode; +import com.google.escapevelocity.TokenNode.IfTokenNode; +import com.google.escapevelocity.TokenNode.MacroDefinitionTokenNode; +import com.google.escapevelocity.TokenNode.NestedTokenNode; +import java.io.IOException; +import java.io.LineNumberReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A parser that reads input from the given {@link Reader} and parses it to produce a + * {@link Template}. + * + * @author emcmanus@google.com (Éamonn McManus) + */ +class Parser { + private static final int EOF = -1; + + private final LineNumberReader reader; + private final String resourceName; + private final Template.ResourceOpener resourceOpener; + + /** + * The invariant of this parser is that {@code c} is always the next character of interest. + * This means that we never have to "unget" a character by reading too far. For example, after + * we parse an integer, {@code c} will be the first character after the integer, which is exactly + * the state we will be in when there are no more digits. + */ + private int c; + + Parser(Reader reader, String resourceName, Template.ResourceOpener resourceOpener) + throws IOException { + this.reader = new LineNumberReader(reader); + this.reader.setLineNumber(1); + next(); + this.resourceName = resourceName; + this.resourceOpener = resourceOpener; + } + + /** + * Parse the input completely to produce a {@link Template}. + * + *

Parsing happens in two phases. First, we parse a sequence of "tokens", where tokens include + * entire references such as

+   *    ${x.foo()[23]}
+   * 
or entire directives such as
+   *    #set ($x = $y + $z)
+   * 
But tokens do not span complex constructs. For example,
+   *    #if ($x == $y) something #end
+   * 
is three tokens:
+   *    #if ($x == $y)
+   *    (literal text " something ")
+   *   #end
+   * 
+ * + *

The second phase then takes the sequence of tokens and constructs a parse tree out of it. + * Some nodes in the parse tree will be unchanged from the token sequence, such as the

+   *    ${x.foo()[23]}
+   *    #set ($x = $y + $z)
+   * 
examples above. But a construct such as the {@code #if ... #end} mentioned above will + * become a single IfNode in the parse tree in the second phase. + * + *

The main reason for this approach is that Velocity has two kinds of lexical contexts. At the + * top level, there can be arbitrary literal text; references like ${x.foo()}; and + * directives like {@code #if} or {@code #set}. Inside the parentheses of a directive, however, + * neither arbitrary text nor directives can appear, but expressions can, so we need to tokenize + * the inside of

+   *    #if ($x == $a + $b)
+   * 
as the five tokens "$x", "==", "$a", "+", "$b". Rather than having a classical + * parser/lexer combination, where the lexer would need to switch between these two modes, we + * replace the lexer with an ad-hoc parser that is the first phase described above, and we + * define a simple parser over the resultant tokens that is the second phase. + */ + Template parse() throws IOException { + ImmutableList tokens = parseTokens(); + return new Reparser(tokens).reparse(); + } + + private ImmutableList parseTokens() throws IOException { + ImmutableList.Builder tokens = ImmutableList.builder(); + Node token; + do { + token = parseNode(); + tokens.add(token); + } while (!(token instanceof EofNode)); + return tokens.build(); + } + + private int lineNumber() { + return reader.getLineNumber(); + } + + /** + * Gets the next character from the reader and assigns it to {@code c}. If there are no more + * characters, sets {@code c} to {@link #EOF} if it is not already. + */ + private void next() throws IOException { + if (c != EOF) { + c = reader.read(); + } + } + + /** + * If {@code c} is a space character, keeps reading until {@code c} is a non-space character or + * there are no more characters. + */ + private void skipSpace() throws IOException { + while (Character.isWhitespace(c)) { + next(); + } + } + + /** + * Gets the next character from the reader, and if it is a space character, keeps reading until + * a non-space character is found. + */ + private void nextNonSpace() throws IOException { + next(); + skipSpace(); + } + + /** + * Skips any space in the reader, and then throws an exception if the first non-space character + * found is not the expected one. Sets {@code c} to the first character after that expected one. + */ + private void expect(char expected) throws IOException { + skipSpace(); + if (c == expected) { + next(); + } else { + throw parseException("Expected " + expected); + } + } + + /** + * Parses a single node from the reader, as part of the first parsing phase. + *
{@code
+   *