/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.openjpa.lib.util;

import java.io.IOException;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

/**
 * Class loader type that can be configured to delegate to multiple
 * internal class loaders.
 * The {@link #THREAD_LOADER} constant is a marker that will be replaced
 * with the context loader of the current thread.
 *
 * @author Abe White
 */
public class MultiClassLoader extends ClassLoader {

    /**
     * Marker that will be replaced with the context loader of the current
     * thread whenever it is discovered in the class loader list.
     */
    public static final ClassLoader THREAD_LOADER = null;

    /**
     * The standard system class loader.
     */
    public static final ClassLoader SYSTEM_LOADER =
        AccessController.doPrivileged(
            J2DoPrivHelper.getSystemClassLoaderAction());

    private List<ClassLoader> _loaders = new ArrayList<>(5);

    /**
     * Constructor; initializes the loader with an empty list of delegates.
     */
    public MultiClassLoader() {
        super(null);
    }

    /**
     * Construct with the class loaders of another multi loader.
     */
    public MultiClassLoader(MultiClassLoader other) {
        super(null);
        addClassLoaders(other);
    }

    public MultiClassLoader(ClassLoader... loaders) {
        for (ClassLoader loader : loaders) {
            addClassLoader(loader);
        }
    }

    /**
     * Returns true if the list contains the given class loader or marker.
     */
    public boolean containsClassLoader(ClassLoader loader) {
        return _loaders.contains(loader);
    }

    /**
     * Return an array of all contained class loaders.
     */
    public ClassLoader[] getClassLoaders() {
        ClassLoader[] loaders = new ClassLoader[size()];
        ClassLoader loader;
        Iterator<ClassLoader> itr = _loaders.iterator();
        for (int i = 0; i < loaders.length; i++) {
            loader = itr.next();
            if (loader == THREAD_LOADER)
                loader = AccessController.doPrivileged(
                    J2DoPrivHelper.getContextClassLoaderAction());
            loaders[i] = loader;
        }
        return loaders;
    }

    /**
     * Return the class loader at the given index.
     */
    public ClassLoader getClassLoader(int index) {
        ClassLoader loader = (ClassLoader) _loaders.get(index);
        if (loader == THREAD_LOADER)
            loader = AccessController.doPrivileged(
                J2DoPrivHelper.getContextClassLoaderAction());
        return loader;
    }

    /**
     * Add the given class loader to the set of loaders that will be tried.
     *
     * @return true if the loader was added, false if already in the list
     */
    public boolean addClassLoader(ClassLoader loader) {
        if (_loaders.contains(loader))
            return false;
        return _loaders.add(loader);
    }

    /**
     * Add the given class loader at the specified index.
     *
     * @return true if the loader was added, false if already in the list
     */
    public boolean addClassLoader(int index, ClassLoader loader) {
        if (_loaders.contains(loader))
            return false;
        _loaders.add(index, loader);
        return true;
    }

    /**
     * Set the class loaders of this loader to those of the given loader.
     */
    public void setClassLoaders(MultiClassLoader multi) {
        clear();
        addClassLoaders(multi);
    }

    /**
     * Adds all class loaders from the given multi loader starting at the
     * given index.
     *
     * @return true if any loaders were added, false if all already in list
     */
    public boolean addClassLoaders(int index, MultiClassLoader multi) {
        if (multi == null)
            return false;

        // use iterator so that the thread loader is not resolved
        boolean added = false;
        for (ClassLoader loader : multi._loaders) {
            if (addClassLoader(index, loader)) {
                index++;
                added = true;
            }
        }
        return added;
    }

    /**
     * Adds all the class loaders from the given multi loader.
     *
     * @return true if any loaders were added, false if all already in list
     */
    public boolean addClassLoaders(MultiClassLoader multi) {
        if (multi == null)
            return false;

        // use iterator so that the thread loader is not resolved
        boolean added = false;
        for (ClassLoader loader : multi._loaders) {
            added = addClassLoader(loader) || added;
        }
        return added;
    }

    /**
     * Remove the given loader from the list.
     *
     * @return true if removed, false if not in list
     */
    public boolean removeClassLoader(ClassLoader loader) {
        return _loaders.remove(loader);
    }

    /**
     * Clear the list of class loaders.
     */
    public void clear() {
        _loaders.clear();
    }

    /**
     * Return the number of internal class loaders.
     */
    public int size() {
        return _loaders.size();
    }

    /**
     * Return true if there are no internal class laoders.
     */
    public boolean isEmpty() {
        return _loaders.isEmpty();
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        ClassLoader loader;
        for (ClassLoader classLoader : _loaders) {
            loader = classLoader;
            if (loader == THREAD_LOADER)
                loader = AccessController.doPrivileged(
                        J2DoPrivHelper.getContextClassLoaderAction());
            try {
                return Class.forName(name, false, loader);
            }
            catch (Throwable t) {
            }
        }
        throw new ClassNotFoundException(name);
    }

    @Override
    protected URL findResource(String name) {
        ClassLoader loader;
        URL rsrc;
        for (ClassLoader classLoader : _loaders) {
            loader = classLoader;
            if (loader == THREAD_LOADER)
                loader = AccessController.doPrivileged(
                        J2DoPrivHelper.getContextClassLoaderAction());

            if (loader == null) // skip
                continue;

            rsrc = AccessController.doPrivileged(
                    J2DoPrivHelper.getResourceAction(loader, name));
            if (rsrc != null)
                return rsrc;
        }
        return null;
    }

    @Override
    protected Enumeration<URL> findResources(String name) throws IOException {
        ClassLoader loader;
        Enumeration<URL> rsrcs;
        URL rsrc;
        Vector<URL> all = new Vector<>();
        for (ClassLoader classLoader : _loaders) {
            loader = classLoader;
            if (loader == THREAD_LOADER)
                loader = AccessController.doPrivileged(
                        J2DoPrivHelper.getContextClassLoaderAction());

            if (loader == null) // skip
                continue;

            try {
                rsrcs = AccessController.doPrivileged(
                        J2DoPrivHelper.getResourcesAction(loader, name));
                while (rsrcs.hasMoreElements()) {
                    rsrc = rsrcs.nextElement();
                    if (!all.contains(rsrc))
                        all.addElement(rsrc);
                }
            }
            catch (PrivilegedActionException pae) {
                throw (IOException) pae.getException();
            }
        }
        return all.elements();
    }

    @Override
    public boolean equals(Object other) {
        if (other == this)
            return true;
        if (!(other instanceof MultiClassLoader))
            return false;
        return ((MultiClassLoader) other)._loaders.equals(_loaders);
    }

    @Override
    public int hashCode() {
        return _loaders.hashCode();
    }
}
