org.eclipse.jdt.internal.compiler.batch.FileSystem.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.jdt.internal.compiler.batch.FileSystem.java

Source

/*******************************************************************************
 * Copyright (c) 2000, 2019 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Stephan Herrmann - Contribution for
 *                        Bug 440687 - [compiler][batch][null] improve command line option for external annotations
 *******************************************************************************/
package org.eclipse.jdt.internal.compiler.batch;

import java.io.File;
import java.io.IOException;
import java.nio.file.InvalidPathException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.zip.ZipFile;

import javax.lang.model.SourceVersion;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.CompilationResult;
import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
import org.eclipse.jdt.internal.compiler.classfmt.ExternalAnnotationDecorator;
import org.eclipse.jdt.internal.compiler.env.AccessRuleSet;
import org.eclipse.jdt.internal.compiler.env.IModulePathEntry;
import org.eclipse.jdt.internal.compiler.env.IModule;
import org.eclipse.jdt.internal.compiler.env.IModuleAwareNameEnvironment;
import org.eclipse.jdt.internal.compiler.env.NameEnvironmentAnswer;
import org.eclipse.jdt.internal.compiler.lookup.ModuleBinding;
import org.eclipse.jdt.internal.compiler.parser.Parser;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
import org.eclipse.jdt.internal.compiler.problem.ProblemReporter;
import org.eclipse.jdt.internal.compiler.env.IUpdatableModule;
import org.eclipse.jdt.internal.compiler.env.IUpdatableModule.UpdateKind;
import org.eclipse.jdt.internal.compiler.env.IUpdatableModule.UpdatesByKind;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.util.JRTUtil;
import org.eclipse.jdt.internal.compiler.util.SuffixConstants;
import org.eclipse.jdt.internal.compiler.util.Util;

public class FileSystem implements IModuleAwareNameEnvironment, SuffixConstants {

    // Keep the type as ArrayList and not List as there are clients that are already written to expect ArrayList.
    public static ArrayList<FileSystem.Classpath> EMPTY_CLASSPATH = new ArrayList<>();

    /**
     * A <code>Classpath</code>, even though an IModuleLocation, can represent a plain
     * classpath location too. The FileSystem tells the Classpath whether to behave as a module or regular class
     * path via {@link Classpath#acceptModule(IModule)}.
     *
     * Sub types of classpath are responsible for appropriate behavior based on this.
     */
    public interface Classpath extends IModulePathEntry {
        char[][][] findTypeNames(String qualifiedPackageName, String moduleName);

        NameEnvironmentAnswer findClass(char[] typeName, String qualifiedPackageName, String moduleName,
                String qualifiedBinaryFileName);

        NameEnvironmentAnswer findClass(char[] typeName, String qualifiedPackageName, String moduleName,
                String qualifiedBinaryFileName, boolean asBinaryOnly);

        boolean isPackage(String qualifiedPackageName, /*@Nullable*/String moduleName);

        default boolean hasModule() {
            return getModule() != null;
        }

        default boolean hasCUDeclaringPackage(String qualifiedPackageName,
                Function<CompilationUnit, String> pkgNameExtractor) {
            return hasCompilationUnit(qualifiedPackageName, null);
        }

        /**
         * Return a list of the jar file names defined in the Class-Path section
         * of the jar file manifest if any, null else. Only ClasspathJar (and
         * extending classes) instances may return a non-null result.
         * @param  problemReporter problem reporter with which potential
         *         misconfiguration issues are raised
         * @return a list of the jar file names defined in the Class-Path
         *         section of the jar file manifest if any
         */
        List<Classpath> fetchLinkedJars(ClasspathSectionProblemReporter problemReporter);

        /**
         * This method resets the environment. The resulting state is equivalent to
         * a new name environment without creating a new object.
         */
        void reset();

        /**
         * Return a normalized path for file based classpath entries. This is an
         * absolute path in which file separators are transformed to the
         * platform-agnostic '/', ending with a '/' for directories. This is an
         * absolute path in which file separators are transformed to the
         * platform-agnostic '/', deprived from the '.jar' (resp. '.zip')
         * extension for jar (resp. zip) files.
         * @return a normalized path for file based classpath entries
         */
        char[] normalizedPath();

        /**
         * Return the path for file based classpath entries. This is an absolute path
         * ending with a file separator for directories, an absolute path including the '.jar'
         * (resp. '.zip') extension for jar (resp. zip) files.
         * @return the path for file based classpath entries
         */
        String getPath();

