Thursday, March 25, 2010

Thoughts on Spring Roo

I started playing with Spring Roo (V1.0.2 at the time of this writing) to see if it would be useful as a RAD tool. I have found several things I like, and some things I don't like. Roo is still pretty new, though, and a lot of the things I do not like may soon be fixed.

What I Like
I like the way Roo generates code. Generating aspects during code generation to augment the model objects is very imaginative and works really well. I like the way it all works together in SpringSource Tool Suite as well. The code generated is really nice.

I also kind of like the command line interface. It provides a simple way to create a model from scratch. As a lover of the shell from way back, it is a very comfortable environment to work in. The tab completion is brilliant and removes the learning curve of the commands for all intents and purposes.

The generated web controller classes are also very REST-like, which is very nice because it allows you to adapt them to a REST API very easily.

Other interfaces such as JMS, email, and security are very well done.

You can provide plugins. That is very nice, there is always a need to tweak or add functionality to a tool like this.

What I don't Like
The command line has no undo capability. Sometimes you make a mistake, and you really do need a way to go back sometimes and do something again. Even a course grained undo to drop an entire entity would help. You could then just create that single entity again. It would also be nice to have a recording of the whole session. This could be used to see exactly what was done, and maybe edit it and be able to play it back when mistakes were made.

Another problem with the command line is the requirement to completely grok your entire model before you start. I am usually unable to hold the entire model in my head without making mistakes inputting it to the command line, and so need to turn to some kind of UML modeling tool to help me conceptualize all the entities and parts. I guess you could do that anyway, and then use the command line to enter the model into Roo, but that seems like a duplication of effort. It would really be useful to be able to import a model (XMI) into Roo.

The relations between entities are also a little off in places. Many-to-many relationships work pretty well, but one-to-many is problematic and one-to-one seems to be non-existent.  The one-to-many relationships are very unintuitive to use in the generated web application. And a composing relationship, where the master creates and manages the life cycle of the many side, is not supported at all. It is possible to rewrite the generated code to do this, but it should be a possibility right out of the box.

I am sure these issues can be overcome in time.

Other Thoughts
On the topic of modeling, I think importing a model into Roo would be a big step forward in usability. Eclipse, the technology under STS,  already has fantastic modeling capabilities with it's modeling project, and that would seem to be an excellent starting point to be able to provide modeling support. Having the EMF meta-model available would be wonderfully valuable. It would be possible to use other tools to work with the model, and we would also be able to directly work with the model using the EMF APIs if we chose to do so. The command line could be transformed into something that actually builds up the model using the EMF meta-model APIs. EMF does not require a UI, so that code could still run outside of STS or any other IDE, preserving the goal of IDE independence.

Roo must also get better at its one-to-* relationships, and provide for a composing relationship. For example, consider two entities: Person and Address, where there is a one-to-many relationship between Person and Address. In a composing relationship when a new address is required you do that from the Person screen/object. You always navigate to the Address object via a Person, and you add and delete addresses vial a Person.

REST support needs to improve as well. The web controllers are already very nicely RESTful, but Roo needs a way to turn on and generate the full support for REST without the user having to edit the context files manually. The REST support will then need to carefully consider the one-to-* relationships and composing relationships to generate all the methods in the correct spots. For instance, the Person controller would add an Address to a person.

It is still a little too difficult to work with large models with Roo, but it is surprisingly easy to work with already, with many powerful features. I think Spring Roo is a very nice RAD tool so far, and I believe it will get better pretty quickly.

Tuesday, December 8, 2009

Configuring the Spring Dynamic Modules Tomcat Server

Spring Dynamic Modules&tm; has a nice Tomcat server implementation that can be started in the OSGi container. This is very handy, but what is a pain is that it can only be configured by fragment bundles. Basically, you create a new server.xml file (as found in the tomcat conf/ directory) and put it in a fragment bundle with the fragment host being org.springframework.osgi.catalina.start.osgi.

That works very well, but it is very painful to have to create a fragment bundle for every configuration. Especially when you have more than one OSGi container running on a single host. In this case you may only want to change the port number(s) of the listeners so that multiple tomcat instances do not clash. It would be nice if we could pull some of the configuration information from the OSGi Configuration Admin service. Here is how to do that

First, grab a copy of Spring's tomcat starter bundle source code. The bundle is the tomcat.start.osgi bundle available from Spring's S3 Maven repo at http://maven.springframework.org/osgi. The source is available as well.

The class that does the work is org.springframework.osgi.web.tomcat.internal.Activator. This class starts tomcat using a separate thread. Looking at the source we can see that the bundle classpath is searched for a resource named conf/server.xml. This file is copied to a temporary location and tomcat is instructed to load using it as its configuration. A default version of this file is provided in the bundle. It looks like:

