Chapter 7. Hot Web Topics

Traditionally, web applications were lonely and isolated. Each of them had their own set of users and data, and never acknowledged the existence of other applications. Today, things are changing. Web applications provide public APIs so that other applications can programmatically interact with their data and users. Users can visit sites and later request that the site notifies them when it has been updated.

Another new development is the widespread adoption of dynamic JavaScript-enabled web applications. Gone are the days of slow waits between pages while things get loaded; AJAX allows web applications to be almost as responsive as native desktop applications. In this chapter, we'll take a look at how to get your Catalyst application in on the fun! We'll add a REST API to allow other programs to easily access our application. Then, we'll use this API to add AJAX interactivity to our application. Finally, we'll modify our mini-blog application to allow users to subscribe to the latest posts.

REST

A feature that many public websites are beginning to offer to their users is the ability to get the information out of the application through a well-defined API (Application Programming Interface). Instead of forcing users to "screen-scrape" the HTML (programmatically parsing the pages that normal browser-based visitors see on the screen), which is tedious and prone to error, the application provides developers with the ability to access the application's entities in a serialized (XML, JSON, and so on) format. Thanks to a wide variety of data deserializers for nearly every programming language, getting data from a web application into your application is extremely easy to implement and maintain with this approach.

There are two main methods of providing this functionality, RPC (remote procedure call), such as XML-RPC or SOAP (Simple Object Access Protocol), and REST (Representational State Transfer). RPC-based approaches create an "RPC endpoint" URL for your application and clients send requests (like 'give me address 42' or 'delete person 10') to this URL and get responses back. All requests use the same endpoint.

REST uses a cleaner approach; it provides a URL for each entity ("noun") and the client uses HTTP verbs to operate on these. Instead of sending an HTTP GET request to /address/42/delete to delete address 42, we send an HTTP DELETE to /address/42. A GET request to that location would return the serialized address and a PUT request would update the data with the serialized version sent with the request. POST-ing to the /address URL will create a new address.

To understand REST, better think of a library project. In REST architecture, the URIs will be well formed and will follow a structure. For example, you will have the following URIs:

  • Book/add
  • Book/edit/1
  • Book/delete/1
  • Author/add

In addition to the URIs following a structure, you will also have to follow certain conventions, such as reading operations are always GET requests, a new entry is always a POST request, and so on.

In addition, all errors (and status indication) that can arise during this process are returned as standard HTTP codes. If an address isn't found, the REST interface returns '404 Not Found'. When an entity is created, the server returns '201 Created' along with the serialized entity. If a certain HTTP method doesn't make sense for a certain entity, '405 Method Not Allowed' is returned. There's no strict standard dictating what to return when, but the convention is to pick the most descriptive HTTP status code. The idea is to make it as easy as possible for an off-the-shelf HTTP client to understand what the response means.

For a more detailed reading on REST, After this line add the following two link:

http://tomayko.com/writings/rest-to-my-wife

http://en.wikipedia.org/wiki/Representational_State_Transfer

We'll see all of this in action as we add a REST interface to our AddressBook application.

Getting some REST

In this section, we'll add REST to the AddressBook application so that API clients can easily look up people and their addresses. We'll write a simple command-line client for accessing this interface, and we'll use it in the next section to implement an interactive JavaScript interface.

The first step is to install one of Catalyst's REST implementations, Catalyst::Controller::REST (found in the Catalyst::Action::REST distribution). Catalyst::Controller::REST is a Controller base class that will handle most of the REST details for us. It will automatically determine the type of incoming data and deserialize it into a Perl object. When we send a response, it will automatically serialize the response entity into the format that the client requested. It also adds some convenience functions, so we don't have to remember the proper HTTP response codes for each operation. Finally, it includes an ActionClass that will dispatch different HTTP methods to different Perl methods. We'll make use of all these features when creating the REST interface.

REST Controller

To provide a clean namespace for our REST actions, we're going to implement the REST interface in a separate controller called "REST". Let's start by creating /lib/AddressBook/Controller/REST.pm as follows:

package AddressBook::Controller::REST;
use strict;
use warnings;
use base qw(Catalyst::Controller::REST);
__PACKAGE__->config->{serialize}{default} = 'JSON';
1;

This creates the basic Controller, and sets the default serialization method to JSON (JavaScript Object Notation). Clients can override this by sending a standard HTTP Accept: header. They specify the format that their PUT or POST request is in by setting a Content-type: header. Formats supported by Catalyst::Controller::REST include JSON, YAML, XML, raw PHP, raw Perl, FreezeThaw, and Storable. Refer to the following documentation for a full list and what headers to send for each type:

http://search.cpan.org/~bobtfish/Catalyst-Action-REST-0.85/lib/Catalyst/Controller/REST.pm

REST authentication

Before we write any more code, we need to think about how to handle authentication for the REST interface. As users of the API will most likely not be using a browser, requiring them to log in, get a cookie, and send it with each request would be cumbersome. As a result, REST (and RPC) interfaces usually use some other sort of authentication.

