/*
 * Copyright (c) 2015, 2024, 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 jdk.internal.loader;

import java.io.IOException;
import java.lang.module.Configuration;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleReader;
import java.lang.module.ModuleReference;
import java.lang.module.ResolvedModule;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.ByteBuffer;
import java.security.CodeSigner;
import java.security.CodeSource;
import java.security.SecureClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;

import jdk.internal.access.SharedSecrets;
import jdk.internal.module.Resources;

/**
 * A class loader that loads classes and resources from a collection of
 * modules, or from a single module where the class loader is a member
 * of a pool of class loaders.
 *
 * <p> The delegation model used by this ClassLoader differs to the regular
 * delegation model. When requested to load a class then this ClassLoader first
 * maps the class name to its package name. If there a module defined to the
 * Loader containing the package then the class loader attempts to load from
 * that module. If the package is instead defined to a module in a "remote"
 * ClassLoader then this class loader delegates directly to that class loader.
 * The map of package name to remote class loader is created based on the
 * modules read by modules defined to this class loader. If the package is not
 * local or remote then this class loader will delegate to the parent class
 * loader. This allows automatic modules (for example) to link to types in the
 * unnamed module of the parent class loader.
 *
 * @see ModuleLayer#defineModulesWithOneLoader
 * @see ModuleLayer#defineModulesWithManyLoaders
 */

public final class Loader extends SecureClassLoader {

    static {
        ClassLoader.registerAsParallelCapable();
    }

    // the pool this loader is a member of; can be null
    private final LoaderPool pool;

    // parent ClassLoader, can be null
    private final ClassLoader parent;

    // maps a module name to a module reference
    private final Map<String, ModuleReference> nameToModule;

    // maps package name to a module loaded by this class loader
    private final Map<String, LoadedModule> localPackageToModule;

    // maps package name to a remote class loader, populated post initialization
    private final Map<String, ClassLoader> remotePackageToLoader
        = new ConcurrentHashMap<>();

    // maps a module reference to a module reader, populated lazily
    private final Map<ModuleReference, ModuleReader> moduleToReader
        = new ConcurrentHashMap<>();

    /**
     * A module defined/loaded to a {@code Loader}.
     */
    private static class LoadedModule {
        private final ModuleReference mref;
        private final CodeSource cs;

        LoadedModule(ModuleReference mref) {
            URL url = null;
            if (mref.location().isPresent()) {
                try {
                    url = mref.location().get().toURL();
                } catch (MalformedURLException | IllegalArgumentException e) { }
            }
            this.mref = mref;
            this.cs = new CodeSource(url, (CodeSigner[]) null);
        }

        ModuleReference mref() { return mref; }
        String name() { return mref.descriptor().name(); }
        CodeSource codeSource() { return cs; }
    }


    /**
     * Creates a {@code Loader} in a loader pool that loads classes/resources
     * from one module.
     */
    public Loader(ResolvedModule resolvedModule,
                  LoaderPool pool,
                  ClassLoader parent)
    {
        super("Loader-" + resolvedModule.name(), parent);

        this.pool = pool;
        this.parent = parent;

        ModuleReference mref = resolvedModule.reference();
        ModuleDescriptor descriptor = mref.descriptor();
        String mn = descriptor.name();
        this.nameToModule = Map.of(mn, mref);

        Map<String, LoadedModule> localPackageToModule = new HashMap<>();
        LoadedModule lm = new LoadedModule(mref);
        descriptor.packages().forEach(pn -> localPackageToModule.put(pn, lm));
        this.localPackageToModule = localPackageToModule;
    }

