My favorites | English | Sign in

Google Base Data API (Labs)

Accessing the Google Base data API using Perl

The Perl Recipe application is a small but complete example that demonstrates the five Google Base API services: queries, insertions, deletions, updates, and batch commands. It uses the standard libcurl library to perform efficient HTTP requests for authentication and interaction with Google Base API.

Contents

  1. Using the Recipe application
  2. Stepping through the Recipe application code
    1. Setup and token exchange
      1. Building the introductory page
      2. Exchanging the single-session token
    2. Constructing XML requests and parsing XML responses
      1. Creating the item insertion request
      2. Creating the batch deletion request
      3. Parsing the user's items feed
    3. Interacting with the Google Base feed
      1. Retrieving the user's entries
      2. Inserting new entries
      3. Updating existing entries
      4. Deleting existing entries
      5. Executing a batch command
  3. Troubleshooting

Using the Recipe application

You can download the source code for the sample program and host it on your own web server. Note that this application is a web application and is not intended to be run from the command line.

We have tested the Recipe application with the latest versions of WWW::Curl::Easy (version 3.02) and XML::Parser (version 2.34).  These Perl modules provide support for executing HTTP requests and parsing XML documents, respectively.

After installing the Perl sample application, replace line 10 with a valid API Key tied to your domain.

When you first use the Recipe application, you will have to log in to your Google Account and grant the script permission to access your Google Base entries.

Stepping through the Recipe application code

The Recipe application allows users to upload new recipes to Google Base, edit or delete recipes they have already added, or delete all their recipes with a batch request. It uses GET and POST parameters to maintain two key elements of program state: the user's AuthSub token and requested action (insert, update, delete, or batch).

Setup and token exchange

This section describes how the Recipe application initially interacts with the user and obtains a session token from the AuthSub server.

Building the introductory page: showIntroPage()

When a user first arrives at the Recipe application, no GET or POST parameters are provided to the form. This indicates that the user has not yet obtained a single-session token. The code below sets up a link to the AuthSubRequest page with these options:

  • session, indicating this token can be exchanged for a multi-use (session) token;
  • next, with a link back to the Recipe application;
  • scope, indicating that the Recipe application will only access the items feed.
sub showIntroPage {
  my $redirect_url =
    'https://www.google.com/accounts/AuthSubRequest?session=1';
  $redirect_url .= '&next=';
  $redirect_url .= uri_escape(getSelfURI());
  $redirect_url .= "&scope=";
  $redirect_url .= uri_escape("http://www.google.com/base/feeds");

  ...

}

The remainder of showIntroPage (omitted for brevity) prints the HTML tags to create a table containing this link.

Exchanging the single-session token: exchangeToken()

After the user authenticates to his Google Account using AuthSub, he is redirected back to the Recipe application page (as a result of the next parameter to AuthSubRequest). The redirection URL will contain one GET parameter: the single-use token granted by AuthSub.

The exchangeToken() function below is used to exchange the user's single-use token for a multi-use (session) token. The resulting session token is used for all interaction with the Google Base API: queries, insertions, deletions, updates, and batch commands. exchangeToken() uses the standard procedure for performing an HTTP request with libcurl, namely:

  1. Create a handle for the request with new WWW:Curl::Easy .
  2. Set the options for the request with $curl->setopt(). In this case, we specify the AuthSubSessionToken URL (CURLOPT_URL) as well as the single-use AuthSub token (CURLOPT_HTTPHEADER). We also request that the HTTP response be returned rather than printed directly (CURLOPT_RETURNTRANSFER) and that the request should fail silently if an error occurs (CURLOPT_FAILONERROR).
  3. Execute the request with $curl->perform() and catch the result.
  4. Note that, unlike the PHP sample application, there is no need to manually close the cURL handle; Perl does this automatically.

Finally, the HTTP response (Token=...) is parsed with the split() function and the result returned.

