#!/usr/bin/perl
#$Id: pdkb.pl,v 1.16 2001/07/26 09:19:32 connolly Exp $
#
# pdkb -- Palm::Datebook knowledge base
#
# usage:
#  perl pdkb.pl --serve DATEBOOK
# to start an HTTP server; the port chosen by pdkb is
# written to STDERR. It serves
#     a form /
#     a record, in RDF /DatebookDB/nnnn
#     a range of records, in RDF
#         /DatebookDB/byDate?min=YYYY-MM-DD&max=YYYY-MM-DD
#
#  perl pdkb.pl DATEBOOK MINDATE MAXDATE
# e.g.
#  perl pdkb.pl ~/.palm/backup/DatebookDB.pdb 2001-04-04 2001-04-04 >,today.rdf
# the by-date selection is crude; any repeating event
# that overlaps the interval at all is selected;
# e.g. stuff that happens every wednesday in March 2001
# is selected even when you ask about just one tuesday in March.
#
#  perl pdkb.pl --dump-addresses ADDRESSBOOK
# to dump a palm addressbook in RDF. @@schema needs to
# be specialized to each palm.
#
# TODO:
#  - establish symbolic (URI) weekday and month names.
#
# CHANGES: at end of file

use strict;
use POSIX;
use Palm::Datebook; # cf http://www.ooblick.com/software/coldsync/#p5-Palm
use Palm::Address;
use HTTP::Daemon;   # cf libwww-perl-5.47
                    # http://search.cpan.org/search?dist=libwww-perl
                    # hmm... I'd like to be able to extract dependency 
                    # info from comments like this by machine.
                    # hmm... integration of perl's POD with URIs? with RDF?
use HTTP::Status;

require 'trdf.pl';


my($pdns) = 'http://www.w3.org/2000/08/palm56/datebook#';
my($pans) = 'http://www.w3.org/2000/08/palm56/addr#';
my($pdbns) = 'http://www.w3.org/2000/08/palm56/pdb#';
my($pduns) = 'http://www.w3.org/2000/08/palm56/pdbUtil#';
my($RDFns) = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
my($RDFSns) = 'http://www.w3.org/2000/01/rdf-schema#';
my($RDFmt) = 'application/rdf+xml'; #cf http://www.w3.org/2001/03mr/rdf_mt , RFC 3023


my($Out); # NASTY HACK