    /**
     * Creates a {@code Loader} that loads classes/resources from a collection
     * of modules.
     *
     * @throws IllegalArgumentException
     *         If two or more modules have the same package
     */
    public Loader(Collection<ResolvedModule> modules, ClassLoader parent) {
        super(parent);

        this.pool = null;
        this.parent = parent;

        Map<String, ModuleReference> nameToModule = new HashMap<>();
        Map<String, LoadedModule> localPackageToModule = new HashMap<>();
        for (ResolvedModule resolvedModule : modules) {
            ModuleReference mref = resolvedModule.reference();
            ModuleDescriptor descriptor = mref.descriptor();
            nameToModule.put(descriptor.name(), mref);
            descriptor.packages().forEach(pn -> {
                LoadedModule lm = new LoadedModule(mref);
                if (localPackageToModule.put(pn, lm) != null)
                    throw new IllegalArgumentException("Package "
                        + pn + " in more than one module");
            });
        }
        this.nameToModule = nameToModule;
        this.localPackageToModule = localPackageToModule;
    }

    /**
     * Completes initialization of this Loader. This method populates
     * remotePackageToLoader with the packages of the remote modules, where
     * "remote modules" are the modules read by modules defined to this loader.
     *
     * @param cf the Configuration containing at least modules to be defined to
     *           this class loader
     *
     * @param parentModuleLayers the parent ModuleLayers
     */
    public Loader initRemotePackageMap(Configuration cf,
                                       List<ModuleLayer> parentModuleLayers)
    {
        for (String name : nameToModule.keySet()) {
            ResolvedModule resolvedModule = cf.findModule(name).get();
            assert resolvedModule.configuration() == cf;

            for (ResolvedModule other : resolvedModule.reads()) {
                String mn = other.name();
                ClassLoader loader;

                if (other.configuration() == cf) {

                    // The module reads another module in the newly created
                    // layer. If all modules are defined to the same class
                    // loader then the packages are local.
                    if (pool == null) {
                        assert nameToModule.containsKey(mn);
                        continue;
                    }

                    loader = pool.loaderFor(mn);
                    assert loader != null;

                } else {

                    // find the layer for the target module
                    ModuleLayer layer = parentModuleLayers.stream()
                        .map(parent -> findModuleLayer(parent, other.configuration()))
                        .flatMap(Optional::stream)
                        .findAny()
                        .orElseThrow(() ->
                            new InternalError("Unable to find parent layer"));

                    // find the class loader for the module
                    // For now we use the platform loader for modules defined to the
                    // boot loader
                    assert layer.findModule(mn).isPresent();
                    loader = layer.findLoader(mn);
                    if (loader == null)
                        loader = ClassLoaders.platformClassLoader();
                }

                // find the packages that are exported to the target module
                ModuleDescriptor descriptor = other.reference().descriptor();
                if (descriptor.isAutomatic()) {
                    ClassLoader l = loader;
                    descriptor.packages().forEach(pn -> remotePackage(pn, l));
                } else {
                    String target = resolvedModule.name();
                    for (ModuleDescriptor.Exports e : descriptor.exports()) {
                        boolean delegate;
                        if (e.isQualified()) {
                            // qualified export in same configuration
                            delegate = (other.configuration() == cf)
                                    && e.targets().contains(target);
                        } else {
                            // unqualified
                            delegate = true;
                        }

                        if (delegate) {
                            remotePackage(e.source(), loader);
                        }
                    }
                }
            }

        }

        return this;
    }

    /**
     * Adds to remotePackageToLoader so that an attempt to load a class in
     * the package delegates to the given class loader.
     *
     * @throws IllegalStateException
     *         if the package is already mapped to a different class loader
     */
    private void remotePackage(String pn, ClassLoader loader) {
        ClassLoader l = remotePackageToLoader.putIfAbsent(pn, loader);
        if (l != null && l != loader) {
            throw new IllegalStateException("Package "
                + pn + " cannot be imported from multiple loaders");
        }
    }


    /**
     * Find the layer corresponding to the given configuration in the tree
     * of layers rooted at the given parent.
     */
    private Optional<ModuleLayer> findModuleLayer(ModuleLayer parent, Configuration cf) {
        return SharedSecrets.getJavaLangAccess().layers(parent)
                .filter(l -> l.configuration() == cf)
                .findAny();
    }


    /**
     * Returns the loader pool that this loader is in or {@code null} if this
     * loader is not in a loader pool.
     */
    public LoaderPool pool() {
        return pool;
    }


    // -- resources --