sub exchangeToken {
  my $token = shift;
  my $curl = new WWW::Curl::Easy;
  my @body;

  my @authHeader = ("Authorization: AuthSub token=\"" . $token . "\"");

  $curl->setopt(CURLOPT_URL,
    "https://www.google.com/accounts/AuthSubSessionToken");
  $curl->setopt(CURLOPT_FAILONERROR, 1); 

  $curl->setopt(CURLOPT_WRITEFUNCTION, \&writeCallback );
  $curl->setopt(CURLOPT_HEADERFUNCTION, \&headerCallback );
  $curl->setopt(CURLOPT_FILE, \@body);

  $curl->setopt(CURLOPT_FOLLOWLOCATION, 1);
  $curl->setopt(CURLOPT_HTTPHEADER, \@authHeader);

  my $result = $curl->perform();

  if ($result > 0) {
    return 0;
  } else {
    # Extract everything to the right of the equals sign in
    # the response "Token=..."
    my @splitStr = split(/=/, $body[0]);
    return trim($splitStr[1]);
  }
}

Constructing XML requests and parsing XML responses

This section describes how the Recipe application builds XML requests to be sent to the Google Base feed and how it parses the XML responses.

Creating the item insertion request: buildInsertXML()

The buildInsertXML() function is used to construct the XML document that is sent when the user inserts a new recipe. It simply formats the POST fields submitted by the user (recipe_title, cuisine, and so forth) to conform to the Atom format for a single entry.

sub buildInsertXML {
  my $result = "<?xml version='1.0'?>" . "\n";
  $result .= "<entry xmlns='http://www.w3.org/2005/Atom'" .
    " xmlns:g='http://base.google.com/ns/1.0'>" . "\n";
  $result .= "<category scheme='http://base.google.com/categories/itemtypes'" .
    " term='Recipes'/>" . "\n";
  $result .= "<title type='text'>" . param('recipe_title') . "</title>" . "\n";
  $result .= "<g:cuisine>" . param('cuisine') . "</g:cuisine>" . "\n";
  $result .= "<g:item_type type='text'>Recipes</g:item_type>" . "\n";
  $result .= "<g:cooking_time type='intUnit'>" . param('cooking_time_val') .
             " " . param('cooking_time_units') . "</g:cooking_time>" . "\n";
  $result .= "<g:main_ingredient type='text'>" . param('main_ingredient') .
    "</g:main_ingredient>" . "\n";
  $result .= "<g:serving_count type='number'>" . param('serves') .
    "</g:serving_count>" . "\n";
  $result .= "<content>" . param('recipe_text') . "</content>" . "\n";
  $result .= "</entry>" . "\n";

  return $result;
}

Creating the batch deletion request: buildBatchXML()

The buildBatchDeleteXML () function creates the XML document used to perform a batch deletion request. It formats the links to each item's feed URI (submitted as POST fields when the user clicks the Delete All button) into the Atom format for batch processing.

sub buildBatchDeleteXML {
  my $counter = 0;

  my $result = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
  $result .= '<feed xmlns="http://www.w3.org/2005/Atom"' . "\n";
  $result .= ' xmlns:g="http://base.google.com/ns/1.0"' . "\n";
  $result .= ' xmlns:batch="http://schemas.google.com/gdata/batch">"' . "\n";
  for my $key (param()) {
    if (index($key, "link_") == 0) {
      $counter++;

      $result .= '<entry>' . "\n";
      $result .= '<id>' . param($key) . '</id>' . "\n";
      $result .= '<batch:operation type="delete"/>' . "\n";
      $result .= '<batch:id>' . $counter . '</batch:id>' . "\n";
      $result .= '</entry>' . "\n";
    }
  }
  $result .= '</feed>' . "\n";

  return $result;
}

Parsing the user's items feed

The Recipe application uses the XML::Parser module to parse the user's items feed into an internal array of entries. This function executes three provided handlers, or callback functions, as it parses an XML document: one when a start tag is read (handleXMLstart()), one when an end tag is read (handleXMLend()) and one when character data is read (handleXMLchar()). These handlers coordinate using the following global variables:

  • $parsedEntries (array)
    contains the array of user-entered recipes. This variable is actually an array of hashes: each element of $parsedEntries is itself a hashtable of recipe attributes (keys) and their values.
  • $foundEntry (Boolean)
    specifies whether the application is currently processing an entry (true) or another XML tag (false). $foundEntry is set to true when startElement reads an opening ENTRY tag, and set to false when endElement reads a closing ENTRY tag.
  • $curElement (string):
    denotes the current tag being processed. $curElement is set by startElement(), then used by characterData() to update $parsedEntries.
