From f0a79ec47a70e0fbc1b5213828be01021e20fdd2 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 7 Dec 2021 17:18:41 +0100 Subject: [PATCH] Qute user tags - use defaulted keys if appropriate - i.e. for params that define no key and contain only single part - also use Mapper instead of Map for the data passed to a template - Mapper value resolver has priority 15, i.e. is called before a generated value resolver - resolves #21855 - related to #21861 --- docs/src/main/asciidoc/qute-reference.adoc | 19 +++-- .../io/quarkus/qute/LoopSectionHelper.java | 2 +- .../main/java/io/quarkus/qute/Parameter.java | 66 ++++++++++++++++- .../src/main/java/io/quarkus/qute/Parser.java | 55 +++++++------- .../quarkus/qute/ResolutionContextImpl.java | 9 ++- .../main/java/io/quarkus/qute/Results.java | 11 ++- .../java/io/quarkus/qute/SectionHelper.java | 9 +++ .../io/quarkus/qute/SectionHelperFactory.java | 50 ++++++++++++- .../java/io/quarkus/qute/SectionNode.java | 7 ++ .../java/io/quarkus/qute/TemplateImpl.java | 2 +- .../io/quarkus/qute/TemplateInstanceBase.java | 2 +- .../io/quarkus/qute/UserTagSectionHelper.java | 74 ++++++++++++++++--- .../java/io/quarkus/qute/ValueResolvers.java | 2 +- .../java/io/quarkus/qute/EscaperTest.java | 2 +- .../test/java/io/quarkus/qute/SimpleTest.java | 6 +- .../java/io/quarkus/qute/UserTagTest.java | 18 +++++ 16 files changed, 270 insertions(+), 64 deletions(-) diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 74df5cc6c888a..1c80491b7feb7 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -962,8 +962,8 @@ NOTE: The evaluated template is parsed and evaluated every time the section is e [[user_tags]] ==== User-defined Tags -User-defined tags can be used to include a template and optionally pass some parameters. -Let's suppose we have a template called `itemDetail.html`: +User-defined tags can be used to include a tag template and optionally pass some parameters. +Let's suppose we have a tag template called `itemDetail.html`: [source] ---- @@ -973,21 +973,21 @@ Let's suppose we have a template called `itemDetail.html`: {/if} ---- <1> `showImage` is a named parameter. -<2> `it` is a special key that is replaced with the first unnamed param of the tag. +<2> `it` is a special key that is replaced with the first unnamed parameter of the tag. <3> (optional) `nested-content` is a special key that will be replaced by the content of the tag. -Now if we register this template under the name `itemDetail.html` and if we add a `UserTagSectionHelper` to the engine: +In Quarkus, all files from the `src/main/resources/templates/tags` are registered and monitored automatically. +For Qute standalone, you need to put the parsed template under the name `itemDetail.html` and register a relevant `UserTagSectionHelper` to the engine: [source,java] ---- Engine engine = Engine.builder() .addSectionHelper(new UserTagSectionHelper.Factory("itemDetail","itemDetail.html")) .build(); +engine.putTemplate("itemDetail.html", engine.parse("...")); ---- -NOTE: In Quarkus, all files from the `src/main/resources/templates/tags` are registered and monitored automatically! - -We can include the tag like this: +Then, we can call the tag like this: [source,html] ---- @@ -1004,6 +1004,11 @@ We can include the tag like this: <1> `item` is resolved to an iteration element and can be referenced using the `it` key in the tag template. <2> Tag content injected using the `nested-content` key in the tag template. +By default, the tag template can reference data from the parent context. +For example, the tag above could use the following expression `{items.size}`. +However, sometimes it might be useful to disable this behavior and execute the tag as an _isolated_ template, i.e. without access to the context of the template that calls the tag. +In this case, just add `_isolated` or `_isolated=true` argument to the call site, e.g. `{#itemDetail item showImage=true _isolated /}`. + === Rendering Output `TemplateInstance` provides several ways to trigger the rendering and consume the result. 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 bf8eceac32829..885942ff73e2a 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 @@ -170,7 +170,7 @@ public ParametersInfo getParameters() { return ParametersInfo.builder() .addParameter(ALIAS, EMPTY) .addParameter(IN, EMPTY) - .addParameter(new Parameter(ITERABLE, null, true)) + .addParameter(Parameter.builder(ITERABLE).optional()) .build(); } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parameter.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parameter.java index 613c6d04add80..43e79c48c84cf 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parameter.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parameter.java @@ -1,14 +1,20 @@ package io.quarkus.qute; import io.quarkus.qute.SectionHelperFactory.ParametersInfo; +import java.util.Objects; +import java.util.function.Predicate; /** - * Definition of a section parameter. + * Definition of a section factory parameter. * * @see ParametersInfo * @see SectionHelperFactory#getParameters() */ -public class Parameter { +public final class Parameter { + + public static Builder builder(String name) { + return new Builder(name); + } public static final String EMPTY = "$empty$"; @@ -18,10 +24,19 @@ public class Parameter { public final boolean optional; + public final Predicate valuePredicate; + + private static final Predicate ALWAYS_TRUE = v -> true; + public Parameter(String name, String defaultValue, boolean optional) { - this.name = name; + this(name, defaultValue, optional, ALWAYS_TRUE); + } + + private Parameter(String name, String defaultValue, boolean optional, Predicate valuePredicate) { + this.name = Objects.requireNonNull(name); this.defaultValue = defaultValue; this.optional = optional; + this.valuePredicate = valuePredicate != null ? valuePredicate : ALWAYS_TRUE; } public String getName() { @@ -32,7 +47,7 @@ public String getDefaultValue() { return defaultValue; } - public boolean hasDefatulValue() { + public boolean hasDefaultValue() { return defaultValue != null; } @@ -40,6 +55,16 @@ public boolean isOptional() { return optional; } + /** + * Allows a factory parameter to refuse a value of an unnamed actual parameter. + * + * @param value + * @return {@code true} if the value is acceptable, {@code false} otherwise + */ + public boolean accepts(String value) { + return valuePredicate.test(value); + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); @@ -48,4 +73,37 @@ public String toString() { return builder.toString(); } + public static class Builder { + + private final String name; + private String defaultValue; + private boolean optional; + private Predicate valuePredicate; + + public Builder(String name) { + this.name = name; + this.optional = false; + } + + public Builder defaultValue(String defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + public Builder optional() { + this.optional = true; + return this; + } + + public Builder valuePredicate(Predicate valuePredicate) { + this.valuePredicate = valuePredicate; + return this; + } + + public Parameter build() { + return new Parameter(name, defaultValue, optional, valuePredicate); + } + + } + } 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 f3a873c62c531..5ecd027b34817 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 @@ -24,6 +24,7 @@ import java.util.concurrent.CompletionStage; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import org.jboss.logging.Logger; @@ -548,28 +549,15 @@ private void processParams(String tag, String label, Iterator iter, Sect } } + Predicate included = params::containsKey; // Then process positional params if (actualSize < factoryParams.size()) { // The number of actual params is less than factory params // We need to choose the best fit for positional params for (String param : paramValues) { - Parameter found = null; - for (Parameter factoryParam : factoryParams) { - // Prefer params with no default value - if (factoryParam.defaultValue == null && !params.containsKey(factoryParam.name)) { - found = factoryParam; - params.put(factoryParam.name, param); - break; - } - } - if (found == null) { - for (Parameter factoryParam : factoryParams) { - if (!params.containsKey(factoryParam.name)) { - found = factoryParam; - params.put(factoryParam.name, param); - break; - } - } + Parameter found = findFactoryParameter(param, factoryParams, included, true); + if (found != null) { + params.put(found.name, param); } } } else { @@ -577,15 +565,10 @@ private void processParams(String tag, String label, Iterator iter, Sect int generatedIdx = 0; for (String param : paramValues) { // Positional param - Parameter found = null; - for (Parameter factoryParam : factoryParams) { - if (!params.containsKey(factoryParam.name)) { - found = factoryParam; - params.put(factoryParam.name, param); - break; - } - } - if (found == null) { + Parameter found = findFactoryParameter(param, factoryParams, included, false); + if (found != null) { + params.put(found.name, param); + } else { params.put("" + generatedIdx++, param); } } @@ -593,7 +576,7 @@ private void processParams(String tag, String label, Iterator iter, Sect // Use the default values if needed factoryParams.stream() - .filter(Parameter::hasDefatulValue) + .filter(Parameter::hasDefaultValue) .forEach(p -> params.putIfAbsent(p.name, p.defaultValue)); // Find undeclared mandatory params @@ -609,6 +592,24 @@ private void processParams(String tag, String label, Iterator iter, Sect params.forEach(block::addParameter); } + private Parameter findFactoryParameter(String paramValue, List factoryParams, Predicate included, + boolean noDefaultValueTakesPrecedence) { + if (noDefaultValueTakesPrecedence) { + for (Parameter param : factoryParams) { + // Params with no default value take precedence + if (param.accepts(paramValue) && !param.hasDefaultValue() && !included.test(param.name)) { + return param; + } + } + } + for (Parameter param : factoryParams) { + if (param.accepts(paramValue) && !included.test(param.name)) { + return param; + } + } + return null; + } + /** * * @param part diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java index 55986531e0e23..b9eb6e794ecaf 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java @@ -2,20 +2,21 @@ import java.util.Map; import java.util.concurrent.CompletionStage; +import java.util.function.Function; class ResolutionContextImpl implements ResolutionContext { private final Object data; private final Evaluator evaluator; private final Map extendingBlocks; - private final TemplateInstance templateInstance; + private final Function attributeFun; ResolutionContextImpl(Object data, - Evaluator evaluator, Map extendingBlocks, TemplateInstance templateInstance) { + Evaluator evaluator, Map extendingBlocks, Function attributeFun) { this.data = data; this.evaluator = evaluator; this.extendingBlocks = extendingBlocks; - this.templateInstance = templateInstance; + this.attributeFun = attributeFun; } @Override @@ -53,7 +54,7 @@ public SectionBlock getExtendingBlock(String name) { @Override public Object getAttribute(String key) { - return templateInstance.getAttribute(key); + return attributeFun.apply(key); } @Override 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 299276be827b4..1ccd64819c958 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 @@ -159,7 +159,7 @@ public String asMessage() { if (name != null) { Object base = getBase().orElse(null); List params = getParams(); - boolean isDataMap = (base instanceof Map) && ((Map) base).containsKey(TemplateInstanceBase.DATA_MAP_KEY); + 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" @@ -190,6 +190,15 @@ public String asMessage() { } } + private boolean isDataMap(Object base) { + if (base instanceof Map) { + return ((Map) base).containsKey(TemplateInstanceBase.DATA_MAP_KEY); + } else if (base instanceof Mapper) { + return ((Mapper) base).get(TemplateInstanceBase.DATA_MAP_KEY) != null; + } + return false; + } + @Override public String toString() { return "NOT_FOUND"; diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelper.java index 9d2002688ca09..110dfba844144 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelper.java @@ -1,5 +1,6 @@ package io.quarkus.qute; +import java.util.Map; import java.util.concurrent.CompletionStage; /** @@ -28,6 +29,14 @@ public interface SectionResolutionContext { */ ResolutionContext resolutionContext(); + /** + * + * @param data + * @param extendingBlocks + * @return a new resolution context + */ + ResolutionContext newResolutionContext(Object data, Map extendingBlocks); + /** * Execute the main block with the current resolution context. * diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java index 9b8e3c8327468..e596cbc29d813 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java @@ -26,9 +26,34 @@ default List getDefaultAliases() { } /** + * A factory may define {@code factory parameters} for the start tag of any section block. A factory {@link Parameter} has a + * name and optional default value. The default value is automatically assigned if no other value is set by a parser. A + * parameter may be optional. A non-optional parameter that has no value assigned results in a parser error. + *

