package bluej;

import bluej.collect.DataCollector;
import bluej.extensions.event.ApplicationEvent;
import bluej.extmgr.ExtensionWrapper;
import bluej.extmgr.ExtensionsManager;
import bluej.pkgmgr.PkgMgrFrame;
import bluej.pkgmgr.Project;
import bluej.prefmgr.PrefMgr;
import bluej.utility.Debug;
import bluej.utility.DialogManager;
import bluej.utility.Utility;
import bluej.utility.javafx.FXPlatformRunnable;
import bluej.utility.javafx.JavaFXUtil;
import de.codecentric.centerdevice.MenuToolkit;
import de.codecentric.centerdevice.dialogs.about.AboutStageBuilder;
import javafx.application.Platform;
import javafx.concurrent.Worker.State;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.BorderPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLAnchorElement;
import threadchecker.OnThread;
import threadchecker.Tag;

import javax.swing.*;
import java.awt.*;
import java.awt.desktop.QuitResponse;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.util.EventListener;
import java.util.List;
import java.util.Properties;
import java.util.Scanner;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;


| BlueJ starts here. The Boot class, which is responsible for dealing with | specialised class loaders, constructs an object of this class to initiate the | "real" BlueJ. |* * @author Michael Kolling */ public class Main{ private static final String MESSAGE_ROOT = "https://www.bluej.org/message/"; private static final String TESTING_MESSAGE_ROOT = "https://www.bluej.org/message_test/"; /** * Whether we've officially launched yet. While false "open file" requests only * set initialProject. */ private static boolean launched = false; /** On MacOS X, this will be set to the project we should open (if any) */ private static List<File> initialProjects; | |private static QuitResponse macEventResponse = null; // used to respond to external quit events on MacOS | |/** | Only used on Mac. For some reason, executing the AppleJavaExtensions open | file handler (that is set from Boot.main) on initial load (e.g. because | the user double-clicked a project.greenfoot file) means that later on, | the context class loader is null on the JavaFX thread. | Honestly, I [NB] have no idea what the hell is going on there. | But the work-around is apparent: store the context class loader early on, | then if it is null later, restore it. (There's no problem if the file | handler is not executed; the context class loader is the same early on as | later on the FX thread). | private static ClassLoader storedContextClassLoader;
| The mechanism to show the initial GUI | private static GuiHandler guiHandler = null;
| Entry point to starting up the system. Initialise the system and start | the first package manager frame. | @OnThread(Tag.Any) public Main() { Boot boot = Boot.getInstance(); final String[] args = Boot.cmdLineArgs; Properties commandLineProps = boot.getCommandLineProperties(); File bluejLibDir = Boot.getBluejLibDir(); Config.initialise(bluejLibDir, commandLineProps, boot.isGreenfoot()); CompletableFuture<Stage> futureMainWindow = new CompletableFuture<>(); if (!Config.isGreenfoot()) new Thread(() -> fetchAndShowCentralMsg(PrefMgr.getFlag(PrefMgr.NEWS_TESTING) ? TESTING_MESSAGE_ROOT : MESSAGE_ROOT, futureMainWindow)).start(); if (guiHandler == null) { guiHandler = new BlueJGuiHandler(); } if (Config.isMacOS()) { prepareMacOSApp(); } SwingUtilities.invokeLater(() -> { List<ExtensionWrapper> loadedExtensions = ExtensionsManager.getInstance().getLoadedExtensions(null); Platform.runLater(() -> { DataCollector.bluejOpened(getOperatingSystem(), getJavaVersion(), getBlueJVersion(), getInterfaceLanguage(), loadedExtensions); Stage stage = processArgs(args); futureMainWindow.complete(stage); }); }); new Thread() { @Override public void run() { updateStats(); } }.start(); }
| Start everything off. This is used to open the projects specified on the | command line when starting BlueJ. Any parameters starting with '-' are | ignored for now. | | @return A handle to the main window which was opened, or null if there was no window opened. | @OnThread(Tag.FXPlatform) private static Stage processArgs(String[] args) { launched = true; boolean oneOpened = false; if (args.length > 0) { for (String arg : args) { if (!arg.startsWith("-")) { oneOpened |= guiHandler.tryOpen(new File(arg), true); } } } if (initialProjects != null) { for (File initialProject : initialProjects) { oneOpened |= guiHandler.tryOpen(initialProject, true); } } if (!oneOpened) { boolean openOrphans = "true".equals(Config.getPropString("bluej.autoOpenLastProject")); if (openOrphans && hadOrphanPackages()) { String exists = ""; for (int i = 1; exists != null; i++) { exists = Config.getPropString(Config.BLUEJ_OPENPACKAGE + i, null); if (exists != null) { oneOpened |= guiHandler.tryOpen(new File(exists), false); } } } } Stage window = guiHandler.initialOpenComplete(oneOpened); Boot.getInstance().disposeSplashWindow(); ExtensionsManager.getInstance().delegateEvent(new ApplicationEvent(ApplicationEvent.APP_READY_EVENT)); return window; }
| Prepare MacOS specific behaviour (About menu, Preferences menu, Quit | menu) | private static void prepareMacOSApp() { storedContextClassLoader = Thread.currentThread().getContextClassLoader(); initialProjects = Boot.getMacInitialProjects(); prepareMacOSMenuSwing(); prepareMacOSMenuFX(); if (Config.isGreenfoot()) { Debug.message("Disabling App Nap"); try { Runtime.getRuntime().exec("defaults write org.greenfoot NSAppSleepDisabled -bool YES"); } catch (IOException e) { Debug.reportError("Error disabling App Nap", e); } } }
| Prepare Mac Application FX menu using the NSMenuFX library. | This is needed for due to a bug in the JDK APIs, not responding to | handleAbout() etc, when the menu is on FX. | private static void prepareMacOSMenuFX() { Platform.runLater(() -> { FXMLLoader.setDefaultClassLoader(AboutStageBuilder.class.getClassLoader()); MenuToolkit menuToolkit = MenuToolkit.toolkit(); Menu defaultApplicationMenu = menuToolkit.createDefaultApplicationMenu(Config.getApplicationName()); menuToolkit.setApplicationMenu(defaultApplicationMenu); defaultApplicationMenu.getItems().get(0).setOnAction(event -> guiHandler.handleAbout()); MenuItem preferences = new MenuItem(Config.getString("menu.tools.preferences")); if (Config.hasAcceleratorKey("menu.tools.preferences")) { preferences.setAccelerator(Config.getAcceleratorKeyFX("menu.tools.preferences")); } preferences.setOnAction(event -> guiHandler.handlePreferences()); defaultApplicationMenu.getItems().add(1, preferences); defaultApplicationMenu.getItems().get(defaultApplicationMenu.getItems().size()-1). setOnAction(event -> guiHandler.handleQuit()); }); }
| Prepare Mac Application Swing menu using the java.awt.Desktop APIs. | @SuppressWarnings("threadchecker") private static void prepareMacOSMenuSwing() { Desktop.getDesktop().setAboutHandler(e -> { Platform.runLater(() -> guiHandler.handleAbout()); }); Desktop.getDesktop().setPreferencesHandler(e -> { Platform.runLater(() -> guiHandler.handlePreferences()); }); Desktop.getDesktop().setQuitHandler((e, response) -> { macEventResponse = response; Platform.runLater(() -> guiHandler.handleQuit()); }); Desktop.getDesktop().setOpenFileHandler(e -> { if (launched) { List<File> files = e.getFiles(); Platform.runLater(() -> { for (File file : files) { guiHandler.tryOpen(file, true); } }); } else { initialProjects = e.getFiles(); } }); Boot.getInstance().setQuitHandler(() -> Platform.runLater(() -> guiHandler.handleQuit())); }
| Handle the "quit" command: if any projects are open, prompt user to make sure, and quit if |* confirmed. */ @OnThread(Tag.FXPlatform) public static void wantToQuit() { int projectCount = Project.getOpenProjectCount(); | |// We set a null owner here to make the dialog come to the front of all windows; | |// the user may have triggered the quit shortcut from any window, not just a PkgMgrFrame: | |int answer = projectCount <= 1 ? 0 : DialogManager.askQuestionFX(null, "quit-all"); if (answer == 0) { doQuit(); } else { SwingUtilities.invokeLater(() -> { if (macEventResponse != null) { macEventResponse.cancelQuit(); | |macEventResponse = null; | |} | |}); | |} | |} | |/** | Perform the closing down and quitting of BlueJ, including unloading | extensions. | @OnThread(Tag.FXPlatform) public static void doQuit() { guiHandler.doExitCleanup(); SwingUtilities.invokeLater(() -> { ExtensionsManager extMgr = ExtensionsManager.getInstance(); extMgr.unloadExtensions(); Platform.runLater(() -> bluej.Main.exit()); }); }
| Checks if there were orphan packages on last exit by looking for | existence of a valid BlueJ project among the saved values for the | orphaned packages. | | @return whether a valid orphaned package exist. | public static boolean hadOrphanPackages() { String dir = ""; for (int i = 1; dir != null; i++) { dir = Config.getPropString(Config.BLUEJ_OPENPACKAGE + i, null); if (dir != null) { if (Project.isProject(dir)) { return true; } } } return false; }
| Send statistics of use back to bluej.org | private static void updateStats() { String uidPropName; String baseURL; String appVersion; if (Config.isGreenfoot()) { uidPropName = "greenfoot.uid"; baseURL = "http://stats.greenfoot.org/updateGreenfoot.php"; appVersion = Boot.GREENFOOT_VERSION; } else { uidPropName = "bluej.uid"; baseURL = "http://stats.bluej.org/updateBlueJ.php"; appVersion = getBlueJVersion(); } String language = getInterfaceLanguage(); String javaVersion = getJavaVersion(); String systemID = getOperatingSystem(); String editorStats = ""; int javaEditors = Config.getEditorCount(Config.SourceType.Java); int strideEditors = Config.getEditorCount(Config.SourceType.Stride); try { if (javaEditors != -1 && strideEditors != -1) { editorStats = "&javaeditors=" + URLEncoder.encode(Integer.toString(javaEditors), "UTF-8") + "&strideeditors=" + URLEncoder.encode(Integer.toString(strideEditors), "UTF-8"); } } catch (UnsupportedEncodingException ex) { Debug.reportError(ex); } Config.resetEditorsCount(); String uid = Config.getPropString(uidPropName, null); if (uid == null) { uid = UUID.randomUUID().toString(); Config.putPropString(uidPropName, uid); } else if (uid.equalsIgnoreCase("private")) { return; } try { URL url = new URL(baseURL + "?uid=" + URLEncoder.encode(uid, "UTF-8") + "&osname=" + URLEncoder.encode(systemID, "UTF-8") + "&appversion=" + URLEncoder.encode(appVersion, "UTF-8") + "&javaversion=" + URLEncoder.encode(javaVersion, "UTF-8") + "&language=" + URLEncoder.encode(language, "UTF-8") + editorStats ); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.connect(); int rc = conn.getResponseCode(); conn.disconnect(); if (rc != 200) Debug.reportError("Update stats failed, HTTP response code: " + rc); } catch (Exception ex) { Debug.reportError("Update stats failed: " + ex.getClass().getName() + ": " + ex.getMessage()); } } private static String getBlueJVersion() { return Boot.BLUEJ_VERSION; } private static String getOperatingSystem() { String osArch = System.getProperty("os.arch"); if (Config.isWinOS()) { String arch = System.getenv("PROCESSOR_ARCHITECTURE"); String wow64Arch = System.getenv("PROCESSOR_ARCHITEW6432"); osArch += (arch != null && arch.endsWith("64")) || (wow64Arch != null && wow64Arch.endsWith("64")) ? "(64)" : ""; } return System.getProperty("os.name") + "/" + osArch + "/" + System.getProperty("os.version"); } private static String getJavaVersion() { return System.getProperty("java.version"); } private static String getInterfaceLanguage() { return Config.language; }
| Exit BlueJ. | | The open frame count should be zero by this point as PkgMgrFrame is | responsible for cleaning itself up before getting here. | @OnThread(Tag.FXPlatform) private static void exit() { DataCollector.bluejClosed(); Config.handleExit(); JavaFXUtil.runAfterCurrent(() -> SwingUtilities.invokeLater(() -> System.exit(0))); } public static ClassLoader getStoredContextClassLoader() { return storedContextClassLoader; }
| Set the inital GUI, created after the initial project is opened. | @param initialGUI A consume which displays the GUI for the initial project. | public static void setGuiHandler(GuiHandler initialGUI) { Main.guiHandler = initialGUI; }
| Fetch and show a message from bluej.org, by looking at the central index then fetching | the current message (if any, and if unseen). | | @param withStage A future which will complete with a parent window (or null if none). | The message should be shown as the modal child of this window. | private static void fetchAndShowCentralMsg(String messageRoot, CompletableFuture<Stage> withStage) { try { Scanner scanner = new Scanner(new URL(messageRoot + "latest.txt").openStream(), "UTF-8").useDelimiter("\n"); LocalDate startDate = LocalDate.parse(scanner.nextLine()); LocalDate endDate = LocalDate.parse(scanner.nextLine()); LocalDate lastSeen = null; try { lastSeen = LocalDate.parse(Config.getPropString(Config.MESSAGE_LATEST_SEEN)); } catch (Exception e) { } boolean seenMessage = lastSeen != null && (startDate.isBefore(lastSeen) || startDate.isEqual(lastSeen)); boolean expired = LocalDate.now().isAfter(endDate); if (!seenMessage && !expired) { Platform.runLater(() -> { WebView webView = new WebView(); FXPlatformRunnable preventTimeout = JavaFXUtil.runAfter(Duration.seconds(5), () -> { webView.getEngine().getLoadWorker().cancel(); }); AtomicBoolean shownWindow = new AtomicBoolean(false); JavaFXUtil.addChangeListener(webView.getEngine().getLoadWorker().stateProperty(), state -> { if (state == State.SUCCEEDED && !shownWindow.get()) { shownWindow.set(true); JavaFXUtil.runNowOrLater(() -> { preventTimeout.run(); makeLinksOpenExternally(webView.getEngine().getDocument()); }); withStage.handle((parent, error) -> { if (parent != null) { JavaFXUtil.runNowOrLater(() -> showMessageWindow(startDate, webView, parent)); } return null; }); } }); webView.getEngine().load(messageRoot + startDate.toString() + ".html"); }); } } catch (MalformedURLException e) { Debug.reportError(e); } catch (IOException e) { } }
| Overrides the click behaviour of all <a> tags in the document | so that they open in an external browser window. | | @param document The document in which to override the links | private static void makeLinksOpenExternally(Document document) { NodeList nodeList = document.getElementsByTagName("a"); for (int i = 0; i < nodeList.getLength(); i++) { Node node= nodeList.item(i); EventTarget eventTarget = (EventTarget) node; eventTarget.addEventListener("click", new org.w3c.dom.events.EventListener() { @Override public void handleEvent(org.w3c.dom.events.Event evt) { EventTarget target = evt.getCurrentTarget(); HTMLAnchorElement anchorElement = (HTMLAnchorElement) target; String href = anchorElement.getHref(); SwingUtilities.invokeLater(() -> Utility.openWebBrowser(href)); evt.preventDefault(); } }, false); } }
| Shows the message fetched from the server in a new modal window. When the window | is closed, record that the user has seen the message. | | @param startDate The start date (identifier) of the message being shown. | @param webView The WebView component in which the message has been loaded | @param parent The parent window. Must be non-null | @OnThread(Tag.FXPlatform) private static void showMessageWindow(LocalDate startDate, WebView webView, Stage parent) { Stage window = new Stage(); window.initModality(Modality.WINDOW_MODAL); window.initOwner(parent); window.setTitle(Config.getString("bluej.central.msg.title")); Button button = new Button(Config.getString("okay")); button.setDefaultButton(true); button.setOnAction(e -> { window.hide(); }); window.setOnHidden(e -> { Config.recordLatestSeen(startDate); }); BorderPane.setAlignment(button, Pos.CENTER); BorderPane.setMargin(button, new Insets(15)); BorderPane.setMargin(webView, new Insets(10)); webView.setPrefWidth(650); webView.setPrefHeight(400); window.setScene(new Scene(new BorderPane(webView, null, null, button, null))); window.show(); window.toFront(); } }

.   Main
.   run
.   processArgs
.   prepareMacOSApp
.   prepareMacOSMenuFX
.   prepareMacOSMenuSwing
.   doQuit
.   hadOrphanPackages
.   updateStats
.   getBlueJVersion
.   getOperatingSystem
.   getJavaVersion
.   getInterfaceLanguage
.   exit
.   getStoredContextClassLoader
.   setGuiHandler
.   fetchAndShowCentralMsg
.   makeLinksOpenExternally
.   handleEvent
.   showMessageWindow




679 neLoCode + 62 LoComm