diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 7ba1991f79de7..a627e5d6d4b1e 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -184,35 +184,27 @@ In this case, all whitespace characters from a standalone line will be printed t [[expressions]] ==== Expressions -An expression consists of: - -* an optional namespace followed by a colon (`:`), -* one or more parts separated by dot (`.`). - -The first part of the expression is always resolved against the <>. -If no result is found for the first part it's resolved against the parent context object (if available). -For an expression that starts with a namespace the current context object is found using all the available ``NamespaceResolver``s. -For an expression that does not start with a namespace the current context object is *derived from the position* of the tag. -All other parts are resolved using ``ValueResolver``s against the result of the previous resolution. - -For example, expression `{name}` has no namespace and single part - `name`. -The "name" will be resolved using all available value resolvers against the current context object. -However, the expression `{global:colors}` has the namespace `global` and single part - `colors`. -First, all available ``NamespaceResolver``s will be used to find the current context object. -And afterwards value resolvers will be used to resolve "colors" against the context object found. +An expression outputs a value. +It consists of one or more parts separated by dot (dot notation) or square brackets (bracket notation). +In the `object[property_name]` syntax, the `property_name` has to be a non-null <> value. +An expression could start with an optional namespace followed by a colon (`:`). +.Expressions Example [source] ---- {name} <1> {item.name} <2> -{global:colors} <3> +{item['name']} <3> +{global:colors} <4> ---- -<1> no namespace, one part -`name` -<2> no namespace, two parts - `item`, `name` -<3> namespace `global`, one part - `colors` +<1> no namespace, one part: `name` +<2> no namespace, two parts: `item`, `name` +<3> equivalent to `{item.name}` but using the bracket notation +<4> namespace `global`, one part: `colors` -An expression part could be a "virtual method" in which case the name can be followed by a list of comma-separated parameters in parentheses: +A part of an expression could be a _virtual method_ in which case the name can be followed by a list of comma-separated parameters in parentheses: +.Virtual Method Example [source] ---- {item.getLabels(1)} <1> @@ -221,6 +213,51 @@ An expression part could be a "virtual method" in which case the name can be fol <1> no namespace, two parts - `item`, `getLabels(1)`, the second part is a virtual method with name `getLabels` and params `1` <2> infix notation, translated to `name.or('John')`; no namespace, two parts - `name`, `or('John')` +NOTE: A parameter of a virtual method can be a <> value. + +[[literals]] +===== Supported Literals + +|=== +|Literal |Examples + +|boolean +|`true`, `false` + +|null +|`null` + +|string +|`'value'`, `"string"` + +|integer +|`1`, `-5` + +|long +|`1l`, `-5L` + +|double +|`1D`, `-5d` + +|float +|`1f`, `-5F` + +|=== + +===== Resolution + +The first part of the expression is always resolved against the <>. +If no result is found for the first part it's resolved against the parent context object (if available). +For an expression that starts with a namespace the current context object is found using all the available ``NamespaceResolver``s. +For an expression that does not start with a namespace the current context object is *derived from the position* of the tag. +All other parts are resolved using ``ValueResolver``s against the result of the previous resolution. + +For example, expression `{name}` has no namespace and single part - `name`. +The "name" will be resolved using all available value resolvers against the current context object. +However, the expression `{global:colors}` has the namespace `global` and single part - `colors`. +First, all available ``NamespaceResolver``s will be used to find the current context object. +And afterwards value resolvers will be used to resolve "colors" against the context object found. + [[current_context_object]] ===== Current Context diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index b5297c230de05..3e01ff17c9ebb 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -659,8 +659,15 @@ public String apply(String id) { Map implicitClassToMethodUsed = new HashMap<>(); for (Expression expression : injectExpressions) { - - String beanName = expression.getParts().get(0).getName(); + Expression.Part firstPart = expression.getParts().get(0); + if (firstPart.isVirtualMethod()) { + incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), + "The inject: namespace must be followed by a bean name", + expression.getOrigin().getLine(), + expression.getOrigin().getTemplateGeneratedId())); + continue; + } + String beanName = firstPart.getName(); BeanInfo bean = namedBeans.get(beanName); if (bean != null) { if (expression.getParts().size() == 1) { diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/AppMessages.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/AppMessages.java index ebc95df60b081..391a0cb0798af 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/AppMessages.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/AppMessages.java @@ -25,4 +25,7 @@ public interface AppMessages { @Message("Item name: {item.name}, age: {item.age}") String itemDetail(Item item); + @Message(key = "dot.test", value = "Dot test!") + String dotTest(); + } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java index 81599c68c7f54..441296a36f96b 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java @@ -69,6 +69,7 @@ public void testResolvers() { foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render()); assertEquals("Hallo Welt! Hallo Jachym! Hello you guys! Hello alpha! Hello!", foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.GERMAN).render()); + assertEquals("Dot test!", engine.parse("{msg:['dot.test']}").render()); } } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvalContext.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvalContext.java index fc52488b2c69a..5bf28609a42a2 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvalContext.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvalContext.java @@ -6,7 +6,7 @@ /** * Evaluation context of a specific part of an expression. * - * @see Expression + * @see Expression.Part */ public interface EvalContext { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expression.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expression.java index 6d8dfc0e869d0..d9ee324a188ee 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expression.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expression.java @@ -72,7 +72,7 @@ interface Part { /** * - * @return the name of the property or virtual method + * @return the name of a property or virtual method */ String getName(); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionImpl.java index 75897a8bbf107..8444086203645 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionImpl.java @@ -9,130 +9,14 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -/** - * - */ final class ExpressionImpl implements Expression { - static class VirtualMethodExpressionPartImpl extends ExpressionPartImpl implements VirtualMethodPart { - - private final List parameters; - - VirtualMethodExpressionPartImpl(String name, List parameters) { - super(name, null); - this.parameters = parameters; - } - - public List getParameters() { - return parameters; - } - - @Override - public boolean isVirtualMethod() { - return true; - } - - @Override - public VirtualMethodPart asVirtualMethod() { - return this; - } - - @Override - public String getTypeInfo() { - return toString(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + Objects.hash(parameters); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - VirtualMethodExpressionPartImpl other = (VirtualMethodExpressionPartImpl) obj; - return Objects.equals(parameters, other.parameters); - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(name).append("("); - for (Iterator iterator = parameters.iterator(); iterator.hasNext();) { - Expression expression = iterator.next(); - builder.append(expression.toOriginalString()); - if (iterator.hasNext()) { - builder.append(","); - } - } - builder.append(")"); - return builder.toString(); - } - - } - - static class ExpressionPartImpl implements Part { - - protected final String name; - protected final String typeInfo; - - ExpressionPartImpl(String name, String typeInfo) { - this.name = name; - this.typeInfo = typeInfo; - } - - public String getName() { - return name; - } - - public String getTypeInfo() { - return typeInfo; - } - - @Override - public int hashCode() { - return Objects.hash(name, typeInfo); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - ExpressionPartImpl other = (ExpressionPartImpl) obj; - return Objects.equals(name, other.name) && Objects.equals(typeInfo, other.typeInfo); - } - - @Override - public String toString() { - return name; - } - - } - static final ExpressionImpl EMPTY = new ExpressionImpl(null, Collections.emptyList(), null, null); /** * * @param value - * @return a non-contextual expression + * @return a "non-contextual" expression */ static ExpressionImpl from(String value) { if (value == null || value.isEmpty()) { @@ -157,7 +41,7 @@ static ExpressionImpl literal(String literal, Object value, Origin origin) { throw new IllegalArgumentException("Literal must not be null"); } return new ExpressionImpl(null, - Collections.singletonList(new ExpressionPartImpl(literal, + Collections.singletonList(new PartImpl(literal, value != null ? Expressions.TYPE_INFO_SEPARATOR + value.getClass().getName() + Expressions.TYPE_INFO_SEPARATOR : null)), @@ -252,4 +136,116 @@ private Object literalValue() { return null; } + static class VirtualMethodPartImpl extends PartImpl implements VirtualMethodPart { + + private final List parameters; + + VirtualMethodPartImpl(String name, List parameters) { + super(name, null); + this.parameters = parameters; + } + + public List getParameters() { + return parameters; + } + + @Override + public boolean isVirtualMethod() { + return true; + } + + @Override + public VirtualMethodPart asVirtualMethod() { + return this; + } + + @Override + public String getTypeInfo() { + return toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Objects.hash(parameters); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + VirtualMethodPartImpl other = (VirtualMethodPartImpl) obj; + return Objects.equals(parameters, other.parameters); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(name).append("("); + for (Iterator iterator = parameters.iterator(); iterator.hasNext();) { + Expression expression = iterator.next(); + builder.append(expression.toOriginalString()); + if (iterator.hasNext()) { + builder.append(","); + } + } + builder.append(")"); + return builder.toString(); + } + + } + + static class PartImpl implements Part { + + protected final String name; + protected final String typeInfo; + + PartImpl(String name, String typeInfo) { + this.name = name; + this.typeInfo = typeInfo; + } + + public String getName() { + return name; + } + + public String getTypeInfo() { + return typeInfo; + } + + @Override + public int hashCode() { + return Objects.hash(name, typeInfo); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + PartImpl other = (PartImpl) obj; + return Objects.equals(name, other.name) && Objects.equals(typeInfo, other.typeInfo); + } + + @Override + public String toString() { + return name; + } + + } } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expressions.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expressions.java index 534c282decb1a..989d8b0f56792 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expressions.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expressions.java @@ -2,7 +2,6 @@ import java.util.Collections; import java.util.List; -import java.util.function.Predicate; import java.util.stream.Collectors; public final class Expressions { @@ -11,6 +10,8 @@ public final class Expressions { static final String LEFT_BRACKET = "("; static final String RIGHT_BRACKET = ")"; + static final String SQUARE_LEFT_BRACKET = "["; + static final String SQUARE_RIGHT_BRACKET = "]"; public static final char TYPE_INFO_SEPARATOR = '|'; private Expressions() { @@ -20,6 +21,10 @@ public static boolean isVirtualMethod(String value) { return value.indexOf(LEFT_BRACKET) != -1; } + public static boolean isBracketNotation(String value) { + return value.startsWith(SQUARE_LEFT_BRACKET); + } + public static String parseVirtualMethodName(String value) { int start = value.indexOf(LEFT_BRACKET); return value.substring(0, start); @@ -29,17 +34,24 @@ public static List parseVirtualMethodParams(String value) { int start = value.indexOf(LEFT_BRACKET); if (start != -1 && value.endsWith(RIGHT_BRACKET)) { String params = value.substring(start + 1, value.length() - 1); - return splitParts(params, Expressions::isParamSeparator, Parser::isStringLiteralSeparator); + return splitParts(params, PARAMS_SPLIT_CONFIG); } throw new IllegalArgumentException("Not a virtual method: " + value); } + public static String parseBracketContent(String value) { + if (value.endsWith(SQUARE_RIGHT_BRACKET)) { + return value.substring(1, value.length() - 1); + } + throw new IllegalArgumentException("Not a bracket notation expression: " + value); + } + public static String buildVirtualMethodSignature(String name, List params) { return name + LEFT_BRACKET + params.stream().collect(Collectors.joining(",")) + RIGHT_BRACKET; } public static List splitParts(String value) { - return splitParts(value, Parser::isSeparator, Parser::isStringLiteralSeparator); + return splitParts(value, DEFAULT_SPLIT_CONFIG); } /** @@ -48,17 +60,10 @@ public static List splitParts(String value) { * @return the parts */ public static List splitTypeInfoParts(String value) { - return splitParts(value, Parser::isSeparator, new Predicate() { - - @Override - public boolean test(Character t) { - return t == TYPE_INFO_SEPARATOR; - } - }.or(Parser::isStringLiteralSeparator)); + return splitParts(value, TYPE_INFO_SPLIT_CONFIG); } - public static List splitParts(String value, Predicate separatorPredicate, - Predicate literalSeparatorPredicate) { + public static List splitParts(String value, SplitConfig splitConfig) { if (value == null || value.isEmpty()) { return Collections.emptyList(); } @@ -70,21 +75,28 @@ public static List splitParts(String value, Predicate separat StringBuilder buffer = new StringBuilder(); for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); - if (separatorPredicate.test(c)) { - // Adjacent separators are ignored - if (!separator) { + if (splitConfig.isSeparator(c)) { + // Adjacent separators may be ignored + if (!separator || !splitConfig.ignoreAdjacentSeparator(c)) { if (!literal && brackets == 0) { + if (splitConfig.shouldPrependSeparator(c)) { + buffer.append(c); + } if (buffer.length() > 0) { + // Flush the part builder.add(buffer.toString()); buffer = new StringBuilder(); } + if (splitConfig.shouldAppendSeparator(c)) { + buffer.append(c); + } separator = true; } else { buffer.append(c); } } } else { - if (literalSeparatorPredicate.test(c)) { + if (splitConfig.isLiteralSeparator(c)) { literal = !literal; } // Non-separator char @@ -132,8 +144,69 @@ public static List splitParts(String value, Predicate separat return builder.build(); } - private static boolean isParamSeparator(char candidate) { - return ',' == candidate; + private static final SplitConfig DEFAULT_SPLIT_CONFIG = new DefaultSplitConfig(); + + private static final SplitConfig PARAMS_SPLIT_CONFIG = new SplitConfig() { + + @Override + public boolean isSeparator(char candidate) { + return ',' == candidate; + } + + }; + + private static final SplitConfig TYPE_INFO_SPLIT_CONFIG = new DefaultSplitConfig() { + + @Override + public boolean isLiteralSeparator(char candidate) { + return candidate == TYPE_INFO_SEPARATOR || Parser.isStringLiteralSeparator(candidate); + } + }; + + private static class DefaultSplitConfig implements SplitConfig { + + @Override + public boolean isSeparator(char candidate) { + return candidate == '.' || candidate == '[' || candidate == ']'; + } + + @Override + public boolean shouldPrependSeparator(char candidate) { + return candidate == ']'; + } + + @Override + public boolean shouldAppendSeparator(char candidate) { + return candidate == '['; + } + + @Override + public boolean ignoreAdjacentSeparator(char candidate) { + return candidate == '.'; + } + + } + + interface SplitConfig { + + boolean isSeparator(char candidate); + + default boolean isLiteralSeparator(char candidate) { + return Parser.isStringLiteralSeparator(candidate); + } + + default boolean shouldPrependSeparator(char candidate) { + return false; + } + + default boolean shouldAppendSeparator(char candidate) { + return false; + } + + default boolean ignoreAdjacentSeparator(char candidate) { + return isSeparator(candidate); + } + } } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java index d0c8a7e9c6d42..8ffb3effd7ee4 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java @@ -686,8 +686,25 @@ private static Part createPart(String namespace, Part first, String value, Scope for (String strParam : strParams) { params.add(parseExpression(strParam.trim(), scope, origin)); } - return new ExpressionImpl.VirtualMethodExpressionPartImpl(name, params); + return new ExpressionImpl.VirtualMethodPartImpl(name, params); + } + // Try to parse the literal for bracket notation + if (Expressions.isBracketNotation(value)) { + value = Expressions.parseBracketContent(value); + Object literal = LiteralSupport.getLiteralValue(value); + if (literal != null && !Result.NOT_FOUND.equals(literal)) { + value = literal.toString(); + } else { + StringBuilder builder = new StringBuilder(literal == null ? "Null" : "Non-literal"); + builder.append(" value used in bracket notation [").append(value).append("]"); + if (!origin.getTemplateId().equals(origin.getTemplateGeneratedId())) { + builder.append(" in template [").append(origin.getTemplateId()).append("]"); + } + builder.append(" on line ").append(origin.getLine()); + throw new IllegalArgumentException(builder.toString()); + } } + String typeInfo = null; if (namespace != null) { typeInfo = value; @@ -696,11 +713,7 @@ private static Part createPart(String namespace, Part first, String value, Scope } else if (first.getTypeInfo() != null) { typeInfo = value; } - return new ExpressionImpl.ExpressionPartImpl(value, typeInfo); - } - - static boolean isSeparator(char candidate) { - return candidate == '.' || candidate == '[' || candidate == ']'; + return new ExpressionImpl.PartImpl(value, typeInfo); } /** diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ExpressionTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ExpressionTest.java index 5a5e397ac899e..3e0e1eb6e16bb 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ExpressionTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ExpressionTest.java @@ -3,6 +3,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import io.quarkus.qute.Expression.Part; import java.util.Arrays; @@ -17,7 +19,6 @@ public class ExpressionTest { public void testExpressions() throws InterruptedException, ExecutionException { verify("data:name.value", "data", null, name("name", "name"), name("value", "value")); verify("name.value", null, null, name("name"), name("value")); - verify("name[value]", null, null, name("name"), name("value")); verify("0", null, CompletableFuture.completedFuture(Integer.valueOf(0)), name("0", "|java.lang.Integer|")); verify("false", null, CompletableFuture.completedFuture(Boolean.FALSE), name("false", "|java.lang.Boolean|")); verify("null", null, CompletableFuture.completedFuture(null), name("null")); @@ -39,6 +40,24 @@ public void testExpressions() throws InterruptedException, ExecutionException { verify("foo.call(bar.alpha(1),bar.alpha('ping'))", null, null, name("foo"), virtualMethod("call", ExpressionImpl.from("bar.alpha(1)"), ExpressionImpl.from("bar.alpha('ping')"))); verify("'foo:bar'", null, CompletableFuture.completedFuture("foo:bar"), name("'foo:bar'", "|java.lang.String|")); + // bracket notation + verify("name['value']", null, null, name("name"), name("value")); + verify("name[false]", null, null, name("name"), name("false")); + verify("name[1l]", null, null, name("name"), name("1")); + try { + verify("name['value'][1][null]", null, null); + fail(); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("Null value")); + } + try { + verify("name[value]", null, null); + fail(); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("Non-literal value")); + } + //verify("name[1l]['foo']", null, null, name("name"), name("1"), name("foo")); + verify("foo[\"name.dot\"].value", null, null, name("foo"), name("name.dot"), name("value")); } @Test @@ -90,11 +109,11 @@ private Part name(String name) { } private Part name(String name, String typeInfo) { - return new ExpressionImpl.ExpressionPartImpl(name, typeInfo); + return new ExpressionImpl.PartImpl(name, typeInfo); } private Part virtualMethod(String name, Expression... params) { - return new ExpressionImpl.VirtualMethodExpressionPartImpl(name, Arrays.asList(params)); + return new ExpressionImpl.VirtualMethodPartImpl(name, Arrays.asList(params)); } } diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/MapResolverTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/MapResolverTest.java index 81927449353b5..00bd8b4b233ea 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/MapResolverTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/MapResolverTest.java @@ -2,7 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.junit.jupiter.api.Test; @@ -10,16 +10,20 @@ public class MapResolverTest { @Test public void tesMapResolver() { - Map map = new HashMap<>(); + Map map = new LinkedHashMap<>(); map.put("name", "Lu"); + map.put("foo.bar", "Ondrej"); Engine engine = Engine.builder() .addSectionHelper(new LoopSectionHelper.Factory()) .addDefaultValueResolvers() .build(); - assertEquals("Lu,1,false,true,name", - engine.parse("{this.name},{this.size},{this.empty},{this.containsKey('name')},{#each this.keys}{it}{/each}") + assertEquals("Lu,Lu,2,false,true,namefoo.bar::Ondrej,Ondrej", + engine.parse( + "{this.name},{this['name']},{this.size},{this.empty},{this.containsKey('name')}," + + "{#each this.keys}{it}{/each}" + + "::{this.get('foo.bar')},{this['foo.bar']}") .render(map)); }