/*
 * 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.aries.subsystem.itests;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.Test;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.service.subsystem.Subsystem;

public class BundleEventHookTest extends SubsystemTest {
    /*
	 * Bundle-SymbolicName: bundle.a.jar
	 */
	private static final String BUNDLE_A = "bundle.a.jar";
	/*
	 * Bundle-SymbolicName: bundle.b.jar
	 */
	private static final String BUNDLE_B = "bundle.b.jar";
	
	@Override
	public void createApplications() throws Exception {
		createBundleA();
		createBundleB();
	}
	
	private void createBundleA() throws IOException {
		createBundle(name(BUNDLE_A));
	}
	
	private void createBundleB() throws IOException {
		createBundle(name(BUNDLE_B));
	}
    
    /*
     * See https://issues.apache.org/jira/browse/ARIES-982.
     * 
     * When activating, the subsystems bundle must initialize the root subsystem
     * along with any persisted subsystems. Part of the root subsystem 
     * initialization consists of adding all pre-existing bundles as 
     * constituents. In order to ensure that no bundles are missed, a bundle
     * event hook is registered first. The bundle event hook cannot process
     * events until the initialization is complete. Another part of 
     * initialization consists of registering the root subsystem service.
     * Therefore, a potential deadlock exists if something reacts to the
     * service registration by installing an unmanaged bundle.
     */
    @Test
    public void testNoDeadlockWhenSubsystemsInitializing() throws Exception {
    	final Bundle bundle = getSubsystemCoreBundle();
    	bundle.stop();
    	final AtomicBoolean completed = new AtomicBoolean(false);
    	final ExecutorService executor = Executors.newFixedThreadPool(2);
    	try {
	    	bundleContext.addServiceListener(new ServiceListener() {
				@Override
				public void serviceChanged(ServiceEvent event) {
					Future<?> future = executor.submit(new Runnable() {
						public void run() {
							try {
			    				Bundle a = bundle.getBundleContext().installBundle(BUNDLE_A, new FileInputStream(BUNDLE_A));
			    				completed.set(true);
								a.uninstall();
			    			}
			    			catch (Exception e) {
			    				e.printStackTrace();
			    			}
						}
					});
					try {
						future.get();
						completed.set(true);
					}
					catch (Exception e) {
						e.printStackTrace();
					}
				}
	    	}, "(&(objectClass=org.osgi.service.subsystem.Subsystem)(subsystem.id=0))");
	    	Future<?> future = executor.submit(new Runnable() {
	    		public void run() {
	    			try {
	    				bundle.start();
	    			}
	    			catch (Exception e) {
	    				e.printStackTrace();
	    			}
	    		}
	    	});
	    	try {
	    		future.get(3, TimeUnit.SECONDS);
	    		assertTrue("Deadlock detected", completed.get());
	    	}
	    	catch (TimeoutException e) {
	    		fail("Deadlock detected");
	    	}
    	}
    	finally {
    		executor.shutdownNow();
    	}
    }
    
    /*
     * Because bundle events are queued for later asynchronous processing while
     * the root subsystem is initializing, it is possible to see an installed
     * event for a bundle that has been uninstalled (i.e. the bundle revision
     * will be null). These events should be ignored.
     */
    @Test
    public void testIgnoreUninstalledBundleInAsyncInstalledEvent() throws Exception {
    	final Bundle core = getSubsystemCoreBundle();
    	core.stop();
    	final AtomicReference<Bundle> a = new AtomicReference<Bundle>();
    	bundleContext.addServiceListener(
    			new ServiceListener() {
					@SuppressWarnings("unchecked")
					@Override
					public void serviceChanged(ServiceEvent event) {
						if ((event.getType() & (ServiceEvent.REGISTERED | ServiceEvent.MODIFIED)) == 0)
							return;
						if (a.get() != null)
							// We've been here before and already done what needs doing.
							return;
						ServiceReference sr = (ServiceReference)event.getServiceReference();
						bundleContext.getService(sr);
						try {
							// Queue up the installed event.
							a.set(core.getBundleContext().installBundle(BUNDLE_A, new FileInputStream(BUNDLE_A)));
							// Ensure the bundle will be uninstalled before the event is processed.
							a.get().uninstall();
						}
						catch (Exception e) {
							e.printStackTrace();
						}
					}
    			}, 
    			"(&(objectClass=org.osgi.service.subsystem.Subsystem)(subsystem.id=0)(subsystem.state=RESOLVED))");
    	try {
    		// Before the fix, this would fail due to an NPE resulting from a
    		// null bundle revision.
    		core.start();
    	}
    	catch (BundleException e) {
    		e.printStackTrace();
    		fail("Subsystems failed to handle an asynchronous bundle installed event after the bundle was uninstalled");
    	}
    	assertBundleState(a.get(), Bundle.UNINSTALLED);
    	Subsystem root = getRootSubsystem();
    	assertState(Subsystem.State.ACTIVE, root);
    	assertNotConstituent(root, a.get().getSymbolicName());
    }
    
    /*
     * Because bundle events are queued for later asynchronous processing while
     * the root subsystem is initializing, it is possible to see an installed
     * event whose origin bundle has been uninstalled (i.e. the origin bundle's
     * revision will be null). These events should result in the installed
     * bundle being associated with the root subsystem.
     */
    @Test
    public void testIgnoreUninstalledOriginBundleInAsyncInstalledEvent() throws Exception {
    	final Bundle core = getSubsystemCoreBundle();
    	core.stop();
    	final Bundle b = bundleContext.installBundle(BUNDLE_B, new FileInputStream(BUNDLE_B));
    	// Ensure bundle B has a context.
    	b.start();
    	final AtomicReference<Bundle> a = new AtomicReference<Bundle>();
    	bundleContext.addServiceListener(
    			new ServiceListener() {
					@SuppressWarnings("unchecked")
					@Override
					public void serviceChanged(ServiceEvent event) {
						if ((event.getType() & (ServiceEvent.REGISTERED | ServiceEvent.MODIFIED)) == 0)
							return;
						if (a.get() != null)
							// We've been here before and already done what needs doing.
							return;
						ServiceReference sr = (ServiceReference)event.getServiceReference();
						bundleContext.getService(sr);
						try {
							// Queue up the installed event for bundle A using B's context.
							a.set(b.getBundleContext().installBundle(BUNDLE_A, new FileInputStream(BUNDLE_A)));
							// Ensure the origin bundle will be uninstalled before the event is processed.
							b.uninstall();
						}
						catch (Exception e) {
							e.printStackTrace();
						}
					}
    			}, 
    			"(&(objectClass=org.osgi.service.subsystem.Subsystem)(subsystem.id=0)(subsystem.state=RESOLVED))");
    	try {
    		// Before the fix, this would fail due to an NPE resulting from a
    		// null bundle revision.
    		core.start();
    	}
    	catch (BundleException e) {
    		e.printStackTrace();
    		fail("Subsystems failed to handle an asynchronous bundle installed event after the origin bundle was uninstalled");
    	}
    	assertBundleState(a.get(), Bundle.INSTALLED);
    	assertBundleState(b, Bundle.UNINSTALLED);
    	Subsystem root = getRootSubsystem();
    	assertState(Subsystem.State.ACTIVE, root);
    	assertConstituent(root, a.get().getSymbolicName());
    }
}