<Server port="8005" shutdown="SHUTDOWN">

  <Service name="Catalina">
    <Connector port="8080" />

    <Engine name="Catalina" defaultHost="localhost">
      <Host name="localhost" unpackWARs="false" autoDeploy="false"
       liveDeploy="false" deployOnStartup="false"
       xmlValidation="false" xmlNamespaceAware="false"/>
    </Engine>
    
  </Service>
</Server>

A convenient way to introduce the configuration into this file is to do property placeholder substitution while the file is being copied to the temporary location. To do that we only have to change the copyURLToFile method. The following is the replacement method:

 private void copyURLToFile(InputStream inStream, FileOutputStream outStream) {
  PropertyPlaceholderSupport pph = new PropertyPlaceholderSupport(bundleContext);
  byte[] newline = System.getProperty("line.separator").getBytes();
  
  BufferedReader reader = new BufferedReader(new InputStreamReader(inStream));
  String line;
  try {
   while ((line = reader.readLine()) != null) {
    outStream.write(pph.process(line).getBytes());
    outStream.write(newline);
   }
  }
  catch (IOException ex) {
   throw (RuntimeException) new IllegalStateException("Cannot copy URL to file").initCause(ex);
  }
  finally {
   pph.close();
   try {
    inStream.close();
   }
   catch (IOException ignore) {
   }
   try {
    outStream.close();
   }
   catch (IOException ignore) {
   }
  }
 }

This version of the method reads the server xml one line at a time, calling a property placeholder class to substitute variables as it goes. All that is required now is a class that does the property placeholder work. I did not use the Spring property placeholder configurer because I wanted to load properties from Configuration Admin PIDs. Therefore a different property specification is needed. I chose to use a URI to specify the property PID, the property name and the default value. The URI looks like:

pid://thePid/theProperty[?default]

For example: pid://infor.catalina/port?8080

The scheme of the URI is "pid", the authority is used as the PID, the path is used as the property name, and the query string as a default value. You could use any other way of encoding a property reference if this is not to your liking.

This is the PropertyPlaceholderSupport class used:

//**
 * ---Begin Copyright Notice---20090105T1353
 *
 * NOTICE
 *
 * THIS SOFTWARE IS THE PROPERTY OF AND CONTAINS CONFIDENTIAL INFORMATION OF
 * INFOR AND/OR ITS AFFILIATES OR SUBSIDIARIES AND SHALL NOT BE DISCLOSED
 * WITHOUT PRIOR WRITTEN PERMISSION. LICENSED CUSTOMERS MAY COPY AND ADAPT
 * THIS SOFTWARE FOR THEIR OWN USE IN ACCORDANCE WITH THE TERMS OF THEIR
 * SOFTWARE LICENSE AGREEMENT. ALL OTHER RIGHTS RESERVED.
 *
 * (c) COPYRIGHT 2009 INFOR. ALL RIGHTS RESERVED. THE WORD AND DESIGN MARKS
 * SET FORTH HEREIN ARE TRADEMARKS AND/OR REGISTERED TRADEMARKS OF INFOR
 * AND/OR ITS AFFILIATES AND SUBSIDIARIES. ALL RIGHTS RESERVED. ALL OTHER
 * TRADEMARKS LISTED HEREIN ARE THE PROPERTY OF THEIR RESPECTIVE OWNERS.
 *
 * ---End Copyright Notice---
 */
package com.infor.container.catalina.start.internal;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Dictionary;

import org.osgi.framework.BundleContext;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.util.tracker.ServiceTracker;

import com.infor.osgi.util.BundleUtil;


/**
 * Support property place holders in catalina configuration. This class processes
 * place holders with a URI like: <code>pid://<i>thePid</i>/<i>theProperty</i>[?<i>default</i>]</code>.
 * The CM configuration will be consulted first, falling back to a configuration or
 * system property named thePid.theProperty.
 * <p>For example:</p>
 * <code>    pid://infor.catalina/port?8080</code>
 * <p>Will look in CM PID <code>infor.container</code> for a property named <code>port</code>
 * and use that value if it is defined. If there is no property named port in infor.catalina,
 * or infor.catalina does not exist, then search for a context property named 
 * <code>infor.catalina.port</code>. If that does not exist look for a system property named
 * <code>infor.catalina.port</code>. If that also does not exist, then use the default value 8080.</p>
 * 
 * @author dlaidlaw
 *
 */
public class PropertyPlaceholderSupport {
 /** The CM service tracker. */
 private ServiceTracker cmTracker;
 /** The amount of time to wait for CM to become available. */
 private long cmTimeout = 15 * 60 * 1000;
 /** The default PID for the component. */
 private String defaultPid;
 /** The bundle context. */
 private BundleContext context;

 /** The start of a property. */
 public static final String PROPERTY_PREFIX = "${";
 /** The end of a property. */
 public static final String PROPERTY_SUFFIX = "}";
 