        /**
         * Initialize the entry
         */
        void initialize() throws IOException;

        /**
         * Can the current location provide an external annotation file for the given type?
         * @param qualifiedTypeName type name in qualified /-separated notation.
         */
        boolean hasAnnotationFileFor(String qualifiedTypeName);

        /**
         * Accepts to represent a module location with the given module description.
         *
         * @param module
         */
        public void acceptModule(IModule module);

        public String getDestinationPath();

        Collection<String> getModuleNames(Collection<String> limitModules);

        Collection<String> getModuleNames(Collection<String> limitModules, Function<String, IModule> getModule);
    }

    public interface ClasspathSectionProblemReporter {
        void invalidClasspathSection(String jarFilePath);

        void multipleClasspathSections(String jarFilePath);
    }

    /**
     * This class is defined how to normalize the classpath entries.
     * It removes duplicate entries.
     */
    public static class ClasspathNormalizer {
        /**
         * Returns the normalized classpath entries (no duplicate).
         * <p>The given classpath entries are FileSystem.Classpath. We check the getPath() in order to find
         * duplicate entries.</p>
         *
         * @param classpaths the given classpath entries
         * @return the normalized classpath entries
         */
        public static ArrayList<Classpath> normalize(ArrayList<Classpath> classpaths) {
            ArrayList<Classpath> normalizedClasspath = new ArrayList<>();
            HashSet<Classpath> cache = new HashSet<>();
            for (Iterator<Classpath> iterator = classpaths.iterator(); iterator.hasNext();) {
                FileSystem.Classpath classpath = iterator.next();
                if (!cache.contains(classpath)) {
                    normalizedClasspath.add(classpath);
                    cache.add(classpath);
                }
            }
            return normalizedClasspath;
        }
    }

    protected Classpath[] classpaths;
    // Used only in single-module mode when the module descriptor is
    // provided via command line.
    protected IModule module;
    Set<String> knownFileNames;
    protected boolean annotationsFromClasspath; // should annotation files be read from the classpath (vs. explicit separate path)?
    private static HashMap<File, Classpath> JRT_CLASSPATH_CACHE = null;
    protected Map<String, Classpath> moduleLocations = new HashMap<>();

    /** Tasks resulting from --add-reads or --add-exports command line options. */
    Map<String, UpdatesByKind> moduleUpdates = new HashMap<>();
    static boolean isJRE12Plus = false;

    private boolean hasLimitModules = false;

    static {
        try {
            isJRE12Plus = SourceVersion.valueOf("RELEASE_12") != null; //$NON-NLS-1$
        } catch (IllegalArgumentException iae) {
            // fall back to default
        }
    }

    /*
       classPathNames is a collection is Strings representing the full path of each class path
       initialFileNames is a collection is Strings, the trailing '.java' will be removed if its not already.
    */
    public FileSystem(String[] classpathNames, String[] initialFileNames, String encoding) {
        this(classpathNames, initialFileNames, encoding, null);
    }

    protected FileSystem(String[] classpathNames, String[] initialFileNames, String encoding,
            Collection<String> limitModules) {
        final int classpathSize = classpathNames.length;
        this.classpaths = new Classpath[classpathSize];
        int counter = 0;
        this.hasLimitModules = limitModules != null && !limitModules.isEmpty();
        for (int i = 0; i < classpathSize; i++) {
            Classpath classpath = getClasspath(classpathNames[i], encoding, null, null, null);
            try {
                classpath.initialize();
                for (String moduleName : classpath.getModuleNames(limitModules))
                    this.moduleLocations.put(moduleName, classpath);
                this.classpaths[counter++] = classpath;
            } catch (IOException e) {
                // ignore
            }
        }
        if (counter != classpathSize) {
            System.arraycopy(this.classpaths, 0, (this.classpaths = new Classpath[counter]), 0, counter);
        }
        initializeKnownFileNames(initialFileNames);
    }

