Charles Engelke's Blog

December 28, 2011

Chrome Web App Bookshelf – Part 4

Filed under: Uncategorized — Charles Engelke @ 4:25 pm

Note: this is part 4 of the Bookshelf Project I’m working on. Part 1 was a Chrome app “Hello, World” equivalent, part 2 added basic functionality, and part 3 finally called an Amazon web service. This part will finally parse the web service result and refine the call.

When I got to the end of the last post, the app was just dumping a lot of XML, formatted as text, into the web page. I want to pull just the data fields I’m looking for, format them, and put that in the page instead. Those fields are: author, title, release date, availability, list price, Amazon’s price, and a link to the Amazon page. So I copied the text from the page to a file and viewed it in a program that showed the XML as an outline. It’s way too big to show here, but the structure of the response looked like this:

  • ItemLookup
    • Operation Request
    • Items
      • Request
      • Item
        • ASIN
        • DetailPageURL
        • ItemAttributes

It’s that Item element that actually has the response in it, with fields inside ItemAttributes containing most of the information I want. I see Author, Title, PublicationDate, and ListPrice inside of ItemAttributes, and DetailPageURL has the link I need. But I’m missing the availability and Amazon’s price. So back to the ItemLookup function’s documentation, which I’m a bit more ready to understand now. My request can include a specification of one or more ResponseGroup values. The default is just ItemAttributes, which is what I got in this sample response. But other groups might have the two fields I’m missing. After browsing through the documentation, I see that Offers includes both the Amount and Availability, so I’ll add that to my request. Doing so is easy: just change the line that specifies the requested ResponseGroup to:

      params.push({name: "ResponseGroup", value: "ItemAttributes,Offers"});

The resulting XML has what I need, structured as follows:

  • ItemLookup
    • Operation Request
    • Items
      • Request
      • Item
        • ASIN
        • DetailPageURL
        • ItemAttributes
        • OfferSummary
        • Offers
          • Offer
            • OfferListing

The extra information I’m looking for is in the OfferListing element, which contains Price and Availability fields. The Price field (like the ListPrice field mentioned earlier) is itself complex, containing an Amount (an integer, apparently equal to the whole number of cents), a CurrencyCode (USD in my example), and a FormattedPrice. I’m going to go with the FormattedPrice field for now, but I may want to change my mind later.

Okay, this XML has the data I want, how do I get to it? This is the job of extractAndReturnResult, which currently looks like:

      function extractAndReturnResult(data, status, xhr){
         onSuccess(xhr.responseText);
      }

I’m going to put a breakpoint in this function and examine the data, status, and xhr objects that are returned when I run the code. The status object is just a string with the word “success” in it, but data and xhr are much more complex. data is a Document that represents the DOM of the returned XML. xhr has many fields in it, one of which, responseXML, is also a Document. In fact, the debugger tells me that it is the exact same object as data. I can traverse this DOM to get the elements I want. There are native JavaScript ways to do this, but since I’ve already started using jQuery to traverse the web page’s DOM, I’m going to continue to use it to traverse this one.

For example, I can get the element ASIN by searching for element ASIN within Item within Items within the document, using:

         $(data).find("Items Item ASIN")

Actually, though, that will find an array of matching elements which might be empty. To keep it simple at this stage I’m just going to assume that the array has at least one element, and will take the first one as my result. Then I’ll find the text inside that element. That ends up with:

         var asin = $(data).find("Items Item ASIN")[0].textContent;

I’ll change the overall behavior of extractAndReturnResult to pass an object to the success handler instead of just a string, ending up with:

      function extractAndReturnResult(data, status, xhr){
         var result = {
            asin:          $(data).find("Items Item ASIN")[0].textContent,
            author:        $(data).find("Items Item ItemAttributes Author")[0].textContent,
            title:         $(data).find("Items Item ItemAttributes Title")[0].textContent,
            releaseDate:   $(data).find("Items Item ItemAttributes PublicationDate")[0].textContent,
            listPrice:     $(data).find("Items Item ItemAttributes ListPrice FormattedPrice")[0].textContent,
            availability:  $(data).find("Items Item Offers Offer OfferListing Availability")[0].textContent,
            amazonPrice:   $(data).find("Items Item Offers Offer OfferListing Price FormattedPrice")[0].textContent,
            url:           $(data).find("Items Item DetailPageURL")[0].textContent
         };
         onSuccess(result);
      }