 /**
  * Construct a new instance for the specified bundle context.
  * @param context the OSGi bundle context.
  */
 public PropertyPlaceholderSupport(final BundleContext context) {
  super();
  this.context = context;
  
  cmTracker = new ServiceTracker(context, ConfigurationAdmin.class.getName(), null);
  cmTracker.open();
  
  defaultPid = BundleUtil.getInforComponentType(context.getBundle());
 }
 
 /**
  * Close. Releases the service tracker created in the constructor.
  */
 public void close() {
  if (cmTracker != null) {
   cmTracker.close();
   cmTracker = null;
  }
  context = null;
 }
 
 /**
  * Process a string which may contain one or more properties to
  * be substituted.
  * @param str the string to process.
  * @return the same string if there are no property definitions, otherwise,
  *   a new string with the properties substituted.
  * @throws IOException in case there is a ConfigurationAdmin access error.
  */
 public String process(final String str) throws IOException {
  if (str == null || str.length() < 4) {
   return str;
  }
  
  ConfigurationAdmin cm = null;
  try {
   cm = (ConfigurationAdmin) cmTracker.waitForService(cmTimeout);
   if (cm == null) {
    throw new IOException("Timeout waiting for the ConfigurationAdmin service.");
   }
  } catch (InterruptedException e) {
   throw new IOException("Interrupted while waiting for the ConfigurationAdmin Service.", e);
  }
  
  String result = str;
  int start = 0;
  while ((start = result.indexOf(PROPERTY_PREFIX, start)) >= 0) {
   int end = result.indexOf(PROPERTY_SUFFIX, start + 2);
   if (end < 0) {
    break;
   } else if (end - start - 2 <= 0) {
    // empty spec, check for more after it
    start += 3;
    continue;
   }
   
   // A good start and end defined.
   String prefix = (start == 0) ? "" : result.substring(0, start);
   String suffix = (end == result.length() - 1) ? "" : result.substring(end + 1);
   String propDef = result.substring(start + 2, end);
   
   result = prefix.concat(substitute(cm, propDef)).concat(suffix);
  }
  
  return result;
 }
 
 /**
  * Find the substitution value for a property definition.
  * @param cm the {@link ConfigurationAdmin} service.
  * @param propertyDef the property definition.
  * @return the substitute value.
  * @throws IOException in case ConfigurationAdmin cannot be accessed.
  */
 @SuppressWarnings("unchecked")
 protected String substitute(final ConfigurationAdmin cm, final String propertyDef) throws IOException {
  String pid = defaultPid;
  String property = propertyDef;
  String defaultValue = null;
  
  if (propertyDef.startsWith("pid://")) {
   try {
    URI uri = new URI(propertyDef);
    pid = uri.getAuthority();
    property = uri.getPath();
    while (property.startsWith("/")) {
     property = property.substring(1);
    }
    defaultValue = uri.getQuery();
   } catch (URISyntaxException e) {
    throw new IllegalArgumentException("Property definition is not a valid URI: " + propertyDef, e);
   }
  }
  
  Configuration config = cm.getConfiguration(pid);
  if (config == null) {
   return findProperty(pid, property, defaultValue);
  }
  Dictionary<String, ?> dict = config.getProperties();
  if (dict == null) {
   return findProperty(pid, property, defaultValue);
  }
  Object value = dict.get(property);
  if (value == null) {
   return findProperty(pid, property, defaultValue);
  }
  
  return value.toString();
 }
 
 /**
  * Lookup the property in the configuration or system properties, in that order.
  * @param pid the first part of the property name.
  * @param property the second part.
  * @param defaultValue the default value to return if the property does not exist.
  * @return the property if it exists, or the default;
  */
 protected String findProperty(final String pid, final String property, final String defaultValue) {
  String lookup = pid.concat(".").concat(property);
  Object value = context.getProperty(lookup);
  if (value == null) {
   return defaultValue;
  }
  return value.toString();
 }
}

That is all you need to customize the server.xml file for tomcat before it is loaded. Note that this implementation does not listen to configuration changes and so you must stop this bundle and start it again to make any changes take effect. This will bring down the tomcat server and start it up again with the new values.

Tuesday, October 27, 2009

The Configuration Problem

Spring Dynamic Modules for Java has a nice feature to read configuration from the OSGi ConfigurationAdmin service and put them in a PropertyPlaceholderConfigurer to use in your spring contexts. However, there is a rather annoying problem with this support.

The problem is that the PropertyPlaceholderConfigurer is a BeanContextPostProcessor implementation, and therefore gets executed very early in the context creation. Too early in fact. Since the goal is to get configuration properties from the ConfigurationAdmin service, and expose those as a PropertyPlaceholderConfigurer, you would expect that you can simply add a dependency on the ConfigurationAdmin service to your context and have Spring-DM wait for ConfigurationAdmin to become available before processing the PropertyPlaceholderConfigurer. Unfortunately, the PropertyPlaceholderConfigurer is processed before the part where service dependencies are resolved, and it is therefore possible for the PropertyPlaceholderConfigurer to be processed before the ConfigurationAdmin service becomes available, yielding exceptions in your context creation like: could not resolve placeholder x. The standard way to get the PPC populated with ConfigurationAdmin properties is like:

   <!-- Configuration Admin entry -->
   <osgix:cm-properties id="cmProps" persistent-id="com.xyz.myapp">
      <prop key="host">localhost</prop>
   </osgix:cm-properties>

   <!-- placeholder configurer -->
   <ctx:property-placeholder properties-ref="cmProps" />

