1 /*
   2  * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.tools.javac.file;
  27 
  28 import java.io.Closeable;
  29 import java.io.File;
  30 import java.io.FileNotFoundException;
  31 import java.io.IOException;
  32 import java.io.InputStream;
  33 import java.io.UncheckedIOException;
  34 import java.net.URI;
  35 import java.net.URL;
  36 import java.net.URLClassLoader;
  37 import java.nio.file.DirectoryIteratorException;
  38 import java.nio.file.DirectoryStream;
  39 import java.nio.file.FileSystem;
  40 import java.nio.file.FileSystemNotFoundException;
  41 import java.nio.file.FileSystems;
  42 import java.nio.file.Files;
  43 import java.nio.file.InvalidPathException;
  44 import java.nio.file.Path;
  45 import java.nio.file.Paths;
  46 import java.nio.file.ProviderNotFoundException;
  47 import java.nio.file.spi.FileSystemProvider;
  48 import java.util.ArrayList;
  49 import java.util.Arrays;
  50 import java.util.Collection;
  51 import java.util.Collections;
  52 import java.util.EnumMap;
  53 import java.util.EnumSet;
  54 import java.util.HashMap;
  55 import java.util.HashSet;
  56 import java.util.Iterator;
  57 import java.util.LinkedHashMap;
  58 import java.util.LinkedHashSet;
  59 import java.util.List;
  60 import java.util.Map;
  61 import java.util.Objects;
  62 import java.util.NoSuchElementException;
  63 import java.util.Set;
  64 import java.util.function.Predicate;
  65 import java.util.regex.Matcher;
  66 import java.util.regex.Pattern;
  67 import java.util.stream.Collectors;
  68 import java.util.stream.Stream;
  69 import java.util.jar.Attributes;
  70 import java.util.jar.Manifest;
  71 
  72 import javax.lang.model.SourceVersion;
  73 import javax.tools.JavaFileManager;
  74 import javax.tools.JavaFileManager.Location;
  75 import javax.tools.JavaFileObject;
  76 import javax.tools.StandardJavaFileManager;
  77 import javax.tools.StandardJavaFileManager.PathFactory;
  78 import javax.tools.StandardLocation;
  79 
  80 import jdk.internal.jmod.JmodFile;
  81 
  82 import com.sun.tools.javac.main.Option;
  83 import com.sun.tools.javac.resources.CompilerProperties.Errors;
  84 import com.sun.tools.javac.resources.CompilerProperties.LintWarnings;
  85 import com.sun.tools.javac.util.DefinedBy;
  86 import com.sun.tools.javac.util.DefinedBy.Api;
  87 import com.sun.tools.javac.util.ListBuffer;
  88 import com.sun.tools.javac.util.Log;
  89 import com.sun.tools.javac.jvm.ModuleNameReader;
  90 import com.sun.tools.javac.util.Iterators;
  91 import com.sun.tools.javac.util.Pair;
  92 import com.sun.tools.javac.util.StringUtils;
  93 
  94 import static javax.tools.StandardLocation.SYSTEM_MODULES;
  95 import static javax.tools.StandardLocation.PLATFORM_CLASS_PATH;
  96 
  97 import static com.sun.tools.javac.main.Option.BOOT_CLASS_PATH;
  98 import static com.sun.tools.javac.main.Option.ENDORSEDDIRS;
  99 import static com.sun.tools.javac.main.Option.EXTDIRS;
 100 import static com.sun.tools.javac.main.Option.XBOOTCLASSPATH_APPEND;
 101 import static com.sun.tools.javac.main.Option.XBOOTCLASSPATH_PREPEND;
 102 
 103 /**
 104  * This class converts command line arguments, environment variables and system properties (in
 105  * File.pathSeparator-separated String form) into a boot class path, user class path, and source
 106  * path (in {@code Collection<String>} form).
 107  *
 108  * <p>
 109  * <b>This is NOT part of any supported API. If you write code that depends on this, you do so at
 110  * your own risk. This code and its internal interfaces are subject to change or deletion without
 111  * notice.</b>
 112  */
 113 public class Locations {
 114 
 115     /**
 116      * The log to use for warning output
 117      */
 118     private Log log;
 119 
 120     /**
 121      * Access to (possibly cached) file info
 122      */
 123     private FSInfo fsInfo;
 124 
 125     private ModuleNameReader moduleNameReader;
 126 
 127     private PathFactory pathFactory = Paths::get;
 128 
 129     static final Path javaHome = FileSystems.getDefault().getPath(System.getProperty("java.home"));
 130     static final Path thisSystemModules = javaHome.resolve("lib").resolve("modules");
 131 
 132     Map<Path, FileSystem> fileSystems = new LinkedHashMap<>();
 133     List<Closeable> closeables = new ArrayList<>();
 134     private String releaseVersion = null;
 135 
 136     Locations() {
 137         initHandlers();
 138     }
 139 
 140     Path getPath(String first, String... more) {
 141         try {
 142             return pathFactory.getPath(first, more);
 143         } catch (InvalidPathException ipe) {
 144             throw new IllegalArgumentException(ipe);
 145         }
 146     }
 147 
 148     public void close() throws IOException {
 149         ListBuffer<IOException> list = new ListBuffer<>();
 150         closeables.forEach(closeable -> {
 151             try {
 152                 closeable.close();
 153             } catch (IOException ex) {
 154                 list.add(ex);
 155             }
 156         });
 157         if (list.nonEmpty()) {
 158             IOException ex = new IOException();
 159             for (IOException e: list)
 160                 ex.addSuppressed(e);
 161             throw ex;
 162         }
 163     }
 164 
 165     void update(Log log, FSInfo fsInfo) {
 166         this.log = log;
 167         this.fsInfo = fsInfo;
 168     }
 169 
 170     void setPathFactory(PathFactory f) {
 171         pathFactory = f;
 172     }
 173 
 174     boolean isDefaultBootClassPath() {
 175         BootClassPathLocationHandler h
 176                 = (BootClassPathLocationHandler) getHandler(PLATFORM_CLASS_PATH);
 177         return h.isDefault();
 178     }
 179 
 180     boolean isDefaultSystemModulesPath() {
 181         SystemModulesLocationHandler h
 182                 = (SystemModulesLocationHandler) getHandler(SYSTEM_MODULES);
 183         return !h.isExplicit();
 184     }
 185 
 186     /**
 187      * Split a search path into its elements. Empty path elements will be ignored.
 188      *
 189      * @param searchPath The search path to be split
 190      * @return The elements of the path
 191      */
 192     private Iterable<Path> getPathEntries(String searchPath) {
 193         return getPathEntries(searchPath, null);
 194     }
 195 
 196     /**
 197      * Split a search path into its elements. If emptyPathDefault is not null, all empty elements in the
 198      * path, including empty elements at either end of the path, will be replaced with the value of
 199      * emptyPathDefault.
 200      *
 201      * @param searchPath The search path to be split
 202      * @param emptyPathDefault The value to substitute for empty path elements, or null, to ignore
 203      * empty path elements
 204      * @return The elements of the path
 205      */
 206     private Iterable<Path> getPathEntries(String searchPath, Path emptyPathDefault) {
 207         ListBuffer<Path> entries = new ListBuffer<>();
 208         for (String s: searchPath.split(Pattern.quote(File.pathSeparator), -1)) {
 209             if (s.isEmpty()) {
 210                 if (emptyPathDefault != null) {
 211                     entries.add(emptyPathDefault);
 212                 }
 213             } else {
 214                 try {
 215                     entries.add(getPath(s));
 216                 } catch (IllegalArgumentException e) {
 217                     log.warning(LintWarnings.InvalidPath(s));
 218                 }
 219             }
 220         }
 221         return entries;
 222     }
 223 
 224     public void setMultiReleaseValue(String multiReleaseValue) {
 225         // Null is implicitly allowed and unsets the value.
 226         this.releaseVersion = multiReleaseValue;
 227     }
 228 
 229     private boolean contains(Collection<Path> searchPath, Path file) throws IOException {
 230 
 231         if (searchPath == null) {
 232             return false;
 233         }
 234 
 235         Path enclosingJar = null;
 236         if (file.getFileSystem().provider() == fsInfo.getJarFSProvider()) {
 237             URI uri = file.toUri();
 238             if (uri.getScheme().equals("jar")) {
 239                 String ssp = uri.getSchemeSpecificPart();
 240                 int sep = ssp.lastIndexOf("!");
 241                 if (ssp.startsWith("file:") && sep > 0) {
 242                     enclosingJar = Paths.get(URI.create(ssp.substring(0, sep)));
 243                 }
 244             }
 245         }
 246 
 247         Path nf = normalize(file);
 248         for (Path p : searchPath) {
 249             Path np = normalize(p);
 250             if (np.getFileSystem() == nf.getFileSystem()
 251                     && Files.isDirectory(np)
 252                     && nf.startsWith(np)) {
 253                 return true;
 254             }
 255             if (enclosingJar != null
 256                     && Files.isSameFile(enclosingJar, np)) {
 257                 return true;
 258             }
 259         }
 260 
 261         return false;
 262     }
 263 
 264     /**
 265      * Utility class to help evaluate a path option. Duplicate entries are ignored, jar class paths
 266      * can be expanded.
 267      */
 268     private class SearchPath extends LinkedHashSet<Path> {
 269 
 270         private static final long serialVersionUID = 0;
 271 
 272         private boolean expandJarClassPaths = false;
 273         private final transient Set<Path> canonicalValues = new HashSet<>();
 274 
 275         public SearchPath expandJarClassPaths(boolean x) {
 276             expandJarClassPaths = x;
 277             return this;
 278         }
 279 
 280         /**
 281          * What to use when path element is the empty string
 282          */
 283         private transient Path emptyPathDefault = null;
 284 
 285         public SearchPath emptyPathDefault(Path x) {
 286             emptyPathDefault = x;
 287             return this;
 288         }
 289 
 290         public SearchPath addDirectories(String dirs, boolean warn) {
 291             boolean prev = expandJarClassPaths;
 292             expandJarClassPaths = true;
 293             try {
 294                 if (dirs != null) {
 295                     for (Path dir : getPathEntries(dirs)) {
 296                         addDirectory(dir, warn);
 297                     }
 298                 }
 299                 return this;
 300             } finally {
 301                 expandJarClassPaths = prev;
 302             }
 303         }
 304 
 305         public SearchPath addDirectories(String dirs) {
 306             return addDirectories(dirs, true);
 307         }
 308 
 309         private void addDirectory(Path dir, boolean warn) {
 310             if (!Files.isDirectory(dir)) {
 311                 if (warn) {
 312                     log.warning(LintWarnings.DirPathElementNotFound(dir));
 313                 }
 314                 return;
 315             }
 316 
 317             try (Stream<Path> s = Files.list(dir)) {
 318                 s.filter(Locations.this::isArchive)
 319                         .forEach(dirEntry -> addFile(dirEntry, warn));
 320             } catch (IOException ignore) {
 321             }
 322         }
 323 
 324         public SearchPath addFiles(String files, boolean warn) {
 325             if (files != null) {
 326                 addFiles(getPathEntries(files, emptyPathDefault), warn);
 327             }
 328             return this;
 329         }
 330 
 331         public SearchPath addFiles(String files) {
 332             return addFiles(files, true);
 333         }
 334 
 335         public SearchPath addFiles(Iterable<? extends Path> files, boolean warn) {
 336             if (files != null) {
 337                 for (Path file : files) {
 338                     addFile(file, warn);
 339                 }
 340             }
 341             return this;
 342         }
 343 
 344         public SearchPath addFiles(Iterable<? extends Path> files) {
 345             return addFiles(files, true);
 346         }
 347 
 348         public void addFile(Path file, boolean warn) {
 349             if (contains(file)) {
 350                 // discard duplicates
 351                 return;
 352             }
 353 
 354             if (!fsInfo.exists(file)) {
 355                 /* No such file or directory exists */
 356                 if (warn) {
 357                     log.warning(LintWarnings.PathElementNotFound(file));
 358                 }
 359                 super.add(file);
 360                 return;
 361             }
 362 
 363             Path canonFile = fsInfo.getCanonicalFile(file);
 364             if (canonicalValues.contains(canonFile)) {
 365                 /* Discard duplicates and avoid infinite recursion */
 366                 return;
 367             }
 368 
 369             if (fsInfo.isFile(file)) {
 370                 /* File is an ordinary file. */
 371                 if (   !file.getFileName().toString().endsWith(".jmod")
 372                     && !file.endsWith("modules")) {
 373                     if (!isArchive(file)) {
 374                         /* Not a recognized extension; open it to see if
 375                          it looks like a valid zip file. */
 376                         try {
 377                             FileSystems.newFileSystem(file, (ClassLoader)null).close();
 378                             if (warn) {
 379                                 log.warning(LintWarnings.UnexpectedArchiveFile(file));
 380                             }
 381                         } catch (IOException | ProviderNotFoundException e) {
 382                             // FIXME: include e.getLocalizedMessage in warning
 383                             if (warn) {
 384                                 log.warning(LintWarnings.InvalidArchiveFile(file));
 385                             }
 386                             return;
 387                         }
 388                     } else {
 389                         if (fsInfo.getJarFSProvider() == null) {
 390                             log.error(Errors.NoZipfsForArchive(file));
 391                             return ;
 392                         }
 393                     }
 394                 }
 395             }
 396 
 397             /* Now what we have left is either a directory or a file name
 398              conforming to archive naming convention */
 399             super.add(file);
 400             canonicalValues.add(canonFile);
 401 
 402             if (expandJarClassPaths && fsInfo.isFile(file) && !file.endsWith("modules")) {
 403                 addJarClassPath(file, warn);
 404             }
 405         }
 406 
 407         // Adds referenced classpath elements from a jar's Class-Path
 408         // Manifest entry.  In some future release, we may want to
 409         // update this code to recognize URLs rather than simple
 410         // filenames, but if we do, we should redo all path-related code.
 411         private void addJarClassPath(Path jarFile, boolean warn) {
 412             try {
 413                 for (Path f : fsInfo.getJarClassPath(jarFile)) {
 414                     addFile(f, warn);
 415                 }
 416             } catch (IOException e) {
 417                 log.error(Errors.ErrorReadingFile(jarFile, JavacFileManager.getMessage(e)));
 418             }
 419         }
 420     }
 421 
 422     /**
 423      * Base class for handling support for the representation of Locations.
 424      *
 425      * Locations are (by design) opaque handles that can easily be implemented
 426      * by enums like StandardLocation. Within JavacFileManager, each Location
 427      * has an associated LocationHandler, which provides much of the appropriate
 428      * functionality for the corresponding Location.
 429      *
 430      * @see #initHandlers
 431      * @see #getHandler
 432      */
 433     protected abstract static class LocationHandler {
 434 
 435         /**
 436          * @see JavaFileManager#handleOption
 437          */
 438         abstract boolean handleOption(Option option, String value);
 439 
 440         /**
 441          * @see StandardJavaFileManager#hasLocation
 442          */
 443         boolean isSet() {
 444             return (getPaths() != null);
 445         }
 446 
 447         abstract boolean isExplicit();
 448 
 449         /**
 450          * @see StandardJavaFileManager#getLocation
 451          */
 452         abstract Collection<Path> getPaths();
 453 
 454         /**
 455          * @see StandardJavaFileManager#setLocation
 456          */
 457         abstract void setPaths(Iterable<? extends Path> paths) throws IOException;
 458 
 459         /**
 460          * @see StandardJavaFileManager#setLocationForModule
 461          */
 462         abstract void setPathsForModule(String moduleName, Iterable<? extends Path> paths)
 463                 throws IOException;
 464 
 465         /**
 466          * @see JavaFileManager#getLocationForModule(Location, String)
 467          */
 468         Location getLocationForModule(String moduleName) throws IOException {
 469             return null;
 470         }
 471 
 472         /**
 473          * @see JavaFileManager#getLocationForModule(Location, JavaFileObject)
 474          */
 475         Location getLocationForModule(Path file) throws IOException  {
 476             return null;
 477         }
 478 
 479         /**
 480          * @see JavaFileManager#inferModuleName
 481          */
 482         String inferModuleName() {
 483             return null;
 484         }
 485 
 486         /**
 487          * @see JavaFileManager#listLocationsForModules
 488          */
 489         Iterable<Set<Location>> listLocationsForModules() throws IOException {
 490             return null;
 491         }
 492 
 493         /**
 494          * @see JavaFileManager#contains
 495          */
 496         abstract boolean contains(Path file) throws IOException;
 497     }
 498 
 499     /**
 500      * A LocationHandler for a given Location, and associated set of options.
 501      */
 502     private abstract static class BasicLocationHandler extends LocationHandler {
 503 
 504         final Location location;
 505         final Set<Option> options;
 506 
 507         boolean explicit;
 508 
 509         /**
 510          * Create a handler. The location and options provide a way to map from a location or an
 511          * option to the corresponding handler.
 512          *
 513          * @param location the location for which this is the handler
 514          * @param options the options affecting this location
 515          * @see #initHandlers
 516          */
 517         protected BasicLocationHandler(Location location, Option... options) {
 518             this.location = location;
 519             this.options = options.length == 0
 520                     ? EnumSet.noneOf(Option.class)
 521                     : EnumSet.copyOf(Arrays.asList(options));
 522         }
 523 
 524         @Override
 525         void setPathsForModule(String moduleName, Iterable<? extends Path> files) throws IOException {
 526             // should not happen: protected by check in JavacFileManager
 527             throw new UnsupportedOperationException("not supported for " + location);
 528         }
 529 
 530         protected Path checkSingletonDirectory(Iterable<? extends Path> paths) throws IOException {
 531             Iterator<? extends Path> pathIter = paths.iterator();
 532             if (!pathIter.hasNext()) {
 533                 throw new IllegalArgumentException("empty path for directory");
 534             }
 535             Path path = pathIter.next();
 536             if (pathIter.hasNext()) {
 537                 throw new IllegalArgumentException("path too long for directory");
 538             }
 539             checkDirectory(path);
 540             return path;
 541         }
 542 
 543         protected Path checkDirectory(Path path) throws IOException {
 544             Objects.requireNonNull(path);
 545             if (!Files.exists(path)) {
 546                 throw new FileNotFoundException(path + ": does not exist");
 547             }
 548             if (!Files.isDirectory(path)) {
 549                 throw new IOException(path + ": not a directory");
 550             }
 551             return path;
 552         }
 553 
 554         @Override
 555         boolean isExplicit() {
 556             return explicit;
 557         }
 558 
 559     }
 560 
 561     /**
 562      * General purpose implementation for output locations, such as -d/CLASS_OUTPUT and
 563      * -s/SOURCE_OUTPUT. All options are treated as equivalent (i.e. aliases.)
 564      * The value is a single file, possibly null.
 565      */
 566     private class OutputLocationHandler extends BasicLocationHandler {
 567 
 568         private Path outputDir;
 569         private ModuleTable moduleTable;
 570 
 571         OutputLocationHandler(Location location, Option... options) {
 572             super(location, options);
 573         }
 574 
 575         @Override
 576         boolean handleOption(Option option, String value) {
 577             if (!options.contains(option)) {
 578                 return false;
 579             }
 580 
 581             explicit = true;
 582 
 583             // TODO: could/should validate outputDir exists and is a directory
 584             // need to decide how best to report issue for benefit of
 585             // direct API call on JavaFileManager.handleOption(specifies IAE)
 586             // vs. command line decoding.
 587             outputDir = (value == null) ? null : getPath(value);
 588             return true;
 589         }
 590 
 591         @Override
 592         Collection<Path> getPaths() {
 593             return (outputDir == null) ? null : Collections.singleton(outputDir);
 594         }
 595 
 596         @Override
 597         void setPaths(Iterable<? extends Path> paths) throws IOException {
 598             if (paths == null) {
 599                 outputDir = null;
 600             } else {
 601                 explicit = true;
 602                 outputDir = checkSingletonDirectory(paths);
 603             }
 604             moduleTable = null;
 605             listed = false;
 606         }
 607 
 608         @Override
 609         Location getLocationForModule(String name) {
 610             if (moduleTable == null) {
 611                 moduleTable = new ModuleTable();
 612             }
 613             ModuleLocationHandler l = moduleTable.get(name);
 614             if (l == null) {
 615                 Path out = outputDir.resolve(name);
 616                 l = new ModuleLocationHandler(this, location.getName() + "[" + name + "]",
 617                         name, Collections.singletonList(out), true);
 618                 moduleTable.add(l);
 619             }
 620             return l;
 621         }
 622 
 623         @Override
 624         void setPathsForModule(String name, Iterable<? extends Path> paths) throws IOException {
 625             Path out = checkSingletonDirectory(paths);
 626             if (moduleTable == null) {
 627                 moduleTable = new ModuleTable();
 628             }
 629             ModuleLocationHandler l = moduleTable.get(name);
 630             if (l == null) {
 631                 l = new ModuleLocationHandler(this, location.getName() + "[" + name + "]",
 632                         name, Collections.singletonList(out), true);
 633                 moduleTable.add(l);
 634             } else {
 635                 l.searchPath = Collections.singletonList(out);
 636                 moduleTable.updatePaths(l);
 637             }
 638             explicit = true;
 639         }
 640 
 641         @Override
 642         Location getLocationForModule(Path file) {
 643             return (moduleTable == null) ? null : moduleTable.get(file);
 644         }
 645 
 646         private boolean listed;
 647 
 648         @Override
 649         Iterable<Set<Location>> listLocationsForModules() throws IOException {
 650             if (!listed && outputDir != null) {
 651                 try (DirectoryStream<Path> stream = Files.newDirectoryStream(outputDir)) {
 652                     for (Path p : stream) {
 653                         getLocationForModule(p.getFileName().toString());
 654                     }
 655                 }
 656                 listed = true;
 657             }
 658 
 659             if (moduleTable == null || moduleTable.isEmpty())
 660                 return Collections.emptySet();
 661 
 662             return Collections.singleton(moduleTable.locations());
 663         }
 664 
 665         @Override
 666         boolean contains(Path file) throws IOException {
 667             if (moduleTable != null) {
 668                 return moduleTable.contains(file);
 669             } else {
 670                 return (outputDir) != null && normalize(file).startsWith(normalize(outputDir));
 671             }
 672         }
 673     }
 674 
 675     /**
 676      * General purpose implementation for search path locations,
 677      * such as -sourcepath/SOURCE_PATH and -processorPath/ANNOTATION_PROCESSOR_PATH.
 678      * All options are treated as equivalent (i.e. aliases.)
 679      * The value is an ordered set of files and/or directories.
 680      */
 681     private class SimpleLocationHandler extends BasicLocationHandler {
 682 
 683         protected Collection<Path> searchPath;
 684 
 685         SimpleLocationHandler(Location location, Option... options) {
 686             super(location, options);
 687         }
 688 
 689         @Override
 690         boolean handleOption(Option option, String value) {
 691             if (!options.contains(option)) {
 692                 return false;
 693             }
 694 
 695             explicit = true;
 696 
 697             searchPath = value == null ? null
 698                     : Collections.unmodifiableCollection(createPath().addFiles(value));
 699             return true;
 700         }
 701 
 702         @Override
 703         Collection<Path> getPaths() {
 704             return searchPath;
 705         }
 706 
 707         @Override
 708         void setPaths(Iterable<? extends Path> files) {
 709             SearchPath p;
 710             if (files == null) {
 711                 p = computePath(null);
 712             } else {
 713                 explicit = true;
 714                 p = createPath().addFiles(files);
 715             }
 716             searchPath = Collections.unmodifiableCollection(p);
 717         }
 718 
 719         protected SearchPath computePath(String value) {
 720             return createPath().addFiles(value);
 721         }
 722 
 723         protected SearchPath createPath() {
 724             return new SearchPath();
 725         }
 726 
 727         @Override
 728         boolean contains(Path file) throws IOException {
 729             return Locations.this.contains(searchPath, file);
 730         }
 731     }
 732 
 733     /**
 734      * Subtype of SimpleLocationHandler for -classpath/CLASS_PATH.
 735      * If no value is given, a default is provided, based on system properties and other values.
 736      */
 737     private class ClassPathLocationHandler extends SimpleLocationHandler {
 738 
 739         ClassPathLocationHandler() {
 740             super(StandardLocation.CLASS_PATH, Option.CLASS_PATH);
 741         }
 742 
 743         @Override
 744         Collection<Path> getPaths() {
 745             lazy();
 746             return searchPath;
 747         }
 748 
 749         @Override
 750         protected SearchPath computePath(String value) {
 751             String cp = value;
 752 
 753             // CLASSPATH environment variable when run from `javac'.
 754             if (cp == null) {
 755                 cp = System.getProperty("env.class.path");
 756             }
 757 
 758             // If invoked via a java VM (not the javac launcher), use the
 759             // platform class path
 760             if (cp == null && System.getProperty("application.home") == null) {
 761                 cp = System.getProperty("java.class.path");
 762             }
 763 
 764             // Default to current working directory.
 765             if (cp == null) {
 766                 cp = ".";
 767             }
 768 
 769             return createPath().addFiles(cp);
 770         }
 771 
 772         @Override
 773         protected SearchPath createPath() {
 774             return new SearchPath()
 775                     .expandJarClassPaths(true) // Only search user jars for Class-Paths
 776                     .emptyPathDefault(getPath("."));  // Empty path elt ==> current directory
 777         }
 778 
 779         private void lazy() {
 780             if (searchPath == null) {
 781                 setPaths(null);
 782             }
 783         }
 784     }
 785 
 786     /**
 787      * Custom subtype of LocationHandler for PLATFORM_CLASS_PATH.
 788      * Various options are supported for different components of the
 789      * platform class path.
 790      * Setting a value with setLocation overrides all existing option values.
 791      * Setting any option overrides any value set with setLocation, and
 792      * reverts to using default values for options that have not been set.
 793      * Setting -bootclasspath or -Xbootclasspath overrides any existing
 794      * value for -Xbootclasspath/p: and -Xbootclasspath/a:.
 795      */
 796     private class BootClassPathLocationHandler extends BasicLocationHandler {
 797 
 798         private Collection<Path> searchPath;
 799         final Map<Option, String> optionValues = new EnumMap<>(Option.class);
 800 
 801         /**
 802          * Is the bootclasspath the default?
 803          */
 804         private boolean isDefault;
 805 
 806         BootClassPathLocationHandler() {
 807             super(StandardLocation.PLATFORM_CLASS_PATH,
 808                     Option.BOOT_CLASS_PATH, Option.XBOOTCLASSPATH,
 809                     Option.XBOOTCLASSPATH_PREPEND,
 810                     Option.XBOOTCLASSPATH_APPEND,
 811                     Option.ENDORSEDDIRS, Option.DJAVA_ENDORSED_DIRS,
 812                     Option.EXTDIRS, Option.DJAVA_EXT_DIRS);
 813         }
 814 
 815         boolean isDefault() {
 816             lazy();
 817             return isDefault;
 818         }
 819 
 820         @Override
 821         boolean handleOption(Option option, String value) {
 822             if (!options.contains(option)) {
 823                 return false;
 824             }
 825 
 826             explicit = true;
 827 
 828             option = canonicalize(option);
 829             optionValues.put(option, value);
 830             if (option == BOOT_CLASS_PATH) {
 831                 optionValues.remove(XBOOTCLASSPATH_PREPEND);
 832                 optionValues.remove(XBOOTCLASSPATH_APPEND);
 833             }
 834             searchPath = null;  // reset to "uninitialized"
 835             return true;
 836         }
 837         // where
 838         // TODO: would be better if option aliasing was handled at a higher
 839         // level
 840         private Option canonicalize(Option option) {
 841             switch (option) {
 842                 case XBOOTCLASSPATH:
 843                     return Option.BOOT_CLASS_PATH;
 844                 case DJAVA_ENDORSED_DIRS:
 845                     return Option.ENDORSEDDIRS;
 846                 case DJAVA_EXT_DIRS:
 847                     return Option.EXTDIRS;
 848                 default:
 849                     return option;
 850             }
 851         }
 852 
 853         @Override
 854         Collection<Path> getPaths() {
 855             lazy();
 856             return searchPath;
 857         }
 858 
 859         @Override
 860         void setPaths(Iterable<? extends Path> files) {
 861             if (files == null) {
 862                 searchPath = null;  // reset to "uninitialized"
 863             } else {
 864                 isDefault = false;
 865                 explicit = true;
 866                 SearchPath p = new SearchPath().addFiles(files, false);
 867                 searchPath = Collections.unmodifiableCollection(p);
 868                 optionValues.clear();
 869             }
 870         }
 871 
 872         SearchPath computePath() throws IOException {
 873             SearchPath path = new SearchPath();
 874 
 875             String bootclasspathOpt = optionValues.get(BOOT_CLASS_PATH);
 876             String endorseddirsOpt = optionValues.get(ENDORSEDDIRS);
 877             String extdirsOpt = optionValues.get(EXTDIRS);
 878             String xbootclasspathPrependOpt = optionValues.get(XBOOTCLASSPATH_PREPEND);
 879             String xbootclasspathAppendOpt = optionValues.get(XBOOTCLASSPATH_APPEND);
 880             path.addFiles(xbootclasspathPrependOpt);
 881 
 882             if (endorseddirsOpt != null) {
 883                 path.addDirectories(endorseddirsOpt);
 884             } else {
 885                 path.addDirectories(System.getProperty("java.endorsed.dirs"), false);
 886             }
 887 
 888             if (bootclasspathOpt != null) {
 889                 path.addFiles(bootclasspathOpt);
 890             } else {
 891                 // Standard system classes for this compiler's release.
 892                 Collection<Path> systemClasses = systemClasses();
 893                 if (systemClasses != null) {
 894                     path.addFiles(systemClasses, false);
 895                 } else {
 896                     // fallback to the value of sun.boot.class.path
 897                     String files = System.getProperty("sun.boot.class.path");
 898                     path.addFiles(files, false);
 899                 }
 900             }
 901 
 902             path.addFiles(xbootclasspathAppendOpt);
 903 
 904             // Strictly speaking, standard extensions are not bootstrap
 905             // classes, but we treat them identically, so we'll pretend
 906             // that they are.
 907             if (extdirsOpt != null) {
 908                 path.addDirectories(extdirsOpt);
 909             } else {
 910                 // Add lib/jfxrt.jar to the search path
 911                Path jfxrt = javaHome.resolve("lib/jfxrt.jar");
 912                 if (Files.exists(jfxrt)) {
 913                     path.addFile(jfxrt, false);
 914                 }
 915                 path.addDirectories(System.getProperty("java.ext.dirs"), false);
 916             }
 917 
 918             isDefault =
 919                        (xbootclasspathPrependOpt == null)
 920                     && (bootclasspathOpt == null)
 921                     && (xbootclasspathAppendOpt == null);
 922 
 923             return path;
 924         }
 925 
 926         /**
 927          * Return a collection of files containing system classes.
 928          * Returns {@code null} if not running on a modular image.
 929          *
 930          * @throws UncheckedIOException if an I/O errors occurs
 931          */
 932         private Collection<Path> systemClasses() throws IOException {
 933             // Return "modules" jimage file if available
 934             if (Files.isRegularFile(thisSystemModules)) {
 935                 return Collections.singleton(thisSystemModules);
 936             }
 937 
 938             // Exploded module image
 939             Path modules = javaHome.resolve("modules");
 940             if (Files.isDirectory(modules.resolve("java.base"))) {
 941                 try (Stream<Path> listedModules = Files.list(modules)) {
 942                     return listedModules.toList();
 943                 }
 944             }
 945 
 946             // not a modular image that we know about
 947             return null;
 948         }
 949 
 950         private void lazy() {
 951             if (searchPath == null) {
 952                 try {
 953                     searchPath = Collections.unmodifiableCollection(computePath());
 954                 } catch (IOException e) {
 955                     // TODO: need better handling here, e.g. javac Abort?
 956                     throw new UncheckedIOException(e);
 957                 }
 958             }
 959         }
 960 
 961         @Override
 962         boolean contains(Path file) throws IOException {
 963             return Locations.this.contains(searchPath, file);
 964         }
 965     }
 966 
 967     /**
 968      * A LocationHandler to represent modules found from a module-oriented
 969      * location such as MODULE_SOURCE_PATH, UPGRADE_MODULE_PATH,
 970      * SYSTEM_MODULES and MODULE_PATH.
 971      *
 972      * The Location can be specified to accept overriding classes from the
 973      * {@code --patch-module <module>=<path> } parameter.
 974      */
 975     private class ModuleLocationHandler extends LocationHandler implements Location {
 976         private final LocationHandler parent;
 977         private final String name;
 978         private final String moduleName;
 979         private final boolean output;
 980         boolean explicit;
 981         Collection<Path> searchPath;
 982 
 983         ModuleLocationHandler(LocationHandler parent, String name, String moduleName,
 984                 Collection<Path> searchPath, boolean output) {
 985             this.parent = parent;
 986             this.name = name;
 987             this.moduleName = moduleName;
 988             this.searchPath = searchPath;
 989             this.output = output;
 990         }
 991 
 992         @Override @DefinedBy(Api.COMPILER)
 993         public String getName() {
 994             return name;
 995         }
 996 
 997         @Override @DefinedBy(Api.COMPILER)
 998         public boolean isOutputLocation() {
 999             return output;
1000         }
1001 
1002         @Override // defined by LocationHandler
1003         boolean handleOption(Option option, String value) {
1004             throw new UnsupportedOperationException();
1005         }
1006 
1007         @Override // defined by LocationHandler
1008         Collection<Path> getPaths() {
1009             return Collections.unmodifiableCollection(searchPath);
1010         }
1011 
1012         @Override
1013         boolean isExplicit() {
1014             return true;
1015         }
1016 
1017         @Override // defined by LocationHandler
1018         void setPaths(Iterable<? extends Path> paths) throws IOException {
1019             // defer to the parent to determine if this is acceptable
1020             parent.setPathsForModule(moduleName, paths);
1021         }
1022 
1023         @Override // defined by LocationHandler
1024         void setPathsForModule(String moduleName, Iterable<? extends Path> paths) {
1025             throw new UnsupportedOperationException("not supported for " + name);
1026         }
1027 
1028         @Override // defined by LocationHandler
1029         String inferModuleName() {
1030             return moduleName;
1031         }
1032 
1033         @Override
1034         boolean contains(Path file) throws IOException {
1035             return Locations.this.contains(searchPath, file);
1036         }
1037 
1038         @Override
1039         public String toString() {
1040             return name;
1041         }
1042     }
1043 
1044     /**
1045      * A table of module location handlers, indexed by name and path.
1046      */
1047     private class ModuleTable {
1048         private final Map<String, ModuleLocationHandler> nameMap = new LinkedHashMap<>();
1049         private final Map<Path, ModuleLocationHandler> pathMap = new LinkedHashMap<>();
1050 
1051         void add(ModuleLocationHandler h) {
1052             nameMap.put(h.moduleName, h);
1053             for (Path p : h.searchPath) {
1054                 pathMap.put(normalize(p), h);
1055             }
1056         }
1057 
1058         void updatePaths(ModuleLocationHandler h) {
1059             // use iterator, to be able to remove old entries
1060             for (Iterator<Map.Entry<Path, ModuleLocationHandler>> iter = pathMap.entrySet().iterator();
1061                     iter.hasNext(); ) {
1062                 Map.Entry<Path, ModuleLocationHandler> e = iter.next();
1063                 if (e.getValue() == h) {
1064                     iter.remove();
1065                 }
1066             }
1067             for (Path p : h.searchPath) {
1068                 pathMap.put(normalize(p), h);
1069             }
1070         }
1071 
1072         ModuleLocationHandler get(String name) {
1073             return nameMap.get(name);
1074         }
1075 
1076         ModuleLocationHandler get(Path path) {
1077             while (path != null) {
1078                 ModuleLocationHandler l = pathMap.get(path);
1079 
1080                 if (l != null)
1081                     return l;
1082 
1083                 path = path.getParent();
1084             }
1085 
1086             return null;
1087         }
1088 
1089         void clear() {
1090             nameMap.clear();
1091             pathMap.clear();
1092         }
1093 
1094         boolean isEmpty() {
1095             return nameMap.isEmpty();
1096         }
1097 
1098         boolean contains(Path file) throws IOException {
1099             return Locations.this.contains(pathMap.keySet(), file);
1100         }
1101 
1102         Set<Location> locations() {
1103             return Collections.unmodifiableSet(nameMap.values().stream().collect(Collectors.toSet()));
1104         }
1105 
1106         Set<Location> explicitLocations() {
1107             return Collections.unmodifiableSet(nameMap.entrySet()
1108                                                       .stream()
1109                                                       .filter(e -> e.getValue().explicit)
1110                                                       .map(e -> e.getValue())
1111                                                       .collect(Collectors.toSet()));
1112         }
1113     }
1114 
1115     /**
1116      * A LocationHandler for simple module-oriented search paths,
1117      * like UPGRADE_MODULE_PATH and MODULE_PATH.
1118      */
1119     private class ModulePathLocationHandler extends SimpleLocationHandler {
1120         private ModuleTable moduleTable;
1121 
1122         ModulePathLocationHandler(Location location, Option... options) {
1123             super(location, options);
1124         }
1125 
1126         @Override
1127         public boolean handleOption(Option option, String value) {
1128             if (!options.contains(option)) {
1129                 return false;
1130             }
1131             setPaths(value == null ? null : getPathEntries(value));
1132             return true;
1133         }
1134 
1135         @Override
1136         public Location getLocationForModule(String moduleName) {
1137             initModuleLocations();
1138             return moduleTable.get(moduleName);
1139         }
1140 
1141         @Override
1142         public Location getLocationForModule(Path file) {
1143             initModuleLocations();
1144             return moduleTable.get(file);
1145         }
1146 
1147         @Override
1148         Iterable<Set<Location>> listLocationsForModules() {
1149             Set<Location> explicitLocations = moduleTable != null ?
1150                     moduleTable.explicitLocations() : Collections.emptySet();
1151             Iterable<Set<Location>> explicitLocationsList = !explicitLocations.isEmpty()
1152                     ? Collections.singletonList(explicitLocations)
1153                     : Collections.emptyList();
1154 
1155             if (searchPath == null)
1156                 return explicitLocationsList;
1157 
1158             Iterable<Set<Location>> searchPathLocations =
1159                     () -> new ModulePathIterator();
1160             return () -> Iterators.createCompoundIterator(Arrays.asList(explicitLocationsList,
1161                                                                         searchPathLocations),
1162                                                           Iterable::iterator);
1163         }
1164 
1165         @Override
1166         boolean contains(Path file) throws IOException {
1167             if (moduleTable == null) {
1168                 initModuleLocations();
1169             }
1170             return moduleTable.contains(file);
1171         }
1172 
1173         @Override
1174         void setPaths(Iterable<? extends Path> paths) {
1175             if (paths != null) {
1176                 for (Path p: paths) {
1177                     checkValidModulePathEntry(p);
1178                 }
1179             }
1180             super.setPaths(paths);
1181             moduleTable = null;
1182         }
1183 
1184         @Override
1185         void setPathsForModule(String name, Iterable<? extends Path> paths) throws IOException {
1186             List<Path> checkedPaths = checkPaths(paths);
1187             // how far should we go to validate the paths provide a module?
1188             // e.g. contain module-info with the correct name?
1189             initModuleLocations();
1190             ModuleLocationHandler l = moduleTable.get(name);
1191             if (l == null) {
1192                 l = new ModuleLocationHandler(this, location.getName() + "[" + name + "]",
1193                         name, checkedPaths, true);
1194                 moduleTable.add(l);
1195            } else {
1196                 l.searchPath = checkedPaths;
1197                 moduleTable.updatePaths(l);
1198             }
1199             l.explicit = true;
1200             explicit = true;
1201         }
1202 
1203         private List<Path> checkPaths(Iterable<? extends Path> paths) throws IOException {
1204             Objects.requireNonNull(paths);
1205             List<Path> validPaths = new ArrayList<>();
1206             for (Path p : paths) {
1207                 validPaths.add(checkDirectory(p));
1208             }
1209             return validPaths;
1210         }
1211 
1212         private void initModuleLocations() {
1213             if (moduleTable != null) {
1214                 return;
1215             }
1216 
1217             moduleTable = new ModuleTable();
1218 
1219             for (Set<Location> set : listLocationsForModules()) {
1220                 for (Location locn : set) {
1221                     if (locn instanceof ModuleLocationHandler moduleLocationHandler) {
1222                         if (!moduleTable.nameMap.containsKey(moduleLocationHandler.moduleName)) {
1223                             moduleTable.add(moduleLocationHandler);
1224                         }
1225                     }
1226                 }
1227             }
1228         }
1229 
1230         private void checkValidModulePathEntry(Path p) {
1231             if (!Files.exists(p)) {
1232                 // warning may be generated later
1233                 return;
1234             }
1235 
1236             if (Files.isDirectory(p)) {
1237                 // either an exploded module or a directory of modules
1238                 return;
1239             }
1240 
1241             String name = p.getFileName().toString();
1242             int lastDot = name.lastIndexOf(".");
1243             if (lastDot > 0) {
1244                 switch (name.substring(lastDot)) {
1245                     case ".jar":
1246                     case ".jmod":
1247                         return;
1248                 }
1249             }
1250             throw new IllegalArgumentException(p.toString());
1251         }
1252 
1253         class ModulePathIterator implements Iterator<Set<Location>> {
1254             Iterator<Path> pathIter = searchPath.iterator();
1255             int pathIndex = 0;
1256             Set<Location> next = null;
1257 
1258             @Override
1259             public boolean hasNext() {
1260                 if (next != null)
1261                     return true;
1262 
1263                 while (next == null) {
1264                     if (pathIter.hasNext()) {
1265                         Path path = pathIter.next();
1266                         if (Files.isDirectory(path)) {
1267                             next = scanDirectory(path);
1268                         } else {
1269                             next = scanFile(path);
1270                         }
1271                         pathIndex++;
1272                     } else
1273                         return false;
1274                 }
1275                 return true;
1276             }
1277 
1278             @Override
1279             public Set<Location> next() {
1280                 hasNext();
1281                 if (next != null) {
1282                     Set<Location> result = next;
1283                     next = null;
1284                     return result;
1285                 }
1286                 throw new NoSuchElementException();
1287             }
1288 
1289             private Set<Location> scanDirectory(Path path) {
1290                 Set<Path> paths = new LinkedHashSet<>();
1291                 Path moduleInfoClass = null;
1292                 try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
1293                     for (Path entry: stream) {
1294                         if (entry.endsWith("module-info.class")) {
1295                             moduleInfoClass = entry;
1296                             break;  // no need to continue scanning
1297                         }
1298                         paths.add(entry);
1299                     }
1300                 } catch (DirectoryIteratorException | IOException ignore) {
1301                     log.error(Errors.LocnCantReadDirectory(path));
1302                     return Collections.emptySet();
1303                 }
1304 
1305                 if (moduleInfoClass != null) {
1306                     // It's an exploded module directly on the module path.
1307                     // We can't infer module name from the directory name, so have to
1308                     // read module-info.class.
1309                     try {
1310                         String moduleName = readModuleName(moduleInfoClass);
1311                         String name = location.getName()
1312                                 + "[" + pathIndex + ":" + moduleName + "]";
1313                         ModuleLocationHandler l = new ModuleLocationHandler(
1314                                 ModulePathLocationHandler.this, name, moduleName,
1315                                 Collections.singletonList(path), false);
1316                         return Collections.singleton(l);
1317                     } catch (ModuleNameReader.BadClassFile e) {
1318                         log.error(Errors.LocnBadModuleInfo(path));
1319                         return Collections.emptySet();
1320                     } catch (IOException e) {
1321                         log.error(Errors.LocnCantReadFile(path));
1322                         return Collections.emptySet();
1323                     }
1324                 }
1325 
1326                 // A directory of modules
1327                 Set<Location> result = new LinkedHashSet<>();
1328                 int index = 0;
1329                 for (Path entry : paths) {
1330                     Pair<String,Path> module = inferModuleName(entry);
1331                     if (module == null) {
1332                         // diagnostic reported if necessary; skip to next
1333                         continue;
1334                     }
1335                     String moduleName = module.fst;
1336                     Path modulePath = module.snd;
1337                     String name = location.getName()
1338                             + "[" + pathIndex + "." + (index++) + ":" + moduleName + "]";
1339                     ModuleLocationHandler l = new ModuleLocationHandler(
1340                             ModulePathLocationHandler.this, name, moduleName,
1341                             Collections.singletonList(modulePath), false);
1342                     result.add(l);
1343                 }
1344                 return result;
1345             }
1346 
1347             private Set<Location> scanFile(Path path) {
1348                 Pair<String,Path> module = inferModuleName(path);
1349                 if (module == null) {
1350                     // diagnostic reported if necessary
1351                     return Collections.emptySet();
1352                 }
1353                 String moduleName = module.fst;
1354                 Path modulePath = module.snd;
1355                 String name = location.getName()
1356                         + "[" + pathIndex + ":" + moduleName + "]";
1357                 ModuleLocationHandler l = new ModuleLocationHandler(
1358                         ModulePathLocationHandler.this, name, moduleName,
1359                         Collections.singletonList(modulePath), false);
1360                 return Collections.singleton(l);
1361             }
1362 
1363             private Pair<String,Path> inferModuleName(Path p) {
1364                 if (Files.isDirectory(p)) {
1365                     if (Files.exists(p.resolve("module-info.class")) ||
1366                         Files.exists(p.resolve("module-info.sig"))) {
1367                         String name = p.getFileName().toString();
1368                         if (SourceVersion.isName(name))
1369                             return new Pair<>(name, p);
1370                     }
1371                     return null;
1372                 }
1373 
1374                 if (p.getFileName().toString().endsWith(".jar") && fsInfo.exists(p)) {
1375                     FileSystemProvider jarFSProvider = fsInfo.getJarFSProvider();
1376                     if (jarFSProvider == null) {
1377                         log.error(Errors.NoZipfsForArchive(p));
1378                         return null;
1379                     }
1380                     try (FileSystem fs = jarFSProvider.newFileSystem(p, fsInfo.readOnlyJarFSEnv(releaseVersion))) {
1381                         Path moduleInfoClass = fs.getPath("module-info.class");
1382                         if (Files.exists(moduleInfoClass)) {
1383                             String moduleName = readModuleName(moduleInfoClass);
1384                             return new Pair<>(moduleName, p);
1385                         }
1386                         Path mf = fs.getPath("META-INF/MANIFEST.MF");
1387                         if (Files.exists(mf)) {
1388                             try (InputStream in = Files.newInputStream(mf)) {
1389                                 Manifest man = new Manifest(in);
1390                                 Attributes attrs = man.getMainAttributes();
1391                                 if (attrs != null) {
1392                                     String moduleName = attrs.getValue(new Attributes.Name("Automatic-Module-Name"));
1393                                     if (moduleName != null) {
1394                                         if (isModuleName(moduleName)) {
1395                                             return new Pair<>(moduleName, p);
1396                                         } else {
1397                                             log.error(Errors.LocnCantGetModuleNameForJar(p));
1398                                             return null;
1399                                         }
1400                                     }
1401                                 }
1402                             }
1403                         }
1404                     } catch (ModuleNameReader.BadClassFile e) {
1405                         log.error(Errors.LocnBadModuleInfo(p));
1406                         return null;
1407                     } catch (IOException e) {
1408                         log.error(Errors.LocnCantReadFile(p));
1409                         return null;
1410                     }
1411 
1412                     //automatic module:
1413                     String fn = p.getFileName().toString();
1414                     //from ModulePath.deriveModuleDescriptor:
1415 
1416                     // drop .jar
1417                     String mn = fn.substring(0, fn.length()-4);
1418 
1419                     // find first occurrence of -${NUMBER}. or -${NUMBER}$
1420                     Matcher matcher = Pattern.compile("-(\\d+(\\.|$))").matcher(mn);
1421                     if (matcher.find()) {
1422                         int start = matcher.start();
1423 
1424                         mn = mn.substring(0, start);
1425                     }
1426 
1427                     // finally clean up the module name
1428                     mn =  mn.replaceAll("[^A-Za-z0-9]", ".")  // replace non-alphanumeric
1429                             .replaceAll("(\\.)(\\1)+", ".")   // collapse repeating dots
1430                             .replaceAll("^\\.", "")           // drop leading dots
1431                             .replaceAll("\\.$", "");          // drop trailing dots
1432 
1433 
1434                     if (!mn.isEmpty()) {
1435                         return new Pair<>(mn, p);
1436                     }
1437 
1438                     log.error(Errors.LocnCantGetModuleNameForJar(p));
1439                     return null;
1440                 }
1441 
1442                 if (p.getFileName().toString().endsWith(".jmod")) {
1443                     try {
1444                         // check if the JMOD file is valid
1445                         JmodFile.checkMagic(p);
1446 
1447                         // No JMOD file system.  Use JarFileSystem to
1448                         // workaround for now
1449                         FileSystem fs = fileSystems.get(p);
1450                         if (fs == null) {
1451                             FileSystemProvider jarFSProvider = fsInfo.getJarFSProvider();
1452                             if (jarFSProvider == null) {
1453                                 log.error(Errors.LocnCantReadFile(p));
1454                                 return null;
1455                             }
1456                             fs = jarFSProvider.newFileSystem(p, fsInfo.readOnlyJarFSEnv(null));
1457                             try {
1458                                 Path moduleInfoClass = fs.getPath("classes/module-info.class");
1459                                 String moduleName = readModuleName(moduleInfoClass);
1460                                 Path modulePath = fs.getPath("classes");
1461                                 fileSystems.put(p, fs);
1462                                 closeables.add(fs);
1463                                 fs = null; // prevent fs being closed in the finally clause
1464                                 return new Pair<>(moduleName, modulePath);
1465                             } finally {
1466                                 if (fs != null)
1467                                     fs.close();
1468                             }
1469                         }
1470                     } catch (ModuleNameReader.BadClassFile e) {
1471                         log.error(Errors.LocnBadModuleInfo(p));
1472                     } catch (IOException e) {
1473                         log.error(Errors.LocnCantReadFile(p));
1474                         return null;
1475                     }
1476                 }
1477 
1478                 if (false) {  // temp disable, when enabled, massage examples.not-yet.txt suitably.
1479                     log.warning(LintWarnings.LocnUnknownFileOnModulePath(p));
1480                 }
1481                 return null;
1482             }
1483 
1484             private String readModuleName(Path path) throws IOException, ModuleNameReader.BadClassFile {
1485                 if (moduleNameReader == null)
1486                     moduleNameReader = new ModuleNameReader();
1487                 return moduleNameReader.readModuleName(path);
1488             }
1489         }
1490 
1491         //from jdk.internal.module.Checks:
1492         /**
1493          * Returns {@code true} if the given name is a legal module name.
1494          */
1495         private boolean isModuleName(String name) {
1496             int next;
1497             int off = 0;
1498             while ((next = name.indexOf('.', off)) != -1) {
1499                 String id = name.substring(off, next);
1500                 if (!SourceVersion.isName(id))
1501                     return false;
1502                 off = next+1;
1503             }
1504             String last = name.substring(off);
1505             return SourceVersion.isName(last);
1506         }
1507     }
1508 
1509     private class ModuleSourcePathLocationHandler extends BasicLocationHandler {
1510         private ModuleTable moduleTable;
1511         private List<Path> paths;
1512 
1513         ModuleSourcePathLocationHandler() {
1514             super(StandardLocation.MODULE_SOURCE_PATH,
1515                     Option.MODULE_SOURCE_PATH);
1516         }
1517 
1518         @Override
1519         boolean handleOption(Option option, String value) {
1520             explicit = true;
1521             init(value);
1522             return true;
1523         }
1524 
1525         /**
1526          * Initializes the module table, based on a string containing the composition
1527          * of a series of command-line options.
1528          * At most one pattern to initialize a series of modules can be given.
1529          * At most one module-specific search path per module can be given.
1530          *
1531          * @param value a series of values, separated by NUL.
1532          */
1533         void init(String value) {
1534             Pattern moduleSpecificForm = Pattern.compile("([\\p{Alnum}$_.]+)=(.*)");
1535             List<String> pathsForModules = new ArrayList<>();
1536             String modulePattern = null;
1537             for (String v : value.split("\0")) {
1538                 if (moduleSpecificForm.matcher(v).matches()) {
1539                     pathsForModules.add(v);
1540                 } else {
1541                     modulePattern = v;
1542                 }
1543             }
1544             // set the general module pattern first, if given
1545             if (modulePattern != null) {
1546                 initFromPattern(modulePattern);
1547             }
1548             pathsForModules.forEach(this::initForModule);
1549         }
1550 
1551         /**
1552          * Initializes a module-specific override, using {@code setPathsForModule}.
1553          *
1554          * @param value a string of the form: module-name=search-path
1555          */
1556         void initForModule(String value) {
1557             int eq = value.indexOf('=');
1558             String name = value.substring(0, eq);
1559             List<Path> paths = new ArrayList<>();
1560             for (String v : value.substring(eq + 1).split(File.pathSeparator)) {
1561                 try {
1562                     paths.add(Paths.get(v));
1563                 } catch (InvalidPathException e) {
1564                     throw new IllegalArgumentException("invalid path: " + v, e);
1565                 }
1566             }
1567             try {
1568                 setPathsForModule(name, paths);
1569             } catch (IOException e) {
1570                 e.printStackTrace();
1571                 throw new IllegalArgumentException("cannot set path for module " + name, e);
1572             }
1573         }
1574 
1575         /**
1576          * Initializes the module table based on a custom option syntax.
1577          *
1578          * @param value the value such as may be given to a --module-source-path option
1579          */
1580         void initFromPattern(String value) {
1581             Collection<String> segments = new ArrayList<>();
1582             for (String s: value.split(File.pathSeparator)) {
1583                 expandBraces(s, segments);
1584             }
1585 
1586             Map<String, List<Path>> map = new LinkedHashMap<>();
1587             List<Path> noSuffixPaths = new ArrayList<>();
1588             boolean anySuffix = false;
1589             final String MARKER = "*";
1590             for (String seg: segments) {
1591                 int markStart = seg.indexOf(MARKER);
1592                 if (markStart == -1) {
1593                     Path p = getPath(seg);
1594                     add(map, p, null);
1595                     noSuffixPaths.add(p);
1596                 } else {
1597                     if (markStart == 0 || !isSeparator(seg.charAt(markStart - 1))) {
1598                         throw new IllegalArgumentException("illegal use of " + MARKER + " in " + seg);
1599                     }
1600                     Path prefix = getPath(seg.substring(0, markStart - 1));
1601                     Path suffix;
1602                     int markEnd = markStart + MARKER.length();
1603                     if (markEnd == seg.length()) {
1604                         suffix = null;
1605                     } else if (!isSeparator(seg.charAt(markEnd))
1606                             || seg.indexOf(MARKER, markEnd) != -1) {
1607                         throw new IllegalArgumentException("illegal use of " + MARKER + " in " + seg);
1608                     } else {
1609                         suffix = getPath(seg.substring(markEnd + 1));
1610                         anySuffix = true;
1611                     }
1612                     add(map, prefix, suffix);
1613                     if (suffix == null) {
1614                         noSuffixPaths.add(prefix);
1615                     }
1616                 }
1617             }
1618 
1619             initModuleTable(map);
1620             paths = anySuffix ? null : noSuffixPaths;
1621         }
1622 
1623         private void initModuleTable(Map<String, List<Path>> map) {
1624             moduleTable = new ModuleTable();
1625             map.forEach((modName, modPath) -> {
1626                 boolean hasModuleInfo = modPath.stream().anyMatch(checkModuleInfo);
1627                 if (hasModuleInfo) {
1628                     String locnName = location.getName() + "[" + modName + "]";
1629                     ModuleLocationHandler l = new ModuleLocationHandler(this, locnName, modName,
1630                             modPath, false);
1631                     moduleTable.add(l);
1632                 }
1633             });
1634         }
1635         //where:
1636             private final Predicate<Path> checkModuleInfo =
1637                     p -> Files.exists(p.resolve("module-info.java"));
1638 
1639 
1640         private boolean isSeparator(char ch) {
1641             // allow both separators on Windows
1642             return (ch == File.separatorChar) || (ch == '/');
1643         }
1644 
1645         void add(Map<String, List<Path>> map, Path prefix, Path suffix) {
1646             if (!Files.isDirectory(prefix)) {
1647                 log.warning(Files.exists(prefix) ?
1648                     LintWarnings.DirPathElementNotDirectory(prefix) :
1649                     LintWarnings.DirPathElementNotFound(prefix));
1650                 return;
1651             }
1652             try (DirectoryStream<Path> stream = Files.newDirectoryStream(prefix, path -> Files.isDirectory(path))) {
1653                 for (Path entry: stream) {
1654                     Path path = (suffix == null) ? entry : entry.resolve(suffix);
1655                     if (Files.isDirectory(path)) {
1656                         String name = entry.getFileName().toString();
1657                         List<Path> paths = map.get(name);
1658                         if (paths == null)
1659                             map.put(name, paths = new ArrayList<>());
1660                         paths.add(path);
1661                     }
1662                 }
1663             } catch (IOException e) {
1664                 // TODO? What to do?
1665                 System.err.println(e);
1666             }
1667         }
1668 
1669         private void expandBraces(String value, Collection<String> results) {
1670             int depth = 0;
1671             int start = -1;
1672             String prefix = null;
1673             String suffix = null;
1674             for (int i = 0; i < value.length(); i++) {
1675                 switch (value.charAt(i)) {
1676                     case '{':
1677                         depth++;
1678                         if (depth == 1) {
1679                             prefix = value.substring(0, i);
1680                             suffix = value.substring(getMatchingBrace(value, i) + 1);
1681                             start = i + 1;
1682                         }
1683                         break;
1684 
1685                     case ',':
1686                         if (depth == 1) {
1687                             String elem = value.substring(start, i);
1688                             expandBraces(prefix + elem + suffix, results);
1689                             start = i + 1;
1690                         }
1691                         break;
1692 
1693                     case '}':
1694                         switch (depth) {
1695                             case 0:
1696                                 throw new IllegalArgumentException("mismatched braces");
1697 
1698                             case 1:
1699                                 String elem = value.substring(start, i);
1700                                 expandBraces(prefix + elem + suffix, results);
1701                                 return;
1702 
1703                             default:
1704                                 depth--;
1705                         }
1706                         break;
1707                 }
1708             }
1709             if (depth > 0)
1710                 throw new IllegalArgumentException("mismatched braces");
1711             results.add(value);
1712         }
1713 
1714         int getMatchingBrace(String value, int offset) {
1715             int depth = 1;
1716             for (int i = offset + 1; i < value.length(); i++) {
1717                 switch (value.charAt(i)) {
1718                     case '{':
1719                         depth++;
1720                         break;
1721 
1722                     case '}':
1723                         if (--depth == 0)
1724                             return i;
1725                         break;
1726                 }
1727             }
1728             throw new IllegalArgumentException("mismatched braces");
1729         }
1730 
1731         @Override
1732         boolean isSet() {
1733             return (moduleTable != null);
1734         }
1735 
1736         @Override
1737         Collection<Path> getPaths() {
1738             if (paths == null) {
1739                 // This may occur for a complex setting with --module-source-path option
1740                 // i.e. one that cannot be represented by a simple series of paths.
1741                 throw new IllegalStateException("paths not available");
1742             }
1743             return paths;
1744         }
1745 
1746         @Override
1747         void setPaths(Iterable<? extends Path> files) throws IOException {
1748             Map<String, List<Path>> map = new LinkedHashMap<>();
1749             List<Path> newPaths = new ArrayList<>();
1750             for (Path file : files) {
1751                 add(map, file, null);
1752                 newPaths.add(file);
1753             }
1754 
1755             initModuleTable(map);
1756             explicit = true;
1757             paths = Collections.unmodifiableList(newPaths);
1758         }
1759 
1760         @Override
1761         void setPathsForModule(String name, Iterable<? extends Path> paths) throws IOException {
1762             List<Path> validPaths = checkPaths(paths);
1763 
1764             if (moduleTable == null)
1765                 moduleTable = new ModuleTable();
1766 
1767             ModuleLocationHandler l = moduleTable.get(name);
1768             if (l == null) {
1769                 l = new ModuleLocationHandler(this,
1770                         location.getName() + "[" + name + "]",
1771                         name,
1772                         validPaths,
1773                         true);
1774                 moduleTable.add(l);
1775            } else {
1776                 l.searchPath = validPaths;
1777                 moduleTable.updatePaths(l);
1778             }
1779             explicit = true;
1780         }
1781 
1782         private List<Path> checkPaths(Iterable<? extends Path> paths) throws IOException {
1783             Objects.requireNonNull(paths);
1784             List<Path> validPaths = new ArrayList<>();
1785             for (Path p : paths) {
1786                 validPaths.add(checkDirectory(p));
1787             }
1788             return validPaths;
1789         }
1790 
1791         @Override
1792         Location getLocationForModule(String name) {
1793             return (moduleTable == null) ? null : moduleTable.get(name);
1794         }
1795 
1796         @Override
1797         Location getLocationForModule(Path file) {
1798             return (moduleTable == null) ? null : moduleTable.get(file);
1799         }
1800 
1801         @Override
1802         Iterable<Set<Location>> listLocationsForModules() {
1803             if (moduleTable == null)
1804                 return Collections.emptySet();
1805 
1806             return Collections.singleton(moduleTable.locations());
1807         }
1808 
1809         @Override
1810         boolean contains(Path file) throws IOException {
1811             return (moduleTable == null) ? false : moduleTable.contains(file);
1812         }
1813 
1814     }
1815 
1816     private class SystemModulesLocationHandler extends BasicLocationHandler {
1817         private Path systemJavaHome;
1818         private Path modules;
1819         private ModuleTable moduleTable;
1820 
1821         SystemModulesLocationHandler() {
1822             super(StandardLocation.SYSTEM_MODULES, Option.SYSTEM);
1823             systemJavaHome = Locations.javaHome;
1824         }
1825 
1826         @Override
1827         boolean handleOption(Option option, String value) {
1828             if (!options.contains(option)) {
1829                 return false;
1830             }
1831 
1832             explicit = true;
1833 
1834             if (value == null) {
1835                 systemJavaHome = Locations.javaHome;
1836             } else if (value.equals("none")) {
1837                 systemJavaHome = null;
1838             } else {
1839                 update(getPath(value));
1840             }
1841 
1842             modules = null;
1843             return true;
1844         }
1845 
1846         @Override
1847         Collection<Path> getPaths() {
1848             return (systemJavaHome == null) ? null : Collections.singleton(systemJavaHome);
1849         }
1850 
1851         @Override
1852         void setPaths(Iterable<? extends Path> files) throws IOException {
1853             if (files == null) {
1854                 systemJavaHome = null;
1855             } else {
1856                 explicit = true;
1857 
1858                 Path dir = checkSingletonDirectory(files);
1859                 update(dir);
1860             }
1861         }
1862 
1863         @Override
1864         void setPathsForModule(String name, Iterable<? extends Path> paths) throws IOException {
1865             List<Path> checkedPaths = checkPaths(paths);
1866             initSystemModules();
1867             ModuleLocationHandler l = moduleTable.get(name);
1868             if (l == null) {
1869                 l = new ModuleLocationHandler(this,
1870                         location.getName() + "[" + name + "]",
1871                         name,
1872                         checkedPaths,
1873                         true);
1874                 moduleTable.add(l);
1875            } else {
1876                 l.searchPath = checkedPaths;
1877                 moduleTable.updatePaths(l);
1878             }
1879             explicit = true;
1880         }
1881 
1882         private List<Path> checkPaths(Iterable<? extends Path> paths) throws IOException {
1883             Objects.requireNonNull(paths);
1884             List<Path> validPaths = new ArrayList<>();
1885             for (Path p : paths) {
1886                 validPaths.add(checkDirectory(p));
1887             }
1888             return validPaths;
1889         }
1890 
1891         private void update(Path p) {
1892             if (!isCurrentPlatform(p)) {
1893                 var noJavaRuntimeFS = Files.notExists(resolveInJavaHomeLib(p, "jrt-fs.jar"));
1894                 var noModulesFile = Files.notExists(resolveInJavaHomeLib(p, "modules"));
1895                 if (noJavaRuntimeFS || noModulesFile)
1896                     throw new IllegalArgumentException(p.toString());
1897             }
1898             systemJavaHome = p;
1899             modules = null;
1900         }
1901 
1902         private boolean isCurrentPlatform(Path p) {
1903             try {
1904                 return Files.isSameFile(p, Locations.javaHome);
1905             } catch (IOException ex) {
1906                 throw new IllegalArgumentException(p.toString(), ex);
1907             }
1908         }
1909 
1910         private static Path resolveInJavaHomeLib(Path javaHomePath, String name) {
1911             return javaHomePath.resolve("lib").resolve(name);
1912         }
1913 
1914         @Override
1915         Location getLocationForModule(String name) throws IOException {
1916             initSystemModules();
1917             return moduleTable.get(name);
1918         }
1919 
1920         @Override
1921         Location getLocationForModule(Path file) throws IOException {
1922             initSystemModules();
1923             return moduleTable.get(file);
1924         }
1925 
1926         @Override
1927         Iterable<Set<Location>> listLocationsForModules() throws IOException {
1928             initSystemModules();
1929             return Collections.singleton(moduleTable.locations());
1930         }
1931 
1932         @Override
1933         boolean contains(Path file) throws IOException {
1934             initSystemModules();
1935             return moduleTable.contains(file);
1936         }
1937 
1938         private void initSystemModules() throws IOException {
1939             if (moduleTable != null)
1940                 return;
1941 
1942             if (systemJavaHome == null) {
1943                 moduleTable = new ModuleTable();
1944                 return;
1945             }
1946 
1947             if (modules == null) {
1948                 try {
1949                     URI jrtURI = URI.create("jrt:/");
1950                     FileSystem jrtfs;
1951 
1952                     if (isCurrentPlatform(systemJavaHome)) {
1953                         jrtfs = FileSystems.getFileSystem(jrtURI);
1954                     } else {
1955                         try {
1956                             Map<String, String> attrMap =
1957                                     Collections.singletonMap("java.home", systemJavaHome.toString());
1958                             jrtfs = FileSystems.newFileSystem(jrtURI, attrMap);
1959                         } catch (ProviderNotFoundException ex) {
1960                             URL jfsJar = resolveInJavaHomeLib(systemJavaHome, "jrt-fs.jar").toUri().toURL();
1961                             ClassLoader currentLoader = Locations.class.getClassLoader();
1962                             URLClassLoader fsLoader =
1963                                     new URLClassLoader(new URL[] {jfsJar}, currentLoader);
1964 
1965                             jrtfs = FileSystems.newFileSystem(jrtURI, Collections.emptyMap(), fsLoader);
1966 
1967                             closeables.add(fsLoader);
1968                         }
1969 
1970                         closeables.add(jrtfs);
1971                     }
1972 
1973                     modules = jrtfs.getPath("/modules");
1974                 } catch (FileSystemNotFoundException | ProviderNotFoundException e) {
1975                     modules = resolveInJavaHomeLib(systemJavaHome, "modules");
1976                     if (!Files.exists(modules))
1977                         throw new IOException("can't find system classes", e);
1978                 }
1979             }
1980 
1981             moduleTable = new ModuleTable();
1982             try (DirectoryStream<Path> stream = Files.newDirectoryStream(modules, Files::isDirectory)) {
1983                 for (Path entry : stream) {
1984                     String moduleName = entry.getFileName().toString();
1985                     String name = location.getName() + "[" + moduleName + "]";
1986                     ModuleLocationHandler h = new ModuleLocationHandler(this,
1987                             name, moduleName, Collections.singletonList(entry), false);
1988                     moduleTable.add(h);
1989                 }
1990             }
1991         }
1992     }
1993 
1994     private class PatchModulesLocationHandler extends BasicLocationHandler {
1995         private final ModuleTable moduleTable = new ModuleTable();
1996 
1997         PatchModulesLocationHandler() {
1998             super(StandardLocation.PATCH_MODULE_PATH, Option.PATCH_MODULE);
1999         }
2000 
2001         @Override
2002         boolean handleOption(Option option, String value) {
2003             if (!options.contains(option)) {
2004                 return false;
2005             }
2006 
2007             explicit = true;
2008 
2009             moduleTable.clear();
2010 
2011             // Allow an extended syntax for --patch-module consisting of a series
2012             // of values separated by NULL characters. This is to facilitate
2013             // supporting deferred file manager options on the command line.
2014             // See Option.PATCH_MODULE for the code that composes these multiple
2015             // values.
2016             for (String v : value.split("\0")) {
2017                 int eq = v.indexOf('=');
2018                 if (eq > 0) {
2019                     String moduleName = v.substring(0, eq);
2020                     SearchPath mPatchPath = new SearchPath()
2021                             .addFiles(v.substring(eq + 1));
2022                     String name = location.getName() + "[" + moduleName + "]";
2023                     ModuleLocationHandler h = new ModuleLocationHandler(this, name,
2024                             moduleName, mPatchPath, false);
2025                     moduleTable.add(h);
2026                 } else {
2027                     // Should not be able to get here;
2028                     // this should be caught and handled in Option.PATCH_MODULE
2029                     log.error(Errors.LocnInvalidArgForXpatch(value));
2030                 }
2031             }
2032 
2033             return true;
2034         }
2035 
2036         @Override
2037         boolean isSet() {
2038             return !moduleTable.isEmpty();
2039         }
2040 
2041         @Override
2042         Collection<Path> getPaths() {
2043             throw new UnsupportedOperationException();
2044         }
2045 
2046         @Override
2047         void setPaths(Iterable<? extends Path> files) throws IOException {
2048             throw new UnsupportedOperationException();
2049         }
2050 
2051         @Override // defined by LocationHandler
2052         void setPathsForModule(String moduleName, Iterable<? extends Path> files) throws IOException {
2053             throw new UnsupportedOperationException(); // not yet
2054         }
2055 
2056         @Override
2057         Location getLocationForModule(String name) throws IOException {
2058             return moduleTable.get(name);
2059         }
2060 
2061         @Override
2062         Location getLocationForModule(Path file) throws IOException {
2063             return moduleTable.get(file);
2064         }
2065 
2066         @Override
2067         Iterable<Set<Location>> listLocationsForModules() throws IOException {
2068             return Collections.singleton(moduleTable.locations());
2069         }
2070 
2071         @Override
2072         boolean contains(Path file) throws IOException {
2073             return moduleTable.contains(file);
2074         }
2075     }
2076 
2077     Map<Location, LocationHandler> handlersForLocation;
2078     Map<Option, LocationHandler> handlersForOption;
2079 
2080     void initHandlers() {
2081         handlersForLocation = new HashMap<>();
2082         handlersForOption = new EnumMap<>(Option.class);
2083 
2084         BasicLocationHandler[] handlers = {
2085             new BootClassPathLocationHandler(),
2086             new ClassPathLocationHandler(),
2087             new SimpleLocationHandler(StandardLocation.SOURCE_PATH, Option.SOURCE_PATH),
2088             new SimpleLocationHandler(StandardLocation.ANNOTATION_PROCESSOR_PATH, Option.PROCESSOR_PATH),
2089             new SimpleLocationHandler(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, Option.PROCESSOR_MODULE_PATH),
2090             new OutputLocationHandler(StandardLocation.CLASS_OUTPUT, Option.D),
2091             new OutputLocationHandler(StandardLocation.SOURCE_OUTPUT, Option.S),
2092             new OutputLocationHandler(StandardLocation.NATIVE_HEADER_OUTPUT, Option.H),
2093             new ModuleSourcePathLocationHandler(),
2094             new PatchModulesLocationHandler(),
2095             new ModulePathLocationHandler(StandardLocation.UPGRADE_MODULE_PATH, Option.UPGRADE_MODULE_PATH),
2096             new ModulePathLocationHandler(StandardLocation.MODULE_PATH, Option.MODULE_PATH),
2097             new SystemModulesLocationHandler(),
2098         };
2099 
2100         for (BasicLocationHandler h : handlers) {
2101             handlersForLocation.put(h.location, h);
2102             for (Option o : h.options) {
2103                 handlersForOption.put(o, h);
2104             }
2105         }
2106     }
2107 
2108     boolean handleOption(Option option, String value) {
2109         LocationHandler h = handlersForOption.get(option);
2110         return (h == null ? false : h.handleOption(option, value));
2111     }
2112 
2113     boolean hasLocation(Location location) {
2114         LocationHandler h = getHandler(location);
2115         return (h == null ? false : h.isSet());
2116     }
2117 
2118     boolean hasExplicitLocation(Location location) {
2119         LocationHandler h = getHandler(location);
2120         return (h == null ? false : h.isExplicit());
2121     }
2122 
2123     Collection<Path> getLocation(Location location) {
2124         LocationHandler h = getHandler(location);
2125         return (h == null ? null : h.getPaths());
2126     }
2127 
2128     Path getOutputLocation(Location location) {
2129         if (!location.isOutputLocation()) {
2130             throw new IllegalArgumentException();
2131         }
2132         LocationHandler h = getHandler(location);
2133         return ((OutputLocationHandler) h).outputDir;
2134     }
2135 
2136     void setLocation(Location location, Iterable<? extends Path> files) throws IOException {
2137         LocationHandler h = getHandler(location);
2138         if (h == null) {
2139             if (location.isOutputLocation()) {
2140                 h = new OutputLocationHandler(location);
2141             } else {
2142                 h = new SimpleLocationHandler(location);
2143             }
2144             handlersForLocation.put(location, h);
2145         }
2146         h.setPaths(files);
2147     }
2148 
2149     Location getLocationForModule(Location location, String name) throws IOException {
2150         LocationHandler h = getHandler(location);
2151         return (h == null ? null : h.getLocationForModule(name));
2152     }
2153 
2154     Location getLocationForModule(Location location, Path file) throws IOException {
2155         LocationHandler h = getHandler(location);
2156         return (h == null ? null : h.getLocationForModule(file));
2157     }
2158 
2159     void setLocationForModule(Location location, String moduleName,
2160             Iterable<? extends Path> files) throws IOException {
2161         LocationHandler h = getHandler(location);
2162         if (h == null) {
2163             if (location.isOutputLocation()) {
2164                 h = new OutputLocationHandler(location);
2165             } else {
2166                 h = new ModulePathLocationHandler(location);
2167             }
2168             handlersForLocation.put(location, h);
2169         }
2170         h.setPathsForModule(moduleName, files);
2171     }
2172 
2173     String inferModuleName(Location location) {
2174         LocationHandler h = getHandler(location);
2175         return (h == null ? null : h.inferModuleName());
2176     }
2177 
2178     Iterable<Set<Location>> listLocationsForModules(Location location) throws IOException {
2179         LocationHandler h = getHandler(location);
2180         return (h == null ? null : h.listLocationsForModules());
2181     }
2182 
2183     boolean contains(Location location, Path file) throws IOException {
2184         LocationHandler h = getHandler(location);
2185         if (h == null)
2186             throw new IllegalArgumentException("unknown location");
2187         return h.contains(file);
2188     }
2189 
2190     protected LocationHandler getHandler(Location location) {
2191         Objects.requireNonNull(location);
2192         return (location instanceof LocationHandler locationHandler)
2193                 ? locationHandler
2194                 : handlersForLocation.get(location);
2195     }
2196 
2197     /**
2198      * Is this the name of an archive file?
2199      */
2200     private boolean isArchive(Path file) {
2201         String n = StringUtils.toLowerCase(file.getFileName().toString());
2202         return fsInfo.isFile(file)
2203                 && (n.endsWith(".jar") || n.endsWith(".zip"));
2204     }
2205 
2206     static Path normalize(Path p) {
2207         try {
2208             return p.toRealPath();
2209         } catch (IOException e) {
2210             return p.toAbsolutePath().normalize();
2211         }
2212     }
2213 }