Amazon Web Services Utility

Amazon Web Services (AWS) provides a powerful tool for finding and evaluating just about any type of material goods. Whether you are searching by keywords or the UPC code, AWS can provide just about all the information you will ever need to know about a product. However, Amazon has put a lot of hurdles between you and the information you needs. First, as of August 15, 2009, AWS now requires that you sign all your requests. Secondly, they have terrible documentation and confusing error messages that do not always explain how to fix the problem.

Amazon Web Services Utility is a YUI 3 JavaScript/PHP solution that simplifies interacting with AWS by handling the request signing process automatically and securely, and prevening or handling common errors. In addition to returning the result as XML, there is an additional gallery-aws-json package that adds functionality to parse the XML data into JSON objects.

Step 1: Getting Started

The first thing you need to do is get an AWS account (if you dont already have one).

http://aws.amazon.com/

Then go to the "Your Account" tab and the "Security Credentials" link. If you do not have an "Access Key" setup, click the "Create a new Access Key" link and create one. You will need both the "Access Key ID" and the "Secret Access Key" for your Amazon Web Services Utility.

Then download the following 4 files:
http://code.google.com/p/yui-ext-mvc/source/browse/trunk/getAWSSignedUrl.txt (change extension to ".php")
http://code.google.com/p/yui-ext-mvc/source/browse/trunk/assets/js/yahoo-3-ext/aws.js
http://code.google.com/p/yui-ext-mvc/source/browse/trunk/assets/js/yahoo-3-ext/aws-json.js
http://code.google.com/p/yui-ext-mvc/source/browse/trunk/assets/files/io.swf

Step 2: getAWSSignedUrl.php

This file is used to securely create the AWS signed url and should be placed on the web-server you wish to use AWS Utility on. You will need to replace "YOUR_PUBLIC_ACCESS_KEY_ID" and "YOUR_SECRET_ACCESS_KEY" with the ones you created in Step 1. You may also want to replace the $associateTag value with your own associate tag, if you want to be paid anytime someone purchases a product on Amazon using the links generated by this utility.

Ulrich Mierendorff wrote the algorithm that we are using to sign the URLs in aws_signed_request by creating a hash of the request you wish to make using HMAC with SHA256 and base64-encoding.

The signing algorithm is server-side because we do not want our secret access keys available to public, and therefore they cannot be in JavaScript. So anytime we want to make an AWS request, two AJAX requests are required: one to "getAWSSignedUrl.php" to fetch the AWS signed URL, and then one using the signed URL to AWS.

Step 3: aws.js

The Y.Aws has 4 configuration properties: awsSignedUrl is the URL for the "getAWSSignedcUrl.php" and defaults to /getAWSSignedUrl.php; ioSWFURL is the URL for the XDR SWF (required to use JavaScript to make cross-domain request) and defaults to /assets/files/io.swf; operation is the type of AWS Operation to make and defaults to ItemLookup, which is used for ISBN, UPC, and ASIN searches; responseGroup is the type of AWS response you would like and defaults to Small, which contains most of the relevant information about a product. To use the utility, simply call the getXML function.

