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