package org.apache.sling.crankstart.junit; import static org.junit.Assert.assertNotNull; 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.*; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.sling.crankstart.launcher.Launcher; import org.apache.sling.crankstart.launcher.LauncherListener; import org.apache.sling.crankstart.launcher.PropertiesVariableResolver; import org.apache.sling.provisioning.model.ModelUtility.VariableResolver; import org.junit.rules.ExternalResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** JUnit Rule that starts a Crankstart instance, using a set of provisioning * models. See our integration tests for examples. */ public class CrankstartSetup extends ExternalResource { private static final Logger log = LoggerFactory.getLogger(CrankstartSetup.class); private final int port = getAvailablePort(); private final String storagePath = getOsgiStoragePath(); private Thread crankstartThread; private final String baseUrl = "http://localhost:" + port; private final Properties replacementProps = new Properties(); private static List<CrankstartSetup> toCleanup = new ArrayList<CrankstartSetup>(); private static Thread shutdownHook; private VariableResolver variablesResolver = new PropertiesVariableResolver(replacementProps, Launcher.VARIABLE_OVERRIDE_PREFIX); private String [] classpathModelPaths; private boolean shutdown; private int bundlesStarted; private int bundlesFailed; private int totalBundles; @Override public String toString() { return getClass().getSimpleName() + ", port " + port + ", OSGi storage " + storagePath; } public CrankstartSetup() { synchronized (getClass()) { if(shutdownHook == null) { shutdownHook = new Thread(CrankstartSetup.class.getSimpleName() + " shutdown thread") { @Override public void run() { log.info("Starting cleanup"); cleanup(); log.info("Cleanup done"); } }; Runtime.getRuntime().addShutdownHook(shutdownHook); } } } public CrankstartSetup withModelResources(String ... classpathModelPaths) { this.classpathModelPaths = classpathModelPaths; return this; } 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; } private static void mergeModelResource(Launcher launcher, String path) throws Exception { final InputStream is = CrankstartSetup.class.getResourceAsStream(path); assertNotNull("Expecting test resource to be found:" + path, is); final Reader input = new InputStreamReader(is); try { Launcher.mergeModel(launcher.getModel(), input, path); launcher.computeEffectiveModel(); } finally { input.close(); } } public String getBaseUrl() { return baseUrl; } public int getBundlesFailed() { return bundlesFailed; } public int getBundlesStarted() { return bundlesStarted; } public int getTotalBundles() { return totalBundles; } public boolean isShutdownComplete() { return shutdown; } private static void cleanup() { synchronized (toCleanup) { if(toCleanup.isEmpty()) { log.info("No Crankstart instances to cleanup"); return; } log.info("Stopping {} running Crankstart instances...", toCleanup.size()); for(CrankstartSetup s : toCleanup) { s.stopCrankstartInstance(); } toCleanup.clear(); } } @Override protected void before() throws Throwable { if(crankstartThread != null) { log.debug("Already running"); return; } cleanup(); log.info("Starting {}", this); // Add system properties which have the expected prefix for(Object o : System.getProperties().keySet()) { final String key = o.toString(); if(key.startsWith(Launcher.VARIABLE_OVERRIDE_PREFIX)) { replacementProps.setProperty(key, System.getProperty(key)); } } final HttpUriRequest get = new HttpGet(baseUrl); replacementProps.setProperty("crankstart.model.http.port", String.valueOf(port)); replacementProps.setProperty("crankstart.model.osgi.storage.path", storagePath); try { new DefaultHttpClient().execute(get); fail("Expecting connection to " + port + " to fail before starting HTTP service"); } catch(IOException expected) { } shutdown = false; bundlesStarted = 0; bundlesFailed = 0; totalBundles = 0; final Launcher launcher = new Launcher().withVariableResolver(variablesResolver).withListener(new LauncherListener() { @Override public void onStartup(int started, int failed, int totalBundles) { CrankstartSetup.this.bundlesStarted = started; CrankstartSetup.this.bundlesFailed = failed; CrankstartSetup.this.totalBundles = totalBundles; } @Override public void onShutdown() { shutdown = true; } }); for(String path : classpathModelPaths) { mergeModelResource(launcher, path); } launcher.computeEffectiveModel(); crankstartThread = new Thread() { public void run() { try { launcher.launch(); } catch(InterruptedException e) { log.info("Launcher thread was interrupted, exiting"); } catch(Exception e) { e.printStackTrace(); fail("Launcher exception:" + e); } } }; crankstartThread.setDaemon(true); crankstartThread.start(); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { stopCrankstartInstance(); } }); } private void stopCrankstartInstance() { log.info("Stopping {}", this); if(crankstartThread == null) { return; } crankstartThread.interrupt(); try { crankstartThread.join(); } catch(InterruptedException ignore) { } crankstartThread = null; } private static String getOsgiStoragePath() { final File tmpRoot = new File(System.getProperty("java.io.tmpdir")); final File tmpFolder = new File(tmpRoot, CrankstartSetup.class.getSimpleName() + "_" + UUID.randomUUID()); if(!tmpFolder.mkdir()) { fail("Failed to create " + tmpFolder.getAbsolutePath()); } tmpFolder.deleteOnExit(); return tmpFolder.getAbsolutePath(); } }