If ConfigurationAdmin does not happen to already be available in the OSGi container at the time the Spring context is created, the PPC will not be populated and the exceptions will start.

Waiting for ConfigurationAdmin

So how can we force Spring-DM to wait for the ConfigurationAdmin service to become available before processing the PPC? The first thought would be to create a different PPC (wrapping the original, possibly) that implements the org.springframework.osgi.context.DependencyAwareBeanFactoryPostProcessor interface. This would delay the processing of this new PPC until after Spring has resolved all the service dependencies in the context. There is a problem with this approach, however; it would preclude using any properties in the service dependencies themselves. So for instance, you could not use a property placeholder in a filter specification on a service dependency. I did try this, and it works fine, but I wanted to use properties in my filters so another solution was required.

The next thing that could be attempted would be to intercept the context creation before it really gets started and put a wait for ConfigurationAdmin at that point. Spring-DM provides a lot of customization potential, and this proves possible by creating a fragment bundle and providing an implementation of the OsgiApplicationContextCreator interface. This is easily done by extending Spring-DM's DefaultOsgiApplicationContextCreator class and overriding the createApplicationContext(BundleContext) method.

One way to override that method would be to wait for the ConfigurationAdmin service and then delegate to the super(BundleContext) method to create the context, thereby forcing ConfigurationAdmin to become available. However, this will not work because this part of Spring-DM is executed in the OSGi startup thread. This thread is synchronous, starting the context while blocking the container initialization. If ConfigurationAdmin is not available, and you wait for it, it will not become available because you have blocked the startup thread.

So this alone is not sufficient. What is needed is to get the wait for ConfigurationAdmin called by Spring-DM's asynchronous thread that is created to initialize the application context. We will need to create an extension of the OsgiBundleXmlApplicationContext class and have the OsgiApplicationContextCreator we supplied create an instance of this class. The in our custom OsgiBundleXmlApplicationContext class we can override a method that is called by the asynchronous Spring-DM initialization thread, allowing the OSGi startup thread to continue on starting other bundles. It turns out that overriding the prepareRefresh() method is called before the context is refreshed, and more importantly before the BeanFactoryPostProcessors like the PPC are processed.

Of course, it would be a bad idea to wait for ConfigurationAdmin at this point if the initialization of the context is to be done synchronously, so a check on this should be performed first. Then it is a simple matter to put some code in that method to wait for ConfigurationAdmin, then call the super() method and ConfigurationAdmin will be available for the PPC later in the initialization. A timeout on the ConfigurationAdmin wait would be a good idea to prevent waiting forever.

I have put this solution in place in my own use of Spring-DM and now the PPC evaluation always succeeds, even if the ConfigurationAdmin service is registered after the bundles using the PPC.

Friday, May 22, 2009

Spring and OSGi Integration Testing with Maven

Spring has a great test framework for integration testing OSGi bundles in its Spring Dynamic Modules for OSGi&tm; Service Platforms code (Spring-DM for short). This cool piece of code will load up an OSGi container with the Spring framework, and the Spring-DM code and run your JUnit integration test inside the container for you. This is a very handy and powerful tool to have.

What you must do to make this work is extend an abstract base class (like org.springframework.osgi.AbstractConfigurableBundleCreatorTests) and override one of two methods to specify the additional bundles to install into the OSGi container. If you are using Maven there are great features in that class hierarchy for resolving the required bundles in your local maven repository. This is extremely convenient!

As great as this is, there are a couple of things I am not completely happy with there:

  1. You need to specify the bundles being tested, and all its dependencies including versions in code. That information is already in the POM file for the project, which means I now have to maintain the information in two places.
  2. The Spring version and Spring-DM version are specified by the test code. Sometimes in development I want to change and mix-and match these versions, and can't do that unless I override some other methods that also specify the base bundles to load into the framework.

A good solution to these problems would be to use the Maven POM as the source of all the bundles and their versions to load. Then there is only one place you need to maintain that information, and when you change the versions of something in the POM, you do not need to edit the test classes to match. Using the Maven POM we could get all of the transient dependencies of our test project and have the test framework load everything required without any additional coding.

Use Maven

