NAME

FlatFile::DataStore - Perl module that implements a flat file data store.

SYNOPSYS

use FlatFile::DataStore;

# new datastore object

my $dir  = "/my/datastore/directory";
my $name = "dsname";
my $ds   = FlatFile::DataStore->new( { dir => $dir, name => $name } );

# create a record

my $record_data = "This is a test record.";
my $user_data   = "Test1";
my $record = $ds->create( $record_data, $user_data );
my $record_number = $record->keynum;

# retrieve it

$record = $ds->retrieve( $record_number );

# update it

$record->data( "Updating the test record." );
$record = $ds->update( $record );

# delete it

$record = $ds->delete( $record );

# get its history

my @records = $ds->history( $record_number );

DESCRIPTION

FlatFile::DataStore implements a simple flat file data store. When you create (store) a new record, it is appended to the flat file. When you update an existing record, the existing entry in the flat file is flagged as updated, and the updated record is appended to the flat file. When you delete a record, the existing entry is flagged as deleted, and a deleted record is appended to the flat file.

The result is that all versions of a record are retained in the data store, and running a history will return all of them. Another result is that each record in the data store represents a transaction: create, update, or delete.

Methods support the following actions:

- create
- retrieve
- update
- delete
- history

Additionally, FlatFile::DataStore::Utils provides the methods

- validate
- migrate

and others.

See FlatFile::DataStore::Tiehash for a tied interface.

VERSION

FlatFile::DataStore version 0.11

CLASS METHODS

FlatFile::DataStore->new();

Constructs a new FlatFile::DataStore object.

Accepts hash ref giving values for dir and name.

my $ds = FlatFile::DataStore->new(
    { dir  => $dir,
      name => $name,
    } );

To initialize a new data store, edit the "$dir/$name.uri" file and enter a configuration URI (as the only line in the file), or pass the URI as the value of the uri parameter, e.g.,

my $ds = FlatFile::DataStore->new(
    { dir  => $dir,
      name => $name,
      uri  => join( ";" =>
          "http://example.com?name=$name",
          "desc=My%20Data%20Store",
          "defaults=medium",
          "user=8-%20-%7E",
          "recsep=%0A",
          ),
    } );

(See URI Configuration below.)

Returns a reference to the FlatFile::DataStore object.

OBJECT METHODS, Record Processing (CRUD)

create( $record_data[, $user_data] )

Creates a record. The parm $record_data may be one of

- data string
- scalar reference (to the data string)
- FlatFile::DataStore::Record object

The parm $user_data may be omitted if $record_data is an object, in which case the user data will be gotten from it.

Returns a Flatfile::DataStore::Record object.

Note: the record data (but not user data) is stored in the FF::DS::Record object as a scalar reference. This is done for efficiency in the cases where the record data may be very large. Likewise, the first parm to create() is allowed to be a scalar reference.

retrieve( $num[, $pos] )

Retrieves a record. The parm $num may be one of

- a key number, i.e., record sequence number
- a file number

The parm $pos is required if $num is a file number.

Returns a Flatfile::DataStore::Record object.

retrieve_preamble( $keynum )

Retrieves a preamble. The parm $keynum is a key number, i.e., record sequence number

Returns a Flatfile::DataStore::Preamble object.

This method allows getting information about the record, e.g., if it's deleted, what's in the user data, etc., without the overhead of retrieving the full record data.

update( $object_or_string[, $record_data][, $user_data] )

Updates a record. The parm $object_or_string may be one of:

- FlatFile::DataStore::Record object
- FlatFile::DataStore::Preamble object
- Preamble string

The parms $record_data and $user_data may be omitted only if $object_or_string is a FF::DS::Record object, in which case the record and user data will be gotten from it.

Returns a Flatfile::DataStore::Record object.

Note: the record data (but not user data) is stored in the FF::DS::Record object as a scalar reference. This is done for efficiency in the cases where the record data may be very large. Likewise, the first parm to create() is allowed to be a scalar reference.

delete( $object_or_string[, $record_data][, $user_data] )

Deletes a record. The parm $object_or_string may be one of:

- FlatFile::DataStore::Record object
- FlatFile::DataStore::Preamble object
- Preamble string

The parms $record_data and $user_data may be omitted only if $object_or_string is a FF::DS::Record object, in which case the record and user data will be gotten from it.