    protected FileSystem(Classpath[] paths, String[] initialFileNames, boolean annotationsFromClasspath,
            Set<String> limitedModules) {
        final int length = paths.length;
        int counter = 0;
        this.classpaths = new FileSystem.Classpath[length];
        this.hasLimitModules = limitedModules != null && !limitedModules.isEmpty();
        for (int i = 0; i < length; i++) {
            final Classpath classpath = paths[i];
            try {
                classpath.initialize();
                for (String moduleName : classpath.getModuleNames(limitedModules))
                    this.moduleLocations.put(moduleName, classpath);
                this.classpaths[counter++] = classpath;
            } catch (IOException | InvalidPathException exception) {
                // JRE 9 could throw an IAE if the linked JAR paths have invalid chars, such as ":"
                // ignore
            }
        }
        if (counter != length) {
            // should not happen
            System.arraycopy(this.classpaths, 0, (this.classpaths = new FileSystem.Classpath[counter]), 0, counter);
        }
        initializeModuleLocations(limitedModules);
        initializeKnownFileNames(initialFileNames);
        this.annotationsFromClasspath = annotationsFromClasspath;
    }

    private void initializeModuleLocations(Set<String> limitedModules) {
        // First create the mapping of all module/Classpath
        // since the second iteration of getModuleNames() can't be relied on for 
        // to get the right origin of module
        if (limitedModules == null) {
            for (Classpath c : this.classpaths) {
                for (String moduleName : c.getModuleNames(null))
                    this.moduleLocations.put(moduleName, c);
            }
        } else {
            Map<String, Classpath> moduleMap = new HashMap<>();
            for (Classpath c : this.classpaths) {
                for (String moduleName : c.getModuleNames(null)) {
                    moduleMap.put(moduleName, c);
                }
            }
            for (Classpath c : this.classpaths) {
                for (String moduleName : c.getModuleNames(limitedModules,
                        m -> getModuleFromEnvironment(m.toCharArray()))) {
                    Classpath classpath = moduleMap.get(moduleName);
                    this.moduleLocations.put(moduleName, classpath);
                }
            }
        }
    }

    protected FileSystem(Classpath[] paths, String[] initialFileNames, boolean annotationsFromClasspath) {
        this(paths, initialFileNames, annotationsFromClasspath, null);
    }

    public static Classpath getClasspath(String classpathName, String encoding, AccessRuleSet accessRuleSet) {
        return getClasspath(classpathName, encoding, false, accessRuleSet, null, null, null);
    }

    public static Classpath getClasspath(String classpathName, String encoding, AccessRuleSet accessRuleSet,
            Map<String, String> options, String release) {
        return getClasspath(classpathName, encoding, false, accessRuleSet, null, options, release);
    }

    public static Classpath getJrtClasspath(String jdkHome, String encoding, AccessRuleSet accessRuleSet,
            Map<String, String> options) {
        return new ClasspathJrt(new File(convertPathSeparators(jdkHome)), true, accessRuleSet, null);
    }

    public static Classpath getOlderSystemRelease(String jdkHome, String release, AccessRuleSet accessRuleSet) {
        return isJRE12Plus
                ? new ClasspathJep247Jdk12(new File(convertPathSeparators(jdkHome)), release, accessRuleSet)
                : new ClasspathJep247(new File(convertPathSeparators(jdkHome)), release, accessRuleSet);
    }

    public static Classpath getClasspath(String classpathName, String encoding, boolean isSourceOnly,
            AccessRuleSet accessRuleSet, String destinationPath, Map<String, String> options, String release) {
        Classpath result = null;
        File file = new File(convertPathSeparators(classpathName));
        if (file.isDirectory()) {
            if (file.exists()) {
                result = new ClasspathDirectory(file, encoding,
                        isSourceOnly ? ClasspathLocation.SOURCE
                                : ClasspathLocation.SOURCE | ClasspathLocation.BINARY,
                        accessRuleSet, destinationPath == null || destinationPath == Main.NONE ? destinationPath : // keep == comparison valid
                                convertPathSeparators(destinationPath),
                        options);
            }
        } else {
            int format = Util.archiveFormat(classpathName);
            if (format == Util.ZIP_FILE) {
                if (isSourceOnly) {
                    // source only mode
                    result = new ClasspathSourceJar(file, true, accessRuleSet, encoding,
                            destinationPath == null || destinationPath == Main.NONE ? destinationPath : // keep == comparison valid
                                    convertPathSeparators(destinationPath));
                } else if (destinationPath == null) {
                    // class file only mode
                    if (classpathName.endsWith(JRTUtil.JRT_FS_JAR)) {
                        if (JRT_CLASSPATH_CACHE == null) {
                            JRT_CLASSPATH_CACHE = new HashMap<>();
                        } else {
                            result = JRT_CLASSPATH_CACHE.get(file);
                        }
                        if (result == null) {
                            result = new ClasspathJrt(file, true, accessRuleSet, null);
                            try {
                                result.initialize();
                            } catch (IOException e) {
                                // Broken entry, but let clients have it anyway.
                            }
                            JRT_CLASSPATH_CACHE.put(file, result);
                        }
                    } else {
                        result = (release == null) ? new ClasspathJar(file, true, accessRuleSet, null)
                                : new ClasspathMultiReleaseJar(file, true, accessRuleSet, destinationPath, release);
                    }
                }
            } else if (format == Util.JMOD_FILE) {
                return new ClasspathJmod(file, true, accessRuleSet, null);
            }

        }
        return result;
    }

