diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/HotDeploymentWatchedFileBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/HotDeploymentWatchedFileBuildItem.java
index 359c546612e6f..ca9973d05a6c2 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/HotDeploymentWatchedFileBuildItem.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/HotDeploymentWatchedFileBuildItem.java
@@ -9,8 +9,15 @@
* {@link io.quarkus.bootstrap.devmode.DependenciesFilter#getReloadableModules(io.quarkus.bootstrap.model.ApplicationModel)
* reloadable module} that, if modified, may result in a hot redeployment when in the dev mode.
*
- * A file may be identified with an exact location or a matching predicate. See {@link Builder#setLocation(String)} and
+ * A file may be identified with an location or a matching predicate. See {@link Builder#setLocation(String)} and
* {@link Builder#setLocationPredicate(Predicate)}.
+ *
+ * The location may be:
+ *
+ * - a relative file path; e.g. {@code foo/bar.sample}
+ * - an absolute file path; e.g. {@code /home/foo/bar.sample}
+ * - a glob pattern as defined in {@link java.nio.file.FileSystem#getPathMatcher(String)}; e.g. {@code *.sample}
+ *
*
* If multiple build items match the same file then the final value of {@code restartNeeded} is computed as a logical OR of all
* the {@link #isRestartNeeded()} values.
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java
index d256e61e2b500..90efe5480d3f1 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java
@@ -944,9 +944,8 @@ Set checkForFileChange(Function watchedRoots = roots;
if (isAbsolute) {
// absolute files are assumed to be read directly from the project root.
@@ -961,7 +960,7 @@ Set checkForFileChange(Function checkForFileChange(Function existing) {
- ret.add(watchedFilePath);
+ ret.add(file.toString());
//a write can be a 'truncate' + 'write'
//if the file is empty we may be seeing the middle of a write
if (Files.size(file) == 0) {
@@ -1004,7 +1003,7 @@ Set checkForFileChange(Function watchedF
List, Boolean>> watchedFilePredicates, boolean isTest) {
if (isTest) {
setWatchedFilePathsInternal(watchedFilePaths, test,
- s -> s.getTest().isPresent() ? asList(s.getTest().get(), s.getMain()) : singletonList(s.getMain()));
+ s -> s.getTest().isPresent() ? asList(s.getTest().get(), s.getMain()) : singletonList(s.getMain()),
+ watchedFilePredicates);
} else {
main.watchedFileTimestamps.clear();
main.watchedFilePredicates = watchedFilePredicates;
- setWatchedFilePathsInternal(watchedFilePaths, main, s -> singletonList(s.getMain()));
+ setWatchedFilePathsInternal(watchedFilePaths, main, s -> singletonList(s.getMain()), watchedFilePredicates);
}
return this;
}
private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map watchedFilePaths,
- TimestampSet timestamps, Function> cuf) {
+ TimestampSet timestamps, Function> cuf,
+ List, Boolean>> watchedFilePredicates) {
timestamps.watchedFilePaths = watchedFilePaths;
Map extraWatchedFilePaths = new HashMap<>();
+ Set watchedRootPaths = new HashSet<>();
for (DevModeContext.ModuleInfo module : context.getAllModules()) {
List compilationUnits = cuf.apply(module);
for (DevModeContext.CompilationUnit unit : compilationUnits) {
@@ -1110,24 +1112,48 @@ private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map
rootPaths = PathList.of(Path.of(rootPath));
}
for (Path root : rootPaths) {
- for (String path : watchedFilePaths.keySet()) {
- Path config = root.resolve(path);
- if (config.toFile().exists()) {
- try {
- FileTime lastModifiedTime = Files.getLastModifiedTime(config);
- timestamps.watchedFileTimestamps.put(config, lastModifiedTime.toMillis());
- } catch (IOException e) {
- throw new UncheckedIOException(e);
+ // First find all matching paths from the root
+ try (final Stream walk = Files.walk(root)) {
+ walk.forEach(path -> {
+ if (path.equals(root)) {
+ return;
+ }
+ // Use the relative path to match the watched file
+ // /some/more/complex/path/src/main/resources/foo/bar.txt -> foo/bar.txt
+ Path relativePath = root.relativize(path);
+ String relativePathStr = relativePath.toString();
+ if (watchedFilePaths.containsKey(relativePathStr)
+ || watchedFilePredicates.stream().anyMatch(e -> e.getKey().test(relativePathStr))) {
+ log.debugf("Watch %s from: %s", relativePathStr, root);
+ watchedRootPaths.add(relativePathStr);
+ putLastModifiedTime(path, relativePath, timestamps);
+ }
+ });
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+
+ // Then process watched paths that are not matched with a path from a resource root,
+ // for example absolute paths and glob patterns
+ for (String watchedFilePath : watchedFilePaths.keySet()) {
+ Path path = Paths.get(watchedFilePath);
+ if (path.isAbsolute()) {
+ if (Files.exists(path)) {
+ log.debugf("Watch %s", path);
+ putLastModifiedTime(path, path, timestamps);
}
- } else {
- timestamps.watchedFileTimestamps.put(config, 0L);
- Map extraWatchedFileTimestamps = expandGlobPattern(root, config);
- timestamps.watchedFileTimestamps.putAll(extraWatchedFileTimestamps);
- for (Path extraPath : extraWatchedFileTimestamps.keySet()) {
- extraWatchedFilePaths.put(root.relativize(extraPath).toString(),
- timestamps.watchedFilePaths.get(path));
+ } else if (!watchedRootPaths.contains(watchedFilePath) && maybeGlobPattern(watchedFilePath)) {
+ Path resolvedPath = root.resolve(watchedFilePath);
+ Map extraWatchedFileTimestamps = expandGlobPattern(root, resolvedPath, watchedFilePath);
+ if (!extraWatchedFileTimestamps.isEmpty()) {
+ timestamps.watchedFileTimestamps.put(resolvedPath, 0L);
+ timestamps.watchedFileTimestamps.putAll(extraWatchedFileTimestamps);
+ for (Path extraPath : extraWatchedFileTimestamps.keySet()) {
+ extraWatchedFilePaths.put(extraPath.toString(),
+ timestamps.watchedFilePaths.get(watchedFilePath));
+ }
+ timestamps.watchedFileTimestamps.putAll(extraWatchedFileTimestamps);
}
- timestamps.watchedFileTimestamps.putAll(extraWatchedFileTimestamps);
}
}
}
@@ -1137,6 +1163,19 @@ private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map
return this;
}
+ private boolean maybeGlobPattern(String path) {
+ return path.contains("*") || path.contains("?");
+ }
+
+ private void putLastModifiedTime(Path filePath, Path keyPath, TimestampSet timestamps) {
+ try {
+ FileTime lastModifiedTime = Files.getLastModifiedTime(filePath);
+ timestamps.watchedFileTimestamps.put(keyPath, lastModifiedTime.toMillis());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
public void addHotReplacementSetup(HotReplacementSetup service) {
hotReplacementSetup.add(service);
}
@@ -1171,15 +1210,17 @@ public void close() throws IOException {
}
}
- private Map expandGlobPattern(Path root, Path configFile) {
- PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + configFile.toString());
+ private Map expandGlobPattern(Path root, Path path, String pattern) {
+ PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + path.toString());
Map files = new HashMap<>();
try {
Files.walkFileTree(root, new SimpleFileVisitor() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (pathMatcher.matches(file)) {
- files.put(file, attrs.lastModifiedTime().toMillis());
+ Path relativePath = root.relativize(file);
+ log.debugf("Glob pattern [%s] matched %s from %s", pattern, relativePath, root);
+ files.put(relativePath, attrs.lastModifiedTime().toMillis());
}
return FileVisitResult.CONTINUE;
}
diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AdditionalWatchedResourcesDevModeTest.java b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AdditionalWatchedResourcesDevModeTest.java
index 9bd310ec7badf..2f4a3d23c2c0a 100644
--- a/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AdditionalWatchedResourcesDevModeTest.java
+++ b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AdditionalWatchedResourcesDevModeTest.java
@@ -74,6 +74,10 @@ public void globWatch() {
TEST.modifyResourceFile(SAMPLE_FILE, oldSource -> MODIFIED);
RestAssured.get("/content/{name}", SAMPLE_FILE).then().body(is(MODIFIED));
+
+ TEST.modifyResourceFile(SAMPLE_FILE, oldSource -> INITIAL);
+
+ RestAssured.get("/content/{name}", SAMPLE_FILE).then().body(is(INITIAL));
}
@Test