Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type-safe message bundles - support dynamic keys.. #11613

Merged
merged 1 commit into from
Aug 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1185,7 +1185,17 @@ Subsequently, the bundles can be used at runtime:

1. Directly in your code via `io.quarkus.qute.i18n.MessageBundles#get()`; e.g. `MessageBundles.get(AppMessages.class).hello_name("Lucie")`
2. Injected in your beans via `@Inject`; e.g. `@Inject AppMessages`
3. Referenced in the templates via the message bundle name; e.g. `{msg:hello_name('Lucie')}`
3. Referenced in the templates via the message bundle namespace:
+
[source,html]
----
{msg:hello_name('Lucie')} <1> <2> <3>
{msg:message(myKey,'Lu')} <4>
----
<1> `msg` is the default namespace.
<2> `hello_name` is the message key.
<3> `Lucie` is the parameter of the message bundle interface method.
<4> It is also possible to obtain a localized message for a key resolved at runtime using a reserved key `message`. The validation is skipped in this case though.

.Message Bundle Interface Example
[source,java]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.quarkus.qute.deployment;

import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.i18n.MessageBundles;

final class Descriptors {

static final MethodDescriptor TEMPLATE_INSTANCE = MethodDescriptor.ofMethod(Template.class, "instance",
TemplateInstance.class);
static final MethodDescriptor TEMPLATE_INSTANCE_DATA = MethodDescriptor.ofMethod(TemplateInstance.class, "data",
TemplateInstance.class, String.class, Object.class);
static final MethodDescriptor TEMPLATE_INSTANCE_RENDER = MethodDescriptor.ofMethod(TemplateInstance.class, "render",
String.class);
static final MethodDescriptor BUNDLES_GET_TEMPLATE = MethodDescriptor.ofMethod(MessageBundles.class, "getTemplate",
Template.class, String.class);

}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.quarkus.qute.deployment;

import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Stream;

import org.jboss.jandex.DotName;

import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.api.CheckedTemplate;
import io.quarkus.qute.api.ResourcePath;
import io.quarkus.qute.i18n.Localized;
import io.quarkus.qute.i18n.Message;
import io.quarkus.qute.i18n.MessageBundle;
import io.quarkus.qute.i18n.MessageParam;