    private void initializeKnownFileNames(String[] initialFileNames) {
        if (initialFileNames == null) {
            this.knownFileNames = new HashSet<>(0);
            return;
        }
        this.knownFileNames = new HashSet<>(initialFileNames.length * 2);
        for (int i = initialFileNames.length; --i >= 0;) {
            File compilationUnitFile = new File(initialFileNames[i]);
            char[] fileName = null;
            try {
                fileName = compilationUnitFile.getCanonicalPath().toCharArray();
            } catch (IOException e) {
                // this should not happen as the file exists
                continue;
            }
            char[] matchingPathName = null;
            final int lastIndexOf = CharOperation.lastIndexOf('.', fileName);
            if (lastIndexOf != -1) {
                fileName = CharOperation.subarray(fileName, 0, lastIndexOf);
            }
            CharOperation.replace(fileName, '\\', '/');
            boolean globalPathMatches = false;
            // the most nested path should be the selected one
            for (int j = 0, max = this.classpaths.length; j < max; j++) {
                char[] matchCandidate = this.classpaths[j].normalizedPath();
                boolean currentPathMatch = false;
                if (this.classpaths[j] instanceof ClasspathDirectory
                        && CharOperation.prefixEquals(matchCandidate, fileName)) {
                    currentPathMatch = true;
                    if (matchingPathName == null) {
                        matchingPathName = matchCandidate;
                    } else {
                        if (currentPathMatch) {
                            // we have a second source folder that matches the path of the source file
                            if (matchCandidate.length > matchingPathName.length) {
                                // we want to preserve the shortest possible path
                                matchingPathName = matchCandidate;
                            }
                        } else {
                            // we want to preserve the shortest possible path
                            if (!globalPathMatches && matchCandidate.length < matchingPathName.length) {
                                matchingPathName = matchCandidate;
                            }
                        }
                    }
                    if (currentPathMatch) {
                        globalPathMatches = true;
                    }
                }
            }
            if (matchingPathName == null) {
                this.knownFileNames.add(new String(fileName)); // leave as is...
            } else {
                this.knownFileNames.add(
                        new String(CharOperation.subarray(fileName, matchingPathName.length, fileName.length)));
            }
            matchingPathName = null;
        }
    }

    /** TESTS ONLY */
    public void scanForModules(Parser parser) {
        for (int i = 0, max = this.classpaths.length; i < max; i++) {
            File file = new File(this.classpaths[i].getPath());
            IModule iModule = ModuleFinder.scanForModule(this.classpaths[i], file, parser, false, null);
            if (iModule != null)
                this.moduleLocations.put(String.valueOf(iModule.name()), this.classpaths[i]);
        }
    }

    @Override
    public void cleanup() {
        for (int i = 0, max = this.classpaths.length; i < max; i++)
            this.classpaths[i].reset();
    }

    private static String convertPathSeparators(String path) {
        return File.separatorChar == '/' ? path.replace('\\', '/') : path.replace('/', '\\');
    }