Now I have to change the function that gets this result and puts it into the web page, since it’s no longer getting a string. That function used to be an inline function in the main.js file:

                     function(message){
                        message = message.replace(/&/g, "&");
                        message = message.replace(/</g, "&lt;");
                        message = message.replace(/>/g, "&gt;");
                        $("#results").append(message);
                     },

I’m going to change this to refer to a named function called insertResponse, and define that function, shown below:

      function insertResponse(response){
         var html = '<a href="' + response.URL + '">';
         html = html + response.title + '</a> by ' + response.author;
         html = html + ' lists for ' + response.listPrice;
         html = html + ' but sells for ' + response.amazonPrice;
         html = html + '. It was released on ' + response.releaseDate;
         html = html + ' with availability ' + response.availability;
         html = html + '.';
         $("#results").append(html);
      }

It’s verbose, but shows the information. When I look up the same book now, I get a more usable response than before:

JavaScript: The Definitive Guide: Activate Your Web Pages (Definitive Guides) by David Flanagan lists for $49.99 but sells for $31.49. It was released on 2011-05-10 with availability Usually ships in 24 hours.

That’s a good stopping point. There’s still a lot to do before this is a releasable web app. At the very least, I need to check for empty responses and escape any special characters in the data I display. I also want to maintain a list of books, not just look up a single book, and have that list persist between different invocations of this program. So there’s plenty more to come.

Advertisement

December 11, 2011

Chrome Web App Bookshelf – Part 3

Filed under: Uncategorized — Charles Engelke @ 11:21 am

Note: this is part 3 of the Bookshelf Project I’m working on. Part 1 was a Chrome app “Hello, World” equivalent, and part 2 added basic functionality. This part will finally call an Amazon Web Service via JavaScript.

I’ve been approaching this project from the top down so far, starting with creating a nearly empty shell as a Chrome app, then putting in the necessary logic to make it perform a minimal function. The next thing to add is actually calling the Amazon Web Service that looks up an ISBN and returns information about the product. For that, I’m going to switch to a more bottom-up point of view, focusing at first on just that web service call.

I’m going to put the JavaScript for talking to AWS into a separate file called aws.js. That file needs to be loaded into the web page before any file that references it, and after any file it references. I’ll be using jQuery, so the script tags in the main.html page need to look like this:

   <script src="jquery.js"></script>
   <script src="aws.js"></script>
   <script src="main.js"></script>

Within aws.js I’m going to declare a single function that will be used as a constructor for an Amazon Web Services accessing object. That object will have methods to perform the actual calls. Any access to AWS requires credentials. The REST API (which is what I’ll use) requires an access key ID and a secret access key, so I’ll pass those as parameters to the constructor. The overall code will look like this:

var AWS = function(accessKeyId, secretAccessKey){
   var self = this;

   self.itemLookup = function(itemId, onSuccess, onError){
      // code to call AWS Product Advertising API ItemLookup function
   }
}

I could have just used this throughout, instead of defining self as a copy of it, but the JavaScript this variable is kind of tricky in what it references at various times. During the initial call to the constructor it definitely refers to the new object being created, so I’ll save it and use that saved value from then on. This library can be invoked with code like the following (with real access credentials and a sample ISBN in place of the examples):

var aws = new AWS('my access id', 'my secret key');
aws.itemLookup('1234567890',
               function(){alert('it worked');},
               function(){alert('it failed');});

Now, what does that missing code look like? AWS REST API calls use various HTTP methods, but most of them (including this one) just use GET with no special HTTP headers. So if we can build the right URL it will be easy to invoke it. The form of that URL is endpoint?parameters, where endpoint is a web address specific to the API family, and parameters is a normal query string of the form name1=value1&name2=value2&…namen=valuen where the names and values depend on the specific function.

The ItemLookup function I want to use is part of the AWS Product Advertising API. For that API, the endpoint is https://webservices.amazon.com/onca/xml (you can use the http version instead, but I always use the secure version if at all possible). Regardless of the function called, the parameters must always include:

  • Service – the value is always AWSECommerceService for this API
  • AWSAccessKeyId – the accessKeyId part of the credentials
  • AssociateTag – this is a new requirement since November 2011; I’m going to have to add this to either the code, the constructor call, or the method call
  • Operation – the name of the function, ItemLookup in this case
  • Timestamp – when the request was created; AWS will only honor it for 15 minutes to prevent future “replay” attacks
  • Signature – a cryptographic signature created from all the other parameters and the secret access key

