C# Extension Method: ToPascalCase

A function to convert some text to Pascal case always seems to come in handy when writing code generators. So here is a possible implementation:

using System.Text;
using System.Text.RegularExpressions;

public static class StringExtensions
{
    public static string Capatalize(this string text)
    {
        if(string.IsNullOrEmpty(text))
        {
            return string.Empty;
        }

        int index = 0;
        while(index < text.Length)
        {
            if(!char.IsDigit(text, index))
            {
                break;
            }
            index++;
        }

        if(index < text.Length)
        {
            return string.Format(@"{0}{1}", text.Substring(0, index + 1).ToUpper(), text.Substring(index + 1));
        }

        return text;
    }

    public static string ToPascalCase(this string text)
    {
        if(string.IsNullOrEmpty(text))
        {
            return string.Empty;
        }

        var sb = new StringBuilder();
        bool firstWord = true;
        foreach(object match in Regex.Matches(text, "[A-Za-z0-9]+"))
        {
            if(firstWord)
            {
                sb.Append(match.ToString().Capatalize());
                firstWord = false;
            }
            else
            {
                sb.Append(match.ToString().Capatalize());
            }
        }
        return sb.ToString();
    }
}

0 comments

Aperture MetaWeblog Plugin: Showing progress

Changing the plug-in

In order to show progress, we need to initialize the number of steps in the exportManagerShouldBeginExport method and update it during the exportManagerShouldWriteImageData method. Add the following code snippet to the exportManagerShouldBeginExport method.

- (void)exportManagerShouldBeginExport
{
 // create the blog instance
 _blog = [[MetaWeblog alloc] init];
 [_blog setUrl: [blogAccessPointTextField stringValue]];
 [_blog setBlogId: [blogIdTextField stringValue]];
 [_blog setUsername: [blogUserNameTextField stringValue]];
 [_blog setPassword: [blogPasswordTextField stringValue]];
  
 // save settings
 NSAutoreleasePool *pool =  [[NSAutoreleasePool alloc] init];
 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
 NSString* pluginIdentifier = [[[NSBundle bundleForClass: [self class]] infoDictionary] objectForKey: @"CFBundleIdentifier"];
 NSMutableDictionary* settings = [NSMutableDictionary dictionaryWithDictionary: [defaults persistentDomainForName:pluginIdentifier]];
 [settings setValue: [_blog url] forKey: @"accessPoint"]; [settings setValue: [_blog blogId] forKey: @"blogId"]; [settings setValue: [_blog username] forKey: @"username"]; [settings setValue: [_blog password] forKey: @"password"]; [defaults setPersistentDomain:settings forName:pluginIdentifier]; 
 [defaults synchronize];
 [pool release];
 
 // initialize the progress
 unsigned long count = [_exportManager imageCount];
 [self lockProgress];
 exportProgress.totalValue = count * 3;
  exportProgress.currentValue = 0;
 exportProgress.message = @"Exporting...";
 [self unlockProgress]; 
 [_exportManager shouldBeginExport];
}

One thing to note is that we set the total number of steps (totalValue) to the number of images being exported times three. Why? Well, it takes a while for Aperture to scale and export the image, another for us to upload the image to the server, and can even take a few seconds to create a post. In order to make it easier to track progress we know there are three times the steps than there are images. Chances are that as the plug-in becomes more sophisticated, more steps will be added.

The next step is to add a method to update progress. First, add the updateProgress method prototype to MetaWeblogPlugIn.h:

#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
#import "ApertureExportManager.h"
#import "ApertureExportPlugIn.h"
#import "MetaWeblog.h"

@interface MetaWeblogPlugIn : NSObject <ApertureExportPlugIn>
{
 id _apiManager; 
 NSObject<ApertureExportManager, PROAPIObject> *_exportManager; 
 NSLock *_progressLock;
 NSArray *_topLevelNibObjects;
 MetaWeblog* _blog;
 ApertureExportProgress exportProgress;
 IBOutlet NSView *settingsView;
 IBOutlet NSView *firstView;
 IBOutlet NSView *lastView;
 IBOutlet NSTextField *blogAccessPointTextField;
 IBOutlet NSTextField *blogUserNameTextField;
 IBOutlet NSSecureTextField *blogPasswordTextField;
 IBOutlet NSTextField *blogIdTextField;
}

- (void)updateProgress: (NSString*) msg;

@end

In MetaWeblogPlugIn.m, lets implement the method

- (void)updateProgress: (NSString*) msg
{
 [self lockProgress];
 [msg retain];
 exportProgress.currentValue += 1;
 exportProgress.message = msg;
 [self unlockProgress]; 
}

This method allows us to increment the current progress by one and set a message for that step. To use the method, lets update the exportManagerShouldWriteImageData method to call it whenever it is about to do something:

- (BOOL)exportManagerShouldWriteImageData:(NSData *)imageData toRelativePath:(NSString *)path forImageAtIndex:(unsigned)index
{
 NSAutoreleasePool *pool =  [[NSAutoreleasePool alloc] init];
 
 NSDictionary* properties = [_exportManager propertiesForImageAtIndex: index];
 NSString* versionName = [properties objectForKey:@"kExportKeyVersionName"];
 NSString* name = [path lastPathComponent];
 [self updateProgress: [NSString stringWithFormat: @"Uploading %@", versionName]];

 NSString* url = [_blog newMediaObject: name andType: @"image/jpeg" andBits: imageData];
 if(url != nil)
 {
  NSLog(@"Uploaded image %@", url);
     
  Post* post = [[Post alloc] init];
  post.title = versionName;
  post.description = [NSString stringWithFormat: @"<img src="%@" />", url];
  post.dateCreated = [NSCalendarDate init];
  
  [self updateProgress: [NSString stringWithFormat: @"Creating post for %@", versionName]];
  NSString* postId = [_blog newPost: post];
  NSLog(@"Created post with id %@", postId);
  
  [post release];
 }
 else
 {
  NSLog(@"Failed to upload the image '%@'", path);
 }
 
 [self updateProgress: @"Exporting..."];
 [pool release];
 return FALSE; 
}

The above code is rather straight forward. We simply update progress before we upload the image to the server. Once that is done, we update the progress before we create a post and finally we set it to the default "Exporting..." for the next image.

Testing

To test, rebuild and redeploy the plug-in. Restart Aperture and export ten or more images. You should now be able to see progress as Aperture exports and then uploads the image, before creating a post for it

Conclusion

In this installment we simply provide the user with feedback in terms of the steps we are taking while exporting images to a blog using the MetaWeblog API.

0 comments

Aperture MetaWeblog Plugin: Showing progress

T

Changing the plug-in

In order to show progress, we need to initialize the number of steps in the exportManagerShouldBeginExport method and update it during the exportManagerShouldWriteImageData method. Add the following code snippet to the exportManagerShouldBeginExport method.

- (void)exportManagerShouldBeginExport
{
 // create the blog instance
 _blog = [[MetaWeblog alloc] init];
 [_blog setUrl: [blogAccessPointTextField stringValue]];
 [_blog setBlogId: [blogIdTextField stringValue]];
 [_blog setUsername: [blogUserNameTextField stringValue]];
 [_blog setPassword: [blogPasswordTextField stringValue]];
  
 // save settings
 NSAutoreleasePool *pool =  [[NSAutoreleasePool alloc] init];
 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
 NSString* pluginIdentifier = [[[NSBundle bundleForClass: [self class]] infoDictionary] objectForKey: @"CFBundleIdentifier"];
 NSMutableDictionary* settings = [NSMutableDictionary dictionaryWithDictionary: [defaults persistentDomainForName:pluginIdentifier]];
 [settings setValue: [_blog url] forKey: @"accessPoint"]; [settings setValue: [_blog blogId] forKey: @"blogId"]; [settings setValue: [_blog username] forKey: @"username"]; [settings setValue: [_blog password] forKey: @"password"]; [defaults setPersistentDomain:settings forName:pluginIdentifier]; 
 [defaults synchronize];
 [pool release];
 
 // initialize the progress
 unsigned long count = [_exportManager imageCount];
 [self lockProgress];
 exportProgress.totalValue = count * 3;
  exportProgress.currentValue = 0;
 exportProgress.message = @"Exporting...";
 [self unlockProgress]; 
 [_exportManager shouldBeginExport];
}