    private NameEnvironmentAnswer findClass(String qualifiedTypeName, char[] typeName, boolean asBinaryOnly,
            /*NonNull*/char[] moduleName) {
        NameEnvironmentAnswer answer = internalFindClass(qualifiedTypeName, typeName, asBinaryOnly, moduleName);
        if (this.annotationsFromClasspath && answer != null && answer.getBinaryType() instanceof ClassFileReader) {
            for (int i = 0, length = this.classpaths.length; i < length; i++) {
                Classpath classpathEntry = this.classpaths[i];
                if (classpathEntry.hasAnnotationFileFor(qualifiedTypeName)) {
                    // in case of 'this.annotationsFromClasspath' we indeed search for .eea entries inside the main zipFile of the entry:
                    ZipFile zip = classpathEntry instanceof ClasspathJar ? ((ClasspathJar) classpathEntry).zipFile
                            : null;
                    boolean shouldClose = false; // don't close classpathEntry.zipFile, which we don't own
                    try {
                        if (zip == null) {
                            zip = ExternalAnnotationDecorator.getAnnotationZipFile(classpathEntry.getPath(), null);
                            shouldClose = true;
                        }
                        answer.setBinaryType(ExternalAnnotationDecorator.create(answer.getBinaryType(),
                                classpathEntry.getPath(), qualifiedTypeName, zip));
                        return answer;
                    } catch (IOException e) {
                        // ignore broken entry, keep searching
                    } finally {
                        if (shouldClose && zip != null)
                            try {
                                zip.close();
                            } catch (IOException e) {
                                /* nothing */ }
                    }
                }
            }
            // globally configured (annotationsFromClasspath), but no .eea found, decorate in order to answer NO_EEA_FILE:
            answer.setBinaryType(new ExternalAnnotationDecorator(answer.getBinaryType(), null));
        }
        return answer;
    }

    private NameEnvironmentAnswer internalFindClass(String qualifiedTypeName, char[] typeName, boolean asBinaryOnly,
            /*NonNull*/char[] moduleName) {
        if (this.knownFileNames.contains(qualifiedTypeName))
            return null; // looking for a file which we know was provided at the beginning of the compilation

        String qualifiedBinaryFileName = qualifiedTypeName + SUFFIX_STRING_class;
        String qualifiedPackageName = qualifiedTypeName.length() == typeName.length ? Util.EMPTY_STRING
                : qualifiedBinaryFileName.substring(0, qualifiedTypeName.length() - typeName.length - 1);

        LookupStrategy strategy = LookupStrategy.get(moduleName);
        if (strategy == LookupStrategy.Named) {
            if (this.moduleLocations != null) {
                // searching for a specific named module:
                String moduleNameString = String.valueOf(moduleName);
                Classpath classpath = this.moduleLocations.get(moduleNameString);
                if (classpath != null) {
                    return classpath.findClass(typeName, qualifiedPackageName, moduleNameString,
                            qualifiedBinaryFileName);
                }
            }
            return null;
        }
        String qp2 = File.separatorChar == '/' ? qualifiedPackageName
                : qualifiedPackageName.replace('/', File.separatorChar);
        NameEnvironmentAnswer suggestedAnswer = null;
        if (qualifiedPackageName == qp2) {
            for (int i = 0, length = this.classpaths.length; i < length; i++) {
                if (!strategy.matches(this.classpaths[i], Classpath::hasModule))
                    continue;
                NameEnvironmentAnswer answer = this.classpaths[i].findClass(typeName, qualifiedPackageName, null,
                        qualifiedBinaryFileName, asBinaryOnly);
                if (answer != null) {
                    if (answer.moduleName() != null
                            && !this.moduleLocations.containsKey(String.valueOf(answer.moduleName())))
                        continue; // type belongs to an unobservable module
                    if (!answer.ignoreIfBetter()) {
                        if (answer.isBetter(suggestedAnswer))
                            return answer;
                    } else if (answer.isBetter(suggestedAnswer))
                        // remember suggestion and keep looking
                        suggestedAnswer = answer;
                }
            }
        } else {
            String qb2 = qualifiedBinaryFileName.replace('/', File.separatorChar);
            for (int i = 0, length = this.classpaths.length; i < length; i++) {
                Classpath p = this.classpaths[i];
                if (!strategy.matches(p, Classpath::hasModule))
                    continue;
                NameEnvironmentAnswer answer = !(p instanceof ClasspathDirectory)
                        ? p.findClass(typeName, qualifiedPackageName, null, qualifiedBinaryFileName, asBinaryOnly)
                        : p.findClass(typeName, qp2, null, qb2, asBinaryOnly);
                if (answer != null) {
                    if (answer.moduleName() != null
                            && !this.moduleLocations.containsKey(String.valueOf(answer.moduleName())))
                        continue; // type belongs to an unobservable module
                    if (!answer.ignoreIfBetter()) {
                        if (answer.isBetter(suggestedAnswer))
                            return answer;
                    } else if (answer.isBetter(suggestedAnswer))
                        // remember suggestion and keep looking
                        suggestedAnswer = answer;
                }
            }
        }
        return suggestedAnswer;
    }

