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.
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.
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).
This section describes how the Recipe application initially interacts with the user and obtains a session token from the AuthSub server.
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:
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.
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:
new WWW:Curl::Easy .$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).$curl->perform() and catch the result.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]);
}
}
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.
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;
}
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;
}
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)$parsedEntries is itself a hashtable of recipe attributes (keys) and their values.$foundEntry (Boolean)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): $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;
}
}
This section describes how the Recipe application interacts with the Google Base feed to retrieve, insert, update, and delete items.
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));
}
}
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;
}
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;
}
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;
}
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;
}
Can't get the Perl sample code to work on your server? Here are a few suggestions:
$curl->errbuf to drill down and see exactly what error is occurring. For example:if ($curl->perform() != 0) {
print "Error: " . $curl->errbuf . "\n";
};