FELIX-2033: Provide an easy to use layer for writing pax-exam test for Karaf

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@906029 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/karaf/itests/pom.xml b/karaf/itests/pom.xml
index abc44d4..022dfa9 100644
--- a/karaf/itests/pom.xml
+++ b/karaf/itests/pom.xml
@@ -65,6 +65,12 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.felix.karaf.tooling</groupId>
+            <artifactId>org.apache.felix.karaf.tooling.testing</artifactId>
+            <scope>test</scope>
+        </dependency>
+
         <!-- Pax EXAM -->
         <dependency>
             <groupId>org.ops4j.pax.exam</groupId>
diff --git a/karaf/itests/src/test/java/org/apache/felix/karaf/shell/itests/CoreTest.java b/karaf/itests/src/test/java/org/apache/felix/karaf/shell/itests/CoreTest.java
index 570f031..fb3d014 100644
--- a/karaf/itests/src/test/java/org/apache/felix/karaf/shell/itests/CoreTest.java
+++ b/karaf/itests/src/test/java/org/apache/felix/karaf/shell/itests/CoreTest.java
@@ -16,28 +16,32 @@
  */
 package org.apache.felix.karaf.shell.itests;
 
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Dictionary;
+import java.util.List;
+
+import org.apache.felix.karaf.testing.Helper;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import static org.ops4j.pax.exam.CoreOptions.bootClasspathLibrary;
-import static org.ops4j.pax.exam.CoreOptions.felix;
-import static org.ops4j.pax.exam.CoreOptions.maven;
-import static org.ops4j.pax.exam.CoreOptions.options;
-import static org.ops4j.pax.exam.CoreOptions.systemPackages;
-import static org.ops4j.pax.exam.CoreOptions.systemProperty;
-import static org.ops4j.pax.exam.CoreOptions.equinox;
-import static org.ops4j.pax.exam.CoreOptions.wrappedBundle;
 import org.ops4j.pax.exam.Option;
-import static org.ops4j.pax.exam.OptionUtils.combine;
 import org.ops4j.pax.exam.junit.Configuration;
 import org.ops4j.pax.exam.junit.JUnit4TestRunner;
 import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
 import org.osgi.service.command.CommandProcessor;
 import org.osgi.service.command.CommandSession;
 
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.ops4j.pax.exam.CoreOptions.equinox;
+import static org.ops4j.pax.exam.CoreOptions.felix;
+import static org.ops4j.pax.exam.CoreOptions.options;
+import static org.ops4j.pax.exam.CoreOptions.systemProperty;
+import static org.ops4j.pax.exam.OptionUtils.combine;
+
 @RunWith(JUnit4TestRunner.class)
