package com.tyndalehouse.step.server; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.imageio.ImageIO; import javax.servlet.ServletException; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.Calendar; import java.util.Locale; import java.util.ResourceBundle; /** * Tomcat server for STEP */ public class STEPTomcatServer { public static final String SHUTDOWN_CONTEXT = "shutdown"; public static final String ENGLISH_GENERIC_ERROR = "An error has occurred"; public static final String ENGLISH_BROWSER_ERROR = "STEP was unable to launch the browser."; public static final String BACKGROUND_LAUNCH = "backgroundLaunch"; public static final String STEP_DESCRIPTION = "STEP :: Scripture Tools for Every Person\n\u00a9 Tyndale House " + Calendar.getInstance().get(Calendar.YEAR); public static final String STEP_TITLE = "STEP :: Scripture Tools for Every Person"; public static final int MAX_WAIT_TO_TEST_PORT_IN_USE_MS = 150; private static final Logger LOGGER = LoggerFactory.getLogger(STEPTomcatServer.class); private static final int DEFAULT_STEP_PORT = 8989; private static final String DEFAULT_WAR_LOCATION = "step-web"; private static final String DEFAULT_WAR_CONTEXT = ""; private final InetSocketAddress socket; private final String warPath; private final int stepPort; private final String contextPath; private final String browserUrl; private InetAddress listeningAddress; private ResourceBundle setupMessages = null; private ResourceBundle errorMessages = null; private ResourceBundle htmlMessages = null; private boolean ignoreBrowserError = Boolean.getBoolean("ignoreBrowserError"); private boolean backgroundLaunch; public STEPTomcatServer(boolean backgroundLaunch) throws MalformedURLException { this.backgroundLaunch = backgroundLaunch; try { listeningAddress = InetAddress.getByName("localhost"); } catch (UnknownHostException ex) { try { listeningAddress = InetAddress.getByAddress("localhost", new byte[]{0x7f, 0x00, 0x00, 0x01}); } catch (UnknownHostException ex1) { try { listeningAddress = InetAddress.getLocalHost(); } catch (UnknownHostException ex3) { //can't do much here LOGGER.error("Unable to obtain IP address to bind on."); throw new RuntimeException(ex3.getMessage()); } } } this.stepPort = getStepPort(); this.socket = new InetSocketAddress(listeningAddress, this.stepPort); this.warPath = getWarPath(); this.contextPath = getContextPath(); this.browserUrl = String.format("http://%s:%s/%s", this.listeningAddress.getHostName(), this.stepPort, this.contextPath); } private static void failedToLaunchWarning() { JOptionPane.showMessageDialog(null, "STEP was unable to launch. Please try again, or contact the STEP team for help", ENGLISH_GENERIC_ERROR, JOptionPane.ERROR_MESSAGE); } /** * Main method that kicks off embedded tomcat * * @param args the arguments * @throws ServletException the servlet exception * @throws LifecycleException any lifecycle exception */ public static void main(String[] args) throws ServletException, LifecycleException { try { System.setProperty("step.jetty", "true"); final boolean backgroundLaunch = args.length > 0 && BACKGROUND_LAUNCH.equals(args[0]); if (backgroundLaunch) { if (SplashScreen.getSplashScreen() != null) { SplashScreen.getSplashScreen().close(); } } new STEPTomcatServer(backgroundLaunch).start(); ////// //note: if successful, never gets past the line above. ///// } catch (final Exception e) { failedToLaunchWarning(); LOGGER.error(e.getMessage(), e); } } private void finishStartUp() { if (!this.backgroundLaunch) { launchBrowser(); closeSpashScreen(); } } /** * Closes the splash screen */ private void closeSpashScreen() { SplashScreen screen = SplashScreen.getSplashScreen(); if (screen != null) { screen.close(); } } /** * Reads the bundles of translations from the various bundles in step-core. We pass in a class loader, so that we * can read the bundle off there! * * @param classLoader */ private void initLanguages(final ClassLoader classLoader) { setupMessages = ResourceBundle.getBundle("SetupBundle", Locale.getDefault(), classLoader); htmlMessages = ResourceBundle.getBundle("HtmlBundle", Locale.getDefault(), classLoader); errorMessages = ResourceBundle.getBundle("ErrorBundle", Locale.getDefault(), classLoader); } /** * Launch browser. */ private void launchBrowser() { try { Desktop.getDesktop().browse(new URI(browserUrl)); } catch (final IOException e1) { if (!ignoreBrowserError) { showError("error_generic", ENGLISH_GENERIC_ERROR, "error_unable_to_show_browser", ENGLISH_BROWSER_ERROR); } LOGGER.error("Unable to launch browser.", e1); } catch (final URISyntaxException e1) { if (!ignoreBrowserError) { showError("error_generic", ENGLISH_GENERIC_ERROR, "error_unable_to_show_browser", ENGLISH_BROWSER_ERROR); } LOGGER.error("Unable to launch browser.", e1); } } /** * Reads the error bundle, if loaded, or defaults to the default parameters and shows a dialog message * * @param title the title bar message * @param defaultTitle the title if no error bundle has been loaded * @param bundleKey the key to the error bundle * @param defaultEnglishMessage the message to display if no error bundle has been loaded */ private void showError(String title, String defaultTitle, String bundleKey, String defaultEnglishMessage, String... args) { String finalTitle = this.errorMessages == null ? defaultTitle : this.errorMessages.getString(title); String message = this.errorMessages == null ? defaultEnglishMessage : setupMessages.getString(bundleKey); String formattedMessage = String.format(message, args); JOptionPane.showMessageDialog(null, formattedMessage, finalTitle, JOptionPane.ERROR_MESSAGE); } /** * Adds the system tray. * * @param server the server */ private void addSystemTray(final Tomcat server) { setDefaultUILookAndFeel(); if (!SystemTray.isSupported()) { return; } Image icon; try { icon = ImageIO.read(getClass().getResource("/step.png")).getScaledInstance(16, 16, Image.SCALE_DEFAULT); } catch (final IOException e1) { LOGGER.error("Failed to load image", e1); return; } final TrayIcon trayIcon = new TrayIcon(icon); final MenuItem aboutItem = new MenuItem(htmlMessages.getString("help_about")); final MenuItem launchStepBrowser = new MenuItem(htmlMessages.getString("launch_browser")); final MenuItem exitItem = new MenuItem(htmlMessages.getString("tools_exit")); final PopupMenu popupMenu = new PopupMenu(); aboutItem.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { final int showOptionDialog = JOptionPane.showOptionDialog(null, STEP_DESCRIPTION, STEP_TITLE, JOptionPane.OK_CANCEL_OPTION, JOptionPane.INFORMATION_MESSAGE, null, new Object[]{htmlMessages.getString("launch_browser"), htmlMessages.getString("close")}, htmlMessages.getString("launch_browser")); if (showOptionDialog == 0) { launchBrowser(); } } }); exitItem.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { try { server.stop(); } catch (final Exception ex) { LOGGER.error("Error while shutting down", ex); } System.exit(0); } }); launchStepBrowser.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { launchBrowser(); } }); popupMenu.add(launchStepBrowser); popupMenu.add(aboutItem); popupMenu.add(exitItem); trayIcon.setToolTip("STEP :: Scripture Tools for Every Person"); trayIcon.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { launchBrowser(); } }); trayIcon.setPopupMenu(popupMenu); try { SystemTray.getSystemTray().add(trayIcon); if (!backgroundLaunch) { trayIcon.displayMessage( htmlMessages.getString("step_running"), String.format(htmlMessages.getString("open_browser"), browserUrl), TrayIcon.MessageType.INFO); } } catch (final AWTException e) { LOGGER.error("Unable to add system tray icon", e); } } /** * Sets the default ui look and feel. */ private void setDefaultUILookAndFeel() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (final ClassNotFoundException e2) { LOGGER.error("Failed to change look and feel", e2); } catch (final InstantiationException e2) { LOGGER.error("Failed to change look and feel", e2); } catch (final IllegalAccessException e2) { LOGGER.error("Failed to change look and feel", e2); } catch (final UnsupportedLookAndFeelException e2) { LOGGER.error("Failed to change look and feel", e2); } } /** * @return the port on which the server listens */ private int getStepPort() { Integer port = Integer.getInteger("step.war.port"); if (port == null) { return DEFAULT_STEP_PORT; } return port; } /** * @return the context on which STEP will live. either step.war.context, or the default location which matches the * default location on file. */ private String getContextPath() { final String pathToWar = System.getProperty("step.war.context"); if (pathToWar == null || pathToWar.length() == 0) { return DEFAULT_WAR_CONTEXT; } return pathToWar; } private String getWarPath() { String webappDirLocation = System.getProperty("step.war.path"); if (webappDirLocation == null) { webappDirLocation = DEFAULT_WAR_LOCATION; } return webappDirLocation; } /** * creates and configures the Jetty server * * @return the Server object if required to make modifications */ private Tomcat start() throws Exception { final Tomcat tomcat = new Tomcat(); tomcat.setPort(this.stepPort); try (Socket c = new Socket()) { c.connect(socket, MAX_WAIT_TO_TEST_PORT_IN_USE_MS); c.close(); //connected succesfully, so no need to start tomcat again finishStartUp(); } catch (IOException e) { //timed-out, so need to deploy app try { final String absolutePath = new File(this.warPath).getAbsolutePath(); tomcat.addWebapp("", absolutePath); LOGGER.debug("Starting tomcat with path [{}] on port [{}]", absolutePath, this.stepPort); tomcat.start(); final org.apache.catalina.Container child = tomcat.getHost().findChild(""); initLanguages(((Context)child).getLoader().getClassLoader()); addSystemTray(tomcat); finishStartUp(); // add shutdown hook to stop server Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { stopContainer(tomcat); } }); tomcat.getServer().await(); return tomcat; } catch (LifecycleException | ServletException ex) { throw e; } } return null; } /** * Stops the embedded Tomcat server. */ public void stopContainer(final Tomcat tomcat) { try { if (tomcat != null) { tomcat.stop(); } } catch (LifecycleException exception) { LOGGER.warn("Cannot Stop Tomcat {}", exception.getMessage(), exception); } } }