diff options
author | Éamonn McManus <eamonn@mcmanus.net> | 2018-04-20 17:14:33 -0700 |
---|---|---|
committer | Éamonn McManus <eamonn@mcmanus.net> | 2018-04-20 17:14:33 -0700 |
commit | 89ea4ddf7f3d0005543fb740481eeb0dc5cddac9 (patch) | |
tree | 9961d4cc4e5047bc1275746c952b9247e542384d | |
parent | d68715b3e62b4f1b8ebd7fa051004934a2ba913d (diff) | |
download | escapevelocity-89ea4ddf7f3d0005543fb740481eeb0dc5cddac9.tar.gz |
Update to latest Google version of EscapeVelocity.
16 files changed, 638 insertions, 152 deletions
@@ -26,21 +26,18 @@ A reimplementation of a subset of the Apache Velocity templating system. </description> - <!-- TODO(emcmanus) <scm> - <url>http://github.com/google/auto</url> - <connection>scm:git:git://github.com/google/auto.git</connection> - <developerConnection>scm:git:ssh://git@github.com/google/auto.git</developerConnection> + <url>http://github.com/google/escapevelocity</url> + <connection>scm:git:git://github.com/google/escapevelocity.git</connection> + <developerConnection>scm:git:ssh://git@github.com/google/escapevelocity.git</developerConnection> <tag>HEAD</tag> </scm> - --> <dependencies> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.5-jre</version> - <scope>test</scope> </dependency> <!-- test dependencies --> <dependency> @@ -76,8 +73,8 @@ <artifactId>maven-compiler-plugin</artifactId> <version>3.7.0</version> <configuration> - <source>1.7</source> - <target>1.7</target> + <source>1.8</source> + <target>1.8</target> <compilerArgument>-Xlint:all</compilerArgument> <showWarnings>true</showWarnings> <showDeprecation>true</showDeprecation> diff --git a/src/main/java/com/google/escapevelocity/ConstantExpressionNode.java b/src/main/java/com/google/escapevelocity/ConstantExpressionNode.java index a4dfe17..982a4a9 100644 --- a/src/main/java/com/google/escapevelocity/ConstantExpressionNode.java +++ b/src/main/java/com/google/escapevelocity/ConstantExpressionNode.java @@ -11,6 +11,25 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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; /** diff --git a/src/main/java/com/google/escapevelocity/DirectiveNode.java b/src/main/java/com/google/escapevelocity/DirectiveNode.java index cf33f55..db6a3a3 100644 --- a/src/main/java/com/google/escapevelocity/DirectiveNode.java +++ b/src/main/java/com/google/escapevelocity/DirectiveNode.java @@ -11,8 +11,29 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.common.base.Verify; +import com.google.common.collect.ImmutableList; import java.util.Arrays; import java.util.Iterator; import java.util.Map; @@ -186,7 +207,7 @@ abstract class DirectiveNode extends Node { @Override Object evaluate(EvaluationContext context) { - assert macro != null : "Macro should have been linked: #" + name; + Verify.verifyNotNull(macro, "Macro #%s 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 index 43b7868..4c4b27d 100644 --- a/src/main/java/com/google/escapevelocity/EvaluationContext.java +++ b/src/main/java/com/google/escapevelocity/EvaluationContext.java @@ -11,6 +11,25 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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; @@ -60,17 +79,9 @@ interface EvaluationContext { Runnable undo; if (vars.containsKey(var)) { final Object oldValue = vars.get(var); - undo = new Runnable() { - @Override public void run() { - vars.put(var, oldValue); - } - }; + undo = () -> vars.put(var, oldValue); } else { - undo = new Runnable() { - @Override public void run() { - vars.remove(var); - } - }; + undo = () -> 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 index 67aa15c..c64318c 100644 --- a/src/main/java/com/google/escapevelocity/EvaluationException.java +++ b/src/main/java/com/google/escapevelocity/EvaluationException.java @@ -11,6 +11,25 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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; /** diff --git a/src/main/java/com/google/escapevelocity/ExpressionNode.java b/src/main/java/com/google/escapevelocity/ExpressionNode.java index 4ee29c5..e666ed1 100644 --- a/src/main/java/com/google/escapevelocity/ExpressionNode.java +++ b/src/main/java/com/google/escapevelocity/ExpressionNode.java @@ -11,6 +11,25 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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; diff --git a/src/main/java/com/google/escapevelocity/Macro.java b/src/main/java/com/google/escapevelocity/Macro.java index 151ded2..fbb1764 100644 --- a/src/main/java/com/google/escapevelocity/Macro.java +++ b/src/main/java/com/google/escapevelocity/Macro.java @@ -11,8 +11,29 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.common.base.Verify; +import com.google.common.collect.ImmutableList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -50,7 +71,7 @@ class Macro { Object evaluate(EvaluationContext context, List<Node> thunks) { try { - assert thunks.size() == parameterNames.size() : "Argument mistmatch for " + name; + Verify.verify(thunks.size() == parameterNames.size(), "Argument mistmatch for %s", name); Map<String, Node> parameterThunks = new LinkedHashMap<>(); for (int i = 0; i < parameterNames.size(); i++) { parameterThunks.put(parameterNames.get(i), thunks.get(i)); @@ -121,12 +142,9 @@ class Macro { } else { parameterThunks.remove(var); final Runnable originalUndo = originalEvaluationContext.setVar(var, value); - return new Runnable() { - @Override - public void run() { - originalUndo.run(); - parameterThunks.put(var, thunk); - } + return () -> { + 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 index eca745f..d11c95d 100644 --- a/src/main/java/com/google/escapevelocity/Node.java +++ b/src/main/java/com/google/escapevelocity/Node.java @@ -11,8 +11,29 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.common.collect.ImmutableList; + /** * A node in the parse tree. * diff --git a/src/main/java/com/google/escapevelocity/ParseException.java b/src/main/java/com/google/escapevelocity/ParseException.java index 241a192..9d4a39c 100644 --- a/src/main/java/com/google/escapevelocity/ParseException.java +++ b/src/main/java/com/google/escapevelocity/ParseException.java @@ -11,6 +11,25 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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; /** diff --git a/src/main/java/com/google/escapevelocity/Parser.java b/src/main/java/com/google/escapevelocity/Parser.java index 9982be3..0beaf18 100644 --- a/src/main/java/com/google/escapevelocity/Parser.java +++ b/src/main/java/com/google/escapevelocity/Parser.java @@ -11,6 +11,25 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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; @@ -29,14 +48,16 @@ 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 com.google.common.base.CharMatcher; +import com.google.common.base.Verify; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.Iterables; +import com.google.common.primitives.Chars; +import com.google.common.primitives.Ints; 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 @@ -53,12 +74,20 @@ class Parser { /** * 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. + * This means that we almost 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. + * + * <p>Sometimes we need to read two characters ahead, and in that case we use {@link #pushback}. */ private int c; + /** + * A single character of pushback. If this is not negative, the {@link #next()} method will + * return it instead of reading a character. + */ + private int pushback = -1; + Parser(Reader reader, String resourceName, Template.ResourceOpener resourceOpener) throws IOException { this.reader = new LineNumberReader(reader); @@ -127,11 +156,29 @@ class Parser { */ private void next() throws IOException { if (c != EOF) { - c = reader.read(); + if (pushback < 0) { + c = reader.read(); + } else { + c = pushback; + pushback = -1; + } } } /** + * Saves the current character {@code c} to be read again, and sets {@code c} to the given + * {@code c1}. Suppose the text contains {@code xy} and we have just read {@code y}. + * So {@code c == 'y'}. Now if we execute {@code pushback('x')}, we will have + * {@code c == 'x'} and the next call to {@link #next()} will set {@code c == 'y'}. Subsequent + * calls to {@code next()} will continue reading from {@link #reader}. So the pushback + * essentially puts us back in the state we were in before we read {@code y}. + */ + private void pushback(int c1) { + pushback = c; + c = c1; + } + + /** * If {@code c} is a space character, keeps reading until {@code c} is a non-space character or * there are no more characters. */ @@ -174,17 +221,24 @@ class Parser { private Node parseNode() throws IOException { if (c == '#') { next(); - if (c == '#') { - return parseComment(); - } else if (isAsciiLetter(c) || c == '{') { - return parseDirective(); - } else if (c == '[') { - return parseHashSquare(); - } else { - // For consistency with Velocity, we treat # not followed by # or a letter as a plain - // character, and we treat #$foo as a literal # followed by the reference $foo. - // But the # is its own ConstantExpressionNode; we don't try to merge it with adjacent text. - return new ConstantExpressionNode(resourceName, lineNumber(), "#"); + switch (c) { + case '#': + return parseLineComment(); + case '*': + return parseBlockComment(); + case '[': + return parseHashSquare(); + case '{': + return parseDirective(); + default: + if (isAsciiLetter(c)) { + return parseDirective(); + } else { + // For consistency with Velocity, we treat # not followed by a letter or one of the + // characters above as a plain character, and we treat #$foo as a literal # followed by + // the reference $foo. + return parsePlainText('#'); + } } } if (c == EOF) { @@ -200,13 +254,15 @@ class Parser { assert c == '['; next(); if (c != '[') { - return new ConstantExpressionNode(resourceName, lineNumber(), "#["); + return parsePlainText(new StringBuilder("#[")); } + int startLine = lineNumber(); next(); StringBuilder sb = new StringBuilder(); while (true) { if (c == EOF) { - throw parseException("Unterminated #[[ - did not see matching ]]#"); + throw new ParseException( + "Unterminated #[[ - did not see matching ]]#", resourceName, startLine); } if (c == '#') { // This might be the last character of ]]# or it might just be a random #. @@ -458,10 +514,10 @@ class Parser { } /** - * Parses and discards a comment, which is {@code ##} followed by any number of characters up to - * and including the next newline. + * Parses and discards a line comment, which is {@code ##} followed by any number of characters + * up to and including the next newline. */ - private Node parseComment() throws IOException { + private Node parseLineComment() throws IOException { int lineNumber = lineNumber(); while (c != '\n' && c != EOF) { next(); @@ -471,6 +527,27 @@ class Parser { } /** + * Parses and discards a block comment, which is {@code #*} followed by everything up to and + * including the next {@code *#}. + */ + private Node parseBlockComment() throws IOException { + assert c == '*'; + int startLine = lineNumber(); + int lastC = '\0'; + next(); + while (!(lastC == '*' && c == '#')) { + if (c == EOF) { + throw new ParseException( + "Unterminated #* - did not see matching *#", resourceName, startLine); + } + lastC = c; + next(); + } + next(); + return new CommentTokenNode(resourceName, startLine); + } + + /** * Parses plain text, which is text that contains neither {@code $} nor {@code #}. The given * {@code firstChar} is the first character of the plain text, and {@link #c} is the second * (if the plain text is more than one character). @@ -478,7 +555,10 @@ class Parser { private Node parsePlainText(int firstChar) throws IOException { StringBuilder sb = new StringBuilder(); sb.appendCodePoint(firstChar); + return parsePlainText(sb); + } + private Node parsePlainText(StringBuilder sb) throws IOException { literal: while (true) { switch (c) { @@ -508,7 +588,27 @@ class Parser { * * <p>On entry to this method, {@link #c} is the character immediately after the {@code $}. */ - private ReferenceNode parseReference() throws IOException { + private Node parseReference() throws IOException { + if (c == '{') { + next(); + if (!isAsciiLetter(c)) { + return parsePlainText(new StringBuilder("${")); + } + ReferenceNode node = parseReferenceNoBrace(); + expect('}'); + return node; + } else { + return parseReferenceNoBrace(); + } + } + + /** + * Same as {@link #parseReference()}, except it really must be a reference. A {@code $} in + * normal text doesn't start a reference if it is not followed by an identifier. But in an + * expression, for example in {@code #if ($x == 23)}, {@code $} must be followed by an + * identifier. + */ + private ReferenceNode parseRequiredReference() throws IOException { if (c == '{') { next(); ReferenceNode node = parseReferenceNoBrace(); @@ -568,6 +668,11 @@ class Parser { private ReferenceNode parseReferenceMember(ReferenceNode lhs) throws IOException { assert c == '.'; next(); + if (!isAsciiLetter(c)) { + // We've seen something like `$foo.!`, so it turns out it's not a member after all. + pushback('.'); + return lhs; + } String id = parseId("Member"); ReferenceNode reference; if (c == '(') { @@ -670,19 +775,15 @@ class Parser { * Maps a code point to the operators that begin with that code point. For example, maps * {@code <} to {@code LESS} and {@code LESS_OR_EQUAL}. */ - private static final Map<Integer, List<Operator>> CODE_POINT_TO_OPERATORS; + private static final ImmutableListMultimap<Integer, Operator> CODE_POINT_TO_OPERATORS; static { - Map<Integer, List<Operator>> map = new HashMap<>(); + ImmutableListMultimap.Builder<Integer, Operator> builder = ImmutableListMultimap.builder(); for (Operator operator : Operator.values()) { if (operator != Operator.STOP) { - Integer key = operator.symbol.codePointAt(0); - if (!map.containsKey(key)) { - map.put(key, new ArrayList<Operator>()); - } - map.get(key).add(operator); + builder.put((int) operator.symbol.charAt(0), operator); } } - CODE_POINT_TO_OPERATORS = Collections.unmodifiableMap(map); + CODE_POINT_TO_OPERATORS = builder.build(); } /** @@ -753,17 +854,17 @@ class Parser { */ private void nextOperator() throws IOException { skipSpace(); - List<Operator> possibleOperators = CODE_POINT_TO_OPERATORS.get(c); - if (possibleOperators == null) { + ImmutableList<Operator> possibleOperators = CODE_POINT_TO_OPERATORS.get(c); + if (possibleOperators.isEmpty()) { currentOperator = Operator.STOP; return; } - int firstChar = c; + char firstChar = Chars.checkedCast(c); next(); Operator operator = null; for (Operator possibleOperator : possibleOperators) { if (possibleOperator.symbol.length() == 1) { - assert operator == null; + Verify.verify(operator == null); operator = possibleOperator; } else if (possibleOperator.symbol.charAt(1) == c) { next(); @@ -771,7 +872,8 @@ class Parser { } } if (operator == null) { - throw parseException("Expected " + possibleOperators.get(0) + ", not just " + firstChar); + throw parseException( + "Expected " + Iterables.getOnlyElement(possibleOperators) + ", not just " + firstChar); } currentOperator = operator; } @@ -818,7 +920,7 @@ class Parser { ExpressionNode node; if (c == '$') { next(); - node = parseReference(); + node = parseRequiredReference(); } else if (c == '"') { node = parseStringLiteral(); } else if (c == '-') { @@ -869,10 +971,8 @@ class Parser { sb.appendCodePoint(c); next(); } - int value; - try { - value = Integer.parseInt(sb.toString()); - } catch (NumberFormatException e) { + Integer value = Ints.tryParse(sb.toString()); + if (value == null) { throw parseException("Invalid integer: " + sb); } return new ConstantExpressionNode(resourceName, lineNumber(), value); @@ -896,29 +996,31 @@ class Parser { return new ConstantExpressionNode(resourceName, lineNumber(), value); } - private static final ImmutableAsciiSet ASCII_LETTER = - ImmutableAsciiSet.ofRange('A', 'Z') - .union(ImmutableAsciiSet.ofRange('a', 'z')); + private static final CharMatcher ASCII_LETTER = + CharMatcher.inRange('A', 'Z') + .or(CharMatcher.inRange('a', 'z')) + .precomputed(); - private static final ImmutableAsciiSet ASCII_DIGIT = - ImmutableAsciiSet.ofRange('0', '9'); + private static final CharMatcher ASCII_DIGIT = + CharMatcher.inRange('0', '9') + .precomputed(); - private static final ImmutableAsciiSet ID_CHAR = + private static final CharMatcher ID_CHAR = ASCII_LETTER - .union(ASCII_DIGIT) - .union(ImmutableAsciiSet.of('-')) - .union(ImmutableAsciiSet.of('_')); + .or(ASCII_DIGIT) + .or(CharMatcher.anyOf("-_")) + .precomputed(); private static boolean isAsciiLetter(int c) { - return ASCII_LETTER.contains(c); + return (char) c == c && ASCII_LETTER.matches((char) c); } private static boolean isAsciiDigit(int c) { - return ASCII_DIGIT.contains(c); + return (char) c == c && ASCII_DIGIT.matches((char) c); } private static boolean isIdChar(int c) { - return ID_CHAR.contains(c); + return (char) c == c && ID_CHAR.matches((char) c); } /** diff --git a/src/main/java/com/google/escapevelocity/ReferenceNode.java b/src/main/java/com/google/escapevelocity/ReferenceNode.java index 865d02a..6302561 100644 --- a/src/main/java/com/google/escapevelocity/ReferenceNode.java +++ b/src/main/java/com/google/escapevelocity/ReferenceNode.java @@ -11,14 +11,35 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.primitives.Primitives; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -227,13 +248,11 @@ abstract class ReferenceNode extends ExpressionNode { throw evaluationException( "Parameters for method " + id + " have wrong types: " + argValues); case 1: - return invokeMethod(compatibleMethods.get(0), lhsValue, argValues); + return invokeMethod(Iterables.getOnlyElement(compatibleMethods), lhsValue, argValues); default: - StringBuilder error = new StringBuilder("Ambiguous method invocation, could be one of:"); - for (Method method : compatibleMethods) { - error.append("\n ").append(method); - } - throw evaluationException(error.toString()); + throw evaluationException( + "Ambiguous method invocation, could be one of:\n " + + Joiner.on("\n ").join(compatibleMethods)); } } @@ -258,29 +277,11 @@ abstract class ReferenceNode extends ExpressionNode { return true; } - private static final Map<Class<?>, Class<?>> BOXED_TO_UNBOXED; - static { - Map<Class<?>, Class<?>> map = new HashMap<>(); - map.put(Byte.class, byte.class); - map.put(Short.class, short.class); - map.put(Integer.class, int.class); - map.put(Long.class, long.class); - map.put(Float.class, float.class); - map.put(Double.class, double.class); - map.put(Character.class, char.class); - map.put(Boolean.class, boolean.class); - BOXED_TO_UNBOXED = Collections.unmodifiableMap(map); - } - private static boolean primitiveIsCompatible(Class<?> primitive, Object value) { - if (value == null) { - return false; - } - Class<?> unboxed = BOXED_TO_UNBOXED.get(value.getClass()); - if (unboxed == null) { + if (value == null || !Primitives.isWrapperType(value.getClass())) { return false; } - return primitiveTypeIsAssignmentCompatible(primitive, unboxed); + return primitiveTypeIsAssignmentCompatible(primitive, Primitives.unwrap(value.getClass())); } private static final ImmutableList<Class<?>> NUMERICAL_PRIMITIVES = ImmutableList.<Class<?>>of( diff --git a/src/main/java/com/google/escapevelocity/Reparser.java b/src/main/java/com/google/escapevelocity/Reparser.java index 6235bc4..8e86180 100644 --- a/src/main/java/com/google/escapevelocity/Reparser.java +++ b/src/main/java/com/google/escapevelocity/Reparser.java @@ -11,6 +11,25 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 static com.google.escapevelocity.Node.emptyNode; @@ -29,6 +48,10 @@ import com.google.escapevelocity.TokenNode.IfOrElseIfTokenNode; import com.google.escapevelocity.TokenNode.IfTokenNode; import com.google.escapevelocity.TokenNode.MacroDefinitionTokenNode; import com.google.escapevelocity.TokenNode.NestedTokenNode; +import com.google.common.base.CharMatcher; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import java.util.Map; import java.util.Set; import java.util.TreeMap; @@ -67,7 +90,7 @@ class Reparser { private final Map<String, Macro> macros; Reparser(ImmutableList<Node> nodes) { - this(nodes, new TreeMap<String, Macro>()); + this(nodes, new TreeMap<>()); } private Reparser(ImmutableList<Node> nodes, Map<String, Macro> macros) { @@ -94,7 +117,7 @@ class Reparser { * ({@code $x} or {@code $x.foo} etc); a macro definition; or another {@code #set}. */ private static ImmutableList<Node> removeSpaceBeforeSet(ImmutableList<Node> nodes) { - assert nodes.get(nodes.size() - 1) instanceof EofNode : nodes.get(nodes.size() - 1); + assert Iterables.getLast(nodes) instanceof EofNode; // Since the last node is EofNode, the i + 1 and i + 2 accesses below are safe. ImmutableList.Builder<Node> newNodes = ImmutableList.builder(); for (int i = 0; i < nodes.size(); i++) { @@ -120,18 +143,7 @@ class Reparser { private static boolean isWhitespaceLiteral(Node node) { if (node instanceof ConstantExpressionNode) { Object constant = node.evaluate(null); - if (constant instanceof String) { - String s = (String) constant; - int i = 0; - while (i < s.length()) { - int c = s.codePointAt(i); - if (!Character.isWhitespace(c)) { - return false; - } - i += Character.charCount(c); - } - return true; - } + return constant instanceof String && CharMatcher.whitespace().matchesAllOf((String) constant); } return false; } diff --git a/src/main/java/com/google/escapevelocity/Template.java b/src/main/java/com/google/escapevelocity/Template.java index 646c42b..3613dbe 100644 --- a/src/main/java/com/google/escapevelocity/Template.java +++ b/src/main/java/com/google/escapevelocity/Template.java @@ -11,6 +11,25 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.EvaluationContext.PlainEvaluationContext; @@ -60,15 +79,12 @@ public class Template { * Parses a VTL template from the given {@code Reader}. The given Reader will be closed on * return from this method. */ - public static Template parseFrom(final Reader reader) throws IOException { - ResourceOpener resourceOpener = new ResourceOpener() { - @Override - public Reader openResource(String resourceName) throws IOException { - if (resourceName == null) { - return reader; - } else { - throw new IOException("No ResourceOpener has been configured to read " + resourceName); - } + public static Template parseFrom(Reader reader) throws IOException { + ResourceOpener resourceOpener = resourceName -> { + if (resourceName == null) { + return reader; + } else { + throw new IOException("No ResourceOpener has been configured to read " + resourceName); } }; try { diff --git a/src/main/java/com/google/escapevelocity/TokenNode.java b/src/main/java/com/google/escapevelocity/TokenNode.java index 1e92109..a7226fc 100644 --- a/src/main/java/com/google/escapevelocity/TokenNode.java +++ b/src/main/java/com/google/escapevelocity/TokenNode.java @@ -11,8 +11,28 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.common.collect.ImmutableList; import java.util.List; /** @@ -164,4 +184,3 @@ abstract class TokenNode extends Node { } } } - diff --git a/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java b/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java index 660c237..c71eb1a 100644 --- a/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java +++ b/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java @@ -15,11 +15,11 @@ package com.google.escapevelocity; import static com.google.common.truth.Truth.assertThat; +import com.google.escapevelocity.ReferenceNode.MethodReferenceNode; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Primitives; import com.google.common.truth.Expect; -import com.google.escapevelocity.ReferenceNode.MethodReferenceNode; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Collections; diff --git a/src/test/java/com/google/escapevelocity/TemplateTest.java b/src/test/java/com/google/escapevelocity/TemplateTest.java index bd769d6..0437734 100644 --- a/src/test/java/com/google/escapevelocity/TemplateTest.java +++ b/src/test/java/com/google/escapevelocity/TemplateTest.java @@ -14,24 +14,32 @@ package com.google.escapevelocity; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.truth.Expect; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.StringReader; import java.io.StringWriter; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; +import java.util.function.Supplier; +import org.apache.commons.collections.ExtendedProperties; import org.apache.velocity.VelocityContext; +import org.apache.velocity.exception.ResourceNotFoundException; import org.apache.velocity.runtime.RuntimeConstants; import org.apache.velocity.runtime.RuntimeInstance; import org.apache.velocity.runtime.log.NullLogChute; import org.apache.velocity.runtime.parser.node.SimpleNode; +import org.apache.velocity.runtime.resource.Resource; +import org.apache.velocity.runtime.resource.loader.ResourceLoader; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -52,18 +60,21 @@ public class TemplateTest { private RuntimeInstance velocityRuntimeInstance; @Before - public void setUp() { - velocityRuntimeInstance = new RuntimeInstance(); + public void initVelocityRuntimeInstance() { + velocityRuntimeInstance = newVelocityRuntimeInstance(); + velocityRuntimeInstance.init(); + } + + private RuntimeInstance newVelocityRuntimeInstance() { + RuntimeInstance runtimeInstance = new RuntimeInstance(); // Ensure that $undefinedvar will produce an exception rather than outputting $undefinedvar. - velocityRuntimeInstance.setProperty(RuntimeConstants.RUNTIME_REFERENCES_STRICT, "true"); - velocityRuntimeInstance.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, - new NullLogChute()); + runtimeInstance.setProperty(RuntimeConstants.RUNTIME_REFERENCES_STRICT, "true"); // Disable any logging that Velocity might otherwise see fit to do. - velocityRuntimeInstance.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new NullLogChute()); - - velocityRuntimeInstance.init(); + runtimeInstance.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, new NullLogChute()); + runtimeInstance.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new NullLogChute()); + return runtimeInstance; } private void compare(String template) { @@ -71,7 +82,7 @@ public class TemplateTest { } private void compare(String template, Map<String, ?> vars) { - compare(template, Suppliers.ofInstance(vars)); + compare(template, () -> vars); } /** @@ -89,11 +100,13 @@ public class TemplateTest { try { escapeVelocityRendered = Template.parseFrom(new StringReader(template)).evaluate(escapeVelocityVars); - } catch (IOException e) { - throw new AssertionError(e); + } catch (Exception e) { + throw new AssertionError( + "EscapeVelocity failed, but Velocity succeeded and returned: <" + velocityRendered + ">", + e); } - String failure = "from velocity: <" + velocityRendered + ">\n" - + "from escape velocity: <" + escapeVelocityRendered + ">\n"; + String failure = "from Velocity: <" + velocityRendered + ">\n" + + "from EscapeVelocity: <" + escapeVelocityRendered + ">\n"; expect.withMessage(failure).that(escapeVelocityRendered).isEqualTo(velocityRendered); } @@ -124,17 +137,58 @@ public class TemplateTest { } @Test - public void comment() { + public void lineComment() { compare("line 1 ##\n line 2"); } @Test + public void blockComment() { + compare("line 1 #* blah\n line 2 * #\n line 3 *# \n line 4"); + compare("foo #*# bar *# baz"); + compare("foo #* one *# #* two *# #* three *#"); + compare("foo #** bar *# #* baz **#"); + } + + @Test + public void ignoreHashIfNotDirectiveOrComment() { + compare("# if is not a directive because of the space"); + compare("#<foo>"); + compare("# <foo>"); + compare("${foo}#${bar}", ImmutableMap.of("foo", "xxx", "bar", "yyy")); + } + + @Test + public void blockQuote() { + compare("#[[]]#"); + compare("x#[[]]#y"); + compare("#[[$notAReference #notADirective]]#"); + compare("#[[ [[ ]] ]# ]]#"); + compare("#[ foo"); + compare("x\n #[[foo\nbar\nbaz]]#y"); + } + + @Test public void substituteNoBraces() { compare(" $x ", ImmutableMap.of("x", 1729)); compare(" ! $x ! ", ImmutableMap.of("x", 1729)); } @Test + public void dollarWithoutId() { + compare(" $? "); + compare(" $$ "); + compare(" $. "); + compare(" $[ "); + } + + @Test + public void doubleDollar() { + // The first $ is plain text and the second one starts a reference. + compare(" $$foo ", ImmutableMap.of("foo", true)); + compare(" $${foo} ", ImmutableMap.of("foo", true)); + } + + @Test public void substituteWithBraces() { compare("a${x}\nb", ImmutableMap.of("x", "1729")); } @@ -150,6 +204,18 @@ public class TemplateTest { } @Test + public void substituteNotPropertyId() { + compare("$foo.!", ImmutableMap.of("foo", false)); + } + + /* TODO(emcmanus): make this work. + @Test + public void substituteNotPropertyId() { + compare("$foo.!", ImmutableMap.of("foo", false)); + } + */ + + @Test public void substituteNestedProperty() { compare("\n$t.name.empty\n", ImmutableMap.of("t", Thread.currentThread())); } @@ -206,6 +272,10 @@ public class TemplateTest { compare("<AZaz-foo_bar23>", ImmutableMap.of("AZaz-foo_bar23", "(P)")); } + /** + * A public class with a public {@code get} method that has one argument. That means instances can + * be used like {@code $indexable["foo"]}. + */ public static class Indexable { public String get(String y) { return "[" + y + "]"; @@ -319,7 +389,7 @@ public class TemplateTest { /** * Tests the surprising definition of equality mentioned in - * {@link ExpressionNode.EqualsExpressionNode}. + * {@link ExpressionNode.BinaryExpressionNode}. */ @Test public void funkyEquals() { @@ -452,6 +522,7 @@ public class TemplateTest { compare("x #set($x = 0) #set($x = 0) #set($x = 0) y"); compare("x ## comment\n #set($x = 0) y"); + compare("x #* comment *# #set($x = 0) y"); } @Test @@ -633,6 +704,14 @@ public class TemplateTest { } @Test + public void badBraceReference() throws IOException { + String template = "line 1\nline 2\nbar${foo.!}baz"; + thrown.expect(ParseException.class); + thrown.expectMessage("Expected }, on line 3, at text starting: .!}baz"); + Template.parseFrom(new StringReader(template)); + } + + @Test public void undefinedMacro() throws IOException { String template = "#oops()"; thrown.expect(ParseException.class); @@ -650,4 +729,117 @@ public class TemplateTest { Template.parseFrom(new StringReader(template)); } + @Test + public void unclosedBlockQuote() throws IOException { + String template = "foo\nbar #[[\nblah\nblah"; + thrown.expect(ParseException.class); + thrown.expectMessage("Unterminated #[[ - did not see matching ]]#, on line 2"); + Template.parseFrom(new StringReader(template)); + } + + @Test + public void unclosedBlockComment() throws IOException { + String template = "foo\nbar #*\nblah\nblah"; + thrown.expect(ParseException.class); + thrown.expectMessage("Unterminated #* - did not see matching *#, on line 2"); + Template.parseFrom(new StringReader(template)); + } + + /** + * A Velocity ResourceLoader that looks resources up in a map. This allows us to test directives + * that read "resources", for example {@code #parse}, without needing to make separate files to + * put them in. + */ + private static final class MapResourceLoader extends ResourceLoader { + private final ImmutableMap<String, String> resourceMap; + + MapResourceLoader(ImmutableMap<String, String> resourceMap) { + this.resourceMap = resourceMap; + } + + @Override + public void init(ExtendedProperties configuration) { + } + + @Override + public InputStream getResourceStream(String source) { + String resource = resourceMap.get(source); + if (resource == null) { + throw new ResourceNotFoundException(source); + } + return new ByteArrayInputStream(resource.getBytes(StandardCharsets.ISO_8859_1)); + } + + @Override + public boolean isSourceModified(Resource resource) { + return false; + } + + @Override + public long getLastModified(Resource resource) { + return 0; + } + }; + + private String renderWithResources( + String templateResourceName, + ImmutableMap<String, String> resourceMap, + ImmutableMap<String, String> vars) { + MapResourceLoader mapResourceLoader = new MapResourceLoader(resourceMap); + RuntimeInstance runtimeInstance = newVelocityRuntimeInstance(); + runtimeInstance.setProperty("resource.loader", "map"); + runtimeInstance.setProperty("map.resource.loader.instance", mapResourceLoader); + runtimeInstance.init(); + org.apache.velocity.Template velocityTemplate = + runtimeInstance.getTemplate(templateResourceName); + StringWriter velocityWriter = new StringWriter(); + VelocityContext velocityContext = new VelocityContext(new TreeMap<>(vars)); + velocityTemplate.merge(velocityContext, velocityWriter); + return velocityWriter.toString(); + } + + @Test + public void parseDirective() throws IOException { + // If outer.vm does #parse("nested.vm"), then we should be able to #set a variable in + // nested.vm and use it in outer.vm, and we should be able to define a #macro in nested.vm + // and call it in outer.vm. + ImmutableMap<String, String> resources = ImmutableMap.of( + "outer.vm", + "first line\n" + + "#parse (\"nested.vm\")\n" + + "<#decorate (\"left\" \"right\")>\n" + + "$baz skidoo\n" + + "last line\n", + "nested.vm", + "nested template first line\n" + + "[#if ($foo == $bar) equal #else not equal #end]\n" + + "#macro (decorate $a $b) < $a | $b > #end\n" + + "#set ($baz = 23)\n" + + "nested template last line\n"); + + ImmutableMap<String, String> vars = ImmutableMap.of("foo", "foovalue", "bar", "barvalue"); + + String velocityResult = renderWithResources("outer.vm", resources, vars); + + Template.ResourceOpener resourceOpener = resourceName -> { + String resource = resources.get(resourceName); + if (resource == null) { + throw new FileNotFoundException(resourceName); + } + return new StringReader(resource); + }; + Template template = Template.parseFrom("outer.vm", resourceOpener); + + String result = template.evaluate(vars); + assertThat(result).isEqualTo(velocityResult); + + ImmutableMap<String, String> badVars = ImmutableMap.of("foo", "foovalue"); + try { + template.evaluate(badVars); + fail(); + } catch (EvaluationException e) { + assertThat(e).hasMessageThat().isEqualTo( + "In expression on line 2 of nested.vm: Undefined reference $bar"); + } + } } |