getXML: function(conf, callback, ctx) {
	if (_isReady) {
		var cfg = Y.Lang.isObject(conf) ? conf : {},
			url = this.get(awsSignedUrl),
			operation = this.get(operation),
			responseGroup = this.get(responseGroup),
			context = ctx || this;

		if (_timer) {_timer.cancel();}

		// build URL to fetch AWS URL
		if (-1 === url.indexOf(?)) {url += ?;}
		url += &ResponseGroup= + responseGroup + &Operation= + operation;

		Y.each(cfg, function(value, key) {
			url += & + key + = + value;
		});

		// fetch AWS URL
		Y.io(url, {on: {success: function(id,o) {
			try {
				// parse AWS URL
				var data = Y.JSON.parse(o.responseText);

				if (! data.aws_request) {
					// todo: update message to show missing parameters
					alert(AWS request is missing required parameters);
					return;
				}

				Y.io(data.aws_request, {context: context, on:{success: function(id,o) {
					// this is intentional, I cannot get "Y.DataSchema.XML" to work with namespaces
					var responseText = o.responseText.replace(/xmlns=".*?"/, ), code, msg; // remove the namespace
					o.responseXML = Y.DataType.XML.parse(responseText); // convert to XML

					code = o.responseXML.getElementsByTagName(Code)[0];
					msg = o.responseXML.getElementsByTagName(Message)[0];

					// show AWS errors
					if (code && msg && code.firstChild.nodeValue) {
						alert(code.firstChild.nodeValue + "\n" + msg.firstChild.nodeValue);
						return;
					}

					callback.apply(this, arguments);
				}}, xdr:{/*dataType:xml, */responseXML:false, use:flash}});
			}
			catch (e) {
				alert(Call to getAWSSignedUrl was not parsable);
			}
		}}});
	}
	else {
		_timer = Y.later(_timeoutLength, this, this.getXML, arguments);
		_timeoutLength += _timeoutStep;
	}
}

There are three parameters to passing into this function: conf the parameters to use for the request, callback the callback function to execute when AWS data returns, and ctx the execution context of the callback. The function first checks if the XDR SWF is ready, then fetches a few parameters from the configuration. Using those parameters and the provided conf a URL is constructed. That URL is passed to "getAWSSignedUrl.php" and signed. If "getAWSSignedUrl.php" validated the URL and successfully signed it, a request is make to AWS using the XDR SWF. When that returns the XML namespace needs to be removed as there is a bug with Y.DataSchema.XML that causes it to not be able to parse the XML when a namespace is defined. If there was an error message, then it is shown, otherwise the arguments and context are all passed into the callback function. The callback function should expect the same parameters that are returned from any YUI 3 IO call.

Step 4: aws-json.js

This addon augments the Y.Aws.prototype with a new method getJSON, which accepts the same parameters as getXML. The difference is that the object passed as the second argument of the callback will also have a responseJSON object in addition to responseText and responseXML. This JSON object will be the parsed XML, and changes depending on the ResponseGroup used. Currently only "Medium", "Small", and the several ResponseGroups that make those two up are supported. Until I finish implementing a parse for all ResponseGroups, it might be best to use getXML if you know you are going to need something that is unsupported.

getJSON: function(item, callback, ctx) {
	this.getXML(item, function(id, o) {
		if (o.responseXML) {
			var json = this._parseAWSContent(o.responseXML);

			if (json) {
				o.responseJSON = json;
				callback.apply(ctx, arguments);
			}
			else {
				alert(AWS request is missing the response type);
			}
		}
		else {
			alert(AWS request failed.);
		}
	}, ctx);
},
_parseAWSContent: function(doc) {
	var args = Y.DataSchema.XML.apply(AWS_SCHEMA.args, doc),
		response = ,
		responseGroup = Y.Array.find(args.results, function(arg) {
			return NAME_RESPONSE_GROUP === arg.name;
		});

	if (responseGroup.value) {
		response = Y.DataSchema.XML.apply(AWS_SCHEMA[responseGroup.value], doc);
	}

	return response;
}

The getJSON function leverages getXML, and simply parses the XML using _parseAWSContent if responseXML is available. The parsing function uses Y.DataSchema.XML to parse the XML. There various schemas available are ordered by ResponseGroup in the beginning of aws-json.js.

Using AWS Utility

var aws = new Y.Aws(); // using defaults

// fetch XML
aws.getXML({ItemId:978-0470227800, IdType: UPC}, function(id, o) {
	// your code, using o.responseXML
});

// fetch JSON
aws.getJSON({ItemId:978-0470227800, IdType: UPC&rsquot;}, function(id, o) {
	// your code, using o.responseJSON
});

This will fetch "Professional JavaScript for Web Developers" by Nichalos Zakas from AWS. I have also included an AWS Test Page so you can play around with your own searches.