Cocoa Amazon Web Services.

While working on a mp4 meta data tagger application, I wanted to use the Amazon Web service API to get the meta data info for the video files.  I remember seeing an example of this in one of my readings.  I found it as one of the chapters in Aaron Hillgass book Cocoa(R) Programming for Mac(R) OS X (3rd Edition).  However, the info was somewhat outdated as Amazon has changed the Restful request requirements.  What follows below is how I accomplished this.  While I would like to claim most of this work my own, that would not be true.  The vast majority of the info was garnered from this post on the bignerd ranch forum.  I simply clarified, adapted and expanded the info to present in this page. The following is the AmaZone project from the book adapted to grab video information.  I would highly suggest reading the chapter to get an idea of what needs to be accomplished.

Creating a Cocoa application that accesses Amazon’s web service requires a quite of bit of coding and time.  Below is a list of requirements to get this going.

1.  Sign up for Amazon Web Service (AWS).

2.  Your Access Key and Secret Key from the AWS service. These can be found under the Account tab Security Credentials.

AWS can be accessed through SOAP or Restful request.  The Restful services is the newer of the two services ad is probably the simplest route to go.  To access the service you will use your Access Key, which any one can see, and your secret key, which is used as part of a Base64 encoding scheme to produce a signature.  Through this process, AWS can assure that you are who you claim to be and no one is tampering with your request.

Open the project in Xcode.

To make this process simpler, start by using the existing code from the book found here on the download examples button.  Once the download is complete, navigate to the AmaZone directory and open the project file.

amazonepict.jpg

Adding the OpenSSL libcrypto.dylib to the AmaZone project.

Libcrypto.dylib is part of the OpenSSL Cryptography and SSL/TLS ToolKit and is included in the Xcode developer Frameworks. We will use this framework to pass the html request to for encoding into a Base64 string. This will create the signature necessary for the Restful request to be successful.  To add it, simply right click on Frameworks in the Groups & Files pane in Xcode and Select Add –> Existing Frameworks.  In the frameworks search window it may be helpful to filter the search to “Dylibs” in the drop down menu.  This should bring up libcrypto.dylib and selecting it should add it to the projects frameworks.

framworkssearch.jpg

Making Changes to the AppController file.

In the AppController.m file add the following #includes.

#import <openssl/ssl.h> //for BIO, etc

#import <CommonCrypto/CommonHMAC.h> //for kCCHmacAlgSHA256

#import <CommonCrypto/CommonDigest.h> //for CC_SHA256_DIGEST_LENGTH


Also add your Access Key and Secret Key.

#define AWS_ID @“AKXAJDX2BXQWOKM47XXX”

#define SECRET_KEY @“v111+11111u1i11111hhxVYvzPC1111111111111″

Now we need two things to happen

1.  encode the string into a Base64String.

2.  Make an NSString from the Base64String.

To accomplish this create two categories.

While still in AppController.m we add a method to NSData to accomplish the encoding.  This is done this by creating a Category for NSData .

Insert this before the implementation statement for AppController.

@interface NSData (Base64)

 

- (NSString *)encodeBase64;

- (NSString *)encodeBase64WithNewlines: (BOOL) encodeWithNewlines;

 

@end

 

@implementation NSData (Base64)

 

- (NSString *)encodeBase64

{

return [self encodeBase64WithNewlines: NO];

}

 

- (NSString *)encodeBase64WithNewlines: (BOOL) encodeWithNewlines

{

// Create a memory buffer which will contain the Base64 encoded string

BIO * mem = BIO_new(BIO_s_mem());

// Push on a Base64 filter so that writing to the buffer encodes the data

BIO * b64 = BIO_new(BIO_f_base64());

if (!encodeWithNewlines)

BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);

mem = BIO_push(b64, mem);

// Encode all the data

BIO_write(mem, [self bytes], [self length]);

BIO_flush(mem);

// Create a new string from the data in the memory buffer

char * base64Pointer;

BIO_get_mem_data(mem, &base64Pointer);

// deprecated

// long base64Length = BIO_get_mem_data(mem, &base64Pointer);

// NSString * base64String = [NSString stringWithCString: base64Pointer

// length: base64Length];

BIO_get_mem_data(mem, &base64Pointer);

NSString * base64String = [NSString stringWithUTF8String: base64Pointer];

// Clean up and go home

BIO_free_all(mem);

return base64String;

}

 

@end

Now we need to add a method to NSString so we can take the encoded base64String and form a proper URL. Again this is added before the @implementation statement in the AppController.m file.

@interface NSString (ForURL)

 

- (NSString*)reallyEncodeURL;

 

@end

 

@implementation NSString (ForURL)

 

- (NSString*)reallyEncodeURL

{

return (NSString*)CFURLCreateStringByAddingPercentEscapes(

NULL,

(CFStringRef)self,

NULL,

(CFStringRef)@”!*’();:@&=+$,/?%#[]“,

kCFStringEncodingUTF8 );

}

 

@end

Now we need to direct the old fetchBooks method to use the encoding.  Here I also varied from the original AmaZone.  The original AmaZone was a book search while this is a video search.  If you notice in the response_group I included the cover image as well as the Editorial review (summary).

Replace the old fetchBooks method with this one.

- (void)fetchBooks:(id)sender

