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