Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
541 views
in Technique[技术] by (71.8m points)

macos - Handle custom URL schemes in an OS X Java application

The Info.plist of our Java-based application contains following entries:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
<plist version="0.9">
    <dict>
        ...
        <key>CFBundleURLTypes</key>
        <array>
            <dict>
                <key>CFBundleURLName</key>
                <string>myApp handler</string>
                <key>CFBundleURLSchemes</key>
                <array>
                    <string>myapp</string>
                </array>
            </dict>
        </array>
        ...
    </dict>
</plist>

It should handle an URL like myapp://foobar/bazz. Opening the application works fine, but how the application should obtain the clicked URL?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

For Objective C the answer can be found here: When an OS X app is launched by a registered URL scheme, how do you access the full URL?

The solution for Java is to rewrite the ObjC code into plain C, then translate that into Java, with the help of a set of classes under org.eclipse.swt.internal.cocoa.*.

As a reference for the ObjC-to-C translation, we need Apple's Objective-C Runtime Reference.

Plain C version

First, let's translate

[[NSAppleEventManager sharedAppleEventManager]
    setEventHandler:targetObject
        andSelector:@selector(handleAppleEvent:withReplyEvent:)
      forEventClass:kInternetEventClass
         andEventID:kAEGetURL];

into plain C. To invoke a ObjC function in plain C, we use objc_msgSend(). Furthermore, @selector(method_footprint) is substituted by sel_registerName("method_footprint"), and classes are looked up with objc_getClass(). The types id and SEL are equivalent to a pointer (such as void*) or a full-size int (i.e. same size as a void*).

The result:

// id mgr = [NSAppleEventManager sharedAppleEventManager]
SEL sel_ sharedAppleEventManager = sel_registerName("sharedAppleEventManager");
id mgr = objc_msgSend (objc_getClass("NSAppleEventManager"), sharedAppleEventManager);

// and the rest
SEL sel_setEventHandler = sel_registerName("setEventHandler:andSelector:forEventClass:andEventID:");
SEL sel_handleAppleEvent = sel_registerName("handleAppleEvent:withReplyEvent:");
objc_msgSend (mgr, sel_setEventHandler, targetObject, sel_handleAppleEvent, kInternetEventClass, kAEGetURL);

As you can see, we have two subroutine invocations here: The first calls the sharedAppleEventManager message of the NSAppleEventManager class, retrieving a singleton object from that class. The second call is sending the setEventHandler... message to that object, passing 4 arguments (target object, target message, and two event specifiers).

The callback function's declaration, originally:

- (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent

looks like this in plain C:

void handleAppleEvent (id self, SEL selector, NSAppleEventDescriptor* event, NSAppleEventDescriptor* replyEvent)

This means that when the function gets called, it gets sent not only its object reference (id) but also its method footprint (selector).

The callback code looks like this in ObjC to get to the URL:

NSString url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];

And in plain C:

id desc_id = objc_msgSend (event_id, sel_registerName("paramDescriptorForKeyword:"), '----');
id url_id = objc_msgSend (desc_id, desc_id, sel_registerName("stringValue"));

One part is still missing:

targetObject needs to be initialized before invoking the code above, and a method matching the handleAppleEvent:withReplyEvent: footprint needs to be created in that target object, and then linked to our plain C event handler (handleAppleEvent()).

This means that we have to create an Objective C class, add a method to it, and then create an object instance of it:

// create an NSObject subclass for our target object
char objcClassName[] = "ObjCAppleEventHandler";
id objcClass = objc_allocateClassPair (objc_getClass("NSObject"), objcClassName);

// add the callback method to the class
SEL sel_handleAppleEvent = sel_registerName("handleAppleEvent:withReplyEvent:");
class_addMethod (objcClass, sel_handleAppleEvent, handleAppleEvent, "i@:@@");

// register the class
objc_registerClassPair (objcClass)

// create an object instance
id targetObject = class_createInstance (objcClass, 0);

// ... here follows the above code with the setEventHandler invocation
// (note: `SEL sel_handleAppleEvent` appears twice - the 2nd one can be removed)

This concludes the plain C version.

(Note: The above code was written without testing it, so it may contain errors. The Java code below, however, has been tested.)

Java version

Translation from Plain C to Java is now fairly straight-forward.

The aforementioned ObjC Runtime functions are all available from org.eclipse.swt.internal.cocoa.OS.

First, some presets:

static final long class_NSAppleEventManager = OS.objc_getClass("NSAppleEventManager");
static final long sel_sharedAppleEventManager = OS.sel_registerName("sharedAppleEventManager");
static final long sel_setEventHandler = OS.sel_registerName("setEventHandler:andSelector:forEventClass:andEventID:");
static final long sel_handleAppleEvent = OS.sel_registerName("handleAppleEvent:withReplyEvent:");
static final long sel_paramDescriptorForKeyword = OS.sel_registerName("paramDescriptorForKeyword:");
static final long sel_stringValue = OS.sel_registerName("stringValue");

static final long kInternetEventClass = 0x4755524C; // 'GURL'
static final long kAEGetURL = 0x4755524C; // 'GURL'
static final long kCoreEventClass = 0x61657674; // 'aevt'
static final long kAEOpenApplication = 0x6F617070; // 'oapp'
static final long kAEReopenApplication = 0x72617070; // 'rapp'
static final long keyDirectObject = 0x2d2d2d2d; // '----'

The callback function:

static long handleAppleEvent (long id, long sel, long event_id, long reply_id) {
    // This is a handler for AppleEvents that are registered with [NSAppleEventManager setEventHandler:...]
    // It matches this selector (footprint):
    //   - (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)reply

    // Invoke [[event paramDescriptorForKeyword:keyDirectObject] stringValue] to get the direct object containing the URL
    long direct_desc_id = OS.objc_msgSend (event_id, sel_paramDescriptorForKeyword, keyDirectObject);
    long direct_str_id = OS.objc_msgSend (direct_desc_id, sel_stringValue);
    NSString nsStr = new NSString (direct_str_id);
    String str = nsStr.getString();
    // now 'str' contains the URL

    System.out.println ("handleAppleEvent invoked -- argument: "+url);
    return 0;
}

And the code to register the callback function:

// Get access to a callback function for receiving the sel_handleAppleEvent message
aeCallback = new Callback(Main.class, "handleAppleEvent", 4);
long aeProc = aeCallback.getAddress();

// Create a ObjC class that provides a method with the sel_handleAppleEvent footprint
String objcClassName = "ObjCAppleEventHandler";
long objcClass = OS.objc_allocateClassPair(OS.class_NSObject, objcClassName, 0);
OS.class_addMethod(objcClass, sel_handleAppleEvent, aeProc, "i@:@@");
OS.objc_registerClassPair(objcClass);
long objcHandlerInstance = OS.class_createInstance (objcClass, 0);

// Invoke [[NSAppleEventManager sharedAppleEventManager] setEventHandler:objcHandlerInstance andSelector:sel_handleAppleEvent forEventClass:kInternetEventClass andEventID:kAEGetURL]
long sharedAppleEventMgr = OS.objc_msgSend (class_NSAppleEventManager, sel_sharedAppleEventManager);
OS.objc_msgSend (sharedAppleEventMgr, sel_setEventHandler, objcHandlerInstance, sel_handleAppleEvent, kInternetEventClass, kAEGetURL);

What's left to do is to build an app bundle from this code and then add the CFBundleURLTypes entries to its Info.plist.

A complete sample source file can be downloaded here: http://files.tempel.org/Various/ObjectiveC-bridging.java.zip


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...