Fortunately the Spring code is very flexible and configurable (and nicely documented). So we can subclass the AbstractConfigurableBundleCreatorTests class and add the features we want to it. First, though, we need to have a list of all the transient dependencies of our project. It is probably possible to use the maven libraries to process the POM and figure this out for us, but an even easier solution is to just have the Maven build generate what we need into a text file, and read that in our new class. The Maven dependency plugin suits this purpose perfectly. Just add the plugin to your test project POM as follows:

    <build>
        <plugins>
             <plugin>
                  <groupId>org.apache.maven.plugins</groupId>
                  <artifactId>maven-dependency-plugin</artifactId>
                  <executions>
                       <execution>
                           <id>list-dependencies</id>
                           <phase>generate-sources</phase>
                           <goals>
                                <goal>resolve</goal>
                           </goals>
                           <configuration>
                                <outputFile>target/dependencies.list</outputFile>
                           </configuration>
                       </execution>
                  </executions>
             </plugin>
        </plugins>
    </build>

This generates all the transitive dependencies of any scope into a file called target/dependencies.list in the project. Now we can use that file to tell the Spring code what bundles to load into the integration test OSGi container.

Subclass

The Spring AbstractConfigurableBundleCreatorTests class loads the base bundles into the OSGi environment. It gets the list of base bundles from some resource files in the jar itself. We want to override that behaviour to have Spring only load the bundles specified in the dependency list output above. We need to subclass AbstractConfigurableBundleCreatorTests and override two methods. First override the getTestingFrameworkBundlesConfiguration() method to return a Spring resource pointing to our dependency list.

/**
 * Get a resource containing the list of dependencies for the current bundle.
 * The dependencies file could be hand crafted, but it is better to use the
 * Maven dependency plugin to generate this file from the POM contents. Override
 * this method to change the file name to something other than the default.
 * @return the dependencies list file as generated by the Maven dependency plugin. 
 * @see org.springframework.osgi.test.AbstractDependencyManagerTests#getTestingFrameworkBundlesConfiguration()
 */
@Override
protected Resource getTestingFrameworkBundlesConfiguration() {
 return new FileSystemResource(DEFAULT_DPENDENCIES_LIST_FILENAME);
}
Then override the getTestFrameworkBundlesNames() method to parse that file and return the bundles to be loaded. Spring expects to see an array of Strings returned from this method where each string is a bundle to load, in the form "groupId,artifactId,version". The output of the dependency plugin is close to this, but we do need to convert it.
/**
 * Returns the names of the test bundles to load. Overrides the spring method
 * to replace the properties file configuration with configuration from the
 * output of the Maven dependency plugin.
 * @return the bundle names as CSV values containing the maven groupId, artifactId, and version.
 * @see org.springframework.osgi.test.AbstractDependencyManagerTests#getTestFrameworkBundlesNames()
 * @see AbstractInforBundleTests#getTestingFrameworkBundlesConfiguration();
 * @throws IllegalArgumentException if the dependency list resource retrieved from the
 *   {@link #getTestingFrameworkBundlesConfiguration()} method throws an IOException.
 */
@Override
protected String[] getTestFrameworkBundlesNames() {
    List<MavenArtifact> artifacts = null;
    try {
        artifacts = MavenDependencyListParser.parseDependencies(getTestingFrameworkBundlesConfiguration());
    } catch (IOException e) {
        String error = "Error loading the dependency list resource: " + getTestingFrameworkBundlesConfiguration();
        log.error(error, e);
        throw new IllegalArgumentException(error, e);
    }
 
    if (log.isTraceEnabled()) {
        log.trace("Maven artifacts " + artifacts);
    }
  
    // Filter and get versions
    Iterator<MavenArtifact> iter = artifacts.iterator();
    while (iter.hasNext()) {
        MavenArtifact artifact = iter.next();
        if (artifact.getGroupId().equals(SPRING_GROUP_ID)
                && artifact.getArtifactId().equals(SPRING_CONTEXT_ID)) {
            springBundledVersion = artifact.getVersion();
        } else if (artifact.getGroupId().equals(SPRING_OSGI_GROUP_ID)) {
            if (artifact.getArtifactId().equals(SPRING_OSGI_CORE_ID)) {
                springOsgiVersion = artifact.getVersion();
            }
        } else if (artifact.getGroupId().equals(ECLIPSE_OSGI)
                && artifact.getArtifactId().equals(ECLIPSE_OSGI)) {
            // filter out this since it is started by the framework specification in
            // the POM.
            iter.remove();
        } else if (artifact.getGroupId().equals(BACKPORT_GROUP_ID)
                && JdkVersion.isAtLeastJava15()) {
            // Filter out backport if Java is 1.5 or higher. 
            iter.remove();
        }
    }
 
    // Copy the filtered artifacts to the bundle array that we need
    // to return from this method.
    String[] bundles = new String[artifacts.size()];
    iter = artifacts.iterator();
    for (int i = 0; iter.hasNext(); ++i) {
        MavenArtifact artifact = iter.next();
 
        bundles[i] = artifact.getGroupId() + "," + artifact.getArtifactId() + ","
                + artifact.getVersion();
    }

    // pass properties to test instance running inside OSGi space
    System.getProperties().put(SPRING_OSGI_VERSION_PROP_KEY, springOsgiVersion);
    System.getProperties().put(SPRING_VERSION_PROP_KEY, springBundledVersion);

    // sort the array (as the dependency list can be any order)
    // Is this really required??
    bundles = StringUtils.sortStringArray(bundles);
    if (log.isDebugEnabled()) {
        log.debug("Default framework bundles :" + ObjectUtils.nullSafeToString(bundles));
    }
 
    return bundles;
} 

