From 4b033a5f44e3bd618bc73427564016f17fb9be05 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 --- .../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 +++++ 15 files changed, 258 insertions(+), 57 deletions(-) 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