/*
 * 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.ignite.osgi.classloaders;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.ignite.internal.util.GridConcurrentHashSet;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;

/**
 * A {@link ClassLoader} implementation that first attempts to load the class from the associated {@link Bundle}. As
 * a fallback, it sweeps the entire OSGi container to find the requested class, returning the first hit.
 * <p>
 * It keeps a cache of resolved classes and unresolvable classes, in order to optimize subsequent lookups.
 */
public class ContainerSweepClassLoader extends BundleDelegatingClassLoader {
    /** Classes resolved previously. */
    private final ConcurrentMap<String, Bundle> resolved = new ConcurrentHashMap<>();

    /** Unresolvable classes. */
    private final Set<String> nonResolvable = new GridConcurrentHashSet<>();

    /**
     * Constructor with a {@link Bundle} only.
     *
     * @param bundle The bundle.
     */
    public ContainerSweepClassLoader(Bundle bundle) {
        super(bundle);
    }

    /**
     * Constructor with a {@link Bundle} and another {@link ClassLoader} to check.
     *
     * @param bundle The bundle.
     * @param classLoader The other classloader to check.
     */
    public ContainerSweepClassLoader(Bundle bundle, ClassLoader classLoader) {
        super(bundle, classLoader);
    }

    /**
     * Runs the same logic to find the class as {@link BundleDelegatingClassLoader}, but if not found, sweeps the
     * OSGi container to locate the first {@link Bundle} that can load the class, and uses it to do so.
     *
     * @param name The classname.
     * @param resolve Whether to resolve the class or not.
     * @return The loaded class.
     * @throws ClassNotFoundException
     */
    @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // If we know it's not resolvable, throw the exception immediately.
        if (nonResolvable.contains(name))
            throw classNotFoundException(name);

        Class<?> cls;

        // First, delegate to super, and return the class if found.
        try {
            cls = super.loadClass(name, resolve);
            return cls;
        }
        catch (ClassNotFoundException ignored) {
            // Continue.
        }

        // Else, check the cache.
        if (resolved.containsKey(name))
            return resolved.get(name).loadClass(name);

        // If still unresolved, sweep the container.
        cls = sweepContainer(name);

        // If still unresolved, throw the exception.
        if (cls == null)
            throw classNotFoundException(name);

        return cls;
    }

    /**
     * Sweeps the OSGi container to find the first {@link Bundle} that can load the class.
     *
     * @param name The classname.
     * @return The loaded class.
     */
    protected Class<?> sweepContainer(String name) {
        Class<?> cls = null;

        Bundle[] bundles = bundle.getBundleContext().getBundles();

        int bundleIdx = 0;

        for (; bundleIdx < bundles.length; bundleIdx++) {
            Bundle b = bundles[bundleIdx];

            // Skip bundles that haven't reached RESOLVED state; skip fragments.
            if (b.getState() <= Bundle.RESOLVED || b.getHeaders().get(Constants.FRAGMENT_HOST) != null)
                continue;

            try {
                cls = b.loadClass(name);
                break;
            }
            catch (ClassNotFoundException ignored) {
                // No-op.
            }
        }

        if (cls == null)
            nonResolvable.add(name);
        else
            resolved.put(name, bundles[bundleIdx]);

        return cls;
    }
}