if($ARGV[0] eq '--serve'){
  &serve_datebook($ARGV[1]);
}
elsif($ARGV[0] eq '--dump-addresses'){
  &dump_addresses($ARGV[1], @ARGV[2..$#ARGV]);
}
elsif($ARGV[0] eq '--as-icalendar'){
  shift @ARGV;
  &convertIcalendar($ARGV[0], $ARGV[1], $ARGV[2]);
}
elsif($ARGV[0] eq '--test'){
  &test();
}else{
  convert($ARGV[0], $ARGV[1], $ARGV[2]);
}


sub serve_datebook{
  my($datebookfn) = @_;
  
  my $d = new HTTP::Daemon;
  my $pdb = new Palm::PDB;

  $pdb->Load($datebookfn);
  print STDERR "Serving palm datebook <$datebookfn> at: <", $d->url, ">\n";

  while (my $c = $d->accept) {
    while (my $r = $c->get_request) {
      printf STDERR ("[:rxd \"YYYY-MM-DDTHH:MM:SSZ\"; :from \"%s\"; :method :%s; :res <%s>].\n",
		     inet_ntoa($c->peeraddr), $r->method, $r->url->path_query);
      
      if($r->url->path eq "/"){
	if ($r->method eq 'GET'){
	  &indexPage($c, $r);
	}else{
	  $c->send_error(RC_FORBIDDEN);
	}
      }
      elsif($r->url->path =~ m,^/DatebookDB/(\d+)$,) {
	doRecord($c, $r, $pdb, $1);
      }
      elsif($r->url->path eq "/DatebookDB/") {
	if ($r->method eq 'GET'){
	  &recordsByDate($c, $r, $pdb, '0000-00-00', '9999-99-99');
	}elsif ($r->method eq 'POST'){
	  postRecord($c, $r, $pdb);
	}else{
	  $c->send_error(RC_FORBIDDEN);
	}
      }
      elsif($r->url->path eq "/DatebookDB/byDate"){
	if ($r->method eq 'GET'){
	  my(%args);
	  %args = $r->url->query_form;
	  #print STDERR "== query form ", %args, "\n";

	  if($args{'browse'}){ # @@or Accept: text/html
	    &browseByDate($c, $r, $pdb, $args{'min'}, $args{'max'});
	  }else{
	    &recordsByDate($c, $r, $pdb, $args{'min'}, $args{'max'});
	  }
	}else{
	  $c->send_error(RC_FORBIDDEN);
	}
      }elsif($r->url->path eq "/DatebookDB/editRecord"){
	my(%args);
	%args = $r->url->query_form;
	#print STDERR "== query form ", %args, "\n";

	&editRecord($c, $r, $pdb, $args{'id'}, $r->method);
      } else {
	$c->send_error(RC_NOT_FOUND, sprintf("path: %s", asContent($r->url->path)));
      }
    }
    $c->close;
    undef($c);
  }
}


sub indexPage{
  my($c) = @_;
  my $resp = HTTP::Response->new(RC_OK);
  $resp->header(Content_Type => 'text/html');

  $resp->add_content(<<'EODOC');
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Palm Agent Home</title></head>
<body>
<form action="/DatebookDB/byDate">
<h1>PalmAgent</h1>
<div>
<h2>Datebook</h2>
<p><input type="submit" value="GET"/> records from
<input name="min" value="0000-00-00"/>
to <input name="max" value="9999-99-99"/>.
(<label><input name="browse" type="checkbox"/> browse</label>).
</p>
</div>
<address>
$Id: pdkb.pl,v 1.16 2001/07/26 09:19:32 connolly Exp $ <br class=''/>
part of <a href="http://dev.w3.org/cvsweb/2001/palmagent/">palmagent</a>
by <a href="http://www.w3.org/People/Connolly/">Dan Connolly</a>
</address>
</body>
</html>
EODOC
  $c->send_response($resp);
}


sub recordsByDate{
  my($c, $r, $pdb, $min, $max) = @_;

  tellRange($pdb, $min, $max);
    
  my($resp);
  $resp = HTTP::Response->new(RC_OK);
  $resp->header(Content_Type => $RDFmt);
  
  # hmm... etag, last-modified, if-modified-since...
  
  $resp->add_content($Out);
  $c->send_response($resp);
}

sub byDate{
  $a->{year} == $b->{year} ?
    $a->{month} == $b->{month} ?
      $a->{day} <=> $b->{day}
	: $a->{month} <=> $b->{month}
	  : $a->{year} <=> $b->{year}
}


sub browseByDate{
  my($c, $r, $pdb, $min, $max) = @_;
  my (@recs, $record, @items);

  @recs = @{$pdb->{records}};
  @recs = grep(&matchDates($_, $min, $max), @recs);
  @recs = sort byDate @recs;
  foreach $record (@recs){
    push(@items, browseRecord($record));
  }
    
  my($resp);
  $resp = HTTP::Response->new(RC_OK);
  $resp->header(Content_Type => 'text/html');
  
  # hmm... etag, last-modified, if-modified-since...
  
  $resp->add_content("<html xmlns='http://www.w3.org/1999/xhtml'><head><title>@@</title></head></body\n>");
  $resp->add_content("<ul\n>" . join('', @items) . "</ul\n>");
  $resp->add_content("<address>@@</address></body></html\n>\n");
  $c->send_response($resp);
}


sub browseRecord{
  my($record) = @_;

  my($out);

  $out = sprintf("<li>%04d-%02d-%02d: <a href='/DatebookDB/editRecord?id=%d'>%s</a></li>\n",
		 $record->{year},
		 $record->{month},
		 $record->{day},
		 $record->{id},
		 asContent($record->{description})
		);
  return $out
}

sub editRecord{
  my($c, $r, $pdb, $id, $method) = @_;
  my($record);
  my($resp);

  if($method eq 'GET'){
    if($record = $pdb->findRecordByID($id)){
      my($out);

      $resp = HTTP::Response->new(RC_OK);
      $resp->header(Content_Type => 'text/html');
      
      # hmm... etag, last-modified, if-modified-since...
      
      $out = "<html xmlns='http://www.w3.org/1999/xhtml'><head><title>@@</title></head></body\n>";
      
      $out .= "<p><a href='/'>home</a></p>";
      
      $out .= sprintf("<h1>%02d-%02d: %s</h1>",
		      $record->{month},
		      $record->{day},
		      asContent($record->{description})
		      );
      
      $out .= sprintf("<form action='editRecord' method='POST'/><input name='id' type='hidden' value='%d'><dl><dt>date</dt><dd>%04d-%02d-%02d</dd>",
		      $record->{id},
		      $record->{year},
		      $record->{month},
		      $record->{day});
      $out .= sprintf("<dt>description</dt><dd><input name='description' type='text' value='%s' width='40'/></dd>",
		      asAttr($record->{description}));
      $out .= sprintf("<dt>note</dt><dd><textarea rows='15' cols='40' name='note'>%s</textarea></dd>",
		      asContent($record->{note}));
      
      
      $out .= sprintf("</dl><input type='submit' value='Update'/> %d </form>",
		      $record->{id});
      $out .= "<address>@@</address></body></html\n>\n";
      $resp->add_content($out);
      $c->send_response($resp);
    }else{
      $c->send_error(RC_NOT_FOUND, "record $id")
    }
  }elsif($method eq 'POST'){
    my(%args, $argi);

    #Hmm... generalize to arbitrary forms<->RDF statements?
    $argi = URI->new("http://example/dummy?" . $r->content());
    %args = $argi->query_form;
    $id = $args{id};
    print STDERR "@@ updating $id per ", %args, "\n";

    #@@ Lost updates! etag/if-match/if-modified/since or generation numbers?

    if($record = $pdb->findRecordByID($id)){

      $record->{description} = $args{description};
      $record->{note} = $args{note};
      $record->{attributes}{dirty} = 1;
      
      $pdb->Write(",post.pdb"); #@@HARDCODED
      
      my $resp = HTTP::Response->new(RC_ACCEPTED, "updated $id"); #hmm...
      # $resp->header(Location => sprintf("/DatebookDB/%d", $record->{id}));
      $c->send_response($resp);

    }else{
      $c->send_error(RC_NOT_FOUND, "POSTed record $id")
    }
  }else{
    $c->send_error(RC_FORBIDDEN);
  }
}


sub doRecord{
  my($c, $r, $pdb, $id) = @_;

  if ($r->method eq 'GET'){
    my($record);
    if($record = $pdb->findRecordByID($id)){
      my($resp);
      $resp = HTTP::Response->new(RC_OK);
      $resp->header(Content_Type => $RDFmt);
      
      # hmm... etag, last-modified, if-modified-since...
      
      RDF_startDoc();
      tellRecord($record);
      RDF_endDoc();
      
      #print STDERR "== out: $Out\n\n";
      
      $resp->add_content($Out);
      $c->send_response($resp);
      
    }else{
      $c->send_error(RC_NOT_FOUND)
    }
  }else{
    $c->send_error(RC_FORBIDDEN);
  }
}


sub postRecord{
  my($c, $r, $pdb) = @_;

  if($r->content_type eq $RDFmt){
    #print STDERR "POST request; content: ", $r->content, "\n";

    my($kbr, $kbl);
    ($kbr, $kbl) = parseKB($r->content);

    #DEBUG dumpN3();

    addRecords($pdb, $kbr, $kbl);

    my $resp = HTTP::Response->new(RC_ACCEPTED); #hmm...
    # $resp->header(Location => sprintf("/DatebookDB/%d", $record->{id}));

    $pdb->Write(",post.pdb"); #@@HARDCODED

    $c->send_response($resp);
  }else{
    $c->send_error(RC_UNSUPPORTED_MEDIA_TYPE);
  }
}



sub convert{
  my($datebookfn, $min, $max) = @_;
  
  my $pdb = new Palm::PDB;

  print STDERR "@@ convert", $datebookfn, "\n";
  

  $pdb->Load($datebookfn);
  my ($minDate, $maxDate) = ($min, $max);
  tellRange($pdb, $minDate, $maxDate);
  print $Out;
}


my(%Prefixes);
my($lastSubj);

sub test{
  RDF_startDoc();
  RDF_makeStatement('http://www.w3.org/', 'http://purl.org/dc/elements/1.1/title', undef, "World Wide Web Consortium (W3C)", 'dc');
  RDF_endDoc();
}


# Gag... I'll figure out perl5 OOP/package stuff
# and turn this into RDF::startDoc or whatever eventually...
sub RDF_startDoc{
  undef(%Prefixes);
  $Out = '';
  $lastSubj = ' '; # URIs don't contain spaces
}

sub bind{
  my($pfx, $ns) = @_;

  if($Prefixes{'rdf'}){
    die "cannot bind prefixes after making a statement";
  }

  $Prefixes{$ns} = $pfx;
}


sub RDF_endDoc{
  if($lastSubj ne ' '){
    $Out .= "</rdf:Description\n>";
  }

  $Out .= "</rdf:RDF\n>";
}


sub RDF_makeStatement{
  my($subj, $pred, $objRef, $objLit) = @_;

  #print STDERR "== makeStatement ($subj, $pred, $objRef, $objLit)\n";

  if(!$Prefixes{$RDFns}){
    my($ns);
    $Out .= sprintf("<rdf:RDF");
    $Prefixes{$RDFns} = 'rdf';

    foreach $ns (keys %Prefixes){
      $Out .= sprintf("\n xmlns:%s='%s'",
		     $Prefixes{$ns}, asAttr($ns));
    }
    $Out .= " \n>";
  }


  if($lastSubj ne $subj){
    if($lastSubj ne ' '){
      $Out .= "</rdf:Description\n>";
    }
    $Out .= sprintf("<rdf:Description rdf:about='%s'\n>", asAttr($subj));
  }
  $lastSubj = $subj;

  my($ln, $ns);
  $ns = $pred;
  $ln = $& if ($ns =~ s/[a-zA-Z0-9\._-]+$//);

  my($pfx);

  if($pfx = $Prefixes{$ns}){
    $Out .= sprintf("<%s:%s", $pfx, $ln);
  }else{
    $Out .= sprintf("  <%s xmlns='%s'", asAttr($ln), $ns);
  }

  if($objRef){
    $Out .= sprintf(" rdf:resource='%s'\n/>", asAttr($objRef));
  }
  else{
    if($pfx){
      $Out .= sprintf("\n    >%s</%s:%s\n  >", asContent($objLit), $pfx, $ln);
    }else{
      $Out .= sprintf("\n    >%s</%s\n  >", asContent($objLit), $ln);
    }
  }
}


#########

sub tellRange{
  my($pdb, $isomin, $isomax) = @_;
  my $record;
  
  &RDF_startDoc();
  &bind('pd', $pdns);
  &bind('pdb', $pdbns);
  &bind('pu', $pduns);
  &RDF_makeStatement("", $pduns . 'bookMin', undef, $isomin);
  &RDF_makeStatement("", $pduns . 'bookMax', undef, $isomax);

  # hmm... what should the subject of these be?
  &RDF_makeStatement("", $pdns . 'type', undef, $pdb->{type});
  &RDF_makeStatement("", $pdns . 'creator', undef, $pdb->{creator});
  #@@ serial number of the gizmo?

  # these don't work as I expected they would; in particular,
  # modnum doesn't change after I modify the datebook and resync.
  &RDF_makeStatement("", $pdns . 'modnum', undef, $pdb->{modnum});
  &RDF_makeStatement("", $pdns . 'ctime', undef, e2i($pdb->{ctime}));
  &RDF_makeStatement("", $pdns . 'mtime', undef, e2i($pdb->{mtime}));
  &RDF_makeStatement("", $pdns . 'baktime', undef, e2i($pdb->{baktime}));
  
  foreach $record (@{$pdb->{records}}){
    if(&matchDates($record, $isomin, $isomax)){
      tellRecord($record);
    }
  }
  
  RDF_endDoc();
}


sub matchDates{
  my($record, $isomin, $isomax) = @_;

  my($isodate);
  $isodate = sprintf("%04d-%02d-%02d",
		     $record->{year},
		     $record->{month},
		     $record->{day});
  
  if ($isodate gt $isomax) {
    #printf STDERR "record %s starts after window: %s > %s\n",
    # $record->{id}, $isodate, $isomax;
    return 0;
  } elsif ($record->{repeat}) {
    if ($record->{repeat}{end_year}) {
      my($isoend);
      $isoend = sprintf("%04d-%02d-%02d",
			$record->{repeat}{end_year},
			$record->{repeat}{end_month},
			$record->{repeat}{end_day});
      if ($isoend lt $isomin) {
	#printf STDERR "repeating record %s ends before window: %s < %s\n",
	# $record->{id}, $isoend, $isomin;
	
	return 0;
      }
    }
    #else: no end to the repeat... it's in the window
  }
  
  # doesn't repeat
  elsif ($isodate lt $isomin) {
    #printf STDERR "record %s before window: %s < %s\n",
    # $record->{id}, $isodate, $isomin;
    return 0;
  }
  
  return 1;

}


sub addRecords{
  my($pdb, $kbr, $kbl) = @_;

  #print STDERR "addRecords keys: ", join(' ', keys %{$kbl}), "\n";

  foreach my $ri (grep($kbl->{$_}->{$pdns . 'date'}, keys %{$kbl})){
    my $record = new Palm::Datebook;
    fillRecord($record, $kbr, $kbl, $ri);

    $pdb->append_Record($record);
  }
}

sub fillRecord{
  my($record, $kbr, $kbl, $ri) = @_;

  print STDERR "@@ fillRecord: subject is $ri\n";

  #@@ category
  $record->{private} = $kbl->{$ri}->{$pdbns . 'private'}->[0] ? 1 : 0;

  if($kbl->{$ri}->{$pdns . 'date'}->[0] =~ /(\d+)-(\d+)-(\d+)/){
    print STDERR "@@ fillRecord: date $1 $2 $3\n";
    ($record->{year}, $record->{month}, $record->{day}) = ($1, $2, $3)
  }

  my $t = $kbl->{$ri}->{$pdns . 'start_time'}->[0];
  if($t =~ /(\d+):(\d+)/){
    ($record->{start_hour}, $record->{start_minute}) = ($1, $2);

    $t = $kbl->{$ri}->{$pdns . 'end_time'}->[0];
    $t =~ /(\d+):(\d+)/;
    ($record->{end_hour}, $record->{end_minute}) = ($1, $2);
  }else{
    $record->{start_time} = 255;
  }

  my $a = $kbl->{$ri}->{$pdns . 'alarmAdvance'}->[0];
  if($a){
    $record->{alarm}{advance} = $a;
    my $u = $kbr->{$ri}->{$pdns . 'alarmUnit'}->[0];
    #print STDERR "grokked alarm unit: ", $u, "\n";

    if($u eq $pdns . 'minutes'){
      $record->{alarm}{unit} = 0;
    }
    elsif($u eq $pdns . 'hours'){
      $record->{alarm}{unit} = 1;
    }
    elsif($u eq $pdns . 'days'){
      $record->{alarm}{unit} = 2;
    }
  }
  
  #@@ repeat

  # @@hmm... how to test this exceptionList parsing stuff...
  my($exceptionList);
  if($exceptionList =
     $kbl->{$ri}->{$pdns . 'exceptions'}->[0]){
    my($exc, @excs);
    foreach $exc (split($exceptionList)){
      if($exc =~ /(\d+)-(\d+)-(\d+)/){
	print STDERR "@@ fillRecord exception: date $1 $2 $3\n";
	push(@excs, [$3, $2, $1]);
      }else{
	warn "goofy exception: $exc";
      }
    }
    $record->{exceptions} = \@excs;
  }


  $record->{description} = $kbl->{$ri}->{$pdns . 'description'}->[0];
  $record->{note} = $kbl->{$ri}->{$pdns . 'note'}->[0];

  $record->{"attributes"}{"dirty"} = 1;
}


sub tellPDBRecord{
  my($record) = @_;

  my($thisRec);
  if($record->{id}){
    $thisRec = sprintf("#%d", $record->{id}); #separate filish resource per record. hm... rather #nnn?
    RDF_makeStatement($thisRec, $pdbns . 'id', '', $record->{id});
  }
  else{
    # record not sync'd yet
    $thisRec=sprintf("notSync_%d", $record);
  }
    
  
  RDF_makeStatement($thisRec, $pdbns . 'category', '', $record->{category})
    if $record->{category};
  
  RDF_makeStatement($thisRec, $pdbns . 'private', '',
		$record->{attributes}{private} ? 1 : 0); #hmm... do booleans as class membership? but we need \notin

  #hmm... deleted/expunged is mechanics, not real info, right?
  RDF_makeStatement($thisRec, $pdbns . 'deleted', '',
  	$record->{attributes}{deleted} ? 1 : 0);
  
  RDF_makeStatement($thisRec, $pdbns . 'expunged', '',
  		$record->{attributes}{expunged} ? 1 : 0);
  
  return $thisRec;
  
}

sub tellRecord{
  my($record) = @_;

  my($thisRec);
  $thisRec = tellPDBRecord($record);
  
  RDF_makeStatement($thisRec, $pdns . 'date', '',
		sprintf('%04d-%02d-%02d',
			$record->{year},
			$record->{month},
			$record->{day}));

  if($record->{start_hour} != 255){ #@@ test others too?
    RDF_makeStatement($thisRec, $pdns . 'start_time', '',
		  sprintf('%02d:%02d',
			  $record->{start_hour},
			  $record->{start_minute}));
    RDF_makeStatement($thisRec, $pdns . 'end_time', '',
		  sprintf('%02d:%02d',
			  $record->{end_hour},
			  $record->{end_minute}))
  }else{
    RDF_makeStatement($thisRec, $RDFns . 'type', $pdns . 'Timeless');
  }

  ## who knows what this is  
  # makeStatement($thisRec, $pdns . 'when_changed', '',
  #		$record->{when_changed} ? 1 : 0);
  
  if ($record->{alarm}) {
    RDF_makeStatement($thisRec, $pdns . 'alarmAdvance', '',
		  $record->{alarm}{advance});
    my($unit);
    $unit = ('minutes', 'hours', 'days')[$record->{alarm}{unit}];
    RDF_makeStatement($thisRec, $pdns . 'alarmUnit',
		  $pdns . $unit);
  } else {
    RDF_makeStatement($thisRec, $RDFns . 'type', $pdns . 'NoAlarm');
  }
  
  if ($record->{repeat}) {
    my ($gi);
    
    if ($record->{repeat}{type} == 1) {
      RDF_makeStatement($thisRec, $pdns . 'dailyInterval', '',
		    $record->{repeat}{frequency});
    } elsif ($record->{repeat}{type} == 2) {
      RDF_makeStatement($thisRec, $pdns . 'weeklyInterval', '',
		    $record->{repeat}{frequency});
      RDF_makeStatement($thisRec, $pdns . 'repeat_days', '',
		    "@{$record->{repeat}{repeat_days}}"); #@@ day enum
      RDF_makeStatement($thisRec, $pdns . 'start_of_week', '',
		    $record->{repeat}{start_of_week}); #@@ symbol
    } elsif ($record->{repeat}{type} == 3) {
      RDF_makeStatement($thisRec, $pdns . 'weekOfMonth', '',
		    $record->{repeat}{weeknum});
      RDF_makeStatement($thisRec, $pdns . 'dayOfWeek', '',
		    $record->{repeat}{daynum}); #@@ symbol
    } elsif ($record->{repeat}{type} == 4) {
      RDF_makeStatement($thisRec, $pdns . 'monthlyInterval', '',
		    $record->{repeat}{frequency});
    } elsif ($record->{repeat}{type} == 5) {
      RDF_makeStatement($thisRec, $pdns . 'yearlyInterval', '',
		    $record->{repeat}{frequency});
    } else {
      die "unknown repeat type", $record->{repeat}{type};
    }
    
    if ($record->{repeat}{end_year}) {
      RDF_makeStatement($thisRec, $pdns . 'end_date', '',
		    sprintf('%04d-%02d-%02d',
			    $record->{repeat}{end_year},
			    $record->{repeat}{end_month},
			    $record->{repeat}{end_day}));
    } else {
      RDF_makeStatement($thisRec, $RDFns . 'type', $pdns . 'Indefinite');
    }
    
    
    if ($record->{exceptions}) {
      my($exc, $list);

      foreach $exc (@{$record->{exceptions}}){
	my($day, $month, $year);
	($day, $month, $year) = @{$exc};
	$list .= " " if $list;
	$list .= sprintf("%04d-%02d-%02d", $year, $month, $day);
      }
      RDF_makeStatement($thisRec, $pdns . 'exceptions', '', $list);
    }
  } else {
    RDF_makeStatement($thisRec, $RDFns . 'type', $pdns . 'OneTime');
  }
  
  RDF_makeStatement($thisRec, $pdns . 'description', '',
		$record->{description});
  RDF_makeStatement($thisRec, $pdns . 'note', '',
		$record->{note});
}


sub asContent{
    my($c) = @_;

    $c =~ s,&,&amp;,g;
    $c =~ s,<,&lt;,g;
    $c =~ s,>,&gt;,g;
    $c =~ s/[\200-\377]/'&#'.ord($&).';'/ge;

    #@@hhm... protect newlines too?

    return $c
}

sub asAttr{
  my($c) = @_;
  
  $c =~ s,&,&amp;,g;
  $c =~ s,<,&lt;,g;
  $c =~ s,>,&gt;,g;
  $c =~ s,\",&quot;,g;
  $c =~ s,\',&apos;,g;
  $c =~ s/[\200-\377]/'&#'.ord($&).';'/ge;
  
  #@@hhm... protect newlines too?
  
  return $c
}

sub e2i{
  my($e) = @_;

  return strftime("%Y-%m-%dT%H:%M:%SZ", # pilfered from 2000/04/maillog2rdf/imaplib2.py revision 1.1 date: 2000/08/23 16:29:16
			 gmtime($e));
}

sub dump_addresses{
  my($fn, @catPicks) = @_;

  my $pdb = new Palm::PDB;

  print STDERR "PROGRESS: dump addresses", $fn, "\n";
  print STDERR "PROGRESS: selected categories: ", join(",", @catPicks), "\n"
    if @catPicks;
  
  $pdb->Load($fn);

  &RDF_startDoc();
  &bind('pa', $pans);
  &bind('pdb', $pdbns);
  &bind('u', $RDFSns);

  my($fld, $props, @categories);

  $props = [];
  @categories   = @{$pdb->{appinfo}{categories}};

  for($fld = 1; $fld <= 8; $fld++){
    push(@$props, $pans . $pdb->{appinfo}{fieldLabels}{"phone" . $fld});
  }
    
  my($record);
  
  foreach $record (@{$pdb->{records}}){
    #print STDERR "cat match?" , $categories[$record->{category}]->{name}, "\n";
    if($#catPicks < 0
       || grep($categories[$record->{category}]->{name} =~ /^$_/i, @catPicks)){
      tellAddrRecord($record, $props);
    }
  }

  RDF_endDoc();

  print $Out;
}

sub tellAddrRecord{
  my($record, $props) = @_;

  my($thisRec, $fld);
  $thisRec = tellPDBRecord($record);
  
  #grumble... perl5 syntax is a bear
  foreach $fld (keys %{$record->{fields}}){
    my($p);

    # Hmm... this mapping isn't invertible.
    # Should we export the raw details about how
    # the record is laid out?
    if($fld =~ /^phone/){
      my($n);
      $n = $record->{phoneLabel}{$fld};
      $p = $props->[$n];
    }
    else{
      $p = $pans . $fld;
    }

    # Hmm... @@TODO: recognize email addresses and phone numbers and
    # convert them to URIs?
    RDF_makeStatement($thisRec, $p, '', $record->{fields}{$fld});
  }
  
  RDF_makeStatement($thisRec, $pans . 'display', $props->[$record->{phoneLabel}{display}]);
}


################
# ICalendar hack
#
# based on korganizer data, data from my nokia cellphone
# plus a scan of RFC 2445
# and
#  http://ilrt.org/discovery/2001/06/schemas/ical-full/hybrid.rdf
sub convertIcalendar{
  my($fn, $minDate, $maxDate) = @_;
  
  my $pdb = new Palm::PDB;

  print STDERR "@@ convert", $fn, " to icalendar...\n";

  print "BEGIN:VCALENDAR\nVERSION:1.0\n";
  print "TZ:-0500\n"; #@@HARDCODED

  $pdb->Load($fn);

  my($record);

  foreach $record (@{$pdb->{records}}){
    if(&matchDates($record, $minDate, $maxDate)){
      print "BEGIN:VEVENT\n";

      printf "UID:palm-%d\n", $record->{id}; # sorta making this up...

      printf "X-PILOTID:%d\n", $record->{id}; # found this in korganizer output

      my($y, $m, $d, $hh, $mm, $ss);
      ($y, $m, $d) = ($record->{year}, $record->{month}, $record->{day});
      ($hh, $mm, $ss)= (0,0,0);

      $hh = $record->{start_hour} unless $record->{start_hour} == 255;
      $mm = $record->{start_minute} unless $record->{start_hour} == 255;

      printf "DTSTART:%04d%02d%02dT%02d%02d%02d\n",
	$y, $m, $d, $hh, $mm, $ss;

      ($y, $m, $d) = ($record->{repeat}{end_year},
	$record->{repeat}{end_month}, $record->{repeat}{end_day})
	  if $record->{repeat}{end_year};

      if(0){ #@@not working?
	($hh, $mm, $ss)= (0,0,0);
	$hh = $record->{end_hour} unless $record->{start_hour} == 255;
	$mm = $record->{end_minute} unless $record->{start_hour} == 255;
	
	printf "DTEND:%04d%02d%02dT%02d%02d%02d\n",
	  $y, $m, $d, $hh, $mm, $ss;
      }

      printf "SUMMARY:%s\n", $record->{description};

      my($desc);
      if($desc = $record->{note}){
	$desc =~ s/\r//g;
	$desc =~ s/\n/ /g; #@@KLUDGE! quoting
	printf "DESCRIPTION:%s\n", $desc;
      }

      print "END:VEVENT\n\n";
      
    }
  }

  print "END:VCALENDAR\n";
  
}



# $Log: pdkb.pl,v $
# Revision 1.16  2001/07/26 09:19:32  connolly
# icalendar export hack
#
# Revision 1.15  2001/07/10 21:54:51  connolly
# initial stab at addressbook dump to RDF
#
# Revision 1.14  2001/07/10 21:06:16  connolly
# working on address book dump; changing machines...
#
# Revision 1.13  2001/07/09 22:43:20  connolly
# fixed date comparison bug
#
# Revision 1.12  2001/06/14 01:27:08  connolly
# * hacked HTML forms-based datebook editor: can edit description, note
# * moved recordId->web links from HTML to RDF
# * pdkb.pl adds pdb:id property to events
#
# (also working on merging with mem-news/groups.rdf)
#
# Revision 1.11  2001/05/21 23:14:44  connolly
# datebook2html is now template-driven; bold if alarm; a few other tweaks
#
# Revision 1.10  2001/05/08 15:06:57  connolly
# POST now keys off date property rather than pdu:inserts
#
# Revision 1.9  2001/04/26 06:20:40  connolly
# datebook2html.xsl: added alarm into to the formatted output
# fixed a bug in alarm processing in pdkb.pl
# updated dbrec-test w.r.t. pu:inserts vs. rdf:value
#
# Revision 1.8  2001/04/24 13:47:07  connolly
# worked on WWW10 schedule, merged from many sources.
# pdkb.pl -- takes several events in one POST (pdu:inserts).
# 	updated w.r.t. schema mods, e.g. pd:private -> pdb:private
# 	added a hack to deal with record IDs of unsync'd records
#
# Makefile -- added a rule to build an HTML version of
# 	the datebook with the POSTed stuff, before syncing.
#
# postrdf.py -- quick script to do an HTTP post with the right content type
#
# README.cvs -- tried to document issues that came up: alarms, ...
#
# datebook2html.xsl -- added paperTrail template to format dc:sources
# 	factored out eventSchedule template
# 	updated w.r.t. schema mods, e.g. pd:private -> pdb:private
# 	fixed a fencepost bug in weekOfMonth processing
#
# Revision 1.7  2001/04/19 04:30:34  connolly
# removed dependency on cwm
#
# Revision 1.6  2001/04/15 04:02:57  connolly
# Added code to handle POSTed RDF datebook entries
# (fillRecord supports date, description, note only, pretty much.)
#
# integrating trdf.pl involved a nasty hack
# of changing makeStatemnt to RDF_makeStatement.
# I'll eventually code it in perl5 OOP style,
# but not yet...
#
# Revision 1.4  2001/04/13 19:36:33  connolly
# for em and #rdfig folk, lightly tested
#
# Revision 1.3  2001/04/12 05:36:51  connolly
# changed my mind on Issue rdf-ns-prefix-confusion
#
# Revision 1.2  2001/04/10 14:04:15  connolly
# trying to report detailed versioning info; only got as far as pointing to palmagent source
#
# Revision 1.1  2001/04/05 05:33:02  connolly
# dumps RDF; ugly, but complete save repeat exceptions
#
