Repository: quarkusio/quarkus
Issue: NullPointerException when rendering Qute templates as XML 

**Describe the bug**

```
Caused by: java.lang.NullPointerException
	at io.quarkus.qute.runtime.TemplateProducer$InjectableTemplateInstanceImpl.templateInstance(TemplateProducer.java:168)
	at io.quarkus.qute.runtime.TemplateProducer$InjectableTemplateInstanceImpl.render(TemplateProducer.java:149)
	at micro.apps.greeting.GreetingResource.hello(GreetingResource.java:30)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:167)
	at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:130)
	at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:638)
	at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:504)
	at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$2(ResourceMethodInvoker.java:454)
	at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364)
	at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:456)
	at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:417)
	at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:391)
	at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:68)
	at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:488)
	... 20 more
```

**Expected behavior**
Render the template

**Actual behavior**
NullPointerException is thrown

**To Reproduce**

[greeting-quarkus.zip](https://github.com/quarkusio/quarkus/files/5286885/greeting-quarkus.zip)


Steps to reproduce the behavior:
1. Run `mvn quarkus:dev` in the attached project above
2. `curl http://localhost:8080/greeting` and you should see the error

**Environment (please complete the following information):**
 - Output of `uname -a` or `ver`: Fedora 32 
 - Output of `java -version`: 11
 - GraalVM version (if different from Java): Not used 
 - Quarkus version or git rev: 1.8.1.Final
 - Build tool (ie. output of `mvnw --version` or `gradlew --version`): 
```
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: /home/linuxbrew/.linuxbrew/Cellar/maven/3.6.3/libexec
Java version: 11.0.8, vendor: N/A, runtime: /home/ggastald/.sdkman/candidates/java/11.0.8-open
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "5.8.7-200.fc32.x86_64", arch: "amd64", family: "unix"
```

Changed files:
extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MissingTemplateTest.java
extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/MissingTemplateTest.java
extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java

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 ae0c4a11281..1b642f5e560 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
@@ -84,6 +84,7 @@
 import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem;
 import io.quarkus.gizmo.ClassOutput;
 import io.quarkus.panache.common.deployment.PanacheEntityClassesBuildItem;
+import io.quarkus.qute.CheckedTemplate;
 import io.quarkus.qute.Engine;
 import io.quarkus.qute.EngineBuilder;
 import io.quarkus.qute.Expression;
@@ -104,7 +105,6 @@
 import io.quarkus.qute.UserTagSectionHelper;
 import io.quarkus.qute.Variant;
 import io.quarkus.qute.WhenSectionHelper;
-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.TypeCheck;
@@ -212,10 +212,11 @@ AdditionalBeanBuildItem additionalBeans() {
     }
 
     @BuildStep
-    List<CheckedTemplateBuildItem> collectTemplateTypeInfo(BeanArchiveIndexBuildItem index,
+    List<CheckedTemplateBuildItem> collectCheckedTemplates(BeanArchiveIndexBuildItem index,
             BuildProducer<BytecodeTransformerBuildItem> transformers,
             List<TemplatePathBuildItem> templatePaths,
-            List<CheckedTemplateAdapterBuildItem> templateAdaptorBuildItems) {
+            List<CheckedTemplateAdapterBuildItem> templateAdaptorBuildItems,
+            QuteConfig config) {
         List<CheckedTemplateBuildItem> ret = new ArrayList<>();
 
         Map<DotName, CheckedTemplateAdapter> adaptors = new HashMap<>();
@@ -246,6 +247,20 @@ List<CheckedTemplateBuildItem> collectTemplateTypeInfo(BeanArchiveIndexBuildItem
         checkedTemplateAnnotations.addAll(index.getIndex().getAnnotations(Names.CHECKED_TEMPLATE_OLD));
         checkedTemplateAnnotations.addAll(index.getIndex().getAnnotations(Names.CHECKED_TEMPLATE));
 
+        // Build a set of file paths for validation
+        Set<String> filePaths = new HashSet<String>();
+        for (TemplatePathBuildItem templatePath : templatePaths) {
+            String path = templatePath.getPath();
+            filePaths.add(path);
+            // Also add version without suffix from the path
+            // For example for "items.html" also add "items"
+            for (String suffix : config.suffixes) {
+                if (path.endsWith(suffix)) {
+                    filePaths.add(path.substring(0, path.length() - (suffix.length() + 1)));
+                }
+            }
+        }
+
         for (AnnotationInstance annotation : checkedTemplateAnnotations) {
             if (annotation.target().kind() != Kind.CLASS) {
                 continue;
@@ -293,7 +308,23 @@ List<CheckedTemplateBuildItem> collectTemplateTypeInfo(BeanArchiveIndexBuildItem
                                     templatePath, methodInfo.declaringClass().name(), methodInfo,
                                     checkedTemplateMethod.declaringClass().name(), checkedTemplateMethod));
                 }
-                checkTemplatePath(templatePath, templatePaths, classInfo, methodInfo);
+                if (!filePaths.contains(templatePath)) {
+                    List<String> startsWith = new ArrayList<>();
+                    for (String filePath : filePaths) {
+                        if (filePath.startsWith(templatePath)) {
+                            startsWith.add(filePath);
+                        }
+                    }
+                    if (startsWith.isEmpty()) {
+                        throw new TemplateException(
+                                "No template matching the path " + templatePath + " could be found for: "
+                                        + classInfo.name() + "." + methodInfo.name());
+                    } else {
+                        throw new TemplateException(
+                                startsWith + " match the path " + templatePath
+                                        + " but the file suffix is not configured via the quarkus.qute.suffixes property");
+                    }
+                }
 
                 Map<String, String> bindings = new HashMap<>();
                 List<Type> parameters = methodInfo.parameters();
@@ -318,26 +349,6 @@ List<CheckedTemplateBuildItem> collectTemplateTypeInfo(BeanArchiveIndexBuildItem
         return ret;
     }
 
-    private void checkTemplatePath(String templatePath, List<TemplatePathBuildItem> templatePaths, ClassInfo enclosingClass,
-            MethodInfo methodInfo) {
-        for (TemplatePathBuildItem templatePathBuildItem : templatePaths) {
-            // perfect match
-            if (templatePathBuildItem.getPath().equals(templatePath)) {
-                return;
-            }
-            // if our templatePath is "Foo/hello", make it match "Foo/hello.txt"
-            // if they're not equal and they start with our path, there must be something left after
-            if (templatePathBuildItem.getPath().startsWith(templatePath)
-                    // check that we have an extension, let variant matching work later
-                    && templatePathBuildItem.getPath().charAt(templatePath.length()) == '.') {
-                return;
-            }
-        }
-        throw new TemplateException(
-                "Declared template " + templatePath + " could not be found. Either add it or delete its declaration in "
-                        + enclosingClass.name().toString('.') + "." + methodInfo.name());
-    }
-
     @BuildStep
     TemplatesAnalysisBuildItem analyzeTemplates(List<TemplatePathBuildItem> templatePaths,
             List<CheckedTemplateBuildItem> checkedTemplates, List<MessageBundleMethodBuildItem> messageBundleMethods) {
@@ -1054,7 +1065,8 @@ void validateTemplateInjectionPoints(QuteConfig config, List<TemplatePathBuildIt
                 if (name != null) {
                     // For "@Inject Template items" we try to match "items"
                     // For "@Location("github/pulls") Template pulls" we try to match "github/pulls"
-                    if (filePaths.stream().noneMatch(path -> path.endsWith(name))) {
+                    // For "@Location("foo/bar/baz.txt") Template baz" we try to match "foo/bar/baz.txt"
+                    if (!filePaths.contains(name)) {
                         validationErrors.produce(new ValidationErrorBuildItem(
                                 new TemplateException("No template found for " + injectionPoint.getTargetInfo())));
                     }
diff --git a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MissingTemplateTest.java b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MissingTemplateTest.java
index 99e063bafd4..f6cf3a3dbba 100644
--- a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MissingTemplateTest.java
+++ b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MissingTemplateTest.java
@@ -1,11 +1,13 @@
 package io.quarkus.qute.resteasy.deployment;
 
+import static org.junit.jupiter.api.Assertions.fail;
+
 import org.jboss.shrinkwrap.api.ShrinkWrap;
 import org.jboss.shrinkwrap.api.spec.JavaArchive;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
-import org.wildfly.common.Assert;
 
+import io.quarkus.qute.TemplateException;
 import io.quarkus.test.QuarkusUnitTest;
 
 public class MissingTemplateTest {
@@ -15,13 +17,10 @@ public class MissingTemplateTest {
             .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
                     .addClass(MissingTemplateResource.class)
                     .addAsResource("templates/MissingTemplateResource/hello.txt"))
-            .assertException(t -> {
-                t.printStackTrace();
-                Assert.assertTrue(t.getMessage().contains(
-                        "Declared template MissingTemplateResource/missingTemplate could not be found. Either add it or delete its declaration in io.quarkus.qute.resteasy.deployment.MissingTemplateResource$Templates.missingTemplate"));
-            });
+            .setExpectedException(TemplateException.class);
 
     @Test
     public void emptyTest() {
+        fail();
     }
 }
diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/MissingTemplateTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/MissingTemplateTest.java
index d9f8ad56ebd..c17fda7ce45 100644
--- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/MissingTemplateTest.java
+++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/MissingTemplateTest.java
@@ -1,11 +1,13 @@
 package io.quarkus.resteasy.reactive.qute.deployment;
 
+import static org.junit.jupiter.api.Assertions.fail;
+
 import org.jboss.shrinkwrap.api.ShrinkWrap;
 import org.jboss.shrinkwrap.api.spec.JavaArchive;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
-import org.wildfly.common.Assert;
 
+import io.quarkus.qute.TemplateException;
 import io.quarkus.test.QuarkusUnitTest;
 
 public class MissingTemplateTest {
@@ -15,13 +17,10 @@ public class MissingTemplateTest {
             .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
                     .addClass(MissingTemplateResource.class)
                     .addAsResource("templates/MissingTemplateResource/hello.txt"))
-            .assertException(t -> {
-                t.printStackTrace();
-                Assert.assertTrue(t.getMessage().contains(
-                        "Declared template MissingTemplateResource/missingTemplate could not be found. Either add it or delete its declaration in io.quarkus.resteasy.reactive.qute.deployment.MissingTemplateResource$Templates.missingTemplate"));
-            });
+            .setExpectedException(TemplateException.class);
 
     @Test
     public void emptyTest() {
+        fail();
     }
 }