# Callback function that's fired by the Expat XML parser on parsing
# a start tag.
sub handleXMLstart {
  my($parser, $elem, %attrs) = @_;

  $curElement = lc($elem);
  if ($curElement eq "entry") {
    $foundEntry = 1;
    push(@parsedEntries, {});
  } elsif ($foundEntry && $curElement eq "link") {
    $parsedEntries[$#parsedEntries]{$attrs{"rel"}} = $attrs{"href"};
  }
}

# Callback function that's fired by the Expat XML parser on parsing
# an end tag.
sub handleXMLend {
  my($parser, $elem) = @_;

  if (lc($elem) eq "entry") {
    $foundEntry = 0;
  }
}

# Callback function that's fired by the Expat XML parser on parsing
# a sequence of characters.
sub handleXMLchar {
  my($parser, $chars) = @_;

  if ($foundEntry) {
    $parsedEntries[$#parsedEntries]{lc($curElement)} = $chars;
  }
}

Interacting with the Google Base feeds

This section describes how the Recipe application interacts with the Google Base feed to retrieve, insert, update, and delete items.

Retrieving the user's entries: getItems()

The getItems() function, called to create the array of user-entered recipes, uses libcurl to perform an HTTP GET request on the user's items feed ($itemsFeedURL). It has the same basic structure as exchangeToken() above, with the exception of an added header (X-Google-Key) that specifies the application's developer key. After receiving the HTTP response, getItems() calls XML::Parser->parse() to parse the Atom feed returned by the server.

sub getItems {
  my $token = shift;
  my $curl = new WWW::Curl::Easy;
  my @body;

  my @authHeader = (
    'Content-Type: application/atom+xml',
    'Authorization: AuthSub token="' . trim($token) . '"',
    'X-Google-Key: key=' . $developerKey
  );

  $curl->setopt(CURLOPT_URL, "http://www.google.com/base/feeds/items?");
  $curl->setopt(CURLOPT_FAILONERROR, 1); 

  $curl->setopt(CURLOPT_WRITEFUNCTION, \&writeCallback );
  $curl->setopt(CURLOPT_HEADERFUNCTION, \&headerCallback );
  $curl->setopt(CURLOPT_FILE, \@body);

  $curl->setopt(CURLOPT_FOLLOWLOCATION, 1);
  $curl->setopt(CURLOPT_HTTPHEADER, \@authHeader);

  my $result = $curl->perform();
  if (!$result) {
    my $parser = new XML::Parser(Handlers => {Start => \&handleXMLstart,
                                              End   => \&handleXMLend,
                                              Char  => \&handleXMLchar});
    $parser->parse(join("", @body));
  }
}

Inserting new entries: postItem()

The postItem() function inserts a new recipe by performing an HTTP POST on the user's items feed. The two curl options of note are:

  • CURLOPT_UPLOAD, which specifies that data will be uploaded in an HTTP POST;
  • CURLOPT_READFUNCTION, which specifies the callback function that provides the data to POST;
  • CURLOPT_INFILESIZE, which specifies the length of the data to POST;
sub postItem {
  my $curl = new WWW::Curl::Easy;
  my @body;

  my @authHeader = (
    'Content-Type: application/atom+xml',
    'Authorization: AuthSub token="' . param('session_token') . '"',
    'X-Google-Key: key=' . $developerKey
  );

  $curl->setopt(CURLOPT_URL, "http://www.google.com/base/feeds/items");
  $curl->setopt(CURLOPT_READFUNCTION, \&buildInsertXML);
  $curl->setopt(CURLOPT_INFILESIZE, length(buildInsertXML()));
  $curl->setopt(CURLOPT_UPLOAD, 1);
  $curl->setopt(CURLOPT_CUSTOMREQUEST, "POST");
  $curl->setopt(CURLOPT_FAILONERROR, 1);
  
  $curl->setopt(CURLOPT_WRITEFUNCTION, \&writeCallback );
  $curl->setopt(CURLOPT_HEADERFUNCTION, \&headerCallback );
  $curl->setopt(CURLOPT_FILE, \@body);
  
  $curl->setopt(CURLOPT_FOLLOWLOCATION, 1);  
  $curl->setopt(CURLOPT_HTTPHEADER, \@authHeader);
    
  my $result = $curl->perform();

  return $result;
}

Updating existing entries: updateItem()

The updateItem function updates an existing recipe by performing an HTTP PUT on that recipe's feed URI. This function is identical to postItem() in every respect, except that the custom request is now specified as PUT instead of POST.

sub updateItem {
  my $curl = new WWW::Curl::Easy;
  my @body;

  my @authHeader = (
    'Authorization: AuthSub token="' . param('session_token') . '"',
    'X-Google-Key: key=' . $developerKey,
    'Content-Type: application/atom+xml'
  );

  my $feedURL = param('link');
  chomp $feedURL;

  $curl->setopt(CURLOPT_URL, $feedURL);
  $curl->setopt(CURLOPT_READFUNCTION, \&buildInsertXML);
  $curl->setopt(CURLOPT_INFILESIZE, length(buildInsertXML()));
  $curl->setopt(CURLOPT_UPLOAD, 1);
  $curl->setopt(CURLOPT_CUSTOMREQUEST, "PUT");
  $curl->setopt(CURLOPT_FAILONERROR, 1);
  
  $curl->setopt(CURLOPT_WRITEFUNCTION, \&writeCallback );
  $curl->setopt(CURLOPT_HEADERFUNCTION, \&headerCallback );
  $curl->setopt(CURLOPT_FILE, \@body);
  
  $curl->setopt(CURLOPT_VERBOSE, 1);
  $curl->setopt(CURLOPT_FOLLOWLOCATION, 1);  
  $curl->setopt(CURLOPT_HTTPHEADER, \@authHeader);
    
  my $result = $curl->perform();

  return $result;
}

Deleting existing entries: deleteItem()

The deleteItem() function deletes a recipe the user has entered previously by performing an HTTP DELETE on its feed URI. The DELETE command is specified using the option CURLOPT_CUSTOMREQUEST below.

sub deleteItem {
  my $curl = new WWW::Curl::Easy;
  my $feedURL = param('link');
  my @body;

  my @authHeader = (
    'Authorization: AuthSub token="' . param('session_token') . '"',
    'X-Google-Key: key=' . $developerKey
  );

  chomp $feedURL;
  $curl->setopt(CURLOPT_URL, $feedURL);
  $curl->setopt(CURLOPT_CUSTOMREQUEST, "DELETE");
  $curl->setopt(CURLOPT_FAILONERROR, 1);
  
  $curl->setopt(CURLOPT_WRITEFUNCTION, \&writeCallback );
  $curl->setopt(CURLOPT_HEADERFUNCTION, \&headerCallback );
  $curl->setopt(CURLOPT_FILE, \@body);
  
  $curl->setopt(CURLOPT_FOLLOWLOCATION, 1);  
  $curl->setopt(CURLOPT_HTTPHEADER, \@authHeader);
    
  my $result = $curl->perform();

  return $result;
}

Executing a batch command: batchDelete()

The batchDelete() function deletes all of the user's recipes by issuing a batch request to the Google Base API. This is accomplished by performing an HTTP POST (specified by the POST custom request) to the batch feed URL, specifying the batch XML document as POST data.

sub batchDelete {
  my $curl = new WWW::Curl::Easy;
  my @body;

  my @authHeader = (
    'Content-Type: application/atom+xml',
    'Authorization: AuthSub token="' . param('session_token') . '"',
    'X-Google-Key: key=' . $developerKey
  );

  $curl->setopt(CURLOPT_URL, "http://www.google.com/base/feeds/items/batch");
  $curl->setopt(CURLOPT_READFUNCTION, \&buildBatchDeleteXML);
  $curl->setopt(CURLOPT_INFILESIZE, length(buildBatchDeleteXML()));
  $curl->setopt(CURLOPT_UPLOAD, 1);
  $curl->setopt(CURLOPT_CUSTOMREQUEST, "POST");
  $curl->setopt(CURLOPT_FAILONERROR, 1);
  
  $curl->setopt(CURLOPT_WRITEFUNCTION, \&writeCallback );
  $curl->setopt(CURLOPT_HEADERFUNCTION, \&headerCallback );
  $curl->setopt(CURLOPT_FILE, \@body);
  
  $curl->setopt(CURLOPT_FOLLOWLOCATION, 1);  
  $curl->setopt(CURLOPT_HTTPHEADER, \@authHeader);
    
  my $result = $curl->perform();

  return $result;
}

Troubleshooting

Can't get the Perl sample code to work on your server? Here are a few suggestions:

  • Verify that you are using a valid developer key that is tied to your domain.
  • Try using $curl->errbuf to drill down and see exactly what error is occurring.  For example:

    if ($curl->perform() != 0) { 
    print "Error: " . $curl->errbuf . "\n";
    };