[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[coldsync-hackers] MobileDB to/from CSV converter



	I'm posting this mostly for suggestions.  It uses the following modules:

* the p5-Palm modules from the coldsync page
* the Text::CSV module from CPAN
* File::Basename, FileHandle, Getopt::Std, and Storable are standard with 
Perl

	I guess the coolest thing would be a CSV/MobileDB conduit, but right now 
this seems to convert small MobileDB files to text files with 
comma-separated values, and back.  The file 'blank.pdb' is a mostly-empty 
MobileDB file that I created on my Palm and transferred over.  I would like 
to know how to translate CSV to MobileDB without having such a "starter" 
file.  I'm reading from the source file into memory, then writing the data 
out in the destination file -- this could obviously burn lots of memory in 
the case of a large database.  But the advantage is that something else 
could be substituted for CSV.
# Palm::MobileDB.pm
#
# Perl class for dealing with MobileDB files.
#
# Based on Palm::Memo by Andrew Arensburger.

use strict;
package Palm::MobileDB;
use Palm::Raw();
use Palm::StdAppInfo();
use vars qw( $VERSION @ISA );

$VERSION = sprintf "%d.%03d", '$Revision: 1.10 $ ' =~ m{(\d+)\.(\d+)};
@ISA = qw( Palm::Raw Palm::StdAppInfo );

=head1 NAME

Palm::Memo - Handler for Palm Memo databases.

=head1 SYNOPSIS

    use Palm::Memo;

=head1 DESCRIPTION

The Memo PDB handler is a helper class for the Palm::PDB package. It
parses Memo databases.

=head2 AppInfo block

The AppInfo block begins with standard category support. See
L<Palm::StdAppInfo> for details.

Other fields include:

    $pdb->{appinfo}{sortOrder}

I don't know what this is.

=head2 Sort block

    $pdb->{sort}

This is a scalar, the raw data of the sort block.

=head2 Records

    $record = $pdb->{records}[N]

    $record->{data}

A string, the text of the memo.

=cut
#'

sub import
{
	&Palm::PDB::RegisterPDBHandlers(__PACKAGE__,
		[ "Mdb1", "Mdb1" ],
		);
}

=head2 new

  $pdb = new Palm::Memo;

Create a new PDB, initialized with the various Palm::Memo fields
and an empty record list.

Use this method if you're creating a Memo PDB from scratch.

=cut
#'
sub new
{
	my $classname	= shift;
	my $self	= $classname->SUPER::new(@_);
			# Create a generic PDB. No need to rebless it,
			# though.

	$self->{name} = "MobileDB";	# Default
	$self->{creator} = "Mdb1";
	$self->{type} = "Mdb1";
	$self->{attributes}{resource} = 0;
				# The PDB is not a resource database by
				# default, but it's worth emphasizing,
				# since MemoDB is explicitly not a PRC.

	# Initialize the AppInfo block
	$self->{appinfo} = {
		sortOrder	=> undef,	# XXX - ?
	};

	# Add the standard AppInfo block stuff
	&Palm::StdAppInfo::seed_StdAppInfo($self->{appinfo});

	# Give the PDB a blank sort block
	$self->{sort} = undef;

	# Give the PDB an empty list of records
	$self->{records} = [];

	return $self;
}

=head2 new_Record

  $record = $pdb->new_Record;

Creates a new Memo record, with blank values for all of the fields.

C<new_Record> does B<not> add the new record to C<$pdb>. For that,
you want C<$pdb-E<gt>append_Record>.

=cut

sub new_Record
{
	my $classname = shift;
	my $retval = $classname->SUPER::new_Record(@_);

	$retval->{data} = "";

	return $retval;
}

# ParseAppInfoBlock
# Parse the AppInfo block for Memo databases.
sub ParseAppInfoBlock
{
	my $self = shift;
	my $data = shift;
	my $sortOrder;
	my $i;
	my $appinfo = {};
	my $std_len;

	# Get the standard parts of the AppInfo block
	$std_len = &Palm::StdAppInfo::parse_StdAppInfo($appinfo, $data);

	$data = $appinfo->{other};		# Look at the non-category part

	# Get the rest of the AppInfo block
	my $unpackstr =		# Argument to unpack()
		"x4" .		# Padding
		"C";		# Sort order

	($sortOrder) = unpack $unpackstr, $data;

	$appinfo->{sortOrder} = $sortOrder;

	return $appinfo;
}

sub PackAppInfoBlock
{
	my $self = shift;
	my $retval;
	my $i;

	# Pack the non-category part of the AppInfo block
	$self->{appinfo}{other} =
		pack("x4 C x1", $self->{appinfo}{sortOrder});

	# Pack the AppInfo block
	$retval = &Palm::StdAppInfo::pack_StdAppInfo($self->{appinfo});

	return $retval;
}

sub PackSortBlock
{
	# XXX
	return undef;
}

sub ParseRecord
{
	my $self = shift;
	my %record = @_;

	delete $record{offset};		# This is useless
	$record{data} =~ s/^\xff\xff\xff\x01\xff\x00//; # Trim record head
	$record{data} =~ s/\x00\xff$//;	# Trim record tail

	return \%record;
}

sub PackRecord
{
	my $self = shift;
	my $record = shift;

	return "\xff\xff\xff\x01\xff\x00".$record->{data}."\x00\xff";
}

1;
__END__

=head1 AUTHOR

Andrew Arensburger E<lt>arensb@ooblick.comE<gt>

=head1 SEE ALSO

Palm::PDB(3)

Palm::StdAppInfo(3)

=cut
#!/usr/bin/perl -w

# tpmdbconv.pl

# Tom's Perl MobileDB Converter -- converts CSV files to/from MobileDB files
# By Tom Lee <tjlee@mh5.com>
# Begun 2001/07/18

# This is based on Adam Glass's 'pmdbconv'

# No warranties expressed or implied; use this code at your own risk

# Released under the GNU Public License (GPL)

use File::Basename;
use FileHandle;
use Getopt::Std;
use lib qw(.);
use Palm::PDB;
use Palm::MobileDB;
use strict;
use Storable;
use Text::CSV;
use vars qw($opt_v $opt_i $opt_o $opt_n);

# Generic appinfo block for MobileDB

my(%app_info_block) =
  (
   other => '',
   lastUniqueID => 15,
   categories => [
		    {
		     renamed => 0,
		     id => 0,
		     name => 'Unfiled'
		    },
		    {
		     renamed => 0,
		     id => 1,
		     name => 'FieldLabels'
		    },
		    {
		     renamed => 0,
		     id => 2,
		     name => 'DataRecords'
		    },
		    {
		     renamed => 0,
		     id => 3,
		     name => 'DataRecordsFout'
		    },
		    {
		     renamed => 0,
		     id => 4,
		     name => 'Preferences'
		    },
		    {
		     renamed => 0,
		     id => 5,
		     name => 'DataType'
		    },
		    {
		     renamed => 0,
		     id => 6,
		     name => 'FieldLengths'
		    },
		    {
		     renamed => 0,
		     id => 7,
		     name => ''
		    },
		    {
		     renamed => 0,
		     id => 8,
		     name => ''
		    },
		    {
		     renamed => 0,
		     id => 9,
		     name => ''
		    },
		    {
		     renamed => 0,
		     id => 10,
		     name => ''
		    },
		    {
		     renamed => 0,
		     id => 11,
		     name => ''
		    },
		    {
		     renamed => 0,
		     id => 12,
		     name => ''
		    },
		    {
		     renamed => 0,
		     id => 13,
		     name => ''
		    },
		    {
		     renamed => 0,
		     id => 14,
		     name => ''
		    },
		    {
		     renamed => 0,
		     id => 15,
		     name => ''
		    }
		   ],
   sortOrder => 0,
  );

sub Repeat {
  my($element, $number) = @_;
  my(@array) = ();
  for(my($i) = 0; $i < $number; $i++) {
    push(@array, $element);
  }
  return(\@array);
}

sub PackData {
  my($data) = @_;

  my($i) = 0;
  return(join('', map {
    sprintf("\x00%s%s", chr($i++), $_);
  } @$data));
}

# Generic field type data record for MobileDB

my($type_data) = &PackData(&Repeat('str', 20));

# Generic field length data record for MobileDB

my($length_data) = &PackData(&Repeat('80', 20));

sub ReadCSV {
  my($fh) = @_;
  my($csv) = new Text::CSV();

  # Bins to toss data in

  my(@headers) = ();
  my(@data) = ();

  # Get the header line

  if(defined(my $line = $fh->getline())) {

    # Parse the header line

    if($csv->parse($line)) {
      @headers = $csv->fields();

      # Go through the data

      while(defined($line = $fh->getline())) {
	if($csv->parse($line)) {

	  # Set the data up as a hash; the header line gives the order

	  my(@record) = $csv->fields();
	  my(%hash) = map {
	    $_ => shift(@record);
	  } @headers;

	  # @data is an array of hashrefs

	  push(@data, \%hash);
	} else {
	  die("Unable to parse line of data\n");
	}
      }
    } else {
      die("Unable to parse first line\n");
    }
  } else {
    die("Unable to read first line\n");
  }
  return(\@headers, \@data);
}

sub WriteCSV {
  my($fh, $headers, $data) = @_;
  my($csv) = new Text::CSV();
  if($csv->combine(@$headers)) {
    printf($fh "%s\n", $csv->string());
    foreach my $href (@$data) {
      my(@record) = map {
	$href->{$_};
      } @$headers;
      if($csv->combine(@record)) {
	printf($fh "%s\n", $csv->string());
      } else {
	die("Unable to write line of data for some reason\n");
      }
    }
  } else {
    die("Unable to write header line for some reason\n");
  }
}

sub ParsePDB {
  my($pdb) = @_;
  my(@pdb_records) = @{$pdb->{records}};
  my(@records) = ();

  # Split records by categories.  1 = headers, 2 and 3 are data (3
  # means filtered out, but that's not supported by CSV)

  my(@header_records) = grep {
    $_->{category} == 1;
  } @pdb_records;
  my(@data_records) = grep {
    ($_->{category} == 2) || ($_->{category} == 3);
  } @pdb_records;

  # There should be exactly one record with category 1.

  my($header_data) = $header_records[0]->{data};
  my(@headers) = ($header_data =~ /\x00.([^\x00]+)/g);
  foreach my $data_record (@data_records) {
    my($record_data) = $data_record->{data};
    my(@data) = ($record_data =~ /\x00.([^\x00]+)/g);
    my(%hash) = map {
      $_ => shift(@data);
    } @headers;
    push(@records, \%hash);
  }
  return(\@headers, \@records);
}

sub ConstructPDB {
  my($pdb, $headers, $records) = @_;

  # Clear out any previous data.

  $pdb->{records} = [];

  # Make the headers record.

  my($pdb_record) = new_Record Palm::PDB();
  $pdb_record->{attributes}->{archive} = 1;
  $pdb_record->{category} = 1;
  $pdb_record->{data} = &PackData($headers);
  $pdb->append_Record($pdb_record);

  # Make type and length records.

  $pdb_record = new_Record Palm::PDB();
  $pdb_record->{attributes}->{archive} = 1;
  $pdb_record->{category} = 5;
  $pdb_record->{data} = $type_data;
  $pdb->append_Record($pdb_record);

  $pdb_record = new_Record Palm::PDB();
  $pdb_record->{attributes}->{archive} = 1;
  $pdb_record->{category} = 6;
  $pdb_record->{data} = $length_data;
  $pdb->append_Record($pdb_record);

  # Add the data.

  foreach my $record (@$records) {
    my($pdb_record) = new_Record Palm::PDB();
    $pdb_record->{attributes}->{archive} = 1;
    $pdb_record->{category} = 2;
    $pdb_record->{data} = &PackData([ map {
      $record->{$_};
    } @$headers ]);
    $pdb->append_Record($pdb_record);
  }
  $pdb->{name} = $opt_n || 'NamelessDB';
  $pdb->{attributes}->{backup} = 1;
  $pdb->{type} = 'Mdb1';
  $pdb->{creator} = 'Mdb1';
  $pdb->{appinfo} = &Storable::dclone(\%app_info_block);
  $pdb->{version} = 0;
  $pdb->{modnum} = 0;
}

###############################################################################
# Main program
###############################################################################

getopts('vi:o:n:');

my($i_type, $o_type);
(undef, undef, $i_type) = fileparse($opt_i, '\..*');
(undef, undef, $o_type) = fileparse($opt_o, '\..*');

if($i_type eq $o_type) {
  die("Input and output file extensions identical -- use cp\n");
}

# Read input file

my(@headers) = ();
my(@records) = ();
if($i_type eq '.pdb') {
  my($pdb) = new Palm::PDB();
  if($pdb->Load($opt_i)) {
    my($h, $r) = &ParsePDB($pdb);
    @headers = @$h;
    @records = @$r;
  } else {
    die("Unable to open $opt_i: $!\n");
  }
} else {
  my($fh) = new FileHandle;
  if($fh->open("<$opt_i")) {
    my($h, $r) = &ReadCSV($fh);
    $fh->close();
    @headers = @$h;
    @records = @$r;
  } else {
    die("Unable to open $opt_i: $!\n");
  }
}

# Write output file

if($o_type eq '.pdb') {
  my($pdb) = new Palm::PDB();
  $pdb->Load('blank.pdb');
#  use Data::Dumper;
#  warn(Dumper($pdb));
  &ConstructPDB($pdb, \@headers, \@records);
  unless($pdb->Write($opt_o)) {
    die("Unable to open $opt_o: $!\n");
  }
} else {
  my($fh) = new FileHandle;
  if($fh->open(">$opt_o")) {
    &WriteCSV($fh, \@headers, \@records);
    $fh->close();
  } else {
    die("Unable to open $opt_o: $!\n");
  }
}
Take care,
Tom Lee (flint@kiva.net)