The ItemLookup function requires additional parameters:

  • ItemId – identifies the item to find, or a comma-separated list of up to ten items
  • ResponseGroup – tells how much detail we want in the response; I’m going to have to experiment with the various possibilities to see which groups include the data I want

Instead of just creating the query string directly out of these parameters, I’ll use an array of name/value pairs in my code, create the signature from that, then build the query string to use. The code shapes up as follows:

      var params = [];
      params.push({name: "Service", value: "AWSECommerceService"});
      params.push({name: "AWSAccessKeyId", value: accessKeyId});
      params.push({name: "AssociateTag", value: associateTag});
      params.push({name: "Operation", value: "ItemLookup"});
      params.push({name: "Timestamp", value: formattedTimestamp()});
      params.push({name: "ItemId", value: itemId});
      params.push({name: "ResponseGroup", value: "ItemAttributes"});

      var signature = computeSignature(params, secretAccessKey);
      params.push({name: "Signature", value: signature});

      var queryString = createQueryString(params);
      var url = "https://webservices.amazon.com/onca/xml?"+queryString;

This code assumes that a variable named associateTag already exists. I’m going to add it as a parameter to the main constructor function to make that happen. This code also invokes several helper functions: formattedTimestamp, computeSignature, and createQueryString. I’m going to have to write them inside of this library. The code then needs to make an HTTP GET request to that URL and (if the call is successful) pull the desired data out of the response body, passing that to the onSuccess handler.

I’ll tackle the new functions first, from easiest to hardest. formattedTimestamp just needs to return the current time in a standard format: YYYY-MM-DDTHH:MM:SSZ (the T is a separator between date and time, and the Z indicates UTC time). Actually, I could cheat here if I wanted to. I’ve found that any date in the future is accepted by AWS, so I could hard code the result of this function as 9999-12-31T23:59:59Z. But that strikes me as a loophole in the service that may be closed in the future, so I’ll play fair here.

   function formattedTimestamp(){
      var now = new Date();

      var year = now.getUTCFullYear();

      var month = now.getUTCMonth()+1; // otherwise gives 0..11 instead of 1..12
      if (month < 10) { month = '0' + month; } // leading 0 if needed

      var day = now.getUTCDate();
      if (day < 10) { day = '0' + day; }

      var hour = now.getUTCHours();
      if (hour < 10) { hour = '0' + hour; }

      var minute = now.getUTCMinutes();
      if (minute < 10) { minute = '0' + minute; }

      var second = now.getUTCSeconds();
      if (second < 10) { second = '0' + second; }

      return year+'-'+month+'-'+day+'T'+hour+':'+minute+':'+second+'Z';
   }

createQueryString is a bit trickier, but not much. I just need to build a query string in the standard format. However, I have to remember to URI encode the names and values, in case they include any special characters. And I’m going to add the parameters in sorted order by name, because that will be useful later when computing a signature according to AWS’s rules.

   function createQueryString(params){
      var queryPart = [];
      var i;

      params.sort(byNameField);

      for(i=0; i<params.length; i++){
         queryPart.push(encodeURIComponent(params[i].name) +
                        '=' +
                        encodeURIComponent(params[i].value));
      }

      return queryPart.join("&");

      function byNameField(a, b){
         if (a.name < b.name) { return -1; }
         if (a.name > b.name) { return 1; }
         return 0;
      }
   }

This function actually changes the parameter it is passed: it sorts the array it is given. It would be better behaved to make a copy and sort the copy, but instead I’ll just note this fact and keep it simpler.

Now it’s time for the hard one, computeSignature. Actually, with the steps already taken it’s not that hard any more. The AWS signature is a 256-bit SHA HMAC of a special string that includes the HTTP method, the host name of the end point, the path of the request, and the unsigned query string (as created above), signed using the secret access key. Of course, doing that cryptographic operation would be pretty hard, but I don’t have to. I can use the Stanford JavaScript Crypto Library. I downloaded the minified version of it and put it in my project folder in the file sjcl.js, and loaded it in the main page with a script tag before the aws.js reference there. With that in place, the computeSignature function is not too hard:

   function computeSignature(params, secretAccessKey){

      var stringToSign = 'GET\nwebservices.amazon.com\n/onca/xml\n' +
                         createQueryString(params);

      var key = sjcl.codec.utf8String.toBits(secretAccessKey);
      var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha256);
      var signature = hmac.encrypt(stringToSign);
      signature = sjcl.codec.base64.fromBits(signature);

      return signature;
   }

