From f4022dd243f18c2c5fa81946685e362804d89a7f Mon Sep 17 00:00:00 2001 From: catbref Date: Sat, 25 May 2019 08:37:52 +0100 Subject: [PATCH] initial work on adding bundled node-management UI --- .../java/org/qora/controller/Controller.java | 19 ++++- .../java/org/qora/gui/{GUI.java => Gui.java} | 24 ++++-- src/main/java/org/qora/gui/SplashFrame.java | 10 +-- src/main/java/org/qora/gui/SysTray.java | 49 ++++------- src/main/java/org/qora/settings/Settings.java | 19 +++++ src/main/java/org/qora/ui/UiService.java | 85 +++++++++++++++++++ src/main/java/org/qora/utils/URLViewer.java | 35 ++++++++ src/main/resources/bundled-ui | 1 + 8 files changed, 196 insertions(+), 46 deletions(-) rename src/main/java/org/qora/gui/{GUI.java => Gui.java} (68%) create mode 100644 src/main/java/org/qora/ui/UiService.java create mode 100644 src/main/java/org/qora/utils/URLViewer.java create mode 120000 src/main/resources/bundled-ui diff --git a/src/main/java/org/qora/controller/Controller.java b/src/main/java/org/qora/controller/Controller.java index c3c1ef76..3abbfdf2 100644 --- a/src/main/java/org/qora/controller/Controller.java +++ b/src/main/java/org/qora/controller/Controller.java @@ -27,7 +27,7 @@ import org.qora.data.block.BlockData; import org.qora.data.network.BlockSummaryData; import org.qora.data.network.PeerData; import org.qora.data.transaction.TransactionData; -import org.qora.gui.GUI; +import org.qora.gui.Gui; import org.qora.network.Network; import org.qora.network.Peer; import org.qora.network.message.BlockMessage; @@ -50,6 +50,7 @@ import org.qora.repository.hsqldb.HSQLDBRepositoryFactory; import org.qora.settings.Settings; import org.qora.transaction.Transaction; import org.qora.transaction.Transaction.ValidationResult; +import org.qora.ui.UiService; import org.qora.utils.Base58; import org.qora.utils.NTP; @@ -159,7 +160,7 @@ public class Controller extends Thread { LOGGER.info("Starting up..."); // Potential GUI startup with splash screen, etc. - GUI.getInstance(); + Gui.getInstance(); Security.insertProviderAt(new BouncyCastleProvider(), 0); Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); @@ -223,8 +224,17 @@ public class Controller extends Thread { LOGGER.info("Starting auto-update"); AutoUpdate.getInstance().start(); + LOGGER.info("Starting bundled UI on port " + Settings.getInstance().getUiPort()); + try { + UiService uiService = UiService.getInstance(); + uiService.start(); + } catch (Exception e) { + LOGGER.error("Unable to start bundled UI", e); + System.exit(1); + } + // If GUI is enabled, we're no longer starting up but actually running now - GUI.getInstance().notifyRunning(); + Gui.getInstance().notifyRunning(); } // Main thread @@ -319,6 +329,9 @@ public class Controller extends Thread { if (!isStopping) { isStopping = true; + LOGGER.info("Shutting down bundled UI"); + UiService.getInstance().stop(); + LOGGER.info("Shutting down auto-update"); AutoUpdate.getInstance().shutdown(); diff --git a/src/main/java/org/qora/gui/GUI.java b/src/main/java/org/qora/gui/Gui.java similarity index 68% rename from src/main/java/org/qora/gui/GUI.java rename to src/main/java/org/qora/gui/Gui.java index baedd941..89e38c1d 100644 --- a/src/main/java/org/qora/gui/GUI.java +++ b/src/main/java/org/qora/gui/Gui.java @@ -6,24 +6,34 @@ import java.io.IOException; import java.io.InputStream; import javax.imageio.ImageIO; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -public class GUI { +public class Gui { - private static final Logger LOGGER = LogManager.getLogger(GUI.class); - private static GUI instance; + private static final Logger LOGGER = LogManager.getLogger(Gui.class); + private static Gui instance; private boolean isHeadless; private SplashFrame splash = null; private SysTray sysTray = null; - private GUI() { + private Gui() { this.isHeadless = GraphicsEnvironment.isHeadless(); - if (!this.isHeadless) + if (!this.isHeadless) { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException + | UnsupportedLookAndFeelException e) { + // Use whatever look-and-feel comes by default then + } + showSplash(); + } } private void showSplash() { @@ -40,9 +50,9 @@ public class GUI { } } - public static GUI getInstance() { + public static Gui getInstance() { if (instance == null) - instance = new GUI(); + instance = new Gui(); return instance; } diff --git a/src/main/java/org/qora/gui/SplashFrame.java b/src/main/java/org/qora/gui/SplashFrame.java index ac6f5769..66065ce1 100644 --- a/src/main/java/org/qora/gui/SplashFrame.java +++ b/src/main/java/org/qora/gui/SplashFrame.java @@ -26,7 +26,7 @@ public class SplashFrame { private BufferedImage image; public SplashPanel() { - image = GUI.loadImage("splash.png"); + image = Gui.loadImage("splash.png"); this.setPreferredSize(new Dimension(image.getWidth(), image.getHeight())); this.setLayout(new BorderLayout()); } @@ -42,10 +42,10 @@ public class SplashFrame { this.splashDialog = new JDialog(); List icons = new ArrayList(); - icons.add(GUI.loadImage("icons/icon16.png")); - icons.add(GUI.loadImage("icons/icon32.png")); - icons.add(GUI.loadImage("icons/icon64.png")); - icons.add(GUI.loadImage("icons/icon128.png")); + icons.add(Gui.loadImage("icons/icon16.png")); + icons.add(Gui.loadImage("icons/icon32.png")); + icons.add(Gui.loadImage("icons/icon64.png")); + icons.add(Gui.loadImage("icons/icon128.png")); this.splashDialog.setIconImages(icons); this.splashDialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); diff --git a/src/main/java/org/qora/gui/SysTray.java b/src/main/java/org/qora/gui/SysTray.java index 4e6ba453..c5b50f1c 100644 --- a/src/main/java/org/qora/gui/SysTray.java +++ b/src/main/java/org/qora/gui/SysTray.java @@ -1,26 +1,22 @@ package org.qora.gui; import java.awt.AWTException; -import java.awt.BorderLayout; import java.awt.MenuItem; import java.awt.PopupMenu; import java.awt.SystemTray; import java.awt.TrayIcon; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.io.InputStream; - -import javax.imageio.ImageIO; -import javax.swing.JPanel; +import java.net.MalformedURLException; +import java.net.URL; + import javax.swing.SwingWorker; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qora.controller.Controller; +import org.qora.settings.Settings; +import org.qora.utils.URLViewer; public class SysTray { @@ -30,34 +26,13 @@ public class SysTray { private TrayIcon trayIcon = null; private PopupMenu popupMenu = null; - @SuppressWarnings("serial") - public static class SplashPanel extends JPanel { - private BufferedImage image; - - public SplashPanel() { - try (InputStream in = ClassLoader.getSystemResourceAsStream("images/splash.png")) { - image = ImageIO.read(in); - this.setPreferredSize(new Dimension(image.getWidth(), image.getHeight())); - this.setLayout(new BorderLayout()); - } catch (IOException ex) { - LOGGER.error(ex); - } - } - - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); - g.drawImage(image, 0, 0, null); - } - } - private SysTray() { if (!SystemTray.isSupported()) return; this.popupMenu = createPopupMenu(); - this.trayIcon = new TrayIcon(GUI.loadImage("icons/icon32.png"), "qora-core", popupMenu); + this.trayIcon = new TrayIcon(Gui.loadImage("icons/icon32.png"), "qora-core", popupMenu); this.trayIcon.setImageAutoSize(true); @@ -84,6 +59,18 @@ public class SysTray { private PopupMenu createPopupMenu() { PopupMenu menu = new PopupMenu(); + MenuItem openUi = new MenuItem("Open UI"); + openUi.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + try { + URLViewer.openWebpage(new URL("http://localhost:" + Settings.getInstance().getUiPort())); + } catch (MalformedURLException e1) { + LOGGER.error(e1.getMessage(),e1); + } + } + }); + menu.add(openUi); + MenuItem exit = new MenuItem("Exit"); exit.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { diff --git a/src/main/java/org/qora/settings/Settings.java b/src/main/java/org/qora/settings/Settings.java index 65215835..fb3aa9fc 100644 --- a/src/main/java/org/qora/settings/Settings.java +++ b/src/main/java/org/qora/settings/Settings.java @@ -36,6 +36,13 @@ public class Settings { // Settings, and other config files private String userPath; + // Bundled UI related + private boolean uiEnabled = true; + private int uiPort = 9080; + private String[] uiWhitelist = new String[] { + "::1", "127.0.0.1" + }; + // API-related private boolean apiEnabled = true; private int apiPort = 9085; @@ -182,6 +189,18 @@ public class Settings { return this.userPath; } + public boolean isUiEnabled() { + return this.uiEnabled; + } + + public int getUiPort() { + return this.uiPort; + } + + public String[] getUiWhitelist() { + return this.uiWhitelist; + } + public boolean isApiEnabled() { return this.apiEnabled; } diff --git a/src/main/java/org/qora/ui/UiService.java b/src/main/java/org/qora/ui/UiService.java new file mode 100644 index 00000000..e50dec6a --- /dev/null +++ b/src/main/java/org/qora/ui/UiService.java @@ -0,0 +1,85 @@ +package org.qora.ui; + +import org.eclipse.jetty.rewrite.handler.RedirectPatternRule; +import org.eclipse.jetty.rewrite.handler.RewriteHandler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.InetAccessHandler; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.servlets.CrossOriginFilter; +import org.qora.settings.Settings; + +public class UiService { + + private final Server server; + + public UiService() { + // Create bundled UI server + this.server = new Server(Settings.getInstance().getUiPort()); + + // IP address based access control + InetAccessHandler accessHandler = new InetAccessHandler(); + for (String pattern : Settings.getInstance().getUiWhitelist()) { + accessHandler.include(pattern); + } + this.server.setHandler(accessHandler); + + // URL rewriting + RewriteHandler rewriteHandler = new RewriteHandler(); + accessHandler.setHandler(rewriteHandler); + + // Context + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); + context.setContextPath("/"); + rewriteHandler.setHandler(context); + + // Cross-origin resource sharing + FilterHolder corsFilterHolder = new FilterHolder(CrossOriginFilter.class); + corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*"); + corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET, POST, DELETE"); + corsFilterHolder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false"); + context.addFilter(corsFilterHolder, "/*", null); + + // Bundled-UI static content servlet + ServletHolder uiServlet = new ServletHolder("bundled-ui", DefaultServlet.class); + ClassLoader loader = this.getClass().getClassLoader(); + uiServlet.setInitParameter("resourceBase", loader.getResource("bundled-ui/").toString()); + uiServlet.setInitParameter("dirAllowed", "true"); + uiServlet.setInitParameter("pathInfoOnly", "true"); + context.addServlet(uiServlet, "/*"); + + rewriteHandler.addRule(new RedirectPatternRule("", "/home.html")); // redirect to bundled UI start page + } + + private static UiService instance; + + public static UiService getInstance() { + if (instance == null) { + instance = new UiService(); + } + + return instance; + } + + public void start() { + try { + // Start server + server.start(); + } catch (Exception e) { + // Failed to start + throw new RuntimeException("Failed to start bundled UI", e); + } + } + + public void stop() { + try { + // Stop server + server.stop(); + } catch (Exception e) { + // Failed to stop + } + } + +} diff --git a/src/main/java/org/qora/utils/URLViewer.java b/src/main/java/org/qora/utils/URLViewer.java new file mode 100644 index 00000000..54b54e48 --- /dev/null +++ b/src/main/java/org/qora/utils/URLViewer.java @@ -0,0 +1,35 @@ +package org.qora.utils; + +import java.awt.Desktop; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class URLViewer { + + private static final Logger LOGGER = LogManager.getLogger(URLViewer.class); + + public static void openWebpage(URI uri) { + Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null; + + if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) { + try { + desktop.browse(uri); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + } + } + } + + public static void openWebpage(URL url) { + try { + openWebpage(url.toURI()); + } catch (URISyntaxException e) { + LOGGER.error(e.getMessage(), e); + } + } + +} \ No newline at end of file diff --git a/src/main/resources/bundled-ui b/src/main/resources/bundled-ui new file mode 120000 index 00000000..0e48cc17 --- /dev/null +++ b/src/main/resources/bundled-ui @@ -0,0 +1 @@ +../../../../node-UI \ No newline at end of file