Returns a Flatfile::DataStore::Record object.

Note: the record data (but not user data) is stored in the FF::DS::Record object as a scalar reference. This is done for efficiency in the cases where the record data may be very large. Likewise, the first parm to create() is allowed to be a scalar reference.

history( $keynum )

Retrieves a record's history. The parm $keynum is always a key number, i.e., a record sequence number.

Returns an array of FlatFile::DataStore::Record objects.

The first element of this array is the current record. The last element is the original record. That is, the array is in reverse chronological order.

OBJECT METHODS, Accessors

$ds->specs( [$omap] )

Sets and returns the specs attribute value if $omap is given, otherwise just returns the value.

An 'omap' is an ordered hash as defined in

http://yaml.org/type/omap.html

That is, it's an array of single-key hashes. This ordered hash contains the specifications for constructing and parsing a record preamble as defined in the name.uri file.

$ds->dir( [$dir] )

Sets and returns the dir attribute value if $dir is given, otherwise just returns the value.

If $dir is given and is a null string, the dir object attribute is removed from the object. If $dir is not null, the directory must already exist.

Preamble accessors (from the uri)

The following methods set and return their respective attribute values if $value is given. Otherwise, they just return the value.

$ds->indicator( [$value] );  # length-characters
$ds->transind(  [$value] );  # length-characters
$ds->date(      [$value] );  # length-format
$ds->transnum(  [$value] );  # length-base
$ds->keynum(    [$value] );  # length-base
$ds->reclen(    [$value] );  # length-base
$ds->thisfnum(  [$value] );  # length-base
$ds->thisseek(  [$value] );  # length-base
$ds->prevfnum(  [$value] );  # length-base
$ds->prevseek(  [$value] );  # length-base
$ds->nextfnum(  [$value] );  # length-base
$ds->nextseek(  [$value] );  # length-base
$ds->user(      [$value] );  # length-characters

Other accessors

$ds->name(        [$value] ); # from uri, name of data store
$ds->desc(        [$value] ); # from uri, description of data store
$ds->recsep(      [$value] ); # from uri, character(s)
$ds->uri(         [$value] ); # full uri as is
$ds->preamblelen( [$value] ); # length of preamble string
$ds->toclen(      [$value] ); # length of toc entry
$ds->keylen(      [$value] ); # length of stored keynum
$ds->keybase(     [$value] ); # base   of stored keynum
$ds->translen(    [$value] ); # length of stored transaction number
$ds->transbase(   [$value] ); # base   of stored transation number
$ds->fnumlen(     [$value] ); # length of stored file number
$ds->fnumbase(    [$value] ); # base   of stored file number
$ds->dateformat(  [$value] ); # format from uri
$ds->regx(        [$value] ); # capturing regx for preamble string
$ds->datamax(     [$value] ); # maximum bytes in a data file
$ds->crud(        [$value] ); # hash ref, e.g.,

    {
       create => '+',
       oldupd => '#',
       update => '=',
       olddel => '*',
       delete => '-',
       '+' => 'create',
       '#' => 'oldupd',
       '=' => 'update',
       '*' => 'olddel',
       '-' => 'delete',
    }

(logical actions <=> symbolic indicators)

Accessors for optional attributes

$ds->dirmax( [$value] );  # maximum files in a directory
$ds->dirlev( [$value] );  # number of directory levels
$ds->tocmax( [$value] );  # maximum toc entries
$ds->keymax( [$value] );  # maximum key entries

If no dirmax, directories will keep being added to.

If no dirlev, toc, key, and data files will reside in top-level directory. If dirmax given, dirlev defaults to 1.

If no tocmax, there will be only one toc file, which will grow indefinitely.

If no keymax, there will be only one key file, which will grow indefinitely.

OBJECT METHODS, Other

howmany( [$regx] )

Returns count of records whose indicators match regx, e.g.,

$self->howmany( qr/create|update/ );
$self->howmany( qr/delete/ );
$self->howmany( qr/oldupd|olddel/ );

If no regx, howmany() returns numrecs from the toc file, which should give the same number as qw/create|update/.

lastkeynum()

Returns the last key number used, i.e., the sequence number of the last record added to the data store, as an integer.

nextkeynum()

Returns lastkeynum()+1 (a convenience method). This would be useful for adding a new record to a hash tied to a data store, e.g.,

