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