Skip to content

Commit

Permalink
Merge pull request #17286 from mkouba/issue-17185
Browse files Browse the repository at this point in the history
Mailer templates - make it possible to pass template attributes
  • Loading branch information
mkouba authored May 17, 2021
2 parents fdc3536 + 0640082 commit 168eb0c
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 41 deletions.
2 changes: 1 addition & 1 deletion docs/src/main/asciidoc/mailer.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ public Uni<Response> send() { <1>
<1> The method returns a `Uni`
<2> Transform the `Uni<Void>` returned by `MailTemplate` into `Unit<Response>`

[[testing]]]
[[testing]]
== Testing email sending

Because it is very inconvenient to send emails during development and testing, you can set the `quarkus.mailer.mock` boolean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.quarkus.mailer.i18n;

import io.quarkus.qute.i18n.Message;
import io.quarkus.qute.i18n.MessageBundle;

@MessageBundle
public interface AppMessages {

@Message("Hello {name}!")
String hello_name(String name);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.quarkus.mailer.i18n;

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

import java.util.List;
import java.util.Locale;

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.mailer.Mail;
import io.quarkus.mailer.MailTemplate.MailTemplateInstance;
import io.quarkus.mailer.MockMailbox;
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.test.QuarkusUnitTest;

public class MailMessageBundleTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(Templates.class, AppMessages.class)
.addAsResource("mock-config.properties", "application.properties")
.addAsResource(new StringAsset(
"hello_name=Hallo {name}!"),
"messages/msg_de.properties")
.addAsResource(new StringAsset(""
+ "{msg:hello_name(name)}"), "templates/MailMessageBundleTest/hello.txt"));

@Inject
MockMailbox mailbox;

@Test
public void testSend() {
mailbox.clear();

Templates.hello("Lu").to("[email protected]").subject("Test").send().await().indefinitely();

List<Mail> sent = mailbox.getMessagesSentTo("[email protected]");
assertEquals(1, sent.size());
Mail english = sent.get(0);
assertEquals("Test", english.getSubject());
assertEquals("Hello Lu!", english.getText());

// Set the locale attribute
Templates.hello("Lu").to("[email protected]").subject("Test").setAttribute("locale", Locale.GERMAN).send().await()
.indefinitely();

assertEquals(2, sent.size());
Mail german = sent.get(1);
assertEquals("Test", german.getSubject());
assertEquals("Hallo Lu!", german.getText());
}

