Merge pull request #7334 from mkouba/issue-7325

Qute - add some javadoc to public API
This commit is contained in:
Guillaume Smet
2020-02-21 17:35:21 +01:00
committed by GitHub
22 changed files with 258 additions and 62 deletions

View File

@@ -5,30 +5,44 @@ import java.util.Map;
import java.util.function.Predicate;
/**
* Template engine configuration.
* Represents a central point for template management. It has a dedicated configuration and is able to cache the
* template definitions.
*/
public interface Engine {
/**
*
* @return a new builder instance
*/
static EngineBuilder builder() {
return new EngineBuilder();
}
/**
* Parse the template contents.
* <p>
* Note that this method always returns a new {@link Template} instance.
*
* @param content
* @return the template
* @see Engine#getTemplate(String)
*/
default Template parse(String content) {
return parse(content, null);
}
/**
* Parse the template contents with the specified variant.
* <p>
* Note that this method always returns a new {@link Template} instance.
*
* @param content
* @param variant
* @return the template
* @see Engine#getTemplate(String)
*/
public Template parse(String content, Variant variant);
public SectionHelperFactory<?> getSectionHelperFactory(String name);
public Map<String, SectionHelperFactory<?>> getSectionHelperFactories();
public List<ValueResolver> getValueResolvers();
public List<NamespaceResolver> getNamespaceResolvers();
public Evaluator getEvaluator();
/**
*
* @return an immutable list of result mappers
@@ -44,7 +58,7 @@ public interface Engine {
public Template putTemplate(String id, Template template);
/**
* Obtain a compiled template for the given id. The template could be registered using
* Obtain a template for the given identifier. The template could be registered using
* {@link #putTemplate(String, Template)} or loaded by a template locator.
*
* @param id
@@ -65,4 +79,14 @@ public interface Engine {
*/
public void removeTemplates(Predicate<String> test);
public SectionHelperFactory<?> getSectionHelperFactory(String name);
public Map<String, SectionHelperFactory<?>> getSectionHelperFactories();
public List<ValueResolver> getValueResolvers();
public List<NamespaceResolver> getNamespaceResolvers();
public Evaluator getEvaluator();
}

View File

@@ -93,6 +93,12 @@ public final class EngineBuilder {
}
public EngineBuilder addNamespaceResolver(NamespaceResolver resolver) {
for (NamespaceResolver namespaceResolver : namespaceResolvers) {
if (namespaceResolver.getNamespace().equals(resolver.getNamespace())) {
throw new IllegalArgumentException(
String.format("Namespace %s is already handled by %s", resolver.getNamespace()));
}
}
this.namespaceResolvers.add(resolver);
return this;
}

View File

