/** * Copyright (C) 2011 Brian Ferris (bdferris@google.com) * * Licensed 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.onebusaway.quickstart.bootstrap; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import org.onebusaway.quickstart.BootstrapCommon; import org.onebusaway.quickstart.GuiQuickstartDataModel; import org.onebusaway.quickstart.WebappCommon; /** * This is the bootstrap entry point for the onebusaway-quickstart WAR that * provides an executable war that can be used to quickly build a transit data * bundle and run a simple webapp on top of that bundle. * * We have functionality to bootstrap a Java classpath from JARs contained * within and then execute targets within those JARs. * * @author bdferris * */ public class BootstrapMain { public static void main(String[] args) throws Exception { if (args.length > 0 && isHelp(args[0])) { usage(); System.exit(-1); } /** * This bit of code locates the URL of the WAR from where we're being * executed */ ProtectionDomain protectionDomain = BootstrapMain.class.getProtectionDomain(); URL warUrl = protectionDomain.getCodeSource().getLocation(); /** * We create a temporary directory to extract JARs out of our parent WAR. * The URLClassLoader unfortunately can't point to them directly within the * WAR. */ File tmpDir = getTempDirectory(); if (args.length == 0) { GuiQuickstartDataModel model = performGuiConfiguration(warUrl, tmpDir); if (model.isBuildOnly()) { String gtfsPath = model.getGtfsPath(); String bundlePath = model.getTransitDataBundlePath(); String[] subArgs = {gtfsPath, bundlePath}; performBuild(warUrl, tmpDir, subArgs); } else { List<String> runArgs = new ArrayList<String>(); if (model.isBuildEnabled()) { runArgs.add("-" + WebappCommon.ARG_BUILD); runArgs.add("-" + WebappCommon.ARG_GTFS_PATH + "=" + model.getGtfsPath()); } if (model.getTripUpdatesUrl() != null) { runArgs.add("-gtfsRealtimeTripUpdatesUrl=" + model.getTripUpdatesUrl()); } if (model.getVehiclePositionsUrl() != null) { runArgs.add("-gtfsRealtimeVehiclePositionsUrl=" + model.getVehiclePositionsUrl()); } if (model.getAlertsUrl() != null) { runArgs.add("-gtfsRealtimeAlertsUrl=" + model.getAlertsUrl()); } runArgs.add(model.getTransitDataBundlePath()); performRun(warUrl, tmpDir, false, runArgs.toArray(new String[runArgs.size()])); } } else { String firstArg = args[0]; String[] subArgs = new String[args.length - 1]; System.arraycopy(args, 1, subArgs, 0, subArgs.length); if ("-build".equals(firstArg)) { performBuild(warUrl, tmpDir, subArgs); } else if ("-webapp".equals(firstArg)) { performRun(warUrl, tmpDir, true, subArgs); } else { System.err.println("unexpected first arg: " + firstArg); usage(); System.exit(-1); } } } private static GuiQuickstartDataModel performGuiConfiguration(URL warUrl, File tmpDir) throws ClassNotFoundException, NoSuchMethodException, InterruptedException { /** * Bootstrap the classpath by extracting the JARs in our WAR into the * temporary directory and then creating a special classloader on top of * those JARs. */ URLClassLoader classloader = bootstrapClasspath(warUrl, tmpDir, false); Class<?> c = classloader.loadClass("org.onebusaway.quickstart.bootstrap.GuiBootstrapMain"); Method method = c.getMethod("configureBootstrapArgs"); GuiQuickstartDataModel bootstrapArgs = (GuiQuickstartDataModel) invokeWithProperClassloader( classloader, method); return bootstrapArgs; } private static void performBuild(URL warUrl, File tmpDir, String[] subArgs) throws ClassNotFoundException, NoSuchMethodException, InterruptedException { /** * Bootstrap the classpath by extracting the JARs in our WAR into the * temporary directory and then creating a special classloader on top of * those JARs. Note that we include the WEB-INF/lib JARs since they have the * onebusaway-transit-data-federation-builder.jar and dependency JARS that * we care about. */ URLClassLoader classloader = bootstrapClasspath(warUrl, tmpDir, true); Class<?> c = classloader.loadClass("org.onebusaway.quickstart.bootstrap.BuildBootstrapMain"); Method method = c.getMethod("main", String[].class); invokeWithProperClassloader(classloader, method, (Object) subArgs); } private static void performRun(URL warUrl, File tmpDir, boolean consoleMode, String[] subArgs) throws ClassNotFoundException, NoSuchMethodException, InterruptedException { String tempPath = System.getProperty("java.io.tmpdir"); /** * A fix to handle the crazy path the Mac JVM returns that cause problems * for embedded Jetty */ if (tempPath.startsWith("/var/folders/")) System.setProperty("java.io.tmpdir", "/tmp"); /** * Bootstrap the classpath by extracting the JARs in our WAR into the * temporary directory and then creating a special classloader on top of * those JARs. Note that we exclude the WEB-INF/lib JARs, since those will * be loaded by the webapp container. */ URLClassLoader classloader = bootstrapClasspath(warUrl, tmpDir, false); Class<?> c = classloader.loadClass("org.onebusaway.quickstart.bootstrap.WebappBootstrapMain"); Method method = c.getMethod("run", URL.class, Boolean.TYPE, String[].class); invokeWithProperClassloader(classloader, method, warUrl, consoleMode, subArgs); } private static boolean isHelp(String option) { option = option.replaceAll("-", ""); option = option.toLowerCase(); return option.equals("help") || option.equals("h") || option.equals("?"); } private static Object invokeWithProperClassloader(URLClassLoader classloader, final Method method, final Object... args) throws InterruptedException { final AtomicReference<Object> reference = new AtomicReference<Object>(); Runnable r = new Runnable() { @Override public void run() { try { Object result = method.invoke(null, args); reference.set(result); } catch (Exception ex) { ex.printStackTrace(); System.exit(-1); } } }; Thread thread = new Thread(r); thread.setContextClassLoader(classloader); thread.start(); thread.join(); return reference.get(); } private static File getTempDirectory() throws IOException { File tmpDir = File.createTempFile(BootstrapMain.class.getName() + "-", "-jars"); tmpDir.delete(); tmpDir.mkdirs(); DeleteTempDirectoryOnExitRunnable r = new DeleteTempDirectoryOnExitRunnable( tmpDir); Runtime.getRuntime().addShutdownHook(new Thread(r)); return tmpDir; } private static void usage() { BootstrapCommon.printUsage(BootstrapMain.class, "usage.txt"); } private static URLClassLoader bootstrapClasspath(URL warUrl, File tmpDir, boolean includeWarLibs) { System.err.println("Extracting the bootstrap classpath. This may take a few moments..."); try { List<URL> urls = new ArrayList<URL>(); exploreJar(warUrl, urls, tmpDir, includeWarLibs); URL[] array = urls.toArray(new URL[urls.size()]); return URLClassLoader.newInstance(array); } catch (Exception ex) { throw new IllegalStateException(ex); } } private static void exploreJar(URL location, List<URL> urls, File tmpDir, boolean includeWarLibs) throws IOException, MalformedURLException, URISyntaxException { JarInputStream jar = new JarInputStream(location.openStream()); JarEntry entry = null; while ((entry = jar.getNextJarEntry()) != null) { String name = entry.getName(); /** * The bulk of the JARs we care about are already in the WEB-INF/lib * directory of the parent WAR. However, we've put a few additional * dependencies in META-INF/bootstrap-lib to help with running the * embedded webapp. */ if ((name.startsWith("META-INF/bootstrap-lib") || (includeWarLibs && name.startsWith("WEB-INF/lib"))) && name.endsWith(".jar")) { name = name.replace("META-INF/", ""); File outputJar = new File(tmpDir, name); outputJar.deleteOnExit(); outputJar.getParentFile().mkdirs(); OutputStream out = new BufferedOutputStream(new FileOutputStream( outputJar)); byte[] buffer = new byte[1024]; while (true) { int rc = jar.read(buffer); if (rc == -1) break; out.write(buffer, 0, rc); } out.close(); urls.add(outputJar.toURI().toURL()); } } jar.close(); } /** * This runner should clean up all the extracted wars from the filesystem when * we finish up. * * @author bdferris * */ private static class DeleteTempDirectoryOnExitRunnable implements Runnable { private File _path; public DeleteTempDirectoryOnExitRunnable(File path) { _path = path; } @Override public void run() { deleteFiles(_path); } private void deleteFiles(File file) { if (file.isDirectory()) { for (File child : file.listFiles()) deleteFiles(child); } file.delete(); } }; }