The most common method (used by Flickr and Google, among many others) is to give each user an API key. The API key works like a username and password, but can be disabled if the user abuses the API (without disabling his normal web access). The API key is sent in the headers of every request and the REST interface verifies it before performing the request.

As it seems unlikely that anyone would abuse our AddressBook's API, we'll just send the normal username and password with every request. As there's no standard HTTP header for this, we'll use X-Username and X-Password.

In addition, we'll want the REST interface to work (partially) from a web browser. So, we will also check to see if a user is logged in through the normal cookie method. If he/she is, we won't bother checking for the X-Username and X-Password headers.

Every REST request will need to be authenticated, so we'll add the authentication check in the begin method of the REST Controller as follows:

sub begin : ActionClass('Deserialize') {
my ($self, $c) = @_;
my $username = $c->req->header('X-Username'),
my $password = $c->req->header('X-Password'),
# require either logged in user, or correct user+pass in headers
if ( !$c->user && !$c->login($username, $password) ){
# login failed
$c->res->status(403); # forbidden
$c->res->body("You are not authorized to use the REST API.");
$c->detach;
}
}

Note that we are also applying the Deserialize action class here. Normally, the REST Controller deserializes the request automatically, but we're overriding it's begin method where it would perform this task. So, we re-add it explicitly so that everything continues to work normally.

We also need to tell our normal ACL plugin to ignore the REST area, so at the bottom of AddressBook.pm, we add the following line:

__Package__->allow_access('/rest'), # rest does its own auth

Adding an entity

Now that our interface is protected appropriately, we can start adding some entities. The format for implementing REST entities with Catalyst::Controller::REST is as follows. We first create a standard Catalyst action that acts as the entity (noun). We use the normal :Local, :Global, :Path, and other attributes to map the action to a URL. However, we also add the REST action class to the action. This action class will eliminate some tedium associated with REST. Instead of a chain of if statements like the following:

if ($c->req->method eq 'GET') { handle GET request }
elsif($c->req->method eq 'POST'){ handle POST request }
elsif( ... ) ...
else { return an error }

we can write the following:

sub entity :Local :ActionClass('REST'){
# do some setup for each request
}
sub entity_GET { handle GET request }
sub entity_POST { handle POST request }

Catalyst will take care of returning an HTTP error code when a method isn't implemented, and will also allow an HTTP client to ask what methods are implemented for a given entity.

Let us apply this by creating the shell of a person entity as follows:

sub person :Local :ActionClass('REST') Args(1) {
my ($self, $c, $id) = @_;
$c->stash(id => $id);
}
sub person_GET {
my ($self, $c) = @_;
my $id = $c->stash->{id};
my $person = $c->model('AddressDB::People')->
find({id => $id});
if ($person) {
$self->status_ok($c, entity =>
mk_person_entity($person));
}
else {
$self->status_not_found($c, message =>
'No matching person found'),
}
}
sub mk_person_entity {
my $person = shift;
my $addresses =
[ map {
mk_address_entity($_)->{address}
}
$person->addresses
];
return { person => { firstname => $person->firstname,
lastname => $person->lastname,
fullname => $person->name,
id => $person->id,
addresses => $addresses,
}
};
}

Here, we create the main person action, which optionally accepts an ID number and implements the GET method. Notice that we use some convenience methods in Catalyst::Controller::REST to handle the HTTP plumbing for us. When a record is found successfully, we call status_ok with the entity. When a record is not found, we call status_not_found with an error message. These two methods will send the appropriate HTTP response codes (200 and 404, respectively) and will serialize the provided entity and error message with the client's serializer of choice.

One thing to watch out for is that we don't implement the mk_address_entity method until later in the chapter. So for testing purposes, comment out the map {} statement and the address => $addresses hash entry for now.

Once you've added this code, you're ready to test your first REST action! Start the development server, open a web browser, and point it to http://localhost:3000/rest/person/1. Depending on what headers your browser sends, you'll either see an XML version of the person object:

<opt>
<data id="1" name="person" firstname="Jonathan"
fullname="Jonathan Rockway" lastname="Rockway" />
</opt>

or perhaps a YAML version:

---
person:
firstname: Jonathan
fullname: Jonathan Rockway
id: 1
lastname: Rockway

And if your browser sends no Accept header at all, you'll see the JSON fallback:

{"person":{"firstname":"Jonathan","lastname":"Rockway","id":1, "fulname:"Jonathan Rockway"}}

As you can see, regardless of the format returned, the information is much easier to reuse than it would be without the API.

Now that we have the basic idea of REST, let's add a PUT method for updating the existing records:

sub person_PUT {
my ($self, $c) = @_;
my $id = $c->stash->{id};
my $person = $c->model('AddressDB::People')->
find_or_new({id => $id});
if ($person) {
$person->firstname ($c->req->data->{firstname})
if $c->req->data->{firstname};
$person->lastname ($c->req->data->{lastname} )
if $c->req->data->{lastname};
$person->insert_or_update;
$self->status_created($c,
entity =>
mk_person_entity($person),
location =>
$c->
uri_for("/rest/person/$id"));
}
else {
$self->status_not_found($c,
message => 'No matching person found'),
}
}

This looks similar to GET, but we actually update the record with data from the serialized request (available via $c->req->data). When we update the record successfully, we return a '201 Created' response. By convention, we include the URL where this object can be requested, but we also include the object so that the client doesn't have to make another request. REST is flexible; you can use both a location and an entity, or just one of them.

Note

We're not doing any data validation here (other than not changing fields that aren't sent). In a real application, you'll want to factor the validation code out of the Person Controller (and into the model, usually) and share it between the REST and non-REST interface. When the data is invalid, simply return a '400 Bad Request', which tells the client to not repeat the request without changing the data.

With PUT done, we need a way to create a new record (without providing an ID).

POST serves the following purpose:

sub person_POST {
my ($self, $c) = @_;
my $person = $c->model('AddressDB::People')->
create({ firstname => $c->req->data->{firstname},
lastname => $c->req->data->{lastname},
});
my $id = $person->id;
$self->status_created($c,
entity =>
mk_person_entity($person),
location =>
$c->uri_for("/rest/person/$id"));
}

Finally, we need a way to delete records:

sub person_DELETE {
my ($self, $c) = @_;
my $id = $c->stash->{id};
my $person = $c->model('AddressDB::People')->
find({id => $id});
if ($person) {
$self->status_ok($c,
entity => mk_person_entity($person));
$person->delete;
}
else {
$self->status_not_found($c,
message => 'No matching person found'),
}
}

In this example, we return a copy of the deleted object, but this isn't always necessary. It's perfectly fine for the DELETE method to delete the object and just return '200 OK'.

With the person entity out of the way, let us add an interface to the address objects. The procedure is exactly the same:

sub address : Local : ActionClass('REST'){
my ($self, $c, $id) = @_;
$c->stash(id => $id);
}
sub address_GET {
my ($self, $c) = @_;
my $id = $c->stash->{id};
my $address = $c->model('AddressDB::Addresses')->find({id =>
$id});
if ($address) {
$self->status_ok($c, entity => mk_address_entity($address));
}
else {
$self->status_not_found($c, message => 'No matching address found'),
}
}
sub address_PUT {
my ($self, $c) = @_;
my $id = $c->stash->{id};
eval {
my $address = $c->model('AddressDB::Addresses')->
find_or_new({id => $id});
$address->location($c->req->data->{location})
if $c->req->data->{location};
$address->postal ($c->req->data->{postal})
if $c->req->data->{postal};
$address->phone ($c->req->data->{phone})
if $c->req->data->{phone};
$address->email ($c->req->data->{email})
if $c->req->data->{email};
$address->person ($c->req->data->{person})
if $c->req->data->{person};
$address->insert_or_update;
$id = $address->id;
$self->status_created($c,
entity => mk_address_entity($address),
location => $c->uri_for("/rest/address/$id"));
};
if ($@) {
$self->status_bad_request($c,
message => "Invalid data supplied: $@");
}
}
sub address_POST {
my ($self, $c) = @_;
eval {
my $address = $c->model('AddressDB::Addresses')->
create({ location => $c->req->data->{location},
postal => $c->req->data->{postal},
phone => $c->req->data->{phone},
email => $c->req->data->{email},
person => $c->req->data->{person},
});
my $id = $address->id;
$self->status_created($c,
entity => mk_address_entity($address), location => $c->uri_for("/rest/address/$id"));
};
if ($@) {
$self->status_bad_request($c,
message => "Invalid data supplied: $@");
}
}
sub address_DELETE {
my ($self, $c) = @_;
my $id = $c->stash->{id};
my $address = $c->model('AddressDB::Addresses')-> find({id =>$id});
if ($address) {
$self->status_ok($c, entity => mk_address_entity($address));
$address->delete;
}
else {
$self->status_not_found($c, message => 'No matching address found'),
}
}
sub mk_address_entity {
my $address = shift;
return { address => { id => $address->id,
person => $address->person->id,
person_name => $address->person->name,
email => $address->email,
phone => $address->phone,
postal => $address->postal,
location => $address->location,
}
};
}

Now that you have the mk_address_entity function, you can return the address records for a person when the person is requested, that is, you can remove the comments from the earlier lines.

This time we're doing a bit of validation. As the database might throw an exception when a foreign key or NOT NULL constraint is violated, we wrap database writes in an eval {} block. If there's a database error, we return '400 Bad Request' and the error message from the database. You'll probably want to return a more detailed error message in a real application, as your users probably don't care about what database constraints you have.

REST easy

That's all there is to creating an API for your application—give each object a URL and allow HTTP verbs as actions to perform on that object.

In the next section, we'll use our own REST API to implement AJAX.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.62.122