Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handle custom URL schemes in an OS X Java application

Tags:

java

macos

swt

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?

like image 245
Thomas S. Avatar asked Nov 19 '13 14:11

Thomas S.


1 Answers

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

like image 160
Thomas Tempelmann Avatar answered Oct 04 '22 01:10

Thomas Tempelmann