001/*
002 * Copyright 2017-2022 original authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.micronaut.maven;
017
018import io.methvin.watcher.DirectoryChangeEvent;
019import io.methvin.watcher.DirectoryWatcher;
020import io.micronaut.maven.aot.AbstractAotAnalysisMojo;
021import io.micronaut.maven.core.MojoUtils;
022import io.micronaut.maven.services.CompilerService;
023import io.micronaut.maven.services.DependencyResolutionService;
024import io.micronaut.maven.services.ExecutorService;
025import io.micronaut.maven.jsonschema.ValidateDevConfigurationMojo;
026import io.micronaut.maven.testresources.AbstractTestResourcesMojo;
027import io.micronaut.maven.testresources.TestResourcesHelper;
028import io.micronaut.testresources.buildtools.ServerSettings;
029import io.micronaut.testresources.buildtools.ServerUtils;
030import org.apache.maven.execution.MavenSession;
031import org.apache.maven.model.Build;
032import org.apache.maven.model.FileSet;
033import org.apache.maven.plugin.BuildPluginManager;
034import org.apache.maven.plugin.MojoExecutionException;
035import org.apache.maven.plugins.annotations.Execute;
036import org.apache.maven.plugins.annotations.LifecyclePhase;
037import org.apache.maven.plugins.annotations.Mojo;
038import org.apache.maven.plugins.annotations.Parameter;
039import org.apache.maven.plugins.annotations.ResolutionScope;
040import org.apache.maven.project.MavenProject;
041import org.apache.maven.project.ProjectBuilder;
042import org.apache.maven.project.ProjectBuildingException;
043import org.apache.maven.project.ProjectBuildingRequest;
044import org.apache.maven.project.ProjectBuildingResult;
045import org.apache.maven.toolchain.ToolchainManager;
046import org.codehaus.plexus.util.AbstractScanner;
047import org.codehaus.plexus.util.cli.CommandLineUtils;
048import org.codehaus.plexus.util.xml.Xpp3Dom;
049import org.eclipse.aether.graph.Dependency;
050import org.eclipse.aether.util.artifact.JavaScopes;
051
052import javax.inject.Inject;
053import java.io.File;
054import java.nio.file.Files;
055import java.nio.file.InvalidPathException;
056import java.nio.file.Path;
057import java.util.ArrayList;
058import java.util.Arrays;
059import java.util.Collections;
060import java.util.List;
061import java.util.Optional;
062import java.util.concurrent.atomic.AtomicBoolean;
063import java.util.concurrent.locks.ReentrantLock;
064import java.util.stream.Collectors;
065
066import static io.micronaut.maven.core.MojoUtils.findJavaExecutable;
067import static io.micronaut.maven.core.MojoUtils.hasMicronautMavenPlugin;
068import static io.micronaut.maven.core.MojoUtils.THIS_PLUGIN;
069import static java.nio.file.Files.isDirectory;
070import static java.nio.file.Files.isReadable;
071import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
072
073/**
074 * <p>Executes a Micronaut application in development mode.</p>
075 *
076 * <p>It watches for changes in the project tree. If there are changes in the {@code pom.xml} file, dependencies will be reloaded. If
077 * the changes are anywhere underneath {@code src/main}, it will recompile the project and restart the application.</p>
078 *
079 * <p>The plugin can handle changes in all the languages supported by Micronaut: Java, Kotlin and Groovy.</p>
080 *
081 * @author Álvaro Sánchez-Mariscal
082 * @since 1.0.0
083 */
084@SuppressWarnings("unused")
085@Mojo(name = "run", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, defaultPhase = LifecyclePhase.PREPARE_PACKAGE, aggregator = true)
086@Execute(phase = LifecyclePhase.PROCESS_CLASSES)
087public class RunMojo extends AbstractTestResourcesMojo {
088
089    public static final String MN_APP_ARGS = "mn.appArgs";
090    public static final String EXEC_MAIN_CLASS = "${exec.mainClass}";
091    public static final String RESOURCES_DIR = "src/main/resources";
092
093    private static final List<String> RELEVANT_SRC_DIRS = List.of("resources", "java", "kotlin", "groovy");
094    private static final int LAST_COMPILATION_THRESHOLD = 500;
095    private static final List<String> DEFAULT_EXCLUDES;
096
097    static {
098        DEFAULT_EXCLUDES = new ArrayList<>();
099        Collections.addAll(DEFAULT_EXCLUDES, AbstractScanner.DEFAULTEXCLUDES);
100        Collections.addAll(DEFAULT_EXCLUDES, "**/.idea/**", "**/src/test/**");
101    }
102
103    private final MavenSession mavenSession;
104    private final ProjectBuilder projectBuilder;
105    private final ToolchainManager toolchainManager;
106    private final String javaExecutable;
107    private final DependencyResolutionService dependencyResolutionService;
108    private final CompilerService compilerService;
109    private final ExecutorService executorService;
110
111    /**
112     * The project's target directory.
113     */
114    private File targetDirectory;
115
116    /**
117     * The main class of the application, as defined in the
118     * <a href="https://www.mojohaus.org/exec-maven-plugin/java-mojo.html#mainClass">Exec Maven Plugin</a>.
119     */
120    @Parameter(defaultValue = EXEC_MAIN_CLASS)
121    private String mainClass;
122
123    /**
124     * Whether to start the Micronaut application in debug mode.
125     */
126    @Parameter(property = "mn.debug", defaultValue = "false")
127    private boolean debug;
128
129    /**
130     * Whether to suspend the execution of the application when running in debug mode.
131     */
132    @Parameter(property = "mn.debug.suspend", defaultValue = "false")
133    private boolean debugSuspend;
134
135    /**
136     * The port where remote debuggers can be attached to.
137     */
138    @Parameter(property = "mn.debug.port", defaultValue = "5005")
139    private int debugPort;
140
141    /**
142     * The host where remote debuggers can connect.
143     */
144    @Parameter(property = "mn.debug.host", defaultValue = "127.0.0.1")
145    private String debugHost;
146
147    /**
148     * List of inclusion/exclusion paths that should not trigger an application restart.
149     * For example, you can exclude a particular directory from being watched by adding the following
150     * configuration:
151     * <pre>
152     *     &lt;watches&gt;
153     *         &lt;watch&gt;
154     *             &lt;directory&gt;.some-dir&lt;/directory&gt;
155     *             &lt;excludes&gt;
156     *                 &lt;exclude&gt;**&#47;*&lt;/exclude&gt;
157     *             &lt;/excludes&gt;
158     *         &lt;/watch&gt;
159     *     &lt;/watches&gt;
160     * </pre>
161     * Check the
162     * <a href="https://maven.apache.org/ref/3.3.9/maven-model/apidocs/org/apache/maven/model/FileSet.html">FileSet</a>
163     * documentation for more details.
164     *
165     * @see <a href="https://maven.apache.org/ref/3.3.9/maven-model/apidocs/org/apache/maven/model/FileSet.html">FileSet</a>
166     */
167    @Parameter
168    private List<FileSet> watches;
169
170    /**
171     * <p>List of additional arguments that will be passed to the JVM process, such as Java agent properties.</p>
172     *
173     * <p>When using the command line, user properties will be passed through, eg: <code>mnv mn:run -Dmicronaut.environments=dev</code>.</p>
174     */
175    @Parameter(property = "mn.jvmArgs")
176    private String jvmArguments;
177
178    /**
179     * List of additional arguments that will be passed to the application, after the class name.
180     */
181    @Parameter(property = MN_APP_ARGS)
182    private String appArguments;
183
184    /**
185     * Whether to watch for changes, or finish the execution after the first run.
186     */
187    @Parameter(property = "mn.watch", defaultValue = "true")
188    private boolean watchForChanges;
189
190    /**
191     * Whether to enable or disable Micronaut AOT.
192     */
193    @Parameter(property = "micronaut.aot.enabled", defaultValue = "false")
194    private boolean aotEnabled;
195
196    // These 2 flags are used in the context of watching for changes
197    // the first one makes sure that only one recompilation is processed at a time
198    private final AtomicBoolean recompileRequested = new AtomicBoolean();
199    // the second one makes sure that we wait for the server to be started before we restart it
200    // otherwise a process may be kept alive
201    private final ReentrantLock restartLock = new ReentrantLock();
202
203    private MavenProject runnableProject;
204    private DirectoryWatcher directoryWatcher;
205    private volatile Process process;
206    private String classpath;
207    private int classpathHash;
208    private long lastCompilation;
209    private TestResourcesHelper testResourcesHelper;
210
211    @SuppressWarnings("CdiInjectionPointsInspection")
212    @Inject
213    public RunMojo(MavenSession mavenSession,
214                   BuildPluginManager pluginManager,
215                   ProjectBuilder projectBuilder,
216                   ToolchainManager toolchainManager,
217                   CompilerService compilerService,
218                   ExecutorService executorService,
219                   DependencyResolutionService dependencyResolutionService) {
220        this.mavenSession = mavenSession;
221        this.projectBuilder = projectBuilder;
222        this.toolchainManager = toolchainManager;
223        this.compilerService = compilerService;
224        this.executorService = executorService;
225        this.javaExecutable = findJavaExecutable(toolchainManager, mavenSession);
226        this.dependencyResolutionService = dependencyResolutionService;
227    }
228
229    @Override
230    public void execute() throws MojoExecutionException {
231        try {
232            initialize();
233        } catch (Exception e) {
234            throw new MojoExecutionException(e.getMessage());
235        }
236
237        try {
238            maybeStartTestResourcesServer();
239            runApplication();
240            Thread shutdownHook = new Thread(this::killProcess);
241            Runtime.getRuntime().addShutdownHook(shutdownHook);
242
243            if (process != null && process.isAlive()) {
244                if (watchForChanges) {
245                    var pathsToWatch = new ArrayList<Path>();
246                    for (FileSet fs : watches) {
247                        var directory = runnableProject.getBasedir().toPath().resolve(fs.getDirectory()).toAbsolutePath();
248                        if (Files.exists(directory)) {
249                            pathsToWatch.add(directory);
250                            //If neither includes nor excludes, add a default include
251                            if ((fs.getIncludes() == null || fs.getIncludes().isEmpty()) && (fs.getExcludes() == null || fs.getExcludes().isEmpty())) {
252                                fs.addInclude("**/*");
253                            }
254                        }
255                    }
256
257                    this.directoryWatcher = DirectoryWatcher
258                        .builder()
259                        .paths(pathsToWatch)
260                        .listener(this::handleEvent)
261                        .build();
262
263                    // We use the working directory as the root path, because
264                    // the top-level project information may not be what we
265                    // expect, in particular if we run from a submodule, or
266                    // that we run from root but with the "-pl" option.
267                    // We can safely do this because it's only used to display
268                    // information about paths being watched to the user, the
269                    // actual paths are unchanged.
270                    Path root = Path.of(".").toAbsolutePath();
271                    List<Path> pathList = pathsToWatch.stream()
272                        .map(root::relativize)
273                        .filter(s -> !s.toString().isEmpty())
274                        .filter(Files::exists)
275                        .sorted()
276                        .toList();
277                    getLog().info("👀 Watching for changes in " + pathList);
278                    this.directoryWatcher.watch();
279                } else if (process != null && process.isAlive()) {
280                    process.waitFor();
281                }
282            }
283        } catch (InterruptedException e) {
284            Thread.currentThread().interrupt();
285        } catch (Exception e) {
286            if (getLog().isDebugEnabled()) {
287                getLog().debug("Exception while watching for changes", e);
288            }
289            throw new MojoExecutionException("Exception while watching for changes", e);
290        } finally {
291            killProcess();
292            cleanup();
293        }
294    }
295
296    protected final void initialize() {
297        final MavenProject currentProject = mavenSession.getCurrentProject();
298        if (hasMicronautMavenPlugin(currentProject)) {
299            runnableProject = currentProject;
300        } else {
301            final List<MavenProject> projectsWithPlugin = mavenSession.getProjects().stream()
302                .filter(MojoUtils::hasMicronautMavenPlugin)
303                .toList();
304            if (projectsWithPlugin.size() == 1) {
305                runnableProject = projectsWithPlugin.get(0);
306                log.info("Running project %s".formatted(runnableProject.getArtifactId()));
307            } else {
308                throw new IllegalStateException("The Micronaut Maven Plugin is declared in the following projects: %s. Please specify the project to run with the -pl option."
309                    .formatted(projectsWithPlugin.stream().map(MavenProject::getArtifactId).toList()));
310            }
311        }
312        this.targetDirectory = new File(runnableProject.getBuild().getDirectory());
313        this.testResourcesHelper = new TestResourcesHelper(testResourcesEnabled, shared, buildDirectory, explicitPort,
314                clientTimeout, serverIdleTimeoutMinutes, runnableProject, mavenSession, dependencyResolutionService,
315                toolchainManager, testResourcesVersion, classpathInference, testResourcesDependencies,
316                sharedServerNamespace, debugServer, false, testResourcesSystemProperties);
317        resolveDependencies();
318        if (watches == null) {
319            watches = new ArrayList<>();
320        }
321        // watch pom.xml file changes
322        mavenSession.getAllProjects().stream()
323            .filter(this::isDependencyOfRunnableProject)
324            .map(MavenProject::getBasedir)
325            .map(File::toPath)
326            .forEach(path -> {
327                var fileSet = new FileSet();
328                fileSet.setDirectory(path.toString());
329                fileSet.addInclude("pom.xml");
330                watches.add(fileSet);
331            });
332        // Add the default watch paths
333        mavenSession.getAllProjects().stream()
334            .filter(this::isDependencyOfRunnableProject)
335            .flatMap(p -> {
336                var basedir = p.getBasedir().toPath();
337                return RELEVANT_SRC_DIRS.stream().map(dir -> basedir.resolve("src/main/" + dir));
338            })
339            .forEach(path -> {
340                var fileSet = new FileSet();
341                fileSet.setDirectory(path.toString());
342                fileSet.addInclude("**/*");
343                watches.add(fileSet);
344            });
345
346        compileProject();
347    }
348
349    private boolean isDependencyOfRunnableProject(MavenProject mavenProject) {
350        return mavenProject.equals(runnableProject) || runnableProject.getDependencies().stream()
351            .anyMatch(d -> d.getGroupId().equals(mavenProject.getGroupId()) && d.getArtifactId().equals(mavenProject.getArtifactId()));
352    }
353
354    protected final void setWatches(List<FileSet> watches) {
355        this.watches = watches;
356    }
357
358    final void handleEvent(DirectoryChangeEvent event) {
359        Path path = event.path();
360        Path parent = path.getParent();
361        Path projectRootDirectory = mavenSession.getTopLevelProject().getBasedir().toPath();
362
363        if (matches(path)) {
364            if (getLog().isInfoEnabled()) {
365                getLog().info(String.format("📝 Detected change in %s. Recompiling/restarting...", projectRootDirectory.relativize(path)));
366            }
367            boolean compiledOk = compileProject();
368            if (compiledOk) {
369                try {
370                    runApplication();
371                } catch (Exception e) {
372                    getLog().error("Unable to run application: " + e.getMessage(), e);
373                }
374            }
375        }
376    }
377
378    private boolean matches(Path path) {
379        // Apply default exclusions
380        if (isDefaultExcluded(path) || isDirectory(path, NOFOLLOW_LINKS) || !isReadable(path) || hasBeenCompiledRecently()) {
381            return false;
382        }
383
384        Path projectRootDirectory = mavenSession.getTopLevelProject().getBasedir().toPath();
385
386        String relativePath = projectRootDirectory.relativize(path).toString();
387
388        boolean matches = false;
389        for (FileSet fileSet : watches) {
390            if (fileSet.getIncludes() != null && !fileSet.getIncludes().isEmpty()) {
391                var directory = new File(fileSet.getDirectory());
392                if (directory.exists() && path.getParent().startsWith(directory.getAbsolutePath())) {
393                    for (String includePattern : fileSet.getIncludes()) {
394                        if (pathMatches(includePattern, path) || patternEquals(path, includePattern, directory)) {
395                            matches = true;
396                            if (getLog().isDebugEnabled()) {
397                                getLog().debug("Path [" + relativePath + "] matched the include pattern [" + includePattern + "] of the directory [" + fileSet.getDirectory() + "]");
398                            }
399                            break;
400                        }
401                    }
402                }
403            }
404            if (matches) {
405                break;
406            }
407        }
408
409        // Finally, process excludes only if the path is matching
410        if (matches) {
411            for (FileSet fileSet : watches) {
412                if (fileSet.getExcludes() != null && !fileSet.getExcludes().isEmpty()) {
413                    File directory = new File(fileSet.getDirectory());
414                    if (directory.exists() && path.getParent().startsWith(directory.getAbsolutePath())) {
415                        for (String excludePattern : fileSet.getExcludes()) {
416                            if (pathMatches(excludePattern, path) || patternEquals(path, excludePattern, directory)) {
417                                matches = false;
418                                if (getLog().isDebugEnabled()) {
419                                    getLog().debug("Path [" + relativePath + "] matched the exclude pattern [" + excludePattern + "] of the directory [" + fileSet.getDirectory() + "]");
420                                }
421                                break;
422                            }
423                        }
424                    }
425                }
426                if (!matches) {
427                    break;
428                }
429            }
430        }
431
432        return matches;
433    }
434
435    private boolean isDefaultExcluded(Path path) {
436        boolean excludeTargetDirectory = true;
437        if (this.watches != null && !this.watches.isEmpty()) {
438            for (FileSet fileSet : this.watches) {
439                if (fileSet.getDirectory().equals(this.targetDirectory.getName())) {
440                    excludeTargetDirectory = false;
441                }
442            }
443        }
444        return (excludeTargetDirectory && path.startsWith(targetDirectory.getAbsolutePath())) ||
445            DEFAULT_EXCLUDES.stream()
446                .anyMatch(excludePattern -> pathMatches(excludePattern, path));
447    }
448
449    private boolean hasBeenCompiledRecently() {
450        return (System.currentTimeMillis() - lastCompilation) < LAST_COMPILATION_THRESHOLD;
451    }
452
453    private void cleanup() {
454        if (getLog().isDebugEnabled()) {
455            getLog().debug("Cleaning up");
456        }
457        try {
458            directoryWatcher.close();
459            maybeStopTestResourcesServer();
460        } catch (Exception e) {
461            // Do nothing
462        }
463    }
464
465    private boolean rebuildMavenProject() {
466        boolean success = true;
467        try {
468            ProjectBuildingRequest projectBuildingRequest = mavenSession.getProjectBuildingRequest();
469            projectBuildingRequest.setResolveDependencies(true);
470            ProjectBuildingResult build = projectBuilder.build(runnableProject.getArtifact(), projectBuildingRequest);
471            MavenProject project = build.getProject();
472            runnableProject = project;
473            mavenSession.setCurrentProject(project);
474        } catch (ProjectBuildingException e) {
475            success = false;
476            if (getLog().isWarnEnabled()) {
477                getLog().warn("Error while trying to build the Maven project model", e);
478            }
479        }
480        return success;
481    }
482
483    private boolean resolveDependencies() {
484        try {
485            List<Dependency> dependencies = compilerService.resolveDependencies(runnableProject, true, JavaScopes.PROVIDED, JavaScopes.COMPILE, JavaScopes.RUNTIME);
486            if (dependencies.isEmpty()) {
487                return false;
488            } else {
489                this.classpath = compilerService.buildClasspath(dependencies);
490                return true;
491            }
492        } finally {
493            if (classpath != null) {
494                this.classpathHash = this.classpath.hashCode();
495            }
496        }
497    }
498
499    private boolean classpathHasChanged() {
500        int oldClasspathHash = this.classpathHash;
501        this.classpathHash = this.classpath.hashCode();
502        return oldClasspathHash != classpathHash;
503
504    }
505
506    /**
507     * Runs or restarts the application. Only visible for testing, shouldn't
508     * be called directly.
509     *
510     * @throws Exception if something goes wrong while starting the application
511     */
512    protected void runApplication() throws Exception {
513        if (restartLock.getQueueLength() >= 1) {
514            // if there's more than one restart request, we'll handle them all at once
515            return;
516        }
517        restartLock.lock();
518        try {
519            // Validate Micronaut configuration for the dev environment before starting.
520            executorService.executeGoal(runnableProject, THIS_PLUGIN, ValidateDevConfigurationMojo.MOJO_NAME, configurationValidationConfiguration());
521            runAotIfNeeded();
522            List<String> args = buildRunArguments();
523
524            if (getLog().isDebugEnabled()) {
525                getLog().debug("Running " + String.join(" ", args));
526            }
527
528            killProcess();
529            process = new ProcessBuilder(args)
530                .inheritIO()
531                .directory(targetDirectory)
532                .start();
533        } finally {
534            restartLock.unlock();
535        }
536    }
537
538    private List<String> buildRunArguments() throws Exception {
539        final String reactorClasses = mavenSession.getAllProjects().stream()
540            .filter(this::isDependencyOfRunnableProject)
541            .map(MavenProject::getBuild)
542            .map(Build::getOutputDirectory)
543            .collect(Collectors.joining(File.pathSeparator));
544        String classpathArgument = String.join(File.pathSeparator, reactorClasses, this.classpath);
545        List<String> translatedJvmArguments = translateArguments(jvmArguments);
546
547        var args = new ArrayList<String>();
548        args.add(javaExecutable);
549        addDebugArguments(args);
550        addTestResourcesArguments(args);
551        args.addAll(translatedJvmArguments);
552        addUserProperties(args);
553        addNativeImageAgentArguments(args, translatedJvmArguments);
554        args.add("-classpath");
555        args.add(classpathArgument);
556        args.add("-XX:TieredStopAtLevel=1");
557        args.add("-Dcom.sun.management.jmxremote");
558        args.add(resolveMainClass());
559        args.addAll(translateArguments(appArguments));
560        return args;
561    }
562
563    private void addDebugArguments(List<String> args) {
564        if (debug) {
565            String suspend = debugSuspend ? "y" : "n";
566            args.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=" + suspend + ",address=" + debugHost + ":" + debugPort);
567        }
568    }
569
570    private void addTestResourcesArguments(List<String> args) {
571        if (testResourcesEnabled) {
572            Path testResourcesSettingsDirectory = shared ? ServerUtils.getDefaultSharedSettingsPath(sharedServerNamespace) :
573                AbstractTestResourcesMojo.serverSettingsDirectoryOf(targetDirectory.toPath());
574            Optional<ServerSettings> serverSettings = ServerUtils.readServerSettings(testResourcesSettingsDirectory);
575            serverSettings.ifPresent(settings -> testResourcesHelper.computeSystemProperties(settings)
576                .forEach((k, v) -> args.add("-D" + k + "=" + v)));
577        }
578    }
579
580    private void addNativeImageAgentArguments(List<String> args, List<String> translatedJvmArguments) throws MojoExecutionException {
581        List<String> nativeImageAgentArguments = NativeImageAgentSupport.computeJvmArguments(mavenSession, runnableProject, targetDirectory, translatedJvmArguments);
582        if (!nativeImageAgentArguments.isEmpty()) {
583            if (watchForChanges) {
584                getLog().warn("Native image agent metadata collection is intended for one-shot runs. Prefer mn:run -Dagent=true -Dmn.watch=false");
585            }
586            args.addAll(nativeImageAgentArguments);
587        }
588    }
589
590    private void addUserProperties(List<String> args) {
591        if (!mavenSession.getUserProperties().isEmpty()) {
592            mavenSession.getUserProperties().forEach((k, v) -> args.add("-D" + k + "=" + v));
593        }
594    }
595
596    private String resolveMainClass() {
597        if (mainClass == null) {
598            mainClass = runnableProject.getProperties().getProperty("exec.mainClass");
599        }
600        return mainClass;
601    }
602
603    private List<String> translateArguments(String arguments) throws Exception {
604        if (arguments == null || arguments.isEmpty()) {
605            return List.of();
606        }
607        return Arrays.asList(CommandLineUtils.translateCommandline(arguments));
608    }
609
610    private void runAotIfNeeded() {
611        if (aotEnabled) {
612            try {
613                executorService.executeGoal(runnableProject, THIS_PLUGIN, AbstractAotAnalysisMojo.NAME);
614            } catch (MojoExecutionException e) {
615                getLog().error(e.getMessage());
616            }
617        }
618    }
619
620    private Xpp3Dom configurationValidationConfiguration() {
621        Xpp3Dom configuration = new Xpp3Dom("configuration");
622        var plugin = runnableProject.getPlugin(THIS_PLUGIN);
623        if (plugin == null) {
624            return configuration;
625        }
626        Object pluginConfiguration = plugin.getConfiguration();
627        if (!(pluginConfiguration instanceof Xpp3Dom pluginDom)) {
628            return configuration;
629        }
630        Xpp3Dom validationConfiguration = pluginDom.getChild("configurationValidation");
631        if (validationConfiguration != null) {
632            configuration.addChild(new Xpp3Dom(validationConfiguration));
633        }
634        return configuration;
635    }
636
637    private void maybeStartTestResourcesServer() throws MojoExecutionException {
638        testResourcesHelper.start();
639    }
640
641    private void maybeStopTestResourcesServer() throws MojoExecutionException {
642        testResourcesHelper.stop(true);
643    }
644
645    private boolean compileProject() {
646        // There can be multiple changes detected at the same time, so we want
647        // to keep only one compilation request
648        if (recompileRequested.get()) {
649            return false;
650        }
651        recompileRequested.set(true);
652        try {
653            return doCompile();
654        } finally {
655            recompileRequested.set(false);
656        }
657    }
658
659    private boolean doCompile() {
660        Optional<Long> lastCompilationMillis = compilerService.compileProject();
661        lastCompilationMillis.ifPresent(lc -> this.lastCompilation = lc);
662        return lastCompilationMillis.isPresent();
663    }
664
665    private void killProcess() {
666        if (process != null && process.isAlive()) {
667            if (getLog().isDebugEnabled()) {
668                getLog().debug("Stopping the background process");
669            }
670            process.destroy();
671            try {
672                process.waitFor();
673            } catch (InterruptedException e) {
674                process.destroyForcibly();
675                Thread.currentThread().interrupt();
676            }
677        }
678    }
679
680    private static String normalize(Path path) {
681        return path.toString().replace('\\', '/');
682    }
683
684    private static boolean pathMatches(String pattern, Path path) {
685        return AbstractScanner.match(pattern, normalize(path));
686    }
687
688    private static boolean patternEquals(Path path, String includePattern, File directory) {
689        try {
690            var testPath = normalize(directory.toPath().resolve(includePattern).toAbsolutePath());
691            return testPath.equals(normalize(path.toAbsolutePath()));
692        } catch (InvalidPathException ex) {
693            return false;
694        }
695    }
696
697}