/*
 * 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.felix.framework;

import org.apache.felix.framework.BundleWiringImpl.BundleClassLoader;
import org.apache.felix.framework.cache.Content;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.hooks.weaving.WeavingException;
import org.osgi.framework.hooks.weaving.WeavingHook;
import org.osgi.framework.hooks.weaving.WovenClass;
import org.osgi.framework.hooks.weaving.WovenClassListener;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.framework.wiring.BundleWire;
import org.osgi.framework.wiring.BundleWiring;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class BundleWiringImplTest
{

    private BundleWiringImpl bundleWiring;

    private StatefulResolver mockResolver;

    private BundleRevisionImpl mockRevisionImpl;

    private BundleImpl mockBundle;

    @SuppressWarnings("rawtypes")
    public void initializeSimpleBundleWiring() throws Exception
    {

        mockResolver = mock(StatefulResolver.class);
        mockRevisionImpl = mock(BundleRevisionImpl.class);
        mockBundle = mock(BundleImpl.class);

        Logger logger = new Logger();
        Map configMap = new HashMap();
        List<BundleRevision> fragments = new ArrayList<BundleRevision>();
        List<BundleWire> wires = new ArrayList<BundleWire>();
        Map<String, BundleRevision> importedPkgs = new HashMap<String, BundleRevision>();
        Map<String, List<BundleRevision>> requiredPkgs = new HashMap<String, List<BundleRevision>>();

        when(mockRevisionImpl.getBundle()).thenReturn(mockBundle);
        when(mockBundle.getBundleId()).thenReturn(Long.valueOf(1));

        bundleWiring = new BundleWiringImpl(logger, configMap, mockResolver,
                mockRevisionImpl, fragments, wires, importedPkgs, requiredPkgs);
    }

    @Test
    public void testBundleClassLoader() throws Exception
    {
        bundleWiring = mock(BundleWiringImpl.class);
        BundleClassLoader bundleClassLoader = createBundleClassLoader(
                BundleClassLoader.class, bundleWiring);
        assertNotNull(bundleClassLoader);
    }

    @SuppressWarnings("rawtypes")
    @Test
    public void testFindClassNonExistant() throws Exception
    {
        initializeSimpleBundleWiring();

        BundleClassLoader bundleClassLoader = createBundleClassLoader(
                BundleClassLoader.class, bundleWiring);
        assertNotNull(bundleClassLoader);
        Class foundClass = null;
        try
        {
            foundClass = bundleClassLoader
                    .findClass("org.apache.felix.test.NonExistant");
        } catch (ClassNotFoundException e)
        {
            fail("Class should not throw exception");
        }
        assertNull("Nonexistant Class Should be null", foundClass);
    }

    @SuppressWarnings("rawtypes")
    @Test
    public void testFindClassExistant() throws Exception
    {
        Felix mockFramework = mock(Felix.class);
        HookRegistry hReg = mock(HookRegistry.class);
        Mockito.when(mockFramework.getHookRegistry()).thenReturn(hReg);
        Content mockContent = mock(Content.class);
        Class testClass = TestClass.class;
        String testClassName = testClass.getName();
        String testClassAsPath = testClassName.replace('.', '/') + ".class";
        byte[] testClassBytes = createTestClassBytes(testClass, testClassAsPath);

        List<Content> contentPath = new ArrayList<Content>();
        contentPath.add(mockContent);
        initializeSimpleBundleWiring();

        when(mockBundle.getFramework()).thenReturn(mockFramework);
        when(mockFramework.getBootPackages()).thenReturn(new String[0]);

        when(mockRevisionImpl.getContentPath()).thenReturn(contentPath);
        when(mockContent.getEntryAsBytes(testClassAsPath)).thenReturn(
                testClassBytes);

        BundleClassLoader bundleClassLoader = createBundleClassLoader(
                BundleClassLoader.class, bundleWiring);
        assertNotNull(bundleClassLoader);
        Class foundClass = null;
        try
        {

            foundClass = bundleClassLoader.findClass(TestClass.class.getName());
        } catch (ClassNotFoundException e)
        {
            fail("Class should not throw exception");
        }
        assertNotNull("Class Should be found in this classloader", foundClass);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    @Test
    public void testFindClassWeave() throws Exception
    {
        Felix mockFramework = mock(Felix.class);
        Content mockContent = mock(Content.class);
        ServiceReference<WeavingHook> mockServiceReferenceWeavingHook = mock(ServiceReference.class);
        ServiceReference<WovenClassListener> mockServiceReferenceWovenClassListener = mock(ServiceReference.class);

        Set<ServiceReference<WeavingHook>> hooks = new HashSet<ServiceReference<WeavingHook>>();
        hooks.add(mockServiceReferenceWeavingHook);

        DummyWovenClassListener dummyWovenClassListener = new DummyWovenClassListener();

        Set<ServiceReference<WovenClassListener>> listeners = new HashSet<ServiceReference<WovenClassListener>>();
        listeners.add(mockServiceReferenceWovenClassListener);

        Class testClass = TestClass.class;
        String testClassName = testClass.getName();
        String testClassAsPath = testClassName.replace('.', '/') + ".class";
        byte[] testClassBytes = createTestClassBytes(testClass, testClassAsPath);

        List<Content> contentPath = new ArrayList<Content>();
        contentPath.add(mockContent);
        initializeSimpleBundleWiring();

        when(mockBundle.getFramework()).thenReturn(mockFramework);
        when(mockFramework.getBootPackages()).thenReturn(new String[0]);

        when(mockRevisionImpl.getContentPath()).thenReturn(contentPath);
        when(mockContent.getEntryAsBytes(testClassAsPath)).thenReturn(
                testClassBytes);

        HookRegistry hReg = mock(HookRegistry.class);
        when(hReg.getHooks(WeavingHook.class)).thenReturn(hooks);
        when(mockFramework.getHookRegistry()).thenReturn(hReg);
        when(
                mockFramework.getService(mockFramework,
                        mockServiceReferenceWeavingHook, false)).thenReturn(
                                new GoodDummyWovenHook());

        when(hReg.getHooks(WovenClassListener.class)).thenReturn(
                listeners);
        when(
                mockFramework.getService(mockFramework,
                        mockServiceReferenceWovenClassListener, false))
        .thenReturn(dummyWovenClassListener);

        BundleClassLoader bundleClassLoader = createBundleClassLoader(
                BundleClassLoader.class, bundleWiring);
        assertNotNull(bundleClassLoader);
        Class foundClass = null;
        try
        {

            foundClass = bundleClassLoader.findClass(TestClass.class.getName());
        } catch (ClassNotFoundException e)
        {
            fail("Class should not throw exception");
        }
        assertNotNull("Class Should be found in this classloader", foundClass);
        assertEquals("Weaving should have added a field", 1,
                foundClass.getFields().length);
        assertEquals("There should be 2 state changes fired by the weaving", 2,
                dummyWovenClassListener.stateList.size());
        assertEquals("The first state change should transform the class",
                (Object)WovenClass.TRANSFORMED,
                dummyWovenClassListener.stateList.get(0));
        assertEquals("The second state change should define the class",
                (Object)WovenClass.DEFINED, dummyWovenClassListener.stateList.get(1));
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    @Test
    public void testFindClassBadWeave() throws Exception
    {
        Felix mockFramework = mock(Felix.class);
        Content mockContent = mock(Content.class);
        ServiceReference<WeavingHook> mockServiceReferenceWeavingHook = mock(ServiceReference.class);
        ServiceReference<WovenClassListener> mockServiceReferenceWovenClassListener = mock(ServiceReference.class);

        Set<ServiceReference<WeavingHook>> hooks = new HashSet<ServiceReference<WeavingHook>>();
        hooks.add(mockServiceReferenceWeavingHook);

        DummyWovenClassListener dummyWovenClassListener = new DummyWovenClassListener();

        Set<ServiceReference<WovenClassListener>> listeners = new HashSet<ServiceReference<WovenClassListener>>();
        listeners.add(mockServiceReferenceWovenClassListener);

        Class testClass = TestClass.class;
        String testClassName = testClass.getName();
        String testClassAsPath = testClassName.replace('.', '/') + ".class";
        byte[] testClassBytes = createTestClassBytes(testClass, testClassAsPath);

        List<Content> contentPath = new ArrayList<Content>();
        contentPath.add(mockContent);
        initializeSimpleBundleWiring();

        when(mockBundle.getFramework()).thenReturn(mockFramework);
        when(mockFramework.getBootPackages()).thenReturn(new String[0]);

        when(mockRevisionImpl.getContentPath()).thenReturn(contentPath);
        when(mockContent.getEntryAsBytes(testClassAsPath)).thenReturn(
                testClassBytes);

        HookRegistry hReg = mock(HookRegistry.class);
        when(hReg.getHooks(WeavingHook.class)).thenReturn(hooks);
        when(mockFramework.getHookRegistry()).thenReturn(hReg);
        when(
                mockFramework.getService(mockFramework,
                        mockServiceReferenceWeavingHook, false)).thenReturn(
                                new BadDummyWovenHook());

        when(hReg.getHooks(WovenClassListener.class)).thenReturn(
                listeners);
        when(
                mockFramework.getService(mockFramework,
                        mockServiceReferenceWovenClassListener, false))
        .thenReturn(dummyWovenClassListener);

        BundleClassLoader bundleClassLoader = createBundleClassLoader(
                BundleClassLoader.class, bundleWiring);
        assertNotNull(bundleClassLoader);

        try
        {

            bundleClassLoader.findClass(TestClass.class.getName());
            fail("Class should throw exception");
        } catch (Error e)
        {
            // This is expected
        }

        assertEquals("There should be 1 state changes fired by the weaving", 1,
                dummyWovenClassListener.stateList.size());
        assertEquals(
                "The only state change should be a failed transform on the class",
                (Object)WovenClass.TRANSFORMING_FAILED,
                dummyWovenClassListener.stateList.get(0));

    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    @Test
    public void testFindClassWeaveDefineError() throws Exception
    {
        Felix mockFramework = mock(Felix.class);
        Content mockContent = mock(Content.class);
        ServiceReference<WeavingHook> mockServiceReferenceWeavingHook = mock(ServiceReference.class);
        ServiceReference<WovenClassListener> mockServiceReferenceWovenClassListener = mock(ServiceReference.class);

        Set<ServiceReference<WeavingHook>> hooks = new HashSet<ServiceReference<WeavingHook>>();
        hooks.add(mockServiceReferenceWeavingHook);

        DummyWovenClassListener dummyWovenClassListener = new DummyWovenClassListener();

        Set<ServiceReference<WovenClassListener>> listeners = new HashSet<ServiceReference<WovenClassListener>>();
        listeners.add(mockServiceReferenceWovenClassListener);

        Class testClass = TestClass.class;
        String testClassName = testClass.getName();
        String testClassAsPath = testClassName.replace('.', '/') + ".class";
        byte[] testClassBytes = createTestClassBytes(testClass, testClassAsPath);

        List<Content> contentPath = new ArrayList<Content>();
        contentPath.add(mockContent);
        initializeSimpleBundleWiring();

        when(mockBundle.getFramework()).thenReturn(mockFramework);
        when(mockFramework.getBootPackages()).thenReturn(new String[0]);

        when(mockRevisionImpl.getContentPath()).thenReturn(contentPath);
        when(mockContent.getEntryAsBytes(testClassAsPath)).thenReturn(
                testClassBytes);

        HookRegistry hReg = mock(HookRegistry.class);
        when(hReg.getHooks(WeavingHook.class)).thenReturn(hooks);
        when(mockFramework.getHookRegistry()).thenReturn(hReg);
        when(
                mockFramework.getService(mockFramework,
                        mockServiceReferenceWeavingHook, false)).thenReturn(
                                new BadDefineWovenHook());

        when(hReg.getHooks(WovenClassListener.class)).thenReturn(
                listeners);
        when(
                mockFramework.getService(mockFramework,
                        mockServiceReferenceWovenClassListener, false))
        .thenReturn(dummyWovenClassListener);

        BundleClassLoader bundleClassLoader = createBundleClassLoader(
                BundleClassLoader.class, bundleWiring);
        assertNotNull(bundleClassLoader);
        try
        {

            bundleClassLoader.findClass(TestClass.class.getName());
            fail("Class should throw exception");
        } catch (Throwable e)
        {

        }
        assertEquals("There should be 2 state changes fired by the weaving", 2,
                dummyWovenClassListener.stateList.size());
        assertEquals("The first state change should transform the class",
                (Object)WovenClass.TRANSFORMED,
                dummyWovenClassListener.stateList.get(0));
        assertEquals("The second state change failed the define on the class",
                (Object)WovenClass.DEFINE_FAILED,
                dummyWovenClassListener.stateList.get(1));
    }

    private ConcurrentHashMap<String, ClassLoader> getAccessorCache(BundleWiringImpl wiring) throws NoSuchFieldException, IllegalAccessException {
        Field m_accessorLookupCache = BundleWiringImpl.class.getDeclaredField("m_accessorLookupCache");
        m_accessorLookupCache.setAccessible(true);
        return (ConcurrentHashMap<String, ClassLoader>) m_accessorLookupCache.get(wiring);
    }

    @Test
    public void testFirstGeneratedAccessorSkipClassloading() throws Exception
    {

        String classToBeLoaded = "sun.reflect.GeneratedMethodAccessor21";

        Felix mockFramework = mock(Felix.class);
        when(mockFramework.getBootPackages()).thenReturn(new String[0]);

        initializeSimpleBundleWiring();

        when(bundleWiring.getBundle().getFramework()).thenReturn(mockFramework);

        BundleClassLoader bundleClassLoader = createBundleClassLoader(
                BundleClassLoader.class, bundleWiring);
        assertNotNull(bundleClassLoader);

        try {
            bundleClassLoader.loadClass(classToBeLoaded, true);
            fail();
        } catch (ClassNotFoundException cnf) {
            //this is expected

            //make sure boot delegation was done before CNF was thrown
            verify(mockFramework).getBootPackages();

            //make sure the class is added to the skip class cache
            assertEquals(getAccessorCache(bundleWiring).get(classToBeLoaded), BundleWiringImpl.CNFE_CLASS_LOADER);
        } catch (Exception e) {
            e.printStackTrace();
            fail();
        }
    }

    @SuppressWarnings("rawtypes")
    public void initializeBundleWiringWithImportsAndRequired(Map<String, BundleRevision> importedPkgs, Map<String, List<BundleRevision>> requiredPkgs) throws Exception
    {

        mockResolver = mock(StatefulResolver.class);
        mockRevisionImpl = mock(BundleRevisionImpl.class);
        mockBundle = mock(BundleImpl.class);

        Logger logger = new Logger();
        Map configMap = new HashMap();
        List<BundleRevision> fragments = new ArrayList<BundleRevision>();
        List<BundleWire> wires = new ArrayList<BundleWire>();

        when(mockRevisionImpl.getBundle()).thenReturn(mockBundle);
        when(mockBundle.getBundleId()).thenReturn(Long.valueOf(1));

        bundleWiring = new BundleWiringImpl(logger, configMap, mockResolver,
                mockRevisionImpl, fragments, wires, importedPkgs, requiredPkgs);
    }

    @Test
    public void testAccessorFirstLoadFailed() throws Exception
    {

        String classToBeLoaded = "sun.reflect.GeneratedMethodAccessor21";

        Felix mockFramework = mock(Felix.class);
        when(mockFramework.getBootPackages()).thenReturn(new String[0]);

        Map<String, BundleRevision> importedPkgs = mock(Map.class);
        Map<String, List<BundleRevision>> requiredPkgs = mock(Map.class);

        initializeBundleWiringWithImportsAndRequired(importedPkgs, requiredPkgs);

        when(bundleWiring.getBundle().getFramework()).thenReturn(mockFramework);

        BundleClassLoader bundleClassLoader = createBundleClassLoader(
                BundleClassLoader.class, bundleWiring);
        assertNotNull(bundleClassLoader);

        try {
            bundleClassLoader.loadClass(classToBeLoaded, true);
            fail();
        } catch (ClassNotFoundException cnf) {
            //this is expected

            //make sure boot delegation was done before CNF was thrown
            verify(mockFramework).getBootPackages();

            //make sure imported and required pkgs are searched
            verify(importedPkgs).values();
            verify(requiredPkgs).values();

            //make sure the class is added to the skip class cache
            assertEquals(getAccessorCache(bundleWiring).get(classToBeLoaded), BundleWiringImpl.CNFE_CLASS_LOADER);
        } catch (Exception e) {
            e.printStackTrace();
            fail();
        }
    }

    @Test
    public void testAccessorSubsequentLoadFailed() throws Exception
    {

        String classToBeLoaded = "sun.reflect.GeneratedMethodAccessor21";

        Felix mockFramework = mock(Felix.class);
        when(mockFramework.getBootPackages()).thenReturn(new String[0]);

        Map<String, BundleRevision> importedPkgs = mock(Map.class);
        Map<String, List<BundleRevision>> requiredPkgs = mock(Map.class);

        initializeBundleWiringWithImportsAndRequired(importedPkgs, requiredPkgs);

        when(bundleWiring.getBundle().getFramework()).thenReturn(mockFramework);

        BundleClassLoader bundleClassLoader = createBundleClassLoader(
                BundleClassLoader.class, bundleWiring);
        assertNotNull(bundleClassLoader);

        //first attempt to populate the cache
        try {
            bundleClassLoader.loadClass(classToBeLoaded, true);
            fail();
        } catch (ClassNotFoundException cnf) {
            //this is expected
        }

        //now test that the subsequent class load throws CNF with out boot delegation and import/required packages
        try {

            importedPkgs = mock(Map.class);
            requiredPkgs = mock(Map.class);
            initializeBundleWiringWithImportsAndRequired(importedPkgs, requiredPkgs);
            mockFramework = mock(Felix.class);
            when(mockFramework.getBootPackages()).thenReturn(new String[0]);
            when(bundleWiring.getBundle().getFramework()).thenReturn(mockFramework);
            bundleClassLoader.loadClass(classToBeLoaded, true);
            fail();
        } catch (ClassNotFoundException cnf) {
            //this is expected

            //make sure boot delegation was not used
            verify(mockFramework, never()).getBootPackages();

            //make sure boot import and required packages were not searched
            verify(importedPkgs, never()).values();
            verify(requiredPkgs, never()).values();

        } catch (Exception e) {
            e.printStackTrace();
            fail();
        }
    }

    private BundleRevision getBundleRevision(String classToBeLoaded, BundleClassLoader pkgBundleClassLoader, Object value) throws ClassNotFoundException {
        BundleRevision bundleRevision = mock(BundleRevision.class);
        BundleWiring pkgBundleWiring = mock(BundleWiring.class);
        when(pkgBundleClassLoader.findLoadedClassInternal(classToBeLoaded)).thenAnswer(createAnswer(value));
        when(pkgBundleClassLoader.loadClass(classToBeLoaded)).thenAnswer(createAnswer(value));

        when(pkgBundleWiring.getClassLoader()).thenReturn(pkgBundleClassLoader);
        when(bundleRevision.getWiring()).thenReturn(pkgBundleWiring);
        return bundleRevision;
    }

    @Test
    public void testAccessorLoadImportPackage() throws Exception
    {

        String classToBeLoaded = "sun.reflect.GeneratedMethodAccessor21";

        Felix mockFramework = mock(Felix.class);
        when(mockFramework.getBootPackages()).thenReturn(new String[0]);

        Map<String, BundleRevision> importedPkgs = mock(Map.class);
        BundleClassLoader foundClassLoader = mock(BundleClassLoader.class);
        BundleClassLoader notFoundClassLoader = mock(BundleClassLoader.class);
        BundleRevision bundleRevision1 = getBundleRevision(classToBeLoaded, foundClassLoader, String.class);
        BundleRevision bundleRevision2 = getBundleRevision(classToBeLoaded, notFoundClassLoader, null);
        Map<String, BundleRevision> importedPkgsActual = new LinkedHashMap<String, BundleRevision>();
        importedPkgsActual.put("sun.reflect1", bundleRevision1);
        importedPkgsActual.put("sun.reflect2", bundleRevision2);
        when(importedPkgs.values()).thenReturn(importedPkgsActual.values());
        Map<String, List<BundleRevision>> requiredPkgs = mock(Map.class);

        initializeBundleWiringWithImportsAndRequired(importedPkgs, requiredPkgs);

        when(bundleWiring.getBundle().getFramework()).thenReturn(mockFramework);

        BundleClassLoader bundleClassLoader = createBundleClassLoader(
                BundleClassLoader.class, bundleWiring);
        assertNotNull(bundleClassLoader);

        //call class load to populate the cache
        try {
            Object result = bundleClassLoader.loadClass(classToBeLoaded, true);
            assertNotNull(result);
            assertTrue(getAccessorCache(bundleWiring).containsKey(classToBeLoaded));
            assertEquals(getAccessorCache(bundleWiring).get(classToBeLoaded), foundClassLoader);
            verify(foundClassLoader, times(1)).findLoadedClassInternal(classToBeLoaded);
            verify(notFoundClassLoader, never()).findLoadedClassInternal(classToBeLoaded);
        } catch (Exception e) {
            fail();
        }

        //now make sure subsequent class load happens from cached revision
        Object result = bundleClassLoader.loadClass(classToBeLoaded, true);
        assertNotNull(result);
        //makes sure the look up cache is accessed and the class is loaded from cached revision
        verify(foundClassLoader, times(1)).findLoadedClassInternal(classToBeLoaded);
        verify(foundClassLoader, times(1)).loadClass(classToBeLoaded);
        verify(notFoundClassLoader, never()).findLoadedClassInternal(classToBeLoaded);
    }

    private static <T> Answer<T> createAnswer(final T value) {
        Answer<T> dummy = new Answer<T>() {
            @Override
            public T answer(InvocationOnMock invocation) throws Throwable {
                return value;
            }
        };
        return dummy;
    }

    @Test
    public void testAccessorBootDelegate() throws Exception
    {

        String classToBeLoaded = "sun.reflect.GeneratedMethodAccessor21";

        Felix mockFramework = mock(Felix.class);
        when(mockFramework.getBootPackages()).thenReturn(new String[0]);

        Map<String, BundleRevision> importedPkgs = mock(Map.class);
        BundleRevision bundleRevision1 = mock(BundleRevision.class);
        Map<String, BundleRevision> importedPkgsActual = new HashMap<String, BundleRevision>();
        importedPkgsActual.put("sun.reflect1", bundleRevision1);
        when(importedPkgs.values()).thenReturn(importedPkgsActual.values());
        Map<String, List<BundleRevision>> requiredPkgs = mock(Map.class);

        ClassLoader bootDelegateClassLoader = mock(ClassLoader.class);

        when(bootDelegateClassLoader.loadClass(classToBeLoaded)).thenAnswer(createAnswer(String.class));

        initializeBundleWiringWithImportsAndRequired(importedPkgs, requiredPkgs);

        when(bundleWiring.getBundle().getFramework()).thenReturn(mockFramework);

        Field field = bundleWiring.getClass().getDeclaredField("m_bootClassLoader");
        field.setAccessible(true);
        field.set(bundleWiring, bootDelegateClassLoader);

        BundleClassLoader bundleClassLoader = createBundleClassLoader(
                BundleClassLoader.class, bundleWiring);
        assertNotNull(bundleClassLoader);

        try {
            Object result = bundleClassLoader.loadClass(classToBeLoaded, true);
            assertNotNull(result);
            verify(importedPkgs, never()).values();
            verify(requiredPkgs, never()).values();
            assertTrue(getAccessorCache(bundleWiring).containsKey(classToBeLoaded));
            assertTrue(getAccessorCache(bundleWiring).get(classToBeLoaded) == bootDelegateClassLoader);
        } catch (Exception e) {
            fail();
        }

        //now make sure subsequent class loading happens from boot delegation
        Object result = bundleClassLoader.loadClass(classToBeLoaded, true);
        assertNotNull(result);
        //makes sure the look up cache is accessed and the class is loaded via boot delegation
        verify(importedPkgs, never()).values();
        verify(requiredPkgs, never()).values();
    }

    @Test
    public void testParallelClassload() throws Exception
    {


        Felix mockFramework = mock(Felix.class);
        HookRegistry hReg = mock(HookRegistry.class);
        Mockito.when(mockFramework.getHookRegistry()).thenReturn(hReg);
        Content mockContent = mock(Content.class);
        final Class testClass = TestClassSuper.class;
        final String testClassName = testClass.getName();
        final String testClassAsPath = testClassName.replace('.', '/') + ".class";
        byte[] testClassBytes = createTestClassBytes(testClass, testClassAsPath);

        final Class testClass2 = TestClassChild.class;
        final String testClassName2 = testClass2.getName();
        final String testClassAsPath2 = testClassName2.replace('.', '/') + ".class";
        byte[] testClassBytes2 = createTestClassBytes(testClass2, testClassAsPath2);

        final Class testClass3 = TestClass.class;
        final String testClassName3 = testClass3.getName();
        final String testClassAsPath3 = testClassName3.replace('.', '/') + ".class";
        byte[] testClassBytes3 = createTestClassBytes(testClass3, testClassAsPath3);

        List<Content> contentPath = new ArrayList<Content>();
        contentPath.add(mockContent);
        BundleWiringImpl bundleWiring;

        StatefulResolver mockResolver;

        BundleRevisionImpl mockRevisionImpl;

        BundleImpl mockBundle;

        mockResolver = mock(StatefulResolver.class);
        mockRevisionImpl = mock(BundleRevisionImpl.class);
        mockBundle = mock(BundleImpl.class);

        Logger logger = new Logger();
        Map configMap = new HashMap();
        List<BundleRevision> fragments = new ArrayList<BundleRevision>();
        List<BundleWire> wires = new ArrayList<BundleWire>();
        Map<String, BundleRevision> importedPkgs = new HashMap<String, BundleRevision>();
        Map<String, List<BundleRevision>> requiredPkgs = new HashMap<String, List<BundleRevision>>();

        when(mockRevisionImpl.getBundle()).thenReturn(mockBundle);
        when(mockBundle.getBundleId()).thenReturn(Long.valueOf(1));

        bundleWiring = new BundleWiringImpl(logger, configMap, mockResolver,
            mockRevisionImpl, fragments, wires, importedPkgs, requiredPkgs);

        when(mockBundle.getFramework()).thenReturn(mockFramework);
        when(mockFramework.getBootPackages()).thenReturn(new String[0]);

        when(mockRevisionImpl.getContentPath()).thenReturn(contentPath);
        when(mockContent.getEntryAsBytes(testClassAsPath)).thenReturn(
            testClassBytes);
        when(mockContent.getEntryAsBytes(testClassAsPath2)).thenReturn(
            testClassBytes2);
        when(mockContent.getEntryAsBytes(testClassAsPath3)).thenReturn(
            testClassBytes3);


        final TestBundleClassLoader bundleClassLoader = createBundleClassLoader(
            TestBundleClassLoader.class, bundleWiring);
        assertNotNull(bundleClassLoader);

        Field m_classLoader = bundleWiring.getClass().getDeclaredField("m_classLoader");
        m_classLoader.setAccessible(true);
        m_classLoader.set(bundleWiring, bundleClassLoader);

        assertTrue(bundleClassLoader.isParallel());

        final AtomicInteger loaded = new AtomicInteger();
        new Thread() {
            public void run() {
                try
                {
                    loaded.set(bundleClassLoader.findClass(testClassName2) != null ? 1 : 2);
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                    loaded.set(3);
                }
            }
        }.start();

        while (bundleClassLoader.m_gate.getQueueLength() == 0)
        {
            Thread.sleep(1);
        }

        final AtomicInteger loaded2 = new AtomicInteger();
        new Thread() {
            public void run() {
                try
                {
                    loaded2.set(bundleClassLoader.findClass(testClassName3) != null ? 1 : 2);
                }
                catch (ClassNotFoundException e)
                {
                    e.printStackTrace();
                    loaded2.set(3);
                }
            }
        }.start();

        while (loaded2.get() == 0)
        {
            Thread.sleep(1);
        }

        assertEquals(0, loaded.get());
        assertEquals(1, bundleClassLoader.m_gate.getQueueLength());

        loaded2.set(0);
        Thread tester = new Thread() {
            public void run() {
                try
                {
                    loaded2.set(bundleClassLoader.findClass(testClassName2) != null ? 1 : 2);
                }
                catch (ClassNotFoundException e)
                {
                    e.printStackTrace();
                    loaded2.set(3);
                }
            }
        };
        tester.start();

        Thread.sleep(100);

        assertEquals(0, loaded2.get());
        assertEquals(1, bundleClassLoader.m_gate.getQueueLength());

        bundleClassLoader.m_gate.release();


        while (loaded.get() == 0)
        {
            Thread.sleep(1);
        }

        assertEquals(1, loaded.get());

        while (loaded2.get() == 0)
        {
            Thread.sleep(1);
        }
        assertEquals(1, loaded2.get());
    }

    @Test
    public void testClassloadStress() throws Exception
    {
        ExecutorService executors = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 4);
        final List<Throwable> exceptionsNP = Collections.synchronizedList(new ArrayList<Throwable>());
        final List<Throwable> exceptionsP = Collections.synchronizedList(new ArrayList<Throwable>());

        for (int i = 0; i < 100; i++) {
            executors.submit(i % 2 == 0 ? new Runnable()
            {
                @Override
                public void run()
                {
                    try
                    {
                        testNotParallelClassload();
                    }
                    catch (Throwable e)
                    {
                        exceptionsNP.add(e);
                    }
                }
            } : new Runnable()
            {
                @Override
                public void run()
                {
                    try
                    {
                        testParallelClassload();
                    }
                    catch (Throwable e)
                    {
                        exceptionsP.add(e);
                    }
                }
            });
        }
        executors.shutdown();
        executors.awaitTermination(10, TimeUnit.MINUTES);
        assertTrue(exceptionsNP.toString(), exceptionsNP.isEmpty());
        assertTrue(exceptionsP.toString(), exceptionsP.isEmpty());
    }

    @Test
    public void testNotParallelClassload() throws Exception
    {

        Felix mockFramework = mock(Felix.class);
        HookRegistry hReg = mock(HookRegistry.class);
        Mockito.when(mockFramework.getHookRegistry()).thenReturn(hReg);
        Content mockContent = mock(Content.class);
        final Class testClass = TestClassSuper.class;
        final String testClassName = testClass.getName();
        final String testClassAsPath = testClassName.replace('.', '/') + ".class";
        byte[] testClassBytes = createTestClassBytes(testClass, testClassAsPath);

        final Class testClass2 = TestClassChild.class;
        final String testClassName2 = testClass2.getName();
        final String testClassAsPath2 = testClassName2.replace('.', '/') + ".class";
        byte[] testClassBytes2 = createTestClassBytes(testClass2, testClassAsPath2);

        final Class testClass3 = TestClass.class;
        final String testClassName3 = testClass3.getName();
        final String testClassAsPath3 = testClassName3.replace('.', '/') + ".class";
        byte[] testClassBytes3 = createTestClassBytes(testClass3, testClassAsPath3);

        List<Content> contentPath = new ArrayList<Content>();
        contentPath.add(mockContent);
        BundleWiringImpl bundleWiring;

        StatefulResolver mockResolver;

        BundleRevisionImpl mockRevisionImpl;

        BundleImpl mockBundle;

        mockResolver = mock(StatefulResolver.class);
        mockRevisionImpl = mock(BundleRevisionImpl.class);
        mockBundle = mock(BundleImpl.class);

        Logger logger = new Logger();
        Map configMap = new HashMap();
        List<BundleRevision> fragments = new ArrayList<BundleRevision>();
        List<BundleWire> wires = new ArrayList<BundleWire>();
        Map<String, BundleRevision> importedPkgs = new HashMap<String, BundleRevision>();
        Map<String, List<BundleRevision>> requiredPkgs = new HashMap<String, List<BundleRevision>>();

        when(mockRevisionImpl.getBundle()).thenReturn(mockBundle);
        when(mockBundle.getBundleId()).thenReturn(Long.valueOf(1));

        bundleWiring = new BundleWiringImpl(logger, configMap, mockResolver,
            mockRevisionImpl, fragments, wires, importedPkgs, requiredPkgs);

        when(mockBundle.getFramework()).thenReturn(mockFramework);
        when(mockFramework.getBootPackages()).thenReturn(new String[0]);

        when(mockRevisionImpl.getContentPath()).thenReturn(contentPath);
        when(mockContent.getEntryAsBytes(testClassAsPath)).thenReturn(
            testClassBytes);
        when(mockContent.getEntryAsBytes(testClassAsPath2)).thenReturn(
            testClassBytes2);
        when(mockContent.getEntryAsBytes(testClassAsPath3)).thenReturn(
            testClassBytes3);


        final TestBundleClassLoader2 bundleClassLoader = createBundleClassLoader(
            TestBundleClassLoader2.class, bundleWiring);
        assertNotNull(bundleClassLoader);

        Field m_classLoader = bundleWiring.getClass().getDeclaredField("m_classLoader");
        m_classLoader.setAccessible(true);
        m_classLoader.set(bundleWiring, bundleClassLoader);

        assertFalse(bundleClassLoader.isParallel());

        final AtomicInteger loaded = new AtomicInteger();
        new Thread() {
            public void run() {
                try
                {
                    loaded.set(bundleClassLoader.findClass(testClassName2) != null ? 1 : 2);
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                    loaded.set(3);
                }
            }
        }.start();

        while (bundleClassLoader.m_gate.getQueueLength() == 0)
        {
            Thread.sleep(1);
        }

        final AtomicInteger loaded2 = new AtomicInteger();
        new Thread() {
            public void run() {
                try
                {
                    loaded2.set(bundleClassLoader.findClass(testClassName3) != null ? 1 : 2);
                }
                catch (ClassNotFoundException e)
                {
                    e.printStackTrace();
                    loaded2.set(3);
                }
            }
        }.start();

        Thread.sleep(100);

        assertEquals(0, loaded.get());
        assertEquals(0, loaded2.get());
        assertEquals(1, bundleClassLoader.m_gate.getQueueLength());

        final AtomicInteger loaded3 = new AtomicInteger();
        Thread tester = new Thread() {
            public void run() {
                try
                {
                    loaded3.set(bundleClassLoader.findClass(testClassName2) != null ? 1 : 2);
                }
                catch (ClassNotFoundException e)
                {
                    e.printStackTrace();
                    loaded3.set(3);
                }
            }
        };
        tester.start();

        Thread.sleep(100);

        assertEquals(0, loaded3.get());
        assertEquals(0, loaded2.get());

        assertEquals(0, loaded.get());
        assertEquals(1, bundleClassLoader.m_gate.getQueueLength());

        bundleClassLoader.m_gate.release();


        while (loaded.get() == 0)
        {
            Thread.sleep(1);
        }

        assertEquals(1, loaded.get());

        while (loaded2.get() == 0)
        {
            Thread.sleep(1);
        }
        assertEquals(1, loaded2.get());

        while (loaded3.get() == 0)
        {
            Thread.sleep(1);
        }
        assertEquals(1, loaded3.get());
    }

    private static class TestBundleClassLoader extends BundleClassLoader
    {
        static {
            ClassLoader.registerAsParallelCapable();
        }

        Semaphore m_gate = new Semaphore(0);
        public TestBundleClassLoader(BundleWiringImpl wiring, ClassLoader parent, Logger logger)
        {
            super(wiring, parent, logger);
        }

        @Override
        protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException
        {
            if (name.startsWith("java"))
            {
                return getClass().getClassLoader().loadClass(name);
            }
            return super.loadClass(name, resolve);
        }

        @Override
        protected Class findClass(String name) throws ClassNotFoundException
        {
            if (name.startsWith("java"))
            {
                return getClass().getClassLoader().loadClass(name);
            }
            if (name.equals(TestClassSuper.class.getName()))
            {
                m_gate.acquireUninterruptibly();
            }
            return super.findClass(name);
        }
    }

    private static class TestBundleClassLoader2 extends BundleClassLoader
    {
        Semaphore m_gate = new Semaphore(0);
        public TestBundleClassLoader2(BundleWiringImpl wiring, ClassLoader parent, Logger logger)
        {
            super(wiring, parent, logger);
        }

        @Override
        protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException
        {
            if (name.startsWith("java"))
            {
                return getClass().getClassLoader().loadClass(name);
            }
            return super.loadClass(name, resolve);
        }

        @Override
        protected Class findClass(String name) throws ClassNotFoundException
        {
            if (name.startsWith("java"))
            {
                return getClass().getClassLoader().loadClass(name);
            }
            if (name.equals(TestClassSuper.class.getName()))
            {
                m_gate.acquireUninterruptibly();
            }
            return super.findClass(name);
        }

        @Override
        protected boolean isParallel()
        {
            return false;
        }
    }

    @SuppressWarnings("rawtypes")
    private byte[] createTestClassBytes(Class testClass, String testClassAsPath)
            throws IOException
    {
        InputStream testClassResourceStream = testClass.getClassLoader()
                .getResourceAsStream(testClassAsPath);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int curByte;
        while ((curByte = testClassResourceStream.read()) != -1)
        {
            baos.write(curByte);
        }
        byte[] testClassBytes = baos.toByteArray();
        return testClassBytes;
    }

    @SuppressWarnings("rawtypes")
    private <T> T createBundleClassLoader(
            Class<T> bundleClassLoaderClass, BundleWiringImpl bundleWiring)
                    throws Exception
    {
        Logger logger = new Logger();
        Constructor ctor = BundleRevisionImpl.getSecureAction().getConstructor(
                bundleClassLoaderClass,
                new Class[] { BundleWiringImpl.class, ClassLoader.class,
                        Logger.class });
        BundleRevisionImpl.getSecureAction().setAccesssible(ctor);
        T bundleClassLoader = (T) BundleRevisionImpl
                .getSecureAction().invoke(
                        ctor,
                        new Object[] { bundleWiring,
                                this.getClass().getClassLoader(), logger });
        return bundleClassLoader;
    }

    class TestClass
    {
        // An empty test class to weave.
    }

    class TestClassSuper
    {
        // An empty test class to weave.
    }

    class TestClassChild extends TestClassSuper
    {

    }

    class GoodDummyWovenHook implements WeavingHook
    {
        // Adds the awesomePublicField to a class
        @Override
        @SuppressWarnings("unchecked")
        public void weave(WovenClass wovenClass)
        {
            byte[] wovenClassBytes = wovenClass.getBytes();
            ClassNode classNode = new ClassNode();
            ClassReader reader = new ClassReader(wovenClassBytes);
            reader.accept(classNode, 0);
            classNode.fields.add(new FieldNode(Opcodes.ACC_PUBLIC,
                    "awesomePublicField", "Ljava/lang/String;", null, null));
            ClassWriter writer = new ClassWriter(reader, Opcodes.ASM4);
            classNode.accept(writer);
            wovenClass.setBytes(writer.toByteArray());
        }
    }

    class BadDefineWovenHook implements WeavingHook
    {
        // Adds the awesomePublicField twice to the class. This is bad java.
        @Override
        @SuppressWarnings("unchecked")
        public void weave(WovenClass wovenClass)
        {
            byte[] wovenClassBytes = wovenClass.getBytes();
            ClassNode classNode = new ClassNode();
            ClassReader reader = new ClassReader(wovenClassBytes);
            reader.accept(classNode, 0);
            classNode.fields.add(new FieldNode(Opcodes.ACC_PUBLIC,
                    "awesomePublicField", "Ljava/lang/String;", null, null));
            classNode.fields.add(new FieldNode(Opcodes.ACC_PUBLIC,
                    "awesomePublicField", "Ljava/lang/String;", null, null));
            ClassWriter writer = new ClassWriter(reader, Opcodes.ASM4);
            classNode.accept(writer);
            wovenClass.setBytes(writer.toByteArray());
        }
    }

    class BadDummyWovenHook implements WeavingHook
    {
        // Just Blow up
        @Override
        public void weave(WovenClass wovenClass)
        {
            throw new WeavingException("Bad Weaver!");
        }
    }

    class DummyWovenClassListener implements WovenClassListener
    {
        public List<Integer> stateList = new ArrayList<Integer>();

        @Override
        public void modified(WovenClass wovenClass)
        {
            stateList.add(wovenClass.getState());
        }
    }
}