final class Names {

static final DotName BUNDLE = DotName.createSimple(MessageBundle.class.getName());
static final DotName MESSAGE = DotName.createSimple(Message.class.getName());
static final DotName MESSAGE_PARAM = DotName.createSimple(MessageParam.class.getName());
static final DotName LOCALIZED = DotName.createSimple(Localized.class.getName());
static final DotName RESOURCE_PATH = DotName.createSimple(ResourcePath.class.getName());
static final DotName TEMPLATE = DotName.createSimple(Template.class.getName());
static final DotName ITERABLE = DotName.createSimple(Iterable.class.getName());
static final DotName ITERATOR = DotName.createSimple(Iterator.class.getName());
static final DotName STREAM = DotName.createSimple(Stream.class.getName());
static final DotName MAP = DotName.createSimple(Map.class.getName());
static final DotName MAP_ENTRY = DotName.createSimple(Entry.class.getName());
static final DotName COLLECTION = DotName.createSimple(Collection.class.getName());
static final DotName CHECKED_TEMPLATE = DotName.createSimple(CheckedTemplate.class.getName());
static final DotName TEMPLATE_INSTANCE = DotName.createSimple(TemplateInstance.class.getName());

private Names() {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
Expand Down Expand Up @@ -92,7 +91,6 @@
import io.quarkus.qute.TemplateLocator;
import io.quarkus.qute.UserTagSectionHelper;
import io.quarkus.qute.Variant;
import io.quarkus.qute.api.CheckedTemplate;
import io.quarkus.qute.api.ResourcePath;
import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis;
import io.quarkus.qute.deployment.TypeCheckExcludeBuildItem.Check;
Expand All @@ -114,22 +112,9 @@

public class QuteProcessor {

private static final Logger LOGGER = Logger.getLogger(QuteProcessor.class);

public static final DotName RESOURCE_PATH = DotName.createSimple(ResourcePath.class.getName());
public static final DotName TEMPLATE = DotName.createSimple(Template.class.getName());

static final DotName ITERABLE = DotName.createSimple(Iterable.class.getName());
static final DotName ITERATOR = DotName.createSimple(Iterator.class.getName());
static final DotName STREAM = DotName.createSimple(Stream.class.getName());
static final DotName MAP = DotName.createSimple(Map.class.getName());
public static final DotName RESOURCE_PATH = Names.RESOURCE_PATH;

static final DotName MAP_ENTRY = DotName.createSimple(Entry.class.getName());
static final DotName COLLECTION = DotName.createSimple(Collection.class.getName());
static final DotName STRING = DotName.createSimple(String.class.getName());

static final DotName DOTNAME_CHECKED_TEMPLATE = DotName.createSimple(CheckedTemplate.class.getName());
static final DotName DOTNAME_TEMPLATE_INSTANCE = DotName.createSimple(TemplateInstance.class.getName());
private static final Logger LOGGER = Logger.getLogger(QuteProcessor.class);

@BuildStep
FeatureBuildItem feature() {
Expand Down Expand Up @@ -195,9 +180,9 @@ List<CheckedTemplateBuildItem> collectTemplateTypeInfo(BeanArchiveIndexBuildItem
}
String supportedAdaptors;
if (adaptors.isEmpty()) {
supportedAdaptors = DOTNAME_TEMPLATE_INSTANCE + " is supported";
supportedAdaptors = Names.TEMPLATE_INSTANCE + " is supported";
} else {
StringBuffer strbuf = new StringBuffer(DOTNAME_TEMPLATE_INSTANCE.toString());
StringBuffer strbuf = new StringBuffer(Names.TEMPLATE_INSTANCE.toString());
List<String> adaptorsList = new ArrayList<>(adaptors.size());
for (DotName name : adaptors.keySet()) {
adaptorsList.add(name.toString());
Expand All @@ -209,7 +194,7 @@ List<CheckedTemplateBuildItem> collectTemplateTypeInfo(BeanArchiveIndexBuildItem
supportedAdaptors = strbuf.append(" are supported").toString();
}

for (AnnotationInstance annotation : index.getIndex().getAnnotations(DOTNAME_CHECKED_TEMPLATE)) {
for (AnnotationInstance annotation : index.getIndex().getAnnotations(Names.CHECKED_TEMPLATE)) {
if (annotation.target().kind() != Kind.CLASS)
continue;
ClassInfo classInfo = annotation.target().asClass();
Expand All @@ -228,7 +213,7 @@ List<CheckedTemplateBuildItem> collectTemplateTypeInfo(BeanArchiveIndexBuildItem
DotName returnTypeName = methodInfo.returnType().asClassType().name();
CheckedTemplateAdapter adaptor = null;
// if it's not the default template instance, try to find an adapter
if (!returnTypeName.equals(DOTNAME_TEMPLATE_INSTANCE)) {
if (!returnTypeName.equals(Names.TEMPLATE_INSTANCE)) {
adaptor = adaptors.get(returnTypeName);
if (adaptor == null)
throw new TemplateException("Incompatible checked template return type: " + methodInfo.returnType()
Expand Down Expand Up @@ -881,9 +866,9 @@ void validateTemplateInjectionPoints(QuteConfig config, List<TemplatePathBuildIt

for (InjectionPointInfo injectionPoint : validationPhase.getContext().get(BuildExtension.Key.INJECTION_POINTS)) {

if (injectionPoint.getRequiredType().name().equals(TEMPLATE)) {
if (injectionPoint.getRequiredType().name().equals(Names.TEMPLATE)) {

AnnotationInstance resourcePath = injectionPoint.getRequiredQualifier(RESOURCE_PATH);
AnnotationInstance resourcePath = injectionPoint.getRequiredQualifier(Names.RESOURCE_PATH);
String name;
if (resourcePath != null) {
name = resourcePath.value().asString();
Expand Down Expand Up @@ -942,7 +927,7 @@ public boolean test(Check check) {
return true;
}
// Collection.contains()
if (check.numberOfParameters == 1 && check.classNameEquals(COLLECTION) && check.name.equals("contains")) {
if (check.numberOfParameters == 1 && check.classNameEquals(Names.COLLECTION) && check.name.equals("contains")) {
return true;
}
return false;
Expand Down Expand Up @@ -1036,23 +1021,23 @@ static void processLoopHint(Match match, IndexView index, Expression expression)
match.getParameterizedTypeArguments(), match.getTypeParameters(), new HashMap<>(), index), index);
Function<Type, Type> firstParamType = t -> t.asParameterizedType().arguments().get(0);
// Iterable<Item> => Item
matchType = extractMatchType(closure, ITERABLE, firstParamType);
matchType = extractMatchType(closure, Names.ITERABLE, firstParamType);
if (matchType == null) {
// Stream<Long> => Long
matchType = extractMatchType(closure, STREAM, firstParamType);
matchType = extractMatchType(closure, Names.STREAM, firstParamType);
}
if (matchType == null) {
// Entry<K,V> => Entry<String,Item>
matchType = extractMatchType(closure, MAP, t -> {
matchType = extractMatchType(closure, Names.MAP, t -> {
Type[] args = new Type[2];
args[0] = t.asParameterizedType().arguments().get(0);
args[1] = t.asParameterizedType().arguments().get(1);
return ParameterizedType.create(MAP_ENTRY, args, null);
return ParameterizedType.create(Names.MAP_ENTRY, args, null);
});
}
if (matchType == null) {
// Iterator<Item> => Item
matchType = extractMatchType(closure, ITERATOR, firstParamType);
matchType = extractMatchType(closure, Names.ITERATOR, firstParamType);
}
}
if (matchType != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public interface AppMessages {
@Message("Hello {name}!")
String hello_name(String name);

@Message("Hello {name} {surname}!")
String hello_fullname(String name, String surname);

// key=hello_with_if_section
@Message(key = UNDERSCORED_ELEMENT_NAME, value = "{#if count eq 1}"
+ "{msg:hello_name('you guy')}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ public class MessageBundleTest {
"templates/foo.html")
.addAsResource(new StringAsset(
"hello=Hallo Welt!\nhello_name=Hallo {name}!"),
"messages/msg_de.properties"));
"messages/msg_de.properties")
.addAsResource(new StringAsset(
"{msg:message('hello')} {msg:message(key,'Malachi',surname)}"),
"templates/dynamic.html"));

@Inject
AppMessages messages;
Expand Down Expand Up @@ -71,6 +74,8 @@ public void testResolvers() {
assertEquals("Hallo Welt! Hallo Jachym! Hello you guys! Hello alpha! Hello! Hello foo from alpha!",
foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.GERMAN).render());
assertEquals("Dot test!", engine.parse("{msg:['dot.test']}").render());
assertEquals("Hello world! Hello Malachi Constant!",
engine.getTemplate("dynamic").data("key", "hello_fullname").data("surname", "Constant").render());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public static <T> T get(Class<T> bundleInterface, Localized localized) {
if (!bundleInterface.isAnnotationPresent(MessageBundle.class)
&& !bundleInterface.isAnnotationPresent(Localized.class)) {
throw new IllegalArgumentException(
"Message bundle interface must be annotated either with @Bundle or with @Localized: " + bundleInterface);
"Message bundle interface must be annotated either with @MessageBundle or with @Localized: "
+ bundleInterface);
}
InstanceHandle<T> handle = localized != null ? Arc.container().instance(bundleInterface, localized)
: Arc.container().instance(bundleInterface);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,24 @@
@SuppressWarnings("rawtypes")
public final class EvaluatedParams {

static final EvaluatedParams EMPTY;

static {
CompletableFuture<Void> empty = new CompletableFuture<Void>();
empty.complete(null);
EMPTY = new EvaluatedParams(empty, new CompletableFuture<?>[0]);
}

/**
*
* @param context
* @return the evaluated params
*/
public static EvaluatedParams evaluate(EvalContext context) {
List<Expression> params = context.getParams();
if (params.size() == 1) {
if (params.isEmpty()) {
return EMPTY;
} else if (params.size() == 1) {
return new EvaluatedParams(context.evaluate(params.get(0)));
}
CompletableFuture<?>[] results = new CompletableFuture<?>[params.size()];
Expand All @@ -29,6 +39,28 @@ public static EvaluatedParams evaluate(EvalContext context) {
return new EvaluatedParams(CompletableFuture.allOf(results), results);
}

public static EvaluatedParams evaluateMessageKey(EvalContext context) {
List<Expression> params = context.getParams();
if (params.isEmpty()) {
throw new IllegalArgumentException("No params to evaluate");
}
return new EvaluatedParams(context.evaluate(params.get(0)));
}

public static EvaluatedParams evaluateMessageParams(EvalContext context) {
List<Expression> params = context.getParams();
if (params.size() < 2) {
return EMPTY;
}
CompletableFuture<?>[] results = new CompletableFuture<?>[params.size() - 1];
int i = 0;
Iterator<Expression> it = params.subList(1, params.size()).iterator();
while (it.hasNext()) {
results[i++] = context.evaluate(it.next()).toCompletableFuture();
}
return new EvaluatedParams(CompletableFuture.allOf(results), results);
}

public final CompletionStage stage;
private final CompletableFuture<?>[] results;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class ExpressionTest {
@Test
public void testExpressions() throws InterruptedException, ExecutionException {
verify("data:name.value", "data", null, name("name", "name"), name("value", "value"));
verify("data:getName('value')", "data", null, virtualMethod("getName", ExpressionImpl.from("'value'")));
// ignore adjacent separators
verify("name..value", null, null, name("name"), name("value"));
verify("0", null, CompletableFuture.completedFuture(Integer.valueOf(0)), name("0", "|java.lang.Integer|"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ public void tesNamespaceResolver() {

@Override
public CompletionStage<Object> resolve(EvalContext context) {
return context.getName().equals("foo") ? CompletableFuture.completedFuture("bar") : Results.NOT_FOUND;
if (!context.getName().equals("foo")) {
return Results.NOT_FOUND;
}
CompletableFuture<Object> ret = new CompletableFuture<>();
context.evaluate(context.getParams().get(0)).whenComplete((r, e) -> {
ret.complete(r);
});
return ret;
}

@Override
Expand All @@ -25,7 +32,7 @@ public String getNamespace() {
})
.build();

assertEquals("bar", engine.parse("{global:foo}").render(null));
assertEquals("bar", engine.parse("{global:foo('bar')}").render(null));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ private Descriptors() {
"evaluate",
EvaluatedParams.class,
EvalContext.class);
public static final MethodDescriptor EVALUATED_PARAMS_EVALUATE_MESSAGE_KEY = MethodDescriptor.ofMethod(
EvaluatedParams.class,
"evaluateMessageKey",
EvaluatedParams.class,
EvalContext.class);
public static final MethodDescriptor EVALUATED_PARAMS_EVALUATE_MESSAGE_PARAMS = MethodDescriptor.ofMethod(
EvaluatedParams.class,
"evaluateMessageParams",
EvaluatedParams.class,
EvalContext.class);
public static final MethodDescriptor EVALUATED_PARAMS_GET_RESULT = MethodDescriptor.ofMethod(EvaluatedParams.class,
"getResult",
Object.class,
Expand Down