$h{ $ds->nextkeynum } = "New record data.";

close_files()

This routine will close all open files associated with the data store object. This is used in DESTROY(), but could conceivably be called by the application if it detects too many open files.

$ds->close_files();

The intention is that close_files() can be called any time -- new files would be opened again as needed.

URI Configuration

It may seem odd to use a URI as a configuration file. I needed some configuration approach and wanted to stay as lightweight as possible. The specs for a URI are fairly well-known, and it allows for everything we need, so I chose that approach.

The examples all show a URL, because I thought it would be a nice touch to be able to visit the URL and have the page tell you things about the data store. This is what the utils/flatfile-datastore.cgi program is intended to do, but it is in a very young/rough state so far.

Following are the URI configuration parameters. The order of the preamble parameters does matter: that's the order those fields will appear in each record preamble. Otherwise the order doesn't matter.

Where needed, parameter values may be uri-escaped, e.g., %20 for space.

Parameter values should be percent-encoded (uri escaped). Use %20 for space (don't be tempted to use '+'). Use URI::Escape::uri_escape , if desired, e.g.,

my $name = 'example';
my $dir  = '/example/dir';

use URI::Escape;
my $datastore = FlatFile::DataStore::->new( {
    name => $name,
    dir  => $dir,
    uri  => join( ';' =>
        "http://example.com?name=$name",
        "desc=" . uri_escape( 'My DataStore' ),
        "defaults=medium",
        "user=" . uri_escape( '8- -~' ),
        "recsep=%0A",
    ) }
);

Preamble parameters

All of the preamble parameters are required.

(In fact, four of them are optional, but leaving them out means that you're giving up keeping the linked list of record history, so don't do that unless you have a very good reason.)

indicator

The indicator parameter specifies the single-character record indicators that appear in each record preamble. This parameter has the following form: indicator=length-5CharacterString, e.g.,

indicator=1-+#=*-

The length is always 1. The five characters represent the five states of a record in the data store (in this order):

create(+): the record has not changed since being added
oldupd(#): the record was updated, and this entry is an old version
update(=): this entry is the updated version of a record
olddel(*): the record was deleted, and this entry is the old version
delete(-): the record is deleted, and this entry is the "delete record"

(The reason for a "delete record" is for storing information about the delete process, such has when it was deleted and by whom.)

The five characters shown in the example are the ones used by all examples in the documentation. You're free to use your own characters, but the length must always be 1.

transind

The transind parameter describes the single-character transaction indicators that appear in each record preamble. This parameter has the same format and meanings as the indicator parameter, e.g.,

transind=1-+#=*-

(Note that only three of these are used, but all five must be given and must match the indicator parameter.)

The three characters that are used are create(+), update(=), and delete(-). While the record indicators will change, e.g., from create to oldupd, or from update to olddel, etc., the transaction indicators never change from their original values. So a transaction that created a record will always have the create value, and the same for update and delete.

date

The date parameter specifies how the transaction date is stored in the preamble. It has the form: date=length-format, e.g.,

date=8-yyyymmdd
date=4-yymd

The example shows the two choices for length: 4 or 8. When the length is 8, the format must contain 'yyyy', 'mm', and 'dd' in some order. When the length is 4, the format must contain 'yy', 'm', and 'd' in some order, e.g.,

date=8-mmddyyyy, date=8-ddmmyyyy, etc.
date=4-mdyy, date=4-dmyy, etc.

When the length is 8, the year, month, and day are stored as decimal numbers, e.g., '20100615' for June, 15, 2010. When the length is 4, they are stored as base62 numbers, e.g. 'WQ6F' (yymd) for June 15, 2010.

transnum

The transnum parameter specifies how the transaction number is stored in the preamble. It has the form: transnum=length-base, e.g.,

transnum=4-62

The example says the number is stored as a four-digit base62 integer. The highest transaction number this allows is 'zzzz' base62 which is 14,776,335 decimal. Therefore, the datastore will accommodate up to that many transactions (creates, updates, deletes).

keynum

The keynum parameter specifies how the record sequence number is stored in the preamble. It has the form: keynum=length-base, e.g.,

keynum=4-62

As with the transnum example above, the keynum would be stored as a four-digit base62 integer, and the highest record sequence number allowed would be 14,776,335 ('zzzz' base62). Therefore, the data store could not store more than this many records.

reclen

The reclen parameter specifies how the record length is stored in the preamble. It has the form: reclen=length-base, e.g.,

reclen=4-62

This example allows records to be up to 14,776,335 bytes long.

thisfnum

The thisfnum parameter specifies how the file numbers are stored in the preamble. There are three file number parameters, thisfnum, prevfnum, and nextfnum. They must match each other in length and base. The parameter has the form: thisfnum=length-base, e.g.,

thisfnum=2-36

There is an extra constraint imposed on the file number parameters: they may not use a number base higher than 36. The reason is that the file number appears in file names, and base36 numbers match [0-9A-Z]. By limiting to base36, file names will therefore never differ only by case, e.g., there may be a file named example.Z.data, but never one named example.z.data.

The above example states that the file numbers will be stored as two-digit base36 integers. The highest file number is 'ZZ' base36, which is 1,295 decimal. Therefore, the datastore will allow up to that many data files before filling up. (If a datastore "fills up", it must be migrated to a newly configured datastore that has bigger numbers where needed.)

In a preamble, thisfnum is the number of the datafile where the record is stored. This number combined with the thisseek value and the reclen value gives the precise location of the record data.

thisseek

The thisseek parameter specifies how the seek positions are stored in the preamble. There are three seek parameters, thisseek, prevseek, and nextseek. They must match each other in length and base. The parameter has the form: thisseek=length-base, e.g.,

thisseek=5-62

This example states that the seek positions will be stored as five-digit base62 integers. So the highest seek position is 'zzzzz' base62, which is 916,132,831 decimal. Therefore, each of the datastore's data files may contain up to that many bytes (record data plus preambles).

Incidentally, no record (plus its preamble) may be longer than this, because it just wouldn't fit in a data file.

Also, the size of each data file may be further limited using the datamax parameter (see below). For example, a seek value of '4-62' would allow datafiles up to 14,776,335 bytes long. If you want bigger files, but don't want them bigger than 500 Meg, you can give thisseek=5-62 and datamax=500M.

prevfnum (optional)

The prevfnum parameter specifies how the "previous" file numbers are stored in the preamble. The value of this parameter must exactly match thisfnum (see thisfnum above for more details). It has the form: prevfnum=length-base, e.g.,

prevfnum=2-36

In a preamble, the prevfnum is the number of the datafile where the previous version of the record is stored. This number combined with the prevseek value gives the beginning location of the previous record's data.

This is the first of the four "optional" preamble parameters. If you don't provide this one, don't provide the other three either. If you leave these off, you will not be able to get a record's history of changes, and you will not be able to migrate any history to a new datastore.

So why would to not provide these? You might have a datastore that has very transient data, e.g., indexes, and you don't care about change history. By not including these four optional parameters, when the module updates a record, it will not perform the extra bit of IO to update a previous record's nextfnum and nextseek values. And the preambles will be a little bit shorter.

prevseek (optional)

The prevseek parameter specifies how the "previous" seek positions are stored in the preamble. The value of this parameter must exactly match thisseek (see thisseek above for more details). It has the form prevseek=length-base, e.g.,

prevseek=5-62
nextfnum (optional)

The nextfnum parameter specifies how the "next" file numbers are stored in the preamble. The value of this parameter must exactly match thisfnum (see thisfnum above for more details). It has the form: nextfnum=length-base, e.g.,

nextfnum=2-36

In a preamble, the nextfnum is the number of the datafile where the next version of the record is stored. This number combined with the nextseek value gives the beginning location of the next record's data.

You would have a nextfnum and nextseek in a preamble when it's a previous version of a record whose current version appears later in the datastore. While thisfnum and thisseek are critical for all record retrievals, prevfnum, prevseek, nextfnum, and nextseek are only needed for getting a record's history. They are also used during a migration to help validate that all the data (and transactions) were migrated intact.

nextseek (optional)

The nextseek parameter specifies how the "next" seek positions are stored in the preamble. The value of this parameter must exactly match thisseek (see thisseek above for more details). It has the form nextseek=length-base, e.g.,

nextseek=5-62
user

The user parameter specifies the length and character class for extra user data stored in the preamble. It has the form: user=length-CharacterClass, e.g.,

user=8-+-~    (must match /[ -~]+ */ and not be longer than 8)
user=10-0-9   (must match /[0-9]+ */ and not be longer than 10)
user=1-:      (must be literally ':')

When a record is created, the application supplies a value to store as "user" data. This might be a userid, an md5 digest, multiple fixed-length fields -- whatever is needed or wanted.

This field is required but may be preassigned using the userdata parameter (see below). If no user data is provided or preassigned, it will default to a space.

When this data is stored in the preamble, it is padded on the right with spaces.

Preamble defaults

All of the preamble parameters -- except user -- may be set using one of the defaults provided, e.g.,

http://example.com?name=example;defaults=medium;user=8-+-~
http://example.com?name=example;defaults=large;user=10-0-9

Note that these are in a default order also. And the user parameter is still part of the preamble, so you can make it appear first if you want, e.g.,

http://example.com?name=example;user=8-+-~;defaults=medium
http://example.com?name=example;user=10-0-9;defaults=large

The _nohist versions leave out the optional preamble parameters -- the above caveat about record history still applies.

Finally, if non of these suits, they may still be good starting points for defining your own preambles.

xsmall, xsmall_nohist

When the URI contains defaults=xsmall, the following values are set:

indicator=1-+#=*-
transind=1-+#=*-
date=4-yymd
transnum=2-62   3,843 transactions
keynum=2-62     3,843 records
reclen=2-62     3,843 bytes/record
thisfnum=1-36   35 data files
thisseek=4-62   14,776,335 bytes/file
prevfnum=1-36
prevseek=4-62
nextfnum=1-36
nextseek=4-62

The last four are not set for defaults=xsmall_nohist.

Rough estimates: 3800 records (or transactions), no larger than 3800 bytes each; 517 Megs total (35 * 14.7M).

small, small_nohist

For defaults=small:

indicator=1-+#=*-
transind=1-+#=*-
date=4-yymd
transnum=3-62   238,327 transactions
keynum=2-62     238,327 records
reclen=2-62     238,327 bytes/record
thisfnum=1-36   35 data files
thisseek=5-62   916,132,831 bytes/file
prevfnum=1-36
prevseek=5-62
nextfnum=1-36
nextseek=5-62

The last four are not set for defaults=small_nohist.

Rough estimates: 238K records (or transactions), no larger than 238K bytes each; 32 Gigs total (35 * 916M).

medium, medium_nohist

For defaults=medium:

indicator=1-+#=*-
transind=1-+#=*-
date=4-yymd
transnum=4-62   14,776,335 transactions
keynum=4-62     14,776,335 records
reclen=4-62     14,776,335 bytes/record
thisfnum=2-36   1,295 data files
thisseek=5-62   916,132,831 bytes/file
prevfnum=2-36
prevseek=5-62
nextfnum=2-36
nextseek=5-62

The last four are not set for defaults=medium_nohist.

Rough estimates: 14.7M records (or transactions), no larger than 14.7M bytes each; 1 Terabyte total (1,295 * 916M).

large, large_nohist

For defaults=large:

datamax=1.9G    1,900,000,000 bytes/file
dirmax=300
keymax=100_000
indicator=1-+#=*-
transind=1-+#=*-
date=4-yymd
transnum=5-62   916,132,831 transactions
keynum=5-62     916,132,831 records
reclen=5-62     916,132,831 bytes/record
thisfnum=3-36   46,655 data files
thisseek=6-62   56G per file (but see datamax)
prevfnum=3-36
prevseek=6-62
nextfnum=3-36
nextseek=6-62

The last four are not set for defaults=large_nohist.

Rough estimates: 916M records/transactions, no larger than 916M bytes each; 88 Terabytes total (46,655 * 1.9G).

xlarge, xlarge_nohist

For defaults=xlarge:

datamax=1.9G    1,900,000,000 bytes/file
dirmax=300
dirlev=2
keymax=100_000
tocmax=100_000
indicator=1-+#=*-
transind=1-+#=*-
date=4-yymd
transnum=6-62   56B transactions
keynum=6-62     56B records
reclen=6-62     56G per record (limited to 1.9G by datamax)
thisfnum=4-36   1,679,615 data files
thisseek=6-62   56G per file (but see datamax)
prevfnum=4-36
prevseek=6-62
nextfnum=4-36
nextseek=6-62

The last four are not set for defaults=xlarge_nohist.

Rough estimates: 56B records/transactions, no larger than 1.9G bytes each; 3 Petabytes total (1,679,615 * 1.9G).

Other required parameters

name

The name parameter identifies the datastore by name. This name should be short and uncomplicated, because it is used as the root for the datastore's files.

recsep

The recsep parameter gives the ascii character(s) that will make up the record separator. The "flat file" stategy suggests that these characters ought to match what your OS considers to be a "newline". But in fact, you could use an string of ascii characters.

recsep=%0A       (LF)
recsep=%0D%0A    (CR+LF)
recsep=%0D       (CR)

recsep=%0A---%0A (HR--sort of)

Also, if you develop your data on unix with recsep=%0A and then copy it to a windows machine, the module will continue to use the configured recsep, i.e., it is not tied the to OS.

Other optional parameters

desc

The desc parameter provides a means to give a short description (or perhaps a longer name) of the datastore.

datamax

The datamax parameter give the maximum number of bytes a data file may contain. If you don't provide a datamax, it will be computed from the thisseek value (see thisseek above for more details).

The datamax value is simply a number, e.g.,

datamax=1000000000   (1 Gig)

To make things easier to see, you can add underscores, e.g.,

datamax=1_000_000_000   (1 Gig)

You can also shorten the number with an 'M' for megabytes (10**6) or a 'G' for gigabytes (10**9), e.g.,

datamax=1000M  (1 Gig)
datamax=1G     (1 Gig)

Finally, with 'M' or 'G', you can use fractions, e.g.

datamax=.5M  (500_000)
datamax=1.9G (1_900_000_000)
keymax

The keymax parameter gives the number of record keys that may be stored in a key file. This simply limits the size of the key files, e.g.,

keymax=10_000

The maximum bytes would be:

keymax * (preamble length + recsep length)

The numeric value may use underscores and 'M' or 'G' as described above for datamax.

tocmax

The tocmax parameter gives the number of data file entries that may be stored in a toc (table of contents) file. This simply limits the size of the toc files, e.g.,

tocmax=10_000

Each (fairly short) line in a toc file describes a single data file, so you would need a tocmax only in the extreme case of a datastore with thousands or millions of data files.

The numeric value may use underscores and 'M' or 'G' as described above for datamax.

dirmax

The dirmax parameter gives the number of files (and directories) that may be stored in a datastore directory, e.g.,

dirmax=300

This allows a large number of data files (and key/toc files) to be created without there being too many files in a single directory.

(The numeric value may use underscores and 'M' or 'G' as described above for datamax.)

If you specify dirmax without dirlev (see below), dirlev will default to 1.

Without dirmax and dirlev, a datastore's data files (and key/toc files) will reside in the same directory as the uri file, and the module will not limit how many you may create (though the size of your filesystem might).

With dirmax and dirlev, these files will reside in subdirectories.

Giving a value for dirmax will also limit the number of data files (and key/toc files) a datastore may have, by this formula:

max files = dirmax ** (dirlev + 1)

So dirmax=300 and dirlev=1 would result in a limit of 90,000 data files. If you go to dirlev=2, the limit becomes 27,000,000, which is why you're unlikely to need a dirlev greater than 2.

dirlev

The dirlev parameter gives the number of levels of directories that a datastore may use, e.g.,

dirlev=1

You can give a dirlev without a dirmax, which would store the data files (and key/toc files) in subdirectories, but wouldn't limit how many files may be in each directory.

userdata

The userdata parameter is similar to the userdata parameter in the call to new(). It specifies the default value to use if the application does not provide a value when creating, updating, or deleting a record.

Those provided values will override the value given in the call to new(), which will override the value given here in the uri.

If you don't specify a default value here or in the call to new(), the value defaults to a space (which may then be padded with more spaces).

userdata=:

The example is contrived for a hypothetical datastore that doesn't need this field. Since the field is required, the above setting will always store a colon (and the user parameter might be user=1-:).

CAVEATS

This module is still in an experimental state. The tests are sparse. When I start using it in production, I'll bump the version to 1.00.

Until then (afterwards, too) please use with care.

AUTHOR

Brad Baxter, <bbaxter@cpan.org>

COPYRIGHT AND LICENSE

Copyright (C) 2010 by Brad Baxter

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.8 or, at your option, any later version of Perl 5 you may have available.