One thing to note is that we set the total number of steps (totalValue) to the number of images being exported times three. Why? Well, it takes a while for Aperture to scale and export the image, another for us to upload the image to the server, and can even take a few seconds to create a post. In order to make it easier to track progress we know there are three times the steps than there are images. Chances are that as the plug-in becomes more sophisticated, more steps will be added.

The next step is to add a method to update progress. First, add the updateProgress method prototype to MetaWeblogPlugIn.h:

#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
#import "ApertureExportManager.h"
#import "ApertureExportPlugIn.h"
#import "MetaWeblog.h"

@interface MetaWeblogPlugIn : NSObject <ApertureExportPlugIn>
{
 id _apiManager; 
 NSObject<ApertureExportManager, PROAPIObject> *_exportManager; 
 NSLock *_progressLock;
 NSArray *_topLevelNibObjects;
 MetaWeblog* _blog;
 ApertureExportProgress exportProgress;
 IBOutlet NSView *settingsView;
 IBOutlet NSView *firstView;
 IBOutlet NSView *lastView;
 IBOutlet NSTextField *blogAccessPointTextField;
 IBOutlet NSTextField *blogUserNameTextField;
 IBOutlet NSSecureTextField *blogPasswordTextField;
 IBOutlet NSTextField *blogIdTextField;
}

- (void)updateProgress: (NSString*) msg;

@end

In MetaWeblogPlugIn.m, lets implement the method

- (void)updateProgress: (NSString*) msg
{
 [self lockProgress];
 [msg retain];
 exportProgress.currentValue += 1;
 exportProgress.message = msg;
 [self unlockProgress]; 
}

This method allows us to increment the current progress by one and set a message for that step. To use the method, lets update the exportManagerShouldWriteImageData method to call it whenever it is about to do something:

- (BOOL)exportManagerShouldWriteImageData:(NSData *)imageData toRelativePath:(NSString *)path forImageAtIndex:(unsigned)index
{
 NSAutoreleasePool *pool =  [[NSAutoreleasePool alloc] init];
 
 NSDictionary* properties = [_exportManager propertiesForImageAtIndex: index];
 NSString* versionName = [properties objectForKey:@"kExportKeyVersionName"];
 NSString* name = [path lastPathComponent];
 [self updateProgress: [NSString stringWithFormat: @"Uploading %@", versionName]];

 NSString* url = [_blog newMediaObject: name andType: @"image/jpeg" andBits: imageData];
 if(url != nil)
 {
  NSLog(@"Uploaded image %@", url);
     
  Post* post = [[Post alloc] init];
  post.title = versionName;
  post.description = [NSString stringWithFormat: @"<img src="%@" />", url];
  post.dateCreated = [NSCalendarDate init];
  
  [self updateProgress: [NSString stringWithFormat: @"Creating post for %@", versionName]];
  NSString* postId = [_blog newPost: post];
  NSLog(@"Created post with id %@", postId);
  
  [post release];
 }
 else
 {
  NSLog(@"Failed to upload the image '%@'", path);
 }
 
 [self updateProgress: @"Exporting..."];
 [pool release];
 return FALSE; 
}

The above code is rather straight forward. We simply update progress before we upload the image to the server. Once that is done, we update the progress before we create a post and finally we set it to the default "Exporting..." for the next image.

Testing

To test, rebuild and redeploy the plug-in. Restart Aperture and export ten or more images. You should now be able to see progress as Aperture exports and then uploads the image, before creating a post for it

Conclusion

In this installment we simply provide the user with feedback in terms of the steps we are taking while exporting images to a blog using the MetaWeblog API.

0 comments

Aperture MetaWeblog Plugin: Creating an Post

Creating the Post Objective-C Class

We need a class to represent a post which contains the title, the time created, a description, summary, categories, link, keywords and more text of a blog post. To create one, in Xcode select File, then New File...

200812202356.jpg

Select Objective-C class and then select Next.

200812210003.jpg

Replace the file name with Post.m and select Finish. Replace Post.h with the following:

#import <Cocoa/Cocoa.h>

@interface Post : NSObject {
   NSString* title;
   NSCalendarDate* dateCreated;
   NSString* description;
   NSString* postId;
   NSString* blogId;
   NSString* summary;
   NSArray* categories;
   NSString* link;
   NSString* keywords;
   NSString* more;
}

@property(readwrite,copy) NSString* title;
@property(readwrite,copy) NSCalendarDate* dateCreated;
@property(readwrite,copy) NSString* description;
@property(readwrite,copy) NSString* postId;
@property(readwrite,copy) NSString* blogId;
@property(readwrite,copy) NSString* summary;
@property(readwrite,copy) NSArray *categories;
@property(readwrite,copy) NSString* link;
@property(readwrite,copy) NSString* keywords;
@property(readwrite,copy) NSString* more;

- (id) init;

@end

Replace Post.m with:

#import “Post.h”

@implementation Post

@synthesize title;
@synthesize dateCreated;
@synthesize description;
@synthesize postId;
@synthesize blogId;
@synthesize categories;
@synthesize summary;
@synthesize link;
@synthesize keywords;
@synthesize more;

- (id) init
{
   dateCreated = [NSCalendarDate init];
   description = @“”;
   postId = @“”;
   title = @“”;
   summary = @“”;
   categories = nil;
   link = @“”;
   keywords = @“”;
   more = @“”;
   return self;
}

@end

Creating the MetaWeblog class

Now that we have a class to represent a post, we need one that represents the blogging engine we will use to create posts. In this case, the blogging engine will need to support the MetaWeblog API. At a minimum, we need to be able to support newMediaObject and newPost methods in order to create a post for an image stored in Aperture. Select File, then New File...

Select Objective-C class and then select Next.

200812202357.jpg

Enter MetaWeblog.m as the file name and select Finish. Open the MetaWeblog.h file and replace it with the following:

#import <Cocoa/Cocoa.h>
#import “Post.h”

@interface MetaWeblog : NSObject {
   NSString *url;   
   NSString *username;
   NSString *password;
   NSString *blogId;
}

@property(readwrite, assign) NSString *url; 
@property(readwrite, assign) NSString *username;
@property(readwrite, assign) NSString *password;
@property(readwrite, assign) NSString *blogId; 

- (id) init;

- (NSString*)newPost: (Post*) post;
- (NSString*)newMediaObject: (NSString*)name andType: (NSString*) type andBits:(NSData*) bits;

@end

Replace the MetaWeblog.m file with the following:

#import “MetaWeblog.h”

@implementation MetaWeblog

@synthesize url; 
@synthesize username;
@synthesize password;
@synthesize blogId; 

- (id) init
{
   url = nil; 
   username  = nil;
   password = nil;
   blogId = nil;
   [super init]; 
   return self;
}