The constants used in the above methods are:

/** The path to the resource containing the default bundles to load. */
protected static final String DEFAULT_DPENDENCIES_LIST_FILENAME = "target/dependencies.list";

/** Maven groupId for Spring. */
public static final String SPRING_GROUP_ID = "org.springframework";
/** Maven groupId for Spring OSGi. */
public static final String SPRING_OSGI_GROUP_ID = SPRING_GROUP_ID + ".osgi";
/** Maven artifactId for the Spring context bundle. */
public static final String SPRING_CONTEXT_ID = SPRING_GROUP_ID + ".context";
/** Maven artifactId for the Spring OSGi core bundle. */
public static final String SPRING_OSGI_CORE_ID = SPRING_OSGI_GROUP_ID + ".core";
/** Maven artifactId for the Spring OSGi Test bundle. */
public static final String SPRING_OSGI_TEST_ARTIFACT_ID = SPRING_OSGI_GROUP_ID + ".test";
/** Maven groupId and artifactId for the eclipse framework. */
public static final String ECLIPSE_OSGI = "org.eclipse.osgi";
/** Maven groupId for the backport software library. */
public static final String BACKPORT_GROUP_ID = "edu.emory.mathcs.backport";

/** Store the Spring OSGi version in a system property for the test classes. */
private static final String SPRING_OSGI_VERSION_PROP_KEY = "spring.osgi.version";
/** Store the Spring version in a system property for the test classes. */
private static final String SPRING_VERSION_PROP_KEY = "spring.version";

You also have to override two other methods to return the versions of Spring and Spring-DM that are used. These versions will be discovered from the list of dependencies.

/**
 * Returns the version of the Spring-DM bundles installed by the testing
 * framework.
 * 
 * @return Spring-DM bundles version.
 */
@Override
protected String getSpringDMVersion() {
    if (springOsgiVersion == null) {
        springOsgiVersion = System.getProperty(SPRING_OSGI_VERSION_PROP_KEY);
    }
    return springOsgiVersion;
}

/**
 * Returns the version of the Spring bundles installed by the testing
 * framework.
 * 
 * @return Spring framework dependency version.
 */
@Override
protected String getSpringVersion() {
    if (springBundledVersion == null) {
        springBundledVersion = System.getProperty(SPRING_VERSION_PROP_KEY);
    }
    return springBundledVersion;
}

The getTestFrameworkBundlesNames() method above uses two classes to parse the dependency list:

package com.infor.springframework.osgi.test.internal;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;

import org.springframework.core.io.Resource;

/**
 * Parse the dependency list produced by the Maven dependency plugin
 * and create Maven artifact definitions for each dependency specified.
 * 
 * @author dlaidlaw
 *
 */
public final class MavenDependencyListParser {

    /**
     * Private to prevent instantiation.
     */
    private MavenDependencyListParser() {
        super();
    }

    /**
     * Parse the Maven dependencies list specified as a Spring resource.
     * @param resource the Spring resource pointing to the dependencies list.
     * @return the artifacts from the dependency list.
     * @throws IOException if the resource cannot be read.
     */
    public static List<MavenArtifact> parseDependencies(final Resource resource)
    throws IOException {
        return parseDependencies(new InputStreamReader(resource.getInputStream()));
    }
 
    /**
     * Parse the Maven dependencies list specified as a {@link Reader}.
     * @param reader the reader to use for reading the dependencies list.
     * @return the artifacts from the dependency list.
     * @throws IOException if the resource cannot be read.
     */
    public static List<MavenArtifact> parseDependencies(final Reader reader)
    throws IOException {
        BufferedReader in = new BufferedReader(reader);
        ArrayList<MavenArtifact> artifacts = new ArrayList<MavenArtifact>();
        String line = in.readLine();
        while (line != null) {
            if (isSpecLine(line)) {
                artifacts.add(MavenArtifact.parse(line));
            }
            line = in.readLine();
        }
        return artifacts;
    }
 
    /**
     * A spec line is a line specifying a Maven artifact. For it to be valid
     * there must be at least three colon characters on the line.
     * @param line the line to check.
     * @return true if it is a spec line.
     */
    private static boolean isSpecLine(final String line) {
        int i = line.indexOf(':');
        if (i > 0) {
            // check for a second colon.
            i = line.indexOf(':', i + 1);
            if (i > 0) {
                // check for a third colon
                return (line.indexOf(':', i + 1) >= 0);
            }
        }
        return false;
    }
}
package com.infor.springframework.osgi.test.internal;