{

// Show the user that something is going on

[progress startAnimation:nil];

 

// Put together the request

// See http://www.amazon.com/gp/aws/landing.html

 

// Get the string and percent-escape for insertion into URL

NSString *searchString = [[searchField stringValue] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

NSLog(@”searchString = %@”, searchString);

// Create the URL

//Timestamp format : needs to be yyyy-mm-ddThh:mm+/-timediff , it’s also set to tomorrow to avoid Request Expired error.

NSDate *now = [[NSDate alloc] initWithTimeIntervalSinceNow:24 * 60 * 60];

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];

[dateFormatter setDateFormat:@"yyyy-MM-dd'T'hh:mm:ssZ"];

NSString *timestamp = [dateFormatter stringFromDate:now];

NSString * encoded_timestamp = [timestamp reallyEncodeURL];

NSLog(@”Date formatted %@ => %@”, timestamp, encoded_timestamp);

// parameters have to be binary sorted

NSString *service_URL = @”ecs.amazonaws.com”;

NSString *service_path = @”/onca/xml”;

NSString *access_ID = [NSString stringWithFormat:@"AWSAccessKeyId=%@&",AWS_ID];

NSString *keywords = [NSString stringWithFormat:@"Keywords=%@&",searchString];

NSString *operation = @”Operation=ItemSearch&”;

NSString *response_group = @”ResponseGroup=ItemAttributes%2CImages%2CEditorialReview&”;

NSString *search_index = @”SearchIndex=Video&”;

NSString *service = @”Service=AWSEcommerceService&”;

NSString *signature_method = @”SignatureMethod=HmacSHA256&”;

NSString *signature_version = @”SignatureVersion=2&”;

NSString *ts = [NSString stringWithFormat:@"Timestamp=%@&", encoded_timestamp];

NSString *version = @”Version=2010-01-01″;

//Signature

NSString *string_to_sign = [NSString stringWithFormat:@"GET\n%@\n%@\n%@%@%@%@%@%@%@%@%@%@",

service_URL, service_path, access_ID, keywords, operation, response_group,

search_index, service, signature_method, signature_version, ts, version];

NSLog(@”String to sign = %@”, string_to_sign);

NSString *key = SECRET_KEY;

NSString *data = string_to_sign;

const char *cKey = [key cStringUsingEncoding:NSASCIIStringEncoding];

const char *cData = [data cStringUsingEncoding:NSASCIIStringEncoding];

unsigned char cHMAC[CC_SHA256_DIGEST_LENGTH];

CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), cData, strlen(cData), cHMAC);

NSData *HMAC = [[NSData alloc] initWithBytes:cHMAC

length:sizeof(cHMAC)];

NSString *hash = [HMAC encodeBase64];

NSString *encoded_hash = [hash reallyEncodeURL];

NSLog(@”hash = %@- => %@”, hash, encoded_hash);

// Create the URL

NSString *urlString = [NSString stringWithFormat:@"http://%@%@?%@%@%@%@%@%@%@%@%@%@&Signature=%@",

service_URL, service_path, access_ID, keywords, operation, response_group,

search_index, service, signature_method, signature_version, ts, version, encoded_hash];

NSLog(@”urlString = %@”, urlString); // you can paste this in your web browser to see the XML returned

NSURL *url = [NSURL URLWithString:urlString];

NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url

cachePolicy:NSURLRequestReturnCacheDataElseLoad

timeoutInterval:30]; 

// Fetch the XML response

NSData *urlData;

NSURLResponse *response;

NSError *error;

urlData = [NSURLConnection sendSynchronousRequest:urlRequest

returningResponse:&response

error:&error];

if (!urlData) {

NSRunAlertPanel(@”Error loading”, @”%@”, nil, nil, nil, [error localizedDescription]);

return;

}

 

// Parse the XML response

[doc release];

doc = [[NSXMLDocument alloc] initWithData:urlData

options:0

error:&error];

NSLog(@”doc = %@”, doc);

 

 

//ShowTree(doc, 0);

 

 

if (!doc) {

NSAlert *alert = [NSAlert alertWithError:error];

[alert runModal];

return;

}

[itemNodes release];

itemNodes = [[doc nodesForXPath:@"ItemSearchResponse/Items/Item" error:&error] retain];

if (!itemNodes) {

NSAlert *alert = [NSAlert alertWithError:error];

[alert runModal];

return;

}

 

// Update the interface

[tableView reloadData];

[progress stopAnimation:nil];

}

 

We also need to change the tableview data source methods.

- (id)tableView:(NSTableView *)tv objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row

{

NSXMLNode *node = [itemNodes objectAtIndex:row];

NSString *xPath = [tableColumn identifier];

NSLog(@”XPATH = %@”, xPath);

 

if ([[tableColumn identifier] isEqual:@”Image”])

{

NSString *imageXPath = @”SmallImage/URL”;

NSString *urlString = [self stringForPath:imageXPath ofNode:node];

if (urlString) {

NSURL *url = [NSURL URLWithString:urlString];

NSImage *image = [[NSImage alloc] initWithContentsOfURL:url];

return image;

} else {

return nil;

}

}

return [self stringForPath :x Path ofNode:node];

}

@end

 

Since we have changed the content to video and added images as well as Editorial reviews we need to add two columns in the Tableview.

AmazoneUI.jpg

Add 2 columns.  One with header titled images the other titled Description.  Drag an image cell from the UI library to the Image column. Now run the application and you should get results for your video.

One Response

  1. Anderson - June 10, 2011

    Hi
    I follow your steps and just works on the console.
    All data get back, but nothing shows on the interface.

    Al have identifier like this: ItemAttributes/ASIN, ItemAttributes/Title,…

    Any help?

    Thank you so much

Leave a Reply