- (NSString*)newPost: (Post*) px
{   
   NSString* title = px.title;
   NSString* summary = px.summary;
   NSString* body = px.description;
   NSDate* date = px.dateCreated;
   NSString* more = px.more;
   NSArray* categories = [[NSArray alloc] initWithArray: px.categories copyItems: YES];
   
   NSString *description = body;
   WSMethodInvocationRef rpcCall;
   NSURL *rpcURL = [NSURL URLWithString: url];
   NSString *methodName = @“metaWeblog.newPost”;
   NSArray *postKey = [NSArray arrayWithObjects: 
                   @“description”, 
                   @“dateCreated”, 
                   @“title”, 
                   @“mt_excerpt”, 
                   @“mt_text_more”, 
                   @“categories”, 
                   nil];
   NSArray *postObject = [NSArray arrayWithObjects:
                    description, 
                    date, 
                    title, 
                    summary, 
                    more, 
                    categories, 
                    nil];
   NSDictionary *post = [NSDictionary dictionaryWithObjects: postObject forKeys:postKey]; 
   NSArray *keys = [NSArray arrayWithObjects: @“blog_ID” , @“username”, @“password”, @“post”, @“publish”, nil];
   NSMutableArray *objects = [NSArray arrayWithObjects: blogId, username, password, post, kCFBooleanTrue, nil];
   NSDictionary *params = [NSDictionary dictionaryWithObjects:objects forKeys:keys];
   NSArray *order = [NSArray arrayWithObjects: @“blog_ID”, @“username”, @“password”, @“post”, @“publish”, nil];
   rpcCall = WSMethodInvocationCreate ((CFURLRef) rpcURL, (CFStringRef) methodName, kWSXMLRPCProtocol);
   WSMethodInvocationSetParameters (rpcCall, (CFDictionaryRef) params, (CFArrayRef) order);
   NSDictionary *result = (NSDictionary *) (WSMethodInvocationInvoke (rpcCall));
   if (WSMethodResultIsFault((CFDictionaryRef)result))
   {
     NSString* faultString = [result objectForKey: (NSString *) kWSFaultString];
     NSLog(@“[metaWeblog.newPost] failed = %@“, result);   
     NSException* theException = [NSException exceptionWithName: @“MetaWeblogException” reason: faultString userInfo:nil]; 
     @throw theException; 
   }
   
   NSLog(@“[metaWeblog.newPost] result = %@“, result);   
   return (NSString*) [result objectForKey:(NSString*)kWSMethodInvocationResult ];
}

- (NSString*)newMediaObject: (NSString*)name andType: (NSString*) type andBits:(NSData*) bits
{   
   WSMethodInvocationRef rpcCall;
   NSURL *rpcURL = [NSURL URLWithString: url];
   NSString *methodName = @“metaWeblog.newMediaObject”;
   NSArray *postKey = [NSArray arrayWithObjects: @“name”, @“type”, @“bits”, nil];
   NSArray *postObject = [NSArray arrayWithObjects: name, type, bits, nil];
   NSDictionary *post = [NSDictionary dictionaryWithObjects:postObject forKeys:postKey]; 
   NSArray *keys = [NSArray arrayWithObjects: @“blog_ID” , @“username”, @“password”, @“post”, nil];
   NSMutableArray *objects = [NSArray arrayWithObjects: blogId, username, password, post, nil];
   NSDictionary *params = [NSDictionary dictionaryWithObjects:objects forKeys:keys];
   NSArray *order = [NSArray arrayWithObjects: @“blog_ID”, @“username”, @“password”, @“post”, nil];
   rpcCall = WSMethodInvocationCreate ((CFURLRef) rpcURL, (CFStringRef) methodName, kWSXMLRPCProtocol);
   WSMethodInvocationSetParameters (rpcCall, (CFDictionaryRef) params, (CFArrayRef) order);
   
   NSDictionary *result = (NSDictionary *) (WSMethodInvocationInvoke (rpcCall));
   NSLog(@“[metaWeblog.newMediaObject] result = %@“, result);
   
   NSDictionary *values = [result objectForKey:(NSString*)kWSMethodInvocationResult ];
   return (NSString*) [values objectForKey: @“url”];
}

@end

Modifying the Plug-In

Now that we have a class to create a post and upload a media object (aka an image), we need to modify the plug-in. First, we need to define an instance of MetaWeblog class in the MetaWeblogPlugIn header.

#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
#import “ApertureExportManager.h”
#import “ApertureExportPlugIn.h”
#import “MetaWeblog.h”

@interface MetaWeblogPlugIn : NSObject <ApertureExportPlugIn>
{
 id _apiManager; 
 NSObject<ApertureExportManager, PROAPIObject> *_exportManager; 
 NSLock *_progressLock;
 NSArray *_topLevelNibObjects;
 ApertureExportProgress exportProgress;
 IBOutlet NSView *settingsView;
 IBOutlet NSView *firstView;
 IBOutlet NSView *lastView;
 IBOutlet NSTextField *blogAccessPointTextField;
 IBOutlet NSTextField *blogUserNameTextField;
 IBOutlet NSSecureTextField *blogPasswordTextField;
 IBOutlet NSTextField *blogIdTextField;

    // the blogging engine
 MetaWeblog* _blog;
}

@end

Next, when the export process is about to begin (aka exportManagerShouldBeginExport is being called), we need to initialize the instance and set its properties to the values entered by the UI. Find exportManagerShouldBeginExport in MetaWeblogPlugIn.m and modify it so it looks as follows:

- (void)exportManagerShouldBeginExport
{
  _blog = [[MetaWeblog alloc] init];
  [_blog setUrl: [blogAccessPointTextField stringValue]];
  [_blog setBlogId: [blogIdTextField stringValue]];
  [_blog setUsername: [blogUserNameTextField stringValue]];
  [_blog setPassword: [blogPasswordTextField stringValue]];
  [_exportManager shouldBeginExport];
}

When we are done exporting, we should free the blog instance we created. Find the exportManagerDidFinishExport method in MetaWeblogPlugIn.m and replace it with the following:

- (void)exportManagerDidFinishExport
{
  [_blog release];
  _blog = nil;
  [_exportManager shouldFinishExport];
}

We should do the same when the user cancelled the export. Find the exportManagerShouldCancelExport method in MetaWeblogPlugIn.m and replace it with the following:

- (void)exportManagerShouldCancelExport
{
  [_blog release];
  _blog = nil;
  [_exportManager shouldCancelExport];
}

The next step will be to upload the exportManagerShouldWriteImageData to upload the image and create a post. The assumption for now is that the image will always be JPEG. We’ll deal with supporting other image formats at a later date. We’ll also use the version name as the post title. In a later installment, I’ll use the actual metadata from the image to set the title. Replace the contents of the method to the following:

- (BOOL)exportManagerShouldWriteImageData:(NSData *)imageData toRelativePath:(NSString *)path forImageAtIndex:(unsigned)index
{
  // memory management
  NSAutoreleasePool *pool =  [[NSAutoreleasePool alloc] init];
  
  // get the properties for the image
  NSDictionary* properties = [_exportManager propertiesForImageAtIndex: index];

  // get the version name from the properties 
  NSString* versionName = [properties objectForKey:@“kExportKeyVersionName”];

  // get the actual file name from the path without a directory
  NSString* name = [path lastPathComponent];
 
  // upload the image
  NSString* url = [_blog newMediaObject: name andType: @“image/jpeg” andBits: imageData];
  if(url != nil)
  {
    NSLog(@“Uploaded image %@“, url);

    // create a post
    Post* post = [[Post alloc] init];
    post.title = versionName;
    post.description = [NSString stringWithFormat: @“<img src=“%@“ />“, url];
    post.dateCreated = [NSCalendarDate init];
    
    // create the post
    NSString* postId = [_blog newPost: post];
    NSLog(@“Created post with id %@“, postId);
    [post release];
  }
  else
  {
    NSLog(@“Failed to upload the image ‘%@‘“, path);
  }
  [pool release];

  // don’t write anything to disk
  return FALSE; 
}

Testing the plugin

Quit Aperture if its running. Rebuild and deploy the plug-in. Execute the plug-in, by running Aperture, select a few images, then select File, then Export and then Blog. Enter the details of your blog and select Export. Once the plug-in is done, you should see a few entries for the selected images in your blog.

Conclusion

