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 * <watches> 153 * <watch> 154 * <directory>.some-dir</directory> 155 * <excludes> 156 * <exclude>**/*</exclude> 157 * </excludes> 158 * </watch> 159 * </watches> 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}