    @Override
    public NameEnvironmentAnswer findType(char[][] compoundName, char[] moduleName) {
        if (compoundName != null)
            return findClass(new String(CharOperation.concatWith(compoundName, '/')),
                    compoundName[compoundName.length - 1], false, moduleName);
        return null;
    }

    public char[][][] findTypeNames(char[][] packageName) {
        char[][][] result = null;
        if (packageName != null) {
            String qualifiedPackageName = new String(CharOperation.concatWith(packageName, '/'));
            String qualifiedPackageName2 = File.separatorChar == '/' ? qualifiedPackageName
                    : qualifiedPackageName.replace('/', File.separatorChar);
            if (qualifiedPackageName == qualifiedPackageName2) {
                for (int i = 0, length = this.classpaths.length; i < length; i++) {
                    char[][][] answers = this.classpaths[i].findTypeNames(qualifiedPackageName, null);
                    if (answers != null) {
                        // concat with previous answers
                        if (result == null) {
                            result = answers;
                        } else {
                            int resultLength = result.length;
                            int answersLength = answers.length;
                            System.arraycopy(result, 0, (result = new char[answersLength + resultLength][][]), 0,
                                    resultLength);
                            System.arraycopy(answers, 0, result, resultLength, answersLength);
                        }
                    }
                }
            } else {
                for (int i = 0, length = this.classpaths.length; i < length; i++) {
                    Classpath p = this.classpaths[i];
                    char[][][] answers = !(p instanceof ClasspathDirectory)
                            ? p.findTypeNames(qualifiedPackageName, null)
                            : p.findTypeNames(qualifiedPackageName2, null);
                    if (answers != null) {
                        // concat with previous answers
                        if (result == null) {
                            result = answers;
                        } else {
                            int resultLength = result.length;
                            int answersLength = answers.length;
                            System.arraycopy(result, 0, (result = new char[answersLength + resultLength][][]), 0,
                                    resultLength);
                            System.arraycopy(answers, 0, result, resultLength, answersLength);
                        }
                    }
                }
            }
        }
        return result;
    }

    @Override
    public NameEnvironmentAnswer findType(char[] typeName, char[][] packageName, char[] moduleName) {
        if (typeName != null)
            return findClass(new String(CharOperation.concatWith(packageName, typeName, '/')), typeName, false,
                    moduleName);
        return null;
    }

    @Override
    public char[][] getModulesDeclaringPackage(char[][] packageName, char[] moduleName) {
        String qualifiedPackageName = new String(CharOperation.concatWith(packageName, '/'));
        String moduleNameString = String.valueOf(moduleName);

        LookupStrategy strategy = LookupStrategy.get(moduleName);
        if (strategy == LookupStrategy.Named) {
            if (this.moduleLocations != null) {
                // specific search in a given module:
                Classpath classpath = this.moduleLocations.get(moduleNameString);
                if (classpath != null) {
                    if (classpath.isPackage(qualifiedPackageName, moduleNameString))
                        return new char[][] { moduleName };
                }
            }
            return null;
        }
        // search the entire environment and answer which modules declare that package:
        char[][] allNames = null;
        boolean hasUnobserable = false;
        for (Classpath cp : this.classpaths) {
            if (strategy.matches(cp, Classpath::hasModule)) {
                if (strategy == LookupStrategy.Unnamed) {
                    // short-cut
                    if (cp.isPackage(qualifiedPackageName, moduleNameString))
                        return new char[][] { ModuleBinding.UNNAMED };
                } else {
                    char[][] declaringModules = cp.getModulesDeclaringPackage(qualifiedPackageName, null);
                    if (declaringModules != null) {
                        if (cp instanceof ClasspathJrt && this.hasLimitModules) {
                            declaringModules = filterModules(declaringModules);
                            hasUnobserable |= declaringModules == null;
                        }
                        if (allNames == null)
                            allNames = declaringModules;
                        else
                            allNames = CharOperation.arrayConcat(allNames, declaringModules);
                    }
                }
            }
        }
        if (allNames == null && hasUnobserable)
            return new char[][] { ModuleBinding.UNOBSERVABLE };
        return allNames;
    }

