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