From adfdf98c81ea4c1f889442ed1cbafede23b1c12a Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 28 Nov 2023 14:56:48 +0100 Subject: [PATCH] Qute: improve the key/property/method "not found" error message - which may occur at runtime --- .../PropertyNotFoundDevModeTest.java | 2 +- .../TimeTemplateExtensionsTest.java | 3 +- .../PropertyNotFoundThrowExceptionTest.java | 3 +- .../io/quarkus/qute/LoopSectionHelper.java | 13 ++++ .../src/main/java/io/quarkus/qute/Mapper.java | 23 ++++--- .../io/quarkus/qute/MapperMapWrapper.java | 29 ++++++++ .../main/java/io/quarkus/qute/Results.java | 64 ++++++++++++------ .../java/io/quarkus/qute/IfSectionTest.java | 3 +- .../io/quarkus/qute/NotFoundResultTest.java | 66 +++++++++++++++++++ .../test/java/io/quarkus/qute/SimpleTest.java | 2 +- .../java/io/quarkus/qute/WhenSectionTest.java | 21 +++++- 11 files changed, 188 insertions(+), 41 deletions(-) create mode 100644 independent-projects/qute/core/src/main/java/io/quarkus/qute/MapperMapWrapper.java create mode 100644 independent-projects/qute/core/src/test/java/io/quarkus/qute/NotFoundResultTest.java diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java index 3e32d1500e078..89cabb8733609 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java @@ -27,7 +27,7 @@ public class PropertyNotFoundDevModeTest { @Test public void testExceptionIsThrown() { assertEquals( - "Rendering error in template [foo.html] line 1: Entry \"foo\" not found in the data map in expression {foo.surname}", + "Rendering error in template [foo.html] line 1: Key \"foo\" not found in the template data map with keys [] in expression {foo.surname}", RestAssured.get("test-foo").then().statusCode(200).extract().body().asString()); assertEquals( "Rendering error in template [bar.html] line 1: Property \"name\" not found on the base object \"java.lang.String\" in expression {bar.name}", diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TimeTemplateExtensionsTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TimeTemplateExtensionsTest.java index ec758c43a46f3..9186d303a8fa1 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TimeTemplateExtensionsTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TimeTemplateExtensionsTest.java @@ -59,7 +59,8 @@ public void testInvalidParameter() { engine.parse("{time:format(input.birthday, 'uuuu')}").data("input", Map.of("name", "Quarkus Qute")).render(); fail(); } catch (TemplateException expected) { - assertTrue(expected.getMessage().startsWith("Rendering error: Property \"birthday\" not found on the base object"), + assertTrue( + expected.getMessage().startsWith("Rendering error: Key \"birthday\" not found in the map with keys [name]"), expected.getMessage()); } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/propertynotfound/PropertyNotFoundThrowExceptionTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/propertynotfound/PropertyNotFoundThrowExceptionTest.java index 789b4edb4b351..ef234400539a5 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/propertynotfound/PropertyNotFoundThrowExceptionTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/propertynotfound/PropertyNotFoundThrowExceptionTest.java @@ -36,7 +36,8 @@ public void testException() { } catch (Exception expected) { Throwable rootCause = ExceptionUtil.getRootCause(expected); assertEquals(TemplateException.class, rootCause.getClass()); - assertTrue(rootCause.getMessage().contains("Entry \"foos\" not found in the data map"), rootCause.getMessage()); + assertTrue(rootCause.getMessage().contains("Key \"foos\" not found in the template data map with keys []"), + rootCause.getMessage()); } } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java index d5b38ce6cc2c3..a25d8b4b11932 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletionStage; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -317,6 +318,18 @@ public CompletionStage getAsync(String key) { } } + @Override + public Set mappedKeys() { + if (metadataPrefix != null) { + return Set.of(alias, metadataPrefix + "count", metadataPrefix + "index", metadataPrefix + "indexParity", + metadataPrefix + "hasNext", metadataPrefix + "isLast", metadataPrefix + "isFirst", + metadataPrefix + "isOdd", metadataPrefix + "odd", metadataPrefix + "isEven", metadataPrefix + "even"); + } else { + return Set.of(alias, "count", "index", "indexParity", "hasNext", "isLast", "isFirst", "isOdd", + "odd", "isEven", "even"); + } + } + } enum Code implements ErrorCode { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Mapper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Mapper.java index b8ac2888b9d0a..49bce16d53e5e 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Mapper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Mapper.java @@ -1,6 +1,7 @@ package io.quarkus.qute; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletionStage; /** @@ -28,24 +29,22 @@ default boolean appliesTo(String key) { return true; } + /** + * The returned set may be a subset of the final set of all mapped keys. + * + * @return the set of known mapped keys + */ + default Set mappedKeys() { + return Set.of(); + } + /** * * @param map * @return a mapper that wraps the given map */ static Mapper wrap(Map map) { - return new Mapper() { - - @Override - public boolean appliesTo(String key) { - return map.containsKey(key); - } - - @Override - public Object get(String key) { - return map.get(key); - } - }; + return new MapperMapWrapper(map); } } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/MapperMapWrapper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/MapperMapWrapper.java new file mode 100644 index 0000000000000..762ebba454369 --- /dev/null +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/MapperMapWrapper.java @@ -0,0 +1,29 @@ +package io.quarkus.qute; + +import java.util.Map; +import java.util.Set; + +final class MapperMapWrapper implements Mapper { + + private final Map map; + + MapperMapWrapper(Map map) { + this.map = map; + } + + @Override + public boolean appliesTo(String key) { + return map.containsKey(key); + } + + @Override + public Object get(String key) { + return map.get(key); + } + + @Override + public Set mappedKeys() { + return map.keySet(); + } + +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Results.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Results.java index 079ceec677c3e..0cca2fb5418c5 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Results.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Results.java @@ -1,5 +1,7 @@ package io.quarkus.qute; +import static java.util.function.Predicate.not; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -135,29 +137,49 @@ public String asMessage() { if (name != null) { Object base = getBase().orElse(null); List params = getParams(); - boolean isDataMap = isDataMap(base); - // Entry "foo" not found in the data map - // Property "foo" not found on base object "org.acme.Bar" - // Method "getDiscount(value)" not found on base object "org.acme.Item" StringBuilder builder = new StringBuilder(); - if (isDataMap) { - builder.append("Entry "); - } else if (params.isEmpty()) { - builder.append("Property "); - } else { - builder.append("Method "); - } - builder.append("\"").append(name); - if (!params.isEmpty()) { - builder.append("("); - builder.append(params.stream().map(Expression::toOriginalString).collect(Collectors.joining(","))); - builder.append(")"); - } - builder.append("\" not found"); - if (isDataMap) { - builder.append(" in the data map"); + if (base instanceof Map || base instanceof Mapper) { + builder.append("Key ") + .append("\"") + .append(name) + .append("\" not found in the"); + if (isDataMap(base)) { + // Key "foo" not found in the template data map with keys [] + builder.append(" template data map with keys "); + if (base instanceof Map) { + builder.append(((Map) base).keySet().stream() + .filter(not(TemplateInstanceBase.DATA_MAP_KEY::equals)).collect(Collectors.toList())); + } else if (base instanceof Mapper) { + builder.append(((Mapper) base).mappedKeys().stream() + .filter(not(TemplateInstanceBase.DATA_MAP_KEY::equals)).collect(Collectors.toList())); + } + } else { + // Key "foo" not found in the map with keys [] + builder.append(" map with keys "); + if (base instanceof Map) { + builder.append(((Map) base).keySet()); + } else if (base instanceof Mapper) { + builder.append(((Mapper) base).mappedKeys()); + } + } + } else if (!params.isEmpty()) { + // Method "getDiscount(value)" not found on the base object "org.acme.Item" + builder.append("Method ") + .append("\"") + .append(name) + .append("(") + .append(params.stream().map(Expression::toOriginalString).collect(Collectors.joining(","))) + .append(")") + .append("\" not found") + .append(" on the base object \"").append(base == null ? "null" : base.getClass().getName()) + .append("\""); } else { - builder.append(" on the base object \"").append(base == null ? "null" : base.getClass().getName()) + // Property "foo" not found on the base object "org.acme.Bar" + builder.append("Property ") + .append("\"") + .append(name) + .append("\" not found") + .append(" on the base object \"").append(base == null ? "null" : base.getClass().getName()) .append("\""); } return builder.toString(); diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java index d5d9538ff086f..3f0d88c39f2a4 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java @@ -221,7 +221,8 @@ public void testSafeExpression() { engine.parse("{#if val.is.not.there}NOK{#else}OK{/if}").render(); fail(); } catch (TemplateException expected) { - assertEquals("Rendering error: Entry \"val\" not found in the data map in expression {val.is.not.there}", + assertEquals( + "Rendering error: Key \"val\" not found in the template data map with keys [] in expression {val.is.not.there}", expected.getMessage()); } assertEquals("OK", engine.parse("{#if val.is.not.there??}NOK{#else}OK{/if}").render()); diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/NotFoundResultTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/NotFoundResultTest.java new file mode 100644 index 0000000000000..24b14c90b4d3e --- /dev/null +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/NotFoundResultTest.java @@ -0,0 +1,66 @@ +package io.quarkus.qute; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import io.quarkus.qute.Results.NotFound; + +public class NotFoundResultTest { + + @Test + public void testAsMessage() { + assertEquals("Key \"foo\" not found in the template data map with keys []", + NotFound.from(evalContext(Map.of(TemplateInstanceBase.DATA_MAP_KEY, true))).asMessage()); + assertEquals("Key \"foo\" not found in the map with keys [baz]", + NotFound.from(evalContext(Map.of("baz", true))).asMessage()); + assertEquals("Property \"foo\" not found on the base object \"java.lang.Boolean\"", + NotFound.from(evalContext(Boolean.TRUE)).asMessage()); + assertEquals("Method \"foo(param)\" not found on the base object \"java.lang.Boolean\"", + NotFound.from(evalContext(Boolean.TRUE, "param")).asMessage()); + assertEquals("Key \"foo\" not found in the map with keys [baz]", + NotFound.from(evalContext(Mapper.wrap(Map.of("baz", false)))).asMessage()); + } + + EvalContext evalContext(Object base, Object... params) { + return new EvalContext() { + + @Override + public List getParams() { + return Arrays.stream(params).map(p -> ExpressionImpl.from(p.toString())).collect(Collectors.toList()); + } + + @Override + public String getName() { + return "foo"; + } + + @Override + public Object getBase() { + return base; + } + + @Override + public Object getAttribute(String key) { + return null; + } + + @Override + public CompletionStage evaluate(Expression expression) { + return null; + } + + @Override + public CompletionStage evaluate(String expression) { + return null; + } + }; + } + +} diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/SimpleTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/SimpleTest.java index 3e617c8229b88..642f7bf359058 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/SimpleTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/SimpleTest.java @@ -153,7 +153,7 @@ public void testEmptySectionTag() { @Test public void testNotFound() { - assertEquals("Entry \"foo\" not found in the data map in foo.bar Collection size: 0", + assertEquals("Key \"foo\" not found in the template data map with keys [collection] in foo.bar Collection size: 0", Engine.builder().strictRendering(false).addDefaultValueResolvers() .addResultMapper(new ResultMapper() { diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/WhenSectionTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/WhenSectionTest.java index c8e4eba0be092..d6e428ecb85c4 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/WhenSectionTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/WhenSectionTest.java @@ -59,7 +59,14 @@ public void testSwitchEnum() { try { fail(template.data("state", null).render()); } catch (TemplateException expected) { - assertEquals("Rendering error: Entry \"ON\" not found in the data map in expression {ON}", + // If state is null we can't detect it's an enum value, hence the weird error message + assertEquals("Rendering error: Key \"ON\" not found in the template data map with keys [state] in expression {ON}", + expected.getMessage()); + } + try { + fail(template.render()); + } catch (TemplateException expected) { + assertEquals("Rendering error: Key \"state\" not found in the template data map with keys [] in expression {state}", expected.getMessage()); } } @@ -75,7 +82,14 @@ public void testWhenEnum() { try { fail(template.data("state", null).render()); } catch (TemplateException expected) { - assertEquals("Rendering error: Entry \"ON\" not found in the data map in expression {ON}", + // If state is null we can't detect it's an enum value, hence the weird error message + assertEquals("Rendering error: Key \"ON\" not found in the template data map with keys [state] in expression {ON}", + expected.getMessage()); + } + try { + fail(template.render()); + } catch (TemplateException expected) { + assertEquals("Rendering error: Key \"state\" not found in the template data map with keys [] in expression {state}", expected.getMessage()); } } @@ -129,7 +143,8 @@ public void testWhenNotFound() { try { fail(template.render()); } catch (TemplateException expected) { - assertEquals("Rendering error: Entry \"testMe\" not found in the data map in expression {testMe}", + assertEquals( + "Rendering error: Key \"testMe\" not found in the template data map with keys [] in expression {testMe}", expected.getMessage()); } }