@CheckedTemplate
static class Templates {

static native MailTemplateInstance hello(String name);

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,24 @@ interface MailTemplateInstance {

MailTemplateInstance addInlineAttachment(String name, File file, String contentType, String contentId);

/**
*
* @param key
* @param value
* @return self
* @see io.quarkus.qute.TemplateInstance#data(String, Object)
*/
MailTemplateInstance data(String key, Object value);

/**
*
* @param key
* @param value
* @return self
* @see io.quarkus.qute.TemplateInstance#setAttribute(String, Object)
*/
MailTemplateInstance setAttribute(String key, Object value);

Uni<Void> send();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ public MailTemplateInstance data(String key, Object value) {
return this;
}

@Override
public MailTemplateInstance setAttribute(String key, Object value) {
this.templateInstance.setAttribute(key, value);
return this;
}

@Override
public Uni<Void> send() {
Object variantsAttr = templateInstance.getAttribute(TemplateInstance.VARIANTS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
import io.quarkus.qute.i18n.MessageBundle;
import io.quarkus.qute.i18n.MessageBundles;
import io.quarkus.qute.runtime.MessageBundleRecorder;
import io.quarkus.qute.runtime.QuteConfig;
import io.quarkus.runtime.util.StringUtil;

public class MessageBundleProcessor {
Expand Down Expand Up @@ -317,7 +318,9 @@ void validateMessageBundleMethodsInTemplates(TemplatesAnalysisBuildItem analysis
List<MessageBundleMethodBuildItem> messageBundleMethods,
List<TemplateExpressionMatchesBuildItem> expressionMatches,
BuildProducer<IncorrectExpressionBuildItem> incorrectExpressions,
BuildProducer<ImplicitValueResolverBuildItem> implicitClasses) {
BuildProducer<ImplicitValueResolverBuildItem> implicitClasses,
List<CheckedTemplateBuildItem> checkedTemplates,
QuteConfig config) {

IndexView index = beanArchiveIndex.getIndex();
Function<String, String> templateIdToPathFun = new Function<String, String>() {
Expand Down Expand Up @@ -357,9 +360,26 @@ public String apply(String id) {

for (Entry<TemplateAnalysis, Set<Expression>> exprEntry : expressions.entrySet()) {

TemplateAnalysis templateAnalysis = exprEntry.getKey();

String path = templateAnalysis.path;
for (String suffix : config.suffixes) {
if (path.endsWith(suffix)) {
path = path.substring(0, path.length() - (suffix.length() + 1));
break;
}
}
CheckedTemplateBuildItem checkedTemplate = null;
for (CheckedTemplateBuildItem item : checkedTemplates) {
if (item.templateId.equals(path)) {
checkedTemplate = item;
break;
}
}

Map<Integer, Match> generatedIdsToMatches = Collections.emptyMap();
for (TemplateExpressionMatchesBuildItem templateExpressionMatchesBuildItem : expressionMatches) {
if (templateExpressionMatchesBuildItem.templateGeneratedId.equals(exprEntry.getKey().generatedId)) {
if (templateExpressionMatchesBuildItem.templateGeneratedId.equals(templateAnalysis.generatedId)) {
generatedIdsToMatches = templateExpressionMatchesBuildItem.getGeneratedIdsToMatches();
break;
}
Expand Down Expand Up @@ -412,7 +432,7 @@ public String apply(String id) {
QuteProcessor.validateNestedExpressions(exprEntry.getKey(), defaultBundleInterface,
results, templateExtensionMethods, excludes,
incorrectExpressions, expression, index, implicitClassToMembersUsed,
templateIdToPathFun, generatedIdsToMatches);
templateIdToPathFun, generatedIdsToMatches, checkedTemplate);
Match match = results.get(param.toOriginalString());
if (match != null && !match.isEmpty() && !Types.isAssignableFrom(match.type(),
methodParams.get(idx), index)) {
Expand All @@ -423,6 +443,15 @@ public String apply(String id) {
+ "] does not match the type: " + match.type(),
expression.getOrigin()));
}
} else if (checkedTemplate != null && checkedTemplate.requireTypeSafeExpressions) {
incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
"Only type-safe expressions are allowed in the checked template defined via: "
+ checkedTemplate.method.declaringClass().name() + "."
+ checkedTemplate.method.name()
+ "(); an expression must be based on a checked template parameter "
+ checkedTemplate.bindings.keySet()
+ ", or bound via a param declaration, or the requirement must be relaxed via @CheckedTemplate(requireTypeSafeExpressions = false)",
expression.getOrigin()));
}
idx++;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,29 +534,15 @@ public String apply(String id) {
if (expression.getNamespace().equals(EngineProducer.INJECT_NAMESPACE)) {
validateInjectExpression(templateAnalysis, expression, index, incorrectExpressions,
templateExtensionMethods, excludes, namedBeans, generatedIdsToMatches,
implicitClassToMembersUsed,
templateIdToPathFun);
implicitClassToMembersUsed, templateIdToPathFun, checkedTemplate);
} else {
continue;
}
} else {
if (checkedTemplate != null && checkedTemplate.requireTypeSafeExpressions && !expression.hasTypeInfo()) {
incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
"Only type-safe expressions are allowed in the checked template defined via: "
+ checkedTemplate.method.declaringClass().name() + "."
+ checkedTemplate.method.name()
+ "(); an expression must be based on a checked template parameter "
+ checkedTemplate.bindings.keySet()
+ ", or bound via a param declaration, or the requirement must be relaxed via @CheckedTemplate(requireTypeSafeExpressions = false)",
expression.getOrigin()));
continue;
}

generatedIdsToMatches.put(expression.getGeneratedId(),
validateNestedExpressions(templateAnalysis, null, new HashMap<>(), templateExtensionMethods,
excludes,
incorrectExpressions, expression, index, implicitClassToMembersUsed, templateIdToPathFun,
generatedIdsToMatches));
excludes, incorrectExpressions, expression, index, implicitClassToMembersUsed,
templateIdToPathFun, generatedIdsToMatches, checkedTemplate));
}
}

Expand Down Expand Up @@ -587,25 +573,12 @@ static String buildIgnorePattern(Iterable<String> names) {
return ignorePattern.toString();
}

/**
* @param templateAnalysis
* @param rootClazz
* @param results Map of cached results within a single expression
* @param templateExtensionMethods
* @param excludes
* @param incorrectExpressions
* @param expression
* @param index
* @param implicitClassToMembersUsed
* @param templateIdToPathFun
* @return the last match object
*/
static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassInfo rootClazz, Map<String, Match> results,
List<TemplateExtensionMethodBuildItem> templateExtensionMethods,
List<TypeCheckExcludeBuildItem> excludes,
BuildProducer<IncorrectExpressionBuildItem> incorrectExpressions, Expression expression, IndexView index,
Map<DotName, Set<String>> implicitClassToMembersUsed, Function<String, String> templateIdToPathFun,
Map<Integer, Match> generatedIdsToMatches) {
Map<Integer, Match> generatedIdsToMatches, CheckedTemplateBuildItem checkedTemplate) {

// First validate nested virtual methods
for (Expression.Part part : expression.getParts()) {
Expand All @@ -614,13 +587,27 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI
if (!results.containsKey(param.toOriginalString())) {
validateNestedExpressions(templateAnalysis, null, results, templateExtensionMethods, excludes,
incorrectExpressions, param, index, implicitClassToMembersUsed, templateIdToPathFun,
generatedIdsToMatches);
generatedIdsToMatches, checkedTemplate);
}
}
}
}
// Then validate the expression itself
Match match = new Match(index);

