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 a7efbbbc4f677..fccb78cd8afb6 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 @@ -1,22 +1,51 @@ package io.quarkus.deployment.builditem; +import java.util.function.Predicate; + import io.quarkus.builder.item.MultiBuildItem; /** - * A file that if modified may result in a hot redeployment when in the dev mode. + * Identifies a file 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 + * {@link Builder#setPredicate(Predicate)}. */ public final class HotDeploymentWatchedFileBuildItem extends MultiBuildItem { + public static Builder builder() { + return new Builder(); + } + private final String location; + private final Predicate predicate; private final boolean restartNeeded; + /** + * + * @param location + * @see #builder() + */ public HotDeploymentWatchedFileBuildItem(String location) { this(location, true); } + /** + * + * @param location + * @param restartNeeded + * @see #builder() + */ public HotDeploymentWatchedFileBuildItem(String location, boolean restartNeeded) { + this(location, null, restartNeeded); + } + + private HotDeploymentWatchedFileBuildItem(String location, Predicate predicate, boolean restartNeeded) { + if (location == null && predicate == null) { + throw new IllegalArgumentException("Either location or predicate must be set"); + } this.location = location; + this.predicate = predicate; this.restartNeeded = restartNeeded; } @@ -24,6 +53,18 @@ public String getLocation() { return location; } + public boolean hasLocation() { + return location != null; + } + + public Predicate getPredicate() { + return predicate; + } + + public boolean hasPredicate() { + return predicate != null; + } + /** * * @return {@code true} if a file change should result in an application restart, {@code false} otherwise @@ -32,4 +73,37 @@ public boolean isRestartNeeded() { return restartNeeded; } + public static class Builder { + + private String location; + private Predicate predicate; + private boolean restartNeeded = true; + + public Builder setLocation(String location) { + if (predicate != null) { + throw new IllegalArgumentException("Predicate already set"); + } + this.location = location; + return this; + } + + public Builder setPredicate(Predicate predicate) { + if (location != null) { + throw new IllegalArgumentException("Location already set"); + } + this.predicate = predicate; + return this; + } + + public Builder setRestartNeeded(boolean restartNeeded) { + this.restartNeeded = restartNeeded; + return this; + } + + public HotDeploymentWatchedFileBuildItem build() { + return new HotDeploymentWatchedFileBuildItem(location, predicate, restartNeeded); + } + + } + } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/HotDeploymentWatchedFileBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/HotDeploymentWatchedFileBuildStep.java index d4b832420f7cb..42019a63fc767 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/HotDeploymentWatchedFileBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/HotDeploymentWatchedFileBuildStep.java @@ -2,6 +2,8 @@ import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Predicate; import java.util.stream.Collectors; import io.quarkus.deployment.annotations.BuildStep; @@ -18,14 +20,21 @@ ServiceStartBuildItem setupWatchedFileHotDeployment(List watchedFilePaths = files.stream() + + Map watchedFilePaths = files.stream().filter(HotDeploymentWatchedFileBuildItem::hasLocation) .collect(Collectors.toMap(HotDeploymentWatchedFileBuildItem::getLocation, HotDeploymentWatchedFileBuildItem::isRestartNeeded, (isRestartNeeded1, isRestartNeeded2) -> isRestartNeeded1 || isRestartNeeded2)); + + List, Boolean>> watchedFilePredicates = files.stream() + .filter(HotDeploymentWatchedFileBuildItem::hasPredicate) + .map(f -> Map.entry(f.getPredicate(), f.isRestartNeeded())) + .collect(Collectors.toUnmodifiableList()); + if (launchModeBuildItem.isAuxiliaryApplication()) { - TestWatchedFiles.setWatchedFilePaths(watchedFilePaths); + TestWatchedFiles.setWatchedFilePaths(watchedFilePaths, watchedFilePredicates); } else { - processor.setWatchedFilePaths(watchedFilePaths, false); + processor.setWatchedFilePaths(watchedFilePaths, watchedFilePredicates, false); } } return null; 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 63dad57889b28..ca34f9df5d3fe 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 @@ -29,6 +29,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -331,11 +332,10 @@ private void periodicTestCompile() { } Set filesChanges = new HashSet<>(checkForFileChange(s -> s.getTest().orElse(null), test)); filesChanges.addAll(checkForFileChange(DevModeContext.ModuleInfo::getMain, test)); - boolean configFileRestartNeeded = filesChanges.stream().map(test.watchedFilePaths::get) - .anyMatch(Boolean.TRUE::equals); + boolean fileRestartNeeded = filesChanges.stream().anyMatch(test::isWatchedFileRestartNeeded); ClassScanResult merged = ClassScanResult.merge(changedTestClassResult, changedApp); - if (configFileRestartNeeded) { + if (fileRestartNeeded) { if (testCompileProblem == null) { testSupport.runTests(null); } @@ -462,21 +462,19 @@ public boolean doScan(boolean userInitiated, boolean forceRestart) { main, false); Set filesChanged = checkForFileChange(DevModeContext.ModuleInfo::getMain, main); - boolean configFileRestartNeeded = forceRestart || filesChanged.stream().map(main.watchedFilePaths::get) - .anyMatch(Boolean.TRUE::equals); + boolean fileRestartNeeded = forceRestart || filesChanged.stream().anyMatch(main::isWatchedFileRestartNeeded); boolean instrumentationChange = false; List changedFilesForRestart = new ArrayList<>(); - if (configFileRestartNeeded) { - changedFilesForRestart - .addAll(filesChanged.stream().filter(fn -> Boolean.TRUE.equals(main.watchedFilePaths.get(fn))) - .map(Paths::get).collect(Collectors.toList())); + if (fileRestartNeeded) { + filesChanged.stream().filter(main::isWatchedFileRestartNeeded).map(Paths::get) + .forEach(changedFilesForRestart::add); } changedFilesForRestart.addAll(changedClassResults.getChangedClasses()); changedFilesForRestart.addAll(changedClassResults.getAddedClasses()); changedFilesForRestart.addAll(changedClassResults.getDeletedClasses()); - if (ClassChangeAgent.getInstrumentation() != null && lastStartIndex != null && !configFileRestartNeeded + if (ClassChangeAgent.getInstrumentation() != null && lastStartIndex != null && !fileRestartNeeded && devModeType != DevModeType.REMOTE_LOCAL_SIDE && instrumentationEnabled()) { //attempt to do an instrumentation based reload //if only code has changed and not the class structure, then we can do a reload @@ -530,7 +528,7 @@ public boolean doScan(boolean userInitiated, boolean forceRestart) { //all broken we just assume the reason that they have refreshed is because they have fixed something //trying to watch all resource files is complex and this is likely a good enough solution for what is already an edge case boolean restartNeeded = !instrumentationChange && (changedClassResults.isChanged() - || (IsolatedDevModeMain.deploymentProblem != null && userInitiated) || configFileRestartNeeded); + || (IsolatedDevModeMain.deploymentProblem != null && userInitiated) || fileRestartNeeded); if (restartNeeded) { String changeString = changedFilesForRestart.stream().map(Path::getFileName).map(Object::toString) .collect(Collectors.joining(", ")); @@ -1083,12 +1081,14 @@ public RuntimeUpdatesProcessor setDisableInstrumentationForIndexPredicate( return this; } - public RuntimeUpdatesProcessor setWatchedFilePaths(Map watchedFilePaths, boolean isTest) { + public RuntimeUpdatesProcessor setWatchedFilePaths(Map watchedFilePaths, + List, Boolean>> watchedFilePredicates, boolean isTest) { if (isTest) { setWatchedFilePathsInternal(watchedFilePaths, test, s -> s.getTest().isPresent() ? asList(s.getTest().get(), s.getMain()) : singletonList(s.getMain())); } else { main.watchedFileTimestamps.clear(); + main.watchedFilePredicates = watchedFilePredicates; setWatchedFilePathsInternal(watchedFilePaths, main, s -> singletonList(s.getMain())); } return this; @@ -1225,6 +1225,7 @@ static class TimestampSet { final Map classFilePathToSourceFilePath = new ConcurrentHashMap<>(); // file path -> isRestartNeeded private volatile Map watchedFilePaths = Collections.emptyMap(); + volatile List, Boolean>> watchedFilePredicates = Collections.emptyList(); public void merge(TimestampSet other) { watchedFileTimestamps.putAll(other.watchedFileTimestamps); @@ -1233,7 +1234,21 @@ public void merge(TimestampSet other) { Map newVal = new HashMap<>(watchedFilePaths); newVal.putAll(other.watchedFilePaths); watchedFilePaths = newVal; + // The list of predicates should be effectively immutable + watchedFilePredicates = other.watchedFilePredicates; + } + boolean isWatchedFileRestartNeeded(String changedFile) { + Boolean ret = watchedFilePaths.get(changedFile); + if (ret == null) { + for (Entry, Boolean> e : watchedFilePredicates) { + if (e.getKey().test(changedFile)) { + ret = e.getValue(); + break; + } + } + } + return ret == null ? false : ret; } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java index b72f5753c298e..a03138e2f7e3a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java @@ -154,7 +154,8 @@ private static Pattern getCompiledPatternOrNull(Optional patternStr) { public void init() { if (moduleRunners.isEmpty()) { - TestWatchedFiles.setWatchedFilesListener((s) -> RuntimeUpdatesProcessor.INSTANCE.setWatchedFilePaths(s, true)); + TestWatchedFiles.setWatchedFilesListener( + (paths, predicates) -> RuntimeUpdatesProcessor.INSTANCE.setWatchedFilePaths(paths, predicates, true)); final Pattern includeModulePattern = getCompiledPatternOrNull(config.includeModulePattern); final Pattern excludeModulePattern = getCompiledPatternOrNull(config.excludeModulePattern); for (var module : context.getAllModules()) { diff --git a/core/devmode-spi/src/main/java/io/quarkus/dev/testing/TestWatchedFiles.java b/core/devmode-spi/src/main/java/io/quarkus/dev/testing/TestWatchedFiles.java index dd42190200d8c..f80c0cec06541 100644 --- a/core/devmode-spi/src/main/java/io/quarkus/dev/testing/TestWatchedFiles.java +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/testing/TestWatchedFiles.java @@ -1,7 +1,10 @@ package io.quarkus.dev.testing; +import java.util.List; import java.util.Map; -import java.util.function.Consumer; +import java.util.Map.Entry; +import java.util.function.BiConsumer; +import java.util.function.Predicate; /** * provides a way for a test run to tell the external application about watched paths. @@ -11,19 +14,23 @@ public class TestWatchedFiles { private static volatile Map watchedFilePaths; - private static volatile Consumer> watchedFilesListener; + private static volatile BiConsumer, List, Boolean>>> watchedFilesListener; + private static volatile List, Boolean>> watchedFilePredicates; - public synchronized static void setWatchedFilePaths(Map watchedFilePaths) { + public synchronized static void setWatchedFilePaths(Map watchedFilePaths, + List, Boolean>> watchedFilePredicates) { TestWatchedFiles.watchedFilePaths = watchedFilePaths; + TestWatchedFiles.watchedFilePredicates = watchedFilePredicates; if (watchedFilesListener != null) { - watchedFilesListener.accept(watchedFilePaths); + watchedFilesListener.accept(watchedFilePaths, watchedFilePredicates); } } - public synchronized static void setWatchedFilesListener(Consumer> watchedFilesListener) { + public synchronized static void setWatchedFilesListener( + BiConsumer, List, Boolean>>> watchedFilesListener) { TestWatchedFiles.watchedFilesListener = watchedFilesListener; if (watchedFilePaths != null) { - watchedFilesListener.accept(watchedFilePaths); + watchedFilesListener.accept(watchedFilePaths, watchedFilePredicates); } } }