/**
 * A maven artifact information container. Holds the maven artifact
 * info. Once an instance of this object is created it cannot be changed.
 * @author dlaidlaw
 *
 */
public class MavenArtifact {
    /** The artifact groupId. */
    private String groupId;
    /** The artifact artifactId. */
    private String artifactId;
    /** The artifact type (jar, war, zip, etc). */
    private String type;
    /** The artifact version. */
    private String version;
    /** The artifact scope. */
    private String scope;

    /** The string value of this object, cached so it does not need to be
     * continuously recalculated.
     */
    private transient String stringValue;

    /**
     * Default constructor. All values are null.
     */
    private MavenArtifact() {
        super();
    }

    /**
     * Full constructor, All values are provided.
     * @param groupId the artifact groupId.
     * @param artifactId the artifact ID.
     * @param type the artifact type (jar, war, zip, etc).
     * @param version the artifact version.
     * @param scope the artifact scope (compile, test, etc);
     */
    public MavenArtifact(final String groupId, final String artifactId,
            final String type, final String version, final String scope) {
        super();
        this.groupId = groupId;
        this.artifactId = artifactId;
        this.type = type;
        this.version = version;
        this.scope = scope;
    }
 
    /**
     * Parse a maven artifact specification as output by the Maven dependency plugin
     * resolve goal.
     * @param spec the specification string formatted as
     *   groupId:artifactId:type:version:scope:other.
     * @return the artifact.
     * @see http://maven.apache.org/plugins/maven-dependency-plugin/resolve-mojo.html
     * @throws IllegalArgumentException if the specification string does not contain
     *   the required parts with colon separators.
     */
    public static MavenArtifact parse(final String spec) {
        String[] parts = spec.trim().split(":");
        // CHECKSTYLE:OFF the number four is not magic.
        if (parts.length < 4) {
            // CHECKSTYLE:ON
            throw new IllegalArgumentException(
                    "The specification must contain at least 5 parts separated by a colon (:)."
                    + " The parts are: groupId:artifactId:type:version:scope:other");
        }

        MavenArtifact ma = new MavenArtifact();
        if (parts[0].length() > 0) {
            ma.groupId = parts[0];
        }
        if (parts[1].length() > 0) {
            ma.artifactId = parts[1];
        }
        if (parts[2].length() > 0) {
            ma.type = parts[2];
        }
        // CHECKSTYLE:OFF the numbers three and four are not magic.
        if (parts[3].length() > 0) {
            ma.version = parts[3];
        }
        if (parts.length >= 4 && parts[4].length() > 0) {
            ma.scope = parts[4];
        }
        // CHECKSTYLE:ON

        return ma;
    }
 
    /**
     * Output the values separated by colons exactly as the 
     * parse method would like to see them. Null values are
     * output as the empty string.
     * @return the string representation of this object.
     */
    @Override
    public String toString() {
        if (stringValue == null) {
            stringValue = asString(groupId) + ":" + asString(artifactId) + ":" 
                + asString(type) + ":" + asString(version)
                + ":" + asString(scope);
        }
        return stringValue;
    }

    /**
     * Return "" if the argument is null.
     * @param s the string to output.
     * @return "" if s is null, or s otherwise.
     */
    private String asString(final String s) {
        if (s == null) {
            return "";
        }
        return s;
    }

    /**
     * Check if the other object is an instance of this
     * class and that all its parts equal the parts in
     * this instance.
     * @param other the object to test against this object.
     * @return true if the other object is an instance of
     *   this class and all its parts equals the parts
     *   of this class.
     */
    @Override
    public boolean equals(final Object other) {
        if ((other == null) || !(other instanceof MavenArtifact)) {
            return false;
        }
        MavenArtifact o = (MavenArtifact) other;
        return (isEqual(this.groupId, o.getGroupId())
                && isEqual(this.artifactId, o.getArtifactId())
                && isEqual(this.type, o.getType())
                && isEqual(this.version, o.getVersion())
                && isEqual(this.scope, o.getScope()));
    }
 
    /**
     * Compare this object to the other object, returning true if the
     * other object's groupId and artifactId are equal to this object's
     * groupId and artifactId.
     * @param other the object to compare to this one.
     * @return true if the other equals this object on the two
     *   id properties.
     */
    public boolean idsEqual(final MavenArtifact other) {
        if (other == null) {
            return false;
        }
        return isEqual(this.groupId, other.getGroupId())
                && isEqual(this.artifactId, other.getArtifactId());
    }

    /**
     * Output the hash code for this object.
     * @return the hashCode calculated as the hashCode of
     *   the string returned by the toString method.
     * @see #toString()
     */
    @Override
    public int hashCode() {
        return toString().hashCode();
    }

    /**
     * Both strings are null or they are equal.
     * @param a one string.
     * @param b the other string.
     * @return true if both are null, or both are equal.
     */
    private boolean isEqual(final String a, final String b) {
        if (a == null) {
            return b == null;
        }
        if (b == null) {
            return false;
        }
        return a.equals(b);
     }

