/******************************************************************************
 * Copyright (c) 2006, 2010 VMware Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution. 
 * The Eclipse Public License is available at 
 * http://www.eclipse.org/legal/epl-v10.html and the Apache License v2.0
 * is available at http://www.opensource.org/licenses/apache2.0.php.
 * You may elect to redistribute this code under either of these licenses. 
 * 
 * Contributors:
 *   VMware Inc.
 *****************************************************************************/

package org.eclipse.gemini.blueprint.test;

import java.util.Enumeration;

import org.eclipse.gemini.blueprint.extender.internal.util.concurrent.Counter;
import org.eclipse.gemini.blueprint.extender.support.internal.ConfigUtils;
import org.eclipse.gemini.blueprint.util.OsgiBundleUtils;
import org.eclipse.gemini.blueprint.util.OsgiListenerUtils;
import org.eclipse.gemini.blueprint.util.OsgiStringUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.springframework.util.ObjectUtils;

/**
 * JUnit superclass which offers synchronization for application context
 * initialization. The class <b>automatically</b> determines
 * <em>Spring powered</em> bundles that are installed by the testing framework
 * and (by default), will wait for their application context to fully start.
 * Only after all the application contexts have been fully refreshed, the actual
 * test execution will commence.
 * 
 * <p/>The class also provides utility waiting methods for discovering Spring
 * application context (published as OSGi services) in case programmatic waiting
 * is required (for example, when installing bundles manually).
 * 
 * <p/>As the rest of the other classes, the behaviour of this class can be
 * customized by extending its methods.
 * 
 * @author Costin Leau
 * @author Adrian Colyer
 * 
 */
public abstract class AbstractSynchronizedOsgiTests extends AbstractConfigurableOsgiTests {

	protected static final long DEFAULT_WAIT_TIME = 60L;

	private static final long SECOND = 1000;


	/**
	 * 
	 * Default constructor. Constructs a new
	 * <code>AbstractSynchronizedOsgiTests</code> instance.
	 * 
	 */
	public AbstractSynchronizedOsgiTests() {
		super();
	}

	/**
	 * Constructs a new <code>AbstractSynchronizedOsgiTests</code> instance.
	 * 
	 * @param name test name
	 */
	public AbstractSynchronizedOsgiTests(String name) {
		super(name);
	}

	/**
	 * Waits for a <em>Spring powered</em> bundle, given by its symbolic name
	 * to be fully started.
	 * 
	 * <p/>Forces the current (test) thread to wait for the a Spring application
	 * context to be published under the given symbolic name. This method allows
	 * waiting for full initialization of Spring OSGi bundles before starting
	 * the actual test execution. This method will use the test bundle context
	 * for service lookup.
	 * 
	 * @param forBundleWithSymbolicName bundle symbolic name
	 * @param timeout maximum time to wait (in seconds) for the application
	 * context to be published
	 */
	protected void waitOnContextCreation(String forBundleWithSymbolicName, long timeout) {
		waitOnContextCreation(bundleContext, forBundleWithSymbolicName, timeout);

	}

	/**
	 * Waits for a <em>Spring powered</em> bundle, given by its symbolic name,
	 * to be fully started.
	 * 
	 * <p/>Forces the current (test) thread to wait for the a Spring application
	 * context to be published under the given symbolic name. This method allows
	 * waiting for full initialization of Spring OSGi bundles before starting
	 * the actual test execution.
	 * 
	 * @param context bundle context to use for service lookup
	 * @param forBundleWithSymbolicName bundle symbolic name
	 * @param timeout maximum time to wait (in seconds) for the application
	 * context to be published
	 */
	protected void waitOnContextCreation(BundleContext context, String forBundleWithSymbolicName, long timeout) {
		// translate from seconds to milliseconds
		long time = timeout * SECOND;

		// use the counter to make sure the threads block
		final Counter counter = new Counter("waitForContext on bnd=" + forBundleWithSymbolicName);

		counter.increment();

		String filter = "(org.springframework.context.service.name=" + forBundleWithSymbolicName + ")";

		ServiceListener listener = new ServiceListener() {

			public void serviceChanged(ServiceEvent event) {
				if (event.getType() == ServiceEvent.REGISTERED)
					counter.decrement();
			}
		};

		OsgiListenerUtils.addServiceListener(context, listener, filter);

		if (logger.isDebugEnabled())
			logger.debug("Start waiting for Spring/OSGi bundle=" + forBundleWithSymbolicName);

		try {
			if (counter.waitForZero(time)) {
				waitingFailed(forBundleWithSymbolicName);
			}
			else if (logger.isDebugEnabled()) {
				logger.debug("Found applicationContext for bundle=" + forBundleWithSymbolicName);
			}
		}
		finally {
			// inform waiting thread
			context.removeServiceListener(listener);
		}
	}

