From 70142b44384e2d9ec3d05f1bb25a60491e3e20ec Mon Sep 17 00:00:00 2001 From: Harshitha Onkar Date: Wed, 24 Sep 2025 18:05:45 +0000 Subject: [PATCH] 8359501: Enhance Handling of URIs Reviewed-by: rhalade, ahgross, azvegint, prr --- .../sun/lwawt/macosx/CDesktopPeer.java | 57 ++++++-- .../native/libawt_lwawt/awt/CDesktopPeer.m | 124 ++++++++++++++---- .../classes/sun/awt/windows/WDesktopPeer.java | 53 +++++++- .../native/libawt/windows/awt_Desktop.cpp | 49 ++++++- test/jdk/java/awt/Desktop/BrowseTest.java | 26 +++- .../EditAndPrintTest/EditAndPrintTest.java | 2 +- 6 files changed, 258 insertions(+), 53 deletions(-) diff --git a/src/java.desktop/macosx/classes/sun/lwawt/macosx/CDesktopPeer.java b/src/java.desktop/macosx/classes/sun/lwawt/macosx/CDesktopPeer.java index a4ec0767298..cc0e253f23b 100644 --- a/src/java.desktop/macosx/classes/sun/lwawt/macosx/CDesktopPeer.java +++ b/src/java.desktop/macosx/classes/sun/lwawt/macosx/CDesktopPeer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,7 +34,10 @@ import java.awt.peer.DesktopPeer; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.lang.annotation.Native; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; /** @@ -44,6 +47,12 @@ import java.net.URI; */ public final class CDesktopPeer implements DesktopPeer { + @Native private static final int OPEN = 0; + @Native private static final int BROWSE = 1; + @Native private static final int EDIT = 2; + @Native private static final int PRINT = 3; + @Native private static final int MAIL = 4; + @Override public boolean isSupported(Action action) { return true; @@ -51,27 +60,27 @@ public final class CDesktopPeer implements DesktopPeer { @Override public void open(File file) throws IOException { - this.lsOpenFile(file, false); + this.lsOpenFile(file, OPEN); } @Override public void edit(File file) throws IOException { - this.lsOpenFile(file, false); + this.lsOpenFile(file, EDIT); } @Override public void print(File file) throws IOException { - this.lsOpenFile(file, true); + this.lsOpenFile(file, PRINT); } @Override public void mail(URI uri) throws IOException { - this.lsOpen(uri); + this.lsOpen(uri, MAIL); } @Override public void browse(URI uri) throws IOException { - this.lsOpen(uri); + this.lsOpen(uri, BROWSE); } @Override @@ -162,24 +171,44 @@ public final class CDesktopPeer implements DesktopPeer { } } - private void lsOpen(URI uri) throws IOException { - int status = _lsOpenURI(uri.toString()); + private void lsOpen(URI uri, int action) throws IOException { + int status = _lsOpenURI(uri.toString(), action); if (status != 0 /* noErr */) { - throw new IOException("Failed to mail or browse " + uri + ". Error code: " + status); + String actionString = (action == MAIL) ? "mail" : "browse"; + throw new IOException("Failed to " + actionString + " " + uri + + ". Error code: " + status); } } - private void lsOpenFile(File file, boolean print) throws IOException { - int status = _lsOpenFile(file.getCanonicalPath(), print); + private void lsOpenFile(File file, int action) throws IOException { + int status = -1; + Path tmpFile = null; + String tmpTxtPath = null; + try { + if (action == EDIT) { + tmpFile = Files.createTempFile("TmpFile", ".txt"); + tmpTxtPath = tmpFile.toAbsolutePath().toString(); + } + status = _lsOpenFile(file.getCanonicalPath(), action, tmpTxtPath); + } catch (Exception e) { + throw new IOException("Failed to create tmp file: ", e); + } finally { + if (tmpFile != null) { + Files.deleteIfExists(tmpFile); + } + } if (status != 0 /* noErr */) { - throw new IOException("Failed to open, edit or print " + file + ". Error code: " + status); + String actionString = (action == OPEN) ? "open" + : (action == EDIT) ? "edit" : "print"; + throw new IOException("Failed to " + actionString + " " + file + + ". Error code: " + status); } } - private static native int _lsOpenURI(String uri); + private static native int _lsOpenURI(String uri, int action); - private static native int _lsOpenFile(String path, boolean print); + private static native int _lsOpenFile(String path, int action, String tmpTxtPath); } diff --git a/src/java.desktop/macosx/native/libawt_lwawt/awt/CDesktopPeer.m b/src/java.desktop/macosx/native/libawt_lwawt/awt/CDesktopPeer.m index 7555c7990c4..e1841c9398c 100644 --- a/src/java.desktop/macosx/native/libawt_lwawt/awt/CDesktopPeer.m +++ b/src/java.desktop/macosx/native/libawt_lwawt/awt/CDesktopPeer.m @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,27 +27,60 @@ #import "JNIUtilities.h" #import #import +#import "sun_lwawt_macosx_CDesktopPeer.h" /* * Class: sun_lwawt_macosx_CDesktopPeer * Method: _lsOpenURI - * Signature: (Ljava/lang/String;)I; + * Signature: (Ljava/lang/String;I)I */ JNIEXPORT jint JNICALL Java_sun_lwawt_macosx_CDesktopPeer__1lsOpenURI -(JNIEnv *env, jclass clz, jstring uri) +(JNIEnv *env, jclass clz, jstring uri, jint action) { - OSStatus status = noErr; + __block OSStatus status = noErr; JNI_COCOA_ENTER(env); - // I would love to use NSWorkspace here, but it's not thread safe. Why? I don't know. - // So we use LaunchServices directly. + NSURL *urlToOpen = [NSURL URLWithString:JavaStringToNSString(env, uri)]; + NSURL *appURI = nil; - NSURL *url = [NSURL URLWithString:JavaStringToNSString(env, uri)]; + if (action == sun_lwawt_macosx_CDesktopPeer_BROWSE) { + // To get the defaultBrowser + NSURL *httpsURL = [NSURL URLWithString:@"https://"]; + NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; + appURI = [workspace URLForApplicationToOpenURL:httpsURL]; + } else if (action == sun_lwawt_macosx_CDesktopPeer_MAIL) { + // To get the default mailer + NSURL *mailtoURL = [NSURL URLWithString:@"mailto://"]; + NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; + appURI = [workspace URLForApplicationToOpenURL:mailtoURL]; + } - LSLaunchFlags flags = kLSLaunchDefaults; + if (appURI == nil) { + return -1; + } - LSApplicationParameters params = {0, flags, NULL, NULL, NULL, NULL, NULL}; - status = LSOpenURLsWithRole((CFArrayRef)[NSArray arrayWithObject:url], kLSRolesAll, NULL, ¶ms, NULL, 0); + // Prepare NSOpenConfig object + NSArray *urls = @[urlToOpen]; + NSWorkspaceOpenConfiguration *configuration = [NSWorkspaceOpenConfiguration configuration]; + configuration.activates = YES; // To bring app to foreground + configuration.promptsUserIfNeeded = YES; // To allow macOS desktop prompts + + // dispatch semaphores used to wait for the completion handler to update and return status + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC)); // 1 second timeout + + // Asynchronous call to openURL + [[NSWorkspace sharedWorkspace] openURLs:urls + withApplicationAtURL:appURI + configuration:configuration + completionHandler:^(NSRunningApplication *app, NSError *error) { + if (error) { + status = (OSStatus) error.code; + } + dispatch_semaphore_signal(semaphore); + }]; + + dispatch_semaphore_wait(semaphore, timeout); JNI_COCOA_EXIT(env); return status; @@ -56,32 +89,73 @@ JNI_COCOA_EXIT(env); /* * Class: sun_lwawt_macosx_CDesktopPeer * Method: _lsOpenFile - * Signature: (Ljava/lang/String;Z)I; + * Signature: (Ljava/lang/String;I;Ljava/lang/String;)I; */ JNIEXPORT jint JNICALL Java_sun_lwawt_macosx_CDesktopPeer__1lsOpenFile -(JNIEnv *env, jclass clz, jstring jpath, jboolean print) +(JNIEnv *env, jclass clz, jstring jpath, jint action, jstring jtmpTxtPath) { - OSStatus status = noErr; + __block OSStatus status = noErr; JNI_COCOA_ENTER(env); - // I would love to use NSWorkspace here, but it's not thread safe. Why? I don't know. - // So we use LaunchServices directly. - NSString *path = NormalizedPathNSStringFromJavaString(env, jpath); - - NSURL *url = [NSURL fileURLWithPath:(NSString *)path]; + NSURL *urlToOpen = [NSURL fileURLWithPath:(NSString *)path]; // This byzantine workaround is necessary, or else directories won't open in Finder - url = (NSURL *)CFURLCreateWithFileSystemPath(NULL, (CFStringRef)[url path], kCFURLPOSIXPathStyle, false); + urlToOpen = (NSURL *)CFURLCreateWithFileSystemPath(NULL, (CFStringRef)[urlToOpen path], + kCFURLPOSIXPathStyle, false); - LSLaunchFlags flags = kLSLaunchDefaults; - if (print) flags |= kLSLaunchAndPrint; + NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; + NSURL *appURI = [workspace URLForApplicationToOpenURL:urlToOpen]; + NSURL *defaultTerminalApp = [workspace URLForApplicationToOpenURL:[NSURL URLWithString:@"file:///bin/sh"]]; - LSApplicationParameters params = {0, flags, NULL, NULL, NULL, NULL, NULL}; - status = LSOpenURLsWithRole((CFArrayRef)[NSArray arrayWithObject:url], kLSRolesAll, NULL, ¶ms, NULL, 0); - [url release]; + // Prepare NSOpenConfig object + NSArray *urls = @[urlToOpen]; + NSWorkspaceOpenConfiguration *configuration = [NSWorkspaceOpenConfiguration configuration]; + configuration.activates = YES; // To bring app to foreground + configuration.promptsUserIfNeeded = YES; // To allow macOS desktop prompts + + // pre-checks for open/print/edit before calling openURLs API + if (action == sun_lwawt_macosx_CDesktopPeer_OPEN + || action == sun_lwawt_macosx_CDesktopPeer_PRINT) { + if (appURI == nil + || [[urlToOpen absoluteString] containsString:[appURI absoluteString]] + || [[defaultTerminalApp absoluteString] containsString:[appURI absoluteString]]) { + return -1; + } + // Additionally set forPrinting=TRUE for print + if (action == sun_lwawt_macosx_CDesktopPeer_PRINT) { + configuration.forPrinting = YES; + } + } else if (action == sun_lwawt_macosx_CDesktopPeer_EDIT) { + if (appURI == nil + || [[urlToOpen absoluteString] containsString:[appURI absoluteString]]) { + return -1; + } + // for EDIT: if (defaultApp = TerminalApp) then set appURI = DefaultTextEditor + if ([[defaultTerminalApp absoluteString] containsString:[appURI absoluteString]]) { + NSString *path = NormalizedPathNSStringFromJavaString(env, jtmpTxtPath); + NSURL *tempFilePath = [NSURL fileURLWithPath:(NSString *)path]; + appURI = [workspace URLForApplicationToOpenURL:tempFilePath]; + } + } + + // dispatch semaphores used to wait for the completion handler to update and return status + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC)); // 1 second timeout + + // Asynchronous call - openURLs:withApplicationAtURL + [[NSWorkspace sharedWorkspace] openURLs:urls + withApplicationAtURL:appURI + configuration:configuration + completionHandler:^(NSRunningApplication *app, NSError *error) { + if (error) { + status = (OSStatus) error.code; + } + dispatch_semaphore_signal(semaphore); + }]; + + dispatch_semaphore_wait(semaphore, timeout); JNI_COCOA_EXIT(env); return status; } - diff --git a/src/java.desktop/windows/classes/sun/awt/windows/WDesktopPeer.java b/src/java.desktop/windows/classes/sun/awt/windows/WDesktopPeer.java index 788c1477265..e5b628dd74b 100644 --- a/src/java.desktop/windows/classes/sun/awt/windows/WDesktopPeer.java +++ b/src/java.desktop/windows/classes/sun/awt/windows/WDesktopPeer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -38,6 +38,9 @@ import java.io.File; import java.io.IOException; import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import javax.swing.event.EventListenerList; import sun.awt.shell.ShellFolder; @@ -50,9 +53,11 @@ import sun.awt.shell.ShellFolder; */ final class WDesktopPeer implements DesktopPeer { /* Constants for the operation verbs */ - private static String ACTION_OPEN_VERB = "open"; - private static String ACTION_EDIT_VERB = "edit"; - private static String ACTION_PRINT_VERB = "print"; + private static final String ACTION_OPEN_VERB = "open"; + private static final String ACTION_EDIT_VERB = "edit"; + private static final String ACTION_PRINT_VERB = "print"; + private static final String ACTION_BROWSE_VERB = "browse"; + private static final String ACTION_MAIL_VERB = "mail"; private static native void init(); @@ -95,12 +100,12 @@ final class WDesktopPeer implements DesktopPeer { @Override public void mail(URI uri) throws IOException { - this.ShellExecute(uri, ACTION_OPEN_VERB); + this.ShellExecute(uri, ACTION_MAIL_VERB); } @Override public void browse(URI uri) throws IOException { - this.ShellExecute(uri, ACTION_OPEN_VERB); + this.launchUriInBrowser(uri); } private void ShellExecute(File file, String verb) throws IOException { @@ -121,6 +126,42 @@ final class WDesktopPeer implements DesktopPeer { } } + private void launchUriInBrowser(URI uri) throws IOException { + String defaultBrowser = getDefaultBrowser(); + if (defaultBrowser == null) { + throw new IOException("Failed to get default browser"); + } + + List cmdLineTokens = getCmdLineTokens(uri, defaultBrowser); + try { + ProcessBuilder pb = new ProcessBuilder(cmdLineTokens); + pb.start(); + } catch (Exception e) { + throw new IOException("Error launching Browser: ", e); + } + } + + private static List getCmdLineTokens(URI uri, String defaultBrowser) { + if (defaultBrowser.contains("%1")) { + defaultBrowser = defaultBrowser.replace("%1", uri.toString()); + } else { + defaultBrowser = defaultBrowser + " " + uri.toString(); + } + + List cmdLineTokens = new ArrayList<>(); + int firstIndex = defaultBrowser.indexOf("\""); + int secondIndex = defaultBrowser.indexOf("\"", firstIndex + 1); + + if (firstIndex == 0 && secondIndex != firstIndex) { + cmdLineTokens.add(defaultBrowser.substring(firstIndex, secondIndex + 1)); + defaultBrowser = defaultBrowser.substring(secondIndex + 1).trim(); + } + cmdLineTokens.addAll(Arrays.asList(defaultBrowser.split(" "))); + return cmdLineTokens; + } + + private static native String getDefaultBrowser(); + private static native String ShellExecute(String fileOrUri, String verb); private static final EventListenerList listenerList = new EventListenerList(); diff --git a/src/java.desktop/windows/native/libawt/windows/awt_Desktop.cpp b/src/java.desktop/windows/native/libawt/windows/awt_Desktop.cpp index ba79523249c..ebb43b2f078 100644 --- a/src/java.desktop/windows/native/libawt/windows/awt_Desktop.cpp +++ b/src/java.desktop/windows/native/libawt/windows/awt_Desktop.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,6 +30,12 @@ #include #include #include "awt_Toolkit.h" +#include +#include // for AssocQueryStringW +#include +#include +#include +#include // for SaferiIsExecutableFileType #define BUFFER_LIMIT MAX_PATH+1 @@ -78,14 +84,23 @@ JNIEXPORT jstring JNICALL Java_sun_awt_windows_WDesktopPeer_ShellExecute LPCWSTR fileOrUri_c = JNU_GetStringPlatformChars(env, fileOrUri_j, NULL); CHECK_NULL_RETURN(fileOrUri_c, NULL); LPCWSTR verb_c = JNU_GetStringPlatformChars(env, verb_j, NULL); + if (verb_c == NULL) { JNU_ReleaseStringPlatformChars(env, fileOrUri_j, fileOrUri_c); return NULL; } + if (wcscmp(verb_c, L"open") == 0) { + BOOL isExecutable = SaferiIsExecutableFileType(fileOrUri_c, FALSE); + if (isExecutable) { + return env->NewStringUTF("Unsupported URI content"); + } + } + // set action verb for mail() to open before calling ShellExecute + LPCWSTR actionVerb = wcscmp(verb_c, L"mail") == 0 ? L"open" : verb_c; // 6457572: ShellExecute possibly changes FPU control word - saving it here unsigned oldcontrol87 = _control87(0, 0); - HINSTANCE retval = ::ShellExecute(NULL, verb_c, fileOrUri_c, NULL, NULL, + HINSTANCE retval = ::ShellExecute(NULL, actionVerb, fileOrUri_c, NULL, NULL, SW_SHOWNORMAL); DWORD error = ::GetLastError(); _control87(oldcontrol87, 0xffffffff); @@ -113,10 +128,38 @@ JNIEXPORT jstring JNICALL Java_sun_awt_windows_WDesktopPeer_ShellExecute return errmsg; } } - return NULL; } +/* + * Class: sun_awt_windows_WDesktopPeer + * Method: getDefaultBrowser + * Signature: ()Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_sun_awt_windows_WDesktopPeer_getDefaultBrowser +(JNIEnv *env, jclass cls) +{ + LPCWSTR fileExtension = L"https"; + WCHAR defaultBrowser_c [MAX_PATH]; + DWORD cchBuffer = MAX_PATH; + + // Use AssocQueryString to get the default browser + HRESULT hr = AssocQueryStringW( + ASSOCF_NONE, // No special flags + ASSOCSTR_COMMAND, // Request the command string + fileExtension, // File extension + NULL, // pszExtra (optional) + defaultBrowser_c, // Output buffer - result + &cchBuffer // Size of the output buffer + ); + + if (FAILED(hr)) { + return NULL; + } + + return JNU_NewStringPlatform(env, defaultBrowser_c); +} + /* * Class: sun_awt_windows_WDesktopPeer * Method: moveToTrash diff --git a/test/jdk/java/awt/Desktop/BrowseTest.java b/test/jdk/java/awt/Desktop/BrowseTest.java index 33de1ecdca7..28e08fe16c7 100644 --- a/test/jdk/java/awt/Desktop/BrowseTest.java +++ b/test/jdk/java/awt/Desktop/BrowseTest.java @@ -40,10 +40,27 @@ import jtreg.SkippedException; public class BrowseTest extends JPanel { static final String INSTRUCTIONS = """ - This test could launch default file manager to open user's home - directory, and default web browser to show the URL of java vendor. - After test execution close the native file manager and web browser + Set your default browser as per the test platform. + macOS - Safari + windows - MS Edge + linux - Firefox + + This test checks 2 cases: + + 1) Directory URI: + On macOS and windows, verify that a browser window opens and + EITHER the browser OR native file manager shows the user's + home directory. + + On Linux verify that the user's home directory is shown by the + default file manager. + + 2) Web URI: + Verify that the Web URI (URL of java vendor) opens in the browser. + + After test execution close the native file manager and any web browser windows if they were launched by test. + Also check output for any unexpected EXCEPTIONS, if you see any failure messages press Fail otherwise press Pass. """; @@ -53,7 +70,7 @@ public class BrowseTest extends JPanel { URI dirURI = new File(System.getProperty("user.home")).toURI(); URI webURI = URI.create(System.getProperty("java.vendor.url", "http://www.java.com")); - boolean failed = false; + PassFailJFrame.log("Testing 1st case: Directory URI ..."); try { PassFailJFrame.log("Try to browse " + dirURI + " ..."); desktop.browse(dirURI); @@ -62,6 +79,7 @@ public class BrowseTest extends JPanel { PassFailJFrame.log("EXCEPTION: " + e.getMessage()); } + PassFailJFrame.log("Testing 2nd case: Web URI ..."); try { PassFailJFrame.log("Try to browse " + webURI + " ..."); desktop.browse(webURI); diff --git a/test/jdk/java/awt/Desktop/EditAndPrintTest/EditAndPrintTest.java b/test/jdk/java/awt/Desktop/EditAndPrintTest/EditAndPrintTest.java index b2d7ef28df1..77d86ceb42a 100644 --- a/test/jdk/java/awt/Desktop/EditAndPrintTest/EditAndPrintTest.java +++ b/test/jdk/java/awt/Desktop/EditAndPrintTest/EditAndPrintTest.java @@ -48,7 +48,7 @@ public class EditAndPrintTest extends JPanel { This test tries to edit and print a directory, which will expectedly raise IOException. Then this test would edit and print a .txt file, which should be successful. After test execution close the editor if it was launched by test. - If you see any EXCEPTION messages in the output press FAIL. + If you see any EXCEPTION messages in case of .txt file in the output press FAIL. """; static Desktop desktop;