At the Forge - Creating Mashups

It's a crime not to mashup two or more Web services to deliver more than they can deliver separately.
Adding Amazon Information

Although the above is nice to have, the city information is still hard-coded. What we want is to be able to retrieve information about third-party sellers of a particular book. This means we must get an ISBN from the user, ask Amazon for third-party sellers of that book, and then get the city and state in which each of those sellers resides. Our code will remain largely the same, except for the way we define the cities array, which will be far more complicated. You can see the resulting code in Listing 3.

Getting an ISBN from the end user is fairly straightforward. At the top of the file, we import the CGI class:


<% require 'cgi' %>

Now we can retrieve an ISBN that the user entered:


<% cgi = CGI.new %>
<% isbn = cgi['isbn'] %>

We use this ISBN to find all of the third-party sellers with a copy of this book. (Actually, we're going to look at only up to ten of the third-party vendors; Amazon returns only ten items at a time, and we won't complicate our code by looking for additional pages of results.) We take each returned vendor and put it into our vendors array.

So, let's start by getting information about vendors of used copies of our book. We do this by sending Amazon a REST request for our ISBN:


amazon_params = {'Service' => 'AWSECommerceService',
 'Operation' => 'ItemLookup',
 'AWSAccessKeyId' => 'XXX',
 'ItemId' => isbn,
 'ResponseGroup' => 'Medium,OfferFull',
 'MerchantId' => 'All'}.map {|key,value|
 "#{key}=#{value}"}.join("&")

amazon_response = Net::HTTP.get_response('webservices.amazon.com',
                                        '/onca/xml?' <<
                                        amazon_params)

The above is my preferred technique for keeping track of names and values, especially when I'm passing a lot of them—I create a hash, joining the keys and values with = signs, and then the pairs themselves with ampersands (& signs). This gives me a string that I can hand to Amazon.

The XML response that I get back then contains a lot of information, including details about each offer. That's actually all I care about here; I'm not keeping track of the price of the book (which would be useful, of course), but rather the location of each used copy we can grab. But we can't get that right away; the ItemLookup request gets us only the seller IDs and some basic information about each one. We'll need to grab the seller ID from each offer node, then use that to perform a second Amazon request, obtaining information about the vendor:


xml.root.elements.each("Items/Item/Offers/Offer/Seller/SellerId") do
|seller|
 # Now get information about each vendor
  amazon_vendor_params = {'Service' => 'AWSECommerceService',
      'Operation' => 'SellerLookup',
      'AWSAccessKeyId' => 'XXX',
      'SellerId' => seller.text}.map {|key,value|
      "#{key}=#{value}"}.join("&")

  vendor_response = Net::HTTP.get_response('webservices.amazon.com',
                                               '/onca/xml?' <<
                                               amazon_vendor_params)
  vendor_xml = REXML::Document.new(vendor_response.body)

This code sends a request to Amazon, gets an XML body back, and then looks for the City and State elements that a vendor will produce. Unfortunately, there's no fast and easy way to deal with countries outside of the United States, both with geocoding and with Amazon. Amazon's assumption seems to be that Canada is sort of like the United States, which is false. So, we'll always get the city and state and assume that it is in the United States. If our assumption turns out to be wrong, we'll allow ourselves to be corrected by the geocoder.

As we have grabbed information about each vendor, we have stuck the city and state information in the cities array. Now we're going to use that same array, just as we did in mashup2.rhtml—except now, the source is not a hard-coded list, but rather one that we put together from Amazon information. We had to make only two changes for things to work: a check that we didn't get nil from the geocoder (indicating there was an error, often because the vendor is in Canada), and a use of gsub to change space characters into + signs in the city name.

The results are quite nice to see, even if they're incomplete and a bit on the crude side: By going to a URL such as http://maps.lerner.co.il/mashup3.rhtml?isbn=0812931432, we can see where a number of used copies are located in the United States. This doesn't necessarily reflect the cost of the book, its condition, or the shipping charges—but it can be fun and interesting to see where different books have ended up, and which cities tend to have more (and fewer) used books.

______________________

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

crime mashup

GFL's picture

Another crime mashup for you:
http://www.newhavencrimelog.org/

Error

cbier's picture

I get this error when I try to run the code from listing 3:

undefined method `text' for nil:NilClass
(
NoMethodError
)

The error is from this line:

next if geocoder_xml.root.nil?

Thanks for writing great articles. At the Forge is one of my favorite articles.

Error

Reuven Lerner's picture

Hi, cbier. Sorry that it took a while to respond, but I only realized recently that people were asking questions of me on this forum!

The error message tells us that we're trying to invoke a method -- the "text" method -- on nil. Now, we obviously don't have a literal nil in the code, so that means our variable must have a nil value, and we're trying to invoke an unknown method on it.

My guess, and I can't prove it, is that Amazon gave you back a city for which there wasn't any geocoder information. (Maybe the city name was spelled incorrectly?) Thus you got nil back. But then you tried to invoke a method on it. Where? I'm not sure, but I'm guessing that we needed to do a bit more testing -- checking that geocoder_response wasn't nil, and that geocoder_xml wasn't nil, either.

Reuven
Senior Columnist, Linux Journal

Reuven M. Lerner, Linux Journal Senior Columnist, a longtime Web developer, consultant and trainer,
is completing his PhD in learning sciences at Northwestern University.

White Paper
Linux Management with Red Hat Satellite: Measuring Business Impact and ROI

Linux has become a key foundation for supporting today's rapidly growing IT environments. Linux is being used to deploy business applications and databases, trading on its reputation as a low-cost operating environment. For many IT organizations, Linux is a mainstay for deploying Web servers and has evolved from handling basic file, print, and utility workloads to running mission-critical applications and databases, physically, virtually, and in the cloud. As Linux grows in importance in terms of value to the business, managing Linux environments to high standards of service quality — availability, security, and performance — becomes an essential requirement for business success.

Learn More

Sponsored by Red Hat

White Paper
Private PaaS for the Agile Enterprise

If you already use virtualized infrastructure, you are well on your way to leveraging the power of the cloud. Virtualization offers the promise of limitless resources, but how do you manage that scalability when your DevOps team doesn’t scale? In today’s hypercompetitive markets, fast results can make a difference between leading the pack vs. obsolescence. Organizations need more benefits from cloud computing than just raw resources. They need agility, flexibility, convenience, ROI, and control.

Stackato private Platform-as-a-Service technology from ActiveState extends your private cloud infrastructure by creating a private PaaS to provide on-demand availability, flexibility, control, and ultimately, faster time-to-market for your enterprise.

Learn More

Sponsored by ActiveState