+ * A section block in a template defines the {@code actual parameters}: * - * @return the info about the expected parameters + *

+     * {! The value is "item.isActive". The name is not defined. !}
+     * {#if item.isActive}{/}
+     * 
+     * {! The name is "age" and the value is "10". !}
+     * {#let age=10}{/}
+     * 
+ * + * The actual parameters are parsed taking the factory parameters into account: + *
    + *
  1. Named actual params are processed first and the relevant values are assigned, e.g. the param with name {@code age} + * has the + * value {@code 10},
  2. + *
  3. Then, if the number of actual params is greater or equals to the number of factory params the values are set + * according to position of factory params,
  4. + *
  5. Otherwise, the values are set according to position but params with no default value take precedence.
  6. + *
  7. Finally, all unset parameters that define a default value are initialized with the default value.
  8. + *
+ * + * @return the factory parameters * @see #cacheFactoryConfig() + * @see BlockInfo#getParameters() */ default ParametersInfo getParameters() { return ParametersInfo.EMPTY; @@ -93,6 +118,11 @@ interface BlockInfo extends ParserDelegate { String getLabel(); + /** + * Undeclared params with default values are included. + * + * @return the map of parameters + */ Map getParameters(); default String getParameter(String name) { @@ -196,6 +226,10 @@ default SectionBlock getBlock(String label) { } + /** + * + * @see Parameter + */ public static final class ParametersInfo implements Iterable> { public static Builder builder() { @@ -236,11 +270,15 @@ public static class Builder { } public Builder addParameter(String name) { - return addParameter(SectionHelperFactory.MAIN_BLOCK_NAME, name, null); + return addParameter(Parameter.builder(name)); } public Builder addParameter(String name, String defaultValue) { - return addParameter(SectionHelperFactory.MAIN_BLOCK_NAME, name, defaultValue); + return addParameter(Parameter.builder(name).defaultValue(defaultValue)); + } + + public Builder addParameter(Parameter.Builder param) { + return addParameter(param.build()); } public Builder addParameter(Parameter param) { @@ -248,7 +286,11 @@ public Builder addParameter(Parameter param) { } public Builder addParameter(String blockLabel, String name, String defaultValue) { - return addParameter(blockLabel, new Parameter(name, defaultValue, false)); + return addParameter(blockLabel, Parameter.builder(name).defaultValue(defaultValue)); + } + + public Builder addParameter(String blockLabel, Parameter.Builder parameter) { + return addParameter(blockLabel, parameter.build()); } public Builder addParameter(String blockLabel, Parameter parameter) { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java index acd713f3d5c26..b16347c3ed67c 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java @@ -3,6 +3,7 @@ import io.quarkus.qute.SectionHelper.SectionResolutionContext; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.CompletionStage; import java.util.function.Function; @@ -160,6 +161,12 @@ public ResolutionContext resolutionContext() { return resolutionContext; } + @Override + public ResolutionContext newResolutionContext(Object data, Map extendingBlocks) { + return new ResolutionContextImpl(data, resolutionContext.getEvaluator(), extendingBlocks, + resolutionContext::getAttribute); + } + } } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java index 7777eec5ec438..c205f9b1918b0 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java @@ -98,7 +98,7 @@ public CompletionStage consume(Consumer resultConsumer) { private CompletionStage renderData(Object data, Consumer consumer) { CompletableFuture result = new CompletableFuture<>(); ResolutionContext rootContext = new ResolutionContextImpl(data, - engine.getEvaluator(), null, this); + engine.getEvaluator(), null, this::getAttribute); setAttribute(DataNamespaceResolver.ROOT_CONTEXT, rootContext); // Async resolution root.resolve(rootContext).whenComplete((r, t) -> { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstanceBase.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstanceBase.java index ac14e76ce5cea..7b1df5e402c42 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstanceBase.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstanceBase.java @@ -51,7 +51,7 @@ protected Object data() { return data; } if (dataMap != null) { - return dataMap; + return Mapper.wrap(dataMap); } return EMPTY_DATA_MAP; } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java index 7e6b4cfb36012..d37f11687ac81 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java @@ -12,17 +12,19 @@ public class UserTagSectionHelper implements SectionHelper { - private static final String IT = "it"; private static final String NESTED_CONTENT = "nested-content"; private final Supplier