8359501: Enhance Handling of URIs

Reviewed-by: rhalade, ahgross, azvegint, prr
This commit is contained in:
Harshitha Onkar 2025-09-24 18:05:45 +00:00 committed by bchristi
parent 1f10573734
commit 70142b4438
6 changed files with 258 additions and 53 deletions

View File

@ -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);
}

View File

@ -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 <CoreFoundation/CoreFoundation.h>
#import <ApplicationServices/ApplicationServices.h>
#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, &params, NULL, 0);
// Prepare NSOpenConfig object
NSArray<NSURL *> *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, &params, NULL, 0);
[url release];
// Prepare NSOpenConfig object
NSArray<NSURL *> *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;
}

View File

@ -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<String> 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<String> getCmdLineTokens(URI uri, String defaultBrowser) {
if (defaultBrowser.contains("%1")) {
defaultBrowser = defaultBrowser.replace("%1", uri.toString());
} else {
defaultBrowser = defaultBrowser + " " + uri.toString();
}
List<String> 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();

View File

@ -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 <float.h>
#include <shlobj.h>
#include "awt_Toolkit.h"
#include <Windows.h>
#include <shlwapi.h> // for AssocQueryStringW
#include <wchar.h>
#include <cwchar>
#include <string.h>
#include <winsafer.h> // 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

View File

@ -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);

View File

@ -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;