    /**
     * Returns a URL to a resource of the given name in a module defined to
     * this class loader.
     */
    @Override
    protected URL findResource(String mn, String name) throws IOException {
        ModuleReference mref = (mn != null) ? nameToModule.get(mn) : null;
        if (mref == null)
            return null;   // not defined to this class loader

        // locate resource
        URL url = null;
        Optional<URI> ouri = moduleReaderFor(mref).find(name);
        if (ouri.isPresent()) {
            try {
                url = ouri.get().toURL();
            } catch (MalformedURLException | IllegalArgumentException e) { }
        }
        return url;
    }

    @Override
    public URL findResource(String name) {
        String pn = Resources.toPackageName(name);
        LoadedModule module = localPackageToModule.get(pn);

        if (module != null) {
            try {
                URL url = findResource(module.name(), name);
                if (url != null
                    && (name.endsWith(".class")
                        || url.toString().endsWith("/")
                        || isOpen(module.mref(), pn))) {
                    return url;
                }
            } catch (IOException ioe) {
                // ignore
            }

        } else {
            for (ModuleReference mref : nameToModule.values()) {
                try {
                    URL url = findResource(mref.descriptor().name(), name);
                    if (url != null) return url;
                } catch (IOException ioe) {
                    // ignore
                }
            }
        }

        return null;
    }

    @Override
    public Enumeration<URL> findResources(String name) throws IOException {
        return Collections.enumeration(findResourcesAsList(name));
    }

    @Override
    public URL getResource(String name) {
        Objects.requireNonNull(name);

        // this loader
        URL url = findResource(name);
        if (url == null) {
            // parent loader
            if (parent != null) {
                url = parent.getResource(name);
            } else {
                url = BootLoader.findResource(name);
            }
        }
        return url;
    }

    @Override
    public Enumeration<URL> getResources(String name) throws IOException {
        Objects.requireNonNull(name);

        // this loader
        List<URL> urls = findResourcesAsList(name);

        // parent loader
        Enumeration<URL> e;
        if (parent != null) {
            e = parent.getResources(name);
        } else {
            e = BootLoader.findResources(name);
        }

        // concat the URLs with the URLs returned by the parent
        return new Enumeration<>() {
            final Iterator<URL> iterator = urls.iterator();
            @Override
            public boolean hasMoreElements() {
                return (iterator.hasNext() || e.hasMoreElements());
            }
            @Override
            public URL nextElement() {
                if (iterator.hasNext()) {
                    return iterator.next();
                } else {
                    return e.nextElement();
                }
            }
        };
    }

    /**
     * Finds the resources with the given name in this class loader.
     */
    private List<URL> findResourcesAsList(String name) throws IOException {
        String pn = Resources.toPackageName(name);
        LoadedModule module = localPackageToModule.get(pn);
        if (module != null) {
            URL url = findResource(module.name(), name);
            if (url != null
                    && (name.endsWith(".class")
                    || url.toString().endsWith("/")
                    || isOpen(module.mref(), pn))) {
                return List.of(url);
            } else {
                return Collections.emptyList();
            }
        } else {
            List<URL> urls = new ArrayList<>();
            for (ModuleReference mref : nameToModule.values()) {
                URL url = findResource(mref.descriptor().name(), name);
                if (url != null) {
                    urls.add(url);
                }
            }
            return urls;
        }
    }


    // -- finding/loading classes

    /**
     * Finds the class with the specified binary name.
     */
    @Override
    protected Class<?> findClass(String cn) throws ClassNotFoundException {
        Class<?> c = null;
        LoadedModule loadedModule = findLoadedModule(cn);
        if (loadedModule != null)
            c = findClassInModuleOrNull(loadedModule, cn);
        if (c == null)
            throw new ClassNotFoundException(cn);
        return c;
    }

    /**
     * Finds the class with the specified binary name in the given module.
     * This method returns {@code null} if the class cannot be found.
     */
    @Override
    protected Class<?> findClass(String mn, String cn) {
        Class<?> c = null;
        LoadedModule loadedModule = findLoadedModule(cn);
        if (loadedModule != null && loadedModule.name().equals(mn))
            c = findClassInModuleOrNull(loadedModule, cn);
        return c;
    }

