SLING-4728 - move new Crankstart (that uses provisioning model) under contrib

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1684874 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..62c6c53
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,178 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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">
+    
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache</groupId>
+        <artifactId>apache</artifactId>
+        <version>10</version>
+        <relativePath />
+    </parent>
+
+    <groupId>org.apache.sling</groupId>
+    <artifactId>org.apache.sling.crankstart3.launcher</artifactId>
+    <packaging>jar</packaging>
+    <version>1.0.1-SNAPSHOT</version>
+
+    <name>Apache Sling Crankstart Launcher</name>
+    <inceptionYear>2014</inceptionYear>
+    
+    <description>
+        A different way of starting Sling
+    </description>
+
+    <scm>
+      <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/contrib/crankstart/launcher</connection>
+      <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/contrib/crankstart/launcher</developerConnection>
+      <url>http://sling.apache.org</url>
+    </scm>
+
+    <properties>
+        <pax.url.version>2.1.0</pax.url.version>
+    </properties>
+ 
+    <build>
+        <plugins>
+             <plugin>
+               <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>org.apache.sling.crankstart.launcher.Launcher</mainClass>
+                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+             <plugin>
+             <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>embed-dependencies</id>
+                        <goals>
+                            <goal>unpack-dependencies</goal>
+                        </goals>
+                        <configuration>
+                            <includeGroupIds>org.slf4j,org.ops4j.pax.url,org.apache.sling</includeGroupIds>
+                            <excludeTransitive>false</excludeTransitive>
+                            <outputDirectory>${project.build.directory}/classes</outputDirectory>
+                            <overWriteReleases>false</overWriteReleases>
+                            <overWriteSnapshots>false</overWriteSnapshots>
+                        </configuration>
+                    </execution>
+                </executions>
+             </plugin>
+           <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.6</source>
+                    <target>1.6</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-clean-plugin</artifactId>
+                <version>2.2</version>
+                <configuration>
+                    <filesets>
+                        <fileset>
+                            <directory>${basedir}</directory>
+                            <includes>
+                                <include>CRANKSTART</include>
+                                <include>felix-cache</include>
+                            </includes>
+                        </fileset>
+                    </filesets>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <systemProperties>
+                        <!-- pax url needs the local Maven repository to find snapshots we just built  -->
+                        <org.ops4j.pax.url.mvn.localRepository>${settings.localRepository}/</org.ops4j.pax.url.mvn.localRepository>
+                    </systemProperties>
+                </configuration>
+            </plugin>
+          </plugins>
+    </build>   
+
+    <dependencies>
+       <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <version>4.2.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.framework</artifactId>
+            <version>4.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+         <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.7.6</version>
+            <scope>provided</scope>
+        </dependency>
+         <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>1.7.6</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.provisioning.model</artifactId>
+            <version>1.1.0</version>
+            <scope>provided</scope>
+        </dependency>
+         <dependency>
+            <groupId>org.ops4j.pax.url</groupId>
+            <artifactId>pax-url-aether</artifactId>
+            <version>${pax.url.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.url</groupId>
+            <artifactId>pax-url-commons</artifactId>
+            <version>${pax.url.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.11</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.testing</artifactId>
+            <version>2.0.16</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.json</artifactId>
+            <version>2.0.6</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.4</version>
+            <scope>test</scope>
+        </dependency>
+      </dependencies>
+</project>
diff --git a/src/main/java/org/apache/sling/crankstart/launcher/BundlesInstaller.java b/src/main/java/org/apache/sling/crankstart/launcher/BundlesInstaller.java
new file mode 100644
index 0000000..929ca41
--- /dev/null
+++ b/src/main/java/org/apache/sling/crankstart/launcher/BundlesInstaller.java
@@ -0,0 +1,87 @@
+/*
+ * 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.sling.crankstart.launcher;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import org.apache.sling.provisioning.model.Artifact;
+import org.apache.sling.provisioning.model.ArtifactGroup;
+import org.apache.sling.provisioning.model.Feature;
+import org.apache.sling.provisioning.model.Model;
+import org.apache.sling.provisioning.model.RunMode;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.startlevel.BundleStartLevel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Install bundles from a provisioning model */
+public class BundlesInstaller {
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private final Model model;
+    
+    public BundlesInstaller(Model m) {
+        model = m;
+    }
+    
+    public void installBundles(BundleContext ctx, FeatureFilter filter) throws IOException, BundleException {
+        for(Feature f : model.getFeatures()) {
+            if(filter.ignoreFeature(f)) {
+                log.info("Ignoring feature: {}", f.getName());
+                continue;
+            }
+            
+            log.info("Processing feature: {}", f.getName());
+            for(RunMode rm : f.getRunModes()) {
+                for(ArtifactGroup g : rm.getArtifactGroups()) {
+                    final int startLevel = g.getStartLevel();
+                    for(Artifact a : g) {
+                        // TODO for now, naively assume a is a bundle, and mvn: protocol
+                        final String url = "mvn:" + a.getGroupId() + "/" + a.getArtifactId() + "/" + a.getVersion();
+                        installBundle(ctx, url, startLevel);
+                    }
+                }
+            }
+        }
+    }
+    
+    protected boolean ignoreFeature(Feature f) {
+        return false;
+    }
+    
+    public void installBundle(BundleContext ctx, String bundleUrl, int startLevel) throws IOException, BundleException {
+        final URL url = new URL(bundleUrl);
+        final InputStream bundleStream = url.openStream();
+        try {
+            final Bundle b = ctx.installBundle(bundleUrl, url.openStream());
+            if(startLevel > 0) {
+                final BundleStartLevel bsl = (BundleStartLevel)b.adapt(BundleStartLevel.class);
+                if(bsl == null) {
+                    log.warn("Bundle does not adapt to BundleStartLevel, cannot set start level: {}", bundleUrl);
+                }
+                bsl.setStartLevel(startLevel);
+            }
+            
+            log.info("bundle installed at start level {}: {}", startLevel, bundleUrl);
+        } finally {
+            bundleStream.close();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/crankstart/launcher/Configurations.java b/src/main/java/org/apache/sling/crankstart/launcher/Configurations.java
new file mode 100644
index 0000000..a0edf96
--- /dev/null
+++ b/src/main/java/org/apache/sling/crankstart/launcher/Configurations.java
@@ -0,0 +1,138 @@
+/*
+ * 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.sling.crankstart.launcher;
+
+import java.io.Closeable;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Dictionary;
+
+import org.apache.sling.provisioning.model.Configuration;
+import org.apache.sling.provisioning.model.Feature;
+import org.apache.sling.provisioning.model.Model;
+import org.apache.sling.provisioning.model.RunMode;
+import org.osgi.framework.BundleContext;
+import org.osgi.util.tracker.ServiceTracker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Setup the OSGi configurations based on a provisioning model */
+public class Configurations implements Closeable {
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    public static final String CONFIG_ADMIN_CLASS = "org.osgi.service.cm.ConfigurationAdmin";
+    private final Model model;
+    private final ServiceTracker tracker;
+    private ConfigAdminProxy proxy; 
+    
+    /** We use reflection to talk to ConfigAdmin, to avoid classloader issues as 
+     *  the service comes from inside the OSGi framework and we are outside of that.
+     */
+    private static class ConfigAdminProxy {
+        private final Object svc;
+        
+        ConfigAdminProxy(Object configAdminService) {
+            svc = configAdminService;
+        }
+        
+        Object createFactoryConfiguration(String factoryPid) 
+                throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
+            return svc.getClass()
+                    .getMethod("createFactoryConfiguration", String.class)
+                    .invoke(svc, factoryPid);
+
+        }
+        
+        Object getConfiguration(String pid) 
+                throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
+            return svc.getClass()
+                    .getMethod("getConfiguration", String.class)
+                    .invoke(svc, pid);
+            
+        }
+        
+        void setConfigBundleLocation(Object config, String location) 
+                throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
+            config.getClass()
+            .getMethod("setBundleLocation", String.class)
+            .invoke(config, location);
+        }
+        
+        void updateConfig(Object config, Dictionary<String, Object> properties) 
+                throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
+            config.getClass()
+            .getMethod("update", Dictionary.class)
+            .invoke(config, properties);
+        }
+    }
+    
+    public Configurations(BundleContext ctx, Model m) {
+        model = m;
+        tracker = new ServiceTracker(ctx, CONFIG_ADMIN_CLASS, null);
+        tracker.open();
+    }
+    
+    public void close() {
+        tracker.close();
+    }
+    
+    /** Activate our configurations if possible, and if not done already.
+     *  Can be called as many times as convenient, to make sure this happens
+     *  as early as possible.
+     */
+    public synchronized void maybeConfigure() {
+        if(proxy != null) {
+            log.debug("Configurations already activated, doing nothing");
+            return;
+        }
+        final Object service = tracker.getService(); 
+        if(service == null) {
+            log.debug("ConfigurationAdmin service not yet available, doing nothing");
+            return;
+        }
+        
+        proxy = new ConfigAdminProxy(service);
+        log.info("Activating configurations from provisioning model");
+        for(Feature f : model.getFeatures()) {
+            for(RunMode r : f.getRunModes()) {
+                for(Configuration c : r.getConfigurations()) {
+                    try {
+                        setConfig(c);
+                    } catch(Exception e) {
+                        log.warn("Failed to activate configuration " + c, e);
+                    }
+                }
+            }
+        }
+    }
+    
+    private void setConfig(Configuration c) 
+            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
+        final String factoryPid = c.getFactoryPid();
+        String configSource = null;
+        Object config = null;
+        if(factoryPid != null) {
+            config = proxy.createFactoryConfiguration(factoryPid);
+            configSource = "factory PID " + factoryPid;
+        } else {
+            config = proxy.getConfiguration(c.getPid());
+            configSource = "PID " + c.getPid();
+        }
+
+        proxy.setConfigBundleLocation(config, null);
+        proxy.updateConfig(config, c.getProperties());
+        log.info("Created and updated Configuration using [{}]: [{}]", configSource, config);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/crankstart/launcher/FeatureFilter.java b/src/main/java/org/apache/sling/crankstart/launcher/FeatureFilter.java
new file mode 100644
index 0000000..02d6174
--- /dev/null
+++ b/src/main/java/org/apache/sling/crankstart/launcher/FeatureFilter.java
@@ -0,0 +1,23 @@
+/*
+ * 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.sling.crankstart.launcher;
+
+import org.apache.sling.provisioning.model.Feature;
+
+public interface FeatureFilter {
+    boolean ignoreFeature(Feature f);
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/crankstart/launcher/FrameworkProperties.java b/src/main/java/org/apache/sling/crankstart/launcher/FrameworkProperties.java
new file mode 100644
index 0000000..5213be2
--- /dev/null
+++ b/src/main/java/org/apache/sling/crankstart/launcher/FrameworkProperties.java
@@ -0,0 +1,62 @@
+/*
+ * 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.sling.crankstart.launcher;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.provisioning.model.Feature;
+import org.apache.sling.provisioning.model.KeyValueMap;
+import org.apache.sling.provisioning.model.Model;
+import org.apache.sling.provisioning.model.RunMode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Get OSGi framework properties from a provisioning model */
+public class FrameworkProperties {
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private final Model model;
+    private Map<String, String> fprops;
+    
+    public static final String CRANKSTART_SYSPROP_OVERRIDE_PREFIX = "sling.crankstart.";
+    
+    public FrameworkProperties(Model m) {
+        model = m;
+    }
+    
+    public synchronized Map<String, String> getProperties(FeatureFilter filter) {
+        if(fprops == null) {
+            fprops = new HashMap<String, String>();
+            for(Feature f : model.getFeatures()) {
+                if(filter != null && filter.ignoreFeature(f)) {
+                    continue;
+                }
+                for(RunMode rm : f.getRunModes()) {
+                    final KeyValueMap<String> settings = rm.getSettings();
+                    if(settings.size() > 0) {
+                        log.info("Using settings from Feature {}, RunMode {} as framework properties", f.getName(), rm.getNames());
+                        for(Map.Entry<String, String> e : settings) {
+                            log.info("framework property set from provisioning model: {}={}", e.getKey(), e.getValue());
+                            fprops.put(e.getKey(), e.getValue());
+                        }
+                    }
+                }
+            }
+        }
+        return fprops;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/crankstart/launcher/FrameworkSetup.java b/src/main/java/org/apache/sling/crankstart/launcher/FrameworkSetup.java
new file mode 100644
index 0000000..04689f7
--- /dev/null
+++ b/src/main/java/org/apache/sling/crankstart/launcher/FrameworkSetup.java
@@ -0,0 +1,120 @@
+/*
+ * 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.sling.crankstart.launcher;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import org.apache.sling.provisioning.model.Model;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.launch.Framework;
+import org.osgi.framework.launch.FrameworkFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Setup the OSGi framework based on a provisioning model */
+@SuppressWarnings("serial")
+public class FrameworkSetup extends HashMap<String, Object> implements Callable<Object> {
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    
+    @SuppressWarnings("unchecked")
+    private <T> T require(String key, Class<T> desiredType) throws IllegalStateException {
+        final Object o = get(key);
+        if(o == null) {
+            throw new IllegalStateException("Missing required object:" + key);
+        }
+        if(!o.getClass().isAssignableFrom(desiredType)) {
+            throw new ClassCastException("Object '" + key + "' is not a " + desiredType.getName());
+        }
+        return (T)o;
+    }
+    
+    public Object call() throws Exception {
+        final Model model = require(Launcher.MODEL_KEY, Model.class);
+        
+        log.info("Setting OSGi framework properties");
+        final Map<String, String> fprops = new FrameworkProperties(model).getProperties(Launcher.ONLY_CRANKSTART_FILTER);
+                
+        log.info("Starting the OSGi framework");
+        final FrameworkFactory factory = (FrameworkFactory)getClass().getClassLoader().loadClass("org.apache.felix.framework.FrameworkFactory").newInstance();
+        final Framework framework = factory.newFramework(fprops);
+        framework.start();
+        final Configurations cfg = new Configurations(framework.getBundleContext(), model);
+        setShutdownHook(framework, new Closeable[] { cfg });
+        log.info("OSGi framework started");
+        
+        log.info("Installing bundles from provisioning model");
+        final BundlesInstaller bi = new BundlesInstaller(model);
+        final BundleContext bc = framework.getBundleContext();
+        bi.installBundles(bc, Launcher.NOT_CRANKSTART_FILTER);
+        cfg.maybeConfigure();
+        
+        // TODO shall we gradually increase start levels like the launchpad does?? Reuse that code?
+        final Bundle [] bundles = bc.getBundles();
+        log.info("Starting all bundles ({} bundles installed)", bundles.length);
+        int started = 0;
+        int failed = 0;
+        for(Bundle b : bundles) {
+            try {
+                b.start();
+                started++;
+            } catch(BundleException be) {
+                failed++;
+                log.warn("Error starting bundle " + b.getSymbolicName(), be);
+            }
+            cfg.maybeConfigure();
+        }
+        log.info("{} bundles started, {} failed to start, total {}", started, failed, bundles.length);
+        
+        log.info("OSGi setup done, waiting for framework to stop");
+        framework.waitForStop(0);
+        
+        return null;
+    }
+    
+    private void setShutdownHook(final Framework osgiFramework, final Closeable ... toClose) {
+        // Shutdown the framework when the JVM exits
+        Runtime.getRuntime().addShutdownHook(new Thread() {
+            @Override
+            public void run() {
+                if(osgiFramework != null && osgiFramework.getState() == Bundle.ACTIVE) {
+                    try {
+                        log.info("Stopping the OSGi framework");
+                        osgiFramework.stop();
+                        log.info("Waiting for the OSGi framework to exit");
+                        osgiFramework.waitForStop(0);
+                        log.info("OSGi framework stopped");
+                    } catch(Exception e) {
+                        log.error("Exception while stopping OSGi framework", e);
+                    } finally {
+                        for(Closeable c : toClose) {
+                            try {
+                                c.close();
+                            } catch(IOException ignore) {
+                            }
+                        }
+                    }
+                }
+            }
+        });
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/crankstart/launcher/Launcher.java b/src/main/java/org/apache/sling/crankstart/launcher/Launcher.java
new file mode 100644
index 0000000..8e94874
--- /dev/null
+++ b/src/main/java/org/apache/sling/crankstart/launcher/Launcher.java
@@ -0,0 +1,197 @@
+/*
+ * 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.sling.crankstart.launcher;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.Callable;
+
+import org.apache.sling.provisioning.model.Artifact;
+import org.apache.sling.provisioning.model.ArtifactGroup;
+import org.apache.sling.provisioning.model.Feature;
+import org.apache.sling.provisioning.model.Model;
+import org.apache.sling.provisioning.model.ModelUtility;
+import org.apache.sling.provisioning.model.RunMode;
+import org.apache.sling.provisioning.model.ModelUtility.VariableResolver;
+import org.apache.sling.provisioning.model.io.ModelReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Launch an OSGi app instance using the Sling provisioning model */
+public class Launcher {
+    private final Model model = new Model();
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    
+    public static final String CRANKSTART_FEATURE = ":crankstart";
+    public static final String MODEL_KEY = "model";
+    public static final String FRAMEWORK_KEY = "framework";
+    
+    public static final String VARIABLE_OVERRIDE_PREFIX = "crankstart.model.";
+
+    /** Allow for overriding model variables with system properties */ 
+    private final VariableResolver overridingVariableResolver = new VariableResolver() {
+        @Override
+        public String resolve(Feature f, String variableName) {
+            final String overrideKey = VARIABLE_OVERRIDE_PREFIX + variableName;
+            final String sysProp = System.getProperty(overrideKey);
+            if(sysProp == null) {
+                return f.getVariables().get(variableName);
+            } else {
+                log.info("Overriding model variable {}={} (from system property {})", variableName, sysProp, overrideKey);
+                return sysProp;
+            }
+        }
+        
+    };
+    
+    public static final FeatureFilter NOT_CRANKSTART_FILTER = new FeatureFilter() {
+        @Override
+        public boolean ignoreFeature(Feature f) {
+            return Launcher.CRANKSTART_FEATURE.equals(f.getName());
+        }
+    };
+    
+    public static final FeatureFilter ONLY_CRANKSTART_FILTER = new FeatureFilter() {
+        @Override
+        public boolean ignoreFeature(Feature f) {
+            return !Launcher.CRANKSTART_FEATURE.equals(f.getName());
+        }
+    };
+    
+    public Launcher(String ... args) throws IOException {
+        // Find all files to read and sort the list, to be deterministic
+        final SortedSet<File> toRead = new TreeSet<File>();
+        
+        for(String name : args) {
+            final File f = new File(name);
+            if(f.isDirectory()) {
+                final String [] list = f.list();
+                for(String s : list) {
+                    toRead.add(new File(f, s));
+                }
+            } else {
+                toRead.add(f);
+            }
+        }
+        
+        for(File f : toRead) {
+            mergeModel(f);
+        }
+    }
+    
+    /** Can be called before launch() to read and merge additional models.
+     *  @param r provisioning model to read, closed by this method after reading */ 
+    public void mergeModel(Reader r, String sourceInfo) throws IOException {
+        try {
+            log.info("Merging provisioning model {}", sourceInfo);
+            final Model m = ModelReader.read(r, sourceInfo);
+            ModelUtility.merge(model, ModelUtility.getEffectiveModel(m, overridingVariableResolver));
+        } finally {
+            r.close();
+        }
+    }
+    
+    /** Can be called before launch() to read and merge additional models */
+    public void mergeModel(File f) throws IOException {
+        mergeModel(new BufferedReader(new FileReader(f)), f.getAbsolutePath());
+    }
+    
+    public void launch() throws Exception {
+        // Enable pax URL for mvn: protocol
+        System.setProperty( "java.protocol.handler.pkgs", "org.ops4j.pax.url" );
+
+        // Setup initial classpath to launch the OSGi framework
+        for(URL u : getClasspathURLs(model, CRANKSTART_FEATURE)) {
+            addToClasspath(u);
+        }
+
+        // Need to load FrameworkSetup in this way to avoid any static references to OSGi classes in this class, as those are
+        // not available yet when this class is loaded.
+        final Callable<?> c = (Callable<?>) getClass().getClassLoader().loadClass("org.apache.sling.crankstart.launcher.FrameworkSetup").newInstance();
+        @SuppressWarnings("unchecked") final Map<String, Object> cmap = (Map<String, Object>)c; 
+        cmap.put(MODEL_KEY, model);
+        c.call();
+    }
+    
+    /** Slightly hacky way to add URLs to the system classloader,
+     *  based on http://stackoverflow.com/questions/60764/how-should-i-load-jars-dynamically-at-runtime
+     */  
+    private void addToClasspath(URL u) throws IOException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+        final URLClassLoader sysLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
+        final Method m = URLClassLoader.class.getDeclaredMethod("addURL",new Class[]{URL.class});
+        m.setAccessible(true);
+        m.invoke(sysLoader,new Object[]{ u });
+        log.info("Added to classpath: {}", u);
+    }
+    
+    /** Convert a Model feature to a set of URLs meant to setup
+     *  an URLClassLoader.
+     */
+    List<URL> getClasspathURLs(Model m, String featureName) throws MalformedURLException {
+        final List<URL> result = new ArrayList<URL>();
+        
+        // Add all URLs from the special feature to our classpath
+        final Feature f = m.getFeature(featureName);
+        if(f == null) {
+            log.warn("No {} feature found in provisioning model, nothing to add to our classpath", featureName);
+        } else {
+            for(RunMode rm : f.getRunModes()) {
+                for(ArtifactGroup g : rm.getArtifactGroups()) {
+                    for(Artifact a : g) {
+                        final String url = mvnUrl(a);
+                        try {
+                            result.add(new URL(url));
+                        } catch(MalformedURLException e) {
+                            final MalformedURLException up = new MalformedURLException("Invalid URL [" + url + "]");
+                            up.initCause(e);
+                            throw up;
+                        }
+                    }
+                }
+            }
+        }
+        return result;
+    }
+    
+    public static String mvnUrl(Artifact a) {
+        return "mvn:" + a.getGroupId() + "/" + a.getArtifactId() + "/" + a.getVersion();
+    }
+    
+     public static void main(String [] args) throws Exception {
+        if(args.length < 1) {
+            System.err.println("Usage: Main provisioning-model [provisioning-model ...]"); 
+            System.err.println("Where provisioning-model is either a Sling provisioning model file");
+            System.err.println("or a folder that contains oseveral of those.");
+            System.exit(0);
+        }
+        
+        new Launcher(args).launch();
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/crankstart/launcher/CrankstartBootstrapTest.java b/src/test/java/org/apache/sling/crankstart/launcher/CrankstartBootstrapTest.java
new file mode 100644
index 0000000..680ac93
--- /dev/null
+++ b/src/test/java/org/apache/sling/crankstart/launcher/CrankstartBootstrapTest.java
@@ -0,0 +1,304 @@
+package org.apache.sling.crankstart.launcher;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.ServerSocket;
+import java.util.Random;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.http.HttpResponse;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.apache.sling.commons.json.JSONArray;
+import org.apache.sling.commons.json.JSONObject;
+import org.apache.sling.commons.testing.junit.Retry;
+import org.apache.sling.commons.testing.junit.RetryRule;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+
+/** Verify that we can start the Felix HTTP service
+ *  with a {@link CrankstartBootstrap}. 
+ */
+public class CrankstartBootstrapTest {
+    
+    private static final int port = getAvailablePort();
+    private static DefaultHttpClient client;
+    private static Thread crankstartThread;
+    private static String baseUrl = "http://localhost:" + port;
+    public static final int LONG_TIMEOUT = 10000;
+    public static final int STD_INTERVAL = 250;
+    
+    public static final String [] MODEL_PATHS = {
+        "/crankstart-model.txt",
+        "/provisioning-model/base.txt",
+        "/provisioning-model/sling-extensions.txt",
+        "/provisioning-model/start-level-99.txt",
+        "/provisioning-model/crankstart-tests.txt"
+    };
+            
+    @Rule
+    public final RetryRule retryRule = new RetryRule();
+    
+    private static int getAvailablePort() {
+        int result = -1;
+        ServerSocket s = null;
+        try {
+            try {
+                s = new ServerSocket(0);
+                result = s.getLocalPort();
+            } finally {
+                if(s != null) {
+                    s.close();
+                }
+            }
+        } catch(Exception e) {
+            throw new RuntimeException("getAvailablePort failed", e);
+        }
+        return result;
+    }
+    
+    @Before
+    public void setupHttpClient() {
+        client = new DefaultHttpClient(); 
+    }
+    
+    private void setAdminCredentials() {
+        client.getCredentialsProvider().setCredentials(
+                AuthScope.ANY, 
+                new UsernamePasswordCredentials("admin", "admin"));
+        client.addRequestInterceptor(new PreemptiveAuthInterceptor(), 0);
+    }
+    
+    private static void mergeModelResource(Launcher launcher, String path) throws IOException {
+        final InputStream is = CrankstartBootstrapTest.class.getResourceAsStream(path);
+        assertNotNull("Expecting test resource to be found:" + path, is);
+        final Reader input = new InputStreamReader(is);
+        try {
+            launcher.mergeModel(input, path);
+        } finally {
+            input.close();
+        }
+    }
+     
+    @BeforeClass
+    public static void setup() throws IOException {
+        client = new DefaultHttpClient(); 
+        final HttpUriRequest get = new HttpGet(baseUrl);
+        System.setProperty("crankstart.model.http.port", String.valueOf(port));
+        System.setProperty("crankstart.model.osgi.storage.path", getOsgiStoragePath());
+        
+        try {
+            client.execute(get);
+            fail("Expecting connection to " + port + " to fail before starting HTTP service");
+        } catch(IOException expected) {
+        }
+        
+        final Launcher launcher = new Launcher();
+        for(String path : MODEL_PATHS) {
+            mergeModelResource(launcher, path);
+        }
+        
+        crankstartThread = new Thread() {
+            public void run() {
+                try {
+                    launcher.launch();
+                } catch(Exception e) {
+                    e.printStackTrace();
+                    fail("Launcher exception:" + e);
+                }
+            }
+        };
+        crankstartThread.setDaemon(true);
+        crankstartThread.start();
+    }
+    
+    @AfterClass
+    public static void cleanup() throws InterruptedException {
+        crankstartThread.interrupt();
+        crankstartThread.join();
+    }
+    
+    private void closeConnection(HttpResponse r) throws IOException {
+        if(r != null && r.getEntity() != null) {
+            EntityUtils.consume(r.getEntity());
+        }
+    }
+    
+    @Test
+    @Retry(timeoutMsec=CrankstartBootstrapTest.LONG_TIMEOUT, intervalMsec=CrankstartBootstrapTest.STD_INTERVAL)
+    public void testHttpRoot() throws Exception {
+        final HttpUriRequest get = new HttpGet(baseUrl);
+        HttpResponse response = null;
+        try {
+            response = client.execute(get);
+            assertEquals("Expecting page not found at " + get.getURI(), 404, response.getStatusLine().getStatusCode());
+        } finally {
+            closeConnection(response);
+        }
+    }
+    
+    @Test
+    @Retry(timeoutMsec=CrankstartBootstrapTest.LONG_TIMEOUT, intervalMsec=CrankstartBootstrapTest.STD_INTERVAL)
+    public void testSingleConfigServlet() throws Exception {
+        final HttpUriRequest get = new HttpGet(baseUrl + "/single");
+        HttpResponse response = null;
+        try {
+            response = client.execute(get);
+            assertEquals("Expecting success for " + get.getURI(), 200, response.getStatusLine().getStatusCode());
+        } finally {
+            closeConnection(response);
+        }
+    }
+    
+    @Test
+    @Retry(timeoutMsec=CrankstartBootstrapTest.LONG_TIMEOUT, intervalMsec=CrankstartBootstrapTest.STD_INTERVAL)
+    public void testConfigFactoryServlet() throws Exception {
+        final String [] paths = { "/foo", "/bar/test" };
+        for(String path : paths) {
+            final HttpUriRequest get = new HttpGet(baseUrl + path);
+            HttpResponse response = null;
+            try {
+                response = client.execute(get);
+                assertEquals("Expecting success for " + get.getURI(), 200, response.getStatusLine().getStatusCode());
+            } finally {
+                closeConnection(response);
+            }
+        }
+    }
+    
+    @Test
+    @Retry(timeoutMsec=CrankstartBootstrapTest.LONG_TIMEOUT, intervalMsec=CrankstartBootstrapTest.STD_INTERVAL)
+    public void testJUnitServlet() throws Exception {
+        final String path = "/system/sling/junit";
+        final HttpUriRequest get = new HttpGet(baseUrl + path);
+        HttpResponse response = null;
+        try {
+            response = client.execute(get);
+            assertEquals("Expecting JUnit servlet to be installed via sling extension command, at " + get.getURI(), 200, response.getStatusLine().getStatusCode());
+        } finally {
+            closeConnection(response);
+        }
+    }
+    
+    @Test
+    @Retry(timeoutMsec=CrankstartBootstrapTest.LONG_TIMEOUT, intervalMsec=CrankstartBootstrapTest.STD_INTERVAL)
+    public void testAdditionalBundles() throws Exception {
+        setAdminCredentials();
+        final String basePath = "/system/console/bundles/";
+        final String [] addBundles = {
+                "org.apache.sling.commons.mime",
+                "org.apache.sling.settings"
+        };
+        
+        for(String name : addBundles) {
+            final String path = basePath + name;
+            final HttpUriRequest get = new HttpGet(baseUrl + path);
+            HttpResponse response = null;
+            try {
+                response = client.execute(get);
+                assertEquals("Expecting additional bundle to be present at " + get.getURI(), 200, response.getStatusLine().getStatusCode());
+            } finally {
+                closeConnection(response);
+            }
+        }
+    }
+    
+    @Test
+    @Retry(timeoutMsec=CrankstartBootstrapTest.LONG_TIMEOUT, intervalMsec=CrankstartBootstrapTest.STD_INTERVAL)
+    public void testSpecificStartLevel() throws Exception {
+        // Verify that this bundle is only installed, as it's set to start level 99
+        setAdminCredentials();
+        final String path = "/system/console/bundles/org.apache.commons.collections.json";
+        final HttpUriRequest get = new HttpGet(baseUrl + path);
+        HttpResponse response = null;
+        try {
+            response = client.execute(get);
+            assertEquals("Expecting bundle status to be available at " + get.getURI(), 200, response.getStatusLine().getStatusCode());
+            assertNotNull("Expecting response entity", response.getEntity());
+            String encoding = "UTF-8";
+            if(response.getEntity().getContentEncoding() != null) {
+                encoding = response.getEntity().getContentEncoding().getValue();
+            }
+            final String content = IOUtils.toString(response.getEntity().getContent(), encoding);
+            
+            // Start level is in the props array, with key="Start Level"
+            final JSONObject status = new JSONObject(content);
+            final JSONArray props = status.getJSONArray("data").getJSONObject(0).getJSONArray("props");
+            final String KEY = "key";
+            final String SL = "Start Level";
+            boolean found = false;
+            for(int i=0; i < props.length(); i++) {
+                final JSONObject o = props.getJSONObject(i);
+                if(o.has(KEY) && SL.equals(o.getString(KEY))) {
+                    found = true;
+                    assertEquals("Expecting the start level that we set", "99", o.getString("value"));
+                }
+            }
+            assertTrue("Expecting start level to be found in JSON output", found);
+        } finally {
+            closeConnection(response);
+        }
+    }
+    
+    @Test
+    @Retry(timeoutMsec=CrankstartBootstrapTest.LONG_TIMEOUT, intervalMsec=CrankstartBootstrapTest.STD_INTERVAL)
+    public void testEmptyConfig() throws Exception {
+        setAdminCredentials();
+        assertHttpGet(
+            "/test/config/empty.config.should.work", 
+            "empty.config.should.work#service.pid=(String)empty.config.should.work##EOC#");
+    }
+        
+    @Test
+    @Retry(timeoutMsec=CrankstartBootstrapTest.LONG_TIMEOUT, intervalMsec=CrankstartBootstrapTest.STD_INTERVAL)
+    public void testFelixFormatConfig() throws Exception {
+        setAdminCredentials();
+        assertHttpGet(
+                "/test/config/felix.format.test", 
+                "felix.format.test#array=(String[])[foo, bar.from.launcher.test]#mongouri=(String)mongodb://localhost:27017#service.pid=(String)felix.format.test#service.ranking.launcher.test=(Integer)54321##EOC#");
+    }
+    
+    private void assertHttpGet(String path, String expectedContent) throws Exception {
+        final HttpUriRequest get = new HttpGet(baseUrl + path);
+        HttpResponse response = null;
+        try {
+            response = client.execute(get);
+            assertEquals("Expecting 200 response at " + path, 200, response.getStatusLine().getStatusCode());
+            assertNotNull("Expecting response entity", response.getEntity());
+            String encoding = "UTF-8";
+            if(response.getEntity().getContentEncoding() != null) {
+                encoding = response.getEntity().getContentEncoding().getValue();
+            }
+            final String content = IOUtils.toString(response.getEntity().getContent(), encoding);
+            assertEquals(expectedContent, content);
+        } finally {
+            closeConnection(response);
+        }
+    }
+    
+    private static String getOsgiStoragePath() {
+        final File tmpRoot = new File(System.getProperty("java.io.tmpdir"));
+        final Random random = new Random();
+        final File tmpFolder = new File(tmpRoot, System.currentTimeMillis() + "_" + random.nextInt());
+        if(!tmpFolder.mkdir()) {
+            fail("Failed to create " + tmpFolder.getAbsolutePath());
+        }
+        tmpFolder.deleteOnExit();
+        return tmpFolder.getAbsolutePath();
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/crankstart/launcher/PreemptiveAuthInterceptor.java b/src/test/java/org/apache/sling/crankstart/launcher/PreemptiveAuthInterceptor.java
new file mode 100644
index 0000000..5e82a54
--- /dev/null
+++ b/src/test/java/org/apache/sling/crankstart/launcher/PreemptiveAuthInterceptor.java
@@ -0,0 +1,41 @@
+package org.apache.sling.crankstart.launcher;
+
+import java.io.IOException;
+
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.AuthState;
+import org.apache.http.auth.Credentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.protocol.ClientContext;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.protocol.ExecutionContext;
+import org.apache.http.protocol.HttpContext;
+
+/** It's not like httpclient 4.1 makes this simple... */
+class PreemptiveAuthInterceptor implements HttpRequestInterceptor {
+
+    public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
+
+        AuthState authState = (AuthState) context.getAttribute(ClientContext.TARGET_AUTH_STATE);
+        CredentialsProvider credsProvider = (CredentialsProvider) context.getAttribute(ClientContext.CREDS_PROVIDER);
+        HttpHost targetHost = (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
+
+        // If not auth scheme has been initialized yet
+        if (authState.getAuthScheme() == null) {
+            AuthScope authScope = new AuthScope(targetHost.getHostName(), targetHost.getPort());
+
+            // Obtain credentials matching the target host
+            Credentials creds = credsProvider.getCredentials(authScope);
+
+            // If found, generate BasicScheme preemptively
+            if (creds != null) {
+                authState.setAuthScheme(new BasicScheme());
+                authState.setCredentials(creds);
+            }
+        }
+    }
+}
diff --git a/src/test/resources/crankstart-model.txt b/src/test/resources/crankstart-model.txt
new file mode 100644
index 0000000..b35f96a
--- /dev/null
+++ b/src/test/resources/crankstart-model.txt
@@ -0,0 +1,37 @@
+#
+#  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.
+#
+
+# Test the crankstart launcher by setting up an HTTP
+# server with a few servlets that require specific OSGi configurations
+[feature name=:crankstart]
+
+[variables]
+  felix.framework.version=4.4.0
+  slf4j.version = 1.6.2
+  http.port = 8080
+  osgi.storage.path = CRANKSTART
+  
+[settings]
+  org.osgi.service.http.port = ${http.port}
+  org.osgi.framework.storage = ${osgi.storage.path}
+  
+[artifacts]
+  org.apache.felix/org.apache.felix.framework/${felix.framework.version}
+  org.slf4j/slf4j-api/${slf4j.version}
+  org.slf4j/slf4j-simple/${slf4j.version}
\ No newline at end of file
diff --git a/src/test/resources/provisioning-model/base.txt b/src/test/resources/provisioning-model/base.txt
new file mode 100644
index 0000000..16a4b23
--- /dev/null
+++ b/src/test/resources/provisioning-model/base.txt
@@ -0,0 +1,39 @@
+#
+#  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.
+#
+# This is a feature description
+#
+# A feature consists of variables and run mode dependent artifacts.
+#
+
+# Test our Sling extension commands, that add a bundle via the Sling installer
+# (which requires commons.json and jcr-wrapper)
+[feature name=crankstart.test.base]
+
+[variables]
+  felix.http.jetty.version=2.2.0
+
+[artifacts]
+  org.apache.felix/org.apache.felix.http.jetty/${felix.http.jetty.version}
+  org.apache.felix/org.apache.felix.eventadmin/1.3.2
+  org.apache.felix/org.apache.felix.scr/1.8.2
+  org.apache.felix/org.apache.felix.metatype/1.0.10
+  org.apache.sling/org.apache.sling.commons.osgi/2.2.0
+  org.apache.sling/org.apache.sling.commons.log/2.1.2
+  org.apache.felix/org.apache.felix.configadmin/1.6.0
+  org.apache.felix/org.apache.felix.webconsole/3.1.6
\ No newline at end of file
diff --git a/src/test/resources/provisioning-model/crankstart-tests.txt b/src/test/resources/provisioning-model/crankstart-tests.txt
new file mode 100644
index 0000000..c3b78ff
--- /dev/null
+++ b/src/test/resources/provisioning-model/crankstart-tests.txt
@@ -0,0 +1,53 @@
+#
+#  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.
+#
+# This is a feature description
+#
+# A feature consists of variables and run mode dependent artifacts.
+#
+
+# Test our Sling extension commands, that add a bundle via the Sling installer
+# (which requires commons.json and jcr-wrapper)
+[feature name=crankstart.tests]
+
+[artifacts]
+  org.apache.sling/org.apache.sling.crankstart3.test.services/1.0.3-SNAPSHOT
+  org.apache.sling/org.apache.sling.junit.core/1.0.10
+  org.apache.sling/org.apache.sling.commons.mime/2.1.8
+  org.apache.sling/org.apache.sling.settings/1.3.6
+
+[configurations]
+  org.apache.sling.crankstart.testservices.SingleConfigServlet
+  # TODO should use a variable to verify that they work in configs
+    path="/single"
+    message="doesn't matter"
+
+  org.apache.sling.crankstart.testservices.ConfigFactoryServlet-foo
+    path="/foo"
+    message="Not used"
+  
+  org.apache.sling.crankstart.testservices.ConfigFactoryServlet-bar.test
+    path="/bar/test"
+    message="Not used"
+  
+  felix.format.test
+    mongouri="mongodb://localhost:27017"
+    service.ranking.launcher.test=I"54321"
+    array=["foo","bar.from.launcher.test"]
+  
+  empty.config.should.work
\ No newline at end of file
diff --git a/src/test/resources/provisioning-model/sling-extensions.txt b/src/test/resources/provisioning-model/sling-extensions.txt
new file mode 100644
index 0000000..5cae5a6
--- /dev/null
+++ b/src/test/resources/provisioning-model/sling-extensions.txt
@@ -0,0 +1,33 @@
+#
+#  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.
+#
+# This is a feature description
+#
+# A feature consists of variables and run mode dependent artifacts.
+#
+
+# Test our Sling extension commands, that add a bundle via the Sling installer
+# (which requires commons.json and jcr-wrapper)
+[feature name=sling.extensions]
+
+[artifacts]
+  org.apache.sling/org.apache.sling.installer.core/3.5.0
+  org.apache.sling/org.apache.sling.commons.json/2.0.6
+  org.apache.sling/org.apache.sling.jcr.jcr-wrapper/2.0.0
+  # TODO org.apache.sling/org.apache.sling.crankstart.sling.extensions/1.0.1-SNAPSHOT
+  commons-io/commons-io/2.4
diff --git a/src/test/resources/provisioning-model/start-level-99.txt b/src/test/resources/provisioning-model/start-level-99.txt
new file mode 100644
index 0000000..922a2de
--- /dev/null
+++ b/src/test/resources/provisioning-model/start-level-99.txt
@@ -0,0 +1,24 @@
+#
+#  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.
+#
+# Add a bundle at a start level higher that our framework's to verify
+# that it is installed but not active
+[feature name=startlevel99]
+
+[artifacts startLevel=99]
+  commons-collections/commons-collections/3.2.1