Locations.java revision 3392:04fcbc7234a4
1/*
2 * Copyright (c) 2003, 2016, 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
26package com.sun.tools.javac.file;
27
28import java.io.Closeable;
29import java.io.File;
30import java.io.FileNotFoundException;
31import java.io.IOException;
32import java.io.UncheckedIOException;
33import java.net.URI;
34import java.net.URL;
35import java.net.URLClassLoader;
36import java.nio.file.DirectoryIteratorException;
37import java.nio.file.DirectoryStream;
38import java.nio.file.FileSystem;
39import java.nio.file.FileSystemNotFoundException;
40import java.nio.file.FileSystems;
41import java.nio.file.Files;
42import java.nio.file.Path;
43import java.nio.file.Paths;
44import java.nio.file.ProviderNotFoundException;
45import java.util.ArrayList;
46import java.util.Arrays;
47import java.util.Collection;
48import java.util.Collections;
49import java.util.EnumMap;
50import java.util.EnumSet;
51import java.util.HashMap;
52import java.util.HashSet;
53import java.util.Iterator;
54import java.util.LinkedHashMap;
55import java.util.LinkedHashSet;
56import java.util.List;
57import java.util.Map;
58import java.util.Objects;
59import java.util.NoSuchElementException;
60import java.util.Set;
61import java.util.regex.Matcher;
62import java.util.regex.Pattern;
63import java.util.stream.Collectors;
64import java.util.stream.Stream;
65import java.util.zip.ZipFile;
66
67import javax.lang.model.SourceVersion;
68import javax.tools.JavaFileManager;
69import javax.tools.JavaFileManager.Location;
70import javax.tools.StandardJavaFileManager;
71import javax.tools.StandardJavaFileManager.PathFactory;
72import javax.tools.StandardLocation;
73
74import com.sun.tools.javac.code.Lint;
75import com.sun.tools.javac.main.Option;
76import com.sun.tools.javac.resources.CompilerProperties.Errors;
77import com.sun.tools.javac.resources.CompilerProperties.Warnings;
78import com.sun.tools.javac.util.DefinedBy;
79import com.sun.tools.javac.util.DefinedBy.Api;
80import com.sun.tools.javac.util.ListBuffer;
81import com.sun.tools.javac.util.Log;
82import com.sun.tools.javac.util.Pair;
83import com.sun.tools.javac.util.StringUtils;
84
85import static javax.tools.StandardLocation.PLATFORM_CLASS_PATH;
86
87import static com.sun.tools.javac.main.Option.BOOTCLASSPATH;
88import static com.sun.tools.javac.main.Option.DJAVA_ENDORSED_DIRS;
89import static com.sun.tools.javac.main.Option.DJAVA_EXT_DIRS;
90import static com.sun.tools.javac.main.Option.ENDORSEDDIRS;
91import static com.sun.tools.javac.main.Option.EXTDIRS;
92import static com.sun.tools.javac.main.Option.XBOOTCLASSPATH;
93import static com.sun.tools.javac.main.Option.XBOOTCLASSPATH_APPEND;
94import static com.sun.tools.javac.main.Option.XBOOTCLASSPATH_PREPEND;
95
96/**
97 * This class converts command line arguments, environment variables and system properties (in
98 * File.pathSeparator-separated String form) into a boot class path, user class path, and source
99 * path (in {@code Collection<String>} form).
100 *
101 * <p>
102 * <b>This is NOT part of any supported API. If you write code that depends on this, you do so at
103 * your own risk. This code and its internal interfaces are subject to change or deletion without
104 * notice.</b>
105 */
106public class Locations {
107
108    /**
109     * The log to use for warning output
110     */
111    private Log log;
112
113    /**
114     * Access to (possibly cached) file info
115     */
116    private FSInfo fsInfo;
117
118    /**
119     * Whether to warn about non-existent path elements
120     */
121    private boolean warn;
122
123    private ModuleNameReader moduleNameReader;
124
125    private PathFactory pathFactory = Paths::get;
126
127    static final Path javaHome = FileSystems.getDefault().getPath(System.getProperty("java.home"));
128    static final Path thisSystemModules = javaHome.resolve("lib").resolve("modules");
129
130    Map<Path, FileSystem> fileSystems = new LinkedHashMap<>();
131    List<Closeable> closeables = new ArrayList<>();
132
133    Locations() {
134        initHandlers();
135    }
136
137    Path getPath(String first, String... more) {
138        return pathFactory.getPath(first, more);
139    }
140
141    public void close() throws IOException {
142        ListBuffer<IOException> list = new ListBuffer<>();
143        closeables.forEach(closeable -> {
144            try {
145                closeable.close();
146            } catch (IOException ex) {
147                list.add(ex);
148            }
149        });
150        if (list.nonEmpty()) {
151            IOException ex = new IOException();
152            for (IOException e: list)
153                ex.addSuppressed(e);
154            throw ex;
155        }
156    }
157
158    // could replace Lint by "boolean warn"
159    void update(Log log, Lint lint, FSInfo fsInfo) {
160        this.log = log;
161        warn = lint.isEnabled(Lint.LintCategory.PATH);
162        this.fsInfo = fsInfo;
163    }
164
165    void setPathFactory(PathFactory f) {
166        pathFactory = f;
167    }
168
169    boolean isDefaultBootClassPath() {
170        BootClassPathLocationHandler h
171                = (BootClassPathLocationHandler) getHandler(PLATFORM_CLASS_PATH);
172        return h.isDefault();
173    }
174
175    /**
176     * Split a search path into its elements. Empty path elements will be ignored.
177     *
178     * @param searchPath The search path to be split
179     * @return The elements of the path
180     */
181    private Iterable<Path> getPathEntries(String searchPath) {
182        return getPathEntries(searchPath, null);
183    }
184
185    /**
186     * Split a search path into its elements. If emptyPathDefault is not null, all empty elements in the
187     * path, including empty elements at either end of the path, will be replaced with the value of
188     * emptyPathDefault.
189     *
190     * @param searchPath The search path to be split
191     * @param emptyPathDefault The value to substitute for empty path elements, or null, to ignore
192     * empty path elements
193     * @return The elements of the path
194     */
195    private Iterable<Path> getPathEntries(String searchPath, Path emptyPathDefault) {
196        ListBuffer<Path> entries = new ListBuffer<>();
197        for (String s: searchPath.split(Pattern.quote(File.pathSeparator), -1)) {
198            if (s.isEmpty()) {
199                if (emptyPathDefault != null) {
200                    entries.add(emptyPathDefault);
201                }
202            } else {
203                entries.add(getPath(s));
204            }
205        }
206        return entries;
207    }
208
209    /**
210     * Utility class to help evaluate a path option. Duplicate entries are ignored, jar class paths
211     * can be expanded.
212     */
213    private class SearchPath extends LinkedHashSet<Path> {
214
215        private static final long serialVersionUID = 0;
216
217        private boolean expandJarClassPaths = false;
218        private final Set<Path> canonicalValues = new HashSet<>();
219
220        public SearchPath expandJarClassPaths(boolean x) {
221            expandJarClassPaths = x;
222            return this;
223        }
224
225        /**
226         * What to use when path element is the empty string
227         */
228        private Path emptyPathDefault = null;
229
230        public SearchPath emptyPathDefault(Path x) {
231            emptyPathDefault = x;
232            return this;
233        }
234
235        public SearchPath addDirectories(String dirs, boolean warn) {
236            boolean prev = expandJarClassPaths;
237            expandJarClassPaths = true;
238            try {
239                if (dirs != null) {
240                    for (Path dir : getPathEntries(dirs)) {
241                        addDirectory(dir, warn);
242                    }
243                }
244                return this;
245            } finally {
246                expandJarClassPaths = prev;
247            }
248        }
249
250        public SearchPath addDirectories(String dirs) {
251            return addDirectories(dirs, warn);
252        }
253
254        private void addDirectory(Path dir, boolean warn) {
255            if (!Files.isDirectory(dir)) {
256                if (warn) {
257                    log.warning(Lint.LintCategory.PATH,
258                            "dir.path.element.not.found", dir);
259                }
260                return;
261            }
262
263            try (Stream<Path> s = Files.list(dir)) {
264                s.filter(dirEntry -> isArchive(dirEntry))
265                        .forEach(dirEntry -> addFile(dirEntry, warn));
266            } catch (IOException ignore) {
267            }
268        }
269
270        public SearchPath addFiles(String files, boolean warn) {
271            if (files != null) {
272                addFiles(getPathEntries(files, emptyPathDefault), warn);
273            }
274            return this;
275        }
276
277        public SearchPath addFiles(String files) {
278            return addFiles(files, warn);
279        }
280
281        public SearchPath addFiles(Iterable<? extends Path> files, boolean warn) {
282            if (files != null) {
283                for (Path file : files) {
284                    addFile(file, warn);
285                }
286            }
287            return this;
288        }
289
290        public SearchPath addFiles(Iterable<? extends Path> files) {
291            return addFiles(files, warn);
292        }
293
294        public void addFile(Path file, boolean warn) {
295            if (contains(file)) {
296                // discard duplicates
297                return;
298            }
299
300            if (!fsInfo.exists(file)) {
301                /* No such file or directory exists */
302                if (warn) {
303                    log.warning(Lint.LintCategory.PATH,
304                            "path.element.not.found", file);
305                }
306                super.add(file);
307                return;
308            }
309
310            Path canonFile = fsInfo.getCanonicalFile(file);
311            if (canonicalValues.contains(canonFile)) {
312                /* Discard duplicates and avoid infinite recursion */
313                return;
314            }
315
316            if (fsInfo.isFile(file)) {
317                /* File is an ordinary file. */
318                if (!isArchive(file)
319                        && !file.getFileName().toString().endsWith(".jmod")
320                        && !file.endsWith("modules")) {
321                    /* Not a recognized extension; open it to see if
322                     it looks like a valid zip file. */
323                    try {
324                        // TODO: use of ZipFile should be updated
325                        ZipFile z = new ZipFile(file.toFile());
326                        z.close();
327                        if (warn) {
328                            log.warning(Lint.LintCategory.PATH,
329                                    "unexpected.archive.file", file);
330                        }
331                    } catch (IOException e) {
332                        // FIXME: include e.getLocalizedMessage in warning
333                        if (warn) {
334                            log.warning(Lint.LintCategory.PATH,
335                                    "invalid.archive.file", file);
336                        }
337                        return;
338                    }
339                }
340            }
341
342            /* Now what we have left is either a directory or a file name
343             conforming to archive naming convention */
344            super.add(file);
345            canonicalValues.add(canonFile);
346
347            if (expandJarClassPaths && fsInfo.isFile(file) && !file.endsWith("modules")) {
348                addJarClassPath(file, warn);
349            }
350        }
351
352        // Adds referenced classpath elements from a jar's Class-Path
353        // Manifest entry.  In some future release, we may want to
354        // update this code to recognize URLs rather than simple
355        // filenames, but if we do, we should redo all path-related code.
356        private void addJarClassPath(Path jarFile, boolean warn) {
357            try {
358                for (Path f : fsInfo.getJarClassPath(jarFile)) {
359                    addFile(f, warn);
360                }
361            } catch (IOException e) {
362                log.error("error.reading.file", jarFile, JavacFileManager.getMessage(e));
363            }
364        }
365    }
366
367    /**
368     * Base class for handling support for the representation of Locations.
369     *
370     * Locations are (by design) opaque handles that can easily be implemented
371     * by enums like StandardLocation. Within JavacFileManager, each Location
372     * has an associated LocationHandler, which provides much of the appropriate
373     * functionality for the corresponding Location.
374     *
375     * @see #initHandlers
376     * @see #getHandler
377     */
378    protected abstract class LocationHandler {
379
380        /**
381         * @see JavaFileManager#handleOption
382         */
383        abstract boolean handleOption(Option option, String value);
384
385        /**
386         * @see StandardJavaFileManager#hasLocation
387         */
388        boolean isSet() {
389            return (getPaths() != null);
390        }
391
392        /**
393         * @see StandardJavaFileManager#getLocation
394         */
395        abstract Collection<Path> getPaths();
396
397        /**
398         * @see StandardJavaFileManager#setLocation
399         */
400        abstract void setPaths(Iterable<? extends Path> files) throws IOException;
401
402        /**
403         * @see JavaFileManager#getModuleLocation(Location, String)
404         */
405        Location getModuleLocation(String moduleName) throws IOException {
406            return null;
407        }
408
409        /**
410         * @see JavaFileManager#getModuleLocation(Location, JavaFileObject, String)
411         */
412        Location getModuleLocation(Path dir) {
413            return null;
414        }
415
416        /**
417         * @see JavaFileManager#inferModuleName
418         */
419        String inferModuleName() {
420            return null;
421        }
422
423        /**
424         * @see JavaFileManager#listModuleLocations
425         */
426        Iterable<Set<Location>> listModuleLocations() throws IOException {
427            return null;
428        }
429    }
430
431    /**
432     * A LocationHandler for a given Location, and associated set of options.
433     */
434    private abstract class BasicLocationHandler extends LocationHandler {
435
436        final Location location;
437        final Set<Option> options;
438
439        /**
440         * Create a handler. The location and options provide a way to map from a location or an
441         * option to the corresponding handler.
442         *
443         * @param location the location for which this is the handler
444         * @param options the options affecting this location
445         * @see #initHandlers
446         */
447        protected BasicLocationHandler(Location location, Option... options) {
448            this.location = location;
449            this.options = options.length == 0
450                    ? EnumSet.noneOf(Option.class)
451                    : EnumSet.copyOf(Arrays.asList(options));
452        }
453    }
454
455    /**
456     * General purpose implementation for output locations, such as -d/CLASS_OUTPUT and
457     * -s/SOURCE_OUTPUT. All options are treated as equivalent (i.e. aliases.)
458     * The value is a single file, possibly null.
459     */
460    private class OutputLocationHandler extends BasicLocationHandler {
461
462        private Path outputDir;
463        private Map<String, Location> moduleLocations;
464
465        OutputLocationHandler(Location location, Option... options) {
466            super(location, options);
467        }
468
469        @Override
470        boolean handleOption(Option option, String value) {
471            if (!options.contains(option)) {
472                return false;
473            }
474
475            // TODO: could/should validate outputDir exists and is a directory
476            // need to decide how best to report issue for benefit of
477            // direct API call on JavaFileManager.handleOption(specifies IAE)
478            // vs. command line decoding.
479            outputDir = (value == null) ? null : getPath(value);
480            return true;
481        }
482
483        @Override
484        Collection<Path> getPaths() {
485            return (outputDir == null) ? null : Collections.singleton(outputDir);
486        }
487
488        @Override
489        void setPaths(Iterable<? extends Path> files) throws IOException {
490            if (files == null) {
491                outputDir = null;
492            } else {
493                Iterator<? extends Path> pathIter = files.iterator();
494                if (!pathIter.hasNext()) {
495                    throw new IllegalArgumentException("empty path for directory");
496                }
497                Path dir = pathIter.next();
498                if (pathIter.hasNext()) {
499                    throw new IllegalArgumentException("path too long for directory");
500                }
501                if (!Files.exists(dir)) {
502                    throw new FileNotFoundException(dir + ": does not exist");
503                } else if (!Files.isDirectory(dir)) {
504                    throw new IOException(dir + ": not a directory");
505                }
506                outputDir = dir;
507            }
508            moduleLocations = null;
509        }
510
511        @Override
512        Location getModuleLocation(String name) {
513            if (moduleLocations == null)
514                moduleLocations = new HashMap<>();
515            Location l = moduleLocations.get(name);
516            if (l == null) {
517                l = new ModuleLocationHandler(location.getName() + "[" + name + "]",
518                        name,
519                        Collections.singleton(outputDir.resolve(name)),
520                        true, false);
521                moduleLocations.put(name, l);
522            }
523            return l;
524        }
525    }
526
527    /**
528     * General purpose implementation for search path locations,
529     * such as -sourcepath/SOURCE_PATH and -processorPath/ANNOTATION_PROCESSOR_PATH.
530     * All options are treated as equivalent (i.e. aliases.)
531     * The value is an ordered set of files and/or directories.
532     */
533    private class SimpleLocationHandler extends BasicLocationHandler {
534
535        protected Collection<Path> searchPath;
536
537        SimpleLocationHandler(Location location, Option... options) {
538            super(location, options);
539        }
540
541        @Override
542        boolean handleOption(Option option, String value) {
543            if (!options.contains(option)) {
544                return false;
545            }
546            searchPath = value == null ? null
547                    : Collections.unmodifiableCollection(createPath().addFiles(value));
548            return true;
549        }
550
551        @Override
552        Collection<Path> getPaths() {
553            return searchPath;
554        }
555
556        @Override
557        void setPaths(Iterable<? extends Path> files) {
558            SearchPath p;
559            if (files == null) {
560                p = computePath(null);
561            } else {
562                p = createPath().addFiles(files);
563            }
564            searchPath = Collections.unmodifiableCollection(p);
565        }
566
567        protected SearchPath computePath(String value) {
568            return createPath().addFiles(value);
569        }
570
571        protected SearchPath createPath() {
572            return new SearchPath();
573        }
574    }
575
576    /**
577     * Subtype of SimpleLocationHandler for -classpath/CLASS_PATH.
578     * If no value is given, a default is provided, based on system properties and other values.
579     */
580    private class ClassPathLocationHandler extends SimpleLocationHandler {
581
582        ClassPathLocationHandler() {
583            super(StandardLocation.CLASS_PATH,
584                    Option.CLASSPATH, Option.CP);
585        }
586
587        @Override
588        Collection<Path> getPaths() {
589            lazy();
590            return searchPath;
591        }
592
593        @Override
594        protected SearchPath computePath(String value) {
595            String cp = value;
596
597            // CLASSPATH environment variable when run from `javac'.
598            if (cp == null) {
599                cp = System.getProperty("env.class.path");
600            }
601
602            // If invoked via a java VM (not the javac launcher), use the
603            // platform class path
604            if (cp == null && System.getProperty("application.home") == null) {
605                cp = System.getProperty("java.class.path");
606            }
607
608            // Default to current working directory.
609            if (cp == null) {
610                cp = ".";
611            }
612
613            return createPath().addFiles(cp);
614        }
615
616        @Override
617        protected SearchPath createPath() {
618            return new SearchPath()
619                    .expandJarClassPaths(true) // Only search user jars for Class-Paths
620                    .emptyPathDefault(getPath("."));  // Empty path elt ==> current directory
621        }
622
623        private void lazy() {
624            if (searchPath == null) {
625                setPaths(null);
626            }
627        }
628    }
629
630    /**
631     * Custom subtype of LocationHandler for PLATFORM_CLASS_PATH.
632     * Various options are supported for different components of the
633     * platform class path.
634     * Setting a value with setLocation overrides all existing option values.
635     * Setting any option overrides any value set with setLocation, and
636     * reverts to using default values for options that have not been set.
637     * Setting -bootclasspath or -Xbootclasspath overrides any existing
638     * value for -Xbootclasspath/p: and -Xbootclasspath/a:.
639     */
640    private class BootClassPathLocationHandler extends BasicLocationHandler {
641
642        private Collection<Path> searchPath;
643        final Map<Option, String> optionValues = new EnumMap<>(Option.class);
644
645        /**
646         * Is the bootclasspath the default?
647         */
648        private boolean isDefault;
649
650        BootClassPathLocationHandler() {
651            super(StandardLocation.PLATFORM_CLASS_PATH,
652                    Option.BOOTCLASSPATH, Option.XBOOTCLASSPATH,
653                    Option.XBOOTCLASSPATH_PREPEND,
654                    Option.XBOOTCLASSPATH_APPEND,
655                    Option.ENDORSEDDIRS, Option.DJAVA_ENDORSED_DIRS,
656                    Option.EXTDIRS, Option.DJAVA_EXT_DIRS);
657        }
658
659        boolean isDefault() {
660            lazy();
661            return isDefault;
662        }
663
664        @Override
665        boolean handleOption(Option option, String value) {
666            if (!options.contains(option)) {
667                return false;
668            }
669
670            option = canonicalize(option);
671            optionValues.put(option, value);
672            if (option == BOOTCLASSPATH) {
673                optionValues.remove(XBOOTCLASSPATH_PREPEND);
674                optionValues.remove(XBOOTCLASSPATH_APPEND);
675            }
676            searchPath = null;  // reset to "uninitialized"
677            return true;
678        }
679        // where
680        // TODO: would be better if option aliasing was handled at a higher
681        // level
682        private Option canonicalize(Option option) {
683            switch (option) {
684                case XBOOTCLASSPATH:
685                    return Option.BOOTCLASSPATH;
686                case DJAVA_ENDORSED_DIRS:
687                    return Option.ENDORSEDDIRS;
688                case DJAVA_EXT_DIRS:
689                    return Option.EXTDIRS;
690                default:
691                    return option;
692            }
693        }
694
695        @Override
696        Collection<Path> getPaths() {
697            lazy();
698            return searchPath;
699        }
700
701        @Override
702        void setPaths(Iterable<? extends Path> files) {
703            if (files == null) {
704                searchPath = null;  // reset to "uninitialized"
705            } else {
706                isDefault = false;
707                SearchPath p = new SearchPath().addFiles(files, false);
708                searchPath = Collections.unmodifiableCollection(p);
709                optionValues.clear();
710            }
711        }
712
713        SearchPath computePath() throws IOException {
714            SearchPath path = new SearchPath();
715
716            String bootclasspathOpt = optionValues.get(BOOTCLASSPATH);
717            String endorseddirsOpt = optionValues.get(ENDORSEDDIRS);
718            String extdirsOpt = optionValues.get(EXTDIRS);
719            String xbootclasspathPrependOpt = optionValues.get(XBOOTCLASSPATH_PREPEND);
720            String xbootclasspathAppendOpt = optionValues.get(XBOOTCLASSPATH_APPEND);
721            path.addFiles(xbootclasspathPrependOpt);
722
723            if (endorseddirsOpt != null) {
724                path.addDirectories(endorseddirsOpt);
725            } else {
726                path.addDirectories(System.getProperty("java.endorsed.dirs"), false);
727            }
728
729            if (bootclasspathOpt != null) {
730                path.addFiles(bootclasspathOpt);
731            } else {
732                // Standard system classes for this compiler's release.
733                Collection<Path> systemClasses = systemClasses();
734                if (systemClasses != null) {
735                    path.addFiles(systemClasses, false);
736                } else {
737                    // fallback to the value of sun.boot.class.path
738                    String files = System.getProperty("sun.boot.class.path");
739                    path.addFiles(files, false);
740                }
741            }
742
743            path.addFiles(xbootclasspathAppendOpt);
744
745            // Strictly speaking, standard extensions are not bootstrap
746            // classes, but we treat them identically, so we'll pretend
747            // that they are.
748            if (extdirsOpt != null) {
749                path.addDirectories(extdirsOpt);
750            } else {
751                // Add lib/jfxrt.jar to the search path
752               Path jfxrt = javaHome.resolve("lib/jfxrt.jar");
753                if (Files.exists(jfxrt)) {
754                    path.addFile(jfxrt, false);
755                }
756                path.addDirectories(System.getProperty("java.ext.dirs"), false);
757            }
758
759            isDefault =
760                       (xbootclasspathPrependOpt == null)
761                    && (bootclasspathOpt == null)
762                    && (xbootclasspathAppendOpt == null);
763
764            return path;
765        }
766
767        /**
768         * Return a collection of files containing system classes.
769         * Returns {@code null} if not running on a modular image.
770         *
771         * @throws UncheckedIOException if an I/O errors occurs
772         */
773        private Collection<Path> systemClasses() throws IOException {
774            // Return "modules" jimage file if available
775            if (Files.isRegularFile(thisSystemModules)) {
776                return addAdditionalBootEntries(Collections.singleton(thisSystemModules));
777            }
778
779            // Exploded module image
780            Path modules = javaHome.resolve("modules");
781            if (Files.isDirectory(modules.resolve("java.base"))) {
782                try (Stream<Path> listedModules = Files.list(modules)) {
783                    return addAdditionalBootEntries(listedModules.collect(Collectors.toList()));
784                }
785            }
786
787            // not a modular image that we know about
788            return null;
789        }
790
791        //ensure bootclasspath prepends/appends are reflected in the systemClasses
792        private Collection<Path> addAdditionalBootEntries(Collection<Path> modules) throws IOException {
793            String files = System.getProperty("sun.boot.class.path");
794            if (files == null)
795                return modules;
796
797            Set<Path> paths = new LinkedHashSet<>();
798
799            // The JVM no longer supports -Xbootclasspath/p:, so any interesting
800            // entries should be appended to the set of modules.
801
802            paths.addAll(modules);
803
804            for (String s : files.split(Pattern.quote(File.pathSeparator))) {
805                paths.add(getPath(s));
806            }
807
808            return paths;
809        }
810
811        private void lazy() {
812            if (searchPath == null) {
813                try {
814                searchPath = Collections.unmodifiableCollection(computePath());
815                } catch (IOException e) {
816                    // TODO: need better handling here, e.g. javac Abort?
817                    throw new UncheckedIOException(e);
818                }
819            }
820        }
821    }
822
823    /**
824     * A LocationHander to represent modules found from a module-oriented
825     * location such as MODULE_SOURCE_PATH, UPGRADE_MODULE_PATH,
826     * SYSTEM_MODULES and MODULE_PATH.
827     *
828     * The Location can be specified to accept overriding classes from the
829     * {@code -Xpatch:<module>=<path> } parameter.
830     */
831    private class ModuleLocationHandler extends LocationHandler implements Location {
832        protected final String name;
833        protected final String moduleName;
834        protected final Collection<Path> searchPath;
835        protected final Collection<Path> searchPathWithOverrides;
836        protected final boolean output;
837
838        ModuleLocationHandler(String name, String moduleName, Collection<Path> searchPath,
839                boolean output, boolean allowOverrides) {
840            this.name = name;
841            this.moduleName = moduleName;
842            this.searchPath = searchPath;
843            this.output = output;
844
845            if (allowOverrides && patchMap != null) {
846                SearchPath mPatch = patchMap.get(moduleName);
847                if (mPatch != null) {
848                    SearchPath sp = new SearchPath();
849                    sp.addAll(mPatch);
850                    sp.addAll(searchPath);
851                    searchPathWithOverrides = sp;
852                } else {
853                    searchPathWithOverrides = searchPath;
854                }
855            } else {
856                searchPathWithOverrides = searchPath;
857            }
858        }
859
860        @Override @DefinedBy(Api.COMPILER)
861        public String getName() {
862            return name;
863        }
864
865        @Override @DefinedBy(Api.COMPILER)
866        public boolean isOutputLocation() {
867            return output;
868        }
869
870        @Override // defined by LocationHandler
871        boolean handleOption(Option option, String value) {
872            throw new UnsupportedOperationException();
873        }
874
875        @Override // defined by LocationHandler
876        Collection<Path> getPaths() {
877            // For now, we always return searchPathWithOverrides. This may differ from the
878            // JVM behavior if there is a module-info.class to be found in the overriding
879            // classes.
880            return searchPathWithOverrides;
881        }
882
883        @Override // defined by LocationHandler
884        void setPaths(Iterable<? extends Path> files) throws IOException {
885            throw new UnsupportedOperationException();
886        }
887
888        @Override // defined by LocationHandler
889        String inferModuleName() {
890            return moduleName;
891        }
892    }
893
894    /**
895     * A LocationHandler for simple module-oriented search paths,
896     * like UPGRADE_MODULE_PATH and MODULE_PATH.
897     */
898    private class ModulePathLocationHandler extends SimpleLocationHandler {
899        ModulePathLocationHandler(Location location, Option... options) {
900            super(location, options);
901        }
902
903        @Override
904        public boolean handleOption(Option option, String value) {
905            if (!options.contains(option)) {
906                return false;
907            }
908            setPaths(value == null ? null : getPathEntries(value));
909            return true;
910        }
911
912        @Override
913        Iterable<Set<Location>> listModuleLocations() {
914            if (searchPath == null)
915                return Collections.emptyList();
916
917            return () -> new ModulePathIterator();
918        }
919
920        @Override
921        void setPaths(Iterable<? extends Path> paths) {
922            if (paths != null) {
923                for (Path p: paths) {
924                    checkValidModulePathEntry(p);
925                }
926            }
927            super.setPaths(paths);
928        }
929
930        private void checkValidModulePathEntry(Path p) {
931            if (Files.isDirectory(p)) {
932                // either an exploded module or a directory of modules
933                return;
934            }
935
936            String name = p.getFileName().toString();
937            int lastDot = name.lastIndexOf(".");
938            if (lastDot > 0) {
939                switch (name.substring(lastDot)) {
940                    case ".jar":
941                    case ".jmod":
942                        return;
943                }
944            }
945            throw new IllegalArgumentException(p.toString());
946        }
947
948        class ModulePathIterator implements Iterator<Set<Location>> {
949            Iterator<Path> pathIter = searchPath.iterator();
950            int pathIndex = 0;
951            Set<Location> next = null;
952
953            @Override
954            public boolean hasNext() {
955                if (next != null)
956                    return true;
957
958                while (next == null) {
959                    if (pathIter.hasNext()) {
960                        Path path = pathIter.next();
961                        if (Files.isDirectory(path)) {
962                            next = scanDirectory(path);
963                        } else {
964                            next = scanFile(path);
965                        }
966                        pathIndex++;
967                    } else
968                        return false;
969                }
970                return true;
971            }
972
973            @Override
974            public Set<Location> next() {
975                hasNext();
976                if (next != null) {
977                    Set<Location> result = next;
978                    next = null;
979                    return result;
980                }
981                throw new NoSuchElementException();
982            }
983
984            private Set<Location> scanDirectory(Path path) {
985                Set<Path> paths = new LinkedHashSet<>();
986                Path moduleInfoClass = null;
987                try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
988                    for (Path entry: stream) {
989                        if (entry.endsWith("module-info.class")) {
990                            moduleInfoClass = entry;
991                            break;  // no need to continue scanning
992                        }
993                        paths.add(entry);
994                    }
995                } catch (DirectoryIteratorException | IOException ignore) {
996                    log.error(Errors.LocnCantReadDirectory(path));
997                    return Collections.emptySet();
998                }
999
1000                if (moduleInfoClass != null) {
1001                    // It's an exploded module directly on the module path.
1002                    // We can't infer module name from the directory name, so have to
1003                    // read module-info.class.
1004                    try {
1005                        String moduleName = readModuleName(moduleInfoClass);
1006                        String name = location.getName()
1007                                + "[" + pathIndex + ":" + moduleName + "]";
1008                        ModuleLocationHandler l = new ModuleLocationHandler(name, moduleName,
1009                                Collections.singleton(path), false, true);
1010                        return Collections.singleton(l);
1011                    } catch (ModuleNameReader.BadClassFile e) {
1012                        log.error(Errors.LocnBadModuleInfo(path));
1013                        return Collections.emptySet();
1014                    } catch (IOException e) {
1015                        log.error(Errors.LocnCantReadFile(path));
1016                        return Collections.emptySet();
1017                    }
1018                }
1019
1020                // A directory of modules
1021                Set<Location> result = new LinkedHashSet<>();
1022                int index = 0;
1023                for (Path entry : paths) {
1024                    Pair<String,Path> module = inferModuleName(entry);
1025                    if (module == null) {
1026                        // diagnostic reported if necessary; skip to next
1027                        continue;
1028                    }
1029                    String moduleName = module.fst;
1030                    Path modulePath = module.snd;
1031                    String name = location.getName()
1032                            + "[" + pathIndex + "." + (index++) + ":" + moduleName + "]";
1033                    ModuleLocationHandler l = new ModuleLocationHandler(name, moduleName,
1034                            Collections.singleton(modulePath), false, true);
1035                    result.add(l);
1036                }
1037                return result;
1038            }
1039
1040            private Set<Location> scanFile(Path path) {
1041                Pair<String,Path> module = inferModuleName(path);
1042                if (module == null) {
1043                    // diagnostic reported if necessary
1044                    return Collections.emptySet();
1045                }
1046                String moduleName = module.fst;
1047                Path modulePath = module.snd;
1048                String name = location.getName()
1049                        + "[" + pathIndex + ":" + moduleName + "]";
1050                ModuleLocationHandler l = new ModuleLocationHandler(name, moduleName,
1051                        Collections.singleton(modulePath), false, true);
1052                return Collections.singleton(l);
1053            }
1054
1055            private Pair<String,Path> inferModuleName(Path p) {
1056                if (Files.isDirectory(p)) {
1057                    if (Files.exists(p.resolve("module-info.class"))) {
1058                        String name = p.getFileName().toString();
1059                        if (SourceVersion.isName(name))
1060                            return new Pair<>(name, p);
1061                    }
1062                    return null;
1063                }
1064
1065                if (p.getFileName().toString().endsWith(".jar")) {
1066                    try (FileSystem fs = FileSystems.newFileSystem(p, null)) {
1067                        Path moduleInfoClass = fs.getPath("module-info.class");
1068                        if (Files.exists(moduleInfoClass)) {
1069                            String moduleName = readModuleName(moduleInfoClass);
1070                            return new Pair<>(moduleName, p);
1071                        }
1072                    } catch (ModuleNameReader.BadClassFile e) {
1073                        log.error(Errors.LocnBadModuleInfo(p));
1074                        return null;
1075                    } catch (IOException e) {
1076                        log.error(Errors.LocnCantReadFile(p));
1077                        return null;
1078                    }
1079
1080                    //automatic module:
1081                    String fn = p.getFileName().toString();
1082                    //from ModulePath.deriveModuleDescriptor:
1083
1084                    // drop .jar
1085                    String mn = fn.substring(0, fn.length()-4);
1086
1087                    // find first occurrence of -${NUMBER}. or -${NUMBER}$
1088                    Matcher matcher = Pattern.compile("-(\\d+(\\.|$))").matcher(mn);
1089                    if (matcher.find()) {
1090                        int start = matcher.start();
1091
1092                        mn = mn.substring(0, start);
1093                    }
1094
1095                    // finally clean up the module name
1096                    mn =  mn.replaceAll("[^A-Za-z0-9]", ".")  // replace non-alphanumeric
1097                            .replaceAll("(\\.)(\\1)+", ".")   // collapse repeating dots
1098                            .replaceAll("^\\.", "")           // drop leading dots
1099                            .replaceAll("\\.$", "");          // drop trailing dots
1100
1101
1102                    if (!mn.isEmpty()) {
1103                        return new Pair<>(mn, p);
1104                    }
1105
1106                    log.error(Errors.LocnCantGetModuleNameForJar(p));
1107                    return null;
1108                }
1109
1110                if (p.getFileName().toString().endsWith(".jmod")) {
1111                    try {
1112                        FileSystem fs = fileSystems.get(p);
1113                        if (fs == null) {
1114                            URI uri = URI.create("jar:" + p.toUri());
1115                            fs = FileSystems.newFileSystem(uri, Collections.emptyMap(), null);
1116                            try {
1117                                Path moduleInfoClass = fs.getPath("classes/module-info.class");
1118                                String moduleName = readModuleName(moduleInfoClass);
1119                                Path modulePath = fs.getPath("classes");
1120                                fileSystems.put(p, fs);
1121                                closeables.add(fs);
1122                                fs = null; // prevent fs being closed in the finally clause
1123                                return new Pair<>(moduleName, modulePath);
1124                            } finally {
1125                                if (fs != null)
1126                                    fs.close();
1127                            }
1128                        }
1129                    } catch (ProviderNotFoundException e) {
1130                        // will be thrown if the file is not a valid zip file
1131                        log.error(Errors.LocnCantReadFile(p));
1132                        return null;
1133                    } catch (ModuleNameReader.BadClassFile e) {
1134                        log.error(Errors.LocnBadModuleInfo(p));
1135                    } catch (IOException e) {
1136                        log.error(Errors.LocnCantReadFile(p));
1137                        return null;
1138                    }
1139                }
1140
1141                if (warn && false) {  // temp disable
1142                    log.warning(Warnings.LocnUnknownFileOnModulePath(p));
1143                }
1144                return null;
1145            }
1146
1147            private String readModuleName(Path path) throws IOException, ModuleNameReader.BadClassFile {
1148                if (moduleNameReader == null)
1149                    moduleNameReader = new ModuleNameReader();
1150                return moduleNameReader.readModuleName(path);
1151            }
1152        }
1153
1154    }
1155
1156    private class ModuleSourcePathLocationHandler extends BasicLocationHandler {
1157
1158        private Map<String, Location> moduleLocations;
1159        private Map<Path, Location> pathLocations;
1160
1161
1162        ModuleSourcePathLocationHandler() {
1163            super(StandardLocation.MODULE_SOURCE_PATH,
1164                    Option.MODULESOURCEPATH);
1165        }
1166
1167        @Override
1168        boolean handleOption(Option option, String value) {
1169            init(value);
1170            return true;
1171        }
1172
1173        void init(String value) {
1174            Collection<String> segments = new ArrayList<>();
1175            for (String s: value.split(File.pathSeparator)) {
1176                expandBraces(s, segments);
1177            }
1178
1179            Map<String, Collection<Path>> map = new LinkedHashMap<>();
1180            final String MARKER = "*";
1181            for (String seg: segments) {
1182                int markStart = seg.indexOf(MARKER);
1183                if (markStart == -1) {
1184                    add(map, getPath(seg), null);
1185                } else {
1186                    if (markStart == 0 || !isSeparator(seg.charAt(markStart - 1))) {
1187                        throw new IllegalArgumentException("illegal use of " + MARKER + " in " + seg);
1188                    }
1189                    Path prefix = getPath(seg.substring(0, markStart - 1));
1190                    Path suffix;
1191                    int markEnd = markStart + MARKER.length();
1192                    if (markEnd == seg.length()) {
1193                        suffix = null;
1194                    } else if (!isSeparator(seg.charAt(markEnd))
1195                            || seg.indexOf(MARKER, markEnd) != -1) {
1196                        throw new IllegalArgumentException("illegal use of " + MARKER + " in " + seg);
1197                    } else {
1198                        suffix = getPath(seg.substring(markEnd + 1));
1199                    }
1200                    add(map, prefix, suffix);
1201                }
1202            }
1203
1204            moduleLocations = new LinkedHashMap<>();
1205            pathLocations = new LinkedHashMap<>();
1206            map.forEach((k, v) -> {
1207                String name = location.getName() + "[" + k + "]";
1208                ModuleLocationHandler h = new ModuleLocationHandler(name, k, v, false, false);
1209                moduleLocations.put(k, h);
1210                v.forEach(p -> pathLocations.put(normalize(p), h));
1211            });
1212        }
1213
1214        private boolean isSeparator(char ch) {
1215            // allow both separators on Windows
1216            return (ch == File.separatorChar) || (ch == '/');
1217        }
1218
1219        void add(Map<String, Collection<Path>> map, Path prefix, Path suffix) {
1220            if (!Files.isDirectory(prefix)) {
1221                if (warn) {
1222                    String key = Files.exists(prefix)
1223                            ? "dir.path.element.not.directory"
1224                            : "dir.path.element.not.found";
1225                    log.warning(Lint.LintCategory.PATH, key, prefix);
1226                }
1227                return;
1228            }
1229            try (DirectoryStream<Path> stream = Files.newDirectoryStream(prefix, path -> Files.isDirectory(path))) {
1230                for (Path entry: stream) {
1231                    Path path = (suffix == null) ? entry : entry.resolve(suffix);
1232                    if (Files.isDirectory(path)) {
1233                        String name = entry.getFileName().toString();
1234                        Collection<Path> paths = map.get(name);
1235                        if (paths == null)
1236                            map.put(name, paths = new ArrayList<>());
1237                        paths.add(path);
1238                    }
1239                }
1240            } catch (IOException e) {
1241                // TODO? What to do?
1242                System.err.println(e);
1243            }
1244        }
1245
1246        private void expandBraces(String value, Collection<String> results) {
1247            int depth = 0;
1248            int start = -1;
1249            String prefix = null;
1250            String suffix = null;
1251            for (int i = 0; i < value.length(); i++) {
1252                switch (value.charAt(i)) {
1253                    case '{':
1254                        depth++;
1255                        if (depth == 1) {
1256                            prefix = value.substring(0, i);
1257                            suffix = value.substring(getMatchingBrace(value, i) + 1);
1258                            start = i + 1;
1259                        }
1260                        break;
1261
1262                    case ',':
1263                        if (depth == 1) {
1264                            String elem = value.substring(start, i);
1265                            expandBraces(prefix + elem + suffix, results);
1266                            start = i + 1;
1267                        }
1268                        break;
1269
1270                    case '}':
1271                        switch (depth) {
1272                            case 0:
1273                                throw new IllegalArgumentException("mismatched braces");
1274
1275                            case 1:
1276                                String elem = value.substring(start, i);
1277                                expandBraces(prefix + elem + suffix, results);
1278                                return;
1279
1280                            default:
1281                                depth--;
1282                        }
1283                        break;
1284                }
1285            }
1286            if (depth > 0)
1287                throw new IllegalArgumentException("mismatched braces");
1288            results.add(value);
1289        }
1290
1291        int getMatchingBrace(String value, int offset) {
1292            int depth = 1;
1293            for (int i = offset + 1; i < value.length(); i++) {
1294                switch (value.charAt(i)) {
1295                    case '{':
1296                        depth++;
1297                        break;
1298
1299                    case '}':
1300                        if (--depth == 0)
1301                            return i;
1302                        break;
1303                }
1304            }
1305            throw new IllegalArgumentException("mismatched braces");
1306        }
1307
1308        @Override
1309        boolean isSet() {
1310            return (moduleLocations != null);
1311        }
1312
1313        @Override
1314        Collection<Path> getPaths() {
1315            throw new UnsupportedOperationException();
1316        }
1317
1318        @Override
1319        void setPaths(Iterable<? extends Path> files) throws IOException {
1320            throw new UnsupportedOperationException();
1321        }
1322
1323        @Override
1324        Location getModuleLocation(String name) {
1325            return (moduleLocations == null) ? null : moduleLocations.get(name);
1326        }
1327
1328        @Override
1329        Location getModuleLocation(Path dir) {
1330            return (pathLocations == null) ? null : pathLocations.get(dir);
1331        }
1332
1333        @Override
1334        Iterable<Set<Location>> listModuleLocations() {
1335            if (moduleLocations == null)
1336                return Collections.emptySet();
1337            Set<Location> locns = new LinkedHashSet<>();
1338            moduleLocations.forEach((k, v) -> locns.add(v));
1339            return Collections.singleton(locns);
1340        }
1341
1342    }
1343
1344    private class SystemModulesLocationHandler extends BasicLocationHandler {
1345        private Path systemJavaHome;
1346        private Path modules;
1347        private Map<String, ModuleLocationHandler> systemModules;
1348
1349        SystemModulesLocationHandler() {
1350            super(StandardLocation.SYSTEM_MODULES, Option.SYSTEM);
1351            systemJavaHome = Locations.javaHome;
1352        }
1353
1354        @Override
1355        boolean handleOption(Option option, String value) {
1356            if (!options.contains(option)) {
1357                return false;
1358            }
1359
1360            if (value == null) {
1361                systemJavaHome = Locations.javaHome;
1362            } else if (value.equals("none")) {
1363                systemJavaHome = null;
1364            } else {
1365                update(getPath(value));
1366            }
1367
1368            modules = null;
1369            return true;
1370        }
1371
1372        @Override
1373        Collection<Path> getPaths() {
1374            return (systemJavaHome == null) ? null : Collections.singleton(systemJavaHome);
1375        }
1376
1377        @Override
1378        void setPaths(Iterable<? extends Path> files) throws IOException {
1379            if (files == null) {
1380                systemJavaHome = null;
1381            } else {
1382                Iterator<? extends Path> pathIter = files.iterator();
1383                if (!pathIter.hasNext()) {
1384                    throw new IllegalArgumentException("empty path for directory"); // TODO: FIXME
1385                }
1386                Path dir = pathIter.next();
1387                if (pathIter.hasNext()) {
1388                    throw new IllegalArgumentException("path too long for directory"); // TODO: FIXME
1389                }
1390                if (!Files.exists(dir)) {
1391                    throw new FileNotFoundException(dir + ": does not exist");
1392                } else if (!Files.isDirectory(dir)) {
1393                    throw new IOException(dir + ": not a directory");
1394                }
1395                update(dir);
1396            }
1397        }
1398
1399        private void update(Path p) {
1400            if (!isCurrentPlatform(p) && !Files.exists(p.resolve("jrt-fs.jar")) && !Files.exists(systemJavaHome.resolve("modules")))
1401                throw new IllegalArgumentException(p.toString());
1402            systemJavaHome = p;
1403            modules = null;
1404        }
1405
1406        private boolean isCurrentPlatform(Path p) {
1407            try {
1408                return Files.isSameFile(p, Locations.javaHome);
1409            } catch (IOException ex) {
1410                throw new IllegalArgumentException(p.toString(), ex);
1411            }
1412        }
1413
1414        @Override
1415        Location getModuleLocation(String name) throws IOException {
1416            initSystemModules();
1417            return systemModules.get(name);
1418        }
1419
1420        @Override
1421        Iterable<Set<Location>> listModuleLocations() throws IOException {
1422            initSystemModules();
1423            Set<Location> locns = new LinkedHashSet<>();
1424            for (Location l: systemModules.values())
1425                locns.add(l);
1426            return Collections.singleton(locns);
1427        }
1428
1429        private void initSystemModules() throws IOException {
1430            if (systemModules != null) {
1431                return;
1432            }
1433
1434            if (systemJavaHome == null) {
1435                systemModules = Collections.emptyMap();
1436                return;
1437            }
1438
1439            if (modules == null) {
1440                try {
1441                    URI jrtURI = URI.create("jrt:/");
1442                    FileSystem jrtfs;
1443
1444                    if (isCurrentPlatform(systemJavaHome)) {
1445                        jrtfs = FileSystems.getFileSystem(jrtURI);
1446                    } else {
1447                        try {
1448                            Map<String, String> attrMap =
1449                                    Collections.singletonMap("java.home", systemJavaHome.toString());
1450                            jrtfs = FileSystems.newFileSystem(jrtURI, attrMap);
1451                        } catch (ProviderNotFoundException ex) {
1452                            URL javaHomeURL = systemJavaHome.resolve("jrt-fs.jar").toUri().toURL();
1453                            ClassLoader currentLoader = Locations.class.getClassLoader();
1454                            URLClassLoader fsLoader =
1455                                    new URLClassLoader(new URL[] {javaHomeURL}, currentLoader);
1456
1457                            jrtfs = FileSystems.newFileSystem(jrtURI, Collections.emptyMap(), fsLoader);
1458
1459                            closeables.add(fsLoader);
1460                        }
1461
1462                        closeables.add(jrtfs);
1463                    }
1464
1465                    modules = jrtfs.getPath("/modules");
1466                } catch (FileSystemNotFoundException | ProviderNotFoundException e) {
1467                    modules = systemJavaHome.resolve("modules");
1468                    if (!Files.exists(modules))
1469                        throw new IOException("can't find system classes", e);
1470                }
1471            }
1472
1473            systemModules = new LinkedHashMap<>();
1474            try (DirectoryStream<Path> stream = Files.newDirectoryStream(modules, Files::isDirectory)) {
1475                for (Path entry : stream) {
1476                    String moduleName = entry.getFileName().toString();
1477                    String name = location.getName() + "[" + moduleName + "]";
1478                    ModuleLocationHandler h = new ModuleLocationHandler(name, moduleName,
1479                            Collections.singleton(entry), false, true);
1480                    systemModules.put(moduleName, h);
1481                }
1482            }
1483        }
1484    }
1485
1486    Map<Location, LocationHandler> handlersForLocation;
1487    Map<Option, LocationHandler> handlersForOption;
1488
1489    void initHandlers() {
1490        handlersForLocation = new HashMap<>();
1491        handlersForOption = new EnumMap<>(Option.class);
1492
1493        BasicLocationHandler[] handlers = {
1494            new BootClassPathLocationHandler(),
1495            new ClassPathLocationHandler(),
1496            new SimpleLocationHandler(StandardLocation.SOURCE_PATH, Option.SOURCEPATH),
1497            new SimpleLocationHandler(StandardLocation.ANNOTATION_PROCESSOR_PATH, Option.PROCESSORPATH),
1498            new SimpleLocationHandler(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, Option.PROCESSORMODULEPATH),
1499            new OutputLocationHandler(StandardLocation.CLASS_OUTPUT, Option.D),
1500            new OutputLocationHandler(StandardLocation.SOURCE_OUTPUT, Option.S),
1501            new OutputLocationHandler(StandardLocation.NATIVE_HEADER_OUTPUT, Option.H),
1502            new ModuleSourcePathLocationHandler(),
1503            // TODO: should UPGRADE_MODULE_PATH be merged with SYSTEM_MODULES?
1504            new ModulePathLocationHandler(StandardLocation.UPGRADE_MODULE_PATH, Option.UPGRADEMODULEPATH),
1505            new ModulePathLocationHandler(StandardLocation.MODULE_PATH, Option.MODULEPATH, Option.MP),
1506            new SystemModulesLocationHandler(),
1507        };
1508
1509        for (BasicLocationHandler h : handlers) {
1510            handlersForLocation.put(h.location, h);
1511            for (Option o : h.options) {
1512                handlersForOption.put(o, h);
1513            }
1514        }
1515    }
1516
1517    private Map<String, SearchPath> patchMap;
1518
1519    boolean handleOption(Option option, String value) {
1520        switch (option) {
1521            case XPATCH:
1522                Map<String, SearchPath> map = new LinkedHashMap<>();
1523                int eq = value.indexOf('=');
1524                if (eq > 0) {
1525                    String mName = value.substring(0, eq);
1526                    SearchPath mPatchPath = new SearchPath()
1527                            .addFiles(value.substring(eq + 1));
1528                    boolean ok = true;
1529                    for (Path p: mPatchPath) {
1530                        Path mi = p.resolve("module-info.class");
1531                        if (Files.exists(mi)) {
1532                            log.error(Errors.LocnModuleInfoNotAllowedOnPatchPath(mi));
1533                            ok = false;
1534                        }
1535                    }
1536                    if (ok && !mPatchPath.isEmpty()) {
1537                        map.computeIfAbsent(mName, (_x) -> new SearchPath())
1538                                .addAll(mPatchPath);
1539                    }
1540                } else {
1541                    log.error(Errors.LocnInvalidArgForXpatch(value));
1542                }
1543                patchMap = map;
1544                return true;
1545            default:
1546                LocationHandler h = handlersForOption.get(option);
1547                return (h == null ? false : h.handleOption(option, value));
1548        }
1549    }
1550
1551    boolean hasLocation(Location location) {
1552        LocationHandler h = getHandler(location);
1553        return (h == null ? false : h.isSet());
1554    }
1555
1556    Collection<Path> getLocation(Location location) {
1557        LocationHandler h = getHandler(location);
1558        return (h == null ? null : h.getPaths());
1559    }
1560
1561    Path getOutputLocation(Location location) {
1562        if (!location.isOutputLocation()) {
1563            throw new IllegalArgumentException();
1564        }
1565        LocationHandler h = getHandler(location);
1566        return ((OutputLocationHandler) h).outputDir;
1567    }
1568
1569    void setLocation(Location location, Iterable<? extends Path> files) throws IOException {
1570        LocationHandler h = getHandler(location);
1571        if (h == null) {
1572            if (location.isOutputLocation()) {
1573                h = new OutputLocationHandler(location);
1574            } else {
1575                h = new SimpleLocationHandler(location);
1576            }
1577            handlersForLocation.put(location, h);
1578        }
1579        h.setPaths(files);
1580    }
1581
1582    Location getModuleLocation(Location location, String name) throws IOException {
1583        LocationHandler h = getHandler(location);
1584        return (h == null ? null : h.getModuleLocation(name));
1585    }
1586
1587    Location getModuleLocation(Location location, Path dir) {
1588        LocationHandler h = getHandler(location);
1589        return (h == null ? null : h.getModuleLocation(dir));
1590    }
1591
1592    String inferModuleName(Location location) {
1593        LocationHandler h = getHandler(location);
1594        return (h == null ? null : h.inferModuleName());
1595    }
1596
1597    Iterable<Set<Location>> listModuleLocations(Location location) throws IOException {
1598        LocationHandler h = getHandler(location);
1599        return (h == null ? null : h.listModuleLocations());
1600    }
1601
1602    protected LocationHandler getHandler(Location location) {
1603        Objects.requireNonNull(location);
1604        return (location instanceof LocationHandler)
1605                ? (LocationHandler) location
1606                : handlersForLocation.get(location);
1607    }
1608
1609    /**
1610     * Is this the name of an archive file?
1611     */
1612    private boolean isArchive(Path file) {
1613        String n = StringUtils.toLowerCase(file.getFileName().toString());
1614        return fsInfo.isFile(file)
1615                && (n.endsWith(".jar") || n.endsWith(".zip"));
1616    }
1617
1618    static Path normalize(Path p) {
1619        try {
1620            return p.toRealPath();
1621        } catch (IOException e) {
1622            return p.toAbsolutePath().normalize();
1623        }
1624    }
1625
1626}
1627