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