Sign In/My Account | View Cart  
advertisement


Listen Print

Building Applications with POE

by Matt Cashner
July 23, 2004

Earlier, we talked about the fundamental principles of application design with POE. Now it's time to put my money where my mouth is and build some actual working code.

To make life a bit easier, let's lay out a very simple problem. Let's say we would like accept and parse data that resembles CGI query strings. This data will be key value pairs in which the key and value are separated by ='s and the pairs themselves are delimited by &. An example string we'll use throughout this article is as follows:

  foo=bar&baz=1&bat=2

By the time we're done, we will have a working Filter and Component to handle this incoming data.

Step 1: A Filter

The first step is building a simple filter to parse this incoming data. As we discussed earlier, filters are much easier to deal with because they are unaware of their environment and the POE context in which they are run. Our filter is made even easier since we are just parsing incoming data and not generating an outgoing datastream.

First off, we need the basics of any good module.

  package POE::Filter::SimpleQueryString;

  use warnings;
  use strict;

  use Carp qw(carp croak);

Next we need a constructor.

  sub new { 
    my $class = shift;
    my $self = bless {}, $class;
    return $self;
  }

Related Reading

Learning Perl Objects, References, and Modules

Learning Perl Objects, References, and Modules
By Randal L. Schwartz

Table of Contents
Index
Sample Chapter

Read Online--Safari Search this book on Safari:
 

Code Fragments only

This is about the simplest constructor possible. This very simple filter requires no parameters to operate. It is perfectly reasonable, however, to demand parameters of filter users. For instance, if the filter could rot13 the incoming data before parsing, and a parameter could turn that feature on.

get()

Now we need the ability to parse data. We will be using the newer and much simpler get/put version of the Filter API. This version of the standard POE Filter API requires get() and put() methods with the ability to transform multiple record sets per invocation.

get()'s job is transform raw data into cooked record sets. The example string above (foo=bar&baz=1&bat=2) will become a hash:

  $VAR1 = {
    'foo' => 'bar',
    'baz  => '1',
    'bat  => '2',
  };

A POE Filter is just a normal Perl object with a defined interface.

  sub get {
    my $self = shift;
    my $buffer = shift;

The buffer can and probably will contain multiple records. The size of the buffer is determined by the POE Driver being used and the operating system in question. $buffer will always be an array reference. While it is generally sensible to test $buffer to make sure it conforms to the standard interface, for the purpose of this exercise, we will just trust POE.

In our super-easy format, an individual record is terminated by a \n. Key value pairs are delimited by & and key and value themselves are separated by an =. Note that we aren't dealing with issues like character escaping or data taint. Production quality code will need to deal with these issues.

    my @chunks; 

Each parsed line makes up a chunk of data. We want to represent each record as a distinct entity to the user.

    foreach my $record (@$buffer) { 
      $record =~ s/\x0d\x0a$//;
      my @pairs = split(/&/, $record);
    
      my %chunk;
      foreach my $pair (@pairs) {
        my ($key, $value) = split(/=/, $pair, 2);

So what happens if there is more than one instance of a given key in a record? Simple. We make an array reference. The user will need to inspect the value of each key to determine if they have more than one value:

        if(defined $chunk{$key}) {
          if(ref $chunk{$key} eq 'ARRAY') {
            push @{ $chunk{$key} }, $value;
          } else {
            $chunk{$key} = [ $chunk{$key}, $value ],
          }
        } else {
          $chunk{$key} = $value;
        }
      }
      push @chunks, \%chunk;
    }
    return \@chunks;
  }

put()

We now have a simple query-string-like data parser. This is fine for read-only servers but it makes sense to allow our users to send data back and forth in the same format. To allow for that, we need a put() method. put()'s job is take the cooked form of our records and translate it to the raw form. In this case we will be taking a hash reference that looks like:

  $VAR1 = {
    'foo' => 'bar',
    'baz' => '1',
    'bat' => '2',
  };

And transforming it into:

  foo=bar&baz=1&bat=2

So that whatever Wheel our user has chosen can put the data onto the wire. Like get(), put() is a normal method call on a normal perl object.

  sub put { 
    my $self = shift;
    my $records = shift;

Like get(), the data to act on is passed in as a parameter to the method call. It is always an array reference of records to translate.

Basically, our put() method performs an exact reversal of the get() algorithm. Take each key/value pair and concatenate them with an =. Each pair is then joined with a &.

The only real error condition we are checking for is the presence of non-array data. Since we haven't defined behavior for this condition, we warn the user about the data and skip it:

    my @raw;
    foreach my $record (@$records) {
      my @chunks;
      foreach my $key (sort keys %$record) {
        if(ref $record->{$key}) {
          if(ref $record->{$key} eq 'ARRAY') {
            foreach my $value ( @{ $record->{$key} } ) {
              push @chunks, $key."=".$value;
            }
          } else {
            carp __PACKAGE__." cannot handle data of type 
            ".ref $record->{$key};
          }
        } else {
          push @chunks, $key."=".$record->{$key};
        }
      }
      push @raw, join('&',@chunks)."\x0d\x0a";
    }
    return \@raw;
  }

The raw data is returned to the caller as an array reference of data chunks. The caller has the responsibility of putting the data on the wire in the appropriate fashion.

Pages: 1, 2

Next Pagearrow