if (checkedTemplate != null && checkedTemplate.requireTypeSafeExpressions && !expression.hasTypeInfo()) {
incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
"Only type-safe expressions are allowed in the checked template defined via: "
+ checkedTemplate.method.declaringClass().name() + "."
+ checkedTemplate.method.name()
+ "(); an expression must be based on a checked template parameter "
+ checkedTemplate.bindings.keySet()
+ ", or bound via a param declaration, or the requirement must be relaxed via @CheckedTemplate(requireTypeSafeExpressions = false)",
expression.getOrigin()));
results.put(expression.toOriginalString(), match);
return match;
}

if (rootClazz == null && !expression.hasTypeInfo()) {
// No type info available or a namespace expression
results.put(expression.toOriginalString(), match);
Expand Down Expand Up @@ -875,7 +862,8 @@ private void validateInjectExpression(TemplateAnalysis templateAnalysis, Express
BuildProducer<IncorrectExpressionBuildItem> incorrectExpressions,
List<TemplateExtensionMethodBuildItem> templateExtensionMethods, List<TypeCheckExcludeBuildItem> excludes,
Map<String, BeanInfo> namedBeans, Map<Integer, Match> generatedIdsToMatches,
Map<DotName, Set<String>> implicitClassToMembersUsed, Function<String, String> templateIdToPathFun) {
Map<DotName, Set<String>> implicitClassToMembersUsed, Function<String, String> templateIdToPathFun,
CheckedTemplateBuildItem checkedTemplate) {
Expression.Part firstPart = expression.getParts().get(0);
if (firstPart.isVirtualMethod()) {
incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
Expand All @@ -902,7 +890,7 @@ private void validateInjectExpression(TemplateAnalysis templateAnalysis, Express
generatedIdsToMatches.put(expression.getGeneratedId(),
validateNestedExpressions(templateAnalysis, bean.getImplClazz(), new HashMap<>(),
templateExtensionMethods, excludes, incorrectExpressions, expression, index,
implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches));
implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, checkedTemplate));

} else {
// User is injecting a non-existing bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package io.quarkus.qute.deployment.typesafe;

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

import javax.inject.Named;
import javax.inject.Singleton;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
Expand All @@ -18,10 +23,24 @@ public class CheckedTemplateRequireTypeSafeTest {
@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(Templates.class)
.addAsResource(new StringAsset("Hello {name}! {any}"),
.addClasses(Templates.class, Fool.class)
.addAsResource(new StringAsset("Hello {name}! {any} {inject:fool.getJoke(identifier)}"),
"templates/CheckedTemplateRequireTypeSafeTest/hola.txt"))
.setExpectedException(TemplateException.class);
.assertException(t -> {
Throwable e = t;
TemplateException te = null;
while (e != null) {
if (e instanceof TemplateException) {
te = (TemplateException) e;
break;
}
e = e.getCause();
}
assertNotNull(te);
assertTrue(te.getMessage().contains("Found template problems (2)"), te.getMessage());
assertTrue(te.getMessage().contains("any"), te.getMessage());
assertTrue(te.getMessage().contains("identifier"), te.getMessage());
});

@Test
public void testValidation() {
Expand All @@ -35,4 +54,14 @@ static class Templates {

}

@Singleton
@Named
public static class Fool {

public String getJoke(Integer id) {
return "ok";
}

}

}

0 comments on commit 168eb0c

Please sign in to comment.