In this post we modified the plug-in to post the image to a MetaWeblog compatible blogging system. There are several things that require attention, including saving your blog’s details, improved error handling (which is non-existant) and reusing metadata we already defined in Aperture.

.

0 comments

Aperture MetaWeblog Plugin: Saving User Information

Saving the information

We need to save the information in a plist file. We will save the information whenever we start exporting information. So, in MetaWeblogPlugIn.m, find the exportManagerShouldBeginExport method and add the following code to it:

- (void)exportManagerShouldBeginExport
{
  // create the blog instance
  _blog = [[MetaWeblog alloc] init];
  [_blog setUrl: [blogAccessPointTextField stringValue]];
  [_blog setBlogId: [blogIdTextField stringValue]];
  [_blog setUsername: [blogUserNameTextField stringValue]];
  [_blog setPassword: [blogPasswordTextField stringValue]];
  
  // save settings
  NSAutoreleasePool *pool =  [[NSAutoreleasePool alloc] init];
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  NSString* pluginIdentifier = [[[NSBundle bundleForClass: [self class]] infoDictionary] objectForKey: @"CFBundleIdentifier"];
  NSMutableDictionary* settings = [NSMutableDictionary dictionaryWithDictionary: [defaults persistentDomainForName:pluginIdentifier]];
  [settings setValue: [_blog url] forKey: @"accessPoint"];
  [settings setValue: [_blog blogId] forKey: @"blogId"]; 
  [settings setValue: [_blog username] forKey: @"username"];
  [settings setValue: [_blog password] forKey: @"password"]; 
  [defaults setPersistentDomain:settings forName:pluginIdentifier];  
  [defaults synchronize];
  [pool release];
 
  // Begin Export 
  [_exportManager shouldBeginExport];
}

When the UI is activated for the first time, we need to read the values and populate the corresponding UI controls. So lets overwrite the willBeActivated method

- (void)willBeActivated
{ 
  NSAutoreleasePool *pool =  [[NSAutoreleasePool alloc] init];
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  NSString* pluginIdentifier = [[[NSBundle bundleForClass: [self class]] infoDictionary] objectForKey: @"CFBundleIdentifier"];
  NSMutableDictionary* settings = [NSMutableDictionary dictionaryWithDictionary: [defaults persistentDomainForName:pluginIdentifier]];
  [blogAccessPointTextField setStringValue: [settings valueForKey: @"accessPoint"]];
  [blogIdTextField setStringValue: [settings valueForKey: @"blogId"]];
  [blogUserNameTextField setStringValue: [settings valueForKey: @"username"]];
  [blogPasswordTextField setStringValue: [settings valueForKey: @"password"]];
  [pool release];
}

In the first installment of the plug-in, we configured the Bundle Identifier to be com.bloudraak.export.MetaWeblogPlugIn. You'll find a file with that name (com.bloudraak.export.MetaWeblogPlugIn.plist) in ~/Library/Preferences/. If you open it, you can see the values. In a future installment, we'll look at storing the password in the user's keychain. Until then, the password is stored in clear text in the plist file. Yes, its bad. If it bothers you enough, then feel free to change it.

Conclusion

In this installment persisted the user preferences related to the blogging server. What is outstanding is storing the password in the keychain rather than the plist file.

0 comments

Aperture MetaWeblog Plugin: The UI

Preparing the source code

Before we can update the user interface, we need to update the source. We know from the MetaWeblog API that we will need the following data:

  1. AccessPoint: the URL we will use when consuming the API
  2. Blog Id: the id of the blog in which we will create entries
  3. User Name: the user name of the author that will create the entries
  4. Password: the password of the author that will create the entries

So, in MetaWeblogPlugIn.h lets add the following:

#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
#import "ApertureExportManager.h"
#import "ApertureExportPlugIn.h"

@interface MetaWeblogPlugIn : NSObject <ApertureExportPlugIn>
{
   id _apiManager; 
   NSObject<ApertureExportManager, PROAPIObject> *_exportManager; 
   NSLock *_progressLock;
   NSArray *_topLevelNibObjects;

   ApertureExportProgress exportProgress;

   IBOutlet NSView *settingsView;
   IBOutlet NSView *firstView;
   IBOutlet NSView *lastView;
 IBOutlet NSTextField *blogAccessPointTextField; IBOutlet NSTextField *blogUserNameTextField; IBOutlet NSSecureTextField *blogPasswordTextField; IBOutlet NSTextField *blogIdTextField;

}

We added both NSString and IBOutlet fields for each piece of data we need to connect to the MetaWeblog API. In the MetaWeblogPlugIn.m, we will add methods to deal with changes to the various UI elements. For example, if the text blogAccesPointTextField changes, then we will update the underlying _blogAccessPoint field.

#import "MetaWeblogPlugIn.h"

@implementation MetaWeblogPlugIn

 - (id)initWithAPIManager:(id<PROAPIAccessing>)apiManager
{
  if (self = [super init])
  {
    _apiManager = apiManager;
    _exportManager = [[_apiManager apiForProtocol:@protocol(ApertureExportManager)] retain];
    if (!_exportManager)
      return nil;

    _progressLock = [[NSLock alloc] init];
  }
  return self;
}

- (void)dealloc
{
  [_topLevelNibObjects makeObjectsPerformSelector:@selector(release)];
  [_topLevelNibObjects release];

  [_progressLock release];
  [_exportManager release];

  [super dealloc];
}


#pragma mark -
// UI Methods
#pragma mark UI Methods

- (NSView *)settingsView
{
  if (nil == settingsView)
  {
    // Load the nib using NSNib, and retain the array of top-level objects so we can release
    // them properly in dealloc
    NSBundle *myBundle = [NSBundle bundleForClass:[self class]];
    NSNib *myNib = [[NSNib alloc] initWithNibNamed:"MetaWeblogPlugIn" bundle:myBundle];
    if ([myNib instantiateNibWithOwner:self topLevelObjects:&_topLevelNibObjects])
    {
      [_topLevelNibObjects retain];
    }
    [myNib release];
  }

  return settingsView;
}

- (NSView *)firstView
{
  return firstView;
}

- (NSView *)lastView
{
  return lastView;
}

- (void)willBeActivated
{
}

- (void)willBeDeactivated
{
}

#pragma mark
// Aperture UI Controls
#pragma mark Aperture UI Controls

- (BOOL)allowsOnlyPlugInPresets
{
  return TRUE;  
}

- (BOOL)allowsMasterExport
{
  return FALSE;   
}

- (BOOL)allowsVersionExport
{
  return TRUE;  
}

- (BOOL)wantsFileNamingControls
{
  return FALSE;   
}

- (void)exportManagerExportTypeDidChange
{   
}

#pragma mark -
// Save Path Methods
#pragma mark Save/Path Methods

- (BOOL)wantsDestinationPathPrompt
{
  return FALSE;
}

- (NSString *)destinationPath
{
  return nil;
}

- (NSString *)defaultDirectory
{
  return nil;
}


#pragma mark -
// Export Process Methods
#pragma mark Export Process Methods

- (void)exportManagerShouldBeginExport
{
  NSString* blogAccessPoint = [blogAccessPointTextField stringValue];
  NSString* blogId = [blogIdTextField stringValue];
  NSString* blogUserName = [blogUserNameTextField stringValue];
  NSString* blogpassword = [blogPasswordTextField stringValue];	

  NSLog("blogAccessPoint: %", blogAccessPoint);
  NSLog("blogId: %", blogId);
  NSLog("blogUserName: %", blogUserName);
  NSLog("blogPassword: ********");
  [_exportManager shouldBeginExport];
}

- (void)exportManagerWillBeginExportToPath:(NSString *)path
{

}

- (BOOL)exportManagerShouldExportImageAtIndex:(unsigned)index
{
  return TRUE;
}

