diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 01fb69b3bd9ef..f98141f4f915e 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -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] diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Descriptors.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Descriptors.java new file mode 100644 index 0000000000000..8afc70fe8a2cb --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Descriptors.java @@ -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); + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java index 1aea4304f4538..e8851f761974f 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java @@ -1,8 +1,6 @@ package io.quarkus.qute.deployment; import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; -import static org.objectweb.asm.Opcodes.ACC_FINAL; -import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import java.io.File; import java.io.IOException; @@ -11,9 +9,11 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -69,11 +69,10 @@ import io.quarkus.gizmo.ResultHandle; import io.quarkus.gizmo.TryBlock; import io.quarkus.qute.EvalContext; +import io.quarkus.qute.EvaluatedParams; import io.quarkus.qute.Expression; import io.quarkus.qute.Expression.Part; import io.quarkus.qute.Resolver; -import io.quarkus.qute.Template; -import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.deployment.QuteProcessor.Match; import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; import io.quarkus.qute.generator.Descriptors; @@ -82,7 +81,6 @@ import io.quarkus.qute.i18n.Message; import io.quarkus.qute.i18n.MessageBundle; import io.quarkus.qute.i18n.MessageBundles; -import io.quarkus.qute.i18n.MessageParam; import io.quarkus.qute.runtime.MessageBundleRecorder; import io.quarkus.runtime.util.StringUtil; @@ -94,20 +92,7 @@ public class MessageBundleProcessor { private static final String BUNDLE_DEFAULT_KEY = "defaultKey"; private static final String BUNDLE_LOCALE = "locale"; private static final String MESSAGES = "messages"; - - 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 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); + private static final String MESSAGE = "message"; @BuildStep AdditionalBeanBuildItem beans() { @@ -137,7 +122,7 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv } // First collect all interfaces annotated with @MessageBundle - for (AnnotationInstance bundleAnnotation : index.getAnnotations(BUNDLE)) { + for (AnnotationInstance bundleAnnotation : index.getAnnotations(Names.BUNDLE)) { if (bundleAnnotation.target().kind() == Kind.CLASS) { ClassInfo bundleClass = bundleAnnotation.target().asClass(); if (Modifier.isInterface(bundleClass.flags())) { @@ -161,7 +146,7 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv } Map localeToInterface = new HashMap<>(); for (ClassInfo localizedInterface : localized) { - String locale = localizedInterface.classAnnotation(LOCALIZED).value().asString(); + String locale = localizedInterface.classAnnotation(Names.LOCALIZED).value().asString(); ClassInfo previous = localeToInterface.put(locale, localizedInterface); if (defaultLocale.equals(locale) || previous != null) { throw new MessageBundleException(String.format( @@ -205,8 +190,8 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv ClassInfo bundleInterface = bundle.getDefaultBundleInterface(); beanRegistration.getContext().configure(bundleInterface.name()).addType(bundle.getDefaultBundleInterface().name()) // The default message bundle - add both @Default and @Localized - .addQualifier(DotNames.DEFAULT).addQualifier().annotation(LOCALIZED) - .addValue("value", getDefaultLocale(bundleInterface.classAnnotation(BUNDLE))).done().unremovable() + .addQualifier(DotNames.DEFAULT).addQualifier().annotation(Names.LOCALIZED) + .addValue("value", getDefaultLocale(bundleInterface.classAnnotation(Names.BUNDLE))).done().unremovable() .scope(Singleton.class).creator(mc -> { // Just create a new instance of the generated class mc.returnValue( @@ -218,7 +203,7 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv for (ClassInfo localizedInterface : bundle.getLocalizedInterfaces().values()) { beanRegistration.getContext().configure(localizedInterface.name()) .addType(bundle.getDefaultBundleInterface().name()) - .addQualifier(localizedInterface.classAnnotation(LOCALIZED)) + .addQualifier(localizedInterface.classAnnotation(Names.LOCALIZED)) .unremovable() .scope(Singleton.class).creator(mc -> { // Just create a new instance of the generated class @@ -231,7 +216,7 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv for (Entry entry : bundle.getLocalizedFiles().entrySet()) { beanRegistration.getContext().configure(bundle.getDefaultBundleInterface().name()) .addType(bundle.getDefaultBundleInterface().name()) - .addQualifier().annotation(LOCALIZED) + .addQualifier().annotation(Names.LOCALIZED) .addValue("value", entry.getKey()).done() .unremovable() .scope(Singleton.class).creator(mc -> { @@ -370,9 +355,12 @@ public String apply(String id) { Map implicitClassToMethodUsed = new HashMap<>(); for (Expression expression : expressions) { - // msg:hello_world(foo.name) Part methodPart = expression.getParts().get(0); + if (methodPart.getName().equals(MESSAGE)) { + // Skip validation - dynamic key, e.g. msg:message(myKey,param1,param2) + continue; + } MethodInfo method = methods.get(methodPart.getName()); if (method == null) { @@ -509,7 +497,7 @@ public String apply(String className) { private boolean hasMessageBundleMethod(ClassInfo bundleInterface, String name) { for (MethodInfo method : bundleInterface.methods()) { - if (method.name().equals(name) && method.hasAnnotation(MESSAGE)) { + if (method.name().equals(name) && method.hasAnnotation(Names.MESSAGE)) { return true; } } @@ -521,8 +509,9 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d Map messageTemplates, String locale) { LOGGER.debugf("Generate bundle implementation for %s", bundleInterface); - AnnotationInstance bundleAnnotation = defaultBundleInterface != null ? defaultBundleInterface.classAnnotation(BUNDLE) - : bundleInterface.classAnnotation(BUNDLE); + AnnotationInstance bundleAnnotation = defaultBundleInterface != null + ? defaultBundleInterface.classAnnotation(Names.BUNDLE) + : bundleInterface.classAnnotation(Names.BUNDLE); AnnotationValue nameValue = bundleAnnotation.value(); String bundleName = nameValue != null ? nameValue.asString() : MessageBundle.DEFAULT_NAME; AnnotationValue defaultKeyValue = bundleAnnotation.value(BUNDLE_DEFAULT_KEY); @@ -550,9 +539,12 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d ClassCreator bundleCreator = builder.build(); // key -> method - Map keyMap = new HashMap<>(); + Map keyMap = new LinkedHashMap<>(); + List methods = new ArrayList<>(bundleInterface.methods()); + // Sort methods + methods.sort(Comparator.comparing(MethodInfo::name).thenComparing(Comparator.comparing(MethodInfo::toString))); - for (MethodInfo method : bundleInterface.methods()) { + for (MethodInfo method : methods) { if (!method.returnType().name().equals(DotNames.STRING)) { throw new MessageBundleException( String.format("A message bundle interface method must return java.lang.String on %s: %s", @@ -570,9 +562,9 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d throw new MessageBundleException( String.format("Default bundle method not found on %s: %s", bundleInterface, method)); } - messageAnnotation = defaultBundleMethod.annotation(MESSAGE); + messageAnnotation = defaultBundleMethod.annotation(Names.MESSAGE); } else { - messageAnnotation = method.annotation(MESSAGE); + messageAnnotation = method.annotation(Names.MESSAGE); } if (messageAnnotation == null) { @@ -582,6 +574,11 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d } String key = getKey(method, messageAnnotation, defaultKeyValue); + if (key.equals(MESSAGE)) { + throw new MessageBundleException(String.format( + "A message bundle interface method must not use the key 'message' which is reserved for dynamic lookup; defined for %s#%s()", + bundleInterface, method.name())); + } if (keyMap.containsKey(key)) { throw new MessageBundleException(String.format("Duplicate key [%s] found on %s", key, bundleInterface)); } @@ -596,7 +593,7 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d if (messageTemplate.contains("}")) { if (defaultBundleInterface != null) { if (locale == null) { - AnnotationInstance localizedAnnotation = bundleInterface.classAnnotation(LOCALIZED); + AnnotationInstance localizedAnnotation = bundleInterface.classAnnotation(Names.LOCALIZED); locale = localizedAnnotation.value().asString(); } templateId = bundleName + "_" + locale + "_" + key; @@ -615,10 +612,12 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d bundleMethod.returnValue(bundleMethod.load(messageTemplate)); } else { // Obtain the template, e.g. msg_hello_name - ResultHandle template = bundleMethod.invokeStaticMethod(BUNDLES_GET_TEMPLATE, + ResultHandle template = bundleMethod.invokeStaticMethod( + io.quarkus.qute.deployment.Descriptors.BUNDLES_GET_TEMPLATE, bundleMethod.load(templateId)); // Create a template instance - ResultHandle templateInstance = bundleMethod.invokeInterfaceMethod(TEMPLATE_INSTANCE, template); + ResultHandle templateInstance = bundleMethod + .invokeInterfaceMethod(io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE, template); List paramTypes = method.parameters(); if (!paramTypes.isEmpty()) { // Set data @@ -626,7 +625,8 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d Iterator it = paramTypes.iterator(); while (it.hasNext()) { String name = getParameterName(method, i); - bundleMethod.invokeInterfaceMethod(TEMPLATE_INSTANCE_DATA, templateInstance, + bundleMethod.invokeInterfaceMethod(io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE_DATA, + templateInstance, bundleMethod.load(name), bundleMethod.getMethodParam(i)); i++; it.next(); @@ -634,7 +634,8 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d } // Render the template // At this point it's already validated that the method returns String - bundleMethod.returnValue(bundleMethod.invokeInterfaceMethod(TEMPLATE_INSTANCE_RENDER, templateInstance)); + bundleMethod.returnValue(bundleMethod.invokeInterfaceMethod( + io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE_RENDER, templateInstance)); } } @@ -649,7 +650,7 @@ private String getParameterName(MethodInfo method, int position) { AnnotationInstance paramAnnotation = Annotations .find(Annotations.getParameterAnnotations(method.annotations()).stream() .filter(a -> a.target().asMethodParameter().position() == position).collect(Collectors.toList()), - MESSAGE_PARAM); + Names.MESSAGE_PARAM); if (paramAnnotation != null) { AnnotationValue paramAnnotationValue = paramAnnotation.value(); if (paramAnnotationValue != null && !paramAnnotationValue.asString().equals(Message.ELEMENT_NAME)) { @@ -667,12 +668,71 @@ private String getParameterName(MethodInfo method, int position) { private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreator, Map keyMap) { MethodCreator resolve = bundleCreator.getMethodCreator("resolve", CompletionStage.class, EvalContext.class); + String resolveMethodPrefix = bundleCreator.getClassName().contains("/") + ? bundleCreator.getClassName().substring(bundleCreator.getClassName().lastIndexOf('/') + 1) + : bundleCreator.getClassName(); ResultHandle evalContext = resolve.getMethodParam(0); ResultHandle name = resolve.invokeInterfaceMethod(Descriptors.GET_NAME, evalContext); ResultHandle ret = resolve.newInstance(MethodDescriptor.ofConstructor(CompletableFuture.class)); - // Group messages to workaround limits of a java method body + // First handle dynamic messages, i.e. the "message" virtual method + BytecodeCreator dynamicMessage = resolve.ifTrue(resolve.invokeVirtualMethod(Descriptors.EQUALS, + resolve.load(MESSAGE), name)) + .trueBranch(); + ResultHandle evaluatedMessageKey = dynamicMessage.invokeStaticMethod(Descriptors.EVALUATED_PARAMS_EVALUATE_MESSAGE_KEY, + evalContext); + ResultHandle paramsReady = dynamicMessage.readInstanceField(Descriptors.EVALUATED_PARAMS_STAGE, + evaluatedMessageKey); + + // Define function called when the message key is ready + FunctionCreator whenCompleteFun = dynamicMessage.createFunction(BiConsumer.class); + dynamicMessage.invokeInterfaceMethod(Descriptors.CF_WHEN_COMPLETE, paramsReady, whenCompleteFun.getInstance()); + BytecodeCreator whenComplete = whenCompleteFun.getBytecode(); + AssignableResultHandle whenThis = whenComplete + .createVariable(DescriptorUtils.extToInt(bundleCreator.getClassName())); + whenComplete.assign(whenThis, dynamicMessage.getThis()); + AssignableResultHandle whenRet = whenComplete.createVariable(CompletableFuture.class); + whenComplete.assign(whenRet, ret); + AssignableResultHandle whenEvalContext = whenComplete.createVariable(EvalContext.class); + whenComplete.assign(whenEvalContext, evalContext); + BranchResult throwableIsNull = whenComplete.ifNull(whenComplete.getMethodParam(1)); + BytecodeCreator success = throwableIsNull.trueBranch(); + + // Return if the name is null or NOT_FOUND + ResultHandle resultNotFound = success.readStaticField(Descriptors.RESULT_NOT_FOUND); + BytecodeCreator nameIsNull = success.ifNull(whenComplete.getMethodParam(0)).trueBranch(); + nameIsNull.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE, whenRet, + resultNotFound); + nameIsNull.returnValue(null); + BytecodeCreator nameNotFound = success.ifTrue(success.invokeVirtualMethod(Descriptors.EQUALS, + whenComplete.getMethodParam(0), resultNotFound)).trueBranch(); + nameNotFound.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE, whenRet, resultNotFound); + nameNotFound.returnValue(null); + + // Evaluate the rest of the params + ResultHandle evaluatedMessageParams = success.invokeStaticMethod( + Descriptors.EVALUATED_PARAMS_EVALUATE_MESSAGE_PARAMS, + whenEvalContext); + // Delegate to BundleClassName_resolve_0 (the first group of messages) + ResultHandle res0Ret = success.invokeVirtualMethod( + MethodDescriptor.ofMethod(bundleCreator.getClassName(), resolveMethodPrefix + "_resolve_0", + CompletableFuture.class, String.class, + EvaluatedParams.class, CompletableFuture.class), + whenThis, whenComplete.getMethodParam(0), evaluatedMessageParams, whenRet); + BytecodeCreator ret0Null = success.ifNull(res0Ret).trueBranch(); + ret0Null.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE, whenRet, + resultNotFound); + BytecodeCreator failure = throwableIsNull.falseBranch(); + failure.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE_EXCEPTIONALLY, whenRet, + whenComplete.getMethodParam(1)); + whenComplete.returnValue(null); + // Return from the resolve method + dynamicMessage.returnValue(ret); + + // Proceed with generated messages + // We do group messages to workaround limits of a JVM method body + ResultHandle evaluatedParams = resolve.invokeStaticMethod(Descriptors.EVALUATED_PARAMS_EVALUATE, evalContext); final int groupLimit = 300; int groupIndex = 0; int resolveIndex = 0; @@ -680,24 +740,33 @@ private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreat for (Entry entry : keyMap.entrySet()) { if (resolveGroup == null || groupIndex++ >= groupLimit) { + groupIndex = 0; + String resolveMethodName = resolveMethodPrefix + "_resolve_" + resolveIndex++; if (resolveGroup != null) { - resolveGroup.returnValue(resolveGroup.loadNull()); + // Delegate to the next "resolve_x" method + resolveGroup.returnValue(resolveGroup.invokeVirtualMethod( + MethodDescriptor.ofMethod(bundleCreator.getClassName(), resolveMethodName, CompletableFuture.class, + String.class, + EvaluatedParams.class, CompletableFuture.class), + resolveGroup.getThis(), resolveGroup.getMethodParam(0), resolveGroup.getMethodParam(1), + resolveGroup.getMethodParam(2))); } - groupIndex = 0; - String resolveMethodName = "resolve_" + resolveIndex++; resolveGroup = bundleCreator.getMethodCreator(resolveMethodName, CompletableFuture.class, String.class, - EvalContext.class, CompletableFuture.class).setModifiers(ACC_PRIVATE | ACC_FINAL); - ResultHandle resRet = resolve.invokeVirtualMethod( - MethodDescriptor.ofMethod(bundleCreator.getClassName(), resolveMethodName, CompletableFuture.class, - String.class, EvalContext.class, CompletableFuture.class), - resolve.getThis(), name, evalContext, ret); - resolve.ifNotNull(resRet).trueBranch().returnValue(resRet); + EvaluatedParams.class, CompletableFuture.class).setModifiers(0); + if (resolveIndex == 1) { + ResultHandle resRet = resolve.invokeVirtualMethod( + MethodDescriptor.ofMethod(bundleCreator.getClassName(), resolveMethodName, CompletableFuture.class, + String.class, EvaluatedParams.class, CompletableFuture.class), + resolve.getThis(), name, evaluatedParams, ret); + resolve.ifNotNull(resRet).trueBranch().returnValue(resRet); + } } addMessageMethod(resolveGroup, entry.getKey(), entry.getValue(), resolveGroup.getMethodParam(0), resolveGroup.getMethodParam(1), resolveGroup.getMethodParam(2), bundleCreator.getClassName()); } if (resolveGroup != null) { + // Last group - return null resolveGroup.returnValue(resolveGroup.loadNull()); } @@ -711,11 +780,11 @@ private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreat } private void addMessageMethod(MethodCreator resolve, String key, MethodInfo method, ResultHandle name, - ResultHandle evalContext, + ResultHandle evaluatedParams, ResultHandle ret, String bundleClass) { List methodParams = method.parameters(); - BytecodeCreator matched = resolve.ifNonZero(resolve.invokeVirtualMethod(Descriptors.EQUALS, + BytecodeCreator matched = resolve.ifTrue(resolve.invokeVirtualMethod(Descriptors.EQUALS, resolve.load(key), name)) .trueBranch(); if (method.parameters().isEmpty()) { @@ -724,8 +793,6 @@ private void addMessageMethod(MethodCreator resolve, String key, MethodInfo meth matched.returnValue(ret); } else { // The CompletionStage upon which we invoke whenComplete() - ResultHandle evaluatedParams = matched.invokeStaticMethod(Descriptors.EVALUATED_PARAMS_EVALUATE, - evalContext); ResultHandle paramsReady = matched.readInstanceField(Descriptors.EVALUATED_PARAMS_STAGE, evaluatedParams); diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java new file mode 100644 index 0000000000000..5f248e4ca1144 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java @@ -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() { + } + +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index af230db2e3cbf..de02abbf5392f 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -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; @@ -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; @@ -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() { @@ -195,9 +180,9 @@ List 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 adaptorsList = new ArrayList<>(adaptors.size()); for (DotName name : adaptors.keySet()) { adaptorsList.add(name.toString()); @@ -209,7 +194,7 @@ List 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(); @@ -228,7 +213,7 @@ List 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() @@ -881,9 +866,9 @@ void validateTemplateInjectionPoints(QuteConfig config, List(), index), index); Function firstParamType = t -> t.asParameterizedType().arguments().get(0); // Iterable => Item - matchType = extractMatchType(closure, ITERABLE, firstParamType); + matchType = extractMatchType(closure, Names.ITERABLE, firstParamType); if (matchType == null) { // Stream => Long - matchType = extractMatchType(closure, STREAM, firstParamType); + matchType = extractMatchType(closure, Names.STREAM, firstParamType); } if (matchType == null) { // Entry => Entry - 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 - matchType = extractMatchType(closure, ITERATOR, firstParamType); + matchType = extractMatchType(closure, Names.ITERATOR, firstParamType); } } if (matchType != null) { diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/AppMessages.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/AppMessages.java index 391a0cb0798af..aed50cf3fe181 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/AppMessages.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/AppMessages.java @@ -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')}" diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java index cee340f1ba145..2fa7abb662ed1 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java @@ -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; @@ -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()); } } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java index 2f323890080a7..f84e26f408487 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java @@ -44,7 +44,8 @@ public static T get(Class 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 handle = localized != null ? Arc.container().instance(bundleInterface, localized) : Arc.container().instance(bundleInterface); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatedParams.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatedParams.java index a78c90a32841e..ae0c9a24ca659 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatedParams.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatedParams.java @@ -10,6 +10,14 @@ @SuppressWarnings("rawtypes") public final class EvaluatedParams { + static final EvaluatedParams EMPTY; + + static { + CompletableFuture empty = new CompletableFuture(); + empty.complete(null); + EMPTY = new EvaluatedParams(empty, new CompletableFuture[0]); + } + /** * * @param context @@ -17,7 +25,9 @@ public final class EvaluatedParams { */ public static EvaluatedParams evaluate(EvalContext context) { List 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()]; @@ -29,6 +39,28 @@ public static EvaluatedParams evaluate(EvalContext context) { return new EvaluatedParams(CompletableFuture.allOf(results), results); } + public static EvaluatedParams evaluateMessageKey(EvalContext context) { + List 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 params = context.getParams(); + if (params.size() < 2) { + return EMPTY; + } + CompletableFuture[] results = new CompletableFuture[params.size() - 1]; + int i = 0; + Iterator 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; diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ExpressionTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ExpressionTest.java index 3d24dff4f9dca..fc20d863a5383 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ExpressionTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ExpressionTest.java @@ -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|")); diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/GlobalNamespaceResolverTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/GlobalNamespaceResolverTest.java index a74475bce62d2..ada00970a24ae 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/GlobalNamespaceResolverTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/GlobalNamespaceResolverTest.java @@ -15,7 +15,14 @@ public void tesNamespaceResolver() { @Override public CompletionStage resolve(EvalContext context) { - return context.getName().equals("foo") ? CompletableFuture.completedFuture("bar") : Results.NOT_FOUND; + if (!context.getName().equals("foo")) { + return Results.NOT_FOUND; + } + CompletableFuture ret = new CompletableFuture<>(); + context.evaluate(context.getParams().get(0)).whenComplete((r, e) -> { + ret.complete(r); + }); + return ret; } @Override @@ -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)); } } diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java index 521262bd63ecc..4d4434113fe5d 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java @@ -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,