Skip to content

Commit

Permalink
Merge pull request #12428 from mkouba/issue-7376
Browse files Browse the repository at this point in the history
Qute type-safe templates: generate optimized value resolvers
  • Loading branch information
gastaldi authored Sep 30, 2020
2 parents d150e3b + 8c289c9 commit 23c970a
Show file tree
Hide file tree
Showing 5 changed files with 305 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -351,8 +351,8 @@ public String apply(String id) {

if (!expressions.isEmpty()) {

// Map implicit class -> true if methods were used
Map<ClassInfo, Boolean> implicitClassToMethodUsed = new HashMap<>();
// Map implicit class -> set of used members
Map<ClassInfo, Set<String>> implicitClassToMembersUsed = new HashMap<>();

for (Expression expression : expressions) {
// msg:hello_world(foo.name)
Expand Down Expand Up @@ -402,7 +402,7 @@ public String apply(String id) {
Map<String, Match> results = new HashMap<>();
QuteProcessor.validateNestedExpressions(defaultBundleInterface, results,
templateExtensionMethods, excludes,
incorrectExpressions, expression, index, implicitClassToMethodUsed,
incorrectExpressions, expression, index, implicitClassToMembersUsed,
templateIdToPathFun);
Match match = results.get(param.toOriginalString());
if (match != null && !Types.isAssignableFrom(match.type,
Expand All @@ -420,11 +420,9 @@ public String apply(String id) {
}
}

for (Entry<ClassInfo, Boolean> implicit : implicitClassToMethodUsed.entrySet()) {
implicitClasses.produce(implicit.getValue()
? new ImplicitValueResolverBuildItem(implicit.getKey(),
new TemplateDataBuilder().properties(false).build())
: new ImplicitValueResolverBuildItem(implicit.getKey()));
for (Entry<ClassInfo, Set<String>> e : implicitClassToMembersUsed.entrySet()) {
implicitClasses.produce(new ImplicitValueResolverBuildItem(e.getKey(),
new TemplateDataBuilder().addIgnore(QuteProcessor.buildIgnorePattern(e.getValue())).build()));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.util.concurrent.CompletionStage;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -405,39 +406,52 @@ public String apply(String id) {
}
};

// Map implicit class -> true if methods were used
Map<ClassInfo, Boolean> implicitClassToMethodUsed = new HashMap<>();
// Map implicit class -> set of used members
Map<ClassInfo, Set<String>> implicitClassToMembersUsed = new HashMap<>();

for (TemplateAnalysis analysis : templatesAnalysis.getAnalysis()) {
for (Expression expression : analysis.expressions) {
if (expression.hasNamespace() || expression.isLiteral()) {
continue;
}
validateNestedExpressions(null, new HashMap<>(), templateExtensionMethods, excludes, incorrectExpressions,
expression, index, implicitClassToMethodUsed, templateIdToPathFun);
expression, index, implicitClassToMembersUsed, templateIdToPathFun);
}
}

for (Entry<ClassInfo, Boolean> implicit : implicitClassToMethodUsed.entrySet()) {
implicitClasses.produce(implicit.getValue()
? new ImplicitValueResolverBuildItem(implicit.getKey(), new TemplateDataBuilder().properties(false).build())
: new ImplicitValueResolverBuildItem(implicit.getKey()));
for (Entry<ClassInfo, Set<String>> entry : implicitClassToMembersUsed.entrySet()) {
implicitClasses.produce(new ImplicitValueResolverBuildItem(entry.getKey(),
new TemplateDataBuilder().addIgnore(buildIgnorePattern(entry.getValue())).build()));
}
}

static String buildIgnorePattern(Iterable<String> names) {
// ^(?!\\Qbar\\P|\\Qfoo\\P).*$
StringBuilder ignorePattern = new StringBuilder("^(?!");
for (Iterator<String> iterator = names.iterator(); iterator.hasNext();) {
String memberName = iterator.next();
ignorePattern.append(Pattern.quote(memberName));
if (iterator.hasNext()) {
ignorePattern.append("|");
}
}
ignorePattern.append(").*$");
return ignorePattern.toString();
}

static void validateNestedExpressions(ClassInfo rootClazz, Map<String, Match> results,
List<TemplateExtensionMethodBuildItem> templateExtensionMethods,
List<TypeCheckExcludeBuildItem> excludes,
BuildProducer<IncorrectExpressionBuildItem> incorrectExpressions, Expression expression, IndexView index,
Map<ClassInfo, Boolean> implicitClassToMethodUsed, Function<String, String> templateIdToPathFun) {
Map<ClassInfo, Set<String>> implicitClassToMembersUsed, Function<String, String> templateIdToPathFun) {

// First validate nested virtual methods
for (Expression.Part part : expression.getParts()) {
if (part.isVirtualMethod()) {
for (Expression param : part.asVirtualMethod().getParameters()) {
if (!results.containsKey(param.toOriginalString())) {
validateNestedExpressions(null, results, templateExtensionMethods, excludes, incorrectExpressions,
param, index, implicitClassToMethodUsed, templateIdToPathFun);
param, index, implicitClassToMembersUsed, templateIdToPathFun);
}
}
}
Expand Down Expand Up @@ -476,17 +490,20 @@ static void validateNestedExpressions(ClassInfo rootClazz, Map<String, Match> re
Info info = parts.next();
if (match.clazz != null) {
// By default, we only consider properties
implicitClassToMethodUsed.putIfAbsent(match.clazz, false);
Set<String> membersUsed = implicitClassToMembersUsed.computeIfAbsent(match.clazz, c -> new HashSet<>());
AnnotationTarget member = null;
// First try to find java members
if (info.isVirtualMethod()) {
member = findMethod(info.part.asVirtualMethod(), match.clazz, expression, index, templateIdToPathFun,
results);
if (member != null) {
implicitClassToMethodUsed.put(match.clazz, true);
membersUsed.add(member.asMethod().name());
}
} else if (info.isProperty()) {
member = findProperty(info.asProperty().name, match.clazz, index);
if (member != null) {
membersUsed.add(member.kind() == Kind.FIELD ? member.asField().name() : member.asMethod().name());
}
}
// Java member not found - try extension methods
if (member == null) {
Expand Down Expand Up @@ -654,8 +671,8 @@ public String apply(String id) {
Map<String, BeanInfo> namedBeans = registrationPhase.getContext().beans().withName()
.collect(toMap(BeanInfo::getName, Function.identity()));

// Map implicit class -> true if methods were used
Map<ClassInfo, Boolean> implicitClassToMethodUsed = new HashMap<>();
// Map implicit class -> set of used members
Map<ClassInfo, Set<String>> implicitClassToMembersUsed = new HashMap<>();

for (Expression expression : injectExpressions) {
Expression.Part firstPart = expression.getParts().get(0);
Expand All @@ -674,7 +691,7 @@ public String apply(String id) {
continue;
}
validateNestedExpressions(bean.getImplClazz(), new HashMap<>(), templateExtensionMethods, excludes,
incorrectExpressions, expression, index, implicitClassToMethodUsed, templateIdToPathFun);
incorrectExpressions, expression, index, implicitClassToMembersUsed, templateIdToPathFun);

} else {
// User is injecting a non-existing bean
Expand All @@ -684,11 +701,9 @@ public String apply(String id) {
}
}

for (Entry<ClassInfo, Boolean> implicit : implicitClassToMethodUsed.entrySet()) {
implicitClasses.produce(implicit.getValue()
? new ImplicitValueResolverBuildItem(implicit.getKey(),
new TemplateDataBuilder().properties(false).build())
: new ImplicitValueResolverBuildItem(implicit.getKey()));
for (Entry<ClassInfo, Set<String>> entry : implicitClassToMembersUsed.entrySet()) {
implicitClasses.produce(new ImplicitValueResolverBuildItem(entry.getKey(),
new TemplateDataBuilder().addIgnore(buildIgnorePattern(entry.getValue())).build()));
}
}
}
Expand Down Expand Up @@ -734,47 +749,35 @@ public boolean test(String name) {
}
});

Map<DotName, ClassInfo> nameToClass = new HashMap<>();
ValueResolverGenerator.Builder builder = ValueResolverGenerator.builder().setIndex(index).setClassOutput(classOutput);
Set<DotName> controlled = new HashSet<>();
Map<DotName, AnnotationInstance> uncontrolled = new HashMap<>();
for (AnnotationInstance templateData : index.getAnnotations(ValueResolverGenerator.TEMPLATE_DATA)) {
processsTemplateData(index, templateData, templateData.target(), controlled, uncontrolled, nameToClass);
processsTemplateData(index, templateData, templateData.target(), controlled, uncontrolled, builder);
}
for (AnnotationInstance containerInstance : index.getAnnotations(ValueResolverGenerator.TEMPLATE_DATA_CONTAINER)) {
for (AnnotationInstance templateData : containerInstance.value().asNestedArray()) {
processsTemplateData(index, templateData, containerInstance.target(), controlled, uncontrolled, nameToClass);
processsTemplateData(index, templateData, containerInstance.target(), controlled, uncontrolled, builder);
}
}

for (ImplicitValueResolverBuildItem implicit : implicitClasses) {
if (controlled.contains(implicit.getClazz().name())) {
LOGGER.debugf("Implicit value resolver build item ignored: %s is annotated with @TemplateData");
DotName implicitClassName = implicit.getClazz().name();
if (controlled.contains(implicitClassName)) {
LOGGER.debugf("Implicit value resolver for %s ignored: class is annotated with @TemplateData",
implicitClassName);
continue;
}
AnnotationInstance templateData = uncontrolled.get(implicit.getClazz().name());
if (templateData != null) {
if (!templateData.equals(implicit.getTemplateData())) {
throw new IllegalStateException("Multiple implicit value resolver build items produced for "
+ implicit.getClazz() + " and the synthetic template data is not equal");
}
if (uncontrolled.containsKey(implicitClassName)) {
LOGGER.debugf("Implicit value resolver for %d ignored: %s declared on %s", uncontrolled.get(implicitClassName),
uncontrolled.get(implicitClassName).target());
continue;
}
uncontrolled.put(implicit.getClazz().name(), implicit.getTemplateData());
nameToClass.put(implicit.getClazz().name(), implicit.getClazz());
builder.addClass(implicit.getClazz(), implicit.getTemplateData());
}

ValueResolverGenerator generator = ValueResolverGenerator.builder().setIndex(index).setClassOutput(classOutput)
.setUncontrolled(uncontrolled)
.build();

// @TemplateData
for (DotName name : controlled) {
generator.generate(nameToClass.get(name));
}
// Uncontrolled classes
for (DotName name : uncontrolled.keySet()) {
generator.generate(nameToClass.get(name));
}
ValueResolverGenerator generator = builder.build();
generator.generate();

Set<String> generatedTypes = new HashSet<>();
generatedTypes.addAll(generator.getGeneratedTypes());
Expand Down Expand Up @@ -1282,18 +1285,18 @@ private static AnnotationTarget findMethod(VirtualMethodPart virtualMethod, Clas
}

private void processsTemplateData(IndexView index, AnnotationInstance templateData, AnnotationTarget annotationTarget,
Set<DotName> controlled, Map<DotName, AnnotationInstance> uncontrolled, Map<DotName, ClassInfo> nameToClass) {
Set<DotName> controlled, Map<DotName, AnnotationInstance> uncontrolled, ValueResolverGenerator.Builder builder) {
AnnotationValue targetValue = templateData.value("target");
if (targetValue == null || targetValue.asClass().name().equals(ValueResolverGenerator.TEMPLATE_DATA)) {
ClassInfo annotationTargetClass = annotationTarget.asClass();
controlled.add(annotationTargetClass.name());
nameToClass.put(annotationTargetClass.name(), annotationTargetClass);
builder.addClass(annotationTargetClass, templateData);
} else {
ClassInfo uncontrolledClass = index.getClassByName(targetValue.asClass().name());
if (uncontrolledClass != null) {
uncontrolled.compute(uncontrolledClass.name(), (c, v) -> {
if (v == null) {
nameToClass.put(uncontrolledClass.name(), uncontrolledClass);
builder.addClass(uncontrolledClass, templateData);
return templateData;
}
if (!Objects.equals(v.value(ValueResolverGenerator.IGNORE),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package io.quarkus.qute.deployment.generatedresolvers;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;

import javax.inject.Inject;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.Engine;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateData;
import io.quarkus.qute.ValueResolver;
import io.quarkus.qute.generator.ValueResolverGenerator;
import io.quarkus.test.QuarkusUnitTest;

public class HierarchyTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(Foo.class, Bar.class)
.addAsResource(new StringAsset("{foo.name}"), "templates/test.html"));

@Inject
Template test;

@Inject
Engine engine;

@Test
public void testGeneratedResolvers() {
List<ValueResolver> resolvers = engine.getValueResolvers();
ValueResolver fooResolver = null;
ValueResolver barResolver = null;
for (ValueResolver valueResolver : resolvers) {
if (valueResolver.getClass().getName().endsWith(ValueResolverGenerator.SUFFIX)
&& valueResolver.getClass().getName().contains("Foo")) {
fooResolver = valueResolver;
}
if (valueResolver.getClass().getName().endsWith(ValueResolverGenerator.SUFFIX)
&& valueResolver.getClass().getName().contains("Bar")) {
barResolver = valueResolver;
}
}
assertNotNull(fooResolver);
assertNotNull(barResolver);
assertTrue(barResolver.getPriority() > fooResolver.getPriority(), "Bar resolver priority " + barResolver.getPriority()
+ " is not higher than Foo resolver priority " + fooResolver.getPriority());
assertEquals("bar", test.data("foo", new Bar()).render());
}

@TemplateData
public static class Foo {

public String getName() {
return "foo";
}

}

@TemplateData
public static class Bar extends Foo {

@Override
public String getName() {
return "bar";
}

}

}
Loading

0 comments on commit 23c970a

Please sign in to comment.