- (void)exportManagerWillExportImageAtIndex:(unsigned)index
{
}

- (BOOL)exportManagerShouldWriteImageData:(NSData *)imageData toRelativePath:(NSString *)path forImageAtIndex:(unsigned)index
{
  return TRUE;  
}

- (void)exportManagerDidWriteImageDataToRelativePath:(NSString *)relativePath forImageAtIndex:(unsigned)index
{
}

- (void)exportManagerDidFinishExport
{
  [_exportManager shouldFinishExport];
}

- (void)exportManagerShouldCancelExport
{
  [_exportManager shouldCancelExport];
}

#pragma mark -
 // Progress Methods
#pragma mark Progress Methods

- (ApertureExportProgress *)progress
{
  return &exportProgress;
}

- (void)lockProgress
{
  if (!_progressLock)
    _progressLock = [[NSLock alloc] init];

  [_progressLock lock];
}

- (void)unlockProgress
{
  [_progressLock unlock];
}

@end

Updating the settings view

Now that we have the foundations in place, modify the UI. To do that, open MetaWeblogPlugIn.nib. The settings window will be visible. Delete the "Build Your UI Here"label.

200812202114.jpg

Now add a simple box, three text fields and one secure text fields. Add four labels and organize them such that they look as follows:

200812202118.jpg

The endpoint, blog id and user name are all text fields. The password is the secure text field. Now lets connect the fields accordingly with each instance of IBOutlet we created in the MetaWeblogPlugIn class. First, select the "File's owner".

200812202123.jpg

Select Tools then Connections Inspector.

200812202125.jpg

Using your mouse, select the open circle of the settings view and connect it to the settings view.

200812202129.jpg

Connect the blogAccessPointTextView to the first text field, blogIdTextField to the second text field, the blogPasswordTextField to the last text field and blogUserTextField with the third text field.

200812202131.jpg

Testing the changes

In order to test whether all of this was correctly done, run the plug-in by starting Aperture, then selecting File, Export and then Blog. Enter some values and select Export.

200812202241.jpg

Check the debugger console and you'll see the values entered in the UI.

200812202243.jpg

0 comments

Aperture MetaWeblog Plugin: Initial Setup

Creating a temporary Aperture Library

Now is a good time to create a temporary Aperture library and add a few pictures to it. This is done to avoid corrupting your current library when you are debugging the system and prematurely kill the application. You can do that by:

  1. Quit Aperture if its running
  2. Rename the existing library to something
  3. Start Aperture, which will create a new Library in your Pictures folder
  4. Quit Aperture
  5. Rename the new Library to something like "Temporary Aperture Library"
  6. Rename the old Library back to "Aperture Library"
  7. Start Aperture
  8. In the preferences, change the library location to "Temporary Aperture Library".
  9. Restart Aperture

Create XCode Project

Start XCode. Select File, then New, then Project.

In the New Project dialog, select Standard Apple Plug-ins and then select Aperture Export Plug-In.

200812152312.jpg

Select Choose...

200812152315.jpg

Select the location where you want to save the project and select Save. In my case, I saved it in "~/Projects/Aperture/MetaWeblogPlugIn".

Modifying the Xcode project

Build the Xcode project. You'll notice 27 errors in total. These errors forces us to specify values for the plug-in. All the errors are in "MetaWeblogPlugIn.m". To understand the changes, you'll need to reference the Aperture 2.1 SDK Reference. We should also call shouldCancelExport, shouldFinishExport and shouldBeginExport of _exportManager at the appropriate times. Here is the revised file that no longer fails to build and calls the respective methods of exportManager.

#import "MetaWeblogPlugIn.h"

@implementation MetaWeblogPlugIn

 - (id)initWithAPIManager:(id<PROAPIAccessing>)apiManager
{
  if (self = [super init])
  {
    _apiManager  = apiManager;
    _exportManager = [[_apiManager apiForProtocol:@protocol(ApertureExportManager)] retain];
    if (!_exportManager)
      return nil;
    
    _progressLock = [[NSLock alloc] init];
    
    // Finish your initialization here
  }
  
  return self;
}

- (void)dealloc
{
  // Release the top-level objects from the nib.
  [_topLevelNibObjects makeObjectsPerformSelector:@selector(release)];
  [_topLevelNibObjects release];
  
  [_progressLock release];
  [_exportManager release];
  
  [super dealloc];
}


#pragma mark -
// UI Methods
#pragma mark UI Methods

- (NSView *)settingsView
{
  if (nil == settingsView)
  {
    NSBundle *myBundle = [NSBundle bundleForClass:[self class]];
    NSNib *myNib = [[NSNib alloc] initWithNibNamed:@"MetaWeblogPlugIn" bundle:myBundle];
    if ([myNib instantiateNibWithOwner:self topLevelObjects:&_topLevelNibObjects])
    {
      [_topLevelNibObjects retain];
    }
    [myNib release];
  }
  
  return settingsView;
}

- (NSView *)firstView
{
  return firstView;
}

- (NSView *)lastView
{
  return lastView;
}

- (void)willBeActivated
{
  
}

- (void)willBeDeactivated
{
  
}

#pragma mark
// Aperture UI Controls
#pragma mark Aperture UI Controls

- (BOOL)allowsOnlyPlugInPresets
{
  return TRUE;  
}

- (BOOL)allowsMasterExport
{  
  return FALSE;  
}

- (BOOL)allowsVersionExport
{
  return TRUE;  
}

- (BOOL)wantsFileNamingControls
{
  return TRUE;  
}

- (void)exportManagerExportTypeDidChange
{
  
}


#pragma mark -
// Save Path Methods
#pragma mark Save/Path Methods

- (BOOL)wantsDestinationPathPrompt
{
  return FALSE;
}

- (NSString *)destinationPath
{
  return nil;
}

- (NSString *)defaultDirectory
{
  return nil;
}


#pragma mark -
// Export Process Methods
#pragma mark Export Process Methods

- (void)exportManagerShouldBeginExport
{
  [_exportManager shouldBeginExport];
}

- (void)exportManagerWillBeginExportToPath:(NSString *)path
{

}

- (BOOL)exportManagerShouldExportImageAtIndex:(unsigned)index
{
  return TRUE;
}

- (void)exportManagerWillExportImageAtIndex:(unsigned)index
{
  
}

- (BOOL)exportManagerShouldWriteImageData:(NSData *)imageData toRelativePath:(NSString *)path forImageAtIndex:(unsigned)index
{
  return TRUE;  
}

- (void)exportManagerDidWriteImageDataToRelativePath:(NSString *)relativePath forImageAtIndex:(unsigned)index
{
  
}

- (void)exportManagerDidFinishExport
{
  [_exportManager shouldFinishExport];
}

- (void)exportManagerShouldCancelExport
{
  [_exportManager shouldCancelExport];
}


#pragma mark -
  // Progress Methods
#pragma mark Progress Methods

- (ApertureExportProgress *)progress
{
  return &exportProgress;
}

- (void)lockProgress
{
  
  if (!_progressLock)
    _progressLock = [[NSLock alloc] init];
    
  [_progressLock lock];
}

- (void)unlockProgress
{
  [_progressLock unlock];
}

@end

Aperture plugins are located in one of the following locations:

  1. ~/Library/Application Support/Aperture/Plug-Ins/Export
  2. /Library/Application Support/Aperture/Plug-Ins/Export

By default, the Xcode project does not deploy the plug-in to either one of these locations. This is rather inconvenient, so lets modify the Xcode project to do so automatically. In the project, select Targets and then MetaWeblogPlugIn (or whatever you called your plugin). Right-click on the target MetaWeblogPlugIn and select Get Info.

200812152349.jpg

Now select the Build tab, and browse to the Deployment section. Ensure the Deployment Location is selected.

200812152353.jpg

