/*
* Copyright 2008 Alin Dreghiciu.
* Copyright 2009 Toni Menzel.
*
* Licensed 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.ops4j.pax.exam.container.def.internal;
import static org.ops4j.pax.exam.Constants.START_LEVEL_SYSTEM_BUNDLES;
import static org.ops4j.pax.exam.Constants.WAIT_FOREVER;
import static org.ops4j.pax.exam.CoreOptions.bootDelegationPackage;
import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
import static org.ops4j.pax.exam.CoreOptions.systemProperty;
import static org.ops4j.pax.exam.OptionUtils.combine;
import static org.ops4j.pax.exam.OptionUtils.expand;
import static org.ops4j.pax.exam.OptionUtils.filter;
import static org.ops4j.pax.exam.OptionUtils.remove;
import static org.ops4j.pax.exam.container.def.PaxRunnerOptions.scanBundle;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.rmi.NoSuchObjectException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.ops4j.io.FileUtils;
import org.ops4j.pax.exam.CompositeCustomizer;
import org.ops4j.pax.exam.CoreOptions;
import org.ops4j.pax.exam.Info;
import org.ops4j.pax.exam.Option;
import org.ops4j.pax.exam.container.def.options.BundleScannerProvisionOption;
import org.ops4j.pax.exam.container.def.options.RBCLookupTimeoutOption;
import org.ops4j.pax.exam.container.def.options.Scanner;
import org.ops4j.pax.exam.options.ProvisionOption;
import org.ops4j.pax.exam.options.TestContainerStartTimeoutOption;
import org.ops4j.pax.exam.rbc.Constants;
import org.ops4j.pax.exam.rbc.client.RemoteBundleContextClient;
import org.ops4j.pax.exam.spi.container.TestContainer;
import org.ops4j.pax.exam.spi.container.TestContainerException;
import org.ops4j.pax.exam.spi.container.TimeoutException;
import org.ops4j.pax.runner.Run;
import org.ops4j.pax.runner.handler.internal.URLUtils;
import org.ops4j.pax.runner.platform.DefaultJavaRunner;
import org.ops4j.store.Handle;
import org.ops4j.store.Store;
import org.ops4j.store.StoreFactory;
import org.osgi.framework.Bundle;
/**
* {@link TestContainer} implementation using Pax Runner.
*
* @author Alin Dreghiciu (adreghiciu@gmail.com)
* @since 0.3.0, December 09, 2008
*/
class PaxRunnerTestContainer
implements TestContainer
{
/**
* JCL logger.
*/
// private static final Log LOG = GrowlFactory.getLogger( LogFactory.getLog( PaxRunnerTestContainer.class ),
// "Pax Exam",
// GrowlLogger.GROWL_INFO | GrowlLogger.GROWL_ERROR
// );
private static final Log LOG = LogFactory.getLog( PaxRunnerTestContainer.class );
/**
* Number of ports to check for a free rmi communication port.
*/
private static final int AMOUNT_OF_PORTS_TO_CHECK = 100;
/**
* System bundle id.
*/
private static final int SYSTEM_BUNDLE = 0;
/**
* Remote bundle context client.
*/
private final RemoteBundleContextClient m_remoteBundleContextClient;
/**
* Java runner to be used to start up Pax Runner.
*/
private final DefaultJavaRunner m_javaRunner;
/**
* Pax Runner arguments, out of options.
*/
private final ArgumentsBuilder m_arguments;
/**
* test container start timeout.
*/
private final long m_startTimeout;
private final Store<InputStream> m_store;
private final Map<String, Handle> m_cache;
private final CompositeCustomizer m_customizers;
/**
*
*/
private TestContainerSemaphore m_semaphore;
private boolean m_started = false;
/**
* RMI registry.
*/
private Registry m_registry;
/**
* Constructor.
*
* @param javaRunner java runner to be used to start up Pax Runner
* @param options user startup options
*/
PaxRunnerTestContainer( final DefaultJavaRunner javaRunner, final Option... options )
{
m_javaRunner = javaRunner;
m_startTimeout = getTestContainerStartTimeout( options );
int registryPort = createRegistry();
m_remoteBundleContextClient =
new RemoteBundleContextClient( registryPort, getRMITimeout( options ) );
m_arguments = new ArgumentsBuilder( wrap( expand( combine( options, localOptions() ) ) ) );
m_customizers = new CompositeCustomizer( m_arguments.getCustomizers() );
m_store = StoreFactory.sharedLocalStore();
m_cache = new HashMap<String, Handle>();
}
/**
* {@inheritDoc} Delegates to {@link RemoteBundleContextClient}.
*/
public <T> T getService( final Class<T> serviceType )
{
LOG.debug( "Lookup a [" + serviceType.getName() + "]" );
return m_remoteBundleContextClient.getService( serviceType );
}
/**
* {@inheritDoc} Delegates to {@link RemoteBundleContextClient}.
*/
public <T> T getService( final Class<T> serviceType, final long timeoutInMillis )
{
LOG.debug( "Lookup a [" + serviceType.getName() + "]" );
return m_remoteBundleContextClient.getService( serviceType, timeoutInMillis );
}
/**
* {@inheritDoc} Delegates to {@link RemoteBundleContextClient}.
*/
public long installBundle( final String bundleUrl )
{
LOG.debug( "Preparing and Installing bundle [" + bundleUrl + "] .." );
long id;
try
{
id =
m_remoteBundleContextClient.installBundle( m_store.getLocation( storeAndGetData( bundleUrl ) ).toASCIIString() );
}
catch ( IOException e )
{
throw new RuntimeException( e );
}
LOG.debug( "Installed bundle " + bundleUrl + " as ID: " + id );
return id;
}
private Handle storeAndGetData( String bundleUrl )
{
try
{
Handle handle = m_cache.get( bundleUrl );
if ( handle == null )
{
// new, so build, customize and store
URL url = new URL( bundleUrl );
InputStream in = url.openStream();
in = m_customizers.customizeTestProbe( in );
// store in and overwrite handle
handle = m_store.store( in );
m_cache.put( bundleUrl, handle );
}
return handle;
}
catch ( Exception e )
{
LOG.error( "problem in preparing probe. ", e );
}
return null;
}
/**
* {@inheritDoc} Delegates to {@link RemoteBundleContextClient}.
*/
public long installBundle( final String bundleLocation, final byte[] bundle )
{
LOG.debug( "Installing bundle [" + bundleLocation + "] .." );
final long id = m_remoteBundleContextClient.installBundle( bundleLocation, bundle );
LOG.debug( "Installed bundle " + bundleLocation + " as ID: " + id );
return id;
}
/**
* {@inheritDoc} Delegates to {@link RemoteBundleContextClient}.
*/
public void startBundle( long bundleId )
throws TestContainerException
{
LOG.debug( "Starting test bundle with ID " + bundleId );
m_remoteBundleContextClient.startBundle( bundleId );
LOG.debug( "Started test bundle with ID " + bundleId );
}
/**
* {@inheritDoc} Delegates to {@link RemoteBundleContextClient}.
*/
public void setBundleStartLevel( final long bundleId, final int startLevel )
throws TestContainerException
{
m_remoteBundleContextClient.setBundleStartLevel( bundleId, startLevel );
}
/**
* {@inheritDoc}
*/
public void start()
{
LOG.info( "Starting up the test container (Pax Runner " + Info.getPaxRunnerVersion() + " )" );
/**
*/
m_semaphore = new TestContainerSemaphore( m_arguments.getWorkingFolder() );
// this makes sure the system is ready to launch a new instance.
// this could fail, based on what acquire actually checks.
// this also creates some persistent mark that will be removed by m_semaphore.release()
if ( !m_semaphore.acquire() )
{
// here we can react.
// Prompt user with the fact that there might be another instance running.
if ( !FileUtils.delete( m_arguments.getWorkingFolder() ) )
{
throw new RuntimeException( "There might be another instance of Pax Exam running. Have a look at "
+ m_semaphore.getLockFile().getAbsolutePath() );
}
}
// customize environment
m_customizers.customizeEnvironment( m_arguments.getWorkingFolder() );
long startedAt = System.currentTimeMillis();
URLUtils.resetURLStreamHandlerFactory();
Run.start( m_javaRunner, m_arguments.getArguments() );
LOG.info( "Test container (Pax Runner " + Info.getPaxRunnerVersion() + ") started in "
+ ( System.currentTimeMillis() - startedAt ) + " millis" );
LOG.info( "Wait for test container to finish its initialization "
+ ( m_startTimeout == WAIT_FOREVER ? "without timing out" : "for " + m_startTimeout + " millis" ) );
try
{
waitForState( SYSTEM_BUNDLE, Bundle.ACTIVE, m_startTimeout );
}
catch ( TimeoutException e )
{
throw new TimeoutException( "Test container did not initialize in the expected time of " + m_startTimeout
+ " millis" );
}
m_started = true;
}
/**
* {@inheritDoc}
*/
public void stop()
{
LOG.info( "Shutting down the test container (Pax Runner)" );
try
{
if ( m_started )
{
if ( m_remoteBundleContextClient != null )
{
m_remoteBundleContextClient.stop();
}
if ( m_javaRunner != null )
{
m_javaRunner.waitForExit();
}
if ( m_registry != null )
{
try {
UnicastRemoteObject.unexportObject( m_registry, true );
} catch (NoSuchObjectException e) {
LOG.error( "Problem in shutting down RMI registry. ", e );
}
// this is necessary, unfortunately.. RMI wouldn' stop otherwise
System.gc();
LOG.info( "RMI registry stopped" );
m_registry = null;
}
}
}
finally
{
m_semaphore.release();
m_started = false;
}
}
/**
* {@inheritDoc}
*/
public void waitForState( final long bundleId, final int state, final long timeoutInMillis )
throws TimeoutException
{
m_remoteBundleContextClient.waitForState( bundleId, state, timeoutInMillis );
}
/**
* Return the options required by this container implementation.
*
* @return local options
*/
private Option[] localOptions()
{
return new Option[] {
// remote bundle context bundle
mavenBundle().groupId( "org.ops4j.pax.exam" ).artifactId( "pax-exam-container-rbc" ).version(
Info.getPaxExamVersion() ).update(
Info.isPaxExamSnapshotVersion() ).startLevel(
START_LEVEL_SYSTEM_BUNDLES ),
// rmi communication port
systemProperty( Constants.RMI_PORT_PROPERTY ).value( m_remoteBundleContextClient.getRmiPort().toString() ),
// boot delegation for sun.*. This seems only necessary in Knopflerfish version > 2.0.0
bootDelegationPackage( "sun.*" ) };
}
/**
* Wrap provision options that are not already scanner provision bundles with a {@link BundleScannerProvisionOption}
* in order to force update.
*
* @param options options to be wrapped (can be null or an empty array)
* @return eventual wrapped bundles
*/
private Option[] wrap( final Option... options )
{
if ( options != null && options.length > 0 )
{
// get provison options out of options
final ProvisionOption[] provisionOptions = filter( ProvisionOption.class, options );
if ( provisionOptions != null && provisionOptions.length > 0 )
{
final List<Option> processed = new ArrayList<Option>();
for ( final ProvisionOption provisionOption : provisionOptions )
{
if ( !( provisionOption instanceof Scanner ) )
{
processed.add( scanBundle( provisionOption ).start( provisionOption.shouldStart() ).startLevel(
provisionOption.getStartLevel() ).update(
provisionOption.shouldUpdate() ) );
}
else
{
processed.add( provisionOption );
}
}
// finally combine the processed provision options with the original options
// (where provison options are removed)
return combine( remove( ProvisionOption.class, options ),
processed.toArray( new Option[processed.size()] ) );
}
}
// if there is nothing to process of there are no provision options just return the original options
return options;
}
/**
* Determine the rmi lookup timeout.<br/>
* Timeout is dermined by first looking for a {@link RBCLookupTimeoutOption} in the user options. If not specified a
* default is used.
*
* @param options user options
* @return rmi lookup timeout
*/
private static long getRMITimeout( final Option... options )
{
final RBCLookupTimeoutOption[] timeoutOptions = filter( RBCLookupTimeoutOption.class, options );
if ( timeoutOptions.length > 0 )
{
return timeoutOptions[0].getTimeout();
}
return getTestContainerStartTimeout( options );
}
/**
* Determine the timeout while starting the osgi framework.<br/>
* Timeout is dermined by first looking for a {@link TestContainerStartTimeoutOption} in the user options. If not
* specified a default is used.
*
* @param options user options
* @return rmi lookup timeout
*/
private static long getTestContainerStartTimeout( final Option... options )
{
final TestContainerStartTimeoutOption[] timeoutOptions =
filter( TestContainerStartTimeoutOption.class, options );
if ( timeoutOptions.length > 0 )
{
return timeoutOptions[0].getTimeout();
}
return CoreOptions.waitForFrameworkStartup().getTimeout();
}
@Override
public String toString()
{
return "PaxRunnerTestContainer{}";
}
/**
* Creates an RMI registry on the first free port found.
*
* @return RMI registry
*/
protected int createRegistry()
{
for( int port = Registry.REGISTRY_PORT; port <= Registry.REGISTRY_PORT + AMOUNT_OF_PORTS_TO_CHECK; port++ )
{
try {
m_registry = LocateRegistry.createRegistry( port );
LOG.info( "RMI registry started on port [" + port + "]" );
return port;
} catch (Exception e) {
// ignore and try next port number
}
}
throw new RuntimeException( "No free port in range " + Registry.REGISTRY_PORT + ":" + Registry.REGISTRY_PORT + AMOUNT_OF_PORTS_TO_CHECK );
}
}