	/**
	 * Waits for a <em>Spring powered</em> bundle, given by its symbolic name,
	 * to be fully started.
	 * 
	 * <p/>This method uses the default wait time and test bundle context and is
	 * identical to #waitOnContextCreation(bundleContext,
	 * forBundleWithSymbolicName, {@link #getDefaultWaitTime()}).
	 * 
	 * <p/>This method is used by the testing framework at startup before
	 * executing the actual tests.
	 * 
	 * @param forBundleWithSymbolicName bundle symbolic name
	 * @see #getDefaultWaitTime()
	 * @see #waitOnContextCreation(BundleContext, String, long)
	 */
	protected void waitOnContextCreation(String forBundleWithSymbolicName) {
		waitOnContextCreation(forBundleWithSymbolicName, getDefaultWaitTime());
	}

	private void waitingFailed(String bundleName) {
		logger.warn("Waiting for applicationContext for bundle=" + bundleName + " timed out");

		throw new RuntimeException("Gave up waiting for application context for '" + bundleName + "' to be created");
	}

	/**
	 * Returns the test default waiting time (in seconds). Subclasses should
	 * override this method if the {@link #DEFAULT_WAIT_TIME} is not enough. For
	 * more customization, consider setting
	 * {@link #shouldWaitForSpringBundlesContextCreation()} to false and using
	 * {@link #waitOnContextCreation(BundleContext, String, long)}.
	 * 
	 * @return the default wait time (in seconds) for each spring bundle context
	 * to be published as an OSGi service
	 */
	protected long getDefaultWaitTime() {
		return DEFAULT_WAIT_TIME;
	}

	/**
	 * Indicates whether the test class should wait or not for the context
	 * creation of Spring/OSGi bundles before executing the tests. Default is
	 * true.
	 * 
	 * @return true (the default) if the test will wait for spring bundle
	 * context creation or false otherwise
	 */
	protected boolean shouldWaitForSpringBundlesContextCreation() {
		return true;
	}

	/*
	 * Takes care of automatically waiting for the application context creation
	 * of <em>Spring powered</em> bundles.
	 */
	protected void postProcessBundleContext(BundleContext platformBundleContext) throws Exception {
		if (shouldWaitForSpringBundlesContextCreation()) {
			boolean debug = logger.isDebugEnabled();
			boolean trace = logger.isTraceEnabled();
			if (debug)
				logger.debug("Looking for Spring/OSGi powered bundles to wait for...");

			// determine Spring/OSGi bundles
			Bundle[] bundles = platformBundleContext.getBundles();
			for (int i = 0; i < bundles.length; i++) {
				Bundle bundle = bundles[i];
				String bundleName = OsgiStringUtils.nullSafeSymbolicName(bundle);
				if (OsgiBundleUtils.isBundleActive(bundle)) {
					if (isSpringDMManaged(bundle) && ConfigUtils.getPublishContext(bundle.getHeaders())) {
						if (debug)
							logger.debug("Bundle [" + bundleName + "] triggers a context creation; waiting for it");
						// use platformBundleContext
						waitOnContextCreation(platformBundleContext, bundleName, getDefaultWaitTime());
					}
					else if (trace)
						logger.trace("Bundle [" + bundleName + "] does not trigger a context creation.");
				}
				else {
					if (trace)
						logger.trace("Bundle [" + bundleName + "] is not active (probably a fragment); ignoring");
				}
			}
		}
	}

	/**
	 * Determines if the given bundle, is Spring DM managed or not. This method
	 * is used at startup, for waiting on all Spring DM contexts to be properly
	 * started and published.
	 * 
	 * @param bundle
	 * @return
	 */
	protected boolean isSpringDMManaged(Bundle bundle) {
		if (!ObjectUtils.isEmpty(ConfigUtils.getHeaderLocations(bundle.getHeaders())))
			return true;
		Enumeration enm = bundle.findEntries("META-INF/spring", "*.xml", false);
		return (enm != null && enm.hasMoreElements());
	}
}
