diff --git a/client/build.gradle.kts b/client/build.gradle.kts
index 058b9f2..e4ab79b 100644
--- a/client/build.gradle.kts
+++ b/client/build.gradle.kts
@@ -9,7 +9,9 @@ ktfmt { kotlinLangStyle() }
dependencies {
implementation("com.jgoodies:jgoodies-forms:1.6.0")
implementation("com.miglayout:miglayout-swing:5.2")
+ implementation("com.konghq:unirest-java:3.14.2")
implementation(project(":core"))
+ testImplementation(project(":test-core"))
}
application {
diff --git a/client/src/main/java/bean/HyperlinkAdaptor.java b/client/src/main/java/bean/HyperlinkAdaptor.java
deleted file mode 100644
index f46d59a..0000000
--- a/client/src/main/java/bean/HyperlinkAdaptor.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package bean;
-
-import java.awt.Component;
-import java.awt.Cursor;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-
-import util.Debug;
-
-public class HyperlinkAdaptor extends MouseAdapter
-{
- private HyperlinkListener listener = null;
- private Component listenerWindow = null;
-
- public HyperlinkAdaptor(HyperlinkListener listener)
- {
- if (!(listener instanceof Component))
- {
- Debug.stackTrace("Creating HyperlinkAdaptor with non-component: " + listener);
- }
-
- this.listener = listener;
- this.listenerWindow = (Component)listener;
- }
-
- @Override
- public void mouseClicked(MouseEvent arg0)
- {
- listener.linkClicked(arg0);
- }
-
- @Override
- public void mouseMoved(MouseEvent arg0)
- {
- if (listener.isOverHyperlink(arg0))
- {
- listenerWindow.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
- }
- else
- {
- listenerWindow.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
- }
- }
-
- @Override
- public void mouseExited(MouseEvent arg0)
- {
- listenerWindow.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
- }
-}
diff --git a/client/src/main/java/bean/HyperlinkListener.java b/client/src/main/java/bean/HyperlinkListener.java
deleted file mode 100644
index 747565e..0000000
--- a/client/src/main/java/bean/HyperlinkListener.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package bean;
-
-import java.awt.event.MouseEvent;
-
-public interface HyperlinkListener
-{
- public void linkClicked(MouseEvent arg0);
- public boolean isOverHyperlink(MouseEvent arg0);
-}
diff --git a/client/src/main/java/online/util/DesktopEntropyClient.java b/client/src/main/java/online/util/DesktopEntropyClient.java
index 6a7d147..f5f0e35 100644
--- a/client/src/main/java/online/util/DesktopEntropyClient.java
+++ b/client/src/main/java/online/util/DesktopEntropyClient.java
@@ -75,13 +75,6 @@ public void sendAsyncInSingleThread(MessageSenderParams message)
senderThread.start();
}
- /*public void sendAsync(MessageSenderParams message)
- {
- MessageSender senderRunnable = new MessageSender(this, message);
- Thread senderThread = new Thread(senderRunnable, "MessageSender");
- senderThread.start();
- }*/
-
@Override
public String sendSyncOnDevice(MessageSender runnable)
{
@@ -91,6 +84,6 @@ public String sendSyncOnDevice(MessageSender runnable)
@Override
public void checkForUpdates()
{
- UpdateChecker.checkForUpdates(FILE_NAME_ENTROPY_JAR, SERVER_PORT_NUMBER_DOWNLOAD);
+ UpdateManager.INSTANCE.checkForUpdates(OnlineConstants.ENTROPY_VERSION_NUMBER);
}
}
diff --git a/client/src/main/java/online/util/ResponseHandler.java b/client/src/main/java/online/util/ResponseHandler.java
index 36ef191..e6e606d 100644
--- a/client/src/main/java/online/util/ResponseHandler.java
+++ b/client/src/main/java/online/util/ResponseHandler.java
@@ -265,7 +265,7 @@ private static void handleConnectFailure(Element root)
if (failureReason.contains("out of date"))
{
- promptForUpdate(root);
+ promptForUpdate();
}
else
{
@@ -273,20 +273,15 @@ private static void handleConnectFailure(Element root)
}
}
- private static void promptForUpdate(final Element rootElement)
+ private static void promptForUpdate()
{
- SwingUtilities.invokeLater(new Runnable()
- {
- @Override
- public void run()
- {
- int answer = DialogUtil.showQuestion("Entropy needs to update in order to connect. \n\nUpdate now?", false);
- if (answer == JOptionPane.YES_OPTION)
- {
- UpdateChecker.startUpdate(rootElement, OnlineConstants.FILE_NAME_ENTROPY_JAR, OnlineConstants.SERVER_PORT_NUMBER_DOWNLOAD);
- }
- }
- });
+ SwingUtilities.invokeLater(() -> {
+ int answer = DialogUtil.showQuestion("Entropy needs to update in order to connect. \n\nUpdate now?", false);
+ if (answer == JOptionPane.YES_OPTION)
+ {
+ UpdateManager.INSTANCE.checkForUpdates(OnlineConstants.ENTROPY_VERSION_NUMBER);
+ }
+ });
}
private static void handleKickOff(Element root)
diff --git a/client/src/main/java/screen/AbstractAboutDialog.java b/client/src/main/java/screen/AbstractAboutDialog.java
index 9ee95f3..f33094a 100644
--- a/client/src/main/java/screen/AbstractAboutDialog.java
+++ b/client/src/main/java/screen/AbstractAboutDialog.java
@@ -14,10 +14,10 @@
import javax.swing.SwingConstants;
import bean.HyperlinkAdaptor;
-import bean.HyperlinkListener;
+import bean.IHyperlinkListener;
public abstract class AbstractAboutDialog extends JDialog
- implements HyperlinkListener,
+ implements IHyperlinkListener,
ActionListener
{
public AbstractAboutDialog()
diff --git a/client/src/main/java/util/DialogUtil.java b/client/src/main/java/util/DialogUtil.java
index 9639ebe..f00f3f6 100644
--- a/client/src/main/java/util/DialogUtil.java
+++ b/client/src/main/java/util/DialogUtil.java
@@ -3,13 +3,11 @@
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
-import screen.LoadingDialog;
-
import static utils.InjectedThings.logger;
+@Deprecated()
public class DialogUtil
{
- private static LoadingDialog loadingDialog = new LoadingDialog();
private static boolean shownConnectionLost = false;
public static void showInfo(String infoText)
@@ -104,14 +102,4 @@ public static void showConnectionLost()
shownConnectionLost = true;
}
}
-
- public static void showLoadingDialog(String text)
- {
- logger.info("loaderShown", text);
- loadingDialog.showDialog(text);
- }
- public static void dismissLoadingDialog()
- {
- loadingDialog.dismissDialog();
- }
}
diff --git a/client/src/main/java/util/UpdateChecker.java b/client/src/main/java/util/UpdateChecker.java
deleted file mode 100644
index 291b49b..0000000
--- a/client/src/main/java/util/UpdateChecker.java
+++ /dev/null
@@ -1,110 +0,0 @@
-package util;
-
-import java.io.IOException;
-
-import javax.swing.JOptionPane;
-
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-
-/**
- * Class to check for updates and launch the EntropyUpdater via a batch file if they are available
- */
-public class UpdateChecker implements XmlConstants
-{
- public static void checkForUpdates(String filename, int portForDownload)
- {
- try
- {
- checkForUpdatesAndDoDownloadIfRequired(filename, portForDownload);
- }
- finally
- {
- DialogUtil.dismissLoadingDialog();
- }
- }
-
- private static void checkForUpdatesAndDoDownloadIfRequired(String filename, int portForDownload)
- {
- //Show this here, checking the CRC can take time
- DialogUtil.showLoadingDialog("Checking for updates...");
- String crc = FileUtil.getMd5Crc(filename);
- if (crc == null)
- {
- DialogUtil.showError("Failed to check for updates (couldn't find " + filename + ").");
- return;
- }
-
- Debug.append("Checking for updates - fileCrc is " + crc);
-
- //We have a CRC, so go to the Server and check against it's copy.
- Document crcCheck = factoryCrcCheck(crc, filename);
- String responseStr = AbstractClient.getInstance().sendSync(crcCheck, false);
- if (responseStr == null)
- {
- DialogUtil.showError("Failed to check for updates (unable to connect).");
- return;
- }
-
- DialogUtil.dismissLoadingDialog();
-
- Document xmlResponse = XmlUtil.getDocumentFromXmlString(responseStr);
- Element rootElement = xmlResponse.getDocumentElement();
- String responseName = rootElement.getNodeName();
- if (responseName.equals(RESPONSE_TAG_NO_UPDATES))
- {
- //No need to show a message, should be pretty obvious
- Debug.append("I am up to date");
- return;
- }
-
- //An update is available
- int answer = DialogUtil.showQuestion("An update is available. Would you like to download it now?", false);
- if (answer == JOptionPane.NO_OPTION)
- {
- return;
- }
-
- startUpdate(rootElement, filename, portForDownload);
- }
-
- private static Document factoryCrcCheck(String fileCrc, String fileName)
- {
- Document document = XmlUtil.factoryNewDocument();
- Element rootElement = document.createElement(ROOT_TAG_CRC_CHECK);
- rootElement.setAttribute("FileCrc", fileCrc);
- rootElement.setAttribute("FileName", fileName);
-
- document.appendChild(rootElement);
- return document;
- }
-
- public static void startUpdate(Element rootElement, String filename, int portForUpdate)
- {
- int fileSize = XmlUtil.getAttributeInt(rootElement, "FileSize");
- String version = rootElement.getAttribute("VersionNumber");
- String args = fileSize + " " + version + " " + filename + " " + portForUpdate;
-
- try
- {
- if (AbstractClient.isAppleOs())
- {
- Runtime.getRuntime().exec("update.command " + args);
- }
- else
- {
- Runtime.getRuntime().exec("cmd /c start update.bat " + args);
- }
- }
- catch (IOException ioe)
- {
- Debug.stackTrace(ioe);
- String manualCommand = "update.bat " + args;
-
- String msg = "Failed to launch update.bat - call the following manually to perform the update: \n\n" + manualCommand;
- DialogUtil.showError(msg);
- }
-
- System.exit(0);
- }
-}
diff --git a/client/src/main/kotlin/bean/HyperlinkAdaptor.kt b/client/src/main/kotlin/bean/HyperlinkAdaptor.kt
new file mode 100644
index 0000000..7510cd0
--- /dev/null
+++ b/client/src/main/kotlin/bean/HyperlinkAdaptor.kt
@@ -0,0 +1,36 @@
+package bean
+
+import java.awt.Component
+import java.awt.Cursor
+import java.awt.event.MouseAdapter
+import java.awt.event.MouseEvent
+
+class HyperlinkAdaptor(private val listener: IHyperlinkListener) : MouseAdapter() {
+ private val listenerWindow = listener as Component
+
+ override fun mouseClicked(arg0: MouseEvent) = listener.linkClicked(arg0)
+
+ override fun mouseMoved(arg0: MouseEvent) {
+ if (listener.isOverHyperlink(arg0)) {
+ listenerWindow.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
+ } else {
+ listenerWindow.cursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)
+ }
+ }
+
+ override fun mouseEntered(e: MouseEvent) {
+ if (listener.isOverHyperlink(e)) {
+ listenerWindow.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
+ }
+ }
+
+ override fun mouseDragged(e: MouseEvent?) {
+ if (listenerWindow.cursor.type == Cursor.HAND_CURSOR) {
+ listenerWindow.cursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)
+ }
+ }
+
+ override fun mouseExited(arg0: MouseEvent?) {
+ listenerWindow.cursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)
+ }
+}
diff --git a/client/src/main/kotlin/bean/IHyperlinkListener.kt b/client/src/main/kotlin/bean/IHyperlinkListener.kt
new file mode 100644
index 0000000..58075c7
--- /dev/null
+++ b/client/src/main/kotlin/bean/IHyperlinkListener.kt
@@ -0,0 +1,9 @@
+package bean
+
+import java.awt.event.MouseEvent
+
+interface IHyperlinkListener {
+ fun linkClicked(arg0: MouseEvent)
+
+ fun isOverHyperlink(arg0: MouseEvent): Boolean
+}
diff --git a/client/src/main/kotlin/bean/LinkLabel.kt b/client/src/main/kotlin/bean/LinkLabel.kt
new file mode 100644
index 0000000..e20a46f
--- /dev/null
+++ b/client/src/main/kotlin/bean/LinkLabel.kt
@@ -0,0 +1,20 @@
+package bean
+
+import java.awt.Color
+import java.awt.event.MouseEvent
+import javax.swing.JLabel
+
+class LinkLabel(text: String, private val linkClicked: () -> Unit) :
+ JLabel("$text"), IHyperlinkListener {
+ init {
+ foreground = Color.BLUE
+
+ val adaptor = HyperlinkAdaptor(this)
+ addMouseListener(adaptor)
+ addMouseMotionListener(adaptor)
+ }
+
+ override fun linkClicked(arg0: MouseEvent) = linkClicked()
+
+ override fun isOverHyperlink(arg0: MouseEvent) = true
+}
diff --git a/client/src/main/kotlin/util/DialogUtilNew.kt b/client/src/main/kotlin/util/DialogUtilNew.kt
new file mode 100644
index 0000000..929e30f
--- /dev/null
+++ b/client/src/main/kotlin/util/DialogUtilNew.kt
@@ -0,0 +1,164 @@
+package util
+
+import java.awt.Component
+import java.io.File
+import javax.swing.JFileChooser
+import javax.swing.JOptionPane
+import javax.swing.SwingUtilities
+import screen.LoadingDialog
+import screen.ScreenCache
+import utils.InjectedThings.logger
+
+object DialogUtilNew {
+ private var loadingDialog: LoadingDialog? = null
+
+ fun showInfo(infoText: String, parent: Component = ScreenCache.getMainScreen()) {
+ logDialogShown("Info", "Information", infoText)
+ JOptionPane.showMessageDialog(
+ parent,
+ infoText,
+ "Information",
+ JOptionPane.INFORMATION_MESSAGE
+ )
+ logDialogClosed("Info", null)
+ }
+
+ fun showCustomMessage(message: Any, parent: Component = ScreenCache.getMainScreen()) {
+ logDialogShown("CustomInfo", "Information", "?")
+ JOptionPane.showMessageDialog(
+ parent,
+ message,
+ "Information",
+ JOptionPane.INFORMATION_MESSAGE
+ )
+ logDialogClosed("CustomInfo", null)
+ }
+
+ fun showError(errorText: String, parent: Component? = ScreenCache.getMainScreen()) {
+ dismissLoadingDialog()
+
+ logDialogShown("Error", "Error", errorText)
+ JOptionPane.showMessageDialog(parent, errorText, "Error", JOptionPane.ERROR_MESSAGE)
+ logDialogClosed("Error", null)
+ }
+
+ fun showErrorLater(errorText: String) {
+ SwingUtilities.invokeLater { showError(errorText) }
+ }
+
+ @JvmOverloads
+ fun showQuestion(
+ message: String,
+ allowCancel: Boolean = false,
+ parent: Component = ScreenCache.getMainScreen()
+ ): Int {
+ logDialogShown("Question", "Question", message)
+ val option =
+ if (allowCancel) JOptionPane.YES_NO_CANCEL_OPTION else JOptionPane.YES_NO_OPTION
+ val selection =
+ JOptionPane.showConfirmDialog(
+ parent,
+ message,
+ "Question",
+ option,
+ JOptionPane.QUESTION_MESSAGE
+ )
+ logDialogClosed("Question", selection)
+ return selection
+ }
+
+ fun showLoadingDialog(text: String) {
+ logDialogShown("Loading", "", text)
+ loadingDialog = LoadingDialog()
+ loadingDialog?.showDialog(text)
+ }
+
+ fun dismissLoadingDialog() {
+ val wasVisible = loadingDialog?.isVisible ?: false
+ loadingDialog?.dismissDialog()
+ if (wasVisible) {
+ logDialogClosed("Loading", null)
+ }
+ }
+
+ fun showOption(title: String, message: String, options: List): String? {
+ logDialogShown("Option", title, message)
+ val typedArray = options.toTypedArray()
+ val selection =
+ JOptionPane.showOptionDialog(
+ null,
+ message,
+ title,
+ JOptionPane.DEFAULT_OPTION,
+ JOptionPane.QUESTION_MESSAGE,
+ null,
+ typedArray,
+ options.first()
+ )
+ val selectionStr = if (selection > -1) typedArray[selection] else null
+
+ logDialogClosed("Option", selectionStr)
+ return selectionStr
+ }
+
+ fun showInput(
+ title: String,
+ message: String,
+ options: Array? = null,
+ defaultOption: K? = null
+ ): K? {
+ logDialogShown("Input", title, message)
+ val selection =
+ JOptionPane.showInputDialog(
+ null,
+ message,
+ title,
+ JOptionPane.PLAIN_MESSAGE,
+ null,
+ options,
+ defaultOption
+ ) as K?
+
+ logDialogClosed("Input", selection)
+ return selection
+ }
+
+ fun chooseDirectory(parent: Component?): File? {
+ logDialogShown("File selector", "", "")
+ val fc = JFileChooser()
+ fc.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
+ val option = fc.showDialog(parent, "Select")
+ if (option != JFileChooser.APPROVE_OPTION) {
+ return null
+ }
+
+ val file = fc.selectedFile
+ logDialogClosed("File selector", file?.absolutePath)
+ return file
+ }
+
+ private fun logDialogShown(type: String, title: String, message: String) {
+ logger.info(
+ "dialogShown",
+ "$type dialog shown: $message",
+ "dialogType" to type,
+ "dialogTitle" to title,
+ "dialogMessage" to message
+ )
+ }
+
+ private fun logDialogClosed(type: String, selection: Any?) {
+ var message = "$type dialog closed"
+ selection?.let { message += " - selected ${translateOption(it)}" }
+
+ logger.info("dialogClosed", message, "dialogType" to type, "dialogSelection" to selection)
+ }
+
+ private fun translateOption(option: Any?) =
+ when (option) {
+ JOptionPane.YES_OPTION -> "Yes"
+ JOptionPane.NO_OPTION -> "No"
+ JOptionPane.CANCEL_OPTION -> "Cancel"
+ else -> option
+ }
+}
diff --git a/client/src/main/kotlin/util/UpdateManager.kt b/client/src/main/kotlin/util/UpdateManager.kt
new file mode 100644
index 0000000..f2d7ce9
--- /dev/null
+++ b/client/src/main/kotlin/util/UpdateManager.kt
@@ -0,0 +1,152 @@
+package util
+
+import bean.LinkLabel
+import java.awt.BorderLayout
+import java.io.File
+import javax.swing.JLabel
+import javax.swing.JOptionPane
+import javax.swing.JPanel
+import kong.unirest.Unirest
+import kong.unirest.json.JSONObject
+import kotlin.system.exitProcess
+import utils.InjectedThings.logger
+
+/**
+ * Automatically check for and download updates using the Github API
+ *
+ * https://developer.github.com/v3/repos/releases/#get-the-latest-release
+ */
+object UpdateManager {
+ fun checkForUpdates(currentVersion: String) {
+ // Show this here, checking the CRC can take time
+ logger.info("updateCheck", "Checking for updates - my version is $currentVersion")
+
+ val jsonResponse = queryLatestReleaseJson(OnlineConstants.ENTROPY_REPOSITORY_URL)
+ jsonResponse ?: return
+
+ val metadata = parseUpdateMetadata(jsonResponse)
+ if (metadata == null || !shouldUpdate(currentVersion, metadata)) {
+ return
+ }
+
+ startUpdate(metadata.getArgs(), Runtime.getRuntime())
+ }
+
+ fun queryLatestReleaseJson(repositoryUrl: String): JSONObject? {
+ try {
+ DialogUtilNew.showLoadingDialog("Checking for updates...")
+
+ val response = Unirest.get("$repositoryUrl/releases/latest").asJson()
+ if (response.status != 200) {
+ logger.error(
+ "updateError",
+ "Received non-success HTTP status: ${response.status} - ${response.statusText}",
+ "responseBody" to response.body,
+ )
+ DialogUtilNew.showError("Failed to check for updates (unable to connect).")
+ return null
+ }
+
+ return response.body.`object`
+ } catch (t: Throwable) {
+ logger.error("updateError", "Caught $t checking for updates", t)
+ DialogUtilNew.showError("Failed to check for updates (unable to connect).")
+ return null
+ } finally {
+ DialogUtilNew.dismissLoadingDialog()
+ }
+ }
+
+ fun shouldUpdate(currentVersion: String, metadata: UpdateMetadata): Boolean {
+ val newVersion = metadata.version
+ if (newVersion == currentVersion) {
+ logger.info("updateResult", "Up to date")
+ return false
+ }
+
+ // An update is available
+ logger.info("updateAvailable", "Newer release available - $newVersion")
+
+ if (!AbstractClient.isWindowsOs()) {
+ showManualDownloadMessage(newVersion)
+ return false
+ }
+
+ val answer =
+ DialogUtilNew.showQuestion(
+ "An update is available (${metadata.version}). Would you like to download it now?",
+ false
+ )
+ return answer == JOptionPane.YES_OPTION
+ }
+
+ private fun showManualDownloadMessage(newVersion: String) {
+ val fullUrl = "${OnlineConstants.ENTROPY_MANUAL_DOWNLOAD_URL}/tag/$newVersion"
+ val panel = JPanel()
+ panel.layout = BorderLayout(0, 0)
+ val lblOne =
+ JLabel("An update is available ($newVersion). You can download it manually from:")
+ val linkLabel = LinkLabel(fullUrl) { launchUrl(fullUrl) }
+
+ panel.add(lblOne, BorderLayout.NORTH)
+ panel.add(linkLabel, BorderLayout.SOUTH)
+
+ DialogUtilNew.showCustomMessage(panel)
+ }
+
+ fun parseUpdateMetadata(responseJson: JSONObject): UpdateMetadata? {
+ return try {
+ val remoteVersion = responseJson.getString("tag_name")
+ val assets = responseJson.getJSONArray("assets")
+ val asset = assets.getJSONObject(0)
+
+ val assetId = asset.getLong("id")
+ val fileName = asset.getString("name")
+ val size = asset.getLong("size")
+ UpdateMetadata(remoteVersion, assetId, fileName, size)
+ } catch (t: Throwable) {
+ logger.error(
+ "parseError",
+ "Error parsing update response",
+ t,
+ "responseBody" to responseJson
+ )
+ null
+ }
+ }
+
+ fun startUpdate(args: String, runtime: Runtime) {
+ prepareBatchFile()
+
+ try {
+ runtime.exec("cmd /c start update.bat $args")
+ } catch (t: Throwable) {
+ logger.error("batchError", "Error running update.bat", t)
+ val manualCommand = "update.bat $args"
+
+ val msg =
+ "Failed to launch update.bat - call the following manually to perform the update: \n\n$manualCommand"
+ DialogUtilNew.showError(msg)
+ return
+ }
+
+ exitProcess(0)
+ }
+
+ fun prepareBatchFile() {
+ val updateFile = File("update.bat")
+
+ updateFile.delete()
+ val updateScript = javaClass.getResource("/update/update.bat").readText()
+ updateFile.writeText(updateScript)
+ }
+}
+
+data class UpdateMetadata(
+ val version: String,
+ val assetId: Long,
+ val fileName: String,
+ val size: Long
+) {
+ fun getArgs() = "$size $version $fileName $assetId"
+}
diff --git a/client/src/main/kotlin/util/UrlUtil.kt b/client/src/main/kotlin/util/UrlUtil.kt
new file mode 100644
index 0000000..e8c0a2f
--- /dev/null
+++ b/client/src/main/kotlin/util/UrlUtil.kt
@@ -0,0 +1,12 @@
+package util
+
+import utils.InjectedThings.logger
+
+/** N.B. will likely only work on linux */
+fun launchUrl(url: String, runtime: Runtime = Runtime.getRuntime()) {
+ try {
+ runtime.exec("xdg-open $url")
+ } catch (e: Exception) {
+ logger.error("urlError", "Failed to launch $url", e)
+ }
+}
diff --git a/client/src/main/resources/update/update.bat b/client/src/main/resources/update/update.bat
new file mode 100644
index 0000000..92f8a7b
--- /dev/null
+++ b/client/src/main/resources/update/update.bat
@@ -0,0 +1,17 @@
+@echo off
+
+REM %1 = noOfBytes
+REM %2 = version number
+REM %3 = filename
+REM %4 = assetId
+
+echo Performing download of %1 bytes (Version %2)
+
+curl -LJO -H "Accept: application/octet-stream" https://api.github.com/repos/alyssaburlton/Dartzee/releases/assets/%4
+
+ren Dartzee.jar Dartzee_OLD.jar
+ren %3 Dartzee.jar
+del Dartzee_OLD.jar
+
+start javaw -Xms256m -Xmx512m -jar Dartzee.jar justUpdated trueLaunch
+exit
\ No newline at end of file
diff --git a/client/src/test/kotlin/bean/HyperlinkAdaptorTest.kt b/client/src/test/kotlin/bean/HyperlinkAdaptorTest.kt
new file mode 100644
index 0000000..e111a1c
--- /dev/null
+++ b/client/src/test/kotlin/bean/HyperlinkAdaptorTest.kt
@@ -0,0 +1,94 @@
+package bean
+
+import com.github.alyssaburlton.swingtest.makeMouseEvent
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import io.mockk.mockk
+import io.mockk.verifySequence
+import java.awt.Cursor
+import java.awt.event.MouseEvent
+import javax.swing.JButton
+import javax.swing.JPanel
+import main.kotlin.testCore.AbstractTest
+import org.junit.jupiter.api.Test
+
+private val mouseEventOverLink = makeMouseEvent(JButton())
+private val mouseEventNotOverLink = makeMouseEvent(JButton())
+
+class HyperlinkAdaptorTest : AbstractTest() {
+ @Test
+ fun `Should not accept a non-component listener`() {
+ shouldThrow { HyperlinkAdaptor(NonComponentHyperlinkListener()) }
+ }
+
+ @Test
+ fun `Should respond to mouse clicks`() {
+ val listener = mockk(relaxed = true)
+
+ val adaptor = HyperlinkAdaptor(listener)
+ adaptor.mouseClicked(mouseEventOverLink)
+ adaptor.mouseClicked(mouseEventNotOverLink)
+
+ verifySequence {
+ listener.linkClicked(mouseEventOverLink)
+ listener.linkClicked(mouseEventNotOverLink)
+ }
+ }
+
+ @Test
+ fun `Should change the cursor on mouse movement`() {
+ val listener = TestHyperlinkListener()
+ val adaptor = HyperlinkAdaptor(listener)
+
+ adaptor.mouseMoved(mouseEventNotOverLink)
+ listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)
+
+ adaptor.mouseMoved(mouseEventOverLink)
+ listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
+
+ adaptor.mouseMoved(mouseEventNotOverLink)
+ listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)
+
+ adaptor.mouseEntered(mouseEventNotOverLink)
+ listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)
+
+ adaptor.mouseEntered(mouseEventOverLink)
+ listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
+ }
+
+ @Test
+ fun `Should revert the cursor on mouseExit`() {
+ val listener = TestHyperlinkListener()
+ val adaptor = HyperlinkAdaptor(listener)
+
+ adaptor.mouseMoved(mouseEventOverLink)
+ listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
+
+ adaptor.mouseExited(null)
+ listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)
+ }
+
+ @Test
+ fun `Should revert the cursor on mouseDragged`() {
+ val listener = TestHyperlinkListener()
+ val adaptor = HyperlinkAdaptor(listener)
+
+ adaptor.mouseMoved(mouseEventOverLink)
+ listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
+
+ adaptor.mouseDragged(null)
+ listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)
+ }
+}
+
+private class TestHyperlinkListener : JPanel(), IHyperlinkListener {
+ override fun isOverHyperlink(arg0: MouseEvent) = arg0 === mouseEventOverLink
+
+ override fun linkClicked(arg0: MouseEvent) {}
+}
+
+private class NonComponentHyperlinkListener : IHyperlinkListener {
+ override fun isOverHyperlink(arg0: MouseEvent) = false
+
+ override fun linkClicked(arg0: MouseEvent) {}
+}
diff --git a/client/src/test/kotlin/bean/LinkLabelTest.kt b/client/src/test/kotlin/bean/LinkLabelTest.kt
new file mode 100644
index 0000000..f881a21
--- /dev/null
+++ b/client/src/test/kotlin/bean/LinkLabelTest.kt
@@ -0,0 +1,37 @@
+package bean
+
+import com.github.alyssaburlton.swingtest.doClick
+import com.github.alyssaburlton.swingtest.doHover
+import com.github.alyssaburlton.swingtest.doHoverAway
+import io.kotest.matchers.shouldBe
+import io.mockk.mockk
+import io.mockk.verify
+import java.awt.Color
+import java.awt.Cursor
+import main.kotlin.testCore.AbstractTest
+import org.junit.jupiter.api.Test
+
+class LinkLabelTest : AbstractTest() {
+ @Test
+ fun `Should style text as URL`() {
+ val label = LinkLabel("https://foo.bar") {}
+
+ label.text shouldBe "https://foo.bar"
+ label.foreground shouldBe Color.BLUE
+ }
+
+ @Test
+ fun `Should update cursor on hover, and execute callback on click`() {
+ val callback = mockk<() -> Unit>(relaxed = true)
+ val label = LinkLabel("test", callback)
+
+ label.doHover()
+ label.cursor shouldBe Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
+
+ label.doHoverAway()
+ label.cursor shouldBe Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)
+
+ label.doClick()
+ verify { callback() }
+ }
+}
diff --git a/client/src/test/kotlin/util/DialogUtilNewTest.kt b/client/src/test/kotlin/util/DialogUtilNewTest.kt
new file mode 100644
index 0000000..7e6219d
--- /dev/null
+++ b/client/src/test/kotlin/util/DialogUtilNewTest.kt
@@ -0,0 +1,106 @@
+package util
+
+import com.github.alyssaburlton.swingtest.clickCancel
+import com.github.alyssaburlton.swingtest.clickNo
+import com.github.alyssaburlton.swingtest.clickOk
+import com.github.alyssaburlton.swingtest.clickYes
+import com.github.alyssaburlton.swingtest.findWindow
+import com.github.alyssaburlton.swingtest.flushEdt
+import com.github.alyssaburlton.swingtest.purgeWindows
+import io.kotest.matchers.shouldBe
+import javax.swing.JDialog
+import javax.swing.SwingUtilities
+import logging.Severity
+import main.kotlin.testCore.AbstractTest
+import main.kotlin.testCore.getErrorDialog
+import main.kotlin.testCore.getInfoDialog
+import main.kotlin.testCore.getQuestionDialog
+import main.kotlin.testCore.runAsync
+import org.junit.jupiter.api.Test
+
+class DialogUtilNewTest : AbstractTest() {
+ @Test
+ fun `Should log for INFO dialogs`() {
+ runAsync { DialogUtilNew.showInfo("Something useful") }
+
+ verifyLog("dialogShown", Severity.INFO).message shouldBe
+ "Info dialog shown: Something useful"
+
+ getInfoDialog().clickOk()
+ flushEdt()
+ verifyLog("dialogClosed", Severity.INFO).message shouldBe "Info dialog closed"
+ }
+
+ @Test
+ fun `Should log for ERROR dialogs`() {
+ runAsync { DialogUtilNew.showError("Something bad") }
+
+ verifyLog("dialogShown", Severity.INFO).message shouldBe "Error dialog shown: Something bad"
+ getErrorDialog().clickOk()
+ flushEdt()
+ verifyLog("dialogClosed", Severity.INFO).message shouldBe "Error dialog closed"
+ }
+
+ @Test
+ fun `Should show an ERROR dialog later`() {
+ SwingUtilities.invokeLater { Thread.sleep(500) }
+ DialogUtilNew.showErrorLater("Some error")
+
+ findWindow() shouldBe null
+
+ flushEdt()
+ getErrorDialog().clickOk()
+ flushEdt()
+ verifyLog("dialogClosed", Severity.INFO).message shouldBe "Error dialog closed"
+ }
+
+ @Test
+ fun `Should log for QUESTION dialogs, with the correct selection`() {
+ runAsync { DialogUtilNew.showQuestion("Do you like cheese?") }
+ verifyLog("dialogShown", Severity.INFO).message shouldBe
+ "Question dialog shown: Do you like cheese?"
+ getQuestionDialog().clickYes()
+ flushEdt()
+ verifyLog("dialogClosed", Severity.INFO).message shouldBe
+ "Question dialog closed - selected Yes"
+
+ clearLogs()
+ purgeWindows()
+
+ runAsync { DialogUtilNew.showQuestion("Do you like mushrooms?") }
+ verifyLog("dialogShown", Severity.INFO).message shouldBe
+ "Question dialog shown: Do you like mushrooms?"
+ getQuestionDialog().clickNo()
+ flushEdt()
+ verifyLog("dialogClosed", Severity.INFO).message shouldBe
+ "Question dialog closed - selected No"
+
+ clearLogs()
+ purgeWindows()
+
+ runAsync { DialogUtilNew.showQuestion("Do you want to delete all data?", true) }
+ verifyLog("dialogShown", Severity.INFO).message shouldBe
+ "Question dialog shown: Do you want to delete all data?"
+ getQuestionDialog().clickCancel()
+ flushEdt()
+ verifyLog("dialogClosed", Severity.INFO).message shouldBe
+ "Question dialog closed - selected Cancel"
+ }
+
+ @Test
+ fun `Should log when showing and dismissing loading dialog`() {
+ DialogUtilNew.showLoadingDialog("One moment...")
+ flushEdt()
+ verifyLog("dialogShown", Severity.INFO).message shouldBe
+ "Loading dialog shown: One moment..."
+
+ DialogUtilNew.dismissLoadingDialog()
+ verifyLog("dialogClosed", Severity.INFO).message shouldBe "Loading dialog closed"
+ }
+
+ @Test
+ fun `Should not log if loading dialog wasn't visible`() {
+ DialogUtilNew.dismissLoadingDialog()
+ verifyNoLogs("dialogClosed")
+ }
+}
diff --git a/client/src/test/kotlin/util/UpdateManagerTest.kt b/client/src/test/kotlin/util/UpdateManagerTest.kt
new file mode 100644
index 0000000..3456f33
--- /dev/null
+++ b/client/src/test/kotlin/util/UpdateManagerTest.kt
@@ -0,0 +1,256 @@
+package util
+
+import bean.LinkLabel
+import com.github.alyssaburlton.swingtest.clickNo
+import com.github.alyssaburlton.swingtest.clickOk
+import com.github.alyssaburlton.swingtest.clickYes
+import com.github.alyssaburlton.swingtest.findWindow
+import com.github.alyssaburlton.swingtest.flushEdt
+import com.github.alyssaburlton.swingtest.getChild
+import com.github.alyssaburlton.swingtest.shouldNotBeVisible
+import io.kotest.matchers.ints.shouldBeGreaterThan
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.string.shouldContain
+import io.kotest.matchers.string.shouldStartWith
+import io.kotest.matchers.types.shouldBeInstanceOf
+import io.mockk.every
+import io.mockk.mockk
+import java.io.File
+import java.io.IOException
+import java.util.concurrent.atomic.AtomicBoolean
+import javax.swing.SwingUtilities
+import kong.unirest.Unirest
+import kong.unirest.UnirestException
+import kong.unirest.json.JSONException
+import kong.unirest.json.JSONObject
+import logging.Severity
+import main.kotlin.testCore.AbstractTest
+import main.kotlin.testCore.getDialogMessage
+import main.kotlin.testCore.getErrorDialog
+import main.kotlin.testCore.getInfoDialog
+import main.kotlin.testCore.getQuestionDialog
+import main.kotlin.testCore.runAsync
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Tag
+import org.junit.jupiter.api.Test
+import screen.LoadingDialog
+import testCore.assertDoesNotExit
+import testCore.assertExits
+
+class UpdateManagerTest : AbstractTest() {
+ @BeforeEach
+ fun beforeEach() {
+ Unirest.config().reset()
+ Unirest.config().connectTimeout(2000)
+ Unirest.config().socketTimeout(2000)
+ }
+
+ /** Communication */
+ @Test
+ @Tag("integration")
+ fun `Should log out an unexpected HTTP response, along with the full JSON payload`() {
+ val errorMessage =
+ queryLatestReleastJsonExpectingError("https://api.github.com/repos/alyssaburlton/foo")
+ errorMessage shouldBe "Failed to check for updates (unable to connect)."
+
+ val log = verifyLog("updateError", Severity.ERROR)
+ log.message shouldBe "Received non-success HTTP status: 404 - Not Found"
+ log.keyValuePairs["responseBody"].toString() shouldContain """"message":"Not Found""""
+
+ findWindow()!!.shouldNotBeVisible()
+ }
+
+ @Test
+ @Tag("integration")
+ fun `Should catch and log any exceptions communicating over HTTPS`() {
+ Unirest.config().connectTimeout(100)
+ Unirest.config().socketTimeout(100)
+
+ val errorMessage = queryLatestReleastJsonExpectingError("https://ww.blargh.zcss.w")
+ errorMessage shouldBe "Failed to check for updates (unable to connect)."
+
+ val errorLog = verifyLog("updateError", Severity.ERROR)
+ errorLog.errorObject.shouldBeInstanceOf()
+
+ findWindow()!!.shouldNotBeVisible()
+ }
+
+ private fun queryLatestReleastJsonExpectingError(repositoryUrl: String): String {
+ val result = runAsync { UpdateManager.queryLatestReleaseJson(repositoryUrl) }
+
+ val error = getErrorDialog()
+ val errorText = error.getDialogMessage()
+
+ error.clickOk()
+ flushEdt()
+
+ result shouldBe null
+
+ return errorText
+ }
+
+ @Test
+ @Tag("integration")
+ fun `Should retrieve a valid latest asset from the remote repo`() {
+ val responseJson =
+ UpdateManager.queryLatestReleaseJson(OnlineConstants.ENTROPY_REPOSITORY_URL)!!
+
+ val version = responseJson.getString("tag_name")
+ version.shouldStartWith("v")
+ responseJson.getJSONArray("assets").length() shouldBeGreaterThan 0
+ }
+
+ /** Parsing */
+ @Test
+ fun `Should parse correctly formed JSON`() {
+ val json =
+ """{
+ "tag_name": "foo",
+ "assets": [
+ {
+ "id": 123456,
+ "name": "Dartzee_v_foo.jar",
+ "size": 1
+ }
+ ]
+ }"""
+
+ val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json))!!
+ metadata.version shouldBe "foo"
+ metadata.assetId shouldBe 123456
+ metadata.fileName shouldBe "Dartzee_v_foo.jar"
+ metadata.size shouldBe 1
+ }
+
+ @Test
+ fun `Should log an error if no tag_name is present`() {
+ val json = "{\"other_tag\":\"foo\"}"
+ val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json))
+ metadata shouldBe null
+
+ val log = verifyLog("parseError", Severity.ERROR)
+ log.errorObject.shouldBeInstanceOf()
+ log.keyValuePairs["responseBody"].toString() shouldBe json
+ }
+
+ @Test
+ fun `Should log an error if no assets are found`() {
+ val json = """{"assets":[],"tag_name":"foo"}"""
+ val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json))
+ metadata shouldBe null
+
+ val log = verifyLog("parseError", Severity.ERROR)
+ log.errorObject.shouldBeInstanceOf()
+ log.keyValuePairs["responseBody"].toString() shouldBe json
+ }
+
+ /** Should update? */
+ @Test
+ fun `Should not proceed with the update if the versions match`() {
+ val metadata =
+ UpdateMetadata(
+ OnlineConstants.ENTROPY_VERSION_NUMBER,
+ 123456,
+ "EntropyLive_x_y.jar",
+ 100
+ )
+
+ UpdateManager.shouldUpdate(OnlineConstants.ENTROPY_VERSION_NUMBER, metadata) shouldBe false
+ val log = verifyLog("updateResult")
+ log.message shouldBe "Up to date"
+ }
+
+ @Test
+ fun `Should show an info and not proceed to auto update if OS is not windows`() {
+ AbstractClient.operatingSystem = "foo"
+
+ val metadata = UpdateMetadata("v100", 123456, "Dartzee_x_y.jar", 100)
+ shouldUpdateAsync(OnlineConstants.ENTROPY_VERSION_NUMBER, metadata).get() shouldBe false
+
+ val log = verifyLog("updateAvailable")
+ log.message shouldBe "Newer release available - v100"
+
+ val info = getInfoDialog()
+ val linkLabel = info.getChild()
+ linkLabel.text shouldBe
+ "${OnlineConstants.ENTROPY_MANUAL_DOWNLOAD_URL}/tag/v100"
+ }
+
+ @Test
+ fun `Should not proceed with the update if user selects 'No'`() {
+ AbstractClient.operatingSystem = "windows"
+
+ val metadata = UpdateMetadata("foo", 123456, "Dartzee_x_y.jar", 100)
+ val result = shouldUpdateAsync("bar", metadata)
+
+ val question = getQuestionDialog()
+ question.getDialogMessage() shouldBe
+ "An update is available (foo). Would you like to download it now?"
+ question.clickNo()
+ flushEdt()
+
+ result.get() shouldBe false
+ }
+
+ @Test
+ fun `Should proceed with the update if user selects 'Yes'`() {
+ AbstractClient.operatingSystem = "windows"
+
+ val metadata = UpdateMetadata("foo", 123456, "Dartzee_x_y.jar", 100)
+ val result = shouldUpdateAsync("bar", metadata)
+
+ val question = getQuestionDialog()
+ question.getDialogMessage() shouldBe
+ "An update is available (foo). Would you like to download it now?"
+ question.clickYes()
+ flushEdt()
+
+ result.get() shouldBe true
+ }
+
+ private fun shouldUpdateAsync(currentVersion: String, metadata: UpdateMetadata): AtomicBoolean {
+ val result = AtomicBoolean(false)
+ SwingUtilities.invokeLater {
+ result.set(UpdateManager.shouldUpdate(currentVersion, metadata))
+ }
+
+ flushEdt()
+ return result
+ }
+
+ /** Prepare batch file */
+ @Test
+ fun `Should overwrite existing batch file with the correct contents`() {
+ val updateFile = File("update.bat")
+ updateFile.writeText("blah")
+
+ UpdateManager.prepareBatchFile()
+
+ updateFile.readText() shouldBe javaClass.getResource("/update/update.bat").readText()
+ updateFile.delete()
+ }
+
+ /** Run update */
+ @Test
+ fun `Should log an error and not exit if batch file goes wrong`() {
+ val runtime = mockk()
+ val error = IOException("Argh")
+ every { runtime.exec(any()) } throws error
+
+ runAsync { assertDoesNotExit { UpdateManager.startUpdate("foo", runtime) } }
+
+ val errorDialog = getErrorDialog()
+ errorDialog.getDialogMessage() shouldBe
+ "Failed to launch update.bat - call the following manually to perform the update: \n\nupdate.bat foo"
+
+ val log = verifyLog("batchError", Severity.ERROR)
+ log.errorObject shouldBe error
+ }
+
+ @Test
+ fun `Should exit normally if batch file succeeds`() {
+ val runtime = mockk(relaxed = true)
+
+ assertExits(0) { UpdateManager.startUpdate("foo", runtime) }
+ }
+}
diff --git a/client/src/test/kotlin/util/UrlUtilTest.kt b/client/src/test/kotlin/util/UrlUtilTest.kt
new file mode 100644
index 0000000..e6625df
--- /dev/null
+++ b/client/src/test/kotlin/util/UrlUtilTest.kt
@@ -0,0 +1,34 @@
+package util
+
+import io.kotest.matchers.shouldBe
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import java.io.IOException
+import logging.Severity
+import main.kotlin.testCore.AbstractTest
+import org.junit.jupiter.api.Test
+
+class UrlUtilTest : AbstractTest() {
+ @Test
+ fun `Should execute the expected command`() {
+ val runtime = mockk(relaxed = true)
+ launchUrl("foo.bar", runtime)
+
+ verify { runtime.exec("xdg-open foo.bar") }
+ }
+
+ @Test
+ fun `Should log an appropriate error if launching the URL fails`() {
+ val error = IOException("Oops")
+
+ val runtime = mockk()
+ every { runtime.exec(any()) } throws error
+
+ launchUrl("foo.bar", runtime)
+
+ val log = verifyLog("urlError", Severity.ERROR)
+ log.message shouldBe "Failed to launch foo.bar"
+ log.errorObject shouldBe error
+ }
+}
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index a766fbf..b5a2465 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -9,4 +9,5 @@ ktfmt { kotlinLangStyle() }
dependencies {
implementation("javax.mail:javax.mail-api:1.6.2")
implementation("javax.activation:activation:1.1.1")
+ testImplementation(project(":test-core"))
}
diff --git a/core/src/main/java/util/AbstractClient.java b/core/src/main/java/util/AbstractClient.java
index 548e9a9..36b9417 100644
--- a/core/src/main/java/util/AbstractClient.java
+++ b/core/src/main/java/util/AbstractClient.java
@@ -76,6 +76,10 @@ public static boolean isAppleOs()
{
return operatingSystem.contains("mac") || operatingSystem.contains("darwin");
}
+ public static boolean isWindowsOs()
+ {
+ return operatingSystem.contains("windows");
+ }
public void checkForUpdatesIfRequired()
{
diff --git a/core/src/main/java/util/Debug.java b/core/src/main/java/util/Debug.java
index 5297dc1..7bb4083 100644
--- a/core/src/main/java/util/Debug.java
+++ b/core/src/main/java/util/Debug.java
@@ -4,29 +4,16 @@
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
-import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
public class Debug implements CoreRegistry
{
- private static final long ERROR_MESSAGE_DELAY_MILLIS = 10000; //10s
-
- private static long lastErrorMillis = -1;
-
private static DebugOutput output = null;
- private static DebugExtension debugExtension = null;
private static boolean logToSystemOut = false;
- private static ThreadFactory loggerFactory = new ThreadFactory()
- {
- @Override
- public Thread newThread(Runnable r)
- {
- return new Thread(r, "Logger");
- }
- };
+ private static ThreadFactory loggerFactory = r -> new Thread(r, "Logger");
private static ExecutorService logService = Executors.newFixedThreadPool(1, loggerFactory);
@@ -117,27 +104,12 @@ public static void stackTrace(Throwable t)
{
stackTrace(t, "");
}
- public static void stackTrace(Throwable t, String message)
- {
- stackTrace(t, message, false);
- }
public static void stackTraceNoError(Throwable t)
{
- stackTrace(t, "", true);
+ stackTrace(t, "");
}
- public static void stackTrace(Throwable t, String message, boolean suppressError)
+ public static void stackTrace(Throwable t, String message)
{
- if (debugExtension != null
- && !suppressError)
- {
- boolean showError = System.currentTimeMillis() - lastErrorMillis > ERROR_MESSAGE_DELAY_MILLIS;
- debugExtension.exceptionCaught(showError);
- if (showError)
- {
- lastErrorMillis = System.currentTimeMillis();
- }
- }
-
String datetime = getCurrentTimeForLogging();
String trace = "";
@@ -178,11 +150,7 @@ public static void initialise(DebugOutput output)
{
Debug.output = output;
}
-
- public static void setDebugExtension(DebugExtension debugExtension)
- {
- Debug.debugExtension = debugExtension;
- }
+
public static void setLogToSystemOut(boolean logToSystemOut)
{
Debug.logToSystemOut = logToSystemOut;
diff --git a/core/src/main/java/util/DebugExtension.java b/core/src/main/java/util/DebugExtension.java
deleted file mode 100644
index a193629..0000000
--- a/core/src/main/java/util/DebugExtension.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package util;
-
-public interface DebugExtension
-{
- public void exceptionCaught(boolean showError);
- public void unableToEmailLogs();
- public void sendEmail(String title, String message) throws Exception;
-}
diff --git a/core/src/main/java/util/FileUtil.java b/core/src/main/java/util/FileUtil.java
index c66c49b..0f1bb79 100644
--- a/core/src/main/java/util/FileUtil.java
+++ b/core/src/main/java/util/FileUtil.java
@@ -24,42 +24,6 @@
public class FileUtil
{
- public static File createNewFile(String filePath, String contents)
- {
- File file = new File(filePath);
- boolean success = false;
-
- try
- {
- success = file.createNewFile();
- }
- catch (IOException ioe)
- {
- Debug.append("Caught " + ioe + " creating file " + filePath);
- }
-
- if (!success)
- {
- return null;
- }
-
- //We have created the empty file, now fill it
- try (FileOutputStream fos = new FileOutputStream(filePath))
- {
- byte[] bytes = contents.getBytes("UTF-8");
- fos.write(bytes);
- }
- catch (IOException ioe)
- {
- Debug.append("Caught " + ioe + " trying to insert bytes into file " + filePath);
- deleteFileIfExists(filePath);
- return null;
- }
-
- Debug.append("Successfully created file " + file);
- return file;
- }
-
public static String getMd5Crc(String filePath)
{
String crc = null;
@@ -97,61 +61,6 @@ public static long getFileSize(String filePath)
return fileSize;
}
- public static boolean deleteFileIfExists(String filePath)
- {
- boolean success = false;
-
- try
- {
- Path path = Paths.get(filePath);
- success = Files.deleteIfExists(path);
- }
- catch (Throwable t)
- {
- Debug.stackTrace(t, "Failed to delete file");
- }
-
- return success;
- }
-
- public static String swapInFile(String oldFilePath, String newFilePath)
- {
- boolean success = true;
-
- File oldFile = new File(oldFilePath);
- String oldFileName = oldFile.getName();
- File newFile = new File(newFilePath);
-
- File zzOldFile = new File(oldFile.getParent(), "zz" + oldFileName);
- if (oldFile.exists()
- && !oldFile.renameTo(zzOldFile))
- {
- return "Failed to rename old out of the way.";
- }
-
- success &= newFile.renameTo(new File(oldFile.getParent(), oldFileName));
- if (!success)
- {
- return "Failed to rename new file to " + oldFileName;
- }
-
- if (zzOldFile.isFile())
- {
- success &= deleteFileIfExists(zzOldFile.getPath());
- }
- else
- {
- success &= deleteDirectoryIfExists(zzOldFile);
- }
-
- if (!success)
- {
- return "Failed to delete zz'd old file: " + oldFile.getPath();
- }
-
- return null;
- }
-
public static void saveTextToFile(String text, Path destinationPath)
{
Charset charset = Charset.forName("US-ASCII");
@@ -170,24 +79,6 @@ public static void saveTextToFile(String text, Path destinationPath)
}
}
- public static String getFileContentsAsString(File file)
- {
- String ret = null;
-
- try
- {
- Path path = file.toPath();
- byte[] bytes = Files.readAllBytes(path);
- ret = new String(bytes, "UTF-8");
- }
- catch (Throwable t)
- {
- Debug.stackTrace(t);
- }
-
- return ret;
- }
-
public static String getBase64DecodedFileContentsAsString(File file)
{
try
@@ -211,196 +102,9 @@ public static void encodeAndSaveToFile(Path destinationPath, String stringToWrit
saveTextToFile(encodedStringToWrite, destinationPath);
}
- public static Dimension getImageDim(String path)
- {
- Dimension result = null;
- String suffix = getFileSuffix(path);
- Iterator iter = ImageIO.getImageReadersBySuffix(suffix);
- if (iter.hasNext())
- {
- ImageReader reader = iter.next();
- try (ImageInputStream stream = new FileImageInputStream(new File(path));)
- {
- reader.setInput(stream);
- int width = reader.getWidth(reader.getMinIndex());
- int height = reader.getHeight(reader.getMinIndex());
- result = new Dimension(width, height);
- }
- catch (IOException e)
- {
- Debug.stackTrace(e);
- }
- finally
- {
- reader.dispose();
- }
- }
- else
- {
- Debug.stackTrace("No reader found for file extension: " + suffix + " (full path: " + path + ")");
- }
-
- return result;
- }
-
- private static String getFileSuffix(String path)
- {
- if (path == null
- || path.lastIndexOf('.') == -1)
- {
- return "";
- }
-
- int dotIndex = path.lastIndexOf('.');
- return path.substring(dotIndex + 1);
- }
-
- /**
- * Helper to create a file object for a URL, e.g. from a classpath resource.
- * No longer used
- */
- /*public static File getForURL(URL url)
- {
- try
- {
- URI uri = url.toURI();
- return new File(uri);
- }
- catch (Throwable t)
- {
- Debug.append("Failed to construct file for URL: " + url);
- Debug.stackTrace(t);
- return null;
- }
- }*/
-
public static String stripFileExtension(String filename)
{
int ix = filename.indexOf('.');
return filename.substring(0, ix);
}
-
- public static byte[] getByteArrayForResource(String resourcePath)
- {
- try (InputStream is = FileUtil.class.getResourceAsStream(resourcePath);
- ByteArrayOutputStream baos = new ByteArrayOutputStream())
- {
- byte[] b = new byte[4096];
- int n = 0;
- while ((n = is.read(b)) != -1)
- {
- baos.write(b, 0, n);
- }
-
- return baos.toByteArray();
- }
- catch (IOException ioe)
- {
- Debug.stackTrace(ioe, "Failed to read classpath resource: " + resourcePath);
- return null;
- }
- }
-
- /**
- * Delete a whole directory, recursively clearing out the files/subfolders too.
- */
- public static boolean deleteDirectoryIfExists(File dir)
- {
- if (!dir.exists()
- || !dir.isDirectory())
- {
- //Just don't do anything
- return true;
- }
-
- boolean success = true;
- File[] files = dir.listFiles();
- for (int i=0; i 0)
- {
- outStream.write(buffer, 0, count);
- }
- }
- catch (SocketException se)
- {
- Debug.append("Caught " + se + " for JAR download.");
- }
- catch (Throwable t)
- {
- Debug.stackTrace(t);
- }
- finally
- {
- server.incrementFunctionsHandled();
- server.incrementFunctionsHandledForMessage("DownloadRequest");
-
- if (socket != null)
- {
- try {socket.close();} catch (Throwable t) {}
- }
- }
- }
-}
diff --git a/server/src/main/java/server/DownloadListener.java b/server/src/main/java/server/DownloadListener.java
deleted file mode 100644
index 6c3919b..0000000
--- a/server/src/main/java/server/DownloadListener.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package server;
-
-import java.net.Socket;
-
-public class DownloadListener extends ListenerRunnable
-{
- String filename = "";
-
- public DownloadListener(EntropyServer server, int port, String filename)
- {
- super(server, port);
-
- this.filename = filename;
- }
-
- @Override
- public void handleInboundMessage(EntropyServer server, Socket clientSocket)
- {
- DownloadHandler.handleDownload(server, clientSocket, filename);
- }
-}
diff --git a/server/src/main/java/server/EntropyServer.java b/server/src/main/java/server/EntropyServer.java
index d7127ff..00f2682 100644
--- a/server/src/main/java/server/EntropyServer.java
+++ b/server/src/main/java/server/EntropyServer.java
@@ -103,7 +103,6 @@ public final class EntropyServer extends JFrame
//Other
private String lastCommand = "";
- private SuperHashMap hmClientJarToVersion = new SuperHashMap<>();
private PrivateKey privateKey = null;
@@ -179,7 +178,6 @@ public static void main(String args[])
//Initialise interfaces etc
EncryptionUtil.setBase64Interface(new Base64Desktop());
- Debug.setDebugExtension(new ServerDebugExtension());
Debug.initialise(console);
//Set other variables on Debug
@@ -223,7 +221,6 @@ private void onStart()
readInPrivateKey();
readUsedKeysFromFile();
- readClientVersion();
registerDefaultRooms();
Debug.append("Starting permanent threads");
@@ -367,14 +364,6 @@ private void startListenerThreads()
Debug.stackTrace(t, "Unable to start listener thread on port " + i);
}
}
-
- startDownloadListener(FILE_NAME_ENTROPY_JAR, SERVER_PORT_NUMBER_DOWNLOAD);
- }
- private void startDownloadListener(String filename, int port)
- {
- DownloadListener listener = new DownloadListener(this, port, filename);
- ServerThread downloadListener = new ServerThread(listener, "DownloadListener-" + port);
- downloadListener.start();
}
private void startFunctionThread()
@@ -857,10 +846,6 @@ public Room getRoomForName(String name)
{
return hmRoomByName.get(name);
}
- public String getClientVersion(String jar)
- {
- return hmClientJarToVersion.get(jar);
- }
public void toggleOnline()
{
online = !online;
@@ -1061,45 +1046,6 @@ private void readUsedKeysFromFile()
}
}
- private void readClientVersion()
- {
- try
- {
- Charset charset = Charset.forName("US-ASCII");
- List jarsAndVersions = Files.readAllLines(FILE_PATH_CLIENT_VERSION, charset);
-
- for (int i=0; i jarAndVersion = StringUtil.getListFromDelims(jarAndVersionStr, ";");
-
- String jar = jarAndVersion.get(0);
- String version = jarAndVersion.get(1);
-
- hmClientJarToVersion.put(jar, version);
- }
-
- logClientVersions();
- }
- catch (Throwable t)
- {
- Debug.append("Caught " + t + " trying to read in client version");
- }
- }
- private void logClientVersions()
- {
- Iterator> it = hmClientJarToVersion.entrySet().iterator();
- for (; it.hasNext(); )
- {
- Map.Entry jarAndVersion = it.next();
-
- String jar = jarAndVersion.getKey();
- String version = jarAndVersion.getValue();
-
- Debug.append(FileUtil.stripFileExtension(jar) + " Version: " + version);
- }
- }
-
public ConcurrentHashMap getBlacklist()
{
return blacklist;
@@ -1415,11 +1361,6 @@ else if (command.startsWith(COMMAND_NOTIFY_USER))
else if (command.equals(COMMAND_SERVER_VERSION))
{
Debug.append("Server version: " + OnlineConstants.SERVER_VERSION);
- logClientVersions();
- }
- else if (command.equals(COMMAND_SERVER_RESET_CLIENT_VERSION))
- {
- readClientVersion();
}
else
{
@@ -1480,7 +1421,6 @@ private void printCommands()
Debug.appendWithoutDate(COMMAND_NOTIFICATION_LOGGING);
Debug.appendWithoutDate(COMMAND_NOTIFY_USER + "");
Debug.appendWithoutDate(COMMAND_SERVER_VERSION);
- Debug.appendWithoutDate(COMMAND_SERVER_RESET_CLIENT_VERSION);
}
private void dumpMessageStats()
diff --git a/server/src/main/java/server/MessageHandlerRunnable.java b/server/src/main/java/server/MessageHandlerRunnable.java
index 76659a2..397f6db 100644
--- a/server/src/main/java/server/MessageHandlerRunnable.java
+++ b/server/src/main/java/server/MessageHandlerRunnable.java
@@ -101,8 +101,7 @@ public void run()
Debug.appendWithoutDate("Response: " + responseStr);
}
- if (symmetricKey != null
- && !DownloadHandler.isDownloadMessage(name))
+ if (symmetricKey != null)
{
responseStr = EncryptionUtil.encrypt(responseStr, symmetricKey);
}
@@ -225,15 +224,6 @@ private Document handleUnencryptedMessage(Document message)
Element root = message.getDocumentElement();
String name = root.getNodeName();
- if (DownloadHandler.isDownloadMessage(name))
- {
- return DownloadHandler.processMessage(message, server);
- }
-
- if (name.equals(ROOT_TAG_CLIENT_MAIL))
- {
- return EntropyEmailUtil.handleClientMail(server, root);
- }
if (!name.equals(ROOT_TAG_NEW_SYMMETRIC_KEY))
{
diff --git a/server/src/main/java/server/ServerCommands.java b/server/src/main/java/server/ServerCommands.java
index a427ddd..90601f6 100644
--- a/server/src/main/java/server/ServerCommands.java
+++ b/server/src/main/java/server/ServerCommands.java
@@ -34,5 +34,4 @@ public interface ServerCommands
public static final String COMMAND_NOTIFICATION_LOGGING = "notification logging";
public static final String COMMAND_NOTIFY_USER = "notify ";
public static final String COMMAND_SERVER_VERSION = "version";
- public static final String COMMAND_SERVER_RESET_CLIENT_VERSION = "reset version";
}
diff --git a/server/src/main/java/util/EntropyEmailUtil.java b/server/src/main/java/util/EntropyEmailUtil.java
index 33cf234..813cd38 100644
--- a/server/src/main/java/util/EntropyEmailUtil.java
+++ b/server/src/main/java/util/EntropyEmailUtil.java
@@ -1,38 +1,12 @@
package util;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.util.ArrayList;
-
-import javax.crypto.SecretKey;
import javax.mail.MessagingException;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.NodeList;
-
-import server.EntropyServer;
-
public class EntropyEmailUtil
{
- public static final String EMAIL_ADDRESS_ENTROPY = "entropyDebug@gmail.com";
-
- private static final String ENTROPY_USERNAME = "entropyDebug";
private static final String NO_REPLY_USERNAME = "entropynoreply";
- private static final String ENTROPY_PASSWORD = "****";
private static final String NO_REPLY_PASSWORD = "****";
-
- /**
- * Send an email via the usual EntropyDebug account
- */
- public static void sendEmail(String title, String message) throws MessagingException
- {
- sendEmail(title, message, null);
- }
- public static void sendEmail(String title, String message, ArrayList attachments) throws MessagingException
- {
- EmailUtil.sendEmail(title, message, EMAIL_ADDRESS_ENTROPY, ENTROPY_USERNAME, ENTROPY_PASSWORD, attachments);
- }
+
/**
* Send an email from EntropyNoReply, e.g. a password reset
@@ -41,99 +15,4 @@ public static void sendEmailNoReply(String title, String message, String targetE
{
EmailUtil.sendEmail(title, message, targetEmail, NO_REPLY_USERNAME, NO_REPLY_PASSWORD, null);
}
-
- /**
- * Handle a client mail message
- */
- public static Document handleClientMail(EntropyServer server, Element rootElement)
- {
- String encryptedSymetricKeyStr = rootElement.getAttribute("EncryptedKey");
- String symmetricKeyStr = EncryptionUtil.decrypt(encryptedSymetricKeyStr, server.getPrivateKey(), true);
- SecretKey symmetricKey = EncryptionUtil.reconstructKeyFromString(symmetricKeyStr);
-
- if (symmetricKey == null)
- {
- //Want to know about this. I've seen a client passing up a blank tag for EncryptedKey, and I don't
- //know how that's possible.
- Debug.stackTrace("Failed to reconstruct symmetricKey for EncryptedKey attribute");
- Debug.append("EncryptedKeyStr: " + encryptedSymetricKeyStr);
- Debug.append("SymmetricKeyStr: " + symmetricKeyStr);
- }
-
- String subject = EncryptionUtil.decryptIfPossible(rootElement.getAttribute("Subject"), symmetricKey);
- String body = EncryptionUtil.decryptIfPossible(rootElement.getAttribute("Body"), symmetricKey);
-
- ArrayList attachments = new ArrayList<>();
- NodeList attachmentElements = rootElement.getElementsByTagName("Attachment");
- for (int i=0; i Unit) {
+ val originalSecurityManager = System.getSecurityManager()
+ System.setSecurityManager(NoExitSecurityManager(originalSecurityManager))
+
+ try {
+ fn()
+ fail("Expected exitProcess($expectedStatus), but it wasn't called")
+ } catch (e: ExitException) {
+ e.status shouldBe expectedStatus
+ } finally {
+ System.setSecurityManager(originalSecurityManager)
+ }
+}
+
+fun assertDoesNotExit(fn: () -> Unit) {
+ val originalSecurityManager = System.getSecurityManager()
+ System.setSecurityManager(NoExitSecurityManager(originalSecurityManager))
+
+ try {
+ fn()
+ } catch (e: ExitException) {
+ fail("Called exitProcess(${e.status})")
+ } finally {
+ System.setSecurityManager(originalSecurityManager)
+ }
+}
diff --git a/core/src/test/kotlin/helper/FakeLogDestination.kt b/test-core/src/main/kotlin/testCore/FakeLogDestination.kt
similarity index 92%
rename from core/src/test/kotlin/helper/FakeLogDestination.kt
rename to test-core/src/main/kotlin/testCore/FakeLogDestination.kt
index ab75c8c..7b5c0cf 100644
--- a/core/src/test/kotlin/helper/FakeLogDestination.kt
+++ b/test-core/src/main/kotlin/testCore/FakeLogDestination.kt
@@ -1,4 +1,4 @@
-package helper
+package main.kotlin.testCore
import logging.ILogDestination
import logging.LogRecord
diff --git a/core/src/test/kotlin/helper/TestConstants.kt b/test-core/src/main/kotlin/testCore/TestConstants.kt
similarity index 92%
rename from core/src/test/kotlin/helper/TestConstants.kt
rename to test-core/src/main/kotlin/testCore/TestConstants.kt
index a69de53..6db1008 100644
--- a/core/src/test/kotlin/helper/TestConstants.kt
+++ b/test-core/src/main/kotlin/testCore/TestConstants.kt
@@ -1,3 +1,5 @@
+package main.kotlin.testCore
+
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
diff --git a/core/src/test/kotlin/helper/TestUtils.kt b/test-core/src/main/kotlin/testCore/TestUtils.kt
similarity index 61%
rename from core/src/test/kotlin/helper/TestUtils.kt
rename to test-core/src/main/kotlin/testCore/TestUtils.kt
index fca301b..e628a9c 100644
--- a/core/src/test/kotlin/helper/TestUtils.kt
+++ b/test-core/src/main/kotlin/testCore/TestUtils.kt
@@ -1,10 +1,15 @@
-package helper
+package main.kotlin.testCore
-import CURRENT_TIME
+import com.github.alyssaburlton.swingtest.findAll
+import com.github.alyssaburlton.swingtest.findWindow
+import com.github.alyssaburlton.swingtest.flushEdt
import io.kotest.matchers.maps.shouldContainExactly
import java.awt.Component
import java.awt.Container
import java.time.Instant
+import javax.swing.JDialog
+import javax.swing.JLabel
+import javax.swing.SwingUtilities
import logging.LogRecord
import logging.Severity
@@ -47,3 +52,24 @@ fun addComponents(ret: MutableList, components: Array, desired
}
}
}
+
+fun getInfoDialog() = getOptionPaneDialog("Information")
+
+fun getQuestionDialog() = getOptionPaneDialog("Question")
+
+fun getErrorDialog() = getOptionPaneDialog("Error")
+
+private fun getOptionPaneDialog(title: String) = findWindow { it.title == title }!!
+
+fun JDialog.getDialogMessage(): String {
+ val messageLabels = findAll().filter { it.name == "OptionPane.label" }
+ return messageLabels.joinToString("\n\n") { it.text }
+}
+
+fun runAsync(block: () -> T?): T? {
+ var result: T? = null
+ SwingUtilities.invokeLater { result = block() }
+
+ flushEdt()
+ return result
+}