    /**
     * Loads the class with the specified binary name.
     */
    @Override
    protected Class<?> loadClass(String cn, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(cn)) {
            // check if already loaded
            Class<?> c = findLoadedClass(cn);

            if (c == null) {

                LoadedModule loadedModule = findLoadedModule(cn);

                if (loadedModule != null) {

                    // class is in module defined to this class loader
                    c = findClassInModuleOrNull(loadedModule, cn);

                } else {

                    // type in another module or visible via the parent loader
                    String pn = packageName(cn);
                    ClassLoader loader = remotePackageToLoader.get(pn);
                    if (loader == null) {
                        // type not in a module read by any of the modules
                        // defined to this loader, so delegate to parent
                        // class loader
                        loader = parent;
                    }
                    if (loader == null) {
                        c = BootLoader.loadClassOrNull(cn);
                    } else {
                        c = loader.loadClass(cn);
                    }

                }
            }

            if (c == null)
                throw new ClassNotFoundException(cn);

            if (resolve)
                resolveClass(c);

            return c;
        }
    }


    /**
     * Finds the class with the specified binary name if in a module
     * defined to this ClassLoader.
     *
     * @return the resulting Class or {@code null} if not found
     */
    private Class<?> findClassInModuleOrNull(LoadedModule loadedModule, String cn) {
        ModuleReader reader = moduleReaderFor(loadedModule.mref());

        try {
            // read class file
            String rn = cn.replace('.', '/').concat(".class");
            ByteBuffer bb = reader.read(rn).orElse(null);
            if (bb == null) {
                // class not found
                return null;
            }

            try {
                return defineClass(cn, bb, loadedModule.codeSource());
            } finally {
                reader.release(bb);
            }

        } catch (IOException ioe) {
            // TBD on how I/O errors should be propagated
            return null;
        }
    }

    // -- miscellaneous supporting methods

    /**
     * Find the candidate module for the given class name.
     * Returns {@code null} if none of the modules defined to this
     * class loader contain the API package for the class.
     */
    private LoadedModule findLoadedModule(String cn) {
        String pn = packageName(cn);
        return pn.isEmpty() ? null : localPackageToModule.get(pn);
    }

    /**
     * Returns the package name for the given class name
     */
    private String packageName(String cn) {
        int pos = cn.lastIndexOf('.');
        return (pos < 0) ? "" : cn.substring(0, pos);
    }


    /**
     * Returns the ModuleReader for the given module.
     */
    private ModuleReader moduleReaderFor(ModuleReference mref) {
        return moduleToReader.computeIfAbsent(mref, m -> createModuleReader(mref));
    }

    /**
     * Creates a ModuleReader for the given module.
     */
    private ModuleReader createModuleReader(ModuleReference mref) {
        try {
            return mref.open();
        } catch (IOException e) {
            // Return a null module reader to avoid a future class load
            // attempting to open the module again.
            return new NullModuleReader();
        }
    }

    /**
     * A ModuleReader that doesn't read any resources.
     */
    private static class NullModuleReader implements ModuleReader {
        @Override
        public Optional<URI> find(String name) {
            return Optional.empty();
        }
        @Override
        public Stream<String> list() {
            return Stream.empty();
        }
        @Override
        public void close() {
            throw new InternalError("Should not get here");
        }
    }

    /**
     * Returns true if the given module opens the given package
     * unconditionally.
     *
     * @implNote This method currently iterates over each of the open
     * packages. This will be replaced once the ModuleDescriptor.Opens
     * API is updated.
     */
    private boolean isOpen(ModuleReference mref, String pn) {
        ModuleDescriptor descriptor = mref.descriptor();
        if (descriptor.isOpen() || descriptor.isAutomatic())
            return true;
        for (ModuleDescriptor.Opens opens : descriptor.opens()) {
            String source = opens.source();
            if (!opens.isQualified() && source.equals(pn)) {
                return true;
            }
        }
        return false;
    }
}
