From 2623180ba714319de7341009c28110e6fc1e4162 Mon Sep 17 00:00:00 2001 From: Tony Germano Date: Tue, 3 Nov 2026 13:21:52 -0500 Subject: [PATCH 1/2] Replace OSXAdapter with java.awt.Desktop handlers OSXAdapter is Apple's sample adapter, carried under Apple's own license header, that installs the macOS About and Quit handlers by reflection against com.apple.eawt.Application. Its pre-Java-9 path implements com.apple.eawt.ApplicationListener, which JEP 272 removed, and its Java 9+ path only works because the client launcher passes --add-opens java.desktop/com.apple.eawt. Both reach the same java.awt.desktop handlers that java.awt.Desktop has exposed publicly since Java 9. Register the two handlers through Desktop directly, guarded by isSupported so the IS_OS_MAC check is no longer needed; Linux and Windows report both actions unsupported and keep their Help menu and window close behavior. The quit handler keeps the old contract: Frame.logout(true) does not call System.exit, so it calls performQuit when quitMac returns true and cancelQuit otherwise. Delete OSXAdapter.java and drop the --add-opens for com.apple.eawt, which existed only for it, from the JNLP template, its test, and the devClient task. Assisted-by: Claude:claude-fable-5-1 Signed-off-by: Tony Germano --- client/build.gradle | 1 - .../com/mirth/connect/client/ui/Mirth.java | 17 +- .../mirth/connect/client/ui/OSXAdapter.java | 302 ------------------ server/mirth-client.jnlp | 2 +- .../server/servlets/WebStartServletTest.java | 2 +- 5 files changed, 16 insertions(+), 308 deletions(-) delete mode 100644 client/src/main/java/com/mirth/connect/client/ui/OSXAdapter.java diff --git a/client/build.gradle b/client/build.gradle index a3ffca4acc..4389f8690b 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -129,7 +129,6 @@ tasks.register('devClient', JavaExec) { '--add-opens=java.base/sun.security.pkcs=ALL-UNNAMED', '--add-opens=java.base/sun.security.rsa=ALL-UNNAMED', '--add-opens=java.base/sun.security.x509=ALL-UNNAMED', - '--add-opens=java.desktop/com.apple.eawt=ALL-UNNAMED', '--add-opens=java.desktop/com.apple.eio=ALL-UNNAMED', '--add-opens=java.desktop/java.awt=ALL-UNNAMED', '--add-opens=java.desktop/java.awt.color=ALL-UNNAMED', diff --git a/client/src/main/java/com/mirth/connect/client/ui/Mirth.java b/client/src/main/java/com/mirth/connect/client/ui/Mirth.java index 0145252b6f..0dca370135 100644 --- a/client/src/main/java/com/mirth/connect/client/ui/Mirth.java +++ b/client/src/main/java/com/mirth/connect/client/ui/Mirth.java @@ -175,9 +175,20 @@ public static void initUIManager() { createAlternateKeyBindings(); } - if (SystemUtils.IS_OS_MAC) { - OSXAdapter.setAboutHandler(Mirth.class, Mirth.class.getDeclaredMethod("aboutMac", (Class[]) null)); - OSXAdapter.setQuitHandler(Mirth.class, Mirth.class.getDeclaredMethod("quitMac", (Class[]) null)); + if (Desktop.isDesktopSupported()) { + Desktop desktop = Desktop.getDesktop(); + if (desktop.isSupported(Desktop.Action.APP_ABOUT)) { + desktop.setAboutHandler(e -> aboutMac()); + } + if (desktop.isSupported(Desktop.Action.APP_QUIT_HANDLER)) { + desktop.setQuitHandler((e, response) -> { + if (quitMac()) { + response.performQuit(); + } else { + response.cancelQuit(); + } + }); + } } } catch (Exception e) { LogManager.getLogger(Mirth.class).error("Error initializing look and feel", e); diff --git a/client/src/main/java/com/mirth/connect/client/ui/OSXAdapter.java b/client/src/main/java/com/mirth/connect/client/ui/OSXAdapter.java deleted file mode 100644 index 403ab3f3e0..0000000000 --- a/client/src/main/java/com/mirth/connect/client/ui/OSXAdapter.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * - * File: OSXAdapter.java - * - * Abstract: Hooks existing preferences/about/quit functionality from an existing Java app into - * handlers for the Mac OS X application menu. Uses a Proxy object to dynamically implement the - * com.apple.eawt.ApplicationListener interface and register it with the com.apple.eawt.Application - * object. This allows the complete project to be both built and run on any platform without any - * stubs or placeholders. Useful for developers looking to implement Mac OS X features while - * supporting multiple platforms with minimal impact. - * - * Version: 2.0 - * - * Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in - * consideration of your agreement to the following terms, and your use, installation, modification - * or redistribution of this Apple software constitutes acceptance of these terms. If you do not - * agree with these terms, please do not use, install, modify or redistribute this Apple software. - * - * In consideration of your agreement to abide by the following terms, and subject to these terms, - * Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original - * Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple - * Software, with or without modifications, in source and/or binary forms; provided that if you - * redistribute the Apple Software in its entirety and without modifications, you must retain this - * notice and the following text and disclaimers in all such redistributions of the Apple Software. - * Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or - * promote products derived from the Apple Software without specific prior written permission from - * Apple. Except as expressly stated in this notice, no other rights or licenses, express or - * implied, are granted by Apple herein, including but not limited to any patent rights that may be - * infringed by your derivative works or by other works in which the Apple Software may be - * incorporated. - * - * The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS - * OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, - * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND - * OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. - * - * IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, - * MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY - * OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * Copyright (c) 2003-2007 Apple, Inc., All Rights Reserved - * - */ - -package com.mirth.connect.client.ui; - -import java.io.File; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.security.AccessController; -import java.util.List; - -import org.apache.commons.lang3.math.NumberUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.glassfish.jersey.internal.util.ReflectionHelper; - -public class OSXAdapter implements InvocationHandler { - private static Logger logger = LogManager.getLogger(OSXAdapter.class); - - protected Object targetObject; - protected Method targetMethod; - protected String proxySignature; - - static Object macOSXApplication; - - // Pass this method an Object and Method equipped to perform application shutdown logic - // The method passed should return a boolean stating whether or not the quit should occur - public static void setQuitHandler(final Object target, final Method quitHandler) { - try { - if (isJava9OrGreater()) { - InvocationHandler invocationHandler = new InvocationHandler() { - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - if (method.getName().equals("handleQuitRequestWith")) { - boolean quit = (boolean) quitHandler.invoke(target); - Object quitResponse = args[1]; - quitResponse.getClass().getMethod(quit ? "performQuit" : "cancelQuit").invoke(quitResponse); - } - return null; - } - }; - - setHandler(invocationHandler, "java.awt.desktop.QuitHandler", "setQuitHandler"); - } else { - setHandler(new OSXAdapter("handleQuit", target, quitHandler)); - } - } catch (Exception ex) { - logger.error("OSXAdapter could not set quit handler", ex); - } - } - - // Pass this method an Object and Method equipped to display application info - // They will be called when the About menu item is selected from the application menu - public static void setAboutHandler(Object target, Method aboutHandler) { - boolean enableAboutMenu = (target != null && aboutHandler != null); - - try { - if (isJava9OrGreater()) { - if (enableAboutMenu) { - setHandler(new OSXAdapter("handleAbout", target, aboutHandler), "java.awt.desktop.AboutHandler", "setAboutHandler"); - } - } else { - if (enableAboutMenu) { - setHandler(new OSXAdapter("handleAbout", target, aboutHandler)); - } - // If we're setting a handler, enable the About menu item by calling - // com.apple.eawt.Application reflectively - Method enableAboutMethod = macOSXApplication.getClass().getDeclaredMethod("setEnabledAboutMenu", new Class[] { - boolean.class }); - enableAboutMethod.invoke(macOSXApplication, new Object[] { - Boolean.valueOf(enableAboutMenu) }); - } - } catch (Exception ex) { - logger.error("OSXAdapter could not access the About Menu", ex); - } - } - - // Pass this method an Object and a Method equipped to display application options - // They will be called when the Preferences menu item is selected from the application menu - public static void setPreferencesHandler(Object target, Method prefsHandler) { - boolean enablePrefsMenu = (target != null && prefsHandler != null); - try { - if (isJava9OrGreater()) { - if (enablePrefsMenu) { - setHandler(new OSXAdapter("handlePreferences", target, prefsHandler), "java.awt.desktop.PreferencesHandler", "setPreferencesHandler"); - } - } else { - if (enablePrefsMenu) { - setHandler(new OSXAdapter("handlePreferences", target, prefsHandler)); - } - // If we're setting a handler, enable the Preferences menu item by calling - // com.apple.eawt.Application reflectively - Method enablePrefsMethod = macOSXApplication.getClass().getDeclaredMethod("setEnabledPreferencesMenu", new Class[] { - boolean.class }); - enablePrefsMethod.invoke(macOSXApplication, new Object[] { - Boolean.valueOf(enablePrefsMenu) }); - } - } catch (Exception ex) { - logger.error("OSXAdapter could not set preferences handler", ex); - } - } - - // Pass this method an Object and a Method equipped to handle document events from the Finder - // Documents are registered with the Finder via the CFBundleDocumentTypes dictionary in the - // application bundle's Info.plist - public static void setFileHandler(final Object target, final Method fileHandler) { - try { - if (isJava9OrGreater()) { - InvocationHandler invocationHandler = new InvocationHandler() { - @Override - @SuppressWarnings("unchecked") - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - if (method.getName().equals("openFiles")) { - Object event = args[0]; - List files = (List) event.getClass().getMethod("getFiles").invoke(event); - String searchTerm = (String) event.getClass().getMethod("getSearchTerm").invoke(event); - fileHandler.invoke(target, files, searchTerm); - } - return null; - } - }; - - setHandler(invocationHandler, "java.awt.desktop.OpenFilesHandler", "setOpenFileHandler"); - } else { - setHandler(new OSXAdapter("handleOpenFile", target, fileHandler) { - // Override OSXAdapter.callTarget to send information on the - // file to be opened - public boolean callTarget(Object appleEvent) { - if (appleEvent != null) { - try { - Method getFilenameMethod = appleEvent.getClass().getDeclaredMethod("getFilename", (Class[]) null); - String filename = (String) getFilenameMethod.invoke(appleEvent, (Object[]) null); - this.targetMethod.invoke(this.targetObject, new Object[] { - filename }); - } catch (Exception ex) { - - } - } - return true; - } - }); - } - } catch (Exception ex) { - logger.error("OSXAdapter could not set open files handler", ex); - } - } - - // setHandler creates a Proxy object from the passed OSXAdapter and adds it as an ApplicationListener - public static void setHandler(OSXAdapter adapter) { - try { - Object application = getApplication(); - Class applicationListenerClass = Class.forName("com.apple.eawt.ApplicationListener"); - Method addListenerMethod = application.getClass().getDeclaredMethod("addApplicationListener", new Class[] { - applicationListenerClass }); - // Create a proxy object around this handler that can be reflectively added as an Apple ApplicationListener - Object osxAdapterProxy = Proxy.newProxyInstance(OSXAdapter.class.getClassLoader(), new Class[] { - applicationListenerClass }, adapter); - addListenerMethod.invoke(application, new Object[] { osxAdapterProxy }); - } catch (ClassNotFoundException cnfe) { - System.err.println("This version of Mac OS X does not support the Apple EAWT. ApplicationEvent handling has been disabled (" + cnfe + ")"); - } catch (Exception ex) { // Likely a NoSuchMethodException or an IllegalAccessException loading/invoking eawt.Application methods - logger.error("Mac OS X Adapter could not talk to EAWT", ex); - } - } - - // Each OSXAdapter has the name of the EAWT method it intends to listen for (handleAbout, for example), - // the Object that will ultimately perform the task, and the Method to be called on that Object - protected OSXAdapter(String proxySignature, Object target, Method handler) { - this.proxySignature = proxySignature; - this.targetObject = target; - this.targetMethod = handler; - } - - // Override this method to perform any operations on the event - // that comes with the various callbacks - // See setFileHandler above for an example - public boolean callTarget(Object appleEvent) throws InvocationTargetException, IllegalAccessException { - Object result = targetMethod.invoke(targetObject, (Object[]) null); - if (result == null) { - return true; - } - return Boolean.valueOf(result.toString()).booleanValue(); - } - - // InvocationHandler implementation - // This is the entry point for our proxy object; it is called every time an ApplicationListener method is invoked - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - if (isCorrectMethod(method, args)) { - boolean handled = callTarget(args[0]); - setApplicationEventHandled(args[0], handled); - } - // All of the ApplicationListener methods are void; return null regardless of what happens - return null; - } - - // Compare the method that was called to the intended method when the OSXAdapter instance was created - // (e.g. handleAbout, handleQuit, handleOpenFile, etc.) - protected boolean isCorrectMethod(Method method, Object[] args) { - return (targetMethod != null && proxySignature.equals(method.getName()) && args.length == 1); - } - - // It is important to mark the ApplicationEvent as handled and cancel the default behavior - // This method checks for a boolean result from the proxy method and sets the event accordingly - protected void setApplicationEventHandled(Object event, boolean handled) { - if (event != null && !isJava9OrGreater()) { - try { - Method setHandledMethod = event.getClass().getDeclaredMethod("setHandled", new Class[] { - boolean.class }); - // If the target method returns a boolean, use that as a hint - setHandledMethod.invoke(event, new Object[] { Boolean.valueOf(handled) }); - } catch (Exception ex) { - logger.error("OSXAdapter was unable to handle an ApplicationEvent: " + event, ex); - } - } - } - - @SuppressWarnings("unchecked") - protected static Object getApplication() throws Exception { - if (macOSXApplication == null) { - Class applicationClass = Class.forName("com.apple.eawt.Application"); - - if (isJava9OrGreater()) { - macOSXApplication = applicationClass.getMethod("getApplication").invoke(null); - } else { - macOSXApplication = applicationClass.getConstructor((Class[]) null).newInstance((Object[]) null); - } - } - - return macOSXApplication; - } - - @SuppressWarnings("removal") - protected static void setHandler(InvocationHandler adapter, String interfaceName, String applicationSetter) throws Exception { - Class handlerInterface = Class.forName(interfaceName); - Object handlerImpl = Proxy.newProxyInstance(AccessController.doPrivileged(ReflectionHelper.getClassLoaderPA(handlerInterface)), new Class[] { - handlerInterface }, adapter); - Object application = getApplication(); - application.getClass().getMethod(applicationSetter, handlerInterface).invoke(application, handlerImpl); - } - - protected static boolean isJava9OrGreater() { - String version = System.getProperty("java.version"); - - int index = version.indexOf('-'); - if (index > 0) { - version = version.substring(0, index); - } - - index = version.indexOf('.'); - if (index > 0) { - version = version.substring(0, index); - } - - return NumberUtils.toDouble(version) >= 9; - } -} \ No newline at end of file diff --git a/server/mirth-client.jnlp b/server/mirth-client.jnlp index 21ebd24c74..fcfe433f48 100644 --- a/server/mirth-client.jnlp +++ b/server/mirth-client.jnlp @@ -26,7 +26,7 @@ - + diff --git a/server/src/test/java/com/mirth/connect/server/servlets/WebStartServletTest.java b/server/src/test/java/com/mirth/connect/server/servlets/WebStartServletTest.java index 25945adcbf..dde4a262fa 100644 --- a/server/src/test/java/com/mirth/connect/server/servlets/WebStartServletTest.java +++ b/server/src/test/java/com/mirth/connect/server/servlets/WebStartServletTest.java @@ -462,7 +462,7 @@ private static String normalizeWhitespace(String input) { + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + " \n" From 3b73238d03065b50a6ef2f95a0aca72e14e433c3 Mon Sep 17 00:00:00 2001 From: Tony Germano Date: Tue, 3 Nov 2026 09:29:07 -0500 Subject: [PATCH 2/2] Drop the macOS and Windows fallbacks in BareBonesBrowserLaunch openURL located java.awt.Desktop by reflection and, when that failed, fell back to com.apple.eio.FileManager on macOS or rundll32 on Windows. Java 17 is the baseline, so Desktop is always present, and on macOS and Windows it is always supported on a desktop JVM, so neither branch could be reached. Call Desktop.browse directly and keep only the Unix browser search, which still matters on Linux desktops without the GNOME libraries that Desktop needs. Drop the --add-opens for com.apple.eio from the JNLP template, its test, and the devClient task. Assisted-by: Claude:claude-fable-5-1 Signed-off-by: Tony Germano --- client/build.gradle | 1 - .../client/ui/BareBonesBrowserLaunch.java | 36 +++++++------------ server/mirth-client.jnlp | 2 +- .../server/servlets/WebStartServletTest.java | 2 +- 4 files changed, 14 insertions(+), 27 deletions(-) diff --git a/client/build.gradle b/client/build.gradle index 4389f8690b..bcb71aff13 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -129,7 +129,6 @@ tasks.register('devClient', JavaExec) { '--add-opens=java.base/sun.security.pkcs=ALL-UNNAMED', '--add-opens=java.base/sun.security.rsa=ALL-UNNAMED', '--add-opens=java.base/sun.security.x509=ALL-UNNAMED', - '--add-opens=java.desktop/com.apple.eio=ALL-UNNAMED', '--add-opens=java.desktop/java.awt=ALL-UNNAMED', '--add-opens=java.desktop/java.awt.color=ALL-UNNAMED', '--add-opens=java.desktop/java.awt.font=ALL-UNNAMED', diff --git a/client/src/main/java/com/mirth/connect/client/ui/BareBonesBrowserLaunch.java b/client/src/main/java/com/mirth/connect/client/ui/BareBonesBrowserLaunch.java index a45efb7e79..f171475c63 100644 --- a/client/src/main/java/com/mirth/connect/client/ui/BareBonesBrowserLaunch.java +++ b/client/src/main/java/com/mirth/connect/client/ui/BareBonesBrowserLaunch.java @@ -4,6 +4,8 @@ package com.mirth.connect.client.ui; +import java.awt.Desktop; +import java.net.URI; import java.util.Arrays; /** @@ -40,31 +42,17 @@ public class BareBonesBrowserLaunch { * "https://dna-engine.org/") */ public static void openURL(String url) { - try { // attempt to use Desktop library from JDK 1.6+ - Class d = Class.forName("java.awt.Desktop"); - d.getDeclaredMethod("browse", - new Class[] { java.net.URI.class }).invoke( - d.getDeclaredMethod("getDesktop").invoke(null), - new Object[] { java.net.URI.create(url) }); - } catch (Exception ignore) { // library not available or failed - String osName = System.getProperty("os.name"); + try { + Desktop.getDesktop().browse(URI.create(url)); + } catch (Exception ignore) { // Desktop not supported or failed, assume Unix or Linux try { - if (osName.startsWith("Mac OS")) { - Class.forName("com.apple.eio.FileManager").getDeclaredMethod( - "openURL", new Class[] { String.class }).invoke(null, - new Object[] { url }); - } else if (osName.startsWith("Windows")) - Runtime.getRuntime().exec(new String[] { - "rundll32", "url.dll,FileProtocolHandler", url }); - else { // assume Unix or Linux - String browser = null; - for (String b : browsers) - if (browser == null - && Runtime.getRuntime().exec(new String[] { "which", b }).getInputStream().read() != -1) - Runtime.getRuntime().exec(new String[] { browser = b, url }); - if (browser == null) - throw new Exception(Arrays.toString(browsers)); - } + String browser = null; + for (String b : browsers) + if (browser == null + && Runtime.getRuntime().exec(new String[] { "which", b }).getInputStream().read() != -1) + Runtime.getRuntime().exec(new String[] { browser = b, url }); + if (browser == null) + throw new Exception(Arrays.toString(browsers)); } catch (Exception e) { PlatformUI.MIRTH_FRAME.alertThrowable(PlatformUI.MIRTH_FRAME, e); } diff --git a/server/mirth-client.jnlp b/server/mirth-client.jnlp index fcfe433f48..4a5754db37 100644 --- a/server/mirth-client.jnlp +++ b/server/mirth-client.jnlp @@ -26,7 +26,7 @@ - + diff --git a/server/src/test/java/com/mirth/connect/server/servlets/WebStartServletTest.java b/server/src/test/java/com/mirth/connect/server/servlets/WebStartServletTest.java index dde4a262fa..42d40e02d2 100644 --- a/server/src/test/java/com/mirth/connect/server/servlets/WebStartServletTest.java +++ b/server/src/test/java/com/mirth/connect/server/servlets/WebStartServletTest.java @@ -462,7 +462,7 @@ private static String normalizeWhitespace(String input) { + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + " \n"