/*
 * Copyright (c) 2003, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.tools.javac.file;

import com.sun.tools.javac.code.Lint;
import com.sun.tools.javac.code.Lint.LintCategory;
import com.sun.tools.javac.jvm.ModuleNameReader;
import com.sun.tools.javac.main.Option;
import com.sun.tools.javac.resources.CompilerProperties.Errors;
import com.sun.tools.javac.resources.CompilerProperties.Warnings;
import com.sun.tools.javac.util.DefinedBy;
import com.sun.tools.javac.util.DefinedBy.Api;
import com.sun.tools.javac.util.Iterators;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Log;
import com.sun.tools.javac.util.Pair;
import com.sun.tools.javac.util.StringUtils;

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipFile;

import javax.lang.model.SourceVersion;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileManager.Location;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;

import java9.util.function.Predicate;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
import java9.util.stream.StreamSupport;

import static com.sun.tools.javac.main.Option.BOOT_CLASS_PATH;
import static com.sun.tools.javac.main.Option.ENDORSEDDIRS;
import static com.sun.tools.javac.main.Option.EXTDIRS;
import static com.sun.tools.javac.main.Option.XBOOTCLASSPATH_APPEND;
import static com.sun.tools.javac.main.Option.XBOOTCLASSPATH_PREPEND;

/**
 * This class converts command line arguments, environment variables and system properties (in
 * File.pathSeparator-separated String form) into a boot class path, user class path, and source
 * path (in {@code Collection<String>} form).
 *
 * <p>
 * <b>This is NOT part of any supported API. If you write code that depends on this, you do so at
 * your own risk. This code and its internal interfaces are subject to change or deletion without
 * notice.</b>
 */
public class Locations {
    public static File BOOT_LOCATION =new File("/system/framework/"){
        @Override
        public boolean equals(Object obj) {
            return obj==this;
        }//FIXME, may work if actually occurs
    };

    /**
     * The log to use for warning output
     */
    private Log log;

    /**
     * Access to (possibly cached) file info
     */
    private FSInfo fsInfo;

    /**
     * Whether to warn about non-existent path elements
     */
    private boolean warn;

    private ModuleNameReader moduleNameReader;


    List<Closeable> closeables = new ArrayList<>();
    private Map<String,String> fsEnv = Collections.emptyMap();

    Locations() {
        initHandlers();
    }

    File getFile(String first, String... more) {

        return FileUtils.get(first, more);
    }

    public void close() throws IOException {
        ListBuffer<IOException> list = new ListBuffer<>();
        for(Closeable closeable: closeables){
            try {
                closeable.close();
            } catch (IOException ex) {
                list.add(ex);
            }
        };
        if (list.nonEmpty()) {
            IOException ex = new IOException();
            for (IOException e: list)
                ex.addSuppressed(e);
            throw ex;
        }
    }

    void update(Log log, boolean warn, FSInfo fsInfo) {
        this.log = log;
        this.warn = warn;
        this.fsInfo = fsInfo;
    }

    /**
     * Split a search path into its elements. Empty path elements will be ignored.
     *
     * @param searchFile The search path to be split
     * @return The elements of the path
     */
    private Iterable<File> getFileEntries(String searchFile) {
        return getFileEntries(searchFile, null);
    }

    /**
     * Split a search path into its elements. If emptyFileDefault is not null, all empty elements in the
     * path, including empty elements at either end of the path, will be replaced with the value of
     * emptyFileDefault.
     *
     * @param searchFile The search path to be split
     * @param emptyFileDefault The value to substitute for empty path elements, or null, to ignore
     * empty path elements
     * @return The elements of the path
     */
    private Iterable<File> getFileEntries(String searchFile, File emptyFileDefault) {
        ListBuffer<File> entries = new ListBuffer<>();
        for (String s: searchFile.split(Pattern.quote(File.pathSeparator), -1)) {
            if (s.isEmpty()) {
                if (emptyFileDefault != null) {
                    entries.add(emptyFileDefault);
                }
            } else {
                try {
                    entries.add(getFile(s));
                } catch (IllegalArgumentException e) {
                    if (warn) {
                        log.warning(LintCategory.PATH, Warnings.InvalidPath(s));
                    }
                }
            }
        }
        return entries;
    }

    public void setMultiReleaseValue(String multiReleaseValue) {
        fsEnv = Collections.singletonMap("multi-release", multiReleaseValue);
    }