The signing looks more complicated than it is because the hmac.encrypt function operates on bit strings, not normal JavaScript character strings, so there are extra steps to convert those back and forth.

With those preliminaries out of the way the code can create the URL to use to call the service. I’ll use jQuery to make the Ajax call:

      jQuery.ajax({
         type : "GET",
         url: url,
         data: null,
         success: extractAndReturnResult,
         error: returnErrorMessage
      });

This will call the URL and send the successful response to extractAndReturnResult or an unsuccessful one to returnErrorMessage. I’ve got to write those two functions, and then should be done.

      function extractAndReturnResult(data, status, xhr){
         onSuccess(xhr.responseText);
      }

      function returnErrorMessage(xhr, status, error){
         onError('Ajax request failed with status message '+status);
      }

Both these functions need a lot of work! In particular, extractAndReturnResult doesn’t do what its name says at all. It just returns the raw response from Amazon. But that’s going to be useful for exploring the different options on the call, so I’m keeping it that way for now.

Putting all the above together (and adding the necessary associateTag parameter to the constructor), the aws.js file is:

var AWS = function(accessKeyId, secretAccessKey, associateTag){
   var self = this;

   self.itemLookup = function(itemId, onSuccess, onError){
      var params = [];
      params.push({name: "Service", value: "AWSECommerceService"});
      params.push({name: "AWSAccessKeyId", value: accessKeyId});
      params.push({name: "AssociateTag", value: associateTag});
      params.push({name: "Operation", value: "ItemLookup"});
      params.push({name: "Timestamp", value: formattedTimestamp()});
      params.push({name: "ItemId", value: itemId});
      params.push({name: "ResponseGroup", value: "ItemAttributes"});

      var signature = computeSignature(params, secretAccessKey);
      params.push({name: "Signature", value: signature});

      var queryString = createQueryString(params);
      var url = "https://webservices.amazon.com/onca/xml?"+queryString;

      jQuery.ajax({
         type : "GET",
         url: url,
         data: null,
         success: extractAndReturnResult,
         error: returnErrorMessage
      });

      function extractAndReturnResult(data, status, xhr){
         onSuccess(xhr.responseText);
      }

      function returnErrorMessage(xhr, status, error){
         onError('Ajax request failed with status message '+status);
      }
   }

   function formattedTimestamp(){
      var now = new Date();

      var year = now.getUTCFullYear();

      var month = now.getUTCMonth()+1; // otherwise gives 0..11 instead of 1..12
      if (month < 10) { month = '0' + month; } // leading 0 if needed

      var day = now.getUTCDate();
      if (day < 10) { day = '0' + day; }

      var hour = now.getUTCHours();
      if (hour < 10) { hour = '0' + hour; }

      var minute = now.getUTCMinutes();
      if (minute < 10) { minute = '0' + minute; }

      var second = now.getUTCSeconds();
      if (second < 10) { second = '0' + second; }

      return year+'-'+month+'-'+day+'T'+hour+':'+minute+':'+second+'Z';
   }

   function createQueryString(params){
      var queryPart = [];
      var i;

      params.sort(byNameField);

      for(i=0; i<params.length; i++){
         queryPart.push(encodeURIComponent(params[i].name) +
                        '=' +
                        encodeURIComponent(params[i].value));
      }

      return queryPart.join("&");

      function byNameField(a, b){
         if (a.name < b.name) { return -1; }
         if (a.name > b.name) { return 1; }
         return 0;
      }
   }

   function computeSignature(params, secretAccessKey){

      var stringToSign = 'GET\nwebservices.amazon.com\n/onca/xml\n' +
                         createQueryString(params);

      var key = sjcl.codec.utf8String.toBits(secretAccessKey);
      var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha256);
      var signature = hmac.encrypt(stringToSign);
      signature = sjcl.codec.base64.fromBits(signature);

      return signature;
   }
}

The main.js file needs a little tweaking now to call this properly. The new version is:

$(document).ready(function(){
   var aws = new AWS('my access id', 'my secret key', 'my associate id');
   $("#lookup").click(lookupIsbn);

   function lookupIsbn(){
      var isbn = $("#isbn").attr("value");
      aws.itemLookup(isbn,
                     function(message){
                        message = message.replace(/&/g, "&amp;");
                        message = message.replace(/</g, "&lt;");
                        message = message.replace(/>/g, "&gt;");
                        $("#results").append(message);
                     },
                     function(message){
                        alert("Something went wrong: "+message);
                     }
                     );
   }
});

There are only two real changes here. First, the constructor is called first, to get an object for working with AWS before anything else happens. Second, instead of just dumping the response message in the web page the code first replaces all special HTML characters with their equivalent character entities. That way, the message will be shown as text instead of interpreted as HTML, possibly including code.

And now I’m ready to go. I put an ISBN in the text box and pressed the button… and got this:

Error message: Something went wrong: Ajax request failed with status message error

That doesn’t tell me much, though. But the JavaScript console (opened with Control-Shift-J) is more helpful:

Origin is not allowed by Access-Control-Allow-Origin

Web browsers do not allow pages to make requests to other addresses, so this request was disallowed. That’s a security restriction. There is a new Cross-Origin Resource Sharing specification that allows this when the target web site decides it is safe to do, but AWS doesn’t support it. Not yet, anyway; I’m still hoping. However, Chrome apps can bypass this restriction if they ask. The manifest.json file needs to be changed to request this:

{
   "name": "Books to Buy",
   "description": "Keep a list of books to buy on Amazon, with their price and availability",
   "version": "1",
   "app": {
      "launch": {
         "local_path": "main.html"
      }
   },
   "icons": {
      "16":    "icon_16.png",
      "128":   "icon_128.png"
   },
   "permissions": [
      "https://webservices.amazon.com/*"
   ]
}

The permissions entry tells Chrome to allow this app to make requests to any URL matching the wild card given. I removed and reinstalled the app after making this change, and tried again. And got this:

Page showing XML response from AWS

Success! Sort of. A lot of XML came back from the request, and I need to pull the necessary data out of it. I also need to explore various response groups to get the data I need. And all that will be the subject of the next post in this series.

December 6, 2011

Chrome Web App Bookshelf – Part 2

Filed under: Uncategorized — Charles Engelke @ 11:54 pm

Note: this is part 2 of the Bookshelf Project I’m working on. Part 1 was a Chrome app “Hello, World” equivalent.

Now that we can build a web page and install it as an app in Chrome it’s time to make the page do something. Ideally, something to do with Amazon Web Services. This project is going to work by incrementally adding features, and I will start small. I want a page that has a field to enter an ISBN and a button to ask it to be looked up at Amazon. Information about the matching book (or an error message if there isn’t one) will be displayed below that in the page. While I’m at it, I’ll also change the page title and add a header explaining what the page is.

The new page is still quite simple:

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="utf-8" />
   <title>Books to Buy</title>
</head>
<body>
   <h1>Books to Buy</h1>
   <div id="dataentry">
      <input type="text" id="isbn" />
      <button id="lookup">Look Up!</button>
   </div>
   <div id="results">
   </div>
</body>
</html>

If you have already created and installed the basic web app, you can edit the main.html file to match this. When you next run or refresh the app, you should see something like the following:

Books to Buy page, first try

Of course, if you enter an ISBN and press the button nothing happens. I have to write JavaScript to respond to the button press, call an AWS API to look up the information, parse the information, and place it into the empty results div.

I don’t like to put JavaScript in my web pages directly, so I’ll create a separate file for it and load it by putting a script tag right after the title. There are people who argue for placing script tags at the very end of a page for performance reasons but I don’t see it making much difference here, and I still like them near the top. I’ll put the JavaScript code in a file called main.js, and add a line right after the title tag:

   <script src="main.js"></script>

Since this page is HTML5 (thanks to the <!DOCTYPE html> declaration at the top), I don’t need to specify that this is a JavaScript file; HTML5 assumes all script files are. I don’t use a self-closing tag because that often (maybe always) doesn’t work for reasons I don’t understand.

After saving the file and hitting refresh, nothing looks different. Because the new JavaScript file doesn’t yet exist. I brought up the Chrome Developer Tools by hitting Ctrl-Shift-J, and saw this error message in the console:

chrome-extension://jpnlfejeoenacfaonfmmdiofnheemppo/main.js Failed to load resource

By the way, from this I see that the browser refers to my app with a URL starting with chrome-extension:// followed by an apparently randomly assigned string. I don’t know how that will be useful, but it’s interesting.