Change the configuration to "Release" and repeat the step. Build the project again. You should now see your plug-in in the location "~/Library/Application Support/Aperture/Plug-Ins/Export". There is one more step we need to take before we can run the plug-in. We need to modify "info.plist" with information that will identify our plug-in. Double-click on the "info.plist".

200812152358.jpg

Change the "yourcompany" text in the Bundle Identiier to the name of your name, organization, company or website domain; the displayName to "Blog" (or any text you want to display in the menu); the helpURL to a location on your website, or that of your organization and the uuid to a unique UUID. To generate a UUID, start a terminal session, and enter uuidgen, followed by return.

200812160002.jpg

Replace the "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx" text with the text generated by uuidgen. In this case, uuidgen generated "090F0BE8-EA52-4794-9806-417C5DD6DEE8". Save "Info.plist" and rebuild the project.

Testing the plug-in

Start Aperture. Select File, then Export and notice that the plug-in is visible.

200812160013.jpg

Select Blog (or whatever you set the displayName to).

200812160015.jpg

You can modify a few settings and select Export when you are done. You will not be asked a location where to save the plug-in. The pictures are stored in the "Aperture Export" folder. Since we will upload the image to a blog in a future version of this plug-in, there is no reason for us worry about actually saving the image. It is just nice to know it works.

A final note... debugging

Currently its not possible to run Aperture and the plug-in from within the Xcode project. We can change that by adding an executable. In the Xcode project, right click on Executables and select New Custom Executable.

200812160024.jpg

The New Custom Executable dialog will appear.

200812160025.jpg

Enter Aperture as the executable name and "Applications/Aperture.app" as the executable path (assuming that is where Aperture is running from). Select Finish. The Executable "Aperture" Info dialog appears.

200812160027.jpg

Since we have nothing to modify, just close the window. If Aperture is running, terminate it. From Xcode, select Build and Go. Validate that Aperture starts up. Being adventurous? Try an debug your plugin.

Conclusion

This entry contains my crib notes on how to setup an Aperture project.

References

  1. There are Aperture Export Plug-In tutorials at http://www.bagelturf.com/

0 comments

MetaWeblog API & Objective-C: newPost

Looking at the MetaWeblog API, a post can several fields. I really hate it when methods are bloated with too many parameters, so I'll encapsulate the data related to a post in a "Post" class.

#import <Cocoa/Cocoa.h>

@interface Post : NSObject {
 NSString* title;
 NSCalendarDate* dateCreated;
 NSString* description;
 NSString* postId;
 NSString* blogId;
 NSString* summary;
 NSArray *categories;
 NSString* link;
 NSString* keywords;
 NSString* more;
}

@property(readwrite,copy) NSString* title;
@property(readwrite,copy) NSCalendarDate* dateCreated;
@property(readwrite,copy) NSString* description;
@property(readwrite,copy) NSString* postId;
@property(readwrite,copy) NSString* blogId;
@property(readwrite,copy) NSString* summary;
@property(readwrite,copy) NSArray *categories;
@property(readwrite,copy) NSString* link;
@property(readwrite,copy) NSString* keywords;
@property(readwrite,copy) NSString* more;

- (id) init;

@end

With the implementation not looking any more spectacular.

#import "Post.h"

@implementation Post

@synthesize title;
@synthesize dateCreated;
@synthesize description;
@synthesize postId;
@synthesize blogId;
@synthesize categories;
@synthesize summary;
@synthesize link;
@synthesize keywords;
@synthesize more;

- (id) init
{
 dateCreated = [NSCalendarDate init];
 description = @"";
 postId = @"";
 title = @"";
 summary = @"";
 categories = nil;
 link = @"";
 keywords = @"";
 more = @"";
 return self;
}

@end

Lets add a method to create a new post. First, I'll need a class to encapsulate the MetaWeblog API. Surprise, surprise, its called MetaWeblog. I made the design choice to initialize the instances of this class with the url, credentials and blog id that will be used to create new posts. My gut tells me it is required for all calls.

#import <Cocoa/Cocoa.h>
#import "Post.h"

@interface MetaWeblog : NSObject {
        NSString *url;  
        NSString *username;
        NSString *password;
        NSString *blogId;
}

@property(readwrite,copy) NSString *url; 
@property(readwrite,copy) NSString *username;
@property(readwrite,copy) NSString *password;
@property(readwrite,copy) NSString *blogId; 

- (id) initWithArgs: (NSString*)theUrl andUsername: (NSString*) theUsername andPassword: (NSString*) thePassword andBlogId: (NSString*) theBlogId;
- (NSString*)newPost: (Post*) post;

@end

The implementation looks daunting. In reality, it isn't.

#import "MetaWeblog.h"

@implementation MetaWeblog

@synthesize url; 
@synthesize username;
@synthesize password;
@synthesize blogId; 

- (id) initWithArgs: (NSString*)theUrl andUsername: (NSString*) theUsername andPassword: (NSString*) thePassword andBlogId: (NSString*) theBlogId
{
 url = theUrl;
 username = theUsername;
 password = thePassword;
 blogId = theBlogId;
 [super init]; 
 return self;
}

- (NSString*)newPost: (Post*) post
{
 // create local copies of the attributes
 NSString* title = post.title;
 NSString* summary = post.summary;
 NSString* body = post.description;
 NSDate* date = post.dateCreated;
 NSString* more = post.more;
 NSArray* categories = [[NSArray alloc] initWithArray: post.categories copyItems: YES];
 
 // configure parameters
 NSString *description = body;
 WSMethodInvocationRef rpcCall;
 NSURL *rpcURL = [NSURL URLWithString: url];
 NSString *methodName = @"metaWeblog.newPost";
 NSArray *postKey = [NSArray arrayWithObjects: @"description", @"dateCreated", @"title", @"mt_excerpt", @"mt_text_more", @"categories", nil];
 NSArray *postObject = [NSArray arrayWithObjects: description, date, title, summary, more, categories, nil];
 NSDictionary *post = [NSDictionary dictionaryWithObjects:postObject forKeys:postKey]; 
 NSArray *keys = [NSArray arrayWithObjects: @"blog_ID" , @"username", @"password", @"post", @"publish", nil];
 NSMutableArray *objects = [NSArray arrayWithObjects: blogId, username, password, post, kCFBooleanTrue, nil];
 NSDictionary *params = [NSDictionary dictionaryWithObjects:objects forKeys:keys];
 NSArray *order = [NSArray arrayWithObjects: @"blog_ID", @"username", @"password", @"post", @"publish", nil];
 
 // create the XMLRPC method and invoke it
 rpcCall = WSMethodInvocationCreate ((CFURLRef) rpcURL, (CFStringRef) methodName, kWSXMLRPCProtocol);
 WSMethodInvocationSetParameters (rpcCall, (CFDictionaryRef) params, (CFArrayRef) order);
 NSDictionary *result = (NSDictionary *) (WSMethodInvocationInvoke (rpcCall));
 
 // Check for errors
 if (WSMethodResultIsFault((CFDictionaryRef)result))
 {
  NSString* faultString = [result objectForKey: (NSString *) kWSFaultString];
  NSLog(@"[metaWeblog.newPost] failed = %@", result); 
  NSException* theException = [NSException exceptionWithName: @"MetaWeblogException" reason: faultString userInfo:nil]; 
  @throw theException; 
 }
 
 // Log the success
 NSLog(@"[metaWeblog.newPost] result = %@", result); 

 // Return the URL
 return (NSString*) [result objectForKey:(NSString*)kWSMethodInvocationResult ];
}

@end

Most of the call above is to configure the params and order variables. These are passed into the rpcCall which we create and then invoke. Lastly, we free resources and check for results. Upon success we return the url of the post that was created. Now lets use it to create a post in a weblog. I believe this should work against any weblog, including MovableType, Expression Engine or WordPress. I use ExpressionEngine so, I'm going to use that. One thing to note is that you must link against the "CoreServices" framework in order to compile and run this.