    /**
     * Get the groupId.
     * @return the groupId.
     */
    public String getGroupId() {
        return groupId;
    }

    /**
     * Get the artifactId.
     * @return the artifactId.
     */
    public String getArtifactId() {
        return artifactId;
    }

    /**
     * Get the artifact type (jar, war, zip, etc).
     * @return the type.
     */
    public String getType() {
        return type;
    }

    /**
     * Get the version.
     * @return the version.
     */
    public String getVersion() {
        return version;
    }

    /**
     * Get the scope.
     * @return the scope.
     */
    public String getScope() {
        return scope;
    } 
}

The POM

Your Maven project for the above code will require a POM file. The dependencies in the new test project will also become transient dependencies of any integration test projects that reference this project, and therefore the correct set of bundles will be loaded into the OSGi container. My POM for this project includes these depedencies:

<description>A testing utility bundle that allows the user to set which
    version of spring and spring-dm is being used in the test.
</description>

<properties>
    <version.eclipse.osgi>3.5.0.v20081201-1815</version.eclipse.osgi>
    <version.eclipse.osgi.services>3.1.200.v20071203</version.eclipse.osgi.services>
    <version.equinox.cm>1.0.0.v20080929-1800</version.equinox.cm>
    <version.equinox.event>1.1.0.v20080225</version.equinox.event>
    <version.junit>3.8.2</version.junit>
    <version.spring>2.5.6</version.spring>
    <version.spring.dm>1.2.0</version.spring.dm>
    <version.objectweb.asm>2.2.3</version.objectweb.asm>
    <version.cglib>2.1.3</version.cglib>
    <version.pax.logging>1.3.0</version.pax.logging>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.osgi</groupId>
        <artifactId>org.springframework.osgi.test</artifactId>
        <version>${version.spring.dm}</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.osgi</groupId>
        <artifactId>org.springframework.osgi.core</artifactId>
        <version>${version.spring.dm}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.osgi</groupId>
        <artifactId>org.springframework.osgi.io</artifactId>
        <version>${version.spring.dm}</version>
       <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.osgi</groupId>
        <artifactId>org.springframework.osgi.extender</artifactId>
        <version>${version.spring.dm}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.osgi</groupId>
        <artifactId>org.springframework.osgi.extensions.annotation</artifactId>
        <version>${version.spring.dm}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>org.springframework.test</artifactId>
        <version>${version.spring}</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>org.springframework.context</artifactId>
        <version>${version.spring}</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.objectweb.asm</groupId>
        <artifactId>com.springsource.org.objectweb.asm</artifactId>
        <version>${version.objectweb.asm}</version>
    </dependency>
    <dependency>
        <groupId>net.sourceforge.cglib</groupId>
        <artifactId>com.springsource.net.sf.cglib</artifactId>
        <version>${version.cglib}</version>
    </dependency>
    <dependency>
        <groupId>org.eclipse.osgi</groupId>
        <artifactId>org.eclipse.osgi</artifactId>
        <version>${version.eclipse.osgi}</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.eclipse.osgi</groupId>
        <artifactId>org.eclipse.osgi.services</artifactId>
        <version>${version.eclipse.osgi.services}</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.eclipse.equinox</groupId>
        <artifactId>org.eclipse.equinox.cm</artifactId>
        <version>${version.equinox.cm}</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.eclipse.equinox</groupId>
        <artifactId>org.eclipse.equinox.event</artifactId>
        <version>${version.equinox.event}</version>
        <scope>compile</scope>
    </dependency>
    <!-- Logging -->
    <dependency>
        <groupId>org.ops4j.pax.logging</groupId>
        <artifactId>pax-logging-api</artifactId>
        <version>${version.pax.logging}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.ops4j.pax.logging</groupId>
        <artifactId>pax-logging-service</artifactId>
        <version>${version.pax.logging}</version>
        <scope>provided</scope>
        <exclusions>
            <exclusion>
                <artifactId>log4j</artifactId>
                <groupId>log4j</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- JUnit -->
    <dependency>
        <groupId>org.junit</groupId>
        <artifactId>com.springsource.junit</artifactId>
        <version>${version.junit}</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

Note that I changed the set of bundles loaded from what Spring would load. I used pax-logging instead of SLF4J. At this time the Spring integration test code does not work with JUnit 4, so don't be tempted to update that version until Spring changes the base test framework to accommodate the newer versions.

Summary

Now all you need to do is reference your new project containing the code above in your integration tests and set your POM dependencies to the bundles that are required for the test. All the transient dependencies will be discovered and everything will load into the integration test container.

Note that I used the Eclipse Equinox OSGi container and hard coded that into the code above where the platform bundle is filtered out of the bundles loaded (because it is already loaded by the Spring test framework). It is easy to tweek that section to detect and remove the appropriate bundle if it is present.