@@ -4,7 +4,9 @@ import java.util.List;
import java.util.concurrent.CompletionStage;
/**
* Each part of an {@link Expression} is evaluated with a new context.
* Evaluation context of a specific part of an {@link Expression}.
*
* @see Expression#parts
*/
public interface EvalContext {
@@ -29,8 +31,20 @@ public interface EvalContext {
*/
List<String> getParams();
/**
* Parse and evaluate the given expression using the relevant {@link ResolutionContext}
*
* @param expression
* @return the result
*/
CompletionStage<Object> evaluate(String expression);
/**
* Evaluate the given expression using the relevant {@link ResolutionContext}.
*
* @param expression
* @return the result
*/
CompletionStage<Object> evaluate(Expression expression);
}

View File

@@ -66,14 +66,21 @@ class EvaluatorImpl implements Evaluator {
private CompletionStage<Object> resolveReference(boolean tryParent, Object ref, Iterator<String> parts,
ResolutionContext resolutionContext) {
return resolve(new EvalContextImpl(tryParent, ref, parts.next(), resolutionContext), resolvers.iterator())
.thenCompose(r -> {
if (parts.hasNext()) {
return resolveReference(tryParent, r, parts, resolutionContext);
} else {
return CompletableFuture.completedFuture(r);
}
});
String part = parts.next();
EvalContextImpl evalContext = new EvalContextImpl(tryParent, ref, part, resolutionContext);
if (!parts.hasNext()) {
// The last part - no need to compose
return resolve(evalContext, resolvers.iterator());
} else {
return resolve(evalContext, resolvers.iterator())
.thenCompose(r -> {
if (parts.hasNext()) {
return resolveReference(tryParent, r, parts, resolutionContext);
} else {
return CompletableFuture.completedFuture(r);
}
});
}
}
private CompletionStage<Object> resolve(EvalContextImpl evalContext, Iterator<ValueResolver> resolvers) {

View File

@@ -12,6 +12,12 @@ import java.util.concurrent.ExecutionException;
/**
* Represents a value expression. It could be a literal such as {@code 'foo'}. It could have a namespace such as {@code data}
* for {@code data:name}. It could have several parts such as {@code item} and {@code name} for {@code item.name}.
* <p>
* An expression may have a "type check information" attached. The string has a form {@code [TYPE_INFO]<SECTION_HINT>.foo.baz}
* where TYPE_INFO represent the fully qualified type name (including type parameters) and SECTION_HINT represents an optional
* hint set by the corresponding section helper. For example the expression {@code foo.name} may have the following type check
* info: {@code [org.acme.Foo].name} and the expression {@code it.name} may have
* {@code [java.util.List<org.acme.Label>]<for-element>.name}.
*
* @see Evaluator
*/

View File

@@ -8,9 +8,10 @@ class LiteralSupport {
private static final Logger LOGGER = Logger.getLogger(LiteralSupport.class);
static final Pattern INTEGER_LITERAL_PATTERN = Pattern.compile("(\\+|-)?\\d{1,10}");
static final Pattern LONG_LITERAL_PATTERN = Pattern.compile("(\\+|-)?\\d{1,19}(L|l)");
static final Pattern INTEGER_LITERAL_PATTERN = Pattern.compile("[-+]?\\d{1,10}");
static final Pattern LONG_LITERAL_PATTERN = Pattern.compile("[-+]?\\d{1,19}(L|l)");
static final Pattern DOUBLE_LITERAL_PATTERN = Pattern.compile("[-+]?[0-9]*\\.?[0-9]+(d|D)");
static final Pattern FLOAT_LITERAL_PATTERN = Pattern.compile("[-+]?[0-9]*\\.?[0-9]+(f|F)");
static Object getLiteral(String value) {
if (value == null || value.isEmpty()) {
@@ -25,18 +26,37 @@ class LiteralSupport {
literal = Boolean.FALSE;
} else if (value.equals("null")) {
literal = null;
} else if (INTEGER_LITERAL_PATTERN.matcher(value).matches()) {
try {
literal = Integer.parseInt(value);
} catch (NumberFormatException e) {
LOGGER.warn("Unable to parse integer literal: " + value, e);
}
} else if (LONG_LITERAL_PATTERN.matcher(value).matches()) {
try {
literal = Long
.parseLong(value.substring(0, value.length() - 1));
} catch (NumberFormatException e) {
LOGGER.warn("Unable to parse long literal: " + value, e);
} else {
char firstChar = value.charAt(0);
if (Character.isDigit(firstChar) || firstChar == '-' || firstChar == '+') {
if (INTEGER_LITERAL_PATTERN.matcher(value).matches()) {
try {
literal = Integer.parseInt(value);
} catch (NumberFormatException e) {
LOGGER.warn("Unable to parse integer literal: " + value, e);
}
} else if (LONG_LITERAL_PATTERN.matcher(value).matches()) {
try {
literal = Long
.parseLong(value.substring(0, value.length() - 1));
} catch (NumberFormatException e) {
LOGGER.warn("Unable to parse long literal: " + value, e);
}
} else if (DOUBLE_LITERAL_PATTERN.matcher(value).matches()) {
try {
literal = Double
.parseDouble(value.substring(0, value.length() - 1));
} catch (NumberFormatException e) {
LOGGER.warn("Unable to parse double literal: " + value, e);
}
} else if (FLOAT_LITERAL_PATTERN.matcher(value).matches()) {
try {
literal = Float
.parseFloat(value.substring(0, value.length() - 1));
} catch (NumberFormatException e) {
LOGGER.warn("Unable to parse float literal: " + value, e);
}
}
}
}
return literal;

View File

@@ -1,5 +1,11 @@
package io.quarkus.qute;
/**
* Maps keys to values in a similar way to {@link java.util.Map}. The difference is that it could be stateless, ie. the lookup
* may be performed dynamically.
*
* @see ValueResolvers#mapperResolver()
*/
public interface Mapper {
Object get(String key);

View File

@@ -4,6 +4,9 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
/**
* A result node backed by an array of result nodes.
*/
public class MultiResultNode implements ResultNode {
private final ResultNode[] results;

View File

@@ -5,11 +5,20 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;
/*
* Namespace resolver.
/**
* Namespace resolvers are used to find the current context object for an expression that starts with a namespace declaration.
* <p>
* For example the expression {@code data:colors} declares a namespace {@code data}.
*
* @see EngineBuilder#addNamespaceResolver(NamespaceResolver)
*/
public interface NamespaceResolver extends Resolver {
/**
*
* @param namespace
* @return a new builder instance
*/
static Builder builder(String namespace) {
return new Builder(namespace);
}
@@ -21,10 +30,7 @@ public interface NamespaceResolver extends Resolver {
*/
String getNamespace();
/**
*
*/
class Builder {
final class Builder {
private final String namespace;
private Function<EvalContext, CompletionStage<Object>> resolve;

View File

@@ -45,6 +45,8 @@ class Parser implements Function<String, Expression> {
static final char START_COMPOSITE_PARAM = '(';
static final char END_COMPOSITE_PARAM = ')';
static final String TYPE_CHECK_NAMESPACE = "[" + Expressions.TYPECHECK_NAMESPACE_PLACEHOLDER + "].";
private StringBuilder buffer;
private State state;
private int line;
@@ -590,9 +592,7 @@ class Parser implements Function<String, Expression> {
}
if (literal == Result.NOT_FOUND) {
if (namespace != null) {
// TODO use constants!
typeCheckInfo = "[" + Expressions.TYPECHECK_NAMESPACE_PLACEHOLDER + "]";
typeCheckInfo += "." + parts.stream().collect(Collectors.joining("."));
typeCheckInfo = TYPE_CHECK_NAMESPACE + parts.stream().collect(Collectors.joining("."));
} else if (typeInfos.containsKey(parts.get(0))) {
typeCheckInfo = typeInfos.get(parts.get(0));
if (typeCheckInfo != null) {

View File

@@ -10,20 +10,23 @@ import java.util.concurrent.CompletionStage;
public interface ResolutionContext {
/**
* Parse and evaluate the expression.
*
* @param expression
* @return the result of the evaluated expression
* @return the result
*/
CompletionStage<Object> evaluate(String expression);
/**
* Evaluate the expression.
*
* @param expression
* @return the result of the evaluated expression
* @return the result
*/
CompletionStage<Object> evaluate(Expression expression);
/**
* Create a child resolution context with the specified data and namespace resolvers.
*
* @param data
* @param namespaceResolversFactories
@@ -32,6 +35,7 @@ public interface ResolutionContext {
ResolutionContext createChild(Object data, List<NamespaceResolver> namespaceResolvers);
/**
* Create a child resolution context with the specifiec extending blocks.
*
* @param extendingBlocks
* @return a new child resolution context
@@ -59,7 +63,7 @@ public interface ResolutionContext {
/**
*
* @param name
* @return the extending block or null
* @return the extending block for the specified name or null
*/
SectionBlock getExtendingBlock(String name);

View File

@@ -1,14 +1,21 @@
package io.quarkus.qute;
import io.quarkus.qute.Results.Result;
import java.util.concurrent.CompletionStage;
/**
*
* @see ValueResolver
* @see NamespaceResolver
*/
public interface Resolver {
/**
* This method should return {@link Result#NOT_FOUND} if it's not possible to resolve the context. Any other value is
* considered a valid result, including {@code null}.
*
* @param context
* @return the result
* @see Results#NOT_FOUND
*/
CompletionStage<Object> resolve(EvalContext context);

View File

@@ -3,10 +3,16 @@ package io.quarkus.qute;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
public class Results {
/**
* Result constants.
*/
public final class Results {
public static final CompletionStage<Object> NOT_FOUND = CompletableFuture.completedFuture(Result.NOT_FOUND);
private Results() {
}
public enum Result {
NOT_FOUND,

View File

@@ -4,6 +4,8 @@ import java.util.concurrent.CompletionStage;
/**
* Defines the logic of a section node.
*
* @see SectionHelperFactory
*/
@FunctionalInterface
public interface SectionHelper {

View File

@@ -9,6 +9,8 @@ import java.util.Map;
/**
* Factory to create a new {@link SectionHelper} based on the {@link SectionInitContextImpl}.
*
* @see EngineBuilder#addSectionHelper(SectionHelperFactory)
*/
public interface SectionHelperFactory<T extends SectionHelper> {
@@ -16,7 +18,7 @@ public interface SectionHelperFactory<T extends SectionHelper> {
/**
*
* @return the list of default aliases
* @return the list of default aliases used to match the helper
*/
default List<String> getDefaultAliases() {
return Collections.emptyList();
@@ -32,8 +34,6 @@ public interface SectionHelperFactory<T extends SectionHelper> {
/**
* A nested section tag that matches a name of a block will be added as a block to the current section.
* <p>
*
*
* @return the list of block labels
*/
@@ -41,6 +41,13 @@ public interface SectionHelperFactory<T extends SectionHelper> {
return Collections.emptyList();
}
/**
* By default, all unknown nested sections are ignored, ie. sections with labels not present in the
* {@link #getBlockLabels()}. However, sometimes it might be useful to treat such sections as blocks. See
* {@link IncludeSectionHelper} for an example.
*
* @return true if unknown sections should not be ignored
*/
default boolean treatUnknownSectionsAsBlocks() {
return false;
}

View File

@@ -4,7 +4,7 @@ import java.util.List;
import java.util.function.Consumer;
/**
* A result node backed by an object value.
* A result node backed by a single object value.
*/
public class SingleResultNode implements ResultNode {

View File

@@ -4,11 +4,21 @@ import java.util.Optional;
import java.util.Set;
/**
* Represents a template definition.
* Represents an immutable template definition.
* <p>
* The workflow is as follows:
* <ol>
* <li>Create a new template instance via {@link #instance()} or any convenient method</li>
* <li>Set the model data</li>
* <li>Trigger rendering with {@link TemplateInstance#render()}, {@link TemplateInstance#renderAsync()},
* {@link TemplateInstance#consume(java.util.function.Consumer)} or subscribe to a publisher returned from
* {@link TemplateInstance#publisher()}</li>
* </ol>
*/
public interface Template {
/**
* Template instance represents a rendering configuration.
*
* @return a new template instance
*/

View File

@@ -9,10 +9,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
* A value resolver is automatically generated for a target type.
* <p>
* Note that non-public members, constructors, static initializers, static, synthetic and void methods are always ignored.
* </p>
* This annotation is used to mark a target type for which a value resolver should be automatically generated. Note that
* non-public members, constructors, static initializers, static, synthetic and void methods are always ignored.
*
* @see ValueResolver
*/
@@ -27,7 +25,7 @@ public @interface TemplateData {
Class<?> target() default TemplateData.class;
/**
* The regular expressions that are used to match the members that should be ignored
* The regular expressions that are used to match the members that should be ignored.
*/
String[] ignore() default {};

View File

@@ -4,13 +4,14 @@ import java.io.Reader;
import java.util.Optional;
/**
* Locates template sources.
* Locates template sources. The locator with higher priority takes precedence.
*
* @see Engine#getTemplate(String)
*/
public interface TemplateLocator extends WithPriority {
/**
* Must return {@link Optional#empty()} if it's not possible to locate a template with the specified id.
*
* @param id
* @return the template location for the given id

View File

@@ -1,7 +1,14 @@
package io.quarkus.qute;
import io.quarkus.qute.Results.Result;
/**
* Value resolver.
* Value resolvers are used when evaluating expressions.
* <p>
* First the resolvers that apply to the given {@link EvalContext} are filtered. Then the resolver with highest priority is used
* to resolve the data. If {@link Result#NOT_FOUND} is returned the next available resolver is tried.
*
* @see EvalContext
*/
public interface ValueResolver extends Resolver, WithPriority {

View File

@@ -0,0 +1,22 @@
package io.quarkus.qute;
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.Test;
public class EngineBuilderTest {
@Test
public void testDuplicateNamespace() {
try {
Engine.builder().addNamespaceResolver(NamespaceResolver.builder("foo").resolve(e -> {
return null;
}).build()).addNamespaceResolver(NamespaceResolver.builder("foo").resolve(e -> {
return null;
}).build());
fail();
} catch (IllegalArgumentException expected) {
}
}
}

View File

@@ -0,0 +1,40 @@
package io.quarkus.qute;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import org.junit.jupiter.api.Test;
public class LiteralSupportTest {
@Test
public void testNumbers() {
assertEquals(Integer.valueOf(43), LiteralSupport.getLiteral("43"));
assertEquals(Long.valueOf(1000), LiteralSupport.getLiteral("1000l"));
assertEquals(Long.valueOf(-10), LiteralSupport.getLiteral("-10L"));
assertEquals(Double.valueOf(1.0d), LiteralSupport.getLiteral("1d"));
assertEquals(Double.valueOf(2.12d), LiteralSupport.getLiteral("+2.12d"));
assertEquals(Double.valueOf(-2.12d), LiteralSupport.getLiteral("-2.12d"));
assertEquals(Double.valueOf(123.4d), LiteralSupport.getLiteral("123.4D"));
assertEquals(Float.valueOf(2.12f), LiteralSupport.getLiteral("2.12f"));
assertEquals(Float.valueOf(2.12f), LiteralSupport.getLiteral("2.12F"));
}
@Test
public void testBooleans() {
assertEquals(Boolean.TRUE, LiteralSupport.getLiteral("true"));
assertEquals(Boolean.FALSE, LiteralSupport.getLiteral("false"));
}
@Test
public void testNull() {
assertNull(LiteralSupport.getLiteral("null"));
}
@Test
public void testStrings() {
assertEquals("foo", LiteralSupport.getLiteral("'foo'"));
assertEquals("foo", LiteralSupport.getLiteral("\"foo\""));
}
}