#import <Cocoa/Cocoa.h>
#import "MetaWeblog.h"

int main(int argc, char *argv[])
{
 NSString* url = @"https://example.com/index.php?ACT=52&id=82";
 NSString* username = @"Klaas";
 NSString* password = @"Vakie!";
 NSString* blogId = @"13";
 
 MetaWeblog* blog = [[MetaWeblog alloc] initWithArgs: url 
           andUsername:username 
           andPassword:password 
             andBlogId:blogId];
 Post* post = [[Post alloc] init];
 post.title = @"My first post";
 post.description = @"Tant Sannie; life is lekker here";
 [blog newPost:post];
 [post release];
 [blog release];      
 return 0;
}

Sure enough, running this code snippet results in a "My first post" in the blog. Go ahead, try it. In the next installment, I'll add the ability to upload an image to the server. For an aperture plug-in, this will be critical. I can't wait.

0 comments

MetaWeblog API & Objective-C: newMediaObject

Most blogging systems support the newMediaObject call. It simply uploads an file to a designated location on the server. You get a URL to the object back, which you can embed in your post. First things, first. Lets reopen the XCODE project and modify the MetaWeblog class.

#import <Cocoa/Cocoa.h>
#import “Post.h”

@interface MetaWeblog : NSObject {
 NSString *url; 
 NSString *username;
 NSString *password;
 NSString *blogId;
}

@property(readwrite,assign) NSString *url; 
@property(readwrite,assign) NSString *username;
@property(readwrite,assign) NSString *password;
@property(readwrite,assign) NSString *blogId; 

- (id) initWithArgs: (NSString*)theUrl 
     andUsername: (NSString*) theUsername 
      andPassword: (NSString*) thePassword 
            andBlogId: (NSString*) theBlogId;
- (NSString*)newPost: (Post*) post;
- (NSString*)newMediaObject: (NSString*)name 
                                 andType: (NSString*) type 
                                   andBits:(NSData*) bits;
@end

The implementation of newMediaObject is very similar to that of newPost. First, we setup the parameters, create and invoke the XMLRPC method and check results. Big deal. The beauty of CoreServices is that it takes care of everything. Absolutely sweet.

#import “MetaWeblog.h”

@implementation MetaWeblog

@synthesize url; 
@synthesize username;
@synthesize password;
@synthesize blogId; 

// other methods of the MetaWeblog class.

- (NSString*)newMediaObject: (NSString*)name andType: (NSString*) type andBits:(NSData*) bits
{
 WSMethodInvocationRef rpcCall;
 NSURL *rpcURL = [NSURL URLWithString: url];
 NSString *methodName = @“metaWeblog.newMediaObject”;
 NSArray *postKey = [NSArray arrayWithObjects: @“name”, @“type”, @“bits”, nil];
 NSArray *postObject = [NSArray arrayWithObjects: name, type, bits, nil];
 NSDictionary *post = [NSDictionary dictionaryWithObjects:postObject forKeys:postKey]; 
 NSArray *keys = [NSArray arrayWithObjects: @“blog_ID” , @“username”, @“password”, @“post”, nil];
 NSMutableArray *objects = [NSArray arrayWithObjects: blogId, username, password, post, nil];
 NSDictionary *params = [NSDictionary dictionaryWithObjects:objects forKeys:keys];
 NSArray *order = [NSArray arrayWithObjects: @“blog_ID”, @“username”, @“password”, @“post”, nil];
 rpcCall = WSMethodInvocationCreate ((CFURLRef) rpcURL, (CFStringRef) methodName, kWSXMLRPCProtocol);
 WSMethodInvocationSetParameters (rpcCall, (CFDictionaryRef) params, (CFArrayRef) order);
 
 NSDictionary *result = (NSDictionary *) (WSMethodInvocationInvoke (rpcCall));
 NSLog(@“[metaWeblog.newMediaObject] result = %@“, result);
 
 NSDictionary *values = [result objectForKey:(NSString*)kWSMethodInvocationResult ];
 return (NSString*) [values objectForKey: @“url”];
}

@end

So lets use it.

#import <Cocoa/Cocoa.h>
#import “MetaWeblog.h”

int main(int argc, char *argv[])
{
  NSString* url = @“https://example.com/index.php?ACT=52&id=82”;
  NSString* username = @“Klaas”;
  NSString* password = @“Vakie!“;
  NSString* blogId = @“13”;
 
  // allocate resources
  MetaWeblog* blog = [[MetaWeblog alloc] initWithArgs: url 
            andUsername:username 
           andPassword:password 
             andBlogId:blogId];
  Post* post = [[Post alloc] init];
  NSData* data = [NSData dataWithContentsOfFile: @“/Users/Bloudraak/Pictures/TantSannie.jpg”];
  NSString* mediaUrl = [blog newMediaObject: @“TantSannie.jpg” andType: @“image/jpg” andBits:data];
 
  // create post
  post.title = "My first post";
  post.summary = [NSString stringWithFormat:"<img src="%@" />", mediaUrl];
  post.description = @“Hello Tant Sannie; life is lekker here”;
  [blog newPost:post];
 
  // free resources
  [post release];
  [blog release];    
  [data release];       
  return 0;
}

The code should be self explanatory. First, we load the contents of the image, then upload it using newMediaObject. The result is a url which we convert into an IMG tag and add to the summary of the blog. You can now do all sorts of styling, if you wish, to ensure the image is correctly displayed. This gives me the minimum functionality to get started with my Aperture Plug-In. That said, expect this to be expanded in the near future.

.

0 comments

CollabNet SourceForge Enterprise Edition and .NET

CollabNet claims that CollabNet SourceForge® Enterprise Edition is the number one platform for distributed teams. Implemented as a web application, it enables distributed teams to share the same source code repositories, issue tracking system and collaborate. It enables external systems, such as CruiseControl, to interact with the system through one of its many web services. However, consuming those services via .NET is a challenge because some WSDL do not validate, nor are there any examples of how to consume services from .NET. Hopefully CollabNet will sort things out in the next release.

Generating Service References

Create a directory where you want to generate service references. For this examples, lets use C:SFEE. In c:sfee, execute the svcutil command line. This will generate a file called SourceForge.cs.

svcutil 
  http://sfee-demo/sf-soap43/services/DiscussionApp?wsdl 
  http://sfee-demo/sf-soap43/services/DocumentApp?wsdl
  http://sfee-demo/sf-soap43/services/FrsApp?wsdl
  http://sfee-demo/sf-soap43/services/IntegrationDataApp?wsdl
  http://sfee-demo/sf-soap43/services/NewsApp?wsdl
  http://sfee-demo/sf-soap43/services/RbacApp?wsdl
  http://sfee-demo/sf-soap43/services/ScmApp?wsdl
  http://sfee-demo/sf-soap43/services/SourceForge?wsdl 
  http://sfee-demo/sf-soap43/services/TaskApp?wsdl
  http://sfee-demo/sf-soap43/services/TrackerApp?wsdl
  http://sfee-demo/sf-soap43/services/WikiApp?wsdl 
    /out:SourceForge.cs /n:*,Sfee

At this time, references cannot be generated for FileStorageApp or SimpleFileStorageApp because the schema for namespace "http://xml.apache.org/xml-soap" doesn't exist. Once you have generated service references, you are ready to call the SFEE project.

Consuming the services

Before you can really do anything with the SFEE web services, you need to login. You can do this by calling the SourceForge web service. The login method returns a session id (or token) that you will pass to other methods. Here is an example showing how to list the projects of admin.

SourceForgeSoapClient client = new SourceForgeSoapClient();
string session = client.login("admin", "admin");
ProjectSoapList projects = client.getProjectList(session);
foreach (ProjectSoapRow row in projects.dataRows)
{
  Console.WriteLine(row.title);
}
client.logoff("admin", session);
client.Close();

