/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package dalvik.system;

import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;

import android.annotation.SystemApi;

import sun.misc.CompoundEnumeration;

import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;

import libcore.util.NonNull;
import libcore.util.Nullable;

/**
 * A {@code ClassLoader} implementation that implements a <b>delegate last</b> lookup policy.
 * For every class or resource this loader is requested to load, the following lookup order
 * is employed:
 *
 * <ul>
 *     <li>The boot classpath is always searched first</li>
 *     <li>Then, the list of {@code dex} files associated with this classloaders's
 *     {@code dexPath} is searched.</li>
 *     <li>Finally, this classloader will delegate to the specified {@code parent}.</li>
 * </ul>
 */
public final class DelegateLastClassLoader extends PathClassLoader {

    /**
     * Whether resource loading delegates to the parent class loader. True by default.
     */
    private final boolean delegateResourceLoading;

    /**
     * Equivalent to calling {@link #DelegateLastClassLoader(String, String, ClassLoader, boolean)}
     * with {@code librarySearchPath = null, delegateResourceLoading = true}.
     */
    public DelegateLastClassLoader(String dexPath, ClassLoader parent) {
        this(dexPath, null, parent, true);
    }

    /**
     * Equivalent to calling {@link #DelegateLastClassLoader(String, String, ClassLoader, boolean)}
     * with {@code delegateResourceLoading = true}.
     */
    public DelegateLastClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        this(dexPath, librarySearchPath, parent, true);
    }

    /**
     * Creates a {@code DelegateLastClassLoader} that operates on a given {@code dexPath}
     * and a {@code librarySearchPath}.
     *
     * The {@code dexPath} should consist of one or more of the following, separated by
     * {@code File.pathSeparator}, which is {@code ":"} on Android.
     *
     * <ul>
     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as well as arbitrary
     * resources.
     * <li>Raw ".dex" files (not inside a zip file).
     * </ul>
     *
     * Unlike {@link PathClassLoader}, this classloader will attempt to locate classes
     * (or resources) using the following lookup order.
     * <ul>
     *     <li>The boot classpath is always searched first.</li>
     *     <li>Then, the list of {@code dex} files contained in {@code dexPath} is searched./li>
     *     <li>Lastly, this classloader will delegate to the specified {@code parent}.</li>
     * </ul>
     *
     * Note that this is in contrast to other standard classloaders that follow the delegation
     * model. In those classloaders, the {@code parent} is always searched first.
     *
     * {@code librarySearchPath} specifies one more directories containing native library files,
     * separated by {@code File.pathSeparator}.
     *
     * @param dexPath the list of jar/apk files containing classes and resources, delimited by
     *                {@code File.pathSeparator}, which defaults to {@code ":"} on Android.
     * @param librarySearchPath the list of directories containing native libraries, delimited
     *                          by {@code File.pathSeparator}; may be {@code null}.
     * @param parent the parent class loader. May be {@code null} for the boot classloader.
     * @param delegateResourceLoading whether to delegate resource loading to the parent if
     *                                the resource is not found. This does not affect class
     *                                loading delegation.
     */

    public DelegateLastClassLoader(@NonNull String dexPath, @Nullable String librarySearchPath,
            @Nullable ClassLoader parent, boolean delegateResourceLoading) {
        super(dexPath, librarySearchPath, parent);
        this.delegateResourceLoading = delegateResourceLoading;
    }

    /**
     * Creates a {@code DelegateLastClassLoader} that operates on a given {@code dexPath}
     * and a {@code librarySearchPath}.
     *
     * The {@code dexPath} should consist of one or more of the following, separated by
     * {@code File.pathSeparator}, which is {@code ":"} on Android.
     *
     * <ul>
     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as well as arbitrary
     * resources.
     * <li>Raw ".dex" files (not inside a zip file).
     * </ul>
     *
     * @param dexPath the list of jar/apk files containing classes and resources, delimited by
     *                {@code File.pathSeparator}, which defaults to {@code ":"} on Android.
     * @param librarySearchPath the list of directories containing native libraries, delimited
     *                          by {@code File.pathSeparator}; may be {@code null}.
     * @param parent the parent class loader. May be {@code null} for the boot classloader.
     * @param sharedLibraryLoaders class loaders of Java shared libraries
     *                             used by this new class loader. The shared library loaders are
     *                             always checked before the {@code dexPath} when looking
     *                             up classes and resources.
     *
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public DelegateLastClassLoader(
            String dexPath, String librarySearchPath, ClassLoader parent,
            ClassLoader[] sharedLibraryLoaders) {
        this(dexPath, librarySearchPath, parent, sharedLibraryLoaders, null);
    }

    /**
     * Creates a {@code DelegateLastClassLoader} that operates on a given {@code dexPath}
     * and a {@code librarySearchPath}.
     *
     * The {@code dexPath} should consist of one or more of the following, separated by
     * {@code File.pathSeparator}, which is {@code ":"} on Android.
     *
     * <ul>
     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as well as arbitrary
     * resources.
     * <li>Raw ".dex" files (not inside a zip file).
     * </ul>
     *
     * @param dexPath the list of jar/apk files containing classes and resources, delimited by
     *                {@code File.pathSeparator}, which defaults to {@code ":"} on Android.
     * @param librarySearchPath the list of directories containing native libraries, delimited
     *                          by {@code File.pathSeparator}; may be {@code null}.
     * @param parent the parent class loader. May be {@code null} for the boot classloader.
     * @param sharedLibraryLoaders class loaders of Java shared libraries
     *                             used by this new class loader. The shared library loaders are
     *                             always checked before the {@code dexPath} when looking
     *                             up classes and resources.
     * @param sharedLibraryLoadersAfter class loaders of Java shared libraries
     *                             used by this new class loader. These shared library loaders are
     *                             always checked <b>after</b> the {@code dexPath} when looking
     *                             up classes and resources.
     *
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public DelegateLastClassLoader(
            String dexPath, String librarySearchPath, ClassLoader parent,
            ClassLoader[] sharedLibraryLoaders, ClassLoader[] sharedLibraryLoadersAfter) {
        super(dexPath, librarySearchPath, parent, sharedLibraryLoaders, sharedLibraryLoadersAfter);
        // Delegating is the default behavior.
        this.delegateResourceLoading = true;
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // First, check whether the class has already been loaded. Return it if that's the
        // case.
        Class<?> cl = findLoadedClass(name);
        if (cl != null) {
            return cl;
        }

        // Next, check whether the class in question is present in the boot classpath.
        try {
            return Object.class.getClassLoader().loadClass(name);
        } catch (ClassNotFoundException ignored) {
        }

        // Next, check whether the class in question is present in the dexPath that this classloader
        // operates on, or its shared libraries.
        ClassNotFoundException fromSuper = null;
        try {
            return findClass(name);
        } catch (ClassNotFoundException ex) {
            fromSuper = ex;
        }

        // Finally, check whether the class in question is present in the parent classloader.
        try {
            return getParent().loadClass(name);
        } catch (ClassNotFoundException cnfe) {
            // The exception we're catching here is the CNFE thrown by the parent of this
            // classloader. However, we would like to throw a CNFE that provides details about
            // the class path / list of dex files associated with *this* classloader, so we choose
            // to throw the exception thrown from that lookup.
            throw fromSuper;
        }
    }

    @Override
    public URL getResource(String name) {
        // The lookup order we use here is the same as for classes.

        URL resource = Object.class.getClassLoader().getResource(name);
        if (resource != null) {
            return resource;
        }

        resource = findResource(name);
        if (resource != null) {
            return resource;
        }

        if (delegateResourceLoading) {
            final ClassLoader cl = getParent();
            return (cl == null) ? null : cl.getResource(name);
        }
        return null;
    }

    @Override
    public Enumeration<URL> getResources(String name) throws IOException {
        @SuppressWarnings("unchecked")
        final Enumeration<URL>[] resources = (Enumeration<URL>[]) new Enumeration<?>[] {
                Object.class.getClassLoader().getResources(name),
                findResources(name),
                (getParent() == null || !delegateResourceLoading)
                        ? null : getParent().getResources(name) };

        return new CompoundEnumeration<>(resources);
    }
}
