Skip to content

Commit

Permalink
Qute: dev mode - add config to skip restart for some templates
Browse files Browse the repository at this point in the history
- resolves quarkusio#36692
  • Loading branch information
mkouba committed Oct 30, 2023
1 parent bbdc2ae commit bf706b0
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 8 deletions.
8 changes: 7 additions & 1 deletion docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2632,7 +2632,13 @@ WARNING: Unlike with `@Inject` the templates obtained via `RestTemplate` are not

=== Development Mode

In the development mode, all files located in `src/main/resources/templates` are watched for changes and modifications are immediately visible.
In the development mode, all files located in `src/main/resources/templates` are watched for changes.
By default, a template modification results in an application restart that also triggers build-time validations.

However, it's possible to use the `quarkus.qute.dev-mode.no-restart-templates` configuration property to specify the templates for which the application is not restarted.
The configration value is a regular expression that matches the template path relative from the `templates` directory and `/` is used as a path separator.
For example, `quarkus.qute.dev-mode.no-restart-templates=templates/foo.html` matches the template `src/main/resources/templates/foo.html`.
The matching templates are reloaded and only runtime validations are performed.

[[type-safe-message-bundles]]
=== Type-safe Message Bundles
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3330,8 +3330,11 @@ private static void produceTemplateBuildItems(BuildProducer<TemplatePathBuildIte
String fullPath = basePath + filePath;
LOGGER.debugf("Produce template build items [filePath: %s, fullPath: %s, originalPath: %s", filePath, fullPath,
originalPath);
// NOTE: we cannot just drop the template because a template param can be added
watchedPaths.produce(new HotDeploymentWatchedFileBuildItem(fullPath, true));
boolean restartNeeded = true;
if (config.devMode.noRestartTemplates.isPresent()) {
restartNeeded = !config.devMode.noRestartTemplates.get().matcher(fullPath).matches();
}
watchedPaths.produce(new HotDeploymentWatchedFileBuildItem(fullPath, restartNeeded));
nativeImageResources.produce(new NativeImageResourceBuildItem(fullPath));
templatePaths.produce(
new TemplatePathBuildItem(filePath, originalPath, readTemplateContent(originalPath, config.defaultCharset)));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.quarkus.qute.deployment.devmode;

import java.util.UUID;

import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import io.quarkus.qute.Template;
import io.quarkus.vertx.web.Route;
import io.vertx.ext.web.RoutingContext;

@Singleton
public class NoRestartRoute {

private String id;

@Inject
Template norestart;

@Route(path = "norestart")
public void test(RoutingContext ctx) {
ctx.end(norestart.data("foo", id).render());
}

@PostConstruct
void init() {
id = UUID.randomUUID().toString();
}

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

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

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

import io.quarkus.test.QuarkusDevModeTest;
import io.restassured.response.Response;

public class NoRestartTemplatesDevModeTest {

@RegisterExtension
static final QuarkusDevModeTest config = new QuarkusDevModeTest()
.withApplicationRoot(root -> root
.addClass(NoRestartRoute.class)
.addAsResource(new StringAsset(
"Hello {foo}!"),
"templates/norestart.html")
.addAsResource(new StringAsset(
"quarkus.qute.dev-mode.no-restart-templates=templates/norestart.html"),
"application.properties"));

@Test
public void testNoRestartTemplates() {
Response resp = given().get("norestart");
resp.then()
.statusCode(200);
String val = resp.getBody().asString();
assertTrue(val.startsWith("Hello "));

config.modifyResourceFile("templates/norestart.html", t -> t.concat("!!"));

resp = given().get("norestart");
resp.then().statusCode(200);
assertEquals(val + "!!", resp.getBody().asString());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,10 @@ public class QuteConfig {
@ConfigItem(defaultValue = "UTF-8")
public Charset defaultCharset;

/**
* Dev mode configuration.
*/
@ConfigItem
public QuteDevModeConfig devMode;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.quarkus.qute.runtime;

import java.util.Optional;
import java.util.regex.Pattern;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;

@ConfigGroup
public class QuteDevModeConfig {

/**
* By default, a template modification results in an application restart that triggers build-time validations.
* <p>
* This regular expression can be used to specify the templates for which the application is not restarted.
* I.e. the templates are reloaded and only runtime validations are performed.
* <p>
* The matched input is the template path relative from the {@code templates} directory and the
* {@code /} is used as a path separator. For example, {@code templates/foo.html}.
*/
@ConfigItem
public Optional<Pattern> noRestartTemplates;

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package io.quarkus.qute.runtime;

import java.lang.annotation.Annotation;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -32,6 +35,7 @@
import io.quarkus.qute.TemplateInstanceBase;
import io.quarkus.qute.Variant;
import io.quarkus.qute.runtime.QuteRecorder.QuteContext;
import io.quarkus.runtime.LaunchMode;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;

Expand All @@ -44,7 +48,10 @@ public class TemplateProducer {

private final Map<String, TemplateVariants> templateVariants;

TemplateProducer(Engine engine, QuteContext context, ContentTypes contentTypes) {
// In the dev mode, we need to keep track of injected templates so that we can clear the cached values
private final List<WeakReference<InjectableTemplate>> injectedTemplates;

TemplateProducer(Engine engine, QuteContext context, ContentTypes contentTypes, LaunchMode launchMode) {
this.engine = engine;
Map<String, TemplateVariants> templateVariants = new HashMap<>();
for (Entry<String, List<String>> entry : context.getVariants().entrySet()) {
Expand All @@ -53,6 +60,7 @@ public class TemplateProducer {
templateVariants.put(entry.getKey(), var);
}
this.templateVariants = Collections.unmodifiableMap(templateVariants);
this.injectedTemplates = launchMode == LaunchMode.DEVELOPMENT ? Collections.synchronizedList(new ArrayList<>()) : null;
LOGGER.debugf("Initializing Qute variant templates: %s", templateVariants);
}

Expand All @@ -71,7 +79,7 @@ Template getDefaultTemplate(InjectionPoint injectionPoint) {
LOGGER.warnf("Parameter name not present - using the method name as the template name instead %s", name);
}
}
return new InjectableTemplate(name, templateVariants, engine);
return newInjectableTemplate(name);
}

@Produces
Expand All @@ -87,14 +95,38 @@ Template getTemplate(InjectionPoint injectionPoint) {
if (path == null || path.isEmpty()) {
throw new IllegalStateException("No template location specified");
}
return new InjectableTemplate(path, templateVariants, engine);
return newInjectableTemplate(path);
}

/**
* Used by NativeCheckedTemplateEnhancer to inject calls to this method in the native type-safe methods.
*/
public Template getInjectableTemplate(String path) {
return new InjectableTemplate(path, templateVariants, engine);
return newInjectableTemplate(path);
}

public void clearInjectedTemplates() {
if (injectedTemplates != null) {
synchronized (injectedTemplates) {
for (Iterator<WeakReference<InjectableTemplate>> it = injectedTemplates.iterator(); it.hasNext();) {
WeakReference<InjectableTemplate> ref = it.next();
InjectableTemplate template = ref.get();
if (template == null) {
it.remove();
} else if (template.unambiguousTemplate != null) {
template.unambiguousTemplate.clear();
}
}
}
}
}

private Template newInjectableTemplate(String path) {
InjectableTemplate template = new InjectableTemplate(path, templateVariants, engine);
if (injectedTemplates != null) {
injectedTemplates.add(new WeakReference<>(template));
}
return template;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkus.qute.runtime.devmode;

import java.util.Set;
import java.util.function.Consumer;

import io.quarkus.arc.Arc;
import io.quarkus.dev.spi.HotReplacementContext;
import io.quarkus.dev.spi.HotReplacementSetup;
import io.quarkus.qute.Engine;
import io.quarkus.qute.runtime.TemplateProducer;

public class QuteSetup implements HotReplacementSetup {

@Override
public void setupHotDeployment(HotReplacementContext context) {
context.consumeNoRestartChanges(new Consumer<Set<String>>() {

@Override
public void accept(Set<String> files) {
// Make sure all templates are reloaded
Engine engine = Arc.container().instance(Engine.class).get();
engine.clearTemplates();
TemplateProducer templateProducer = Arc.container().instance(TemplateProducer.class).get();
templateProducer.clearInjectedTemplates();
}
});
}

}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
io.quarkus.qute.runtime.devmode.QuteErrorPageSetup
io.quarkus.qute.runtime.devmode.QuteErrorPageSetup
io.quarkus.qute.runtime.devmode.QuteSetup

0 comments on commit bf706b0

Please sign in to comment.