Working with Projects

List projects of logged in user
SourceForgeSoapClient client = new SourceForgeSoapClient();
string session = client.login("admin", "admin");
// this is interesting.
ProjectSoapList projects = client.getProjectList(session);
foreach (ProjectSoapRow row in projects.dataRows)
{
  Console.WriteLine("=== {0} ===", row.title);
  Console.WriteLine(row.id);
  Console.WriteLine(row.path);
  Console.WriteLine(row.description);
  Console.WriteLine(row.dateCreated);
}
client.logoff("admin", session);
client.Close();
Getting the project when you have an project id

In this example, we get the project data for a project with the id "proj1039" and print its details.

SourceForgeSoapClient client = new SourceForgeSoapClient();
string session = client.login("admin", "admin");
ProjectSoapDO project = client.getProjectData(session, "proj1039");
Console.WriteLine(@"=== {0} ===", project.title);
Console.WriteLine(project.id);
Console.WriteLine(project.path);
Console.WriteLine(project.description);
Console.WriteLine(project.createdDate);
Console.WriteLine(project.createdBy);
Console.WriteLine(project.lastModifiedBy);
Console.WriteLine(project.lastModifiedDate);
Console.WriteLine(project.version);
client.logoff("admin", session);
client.Close();

Working with the Wiki

Getting text of the homepage wiki page
SourceForgeSoapClient client = new SourceForgeSoapClient();
string session = client.login("admin", "admin");
WikiAppSoapClient wiki = new WikiAppSoapClient();
WikiPageSoapDO page = wiki.getWikiPageDataByName(session, "proj1019", "HomePage");
Console.WriteLine(page.wikiText);
client.logoff("admin", session);
client.Close();
Getting text of a specific wiki page

This snippet gets the page with a specific id and prints it to the console.

SourceForgeSoapClient client = new SourceForgeSoapClient();
string session = client.login("admin", "admin");
WikiAppSoapClient wiki = new WikiAppSoapClient();
WikiPageSoapDO page = wiki.getWikiPageData(session, "wiki1026");
Console.WriteLine(page.wikiText);
client.logoff("admin", session);
client.Close();
Creating a new page
SourceForgeSoapClient client = new SourceForgeSoapClient();
string session = client.login("admin", "admin");
WikiAppSoapClient wiki = new WikiAppSoapClient();
wiki.createWikiPage(session, 
   "proj1019", 
   "TestResults", 
   "This is the result of tests we wrote",
   "Some Comment");
client.logoff("admin", session);
client.Close();
Updating an existing page

This snippet gets the text from the home page and prepends a bullet after the "List" heading.

SourceForgeSoapClient client = new SourceForgeSoapClient();
string session = client.login("admin", "admin");
WikiAppSoapClient wiki = new WikiAppSoapClient();
WikiPageSoapDO page = wiki.getWikiPageDataByName(session, "proj1019", "HomePage");
string text = page.wikiText;
const string title = "!!List";
if(string.IsNullOrEmpty(text))
{
  text = string.Format("!!!Headingnn{0}", title);
}
int index = text.LastIndexOf(title);
index += title.Length;
text = text.Insert(index, "n* Bullet ");
page.wikiText = text;
wiki.setWikiPageData(session, page);
client.logoff("admin", session);
client.Close();
Uploading XUnit.net test results to a wiki page

First, you need to save the results of the XUnit.net script to an xml file. Lets assume you save it to C:test.xml.

SourceForgeSoapClient client = new SourceForgeSoapClient();
string session = client.login("admin", "admin");
WikiAppSoapClient wiki = new WikiAppSoapClient();

// Update a specific page
WikiPageSoapDO page = wiki.getWikiPageData(session, "wiki1026");
StringWriter writer = new StringWriter();
 
// transform the XML to a wiki page
XPathDocument doc = new XPathDocument(@"c:test.xml");
XslTransform transform = new XslTransform() ;
transform.Load("xunit.xslt");
transform.Transform(doc, null, writer);
 
// update the wiki
page.wikiText = writer.ToString();
wiki.setWikiPageData(session, page);
client.logoff("admin", session);
client.Close();

The XUnit.XSLT may look as follows. The SourceForge Wiki syntax is very sensitive to spaces, so its recommended that you change the XSLT to format correctly. For example, you have to ensure that no newlines are generated in the data of a bullet point.

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="<a href="http://www.w3.org/1999/XSL/Transform" class="external free" title="http://www.w3.org/1999/XSL/Transform" rel="nofollow">http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="html"/>

 <xsl:template match="/">
!!!Functional Test Results
!!Summary
* Tests run: <xsl:value-of select="sum(//assembly/@total)"/>
* Tests run: <xsl:value-of select="sum(//assembly/@total)"/>
* Failures: <xsl:value-of select="sum(//assembly/@failed)"/>
* Skipped: <xsl:value-of select="sum(//assembly/@skipped)"/>
* Run time: <xsl:value-of select="sum(//assembly/@time)"/>

!!Tests
   <xsl:if test="//assembly/class/test[@result='Fail']">
!Failed Tests
     <xsl:apply-templates select="//assembly/class/test[@result='Fail']">
       <xsl:sort select="@name"/>
     </xsl:apply-templates>
   </xsl:if><xsl:if test="//assembly/class/test[@result='Skip']">
!Skipped Tests
     <xsl:apply-templates select="//assembly/class/test[@result='Skip']">

       <xsl:sort select="@name"/>
     </xsl:apply-templates>
   </xsl:if><xsl:if test="//assembly/class/test[@result='Pass']">
!Passed Tests
     <xsl:apply-templates select="//assembly/class/test[@result='Pass']">
       <xsl:sort select="@name"/>
     </xsl:apply-templates>

   </xsl:if>
 </xsl:template>

 <xsl:template match="test">
*(<xsl:apply-templates select="traits/trait[@name='Category']">
     <xsl:sort select="@name"/>
   </xsl:apply-templates>) <xsl:value-of select="@name"/>

 </xsl:template>
 <xsl:template match="trait">
   <xsl:value-of select="@value"/>
 </xsl:template>
</xsl:stylesheet>

Working with the File Releases

Getting a list of packages

The following snippet gets a list of packages for project with id "proj1019".

SourceForgeSoapClient client = new SourceForgeSoapClient();
string session = client.login("admin", "admin");
const string projectId = "proj1019";
FrsAppSoapClient frs = new FrsAppSoapClient();
PackageSoapList packageList = frs.getPackageList(session, projectId);
foreach (PackageSoapRow row in packageList.dataRows)
{
   PackageSoapDO package = frs.getPackageData(session, row.id);
   Console.WriteLine(package.title);
   Console.WriteLine(package.description);
   Console.WriteLine(package.version);
   Console.WriteLine(package.lastModifiedBy);
   Console.WriteLine(package.lastModifiedDate);
   Console.WriteLine(package.parentFolderId);
   Console.WriteLine(package.path);
   Console.WriteLine();
}
client.logoff("admin", session);
client.Close();
Getting a list of releases for a package

The following snippet gets a list of releases in package "pkg1088". This may be particular useful if you want to have some automated process to download and do something with the latest release.

SourceForgeSoapClient client = new SourceForgeSoapClient();
string session = client.login("admin", "admin");
FrsAppSoapClient frs = new FrsAppSoapClient();
const string packageId = "pkg1088";
ReleaseSoapList releases = frs.getReleaseList(session, packageId);
foreach (ReleaseSoapRow row in releases.dataRows)
{
   ReleaseSoapDO release = frs.getReleaseData(session, row.id);
   Console.WriteLine("{0} ({1})", release.title, release.createdDate);
}
client.logoff("admin", session);
client.Close();

0 comments