    private char[][] filterModules(char[][] declaringModules) {
        char[][] filtered = Arrays.stream(declaringModules)
                .filter(m -> this.moduleLocations.containsKey(new String(m))).toArray(char[][]::new);
        if (filtered.length == 0)
            return null;
        return filtered;
    }

    private Parser getParser() {
        Map<String, String> opts = new HashMap<String, String>();
        opts.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_9);
        return new Parser(new ProblemReporter(DefaultErrorHandlingPolicies.exitOnFirstError(),
                new CompilerOptions(opts), new DefaultProblemFactory(Locale.getDefault())), false);
    }

    @Override
    public boolean hasCompilationUnit(char[][] qualifiedPackageName, char[] moduleName, boolean checkCUs) {
        String qPackageName = String.valueOf(CharOperation.concatWith(qualifiedPackageName, '/'));
        String moduleNameString = String.valueOf(moduleName);
        LookupStrategy strategy = LookupStrategy.get(moduleName);
        Parser parser = checkCUs ? getParser() : null;
        Function<CompilationUnit, String> pkgNameExtractor = (sourceUnit) -> {
            String pkgName = null;
            CompilationResult compilationResult = new CompilationResult(sourceUnit, 0, 0, 1);
            char[][] name = parser.parsePackageDeclaration(sourceUnit.getContents(), compilationResult);
            if (name != null) {
                pkgName = CharOperation.toString(name);
            }
            return pkgName;
        };
        switch (strategy) {
        case Named:
            if (this.moduleLocations != null) {
                Classpath location = this.moduleLocations.get(moduleNameString);
                if (location != null)
                    return checkCUs ? location.hasCUDeclaringPackage(qPackageName, pkgNameExtractor)
                            : location.hasCompilationUnit(qPackageName, moduleNameString);
            }
            return false;
        default:
            for (int i = 0; i < this.classpaths.length; i++) {
                Classpath location = this.classpaths[i];
                if (strategy.matches(location, Classpath::hasModule))
                    if (location.hasCompilationUnit(qPackageName, moduleNameString))
                        return true;
            }
            return false;
        }
    }

    @Override
    public IModule getModule(char[] name) {
        if (this.module != null && CharOperation.equals(name, this.module.name())) {
            return this.module;
        }
        if (this.moduleLocations.containsKey(new String(name))) {
            for (Classpath classpath : this.classpaths) {
                IModule mod = classpath.getModule(name);
                if (mod != null) {
                    return mod;
                }
            }
        }
        return null;
    }

    public IModule getModuleFromEnvironment(char[] name) {
        if (this.module != null && CharOperation.equals(name, this.module.name())) {
            return this.module;
        }
        for (Classpath classpath : this.classpaths) {
            IModule mod = classpath.getModule(name);
            if (mod != null) {
                return mod;
            }
        }
        return null;
    }

    @Override
    public char[][] getAllAutomaticModules() {
        Set<char[]> set = new HashSet<>();
        for (int i = 0, l = this.classpaths.length; i < l; i++) {
            if (this.classpaths[i].isAutomaticModule()) {
                set.add(this.classpaths[i].getModule().name());
            }
        }
        return set.toArray(new char[set.size()][]);
    }

    @Override
    public char[][] listPackages(char[] moduleName) {
        switch (LookupStrategy.get(moduleName)) {
        case Named:
            Classpath classpath = this.moduleLocations.get(new String(moduleName));
            if (classpath != null)
                return classpath.listPackages();
            return CharOperation.NO_CHAR_CHAR;
        default:
            throw new UnsupportedOperationException("can list packages only of a named module"); //$NON-NLS-1$
        }
    }

    void addModuleUpdate(String moduleName, Consumer<IUpdatableModule> update, UpdateKind kind) {
        UpdatesByKind updates = this.moduleUpdates.get(moduleName);
        if (updates == null) {
            this.moduleUpdates.put(moduleName, updates = new UpdatesByKind());
        }
        updates.getList(kind, true).add(update);
    }

    @Override
    public void applyModuleUpdates(IUpdatableModule compilerModule, IUpdatableModule.UpdateKind kind) {
        char[] name = compilerModule.name();
        if (name != ModuleBinding.UNNAMED) { // can't update the unnamed module
            UpdatesByKind updates = this.moduleUpdates.get(String.valueOf(name));
            if (updates != null) {
                for (Consumer<IUpdatableModule> update : updates.getList(kind, false))
                    update.accept(compilerModule);
            }
        }
    }
}