# Palm::Progect.pm
#
# Perl class for dealing with Palm Progect databases.
#
# Author: Michael Graham
# Thanks to Andrew Arensburger's great Palm::* modules
use strict;
package Palm::Progect;
use Palm::StdAppInfo;
use Palm::PDB;
use Palm::Raw;
use Palm::Progect::Constants;
use Palm::Progect::Record;
use Palm::Progect::Prefs;
use Palm::Progect::Converter;
use vars '$VERSION';
$VERSION = '2.0.4';
=head1 NAME
Palm::Progect - Handler for Palm Progect databases.
=head1 SYNOPSIS
use Palm::Progect;
use Palm::Progect::Constants;
my $progect = Palm::Progect->new('options' => { 'quiet' => 1 });
$progect->load_db(
file => $some_file,
);
$progect->export_records(
file => $some_other_file,
format => 'text',
options => {
tabstop => 4,
fill_with_spaces => 1,
date_format => 'dd-mm-yyyy',
},
);
=head1 DESCRIPTION
Palm::Progect is a class for handling Progect Database files.
Progect is a hierarchical organizer for the Palm OS. You can find it at:
L<http://sourceforge.net/projects/progect>
Palm::Progect allows you to load and save Progect databases (and to convert
between database versions), and to import and export records in various formats.
If all you are interested in doing is converting from one format to another,
you should probably look at the C<progconv> utility program which does just that.
These docs are for developers who want to manipulate Progect C<PDB> files
programatically.
=head1 OVERVIEW
You should be able to access all functions of the C<Palm::Progect> system
directly from the C<Palm::Progect> module.
Although the various database drivers and record converters all live in
their own Perl modules, C<Palm::Progect> is the interface to their
functionality. It will transparently delegate to the appropriate module
behind the scenes necessary.
You can load a C<Palm::Progect> database from a Progect C<PDB> file (via
the C<load_db> method), or import records and/or preferences from
another format (such as Text or CSV) (via the C<import_records> and
C<import_prefs> methods).
After a Progect database has been loaded or imported, you will have
a list of records (in C<$progect-E<gt>records>), and a preferences object
(in C<$progect-E<gt>preferences>).
Each record in C<$progect-E<gt>records> is an object of type
L<Palm::Progect::Record>.
for my $rec (@{ $progect->records }) {
my $description = $rec->description;
my $priority = $rec->priority;
print "[$priority] $description\n";
}
See L<Palm::Progect::Record> for the format of these records.
Once you have loaded the records and preferences, you can save them
to a Progect C<PDB> file (via the C<save_db> method), or export
them to another format (such as Text or CSV), via the C<export_records>
and C<export_prefs> methods.
Currently the C<Preferences> interface is not well defined and is
mainly there to allow for future development. See L<BUGS and CAVEATS>.
This module was largely written in support of the B<progconv> utility,
which is a conversion utility which imports and exports between
Progect PDB files and other formats.
=head2 Constructor
=over 4
=item new
Create a new C<Palm::Progect> object:
my $progect = Palm::Progect->new(options => \%Options);
options takes an optional hashref containing arguments to the
system. Currently this allows only a single option:
=over 4
=item quiet
Suppress informational messages when loading and saving databases.
=back
=back
=head2 Methods
=over 4
=item records
A reference to the list of records within the database. Each record
is an object of type C<Palm::Progect::Record>.
=item prefs
A reference to the preferences object within the database. It is an
object of type C<Palm::Progect::Prefs>. For now the prefs object
doesn't do very much and is mostly a placeholder to allow for future
development.
=item options
Reference to the hash of user options passed to the C<new> constructor.
See the C<new> constructor for details.
=item version
The Progect database version currently in use. This can come directly
from the source database (loaded with C<load_db>) or from the user (as
an argument to C<load_db> or C<save_db>).
=begin internal_use_only
=item _palm_pdb
The underlying C<Palm::Raw> database which C<Palm::Progect> uses
to access the database file.
=end internal_use_only
=cut
use CLASS;
use base qw(Class::Accessor Class::Constructor);
my @Accessors = qw(
_palm_pdb
records
prefs
options
version
);
CLASS->mk_accessors(@Accessors);
CLASS->mk_constructor(
Auto_Init => \@Accessors,
Init_Methods => '_init',
);
sub _init {
my $self = shift;
&Palm::PDB::RegisterPDBHandlers('Palm::Raw', [ "lbPG", "DATA" ], );
$self->_palm_pdb(
Palm::Raw->new
);
}
=item load_db(file =E<gt> $filename, version =E<gt> $version)
Load the Progect database file specified by $filename.
The C<version> parameter is optional. Normally you would
leave it out and let C<Palm::Progect> determine the version
from the database file itself.
If you specify a particular C<version>, then C<Palm::Progect> will attempt
to read the database as that version. This would be useful for instance
in the case of a corrupt PDB that indicates an incorrect version, or a
PDB of a version that Palm::Progect does not support (but you want to
try and see if it can read it anyway).
Currently supported versions are C<18> (for Progect database version 0.18) and
C<23> (for Progect database version 0.23).
Progect database version 0.18 was used all the way up until Progect version
0.22, so if you saved a database with Progect 0.22, the database will be
a version 0.18 database.
=cut
sub load_db {
my $self = shift;
my %args = @_;
my $file = $args{'file'};
print STDERR "Loading Progect database from $file\n" unless $self->options->{'quiet'};
$self->_palm_pdb->Load($file);
# Determine the version from the database
# Lucky for us, the db version number is the first byte
# of the appinfo block.
my $appinfo = {};
if ($self->_palm_pdb->{'appinfo'}) {
&Palm::StdAppInfo::parse_StdAppInfo($appinfo, $self->_palm_pdb->{'appinfo'});
}
else {
$appinfo = {
'categories' => [],
'other' => pack('C', 23),
};
}
my $version = unpack 'C', $appinfo->{'other'};
print STDERR "Progect database is version $version\n" unless $self->options->{'quiet'};
# Allow the user to manually override the version
# (after all, the database prefs might be corrupt)
if ($args{version} and $version != $args{version}) {
$version = $args{version};
print STDERR "Forcing version to $version\n" unless $self->options->{'quiet'};
}
my @raw_records = @{ $self->_palm_pdb->{'records'} };
# Categories will always be a list of unique names
my @categories = @{$appinfo->{'categories'}};
# Tell the Record class which categories we know about
Palm::Progect::Record->set_categories(@categories);
# Build @records from @raw_records:
my @records;
for my $raw_record (@raw_records) {
my $record = Palm::Progect::Record->new(
version => $version,
raw_record => $raw_record,
);
push @records, $record;
}
# This doesn't do much at present
my $prefs = Palm::Progect::Prefs->new(
version => $version,
appinfo => $appinfo,
name => _db_name_from_filename($file),
);
$prefs->categories(@categories);
$self->records(@records);
$self->prefs($prefs);
$self->version($version);
}
=item save_db(file =E<gt> $filename, version =E<gt> $version)
Save the records and prefs as a Progect database of version C<$version>
to the filename C<$filename>.
If you do not specify a version then the latest available version is
assumed, unless you have set C<version> before, by a previous call
to C<load_db> or C<save_db>.
Currently supported versions are C<18> (for Progect database version 0.18) and
C<23> (for Progect database version 0.23).
Progect database version 0.18 was used all the way up until Progect version
0.22, so if you saved a database with Progect 0.22, the database will be
a version 0.18 database.
=cut
sub save_db {
my $self = shift;
my %args = @_;
my $file = $args{'file'};
my $save_version = $args{'version'} || 0;
my $loaded_version = $self->version || 0;
# Repair the records tree
$self->repair_tree;
# Pack the raw records from our list of records
my @records = @{ $self->records };
my (@raw_records);
for my $record (@records) {
my $new_record = Palm::Progect::Record->new(
version => $save_version,
from_record => $record,
);
push @raw_records, $new_record->raw_record;
}
# Use our prefs object, if it exists.
# Otherwise, create a new one.
my $prefs = $self->prefs;
$prefs = Palm::Progect::Prefs->new(
version => $save_version,
from_prefs => $self->prefs,
);
$self->prefs($prefs);
# Fetch the final category list from the Record object
my @categories = Palm::Progect::Record::get_categories();
# Pack the categories into the prefs
$prefs->categories(@categories);
$prefs->name(_db_name_from_filename($file));
# $version is now our preferred db version
$self->version($save_version);
# Save our records
$self->records(\@raw_records);
# put @raw_records, $raw_prefs, and some constant stuff into $self->_palm_pdb
$self->_palm_pdb->{'records'} = \@raw_records;
$self->_palm_pdb->{'appinfo'} = $prefs->packed_appinfo;
$self->_palm_pdb->{'creator'} = 'lbPG';
$self->_palm_pdb->{'type'} = "DATA";
$self->_palm_pdb->{'attributes'}{'resource'} = 0;
# This may move to Palm::Progect::Prefs eventually...
$self->_palm_pdb->{'name'} = $prefs->name;
# Finally, write the pdb file
print STDERR "Saving Progect database in version $save_version to $file\n" unless $self->options->{'quiet'};
$self->_palm_pdb->Write($file);
}
=item import_records(%args)
Import records from a file.
The options passed in C<%args> are as follows:
=over 4
=item file
The file to import the records from.
=item format
The conversion format to use when importing the records.
Internally, this determines which module will do the actual conversion.
For instance, specifying a format of C<Text> will cause
C<Palm::Progect::Converter::Text> module to handle the import.
=item append
If true, then C<import_records> will B<append> the records imported from C<file>
to the internal records list. If false, C<import_records> will B<replace>
the internal records list with the records imported from C<file>.
=back
You can pass other options to C<import_records>, and these will be passed
directly to the module that does the eventual conversion. For instance:
$progect->import_records(
file => 'somefile.csv',
format => 'CSV',
date_format => 'dd-mm-yyyy',
);
In this example, the value of C<date_format> will get passed directly
to the C<Palm::Progect::Converter::CSV> module.
=cut
sub import_records {
my $self = shift;
my %args = @_;
my $file = delete $args{'file'};
my $append = delete $args{'append'};
my $converter = Palm::Progect::Converter->new(
%args,
);
$converter->load_records($file, $append);
$self->records($converter->records);
$self->prefs($converter->prefs);
}
=item export_records(%args)
Export records to a file.
The options passed in C<%args> are as follows:
=over 4
=item file
The file to export the records to. If blank, then the
exported records will be written to STDOUT.
=item format
The conversion format to use when exporting the records.
Internally, this determines which module will do the actual conversion.
For instance, specifying a format of C<Text> will cause
C<Palm::Progect::Converter::Text> module to handle the export.
=item append
If true, then C<export_records> will B<append> the exported records to C<file>.
If false, C<export_records> will overwrite C<file> (if it exists)
before exporting the records.
=back
You can pass other options to C<export_records>, and these will be passed
directly to the module that does the eventual conversion. For instance:
$progect->export_records(
file => 'somefile.csv',
format => 'CSV',
date_format => 'dd-mm-yyyy',
);
In this example, the value of C<date_format> will get passed directly
to the C<Palm::Progect::Converter::CSV> module.
=cut
sub export_records {
my $self = shift;
my %args = @_;
my $file = delete $args{'file'};
my $append = delete $args{'append'};
my $converter = Palm::Progect::Converter->new(
%args,
records => $self->records,
prefs => $self->prefs,
);
$converter->save_records($file, $append);
}
=item import_prefs
Import preferences from a file. Currently this is not supported.
=cut
sub import_prefs {
my $self = shift;
my %args = @_;
}
=item export_prefs
Export preferences to a file. Currently this is not supported.
=cut
sub export_prefs {
my $self = shift;
my %args = @_;
}
=item repair_tree
Goes through the list of records and repairs the relationships between them:
$progect->repair_tree;
C<Palm::Progect> calls this method internally just before it saves a Progect
database file.
That means:
=over 4
=item *
Insert the root record (no description, level 0) if necessary.
=item *
Fix the parent/child/sibling relationships (C<has_child>, C<has_next>,
C<has_prev>, etc.) if necessary.
=back
=cut
sub repair_tree {
my $self = shift;
my @records = @{ $self->records };
# Insert the "root record" if necessary
if ($records[0]->level or $records[0]->description) {
my $root_record = new Palm::Progect::Record( version => $self->version );
$root_record->has_child(1);
$root_record->level(0);
$root_record->is_opened(1);
unshift @records, $root_record;
}
# Fix relations between records
for (my $i = 0; $i < @records; $i++) {
my $rec = $records[$i];
$rec->has_child(0);
$rec->has_next(0);
$rec->has_prev(0);
if ($i == 0 and @records > 0) {
$rec->has_prev(0);
my $next_rec = $records[$i+1];
$rec->has_child(1) if $next_rec and $next_rec->level > $rec->level;
# Look ahead to other records, see if we
# can find one at the same level as us,
# before we cross one at a previous level
for (my $j = $i + 1; $j < @records; $j++) {
my $other_record = $records[$j];
last if $other_record->level < $rec->level;
if ($other_record->level == $rec->level) {
$rec->has_next(1);
last;
}
}
}
else {
my $prev_rec = $records[$i-1];
if (@records > $i) {
my $next_rec = $records[$i+1];
$rec->has_child(1) if $next_rec and ($next_rec->level || 0) > ($rec->level || 0);
}
# Look ahead to other records, see if we
# can find one at the same level as us,
# before we cross one at a previous level
if ($i < @records) {
for (my $j = $i + 1; $j < @records; $j++) {
my $other_record = $records[$j];
last if $other_record->level < $rec->level;
if ($other_record->level == $rec->level) {
$rec->has_next(1);
last;
}
}
}
# Same thing, working backwards
for (my $j = $i - 1; $j > 0; $j--) {
my $other_record = $records[$j];
last if $other_record->level < $rec->level;
if ($other_record->level == $rec->level) {
$rec->has_prev(1);
last;
}
}
}
}
$self->records(@records);
}
=back
=begin internal_use_only
=head2 Utility Subroutines
=over 4
=item _db_name_from_filename
This is a subroutine, not a method. Call it like:
my $db_name = _db_name_from_filename($filename);
Given a filename, try to come up with a sensible name for the progect
database. Remove the extension, the C<lbPG> prefix (if any), etc.
=back
=end internal_use_only
=cut
sub _db_name_from_filename {
my $filename = shift;
$filename =~ tr{\\}{/};
$filename =~ tr{:}{/};
$filename = (split m{/}, $filename)[-1];
$filename =~ s/^lbPG-//;
$filename =~ s/\..*?$//;
return $filename;
}
1;
__END__
=head1 BUGS and CAVEATS
=head2 Categories
Palm::Progect reads and writes categories properly from and to Progect C<PDB>
files. As of version 0.25, Progect itself can read these categories properly.
Versions of Progect earlier than 0.25 may have problems reading
the categories as saved by Palm::Progect.
This is due to the fact that Palm::Progect does not write the preferences
block correctly.
As a result, when you load into an older version of Progect a database
that you created with Palm::Progect, You will get a warning that "Your
preferences have been deleted".
Progect will then reset the category list.
However, all of the records will still keep their references to the deleted
categories.
So, if you select "Edit Categories..." and recreate the categories
B<in the exact same order> as they were before, the records will
magically return to their proper categories.
Again, these steps are only required when you are using a version of
Progect that is older than version 0.25.
=head2 Preferences
Preferences are not handled properly yet. They cannot be imported or
exported, and they are not read from the Progect database file.
Additionally, in Progect version 0.23 and earlier, when you load a
database created by Palm::Progect into Progect, you will get a warning
that "Your preferences have been deleted". The preferences for the
database will be reset to sensible defaults.
In Progect version 0.25, you will not get this warning.
=head2 Two-digit Dates
Using a two digit date format will fail for dates before 1950
or after 2049 :).
=head1 AUTHOR
Michael Graham E<lt>mag-perl@occamstoothbrush.comE<gt>
Copyright (C) 2002-2005 Michael Graham. All rights reserved.
This program is free software. You can use, modify,
and distribute it under the same terms as Perl itself.
The latest version of this module can be found on http://www.occamstoothbrush.com/perl/
=head1 SEE ALSO
C<progconv>
L<Palm::PDB(3)>
http://progect.sourceforge.net/
=cut