    private boolean contains(Collection<File> searchFile, File file) throws IOException {

        if (searchFile == null) {
            return false;
        }

        File nf = normalize(file);
        for (File p : searchFile) {
            File np = normalize(p);
            if (FileUtils.startsWith(nf,np)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Utility class to help evaluate a path option. Duplicate entries are ignored, jar class paths
     * can be expanded.
     */
    private class SearchFile extends LinkedHashSet<File> {

        private static final long serialVersionUID = 0;

        private boolean expandJarClassFiles = false;
        private final Set<File> canonicalValues = new HashSet<>();

        public SearchFile expandJarClassFiles(boolean x) {
            expandJarClassFiles = x;
            return this;
        }

        /**
         * What to use when path element is the empty string
         */
        private File emptyFileDefault = null;

        public SearchFile emptyFileDefault(File x) {
            emptyFileDefault = x;
            return this;
        }

        public SearchFile addDirectories(String dirs, boolean warn) {
            boolean prev = expandJarClassFiles;
            expandJarClassFiles = true;
            try {
                if (dirs != null) {
                    for (File dir : getFileEntries(dirs)) {
                        addDirectory(dir, warn);
                    }
                }
                return this;
            } finally {
                expandJarClassFiles = prev;
            }
        }

        public SearchFile addDirectories(String dirs) {
            return addDirectories(dirs, warn);
        }

        private void addDirectory(File dir, boolean warn) {
            if (!dir.isDirectory()) {
                if (warn) {
                    log.warning(Lint.LintCategory.PATH,
                                Warnings.DirPathElementNotFound(dir));
                }
                return;
            }

            try  {
                Stream<File> s = Stream.of(dir.listFiles());
                s.filter(Locations.this::isArchive)
                        .forEach(dirEntry -> addFile(dirEntry, warn));
                s.close();
            } catch (Exception ignore) {
            }
        }

        public SearchFile addFiles(String files, boolean warn) {
            if (files != null) {
                addFiles(getFileEntries(files, emptyFileDefault), warn);
            }
            return this;
        }

        public SearchFile addFiles(String files) {
            return addFiles(files, warn);
        }

        public SearchFile addFiles(Iterable<? extends File> files, boolean warn) {
            if (files != null) {
                for (File file : files) {
                    addFile(file, warn);
                }
            }
            return this;
        }

        public SearchFile addFiles(Iterable<? extends File> files) {
            return addFiles(files, warn);
        }

        public void addFile(File file, boolean warn) {
            if (contains(file)) {
                // discard duplicates
                return;
            }

            if (!fsInfo.exists(file)) {
                /* No such file or directory exists */
                if (warn) {
                    log.warning(Lint.LintCategory.PATH,
                                Warnings.PathElementNotFound(file));
                }
                super.add(file);
                return;
            }

            File canonFile = fsInfo.getCanonicalFile(file);
            if (canonicalValues.contains(canonFile)) {
                /* Discard duplicates and avoid infinite recursion */
                return;
            }

            if (fsInfo.isFile(file)) {
                /* File is an ordinary file. */
                if (!"modules".equals(file.getName())) {
                    if (!isArchive(file)) {
                        /* Not a recognized extension; open it to see if
                         it looks like a valid zip file. */
                        try {
                            new ZipFile(file).close();
                            if (warn) {
                                log.warning(Lint.LintCategory.PATH,
                                        Warnings.UnexpectedArchiveFile(file));
                            }
                        } catch (IOException  e) {
                            // FIXME: include e.getLocalizedMessage in warning
                            if (warn) {
                                log.warning(Lint.LintCategory.PATH,
                                        Warnings.InvalidArchiveFile(file));
                            }
                            return;
                        }
                    }
                }
            }

            /* Now what we have left is either a directory or a file name
             conforming to archive naming convention */
            super.add(file);
            canonicalValues.add(canonFile);

            if (expandJarClassFiles && fsInfo.isFile(file) && !file.getPath().endsWith("modules")) {
                addJarClassFile(file, warn);
            }
        }

        // Adds referenced classpath elements from a jar's Class-File
        // Manifest entry.  In some future release, we may want to
        // update this code to recognize URLs rather than simple
        // filenames, but if we do, we should redo all path-related code.
        private void addJarClassFile(File jarFile, boolean warn) {
            try {
                for (File f : fsInfo.getJarClassPath(jarFile)) {
                    addFile(f, warn);
                }
            } catch (IOException e) {
                log.error(Errors.ErrorReadingFile(jarFile, JavacFileManager.getMessage(e)));
            }
        }
    }

    /**
     * Base class for handling support for the representation of Locations.
     *
     * Locations are (by design) opaque handles that can easily be implemented
     * by enums like StandardLocation. Within JavacFileManager, each Location
     * has an associated LocationHandler, which provides much of the appropriate
     * functionality for the corresponding Location.
     *
     * @see #initHandlers
     * @see #getHandler
     */
    protected static abstract class LocationHandler {

        /**
         * @see JavaFileManager#handleOption
         */
        abstract boolean handleOption(Option option, String value);

        /**
         * @see StandardJavaFileManager#hasLocation
         */
        boolean isSet() {
            return (getFiles() != null);
        }

        /**
         * @see StandardJavaFileManager#getLocation
         */
        abstract Collection<File> getFiles();

        /**
         * @see StandardJavaFileManager#setLocation
         */
        abstract void setFiles(Iterable<? extends File> paths) throws IOException;

        /**
         * @see StandardJavaFileManager#setLocationForModule
         */
        abstract void setFilesForModule(String moduleName, Iterable<? extends File> paths)
                throws IOException;

        /**
         * @see JavaFileManager#getLocationForModule(Location, String)
         */
        Location getLocationForModule(String moduleName) throws IOException {
            return null;
        }

        /**
         * @see JavaFileManager#getLocationForModule(Location, JavaFileObject)
         */
        Location getLocationForModule(File file) throws IOException  {
            return null;
        }

        /**
         * @see JavaFileManager#inferModuleName
         */
        String inferModuleName() {
            return null;
        }

        /**
         * @see JavaFileManager#listLocationsForModules
         */
        Iterable<Set<Location>> listLocationsForModules() throws IOException {
            return Collections.emptyList();
        }

        /**
         * @see JavaFileManager#contains
         */
        abstract boolean contains(File file) throws IOException;
    }

    /**
     * A LocationHandler for a given Location, and associated set of options.
     */
    private static abstract class BasicLocationHandler extends LocationHandler {

        final Location location;
        final Set<Option> options;

        /**
         * Create a handler. The location and options provide a way to map from a location or an
         * option to the corresponding handler.
         *
         * @param location the location for which this is the handler
         * @param options the options affecting this location
         * @see #initHandlers
         */
        protected BasicLocationHandler(Location location, Option... options) {
            this.location = location;
            this.options = options.length == 0
                    ? EnumSet.noneOf(Option.class)
                    : EnumSet.copyOf(Arrays.asList(options));
        }

        @Override
        void setFilesForModule(String moduleName, Iterable<? extends File> files) throws IOException {
            // should not happen: protected by check in JavacFileManager
            throw new UnsupportedOperationException("not supported for " + location);
        }

        protected File checkSingletonDirectory(Iterable<? extends File> paths) throws IOException {
            Iterator<? extends File> pathIter = paths.iterator();
            if (!pathIter.hasNext()) {
                throw new IllegalArgumentException("empty path for directory");
            }
            File path = pathIter.next();
            if (pathIter.hasNext()) {
                throw new IllegalArgumentException("path too long for directory");
            }
            checkDirectory(path);
            return path;
        }

        protected File checkDirectory(File path) throws IOException {
            Objects.requireNonNull(path);
            if (!path.exists()) {
                throw new FileNotFoundException(path + ": does not exist");
            }
            if (!path.isDirectory()) {
                throw new IOException(path + ": not a directory");
            }
            return path;
        }
    }

    /**
     * General purpose implementation for output locations, such as -d/CLASS_OUTPUT and
     * -s/SOURCE_OUTPUT. All options are treated as equivalent (i.e. aliases.)
     * The value is a single file, possibly null.
     */
    private class OutputLocationHandler extends BasicLocationHandler {

        private File outputDir;
        private ModuleTable moduleTable;

        OutputLocationHandler(Location location, Option... options) {
            super(location, options);
        }

        @Override
        boolean handleOption(Option option, String value) {
            if (!options.contains(option)) {
                return false;
            }

            // TODO: could/should validate outputDir exists and is a directory
            // need to decide how best to report issue for benefit of
            // direct API call on JavaFileManager.handleOption(specifies IAE)
            // vs. command line decoding.
            outputDir = (value == null) ? null : getFile(value);
            return true;
        }

        @Override
        Collection<File> getFiles() {
            return (outputDir == null) ? null : Collections.singleton(outputDir);
        }

        @Override
        void setFiles(Iterable<? extends File> paths) throws IOException {
            if (paths == null) {
                outputDir = null;
            } else {
                outputDir = checkSingletonDirectory(paths);
            }
            moduleTable = null;
            listed = false;
        }

        @Override
        Location getLocationForModule(String name) {
            if (moduleTable == null) {
                moduleTable = new ModuleTable();
            }
            ModuleLocationHandler l = moduleTable.get(name);
            if (l == null) {
                File out = FileUtils.resolve(outputDir,name);
                l = new ModuleLocationHandler(this, location.getName() + "[" + name + "]",
                        name, Collections.singletonList(out), true);
                moduleTable.add(l);
            }
            return l;
        }

        @Override
        void setFilesForModule(String name, Iterable<? extends File> paths) throws IOException {
            File out = checkSingletonDirectory(paths);
            if (moduleTable == null) {
                moduleTable = new ModuleTable();
            }
            ModuleLocationHandler l = moduleTable.get(name);
            if (l == null) {
                l = new ModuleLocationHandler(this, location.getName() + "[" + name + "]",
                        name, Collections.singletonList(out), true);
                moduleTable.add(l);
           } else {
                l.searchFile = Collections.singletonList(out);
                moduleTable.updateFiles(l);
            }
        }

        @Override
        Location getLocationForModule(File file) {
            return (moduleTable == null) ? null : moduleTable.get(file);
        }

        private boolean listed;

        @Override
        Iterable<Set<Location>> listLocationsForModules() throws IOException {
            if (!listed && outputDir != null) {
                for (File p : outputDir.listFiles()) {
                    getLocationForModule(p.getPath());
                }

                listed = true;
            }

            if (moduleTable == null || moduleTable.isEmpty())
                return Collections.emptySet();

            return Collections.singleton(moduleTable.locations());
        }

        @Override
        boolean contains(File file) throws IOException {
            if (moduleTable != null) {
                return moduleTable.contains(file);
            } else {
                return (outputDir) != null && normalize(file).getPath().startsWith(normalize(outputDir).getPath());
            }
        }
    }

    /**
     * General purpose implementation for search path locations,
     * such as -sourcepath/SOURCE_PATH and -processorFile/ANNOTATION_PROCESSOR_PATH.
     * All options are treated as equivalent (i.e. aliases.)
     * The value is an ordered set of files and/or directories.
     */
    private class SimpleLocationHandler extends BasicLocationHandler {

        protected Collection<File> searchFile;

        SimpleLocationHandler(Location location, Option... options) {
            super(location, options);
        }

        @Override
        boolean handleOption(Option option, String value) {
            if (!options.contains(option)) {
                return false;
            }
            searchFile = value == null ? null
                    : Collections.unmodifiableCollection(createFile().addFiles(value));
            return true;
        }

        @Override
        Collection<File> getFiles() {
            return searchFile;
        }

        @Override
        void setFiles(Iterable<? extends File> files) {
            SearchFile p;
            if (files == null) {
                p = computeFile(null);
            } else {
                p = createFile().addFiles(files);
            }
            searchFile = Collections.unmodifiableCollection(p);
        }

        protected SearchFile computeFile(String value) {
            return createFile().addFiles(value);
        }

        protected SearchFile createFile() {
            return new SearchFile();
        }

        @Override
        boolean contains(File file) throws IOException {
            return Locations.this.contains(searchFile, file);
        }
    }

    /**
     * Subtype of SimpleLocationHandler for -classpath/CLASS_PATH.
     * If no value is given, a default is provided, based on system properties and other values.
     */
    private class ClassFileLocationHandler extends SimpleLocationHandler {

        ClassFileLocationHandler() {
            super(StandardLocation.CLASS_PATH, Option.CLASS_PATH);
        }

        @Override
        Collection<File> getFiles() {
            lazy();
            return searchFile;
        }

        @Override
        protected SearchFile computeFile(String value) {
            String cp = value;



            // Default to current working directory.
            if (cp == null) {
                cp = ".";
            }

            return createFile().addFiles(cp);
        }

        @Override
        protected SearchFile createFile() {
            return new SearchFile()
                    .expandJarClassFiles(true) // Only search user jars for Class-Files
                    .emptyFileDefault(getFile("."));  // Empty path elt ==> current directory
        }

        private void lazy() {
            if (searchFile == null) {
                setFiles(null);
            }
        }
    }

    /**
     * A LocationHander to represent modules found from a module-oriented
     * location such as MODULE_SOURCE_PATH, UPGRADE_MODULE_PATH,
     * SYSTEM_MODULES and MODULE_PATH.
     *
     * The Location can be specified to accept overriding classes from the
     * {@code --patch-module <module>=<path> } parameter.
     */
    private class ModuleLocationHandler extends LocationHandler implements Location {
        private final LocationHandler parent;
        private final String name;
        private final String moduleName;
        private final boolean output;
        boolean explicit;
        Collection<File> searchFile;

        ModuleLocationHandler(LocationHandler parent, String name, String moduleName,
                Collection<File> searchFile, boolean output) {
            this.parent = parent;
            this.name = name;
            this.moduleName = moduleName;
            this.searchFile = searchFile;
            this.output = output;
        }

        @Override @DefinedBy(Api.COMPILER)
        public String getName() {
            return name;
        }

        @Override @DefinedBy(Api.COMPILER)
        public boolean isOutputLocation() {
            return output;
        }

        @Override // defined by LocationHandler
        boolean handleOption(Option option, String value) {
            throw new UnsupportedOperationException();
        }

        @Override // defined by LocationHandler
        Collection<File> getFiles() {
            return Collections.unmodifiableCollection(searchFile);
        }

        @Override // defined by LocationHandler
        void setFiles(Iterable<? extends File> paths) throws IOException {
            // defer to the parent to determine if this is acceptable
            parent.setFilesForModule(moduleName, paths);
        }

        @Override // defined by LocationHandler
        void setFilesForModule(String moduleName, Iterable<? extends File> paths) {
            throw new UnsupportedOperationException("not supported for " + name);
        }

        @Override // defined by LocationHandler
        String inferModuleName() {
            return moduleName;
        }

        @Override
        boolean contains(File file) throws IOException {
            return Locations.this.contains(searchFile, file);
        }

        @Override
        public String toString() {
            return name;
        }
    }

    /**
     * A table of module location handlers, indexed by name and path.
     */
    private class ModuleTable {
        private final Map<String, ModuleLocationHandler> nameMap = new LinkedHashMap<>();
        private final Map<File, ModuleLocationHandler> pathMap = new LinkedHashMap<>();

        void add(ModuleLocationHandler h) {
            nameMap.put(h.moduleName, h);
            for (File p : h.searchFile) {
                pathMap.put(normalize(p), h);
            }
        }

        void updateFiles(ModuleLocationHandler h) {
            // use iterator, to be able to remove old entries
            for (Iterator<Map.Entry<File, ModuleLocationHandler>> iter = pathMap.entrySet().iterator();
                    iter.hasNext(); ) {
                Map.Entry<File, ModuleLocationHandler> e = iter.next();
                if (e.getValue() == h) {
                    iter.remove();
                }
            }
            for (File p : h.searchFile) {
                pathMap.put(normalize(p), h);
            }
        }

        ModuleLocationHandler get(String name) {
            return nameMap.get(name);
        }

        ModuleLocationHandler get(File path) {
            while (path != null) {
                ModuleLocationHandler l = pathMap.get(path);

                if (l != null)
                    return l;

                path = path.getParentFile();
            }

            return null;
        }

        void clear() {
            nameMap.clear();
            pathMap.clear();
        }

        boolean isEmpty() {
            return nameMap.isEmpty();
        }

        boolean contains(File file) throws IOException {
            return Locations.this.contains(pathMap.keySet(), file);
        }

        Set<Location> locations() {
            return Collections.unmodifiableSet(StreamSupport.stream(nameMap.values()).collect(Collectors.toSet()));
        }

        Set<Location> explicitLocations() {
            return Collections.unmodifiableSet(StreamSupport.stream(nameMap.entrySet())
                                                      .filter(e -> e.getValue().explicit)
                                                      .map(e -> e.getValue())
                                                      .collect(Collectors.toSet()));
        }
    }

    /**
     * A LocationHandler for simple module-oriented search paths,
     * like UPGRADE_MODULE_PATH and MODULE_PATH.
     */
    private class ModuleFileLocationHandler extends SimpleLocationHandler {
        private ModuleTable moduleTable;

        ModuleFileLocationHandler(Location location, Option... options) {
            super(location, options);
        }

        @Override
        public boolean handleOption(Option option, String value) {
            if (!options.contains(option)) {
                return false;
            }
            setFiles(value == null ? null : getFileEntries(value));
            return true;
        }

        @Override
        public Location getLocationForModule(String moduleName) {
            initModuleLocations();
            return moduleTable.get(moduleName);
        }

        @Override
        public Location getLocationForModule(File file) {
            initModuleLocations();
            return moduleTable.get(file);
        }

        @Override
        Iterable<Set<Location>> listLocationsForModules() {
            Set<Location> explicitLocations = moduleTable != null ?
                    moduleTable.explicitLocations() : Collections.emptySet();
            Iterable<Set<Location>> explicitLocationsList = !explicitLocations.isEmpty()
                    ? Collections.singletonList(explicitLocations)
                    : Collections.emptyList();

            if (searchFile == null)
                return explicitLocationsList;

            Iterable<Set<Location>> searchFileLocations =
                    () -> new ModuleFileIterator();
            return () -> Iterators.createCompoundIterator(Arrays.asList(explicitLocationsList,
                                                                        searchFileLocations),
                                                          Iterable::iterator);
        }

        @Override
        boolean contains(File file) throws IOException {
            if (moduleTable == null) {
                initModuleLocations();
            }
            return moduleTable.contains(file);
        }

        @Override
        void setFiles(Iterable<? extends File> paths) {
            if (paths != null) {
                for (File p: paths) {
                    checkValidModuleFileEntry(p);
                }
            }
            super.setFiles(paths);
            moduleTable = null;
        }

        @Override
        void setFilesForModule(String name, Iterable<? extends File> paths) throws IOException {
            List<File> checkedFiles = checkFiles(paths);
            // how far should we go to validate the paths provide a module?
            // e.g. contain module-info with the correct name?
            initModuleLocations();
            ModuleLocationHandler l = moduleTable.get(name);
            if (l == null) {
                l = new ModuleLocationHandler(this, location.getName() + "[" + name + "]",
                        name, checkedFiles, true);
                moduleTable.add(l);
           } else {
                l.searchFile = checkedFiles;
                moduleTable.updateFiles(l);
            }
            l.explicit = true;
        }

        private List<File> checkFiles(Iterable<? extends File> paths) throws IOException {
            Objects.requireNonNull(paths);
            List<File> validFiles = new ArrayList<>();
            for (File p : paths) {
                validFiles.add(checkDirectory(p));
            }
            return validFiles;
        }

        private void initModuleLocations() {
            if (moduleTable != null) {
                return;
            }

            moduleTable = new ModuleTable();

            for (Set<Location> set : listLocationsForModules()) {
                for (Location locn : set) {
                    if (locn instanceof ModuleLocationHandler) {
                        ModuleLocationHandler l = (ModuleLocationHandler) locn;
                        if (!moduleTable.nameMap.containsKey(l.moduleName)) {
                            moduleTable.add(l);
                        }
                    }
                }
            }
        }

        private void checkValidModuleFileEntry(File p) {
            if (!p.exists()) {
                // warning may be generated later
                return;
            }

            if (p.isDirectory()) {
                // either an exploded module or a directory of modules
                return;
            }

            String name = p.getPath();
            int lastDot = name.lastIndexOf(".");
            if (lastDot > 0) {
                switch (name.substring(lastDot)) {
                    case ".jar":
                    case ".jmod":
                        return;
                }
            }
            throw new IllegalArgumentException(p.toString());
        }

        class ModuleFileIterator implements Iterator<Set<Location>> {
            Iterator<File> pathIter = searchFile.iterator();
            int pathIndex = 0;
            Set<Location> next = null;

            @Override
            public boolean hasNext() {
                if (next != null)
                    return true;

                while (next == null) {
                    if (pathIter.hasNext()) {
                        File path = pathIter.next();
                        if (path.isDirectory()) {
                            next = scanDirectory(path);
                        } else {
                            next = scanFile(path);
                        }
                        pathIndex++;
                    } else
                        return false;
                }
                return true;
            }

            @Override
            public Set<Location> next() {
                hasNext();
                if (next != null) {
                    Set<Location> result = next;
                    next = null;
                    return result;
                }
                throw new NoSuchElementException();
            }

            private Set<Location> scanDirectory(File path) {
                Set<File> paths = new LinkedHashSet<>();
                File moduleInfoClass = null;
                try  {
                    for (File entry: path.listFiles()) {
                        if (entry.getPath().endsWith("module-info.class")) {
                            moduleInfoClass = entry;
                            break;  // no need to continue scanning
                        }
                        paths.add(entry);
                    }
                } catch ( Exception ignored) {
                    log.error(Errors.LocnCantReadDirectory(path));
                    return Collections.emptySet();
                }

                if (moduleInfoClass != null) {
                    // It's an exploded module directly on the module path.
                    // We can't infer module name from the directory name, so have to
                    // read module-info.class.
                    try {
                        String moduleName = readModuleName(moduleInfoClass);
                        String name = location.getName()
                                + "[" + pathIndex + ":" + moduleName + "]";
                        ModuleLocationHandler l = new ModuleLocationHandler(
                                ModuleFileLocationHandler.this, name, moduleName,
                                Collections.singletonList(path), false);
                        return Collections.singleton(l);
                    } catch (ModuleNameReader.BadClassFile e) {
                        log.error(Errors.LocnBadModuleInfo(path));
                        return Collections.emptySet();
                    } catch (IOException e) {
                        log.error(Errors.LocnCantReadFile(path));
                        return Collections.emptySet();
                    }
                }

                // A directory of modules
                Set<Location> result = new LinkedHashSet<>();
                int index = 0;
                for (File entry : paths) {
                    Pair<String,File> module = inferModuleName(entry);
                    if (module == null) {
                        // diagnostic reported if necessary; skip to next
                        continue;
                    }
                    String moduleName = module.fst;
                    File moduleFile = module.snd;
                    String name = location.getName()
                            + "[" + pathIndex + "." + (index++) + ":" + moduleName + "]";
                    ModuleLocationHandler l = new ModuleLocationHandler(
                            ModuleFileLocationHandler.this, name, moduleName,
                            Collections.singletonList(moduleFile), false);
                    result.add(l);
                }
                return result;
            }

            private Set<Location> scanFile(File path) {
                Pair<String,File> module = inferModuleName(path);
                if (module == null) {
                    // diagnostic reported if necessary
                    return Collections.emptySet();
                }
                String moduleName = module.fst;
                File moduleFile = module.snd;
                String name = location.getName()
                        + "[" + pathIndex + ":" + moduleName + "]";
                ModuleLocationHandler l = new ModuleLocationHandler(
                        ModuleFileLocationHandler.this, name, moduleName,
                        Collections.singletonList(moduleFile), false);
                return Collections.singleton(l);
            }

            private Pair<String,File> inferModuleName(File p) {
                if (p.isDirectory()) {
                    if (FileUtils.resolve(p,"module-info.class").exists()) {
                        String name = p.getName();
                        if (SourceVersion.isName(name))
                            return new Pair<>(name, p);
                    }
                    return null;
                }

                if (p.getPath().endsWith(".jar") && fsInfo.exists(p)) {

                    try  {
                        OrderedZipFile zipFile = new OrderedZipFile(p.getPath());
                        File moduleInfoClass = zipFile.getEntryFile("module-info.class");
                        if (moduleInfoClass.exists()) {
                            String moduleName = readModuleName(moduleInfoClass);
                            return new Pair<>(moduleName, p);
                        }
                        File mf = zipFile.getEntryFile("META-INF/MANIFEST.MF");
                        if (mf.exists()) {
                            try (InputStream in = FileUtils.newInputStream(mf)) {
                                Manifest man = new Manifest(in);
                                Attributes attrs = man.getMainAttributes();
                                if (attrs != null) {
                                    String moduleName = attrs.getValue(new Attributes.Name("Automatic-Module-Name"));
                                    if (moduleName != null) {
                                        if (isModuleName(moduleName)) {
                                            return new Pair<>(moduleName, p);
                                        } else {
                                            log.error(Errors.LocnCantGetModuleNameForJar(p));
                                            return null;
                                        }
                                    }
                                }
                            }
                        }
                    } catch (ModuleNameReader.BadClassFile e) {
                        log.error(Errors.LocnBadModuleInfo(p));
                        return null;
                    } catch (IOException e) {
                        log.error(Errors.LocnCantReadFile(p));
                        return null;
                    }

                    //automatic module:
                    String fn = p.getName();
                    //from ModuleFile.deriveModuleDescriptor:

                    // drop .jar
                    String mn = fn.substring(0, fn.length()-4);

                    // find first occurrence of -${NUMBER}. or -${NUMBER}$
                    Matcher matcher = Pattern.compile("-(\\d+(\\.|$))").matcher(mn);
                    if (matcher.find()) {
                        int start = matcher.start();

                        mn = mn.substring(0, start);
                    }

                    // finally clean up the module name
                    mn =  mn.replaceAll("[^A-Za-z0-9]", ".")  // replace non-alphanumeric
                            .replaceAll("(\\.)(\\1)+", ".")   // collapse repeating dots
                            .replaceAll("^\\.", "")           // drop leading dots
                            .replaceAll("\\.$", "");          // drop trailing dots


                    if (!mn.isEmpty()) {
                        return new Pair<>(mn, p);
                    }

                    log.error(Errors.LocnCantGetModuleNameForJar(p));
                    return null;
                }

                if (warn && false) {  // temp disable, when enabled, massage examples.not-yet.txt suitably.
                    log.warning(Warnings.LocnUnknownFileOnModulePath(p));
                }
                return null;
            }

            private String readModuleName(File path) throws IOException, ModuleNameReader.BadClassFile {
                if (moduleNameReader == null)
                    moduleNameReader = new ModuleNameReader();
                return moduleNameReader.readModuleName(path);
            }
        }

        //from jdk.internal.module.Checks:
        /**
         * Returns {@code true} if the given name is a legal module name.
         */
        private boolean isModuleName(String name) {
            int next;
            int off = 0;
            while ((next = name.indexOf('.', off)) != -1) {
                String id = name.substring(off, next);
                if (!SourceVersion.isName(id))
                    return false;
                off = next+1;
            }
            String last = name.substring(off);
            return SourceVersion.isName(last);
        }
    }

    private class ModuleSourceFileLocationHandler extends BasicLocationHandler {
        private ModuleTable moduleTable;
        private List<File> paths;

        ModuleSourceFileLocationHandler() {
            super(StandardLocation.MODULE_SOURCE_PATH,
                    Option.MODULE_SOURCE_PATH);
        }

        @Override
        boolean handleOption(Option option, String value) {
            init(value);
            return true;
        }

        void init(String value) {
            Collection<String> segments = new ArrayList<>();
            for (String s: value.split(File.pathSeparator)) {
                expandBraces(s, segments);
            }

            Map<String, List<File>> map = new LinkedHashMap<>();
            List<File> noSuffixFiles = new ArrayList<>();
            boolean anySuffix = false;
            final String MARKER = "*";
            for (String seg: segments) {
                int markStart = seg.indexOf(MARKER);
                if (markStart == -1) {
                    File p = getFile(seg);
                    add(map, p, null);
                    noSuffixFiles.add(p);
                } else {
                    if (markStart == 0 || !isSeparator(seg.charAt(markStart - 1))) {
                        throw new IllegalArgumentException("illegal use of " + MARKER + " in " + seg);
                    }
                    File prefix = getFile(seg.substring(0, markStart - 1));
                    File suffix;
                    int markEnd = markStart + MARKER.length();
                    if (markEnd == seg.length()) {
                        suffix = null;
                    } else if (!isSeparator(seg.charAt(markEnd))
                            || seg.indexOf(MARKER, markEnd) != -1) {
                        throw new IllegalArgumentException("illegal use of " + MARKER + " in " + seg);
                    } else {
                        suffix = getFile(seg.substring(markEnd + 1));
                        anySuffix = true;
                    }
                    add(map, prefix, suffix);
                    if (suffix == null) {
                        noSuffixFiles.add(prefix);
                    }
                }
            }

            initModuleTable(map);
            paths = anySuffix ? null : noSuffixFiles;
        }

        private void initModuleTable(Map<String, List<File>> map) {
            moduleTable = new ModuleTable();
            for(Map.Entry<String,List<File>> entry:map.entrySet()) {
                String modName=entry.getKey();
                List<File> modFile=entry.getValue();
                boolean hasModuleInfo = StreamSupport.stream(modFile).anyMatch(checkModuleInfo);
                if (hasModuleInfo) {
                    String locnName = location.getName() + "[" + modName + "]";
                    ModuleLocationHandler l = new ModuleLocationHandler(this, locnName, modName,
                            modFile, false);
                    moduleTable.add(l);
                }
            }
        }
        //where:
            private final Predicate<File> checkModuleInfo =
                    p -> FileUtils.resolve(p,"module-info.java").exists();


        private boolean isSeparator(char ch) {
            // allow both separators on Windows
            return (ch == File.separatorChar) || (ch == '/');
        }

        void add(Map<String, List<File>> map, File prefix, File suffix) {
            if (!prefix.isDirectory()) {
                if (warn) {
                    String key = prefix.exists()
                            ? "dir.path.element.not.directory"
                            : "dir.path.element.not.found";
                    log.warning(Lint.LintCategory.PATH, key, prefix);
                }
                return;
            }

                for (File entry: prefix.listFiles()) {
                    if(!entry.isDirectory()) continue;
                    File path = (suffix == null) ? entry : FileUtils.resolve(entry,suffix.getPath());
                    if (path.isDirectory()) {
                        String name = entry.getName();
                        List<File> paths = map.get(name);
                        if (paths == null)
                            map.put(name, paths = new ArrayList<>());
                        paths.add(path);
                    }
                }
        }

        private void expandBraces(String value, Collection<String> results) {
            int depth = 0;
            int start = -1;
            String prefix = null;
            String suffix = null;
            for (int i = 0; i < value.length(); i++) {
                switch (value.charAt(i)) {
                    case '{':
                        depth++;
                        if (depth == 1) {
                            prefix = value.substring(0, i);
                            suffix = value.substring(getMatchingBrace(value, i) + 1);
                            start = i + 1;
                        }
                        break;

                    case ',':
                        if (depth == 1) {
                            String elem = value.substring(start, i);
                            expandBraces(prefix + elem + suffix, results);
                            start = i + 1;
                        }
                        break;

                    case '}':
                        switch (depth) {
                            case 0:
                                throw new IllegalArgumentException("mismatched braces");

                            case 1:
                                String elem = value.substring(start, i);
                                expandBraces(prefix + elem + suffix, results);
                                return;

                            default:
                                depth--;
                        }
                        break;
                }
            }
            if (depth > 0)
                throw new IllegalArgumentException("mismatched braces");
            results.add(value);
        }

        int getMatchingBrace(String value, int offset) {
            int depth = 1;
            for (int i = offset + 1; i < value.length(); i++) {
                switch (value.charAt(i)) {
                    case '{':
                        depth++;
                        break;

                    case '}':
                        if (--depth == 0)
                            return i;
                        break;
                }
            }
            throw new IllegalArgumentException("mismatched braces");
        }

        @Override
        boolean isSet() {
            return (moduleTable != null);
        }

        @Override
        Collection<File> getFiles() {
            if (paths == null) {
                // This may occur for a complex setting with --module-source-path option
                // i.e. one that cannot be represented by a simple series of paths.
                throw new IllegalStateException("paths not available");
            }
            return paths;
        }

        @Override
        void setFiles(Iterable<? extends File> files) throws IOException {
            Map<String, List<File>> map = new LinkedHashMap<>();
            List<File> newFiles = new ArrayList<>();
            for (File file : files) {
                add(map, file, null);
                newFiles.add(file);
            }

            initModuleTable(map);
            paths = Collections.unmodifiableList(newFiles);
        }

        @Override
        void setFilesForModule(String name, Iterable<? extends File> paths) throws IOException {
            List<File> validFiles = checkFiles(paths);

            if (moduleTable == null)
                moduleTable = new ModuleTable();

            ModuleLocationHandler l = moduleTable.get(name);
            if (l == null) {
                l = new ModuleLocationHandler(this,
                        location.getName() + "[" + name + "]",
                        name,
                        validFiles,
                        true);
                moduleTable.add(l);
           } else {
                l.searchFile = validFiles;
                moduleTable.updateFiles(l);
            }
        }

        private List<File> checkFiles(Iterable<? extends File> paths) throws IOException {
            Objects.requireNonNull(paths);
            List<File> validFiles = new ArrayList<>();
            for (File p : paths) {
                validFiles.add(checkDirectory(p));
            }
            return validFiles;
        }

        @Override
        Location getLocationForModule(String name) {
            return (moduleTable == null) ? null : moduleTable.get(name);
        }

        @Override
        Location getLocationForModule(File file) {
            return (moduleTable == null) ? null : moduleTable.get(file);
        }

        @Override
        Iterable<Set<Location>> listLocationsForModules() {
            if (moduleTable == null)
                return Collections.emptySet();

            return Collections.singleton(moduleTable.locations());
        }

        @Override
        boolean contains(File file) throws IOException {
            return (moduleTable == null) ? false : moduleTable.contains(file);
        }

    }

    private class PatchModulesLocationHandler extends BasicLocationHandler {
        private final ModuleTable moduleTable = new ModuleTable();

        PatchModulesLocationHandler() {
            super(StandardLocation.PATCH_MODULE_PATH, Option.PATCH_MODULE);
        }

        @Override
        boolean handleOption(Option option, String value) {
            if (!options.contains(option)) {
                return false;
            }

            moduleTable.clear();

            // Allow an extended syntax for --patch-module consisting of a series
            // of values separated by NULL characters. This is to facilitate
            // supporting deferred file manager options on the command line.
            // See Option.PATCH_MODULE for the code that composes these multiple
            // values.
            for (String v : value.split("\0")) {
                int eq = v.indexOf('=');
                if (eq > 0) {
                    String moduleName = v.substring(0, eq);
                    SearchFile mPatchFile = new SearchFile()
                            .addFiles(v.substring(eq + 1));
                    String name = location.getName() + "[" + moduleName + "]";
                    ModuleLocationHandler h = new ModuleLocationHandler(this, name,
                            moduleName, mPatchFile, false);
                    moduleTable.add(h);
                } else {
                    // Should not be able to get here;
                    // this should be caught and handled in Option.PATCH_MODULE
                    log.error(Errors.LocnInvalidArgForXpatch(value));
                }
            }

            return true;
        }

        @Override
        boolean isSet() {
            return !moduleTable.isEmpty();
        }

        @Override
        Collection<File> getFiles() {
            throw new UnsupportedOperationException();
        }

        @Override
        void setFiles(Iterable<? extends File> files) throws IOException {
            throw new UnsupportedOperationException();
        }

        @Override // defined by LocationHandler
        void setFilesForModule(String moduleName, Iterable<? extends File> files) throws IOException {
            throw new UnsupportedOperationException(); // not yet
        }

        @Override
        Location getLocationForModule(String name) throws IOException {
            return moduleTable.get(name);
        }

        @Override
        Location getLocationForModule(File file) throws IOException {
            return moduleTable.get(file);
        }

        @Override
        Iterable<Set<Location>> listLocationsForModules() throws IOException {
            return Collections.singleton(moduleTable.locations());
        }

        @Override
        boolean contains(File file) throws IOException {
            return moduleTable.contains(file);
        }
    }

    /**
     * Split a search path into its elements. Empty path elements will be ignored.
     *
     * @param searchPath The search path to be split
     * @return The elements of the path
     */
    private Iterable<File> getPathEntries(String searchPath) {
        return getPathEntries(searchPath, null);
    }

    /**
     * Split a search path into its elements. If emptyPathDefault is not null, all empty elements in the
     * path, including empty elements at either end of the path, will be replaced with the value of
     * emptyPathDefault.
     *
     * @param searchPath The search path to be split
     * @param emptyPathDefault The value to substitute for empty path elements, or null, to ignore
     * empty path elements
     * @return The elements of the path
     */
    private Iterable<File> getPathEntries(String searchPath, File emptyPathDefault) {
        ListBuffer<File> entries = new ListBuffer<>();
        for (String s: searchPath.split(Pattern.quote(File.pathSeparator), -1)) {
            if (s.isEmpty()) {
                if (emptyPathDefault != null) {
                    entries.add(emptyPathDefault);
                }
            } else {
                try {
                    entries.add(new File(s));
                } catch (IllegalArgumentException e) {
                    if (warn) {
                        log.warning(LintCategory.PATH, Warnings.InvalidPath(s));
                    }
                }
            }
        }
        return entries;
    }
    /**
     * Utility class to help evaluate a path option. Duplicate entries are ignored, jar class paths
     * can be expanded.
     */
    private class SearchPath extends LinkedHashSet<File> {

        private static final long serialVersionUID = 0;

        private boolean expandJarClassPaths = false;
        private final Set<File> canonicalValues = new HashSet<>();

        public SearchPath expandJarClassPaths(boolean x) {
            expandJarClassPaths = x;
            return this;
        }

        /**
         * What to use when path element is the empty string
         */
        private File emptyPathDefault = null;

        public SearchPath emptyPathDefault(File x) {
            emptyPathDefault = x;
            return this;
        }

        public SearchPath addDirectories(String dirs, boolean warn) {
            boolean prev = expandJarClassPaths;
            expandJarClassPaths = true;
            try {
                if (dirs != null) {
                    for (File dir : getPathEntries(dirs)) {
                        addDirectory(dir, warn);
                    }
                }
                return this;
            } finally {
                expandJarClassPaths = prev;
            }
        }

        public SearchPath addDirectories(String dirs) {
            return addDirectories(dirs, warn);
        }

        private void addDirectory(File dir, boolean warn) {
            if (!dir.isDirectory()) {
                if (warn) {
                    log.warning(Lint.LintCategory.PATH,
                            Warnings.DirPathElementNotFound(dir));
                }
                return;
            }

            for (File dirEntry: dir.listFiles(Locations.this::isArchive)) {
                addFile(dirEntry, warn);
            }
        }

        public SearchPath addFiles(String files, boolean warn) {
            if (files != null) {
                addFiles(getPathEntries(files, emptyPathDefault), warn);
            }
            return this;
        }

        public SearchPath addFiles(String files) {
            return addFiles(files, warn);
        }

        public SearchPath addFiles(Iterable<? extends File> files, boolean warn) {
            if (files != null) {
                for (File file : files) {
                    addFile(file, warn);
                }
            }
            return this;
        }

        public SearchPath addFiles(Iterable<? extends File> files) {
            return addFiles(files, warn);
        }

        public void addFile(File file, boolean warn) {
            if (contains(file)) {
                // discard duplicates
                return;
            }

            if (!fsInfo.exists(file)) {
                /* No such file or directory exists */
                if (warn) {
                    log.warning(Lint.LintCategory.PATH,
                            Warnings.PathElementNotFound(file));
                }
                super.add(file);
                return;
            }

            File canonFile = fsInfo.getCanonicalFile(file);
            if (canonicalValues.contains(canonFile)) {
                /* Discard duplicates and avoid infinite recursion */
                return;
            }


            /* Now what we have left is either a directory or a file name
             conforming to archive naming convention */
            super.add(file);
            canonicalValues.add(canonFile);

            if (expandJarClassPaths && fsInfo.isFile(file) ) {
                addJarClassPath(file, warn);
            }
        }

        // Adds referenced classpath elements from a jar's Class-Path
        // Manifest entry.  In some future release, we may want to
        // update this code to recognize URLs rather than simple
        // filenames, but if we do, we should redo all path-related code.
        private void addJarClassPath(File jarFile, boolean warn) {
            try {
                for (File f : fsInfo.getJarClassPath(jarFile)) {
                    addFile(f, warn);
                }
            } catch (IOException e) {
                log.error(Errors.ErrorReadingFile(jarFile, JavacFileManager.getMessage(e)));
            }
        }
    }

    /**
     * Custom subtype of LocationHandler for PLATFORM_CLASS_PATH.
     * Various options are supported for different components of the
     * platform class path.
     * Setting a value with setLocation overrides all existing option values.
     * Setting any option overrides any value set with setLocation, and
     * reverts to using default values for options that have not been set.
     * Setting -bootclasspath or -Xbootclasspath overrides any existing
     * value for -Xbootclasspath/p: and -Xbootclasspath/a:.
     */
    private class BootClassPathLocationHandler extends BasicLocationHandler implements Location{

        private Collection<File> searchPath;
        final Map<Option, String> optionValues = new EnumMap<>(Option.class);

        /**
         * Is the bootclasspath the default?
         */
        private boolean isDefault;

        BootClassPathLocationHandler(StandardLocation location) {
            super(location,
                    BOOT_CLASS_PATH, Option.XBOOTCLASSPATH,
                    XBOOTCLASSPATH_PREPEND,
                    XBOOTCLASSPATH_APPEND,
                    ENDORSEDDIRS, Option.DJAVA_ENDORSED_DIRS,
                    EXTDIRS, Option.DJAVA_EXT_DIRS);
        }

        boolean isDefault() {
            lazy();
            return isDefault;
        }

        @Override
        boolean handleOption(Option option, String value) {
            if (!options.contains(option)) {
                return false;
            }


            option = canonicalize(option);
            optionValues.put(option, value);
            if (option == BOOT_CLASS_PATH) {
                optionValues.remove(XBOOTCLASSPATH_PREPEND);
                optionValues.remove(XBOOTCLASSPATH_APPEND);
            }
            searchPath = null;  // reset to "uninitialized"
            return true;
        }
        // where
        // TODO: would be better if option aliasing was handled at a higher
        // level
        private Option canonicalize(Option option) {
            switch (option) {
                case XBOOTCLASSPATH:
                    return BOOT_CLASS_PATH;
                case DJAVA_ENDORSED_DIRS:
                    return ENDORSEDDIRS;
                case DJAVA_EXT_DIRS:
                    return EXTDIRS;
                default:
                    return option;
            }
        }

        @Override
        Collection<File> getFiles() {
            lazy();
            return searchPath;
        }

        @Override
        void setFiles(Iterable<? extends File> files) {
            if (files == null) {
                searchPath = null;  // reset to "uninitialized"
            } else {
                isDefault = false;
                SearchPath p = new SearchPath().addFiles(files, false);
                searchPath = Collections.unmodifiableCollection(p);
                optionValues.clear();
            }
        }

        @Override
        String inferModuleName() {
            return "java.base";
        }

        @Override
        Iterable<Set<Location>> listLocationsForModules() throws IOException {
            return Collections.singleton(Collections.singleton(this));
        }

        @Override
        Location getLocationForModule(String moduleName) throws IOException {
            if(moduleName.equals("java.base"))
                return this;
            return super.getLocationForModule(moduleName);
        }

        @Override
        Location getLocationForModule(File file) throws IOException {
            if(BOOT_LOCATION.equals(file))
                return this;
            return super.getLocationForModule(file);
        }

        SearchPath computePath() throws IOException {
            SearchPath path = new SearchPath();

            String bootclasspathOpt = optionValues.get(BOOT_CLASS_PATH);
            String endorseddirsOpt = optionValues.get(ENDORSEDDIRS);
            String extdirsOpt = optionValues.get(EXTDIRS);
            String xbootclasspathPrependOpt = optionValues.get(XBOOTCLASSPATH_PREPEND);
            String xbootclasspathAppendOpt = optionValues.get(XBOOTCLASSPATH_APPEND);
            path.addFiles(xbootclasspathPrependOpt);

            if (endorseddirsOpt != null) {
                path.addDirectories(endorseddirsOpt);
            } else {
                path.addDirectories(System.getProperty("java.endorsed.dirs"), false);
            }

            if (bootclasspathOpt != null) {
                path.addFiles(bootclasspathOpt);
            }else path.addFile(BOOT_LOCATION,false);


            path.addFiles(xbootclasspathAppendOpt);

            // Strictly speaking, standard extensions are not bootstrap
            // classes, but we treat them identically, so we'll pretend
            // that they are.
            if (extdirsOpt != null) {
                path.addDirectories(extdirsOpt);
            }

            isDefault =
                    (xbootclasspathPrependOpt == null)
                            && (bootclasspathOpt == null)
                            && (xbootclasspathAppendOpt == null);

            return path;
        }



        private void lazy() {
            if (searchPath == null) {
                try {
                    searchPath = Collections.unmodifiableCollection(computePath());
                } catch (IOException e) {
                    // TODO: need better handling here, e.g. javac Abort?
                    throw new RuntimeException(e);
                }
            }
        }

        @Override
        boolean contains(File file) throws IOException {
            return Locations.this.contains(searchPath, file);
        }

        @Override
        public String getName() {
            return BOOT_LOCATION.getPath();
        }

        @Override
        public boolean isOutputLocation() {
            return false;
        }
    }

    Map<Location, LocationHandler> handlersForLocation;
    Map<Option, LocationHandler> handlersForOption;

    void initHandlers() {
        handlersForLocation = new HashMap<>();
        handlersForOption = new EnumMap<>(Option.class);

        BasicLocationHandler[] handlers = {
            new BootClassPathLocationHandler(StandardLocation.PLATFORM_CLASS_PATH),
            new BootClassPathLocationHandler(StandardLocation.SYSTEM_MODULES),
            new ClassFileLocationHandler(),
            new SimpleLocationHandler(StandardLocation.SOURCE_PATH, Option.SOURCE_PATH),
            new SimpleLocationHandler(StandardLocation.ANNOTATION_PROCESSOR_PATH, Option.PROCESSOR_PATH),
            new SimpleLocationHandler(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, Option.PROCESSOR_MODULE_PATH),
            new OutputLocationHandler(StandardLocation.CLASS_OUTPUT, Option.D),
            new OutputLocationHandler(StandardLocation.SOURCE_OUTPUT, Option.S),
            new OutputLocationHandler(StandardLocation.NATIVE_HEADER_OUTPUT, Option.H),
            new ModuleSourceFileLocationHandler(),
            new PatchModulesLocationHandler(),
            new ModuleFileLocationHandler(StandardLocation.UPGRADE_MODULE_PATH, Option.UPGRADE_MODULE_PATH),
            new ModuleFileLocationHandler(StandardLocation.MODULE_PATH, Option.MODULE_PATH)
        };

        for (BasicLocationHandler h : handlers) {
            handlersForLocation.put(h.location, h);
            for (Option o : h.options) {
                handlersForOption.put(o, h);
            }
        }
    }

    boolean handleOption(Option option, String value) {
        LocationHandler h = handlersForOption.get(option);
        return (h == null ? false : h.handleOption(option, value));
    }

    boolean hasLocation(Location location) {
        LocationHandler h = getHandler(location);
        return (h == null ? false : h.isSet());
    }

    Collection<File> getLocation(Location location) {
        LocationHandler h = getHandler(location);
        return (h == null ? null : h.getFiles());
    }

    File getOutputLocation(Location location) {
        if (!location.isOutputLocation()) {
            throw new IllegalArgumentException();
        }
        LocationHandler h = getHandler(location);
        return ((OutputLocationHandler) h).outputDir;
    }

    void setLocation(Location location, Iterable<? extends File> files) throws IOException {
        LocationHandler h = getHandler(location);
        if (h == null) {
            if (location.isOutputLocation()) {
                h = new OutputLocationHandler(location);
            } else {
                h = new SimpleLocationHandler(location);
            }
            handlersForLocation.put(location, h);
        }
        h.setFiles(files);
    }

    Location getLocationForModule(Location location, String name) throws IOException {
        LocationHandler h = getHandler(location);
        return (h == null ? null : h.getLocationForModule(name));
    }

    Location getLocationForModule(Location location, File file) throws IOException {
        LocationHandler h = getHandler(location);
        return (h == null ? null : h.getLocationForModule(file));
    }

    void setLocationForModule(Location location, String moduleName,
            Iterable<? extends File> files) throws IOException {
        LocationHandler h = getHandler(location);
        if (h == null) {
            if (location.isOutputLocation()) {
                h = new OutputLocationHandler(location);
            } else {
                h = new ModuleFileLocationHandler(location);
            }
            handlersForLocation.put(location, h);
        }
        h.setFilesForModule(moduleName, files);
    }

    String inferModuleName(Location location) {
        LocationHandler h = getHandler(location);
        return (h == null ? null : h.inferModuleName());
    }

    Iterable<Set<Location>> listLocationsForModules(Location location) throws IOException {
        LocationHandler h = getHandler(location);
        return (h == null ? null : h.listLocationsForModules());
    }

    boolean contains(Location location, File file) throws IOException {
        LocationHandler h = getHandler(location);
        if (h == null)
            throw new IllegalArgumentException("unknown location");
        return h.contains(file);
    }

    protected LocationHandler getHandler(Location location) {
        Objects.requireNonNull(location);
        return (location instanceof LocationHandler)
                ? (LocationHandler) location
                : handlersForLocation.get(location);
    }

    /**
     * Is this the name of an archive file?
     */
    private boolean isArchive(File file) {
        String n = StringUtils.toLowerCase(file.getName());
        return fsInfo.isFile(file)
                && (n.endsWith(".jar") || n.endsWith(".zip"));
    }

    static File normalize(File p) {
        try {
            return p.getCanonicalFile();
        } catch (IOException e) {
            return p.getAbsoluteFile();
        }
    }
}