-public class CoreTest extends AbstractIntegrationTest {
+public class CoreTest extends org.apache.felix.karaf.testing.AbstractIntegrationTest {
 
     @Test
     public void testHelp() throws Exception {
@@ -108,49 +112,17 @@
 //    }
 //
     @Configuration
-    public static Option[] configuration() {
-        Option[] options = options(
+    public static Option[] configuration() throws Exception {
+        Option[] options = combine(
+            // Default karaf environment
+            Helper.getDefaultOptions(),
             // this is how you set the default log level when using pax logging (logProfile)
             systemProperty("org.ops4j.pax.logging.DefaultServiceLog.level").value("DEBUG"),
-            systemProperty("karaf.name").value("root"),
-            systemProperty("karaf.home").value("target/karaf.home"),
-            systemProperty("karaf.base").value("target/karaf.home"),
-            systemProperty("karaf.startLocalConsole").value("false"),
-            systemProperty("karaf.startRemoteShell").value("false"),
-
-            // hack system packages
-            systemPackages("org.apache.felix.karaf.jaas.boot;version=1.99"),
-            bootClasspathLibrary(mavenBundle("org.apache.felix.karaf.jaas", "org.apache.felix.karaf.jaas.boot")).afterFramework(),
-            bootClasspathLibrary(mavenBundle("org.apache.felix.karaf", "org.apache.felix.karaf.main")).afterFramework(),
-
-            // Log
-            mavenBundle("org.ops4j.pax.logging", "pax-logging-api"),
-            mavenBundle("org.ops4j.pax.logging", "pax-logging-service"),
-            // Felix Config Admin
-            mavenBundle("org.apache.felix", "org.apache.felix.configadmin"),
-            // Blueprint
-            mavenBundle("org.apache.geronimo.blueprint", "geronimo-blueprint"),
-
-            // Bundles
-            mavenBundle("org.apache.mina", "mina-core"),
-            mavenBundle("org.apache.sshd", "sshd-core"),
-            mavenBundle("org.apache.felix.karaf.jaas", "org.apache.felix.karaf.jaas.config"),
-            mavenBundle("org.apache.felix.gogo", "org.apache.felix.gogo.runtime"),
-            mavenBundle("org.apache.felix.karaf.shell", "org.apache.felix.karaf.shell.console"),
-            mavenBundle("org.apache.felix.karaf.shell", "org.apache.felix.karaf.shell.osgi"),
-            mavenBundle("org.apache.felix.karaf.shell", "org.apache.felix.karaf.shell.log").noStart(),
-
+            // Test on both equinox and felix
             equinox(), felix()
         );
-        // We need to add pax-exam-junit here when running with the ibm
-        // jdk to avoid the following exception during the test run:
-        // ClassNotFoundException: org.ops4j.pax.exam.junit.Configuration
-        if ("IBM Corporation".equals(System.getProperty("java.vendor"))) {
-            Option[] ibmOptions = options(
-                wrappedBundle(maven("org.ops4j.pax.exam", "pax-exam-junit"))
-            );
-            options = combine(ibmOptions, options);
-        }
+        // Stop the shell log bundle 
+        Helper.findMaven(options, "org.apache.felix.karaf.shell", "org.apache.felix.karaf.shell.log").noStart();
         return options;
     }
 
diff --git a/karaf/itests/src/test/java/org/apache/felix/karaf/shell/itests/FeaturesTest.java b/karaf/itests/src/test/java/org/apache/felix/karaf/shell/itests/FeaturesTest.java
index ec5d198..49f5fc4 100644
--- a/karaf/itests/src/test/java/org/apache/felix/karaf/shell/itests/FeaturesTest.java
+++ b/karaf/itests/src/test/java/org/apache/felix/karaf/shell/itests/FeaturesTest.java
@@ -16,28 +16,26 @@
  */
 package org.apache.felix.karaf.shell.itests;
 
-import static org.junit.Assert.assertNotNull;
+import org.apache.felix.karaf.testing.Helper;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import static org.ops4j.pax.exam.CoreOptions.bootClasspathLibrary;
-import static org.ops4j.pax.exam.CoreOptions.equinox;
-import static org.ops4j.pax.exam.CoreOptions.felix;
-import static org.ops4j.pax.exam.CoreOptions.maven;
-import static org.ops4j.pax.exam.CoreOptions.options;
-import static org.ops4j.pax.exam.CoreOptions.systemPackages;
-import static org.ops4j.pax.exam.CoreOptions.systemProperty;
-import static org.ops4j.pax.exam.CoreOptions.wrappedBundle;
 import org.ops4j.pax.exam.Option;
-import static org.ops4j.pax.exam.OptionUtils.combine;
-import static org.ops4j.pax.exam.container.def.PaxRunnerOptions.scanFeatures;
 import org.ops4j.pax.exam.junit.Configuration;
 import org.ops4j.pax.exam.junit.JUnit4TestRunner;
 import org.osgi.service.blueprint.container.BlueprintContainer;
 import org.osgi.service.command.CommandProcessor;
 import org.osgi.service.command.CommandSession;
 
+import static org.junit.Assert.assertNotNull;
+import static org.ops4j.pax.exam.CoreOptions.equinox;
+import static org.ops4j.pax.exam.CoreOptions.felix;
+import static org.ops4j.pax.exam.CoreOptions.maven;
+import static org.ops4j.pax.exam.CoreOptions.systemProperty;
+import static org.ops4j.pax.exam.OptionUtils.combine;
+import static org.ops4j.pax.exam.container.def.PaxRunnerOptions.scanFeatures;
+
 @RunWith(JUnit4TestRunner.class)
-public class FeaturesTest extends AbstractIntegrationTest {
+public class FeaturesTest extends org.apache.felix.karaf.testing.AbstractIntegrationTest {
 
     @Test
     public void testFeatures() throws Exception {
@@ -53,57 +51,20 @@
     }
 
     @Configuration
-    public static Option[] configuration() {
-        Option[] options = options(
+    public static Option[] configuration() throws Exception{
+        return combine(
+            // Default karaf environment
+            Helper.getDefaultOptions(),
             // this is how you set the default log level when using pax logging (logProfile)
             systemProperty("org.ops4j.pax.logging.DefaultServiceLog.level").value("DEBUG"),
-            systemProperty("karaf.name").value("root"),
-            systemProperty("karaf.home").value("target/karaf.home"),
-            systemProperty("karaf.base").value("target/karaf.home"),
-            systemProperty("karaf.startLocalConsole").value("false"),
-            systemProperty("karaf.startRemoteShell").value("false"),
-
-            // hack system packages
-            systemPackages("org.apache.felix.karaf.jaas.boot;version=1.99"),
-            bootClasspathLibrary(mavenBundle("org.apache.felix.karaf.jaas", "org.apache.felix.karaf.jaas.boot")).afterFramework(),
-            bootClasspathLibrary(mavenBundle("org.apache.felix.karaf", "org.apache.felix.karaf.main")).afterFramework(),
-
-            // Log
-            mavenBundle("org.ops4j.pax.logging", "pax-logging-api"),
-            mavenBundle("org.ops4j.pax.logging", "pax-logging-service"),
-            // Felix Config Admin
-            mavenBundle("org.apache.felix", "org.apache.felix.configadmin"),
-            // Felix Preferences Service
-            mavenBundle("org.apache.felix", "org.apache.felix.prefs"),
-            // Blueprint
-            mavenBundle("org.apache.geronimo.blueprint", "geronimo-blueprint"),
-
-            // Bundles
-            mavenBundle("org.apache.mina", "mina-core"),
-            mavenBundle("org.apache.sshd", "sshd-core"),
-            mavenBundle("org.apache.felix.karaf.jaas", "org.apache.felix.karaf.jaas.config"),
-            mavenBundle("org.apache.felix.karaf.shell", "org.apache.felix.karaf.shell.console"),
-            mavenBundle("org.apache.felix.gogo", "org.apache.felix.gogo.runtime"),
-            mavenBundle("org.apache.felix.karaf.shell", "org.apache.felix.karaf.shell.osgi"),
-            mavenBundle("org.apache.felix.karaf.shell", "org.apache.felix.karaf.shell.log").noStart(),
-
+            // add two features
             scanFeatures(
                     maven().groupId("org.apache.felix.karaf").artifactId("apache-felix-karaf").type("xml").classifier("features").versionAsInProject(),
                     "obr", "wrapper"
             ),
-
+            // Test on both equinox and felix
             equinox(), felix()
         );
-        // We need to add pax-exam-junit here when running with the ibm
-        // jdk to avoid the following exception during the test run:
-        // ClassNotFoundException: org.ops4j.pax.exam.junit.Configuration
-        if ("IBM Corporation".equals(System.getProperty("java.vendor"))) {
-            Option[] ibmOptions = options(
-                wrappedBundle(maven("org.ops4j.pax.exam", "pax-exam-junit"))
-            );
-            options = combine(ibmOptions, options);
-        }
-        return options;
     }
 
 }
diff --git a/karaf/itests/src/test/java/org/apache/felix/karaf/shell/itests/HeaderParser.java b/karaf/itests/src/test/java/org/apache/felix/karaf/shell/itests/HeaderParser.java
new file mode 100644
index 0000000..a6b6387
--- /dev/null
+++ b/karaf/itests/src/test/java/org/apache/felix/karaf/shell/itests/HeaderParser.java
@@ -0,0 +1,204 @@
+/**
+ * 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.karaf.shell.itests;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Utility class to parse a standard OSGi header with paths.
+ *
+ * @author <a href="mailto:dev@geronimo.apache.org">Apache Geronimo Project</a>
+ * @version $Rev: 786132 $, $Date: 2009-06-18 17:47:58 +0200 (Thu, 18 Jun 2009) $
+ */
+public final class HeaderParser  {
+
+    // Private constructor for static final class
+    private HeaderParser() {
+    }
+
+    /**
+     * Parse a given OSGi header into a list of paths
+     *
+     * @param header the OSGi header to parse
+     * @return the list of paths extracted from this header
+     */
+    public static List<PathElement> parseHeader(String header) {
+        List<PathElement> elements = new ArrayList<PathElement>();
+        if (header == null || header.trim().length() == 0) {
+            return elements;
+        }
+        String[] clauses = parseDelimitedString(header, ",");
+        for (String clause : clauses) {
+            String[] tokens = clause.split(";");
+            if (tokens.length < 1) {
+                throw new IllegalArgumentException("Invalid header clause: " + clause);
+            }
+            PathElement elem = new PathElement(tokens[0].trim());
+            elements.add(elem);
+            for (int i = 1; i < tokens.length; i++) {
+                int pos = tokens[i].indexOf('=');
+                if (pos != -1) {
+                    if (pos > 0 && tokens[i].charAt(pos - 1) == ':') {
+                        String name = tokens[i].substring(0, pos - 1).trim();
+                        String value = tokens[i].substring(pos + 1).trim();
+                        if (value.startsWith("\"") && value.endsWith("\"")) {
+                            value = value.substring(1, value.length() - 1);
+                        }
+                        elem.addDirective(name, value);
+                    } else {
+                        String name = tokens[i].substring(0, pos).trim();
+                        String value = tokens[i].substring(pos + 1).trim();
+                        if (value.startsWith("\"") && value.endsWith("\"")) {
+                            value = value.substring(1, value.length() - 1);
+                        }
+                        elem.addAttribute(name, value);
+                    }
+                } else {
+                    elem = new PathElement(tokens[i].trim());
+                    elements.add(elem);
+                }
+            }
+        }
+        return elements;
+    }
+
+    /**
+     * Parses delimited string and returns an array containing the tokens. This
+     * parser obeys quotes, so the delimiter character will be ignored if it is
+     * inside of a quote. This method assumes that the quote character is not
+     * included in the set of delimiter characters.
+     * @param value the delimited string to parse.
+     * @param delim the characters delimiting the tokens.
+     * @return an array of string tokens or null if there were no tokens.
+    **/
+    public static String[] parseDelimitedString(String value, String delim)
+    {
+        if (value == null)
+        {
+           value = "";
+        }
+
+        List list = new ArrayList();
+
+        int CHAR = 1;
+        int DELIMITER = 2;
+        int STARTQUOTE = 4;
+        int ENDQUOTE = 8;
+
+        StringBuffer sb = new StringBuffer();
+
+        int expecting = (CHAR | DELIMITER | STARTQUOTE);
+
+        for (int i = 0; i < value.length(); i++)
+        {
+            char c = value.charAt(i);
+
+            boolean isDelimiter = (delim.indexOf(c) >= 0);
+            boolean isQuote = (c == '"');
+
+            if (isDelimiter && ((expecting & DELIMITER) > 0))
+            {
+                list.add(sb.toString().trim());
+                sb.delete(0, sb.length());
+                expecting = (CHAR | DELIMITER | STARTQUOTE);
+            }
+            else if (isQuote && ((expecting & STARTQUOTE) > 0))
+            {
+                sb.append(c);
+                expecting = CHAR | ENDQUOTE;
+            }
+            else if (isQuote && ((expecting & ENDQUOTE) > 0))
+            {
+                sb.append(c);
+                expecting = (CHAR | STARTQUOTE | DELIMITER);
+            }
+            else if ((expecting & CHAR) > 0)
+            {
+                sb.append(c);
+            }
+            else
+            {
+                throw new IllegalArgumentException("Invalid delimited string: " + value);
+            }
+        }
+
+        if (sb.length() > 0)
+        {
+            list.add(sb.toString().trim());
+        }
+
+        return (String[]) list.toArray(new String[list.size()]);
+    }
+
+    public static class PathElement {
+
+        private String path;
+        private Map<String, String> attributes;
+        private Map<String, String> directives;
+
+        public PathElement(String path) {
+            this.path = path;
+            this.attributes = new HashMap<String, String>();
+            this.directives = new HashMap<String, String>();
+        }
+
+        public String getName() {
+            return this.path;
+        }
+
+        public Map<String, String> getAttributes() {
+            return attributes;
+        }
+
+        public String getAttribute(String name) {
+            return attributes.get(name);
+        }
+
+        public void addAttribute(String name, String value) {
+            attributes.put(name, value);
+        }
+
+        public Map<String, String> getDirectives() {
+            return directives;
+        }
+
+        public String getDirective(String name) {
+            return directives.get(name);
+        }
+
+        public void addDirective(String name, String value) {
+            directives.put(name, value);
+        }
+
+        public String toString() {
+            StringBuilder sb = new StringBuilder(this.path);
+            for (Map.Entry<String,String> directive : this.directives.entrySet()) {
+                sb.append(";").append(directive.getKey()).append(":=").append(directive.getValue());
+            }
+            for (Map.Entry<String,String> attribute : this.attributes.entrySet()) {
+                sb.append(";").append(attribute.getKey()).append("=").append(attribute.getValue());
+            }
+            return sb.toString();
+        }
+
+    }
+}
diff --git a/karaf/pom.xml b/karaf/pom.xml
index 187709a..ae3ff8f 100644
--- a/karaf/pom.xml
+++ b/karaf/pom.xml
@@ -309,6 +309,11 @@
                 <version>${pom.version}</version>
             </dependency>
             <dependency>
+                <groupId>org.apache.felix.karaf.tooling</groupId>
+                <artifactId>org.apache.felix.karaf.tooling.testing</artifactId>
+                <version>${pom.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>org.apache.felix</groupId>
                 <artifactId>org.apache.felix.framework</artifactId>
                 <version>${felix.framework.version}</version>
diff --git a/karaf/tooling/pom.xml b/karaf/tooling/pom.xml
index e42b910..2e3b981 100644
--- a/karaf/tooling/pom.xml
+++ b/karaf/tooling/pom.xml
@@ -34,6 +34,7 @@
 
     <modules>
         <module>features-maven-plugin</module>
+        <module>testing</module>
     </modules>
 
 </project>
diff --git a/karaf/tooling/testing/pom.xml b/karaf/tooling/testing/pom.xml
new file mode 100644
index 0000000..4008c9b
--- /dev/null
+++ b/karaf/tooling/testing/pom.xml
@@ -0,0 +1,176 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <!--
+
+        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.
+    -->
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+      <groupId>org.apache.felix.karaf.tooling</groupId>
+      <artifactId>tooling</artifactId>
+      <version>1.3.0-SNAPSHOT</version>
+    </parent>
+
+    <groupId>org.apache.felix.karaf.tooling</groupId>
+    <artifactId>org.apache.felix.karaf.tooling.testing</artifactId>
+    <packaging>bundle</packaging>
+    <version>1.3.0-SNAPSHOT</version>
+    <name>Apache Felix Karaf :: Testing environment</name>
+
+    <description>
+      A bundle to help using Pax-Exam and Karaf.
+    </description>
+
+    <properties>
+        <appendedResourcesDirectory>${basedir}/../../etc/appended-resources</appendedResourcesDirectory>
+    </properties>
+
+    <dependencies>
+        <!-- Pax EXAM -->
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-container-default</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-junit-extender-impl</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.5</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>${pom.basedir}/src/main/resources</directory>
+                <includes>
+                    <include>**/*</include>
+                </includes>
+            </resource>
+        </resources>
+        <filters>
+            <filter>target/filter.txt</filter>
+        </filters>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-antrun-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>create-prop</id>
+                        <phase>initialize</phase>
+                        <configuration>
+                            <tasks>
+                                <taskdef resource="net/sf/antcontrib/antcontrib.properties" classpathref="maven.plugin.classpath" />
+                                <property name="ant.regexp.regexpimpl" value="org.apache.tools.ant.util.regexp.Jdk14RegexpRegexp" />
+                                <property name="mv" value="${project.version}" />
+                                <echo message="Maven version: ${mv}" />
+                                <propertyregex property="ov.p1" input="${mv}" regexp="(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:[^a-zA-Z0-9](.*))?" replace="\1" defaultValue="0" />
+                                <propertyregex property="ov.p2" input="${mv}" regexp="(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:[^a-zA-Z0-9](.*))?" replace=".\2" defaultValue=".0" />
+                                <propertyregex property="ov.p3" input="${mv}" regexp="(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:[^a-zA-Z0-9](.*))?" replace=".\3" defaultValue=".0" />
+                                <propertyregex property="ov.p4" input="${mv}" regexp="(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:[^a-zA-Z0-9](.*))?" replace=".\4" defaultValue="" />
+                                <propertyregex property="ov.p1a" input="${ov.p1}" regexp="(.+)" replace="\1" defaultValue="0" />
+                                <propertyregex property="ov.p2a" input="${ov.p2}" regexp="(\..+)" replace="\1" defaultValue=".0" />
+                                <propertyregex property="ov.p3a" input="${ov.p3}" regexp="(\..+)" replace="\1" defaultValue=".0" />
+                                <propertyregex property="ov.p4a" input="${ov.p4}" regexp="(\..+)" replace="\1" defaultValue="" />
+                                <property name="ov" value="${ov.p1a}${ov.p2a}${ov.p3a}${ov.p4a}" />
+                                <echo message="OSGi version: ${ov}" />
+                                <mkdir dir="target" />
+                                <echo message="karaf.osgi.version = ${ov}" file="target/filter.txt" />
+                            </tasks>
+                        </configuration>
+                        <goals>
+                            <goal>run</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-resources-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-filtered</id>
+                        <!-- here the phase you need -->
+                        <phase>compile</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${basedir}/target/classes/</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>../../assembly/src/main/filtered-resources/etc</directory>
+                                    <filtering>true</filtering>
+                                    <includes>
+                                        <include>config.properties</include>
+                                        <include>startup.properties</include>
+                                    </includes>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <!-- generate dependencies versions -->
+            <plugin>
+                <groupId>org.apache.servicemix.tooling</groupId>
+                <artifactId>depends-maven-plugin</artifactId>
+                <version>1.1</version>
+                <executions>
+                    <execution>
+                        <id>generate-depends-file</id>
+                        <goals>
+                            <goal>generate-depends-file</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <configuration>
+                    <instructions>
+                        <Bundle-SymbolicName>${pom.artifactId}</Bundle-SymbolicName>
+                        <Export-Package>org.apache.felix.karaf.testing;version=${pom.version}</Export-Package>
+                        <Import-Package>
+                            !org.apache.felix.karaf.testing*,
+                            *
+                        </Import-Package>
+                        <Include-Resource>
+                            {maven-resources},
+                            org/apache/felix/karaf/testing/config.properties=target/classes/config.properties,
+                            org/apache/felix/karaf/testing/startup.properties=target/classes/startup.properties
+                        </Include-Resource>
+                        <_versionpolicy>${bnd.version.policy}</_versionpolicy>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/karaf/itests/src/test/java/org/apache/felix/karaf/shell/itests/AbstractIntegrationTest.java b/karaf/tooling/testing/src/main/java/org/apache/felix/karaf/testing/AbstractIntegrationTest.java
similarity index 94%
rename from karaf/itests/src/test/java/org/apache/felix/karaf/shell/itests/AbstractIntegrationTest.java
rename to karaf/tooling/testing/src/main/java/org/apache/felix/karaf/testing/AbstractIntegrationTest.java
index 950a660..17177b6 100644
--- a/karaf/itests/src/test/java/org/apache/felix/karaf/shell/itests/AbstractIntegrationTest.java
+++ b/karaf/tooling/testing/src/main/java/org/apache/felix/karaf/testing/AbstractIntegrationTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.felix.karaf.shell.itests;
+package org.apache.felix.karaf.testing;
 
 import java.util.Collection;
 import java.util.Dictionary;
@@ -22,7 +22,6 @@
 import java.util.LinkedList;
 import java.util.List;
 
-import org.ops4j.pax.exam.CoreOptions;
 import org.ops4j.pax.exam.Inject;
 import org.ops4j.pax.exam.options.MavenArtifactProvisionOption;
 import org.osgi.framework.Bundle;
@@ -34,6 +33,8 @@
 import org.osgi.framework.ServiceReference;
 import org.osgi.util.tracker.ServiceTracker;
 
+import static org.apache.felix.karaf.testing.Helper.mavenBundle;
+
 public abstract class AbstractIntegrationTest {
 
     public static final long DEFAULT_TIMEOUT = 30000;
@@ -104,10 +105,6 @@
         return null;
     }
 
-    public static MavenArtifactProvisionOption mavenBundle(String groupId, String artifactId) {
-        return CoreOptions.mavenBundle().groupId(groupId).artifactId(artifactId).versionAsInProject();
-    }
-
     /*
      * Explode the dictionary into a ,-delimited list of key=value pairs
      */
@@ -137,4 +134,4 @@
         return result;
     }
 
-}
+}
\ No newline at end of file
diff --git a/karaf/tooling/testing/src/main/java/org/apache/felix/karaf/testing/Helper.java b/karaf/tooling/testing/src/main/java/org/apache/felix/karaf/testing/Helper.java
new file mode 100644
index 0000000..f41a716
--- /dev/null
+++ b/karaf/tooling/testing/src/main/java/org/apache/felix/karaf/testing/Helper.java
@@ -0,0 +1,706 @@
+/*
+ * 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.karaf.testing;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.ops4j.pax.exam.CoreOptions;
+import org.ops4j.pax.exam.Inject;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.options.MavenArtifactProvisionOption;
+import org.ops4j.pax.exam.options.SystemPropertyOption;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+import static org.ops4j.pax.exam.CoreOptions.bootClasspathLibrary;
+import static org.ops4j.pax.exam.CoreOptions.frameworkStartLevel;
+import static org.ops4j.pax.exam.CoreOptions.maven;
+import static org.ops4j.pax.exam.CoreOptions.wrappedBundle;
+import static org.ops4j.pax.exam.OptionUtils.combine;
+
+public final class Helper {
+
+    private Helper() {
+    }
+
+    public static MavenArtifactProvisionOption mavenBundle(String groupId, String artifactId) {
+        return CoreOptions.mavenBundle().groupId(groupId).artifactId(artifactId).versionAsInProject();
+    }
+
+    /*
+     * Explode the dictionary into a ,-delimited list of key=value pairs
+     */
+    private static String explode(Dictionary dictionary) {
+        Enumeration keys = dictionary.keys();
+        StringBuffer result = new StringBuffer();
+        while (keys.hasMoreElements()) {
+            Object key = keys.nextElement();
+            result.append(String.format("%s=%s", key, dictionary.get(key)));
+            if (keys.hasMoreElements()) {
+                result.append(", ");
+            }
+        }
+        return result.toString();
+    }
+
+    /*
+     * Provides an iterable collection of references, even if the original array is null
+     */
+    private static final Collection<ServiceReference> asCollection(ServiceReference[] references) {
+        List<ServiceReference> result = new LinkedList<ServiceReference>();
+        if (references != null) {
+            for (ServiceReference reference : references) {
+                result.add(reference);
+            }
+        }
+        return result;
+    }
+
+    private static final String DELIM_START = "${";
+    private static final String DELIM_STOP = "}";
+
+    /**
+     * <p>
+     * This method performs property variable substitution on the
+     * specified value. If the specified value contains the syntax
+     * <tt>${&lt;prop-name&gt;}</tt>, where <tt>&lt;prop-name&gt;</tt>
+     * refers to either a configuration property or a system property,
+     * then the corresponding property value is substituted for the variable
+     * placeholder. Multiple variable placeholders may exist in the
+     * specified value as well as nested variable placeholders, which
+     * are substituted from inner most to outer most. Configuration
+     * properties override system properties.
+     * </p>
+     *
+     * @param val         The string on which to perform property substitution.
+     * @param currentKey  The key of the property being evaluated used to
+     *                    detect cycles.
+     * @param cycleMap    Map of variable references used to detect nested cycles.
+     * @param configProps Set of configuration properties.
+     * @return The value of the specified string after system property substitution.
+     * @throws IllegalArgumentException If there was a syntax error in the
+     *                                  property placeholder syntax or a recursive variable reference.
+     */
+    private static String substVars(String val, String currentKey,
+                                    Map<String, String> cycleMap, Properties configProps)
+            throws IllegalArgumentException {
+        // If there is currently no cycle map, then create
+        // one for detecting cycles for this invocation.
+        if (cycleMap == null) {
+            cycleMap = new HashMap<String, String>();
+        }
+
+        // Put the current key in the cycle map.
+        cycleMap.put(currentKey, currentKey);
+
+        // Assume we have a value that is something like:
+        // "leading ${foo.${bar}} middle ${baz} trailing"
+
+        // Find the first ending '}' variable delimiter, which
+        // will correspond to the first deepest nested variable
+        // placeholder.
+        int stopDelim = val.indexOf(DELIM_STOP);
+
+        // Find the matching starting "${" variable delimiter
+        // by looping until we find a start delimiter that is
+        // greater than the stop delimiter we have found.
+        int startDelim = val.indexOf(DELIM_START);
+        while (stopDelim >= 0) {
+            int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length());
+            if ((idx < 0) || (idx > stopDelim)) {
+                break;
+            } else if (idx < stopDelim) {
+                startDelim = idx;
+            }
+        }
+
+        // If we do not have a start or stop delimiter, then just
+        // return the existing value.
+        if ((startDelim < 0) && (stopDelim < 0)) {
+            return val;
+        }
+        // At this point, we found a stop delimiter without a start,
+        // so throw an exception.
+        else if (((startDelim < 0) || (startDelim > stopDelim))
+                && (stopDelim >= 0)) {
+            throw new IllegalArgumentException(
+                    "stop delimiter with no start delimiter: "
+                            + val);
+        }
+
+        // At this point, we have found a variable placeholder so
+        // we must perform a variable substitution on it.
+        // Using the start and stop delimiter indices, extract
+        // the first, deepest nested variable placeholder.
+        String variable =
+                val.substring(startDelim + DELIM_START.length(), stopDelim);
+
+        // Verify that this is not a recursive variable reference.
+        if (cycleMap.get(variable) != null) {
+            throw new IllegalArgumentException(
+                    "recursive variable reference: " + variable);
+        }
+
+        // Get the value of the deepest nested variable placeholder.
+        // Try to configuration properties first.
+        String substValue = (configProps != null)
+                ? configProps.getProperty(variable, null)
+                : null;
+        if (substValue == null) {
+            // Ignore unknown property values.
+            substValue = System.getProperty(variable, "");
+        }
+
+        // Remove the found variable from the cycle map, since
+        // it may appear more than once in the value and we don't
+        // want such situations to appear as a recursive reference.
+        cycleMap.remove(variable);
+
+        // Append the leading characters, the substituted value of
+        // the variable, and the trailing characters to get the new
+        // value.
+        val = val.substring(0, startDelim)
+                + substValue
+                + val.substring(stopDelim + DELIM_STOP.length(), val.length());
+
+        // Now perform substitution again, since there could still
+        // be substitutions to make.
+        val = substVars(val, currentKey, cycleMap, configProps);
+
+        // Return the value.
+        return val;
+    }
+
+    private static MavenArtifactProvisionOption convertToMaven(String location) {
+        String[] p = location.split("/");
+        if (p.length >= 4 && p[p.length-1].startsWith(p[p.length-3] + "-" + p[p.length-2])) {
+            MavenArtifactProvisionOption opt = new MavenArtifactProvisionOption();
+            int artifactIdVersionLength = p[p.length-3].length() + 1 + p[p.length-2].length(); // (artifactId + "-" + version).length
+            if (p[p.length-1].charAt(artifactIdVersionLength) == '-') {
+                opt.classifier((p[p.length-1].substring(artifactIdVersionLength + 1, p[p.length-1].lastIndexOf('.'))));
+            }
+            StringBuffer sb = new StringBuffer();
+            for (int j = 0; j < p.length - 3; j++) {
+                if (j > 0) {
+                    sb.append('.');
+                }
+                sb.append(p[j]);
+            }
+            opt.groupId(sb.toString());
+            opt.artifactId(p[p.length-3]);
+            opt.version(p[p.length-2]);
+            opt.type(p[p.length-1].substring(p[p.length-1].lastIndexOf('.') + 1));
+            return opt;
+        } else {
+            throw new IllegalArgumentException("Unable to extract maven information from " + location);
+        }
+    }
+
+    public static Properties getDefaultSystemOptions() {
+        return getDefaultSystemOptions("target/karaf.home");
+    }
+
+    public static Properties getDefaultSystemOptions(String karafHome) {
+        Properties sysProps = new Properties();
+        sysProps.setProperty("karaf.name", "root");
+        sysProps.setProperty("karaf.home", karafHome);
+        sysProps.setProperty("karaf.base", karafHome);
+        sysProps.setProperty("karaf.startLocalConsole", "false");
+        sysProps.setProperty("karaf.startRemoteShell", "false");
+        sysProps.setProperty("org.osgi.framework.startlevel.beginning", "100");
+        return sysProps;
+    }
+
+    public static Option[] getDefaultConfigOptions() throws Exception {
+        return getDefaultConfigOptions(getDefaultSystemOptions(),
+                                       getResource("/org/apache/felix/karaf/testing/config.properties"));
+    }
+
+    public static Option[] getDefaultConfigOptions(Properties sysProps, URL configProperties) throws Exception {
+        // Load props
+        Properties configProps = new Properties();
+        InputStream is = configProperties.openStream();
+        try {
+            configProps.load(is);
+        } finally {
+            is.close();
+        }
+        // Set system props
+        for (Enumeration e = sysProps.propertyNames(); e.hasMoreElements();) {
+            String key = (String) e.nextElement();
+            configProps.setProperty(key, sysProps.getProperty(key));
+        }
+        // Perform variable substitution for system properties.
+        for (Enumeration e = configProps.propertyNames(); e.hasMoreElements();) {
+            String name = (String) e.nextElement();
+            configProps.setProperty(name, substVars(configProps.getProperty(name), name, null, configProps));
+        }
+        // Transform to sys props options
+        List<Option> options = new ArrayList<Option>();
+        for (Enumeration e = configProps.propertyNames(); e.hasMoreElements();) {
+            String name = (String) e.nextElement();
+            String value = configProps.getProperty(name);
+            value = value.replaceAll("\r", "").replaceAll("\n", "").replaceAll(" ", "");
+            options.add(new SystemPropertyOption(name).value(value));
+            System.err.println("sysprop: " + name + " = " + value);
+        }
+        if (configProps.getProperty("org.osgi.framework.startlevel.beginning") != null) {
+            options.add(frameworkStartLevel(Integer.parseInt(configProps.getProperty("org.osgi.framework.startlevel.beginning"))));
+        }
+        return options.toArray(new Option[options.size()]);
+    }
+
+    public static Option[] getDefaultProvisioningOptions() throws Exception {
+        return getDefaultProvisioningOptions(getDefaultSystemOptions(),
+                                            getResource("/org/apache/felix/karaf/testing/startup.properties"));
+    }
+
+    private static URL getResource(String location) throws Exception {
+        URL url = null;
+        if (Thread.currentThread().getContextClassLoader() != null) {
+            url = Thread.currentThread().getContextClassLoader().getResource(location);
+        }
+        if (url == null) {
+            url = Helper.class.getResource(location);
+        }
+        System.err.println("Trying to load resource: " + location + ". Found: " + url);
+        return url;
+    }
+
+    public static Option[] getDefaultProvisioningOptions(Properties sysProps, URL configProperties) throws Exception {
+        Properties startupProps = new Properties();
+        InputStream is = configProperties.openStream();
+        try {
+            startupProps.load(is);
+        } finally {
+            is.close();
+        }
+        // Perform variable substitution for system properties.
+        for (Enumeration e = startupProps.propertyNames(); e.hasMoreElements();) {
+            String name = (String) e.nextElement();
+            startupProps.setProperty(name, substVars(startupProps.getProperty(name), name, null, sysProps));
+        }
+        // Transform to sys props options
+        List<Option> options = new ArrayList<Option>();
+        options.add(bootClasspathLibrary(mavenBundle("org.apache.felix.karaf.jaas", "org.apache.felix.karaf.jaas.boot")).afterFramework());
+        options.add(bootClasspathLibrary(mavenBundle("org.apache.felix.karaf", "org.apache.felix.karaf.main")).afterFramework());
+        for (Enumeration e = startupProps.propertyNames(); e.hasMoreElements();) {
+            String name = (String) e.nextElement();
+            String value = startupProps.getProperty(name);
+            MavenArtifactProvisionOption opt = convertToMaven(name);
+            if (opt.getURL().contains("org.apache.felix.karaf.features")) {
+                opt.noStart();
+            }
+            opt.startLevel(Integer.parseInt(value));
+            options.add(opt);
+        }
+        options.add(mavenBundle("org.apache.felix.karaf.tooling", "org.apache.felix.karaf.tooling.testing"));
+        // We need to add pax-exam-junit here when running with the ibm
+        // jdk to avoid the following exception during the test run:
+        // ClassNotFoundException: org.ops4j.pax.exam.junit.Configuration
+        if ("IBM Corporation".equals(System.getProperty("java.vendor"))) {
+            options.add(wrappedBundle(maven("org.ops4j.pax.exam", "pax-exam-junit")));
+        }
+        return options.toArray(new Option[options.size()]);
+    }
+
+    public static Option[] getDefaultOptions() throws Exception {
+        return combine(getDefaultConfigOptions(), getDefaultProvisioningOptions());
+    }
+
+    public static MavenArtifactProvisionOption findMaven(Option[] options, String groupId, String artifactId) {
+        for (Option option : options) {
+            if (option instanceof MavenArtifactProvisionOption) {
+                MavenArtifactProvisionOption mvn = (MavenArtifactProvisionOption) option;
+                if (mvn.getURL().startsWith("mvn:" + groupId + "/" + artifactId + "/")) {
+                    return mvn;
+                }
+            }
+        }
+        return null;
+    }
+
+    public abstract static class AbstractIntegrationTest {
+
+        public static final long DEFAULT_TIMEOUT = 30000;
+
+        @Inject
+        protected BundleContext bundleContext;
+
+        protected <T> T getOsgiService(Class<T> type, long timeout) {
+            return getOsgiService(type, null, timeout);
+        }
+
+        protected <T> T getOsgiService(Class<T> type) {
+            return getOsgiService(type, null, DEFAULT_TIMEOUT);
+        }
+
+        protected <T> T getOsgiService(Class<T> type, String filter, long timeout) {
+            ServiceTracker tracker = null;
+            try {
+                String flt;
+                if (filter != null) {
+                    if (filter.startsWith("(")) {
+                        flt = "(&(" + Constants.OBJECTCLASS + "=" + type.getName() + ")" + filter + ")";
+                    } else {
+                        flt = "(&(" + Constants.OBJECTCLASS + "=" + type.getName() + ")(" + filter + "))";
+                    }
+                } else {
+                    flt = "(" + Constants.OBJECTCLASS + "=" + type.getName() + ")";
+                }
+                Filter osgiFilter = FrameworkUtil.createFilter(flt);
+                tracker = new ServiceTracker(bundleContext, osgiFilter, null);
+                tracker.open(true);
+                // Note that the tracker is not closed to keep the reference
+                // This is buggy, as the service reference may change i think
+                Object svc = type.cast(tracker.waitForService(timeout));
+                if (svc == null) {
+                    Dictionary dic = bundleContext.getBundle().getHeaders();
+                    System.err.println("Test bundle headers: " + explode(dic));
+
+                    for (ServiceReference ref : asCollection(bundleContext.getAllServiceReferences(null, null))) {
+                        System.err.println("ServiceReference: " + ref);
+                    }
+
+                    for (ServiceReference ref : asCollection(bundleContext.getAllServiceReferences(null, flt))) {
+                        System.err.println("Filtered ServiceReference: " + ref);
+                    }
+
+                    throw new RuntimeException("Gave up waiting for service " + flt);
+                }
+                return type.cast(svc);
+            } catch (InvalidSyntaxException e) {
+                throw new IllegalArgumentException("Invalid filter", e);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        protected Bundle installBundle(String groupId, String artifactId) throws Exception {
+            MavenArtifactProvisionOption mvnUrl = mavenBundle(groupId, artifactId);
+            return bundleContext.installBundle(mvnUrl.getURL());
+        }
+
+        protected Bundle getInstalledBundle(String symbolicName) {
+            for (Bundle b : bundleContext.getBundles()) {
+                if (b.getSymbolicName().equals(symbolicName)) {
+                    return b;
+                }
+            }
+            return null;
+        }
+
+        public static MavenArtifactProvisionOption mavenBundle(String groupId, String artifactId) {
+            return CoreOptions.mavenBundle().groupId(groupId).artifactId(artifactId).versionAsInProject();
+        }
+
+        /*
+         * Explode the dictionary into a ,-delimited list of key=value pairs
+         */
+        private static String explode(Dictionary dictionary) {
+            Enumeration keys = dictionary.keys();
+            StringBuffer result = new StringBuffer();
+            while (keys.hasMoreElements()) {
+                Object key = keys.nextElement();
+                result.append(String.format("%s=%s", key, dictionary.get(key)));
+                if (keys.hasMoreElements()) {
+                    result.append(", ");
+                }
+            }
+            return result.toString();
+        }
+
+        /*
+         * Provides an iterable collection of references, even if the original array is null
+         */
+        private static final Collection<ServiceReference> asCollection(ServiceReference[] references) {
+            List<ServiceReference> result = new LinkedList<ServiceReference>();
+            if (references != null) {
+                for (ServiceReference reference : references) {
+                    result.add(reference);
+                }
+            }
+            return result;
+        }
+
+        private static final String DELIM_START = "${";
+        private static final String DELIM_STOP = "}";
+
+        /**
+         * <p>
+         * This method performs property variable substitution on the
+         * specified value. If the specified value contains the syntax
+         * <tt>${&lt;prop-name&gt;}</tt>, where <tt>&lt;prop-name&gt;</tt>
+         * refers to either a configuration property or a system property,
+         * then the corresponding property value is substituted for the variable
+         * placeholder. Multiple variable placeholders may exist in the
+         * specified value as well as nested variable placeholders, which
+         * are substituted from inner most to outer most. Configuration
+         * properties override system properties.
+         * </p>
+         *
+         * @param val         The string on which to perform property substitution.
+         * @param currentKey  The key of the property being evaluated used to
+         *                    detect cycles.
+         * @param cycleMap    Map of variable references used to detect nested cycles.
+         * @param configProps Set of configuration properties.
+         * @return The value of the specified string after system property substitution.
+         * @throws IllegalArgumentException If there was a syntax error in the
+         *                                  property placeholder syntax or a recursive variable reference.
+         */
+        private static String substVars(String val, String currentKey,
+                                        Map<String, String> cycleMap, Properties configProps)
+                throws IllegalArgumentException {
+            // If there is currently no cycle map, then create
+            // one for detecting cycles for this invocation.
+            if (cycleMap == null) {
+                cycleMap = new HashMap<String, String>();
+            }
+
+            // Put the current key in the cycle map.
+            cycleMap.put(currentKey, currentKey);
+
+            // Assume we have a value that is something like:
+            // "leading ${foo.${bar}} middle ${baz} trailing"
+
+            // Find the first ending '}' variable delimiter, which
+            // will correspond to the first deepest nested variable
+            // placeholder.
+            int stopDelim = val.indexOf(DELIM_STOP);
+
+            // Find the matching starting "${" variable delimiter
+            // by looping until we find a start delimiter that is
+            // greater than the stop delimiter we have found.
+            int startDelim = val.indexOf(DELIM_START);
+            while (stopDelim >= 0) {
+                int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length());
+                if ((idx < 0) || (idx > stopDelim)) {
+                    break;
+                } else if (idx < stopDelim) {
+                    startDelim = idx;
+                }
+            }
+
+            // If we do not have a start or stop delimiter, then just
+            // return the existing value.
+            if ((startDelim < 0) && (stopDelim < 0)) {
+                return val;
+            }
+            // At this point, we found a stop delimiter without a start,
+            // so throw an exception.
+            else if (((startDelim < 0) || (startDelim > stopDelim))
+                    && (stopDelim >= 0)) {
+                throw new IllegalArgumentException(
+                        "stop delimiter with no start delimiter: "
+                                + val);
+            }
+
+            // At this point, we have found a variable placeholder so
+            // we must perform a variable substitution on it.
+            // Using the start and stop delimiter indices, extract
+            // the first, deepest nested variable placeholder.
+            String variable =
+                    val.substring(startDelim + DELIM_START.length(), stopDelim);
+
+            // Verify that this is not a recursive variable reference.
+            if (cycleMap.get(variable) != null) {
+                throw new IllegalArgumentException(
+                        "recursive variable reference: " + variable);
+            }
+
+            // Get the value of the deepest nested variable placeholder.
+            // Try to configuration properties first.
+            String substValue = (configProps != null)
+                    ? configProps.getProperty(variable, null)
+                    : null;
+            if (substValue == null) {
+                // Ignore unknown property values.
+                substValue = System.getProperty(variable, "");
+            }
+
+            // Remove the found variable from the cycle map, since
+            // it may appear more than once in the value and we don't
+            // want such situations to appear as a recursive reference.
+            cycleMap.remove(variable);
+
+            // Append the leading characters, the substituted value of
+            // the variable, and the trailing characters to get the new
+            // value.
+            val = val.substring(0, startDelim)
+                    + substValue
+                    + val.substring(stopDelim + DELIM_STOP.length(), val.length());
+
+            // Now perform substitution again, since there could still
+            // be substitutions to make.
+            val = substVars(val, currentKey, cycleMap, configProps);
+
+            // Return the value.
+            return val;
+        }
+
+        private static MavenArtifactProvisionOption convertToMaven(String location) {
+            String[] p = location.split("/");
+            if (p.length >= 4 && p[p.length-1].startsWith(p[p.length-3] + "-" + p[p.length-2])) {
+                MavenArtifactProvisionOption opt = new MavenArtifactProvisionOption();
+                int artifactIdVersionLength = p[p.length-3].length() + 1 + p[p.length-2].length(); // (artifactId + "-" + version).length
+                if (p[p.length-1].charAt(artifactIdVersionLength) == '-') {
+                    opt.classifier((p[p.length-1].substring(artifactIdVersionLength + 1, p[p.length-1].lastIndexOf('.'))));
+                }
+                StringBuffer sb = new StringBuffer();
+                for (int j = 0; j < p.length - 3; j++) {
+                    if (j > 0) {
+                        sb.append('.');
+                    }
+                    sb.append(p[j]);
+                }
+                opt.groupId(sb.toString());
+                opt.artifactId(p[p.length-3]);
+                opt.version(p[p.length-2]);
+                opt.type(p[p.length-1].substring(p[p.length-1].lastIndexOf('.') + 1));
+                return opt;
+            } else {
+                throw new IllegalArgumentException("Unable to extract maven information from " + location);
+            }
+        }
+
+        public static Properties getDefaultSystemOptions() {
+            return getDefaultSystemOptions("target/karaf.home");
+        }
+
+        public static Properties getDefaultSystemOptions(String karafHome) {
+            Properties sysProps = new Properties();
+            sysProps.setProperty("karaf.name", "root");
+            sysProps.setProperty("karaf.home", karafHome);
+            sysProps.setProperty("karaf.base", karafHome);
+            sysProps.setProperty("karaf.startLocalConsole", "false");
+            sysProps.setProperty("karaf.startRemoteShell", "false");
+            sysProps.setProperty("org.osgi.framework.startlevel.beginning", "100");
+            return sysProps;
+        }
+
+        public static Option[] getDefaultConfigOptions() throws Exception {
+            return getDefaultConfigOptions(getDefaultSystemOptions(),
+                                           Helper.class.getResource("/config.properties"));
+        }
+
+        public static Option[] getDefaultConfigOptions(Properties sysProps, URL configProperties) throws Exception {
+            // Load props
+            Properties configProps = new Properties();
+            InputStream is = configProperties.openStream();
+            try {
+                configProps.load(is);
+            } finally {
+                is.close();
+            }
+            // Set system props
+            for (Enumeration e = sysProps.propertyNames(); e.hasMoreElements();) {
+                String key = (String) e.nextElement();
+                configProps.setProperty(key, sysProps.getProperty(key));
+            }
+            // Perform variable substitution for system properties.
+            for (Enumeration e = configProps.propertyNames(); e.hasMoreElements();) {
+                String name = (String) e.nextElement();
+                configProps.setProperty(name, substVars(configProps.getProperty(name), name, null, configProps));
+            }
+            // Transform to sys props options
+            List<Option> options = new ArrayList<Option>();
+            for (Enumeration e = configProps.propertyNames(); e.hasMoreElements();) {
+                String name = (String) e.nextElement();
+                String value = configProps.getProperty(name);
+                value = value.replaceAll("\r", "").replaceAll("\n", "").replaceAll(" ", "");
+                options.add(new SystemPropertyOption(name).value(value));
+            }
+            if (configProps.getProperty("org.osgi.framework.startlevel.beginning") != null) {
+                options.add(frameworkStartLevel(Integer.parseInt(configProps.getProperty("org.osgi.framework.startlevel.beginning"))));
+            }
+            return options.toArray(new Option[options.size()]);
+        }
+
+        public static Option[] getDefaultProvisioningOptions() throws Exception {
+            return getDefaultProvisioningOptions(getDefaultSystemOptions(),
+                                                 Helper.class.getResource("/startup.properties"));
+        }
+
+        public static Option[] getDefaultProvisioningOptions(Properties sysProps, URL configProperties) throws Exception {
+            Properties startupProps = new Properties();
+            InputStream is = configProperties.openStream();
+            try {
+                startupProps.load(is);
+            } finally {
+                is.close();
+            }
+            // Perform variable substitution for system properties.
+            for (Enumeration e = startupProps.propertyNames(); e.hasMoreElements();) {
+                String name = (String) e.nextElement();
+                startupProps.setProperty(name, substVars(startupProps.getProperty(name), name, null, sysProps));
+            }
+            // Transform to sys props options
+            List<Option> options = new ArrayList<Option>();
+            options.add(bootClasspathLibrary(mavenBundle("org.apache.felix.karaf.jaas", "org.apache.felix.karaf.jaas.boot")).afterFramework());
+            options.add(bootClasspathLibrary(mavenBundle("org.apache.felix.karaf", "org.apache.felix.karaf.main")).afterFramework());
+            for (Enumeration e = startupProps.propertyNames(); e.hasMoreElements();) {
+                String name = (String) e.nextElement();
+                String value = startupProps.getProperty(name);
+                MavenArtifactProvisionOption opt = convertToMaven(name);
+                if (opt.getURL().contains("org.apache.felix.karaf.features")) {
+                    opt.noStart();
+                }
+                opt.startLevel(Integer.parseInt(value));
+                options.add(opt);
+            }
+            // We need to add pax-exam-junit here when running with the ibm
+            // jdk to avoid the following exception during the test run:
+            // ClassNotFoundException: org.ops4j.pax.exam.junit.Configuration
+            if ("IBM Corporation".equals(System.getProperty("java.vendor"))) {
+                options.add(wrappedBundle(maven("org.ops4j.pax.exam", "pax-exam-junit")));
+            }
+            return options.toArray(new Option[options.size()]);
+        }
+
+        public static Option[] getDefaultOptions() throws Exception {
+            return combine(getDefaultConfigOptions(), getDefaultProvisioningOptions());
+        }
+
+        public static MavenArtifactProvisionOption findMaven(Option[] options, String groupId, String artifactId) {
+            for (Option option : options) {
+                if (option instanceof MavenArtifactProvisionOption) {
+                    MavenArtifactProvisionOption mvn = (MavenArtifactProvisionOption) option;
+                    if (mvn.getURL().startsWith("mvn:" + groupId + "/" + artifactId + "/")) {
+                        return mvn;
+                    }
+                }
+            }
+            return null;
+        }
+    }
+}
\ No newline at end of file