I need to create a main.js file in the same folder as the main.html file, and put code in it to:

  • Attach an event handler to the button, so that when a user clicks it my code will run.
  • Have that code read the ISBN from the input text box.
  • Call the AWS service to look up the information for that ISBN.
  • If the call works, pull the necessary data out of the response and display it in the results div.
  • If the call fails, either put an error in the div or pop up an alert box.

I’m going to use jQuery to help with this work. That’s a JavaScript library that adds a lot of useful features to JavaScript, and which handles subtle variations between how different browsers implement JavaScript. That second benefit is less important with HTML5, which causes browsers to behave much more consistently than ever before, but I’m used to jQuery and want to use it. I have to download it (either the compressed or uncompressed one will work) and put a copy of it in the same directory as the main web page. I’ll call that downloaded file jquery.js and I’ll add a script tag for it just before the main.js script tag:

   <script src="jquery.js"></script>

Now, what goes in the main.js file? The first thing to do is to attach an event handler to the button’s click event. That’s easy with jQuery:

   $("#lookup").click(lookupIsbn);

The $ is actually a jQuery JavaScript function name (it’s an alias for a function named jQuery). If you give it a string with a CSS selector (which #lookup is, referring to the element with the id lookup) it will return a jQuery object referring to that element, which has added to it a lot of useful methods. One of the methods it adds is click, which takes a function as a parameter. In this case the code is passing a function called lookupIsbn, which means that function should be invoked whenever anybody clicks that button.

There are two problems with this line. The first is pretty obvious: it says to run a function called lookupIsbn but there is no such function. Not yet. I’ll write it soon. The second is more subtle. The browser will execute the JavaScript as soon as possible, which may be before the web page has been fully read and processed. So there may not be an element with id lookup when this code runs and nothing will happen. Or maybe the timing will work out okay and this will do what I want. That would actually be worse because then the code would randomly succeed or fail. I’d rather have consistent behavior, even if that’s consistent failure.

The browser builds a data structure for each page as it reads it, starting with the document element that contains everything else. When it finishes building the page it triggers an event handler on that document element. So I can set up that event to run this code, making it run once the page is ready. jQuery makes that easy by adding a ready method to the document element when we wrap it. So the code should be:

$(document).ready(attachClickHandlerToButton);

function attachClickHandlerToButton(){
   $("#lookup").click(lookupIsbn);
}

In fact, though, people rarely define a named function (like attachClickHandlerToButton) to deal with an action that will happen only once. Instead, they define an anonymous function in place, as follows:

$(document).ready(function(){
   $("#lookup").click(lookupIsbn);
});

I could use the same trick in place of lookupIsbn, but I get uncomfortable when I nest anonymous functions too deeply. The browser’s fine with it, but I’m not. So I have to write lookupIsbn now. That can be defined after the JavaScript above, but I’d rather define it inside the anonymous function, like so:

$(document).ready(function(){
   $("#lookup").click(lookupIsbn);

   function lookupIsbn(){
   // put the code here
   }
});

This prevents any JavaScript code outside of the anonymous function from seeing or using the lookupIsbn function. I don’t much care if they could use it, but if some other code (perhaps in an included third-party library) used the same function name things would get troublesome. This keeps my function definition private and avoids interference with other code.

What goes in there seems pretty straightforward. I have to read the ISBN from the input box, ask AWS for information about that ISBN, parse the result into a readable form, and then display it. That would be something like:

      var isbn = $("#isbn").attr("value");
      var message = askAwsAboutIsbn(isbn);
      $("#results").append(message);

The first line finds the element with id isbn, which is the input element, gets the contents of the value attribute (which is what the user entered), and saves it in a new variable named isbn. The second line magically asks AWS for information, presumably getting a nicely formatted chunk of text back. The last line finds the element with id results and puts an element built from the message text inside of it.

There are some things wrong here. If the message coming back from AWS has HTML inside of it this code will insert it directly into the page, which might end up even running code. I’ve got to fix that. But a bigger problem is the magic askAwsAboutIsbn function call. My code calls the function, waits for a response, then uses the result. But that’s going to involve talking to a remote web site, which is relatively slow. My web page is going to be frozen while waiting for that answer.

The way to handle this freeze is to make the request asynchronous. That is, call askAwsAboutIsbn to get the answer, and give it a function to call when it’s done. Then immediately return instead of waiting for the answer. To do that, the magic askAwsAboutIsbn has to be told not only what ISBN to look up, but also given a function to execute when it’s done. So the code should look something like this:

      var isbn = $("#isbn").attr("value");
      askAwsAboutIsbn(isbn, function(message){
         $("#results").append(message);
      });

This magic box will get the answer without the main program waiting for it. When it has it, it will call the anonymous function, passing the message it got to it, and that function will put the message in the right place on our page. So all that’s left is to write askAwsAboutIsbn. But that’s a pretty tall order, so I’m going to leave it for next time. For now, I’ll just write a stub that returns a canned response:

      function askAwsAboutIsbn(isbn, handleResult){
         handleResult("I don't know the info for "+isbn+" yet.");
      }

This stub just immediately calls the function it was given with a canned response as the parameter. Its purpose is just to see if everything is wired together right.

Putting everything together, there is now a folder called bookshelf that contains six files: main.html, main.js, jquery.js, icon_128.png, icon_16.png, and manifest.json. I haven’t changed the last three of those files and I just downloaded jquery.js. The other two files are the main.html file:

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="utf-8" />
   <title>Books to Buy</title>
   <script src="jquery.js"></script>
   <script src="main.js"></script>
</head>
<body>
   <h1>Books to Buy</h1>
   <div id="dataentry">
      <input type="text" id="isbn" />
      <button id="lookup">Look Up!</button>
   </div>
   <div id="results">
   </div>
</body>
</html>

and the main.js file:

$(document).ready(function(){
   $("#lookup").click(lookupIsbn);

   function lookupIsbn(){
      var isbn = $("#isbn").attr("value");
      askAwsAboutIsbn(isbn, function(message){
         $("#results").append(message);
      });
   }

   function askAwsAboutIsbn(isbn, handleResult){
      handleResult("I don't know the info for "+isbn+" yet.");
   }
});

If all the files are right the application should work when I enter an ISBN and click the button. And it does:

First version of page showing the result

That’s enough for this post. Next time I’ll actually use the AWS web service to look the information up for the given ISBN, parse the result, and display it. There will be plenty to do after that, though: saving data persistently, improving the display, adding a settings page, and packaging the app. So there’s a lot more to come.

December 5, 2011

Chrome Web App Bookshelf – Part 1

Filed under: Uncategorized — Charles Engelke @ 10:44 am

Note: this is part of the Bookshelf Project I’m working on.

Before I can do anything useful in a Chrome Web Application, I’ve got to figure out the very basics. This post is going to cover my “Hello, World” equivalent start, maybe going a bit deeper than that.

At a minimum, my Chrome web app needs four things:

  1. A web page to display and run.
  2. An icon to show on the Chrome applications page.
  3. A favicon.
  4. A manifest describing where the above pieces are, plus anything else I end up needing.

I started by creating a folder to put all these pieces in. I named it bookshelf, but it could have been called anything.

My first web page is going to be just about the bare minimum for HTML5:

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="utf-8" />
   <title>A sample page</title>
</head>
<body>
   <p>This is the sample page.</p>
</body>
</html>

I put that text into a file called main.html in my folder, then went searching for icons. I found a very nice one, in a variety of sizes, at designmoo.com. It’s called Book_icon_by_@akterpnd, and is licensed under a Creative Commons 3.0 Attribution license, so there should be no problems with my using it here. I need a 128 by 128 icon for the application page, and 16 by 16 for the favicon. The set didn’t have a 16 by 16 icon in it, so I resized the smallest one for that. I called the two icons I ended up with icon_128.png and icon_16.png, and put them in my folder, too. They look pretty good, don’t they?

128 by 128 icon for project16 by 16 icon for project

Now I have to write the manifest file. It’s in JSON format, which is just text in a specific syntax. With a text editor I created mainfest.json in my folder, with the following content:

{
   "name": "Books to Buy",
   "description": "Keep a list of books to buy on Amazon, with their price and availability",
   "version": "1",
   "app": {
      "launch": {
         "local_path": "main.html"
      }
   },
   "icons": {
      "16": "icon_16.png",
      "128": "icon_128.png"
   }
}

You can see that this file points my three other files, and also gives the app a name, description, and version. I don’t know what the best practices are for the version numbering, so for now I’ll just keep it at 1.

I think I have a complete Chrome app now. You can create a folder with these files to see for yourself. Once you have these four files in a folder, open Chrome and click the wrench icon in the upper right to get the menu. Select Tools, then Extensions. Check the “Developer Mode” box on the resulting page if you haven’t already, then click the Load Unpacked Extension button. Select the folder with the manifest in it, and you should be good to go. It should look like this:

Chrome extensions page showing the new app

Pretty nice, I think. Go ahead and close this tab. To run the app (with the version of Chrome current as I write this), open a new tab and click Apps on the bar at the bottom of the resulting page. The new app should show up at the end of the page:

New tab page showing new app

Click on the application icon, and the page should open, and even have the right favicon:

The web app, opened

Okay, that’s not much, but this post is the “Hello, World” equivalent. Next time we will add a skeleton for a minimal application, one where you can enter an ISBN and have the page look it up and display the result in the page.

December 4, 2011

The Bookshelf Project – Using Amazon Web Services from JavaScript

Filed under: Uncategorized — Charles Engelke @ 8:08 pm
Tags: , , ,

Many years ago, I got frustrated with using Amazon’s “save for later” shopping cart function to keep track of books I probably wanted to buy someday. The problem I was trying to solve was that I’d find out about an upcoming book by one of my favorite authors months before publication and I didn’t want to forget about it. I could have just preordered the book, but back then there was no Amazon Prime so I always preferred to bundle my book orders to save on shipping. So I’d add the book to the shopping cart and tell it to save it for later. But (at least back then) Amazon was willing to save things in your cart for only so long, and my books would often disappear from the cart before they were published.

I’m a programmer, and Amazon had an API (application program interface), so I did the obvious thing: wrote a program to solve my problem. It was just for me, so I wrote the simplest thing that could possibly work, figuring I’d improve it some day. It was a simple Perl CGI script that I ran under Apache on my personal PC. It used the (then very primitive) Amazon Web Service to look up the book’s information given an ISBN, and saved its data in a tab delimited text file.

That was a long time ago, probably very soon after Amazon introduced its first web services. And I’m still using it today with almost no changes. But I’m no longer happy with it, for several reasons:

  • It only recognizes the old 10 digit ISBN format, not the newer 13 digit one.
  • It can’t find Kindle books at all.
  • It runs only on a PC running an Apache webserver.
  • The data is available on only that device.

The cloud has spoiled me. I want this program to run on any of my web-connected devices, and I want them all to share a common data store. Hence this project.

“Run on any of my web-connected devices” pretty much means running in a browser, so I’ll have to write it in HTML and JavaScript. I’ll use HTML5 and related modern technologies so I can store data in the browser so I can see my saved book list even when off-line.

I know HTML and JavaScript but I’m no expert, so I’m going to build this incrementally, learning as I go. Step 1 will be to get a web page that just uses Amazon Web Services (AWS) to look up the relevant information given an ISBN. And right away, that’s going to require a detour. As a security measure, web browsers won’t let a web page connect (in a useful enough way) to any address but the one hosting the web page itself. My web page isn’t going to be at the same address as AWS, so it seems this is a hopeless task.

There is a way out, called Cross Origin Resource Sharing (CORS). The target web site can tell the web browser that it’s okay, it’s safe to let a “foreign” web page access it. Modern browsers support CORS, so I should be okay. Unfortunately, AWS doesn’t (yet) support CORS, so that’s out. Foiled again!

But there is a stopgap. I can create a Chrome Web Application. That’s pretty much just a normal web page, except that it can tell the web browser to allow access to foreign services. And that’s just what I will do, starting in my next blog post. That will take a while, but after that’s done, I can explore various directions to take it:

  • Maybe AWS will support CORS soon, in which case I’ll be able to use almost the exact same solution on any modern web browser, even on tablets and phones.
  • I can always write server-side code to “tunnel” the web service requests through my server on the way to AWS. That works, but I think it’s inelegant.
  • I might try creating an HP TouchPad application, which uses the same kinds of technologies as the web, but to create native apps. I find that approach very appealing, even though the TouchPad is more-or-less an orphan device now. I’ve got one, and this would be an excuse to develop for it.
  • Tools like PhoneGap let you wrap a web application in a shell to allow it to run as a native app on various mobile platforms. I think they allow operations that normal browsers block, such as CORS. I could find out, anyway.

So I’ve got a lot of potential things to learn and try. First up: creating a Chrome web application, in many steps. If it comes out nice, I’ll even try publishing it in the Chrome Web Store.

Create a free website or blog at WordPress.com.