// // LinkBack.m // LinkBack // // Created by Charles Jolley on Tue Jun 15 2004. // Copyright (c) 2004, Nisus Software, Inc. // All rights reserved. // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // // Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // Neither the name of the Nisus Software, Inc. nor the names of its // contributors may be used to endorse or promote products derived from this // software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // #import "LinkBack.h" #import "LinkBackServer.h" NSString* LinkBackPboardType = @"LinkBackData" ; // LinkBack data keys. These are used in a LinkBack object, which is currently a dictionary. Do not depend on these values. They are public for testing purposes only. NSString* LinkBackServerActionKey = @"serverActionKey" ; NSString* LinkBackServerApplicationNameKey = @"serverAppName" ; NSString* LinkBackServerNameKey = @"serverName" ; NSString* LinkBackServerBundleIdentifierKey = @"bundleId" ; NSString* LinkBackVersionKey = @"version" ; NSString* LinkBackApplicationDataKey = @"appData" ; NSString* LinkBackSuggestedRefreshKey = @"refresh" ; NSString* LinkBackApplicationURLKey = @"ApplicationURL" ; NSString* LinkBackEditActionName = @"_Edit" ; NSString* LinkBackRefreshActionName = @"_Refresh" ; // ........................................................................... // Support Functions // id MakeLinkBackData(NSString* serverName, id appData) { return [NSDictionary linkBackDataWithServerName: serverName appData: appData] ; } id LinkBackGetAppData(id LinkBackData) { return [LinkBackData linkBackAppData] ; } NSString* LinkBackUniqueItemKey() { static int counter = 0 ; NSString* base = [[NSBundle mainBundle] bundleIdentifier] ; unsigned long time = [NSDate timeIntervalSinceReferenceDate] ; return [NSString stringWithFormat: @"%@%.8lx.%.4x",base,time,counter++] ; } BOOL LinkBackDataBelongsToActiveApplication(id data) { return [data linkBackDataBelongsToActiveApplication] ; } NSString* LinkBackEditMultipleMenuTitle() { NSBundle* bundle = [NSBundle bundleForClass: [LinkBack class]] ; NSString* ret = [bundle localizedStringForKey: @"_EditMultiple" value: @"Edit LinkBack Items" table: @"Localized"] ; return ret ; } NSString* LinkBackEditNoneMenuTitle() { NSBundle* bundle = [NSBundle bundleForClass: [LinkBack class]] ; NSString* ret = [bundle localizedStringForKey: @"_EditNone" value: @"Edit LinkBack Item" table: @"Localized"] ; return ret ; } // ........................................................................... // LinkBack Data Category // // Use these methods to create and access linkback data objects. You can also use the helper functions above. @implementation NSDictionary (LinkBackData) + (NSDictionary*)linkBackDataWithServerName:(NSString*)serverName appData:(id)appData { return [self linkBackDataWithServerName: serverName appData: appData actionName: nil suggestedRefreshRate: (float)0]; } + (NSDictionary*)linkBackDataWithServerName:(NSString*)serverName appData:(id)appData suggestedRefreshRate:(NSTimeInterval)rate { return [self linkBackDataWithServerName: serverName appData: appData actionName: LinkBackRefreshActionName suggestedRefreshRate: rate] ; } + (NSDictionary*)linkBackDataWithServerName:(NSString*)serverName appData:(id)appData actionName:(NSString*)action suggestedRefreshRate:(NSTimeInterval)rate { NSDictionary* appInfo = [[NSBundle mainBundle] infoDictionary] ; NSMutableDictionary* ret = [[NSMutableDictionary alloc] init] ; NSString* bundleId = [[NSBundle mainBundle] bundleIdentifier] ; NSString* url = [appInfo objectForKey: @"LinkBackApplicationURL"] ; NSString* appName = [[NSProcessInfo processInfo] processName] ; id version = @"A" ; if (nil==serverName) [NSException raise: NSInvalidArgumentException format: @"LinkBack Data cannot be created without a server name."] ; // callback information [ret setObject: bundleId forKey: LinkBackServerBundleIdentifierKey]; [ret setObject: serverName forKey: LinkBackServerNameKey] ; [ret setObject: version forKey: LinkBackVersionKey] ; // additional information if (appName) [ret setObject: appName forKey: LinkBackServerApplicationNameKey] ; if (action) [ret setObject: action forKey: LinkBackServerActionKey] ; if (appData) [ret setObject: appData forKey: LinkBackApplicationDataKey] ; if (url) [ret setObject: url forKey: LinkBackApplicationURLKey] ; [ret setObject: [NSNumber numberWithDouble: rate] forKey: LinkBackSuggestedRefreshKey] ; return [ret autorelease] ; } - (BOOL)linkBackDataBelongsToActiveApplication { NSString* bundleId = [[NSBundle mainBundle] bundleIdentifier] ; NSString* dataId = [self objectForKey: LinkBackServerBundleIdentifierKey] ; return (dataId && [dataId isEqualToString: bundleId]) ; } - (id)linkBackAppData { return [self objectForKey: LinkBackApplicationDataKey] ; } - (NSString*)linkBackSourceApplicationName { return [self objectForKey: LinkBackServerApplicationNameKey] ; } - (NSString*)linkBackActionName { NSBundle* bundle = [NSBundle bundleForClass: [LinkBack class]] ; NSString* ret = [self objectForKey: LinkBackServerActionKey] ; if (nil==ret) ret = LinkBackEditActionName ; ret = [bundle localizedStringForKey: ret value: ret table: @"Localized"] ; return ret ; } - (NSString*)linkBackEditMenuTitle { NSBundle* bundle = [NSBundle bundleForClass: [LinkBack class]] ; NSString* appName = [self linkBackSourceApplicationName] ; NSString* action = [self linkBackActionName] ; NSString* ret = [bundle localizedStringForKey: @"_EditPattern" value: @"%@ in %@" table: @"Localized"] ; ret = [NSString stringWithFormat: ret, action, appName] ; return ret ; } - (NSString*)linkBackVersion { return [self objectForKey: LinkBackVersionKey] ; } - (NSTimeInterval)linkBackSuggestedRefreshRate { id obj = [self objectForKey: LinkBackSuggestedRefreshKey] ; return (obj) ? [obj floatValue] : 0 ; } - (NSURL*)linkBackApplicationURL { id obj = [self objectForKey: LinkBackApplicationURLKey] ; if (obj) obj = [NSURL URLWithString: obj] ; return obj ; } @end // ........................................................................... // LinkBackServer // // one of these exists for each registered server name. This is the receiver of server requests. // ........................................................................... // LinkBack Class // NSMutableDictionary* keyedLinkBacks = nil ; @implementation LinkBack + (void)initialize { static BOOL inited = NO ; if (inited) return ; inited=YES; [super initialize] ; keyedLinkBacks = [[NSMutableDictionary alloc] init] ; } + (LinkBack*)activeLinkBackForItemKey:(id)aKey { return [keyedLinkBacks objectForKey: aKey] ; } - (id)initServerWithClient: (LinkBack*)aLinkBack delegate: (id)aDel { if ((self = [super init])) { peer = [aLinkBack retain] ; sourceName = [[peer sourceName] copy] ; sourceApplicationName = [[peer sourceApplicationName] copy] ; key = [[peer itemKey] copy] ; isServer = YES ; delegate = aDel ; [keyedLinkBacks setObject: self forKey: key] ; } return self ; } - (id)initClientWithSourceName:(NSString*)aName delegate:(id)aDel itemKey:(NSString*)aKey { if ((self = [super init])) { isServer = NO ; delegate = aDel ; sourceName = [aName copy] ; sourceApplicationName = [[NSProcessInfo processInfo] processName] ; pboard = [[NSPasteboard pasteboardWithUniqueName] retain] ; key = [aKey copy] ; } return self ; } - (void)dealloc { [repobj release] ; [sourceName release] ; if (peer) [self closeLink] ; [peer release] ; if (!isServer) [pboard releaseGlobally] ; // client owns the pboard. [pboard release] ; [super dealloc] ; } // ........................................................................... // General Use methods - (NSPasteboard*)pasteboard { return pboard ; } - (id)representedObject { return repobj ; } - (void)setRepresentedObject:(id)obj { [obj retain] ; [repobj release] ; repobj = obj ; } - (NSString*)sourceName { return sourceName ; } - (NSString*)sourceApplicationName { return sourceApplicationName ; } - (NSString*)itemKey { return key ; } // this method is called to initial a link closure from this side. - (void)closeLink { // inform peer of closure if (peer) { [peer remoteCloseLink] ; [peer release] ; peer = nil ; [self release] ; [keyedLinkBacks removeObjectForKey: [self itemKey]]; } } // this method is called whenever the link is about to be or has been closed by the other side. - (oneway void)remoteCloseLink { if (peer) { [peer release] ; peer = nil ; [self release] ; [keyedLinkBacks removeObjectForKey: [self itemKey]]; } if (delegate) [delegate linkBackDidClose: self] ; } // ........................................................................... // Server-side methods // + (BOOL)publishServerWithName:(NSString*)name delegate:(id)del { return [LinkBackServer publishServerWithName: name delegate: del] ; } + (void)retractServerWithName:(NSString*)name { LinkBackServer* server = [LinkBackServer LinkBackServerWithName: name] ; if (server) [server retract] ; } - (void)sendEdit { if (!peer) [NSException raise: NSGenericException format: @"tried to request edit from a live link not connect to a server."] ; [peer refreshEditWithPasteboardName: [pboard name]] ; } // FROM CLIENT LinkBack - (void)requestEditWithPasteboardName:(bycopy NSString*)pboardName { // get the new pasteboard, if needed if ((!pboard) || ![pboardName isEqualToString: [pboard name]]) pboard = [[NSPasteboard pasteboardWithName: pboardName] retain] ; // pass onto delegate [delegate performSelectorOnMainThread: @selector(linkBackClientDidRequestEdit:) withObject: self waitUntilDone: NO] ; } // ........................................................................... // Client-Side Methods // + (LinkBack*)editLinkBackData:(id)data sourceName:(NSString*)aName delegate:(id)del itemKey:(NSString*)aKey { // if an active live link already exists, use that. Otherwise, create a new one. LinkBack* ret = [keyedLinkBacks objectForKey: aKey] ; if(nil==ret) { BOOL ok = [data isKindOfClass: [NSDictionary class]] ; if (ok) { // collect server contact information from data. NSString* serverName = [data objectForKey: LinkBackServerNameKey] ; NSString* serverId = [data objectForKey: LinkBackServerBundleIdentifierKey]; NSString* appName = [data linkBackSourceApplicationName] ; NSURL* url = [data linkBackApplicationURL] ; if ( !serverName || !serverId) [NSException raise: NSInvalidArgumentException format: @"LinkBackData is not of the correct format: %@", data] ; // create the live link object and try to connect to the server. ret = [[LinkBack alloc] initClientWithSourceName: aName delegate: del itemKey: aKey] ; if (![ret connectToServerWithName: serverName inApplication: serverId fallbackURL: url appName: appName]) { [ret release] ; ret = nil ; } } else { [NSException raise: NSInvalidArgumentException format: @"LinkBackData is not of the correct format: %@", data] ; } } // now with a live link in hand, request an edit if (ret) { // if connected to server, publish data and inform server. NSPasteboard* my_pboard = [ret pasteboard] ; [my_pboard declareTypes: [NSArray arrayWithObject: LinkBackPboardType] owner: ret] ; [my_pboard setPropertyList: data forType: LinkBackPboardType] ; [ret requestEdit] ; // if connection to server failed, return nil. } return ret ; } - (BOOL)connectToServerWithName:(NSString*)aName inApplication:(NSString*)bundleIdentifier fallbackURL:(NSURL*)url appName:(NSString*)appName { // get the LinkBackServer. LinkBackServer* server = [LinkBackServer LinkBackServerWithName: aName inApplication: bundleIdentifier launchIfNeeded: YES fallbackURL: url appName: appName] ; if (!server) return NO ; // failed to get server peer = [[server initiateLinkBackFromClient: self] retain] ; if (!peer) return NO ; // failed to initiate session // if we connected, then add to the list of active keys [keyedLinkBacks setObject: self forKey: [self itemKey]] ; return YES ; } - (void)requestEdit { if (!peer) [NSException raise: NSGenericException format: @"tried to request edit from a live link not connect to a server."] ; [peer requestEditWithPasteboardName: [pboard name]] ; } // RECEIVED FROM SERVER - (void)refreshEditWithPasteboardName:(bycopy NSString*)pboardName { // if pboard has changes, change to new pboard. if (![pboardName isEqualToString: [pboard name]]) { [pboard release] ; pboard = [[NSPasteboard pasteboardWithName: pboardName] retain] ; } // inform delegate [delegate performSelectorOnMainThread: @selector(linkBackServerDidSendEdit:) withObject: self waitUntilDone: NO] ; } @end