File Coverage

blib/lib/RDF/iCalendar/Exporter.pm
Criterion Covered Total %
statement 6 6 100.0
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 8 8 100.0


line stmt bran cond sub pod time code
1             package RDF::iCalendar::Exporter;
2              
3 2     2   23506 use 5.008;
  2         7  
  2         136  
4 2     2   11 use base qw[RDF::vCard::Exporter];
  2         3  
  2         2736  
5             use strict;
6             use warnings;
7             no warnings qw(uninitialized);
8              
9             use DateTime;
10             use MIME::Base64 qw[];
11             use RDF::iCalendar::Entity;
12             use RDF::iCalendar::Line;
13             use RDF::TrineX::Functions
14             -shortcuts,
15             statement => { -as => 'rdf_statement' },
16             iri => { -as => 'rdf_resource' };
17             use Scalar::Util qw[blessed];
18             use URI;
19              
20             require RDF::vCard;
21              
22             # kinda constants
23             sub I { return 'http://www.w3.org/2002/12/cal/icaltzd#' . shift; }
24             sub IX { return 'http://buzzword.org.uk/rdf/icaltzdx#' . shift; }
25             sub RDF { return 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' . shift; }
26             sub V { return 'http://www.w3.org/2006/vcard/ns#' . shift; }
27             sub VX { return 'http://buzzword.org.uk/rdf/vcardx#' . shift; }
28             sub XSD { return 'http://www.w3.org/2001/XMLSchema#' . shift; }
29              
30             sub flatten_node
31             {
32             my $node = shift;
33             return $node->value if $node->is_resource || $node->is_literal;
34             return $node->as_ntriples;
35             }
36              
37             use namespace::clean;
38              
39             our $VERSION = '0.005';
40             our $PRODID = sprintf("+//IDN cpan.org//NONSGML %s v %s//EN", __PACKAGE__, $VERSION);
41              
42             our %cal_dispatch = (
43             );
44              
45             our %dispatch = (
46             IX('contact') => \&_prop_export_contact,
47             I('contact') => \&_prop_export_contact,
48             I('geo') => \&_prop_export_geo,
49             IX('organizer') => \&_prop_export_Person,
50             I('organizer') => \&_prop_export_Person,
51             IX('attendee') => \&_prop_export_Person,
52             I('attendee') => \&_prop_export_Person,
53             I('attach') => \&RDF::vCard::Exporter::_prop_export_binary,
54             I('dtstart') => \&_prop_export_DateTime,
55             I('dtend') => \&_prop_export_DateTime,
56             I('due') => \&_prop_export_DateTime,
57             I('completed') => \&_prop_export_DateTime,
58             I('created') => \&_prop_export_DateTime,
59             I('dtstamp') => \&_prop_export_DateTime,
60             I('last-modified') => \&_prop_export_DateTime,
61             IX('location') => \&_prop_export_location,
62             I('location') => \&_prop_export_location,
63             I('rrule') => \&_prop_export_Recur,
64             I('exrule') => \&_prop_export_Recur,
65             I('valarm') => \&_prop_export_valarm,
66             I('freebusy') => \&_prop_export_freebusy,
67             # RELATED-TO
68             );
69              
70             our %list_dispatch = (
71             I('exdate') => ['exdate', \&_value_export_DateTime],
72             I('rdate') => ['rdate', \&_value_export_DateTime],
73             I('resources') => ['resources', \&_value_export_category],
74             I('categories') => ['categories', \&_value_export_category],
75             I('category') => ['categories', \&_value_export_category],
76             IX('category') => ['categories', \&_value_export_category],
77             );
78            
79             sub _rebless
80             {
81             my ($self, $thing) = @_;
82             if ($thing->isa('RDF::vCard::Line'))
83             {
84             return bless $thing, 'RDF::iCalendar::Line';
85             }
86             if ($thing->isa('RDF::vCard::Entity'))
87             {
88             return bless $thing, 'RDF::iCalendar::Entity';
89             }
90             }
91              
92             sub _debug
93             {
94             # my ($self, @debug) = @_;
95             # printf(@debug);
96             # print "\n";
97             }
98              
99             sub export_cards # need to really use superclass for these
100             {
101             my ($self, $model, %options) = @_;
102             return RDF::vCard::Exporter->new->export_cards($model, %options);
103             }
104              
105             sub export_card # need to really use superclass for these
106             {
107             my ($self, $model, $subject, %options) = @_;
108             return RDF::vCard::Exporter->new->export_card($model, $subject, %options);
109             }
110              
111             sub export_calendars
112             {
113             my ($self, $model, %options) = @_;
114             $model = rdf_parse($model)
115             unless blessed($model) && $model->isa('RDF::Trine::Model');
116            
117             my @subjects = $model->subjects(rdf_resource(RDF('type')), rdf_resource(I('Vcalendar')));
118             push @subjects, $model->subjects(rdf_resource(I('component')), undef);
119             my %subjects = map { flatten_node($_) => $_ } @subjects;
120            
121             my @cals;
122             foreach my $s (values %subjects)
123             {
124             push @cals, $self->export_calendar($model, $s, %options);
125             }
126            
127             if ($options{sort})
128             {
129             return sort { $a->entity_order cmp $b->entity_order } @cals;
130             }
131            
132             return @cals;
133             }
134              
135             sub export_calendar
136             {
137             my ($self, $model, $subject, %options) = @_;
138             $model = RDF::TrineShortcuts::rdf_parse($model)
139             unless blessed($model) && $model->isa('RDF::Trine::Model');
140            
141             my $ical = RDF::iCalendar::Entity->new( profile=>'VCALENDAR' );
142            
143             my %categories;
144             my $triples = $model->get_statements($subject, undef, undef);
145             while (my $triple = $triples->next)
146             {
147             # next
148             # unless (substr($triple->predicate->uri, 0, length(&I)) eq &I or
149             # substr($triple->predicate->uri, 0, length(&IX)) eq &IX);
150              
151             if ($triple->predicate->uri eq I('component'))
152             {
153             $ical->add_component($self->export_component($model, $triple->object));
154             }
155             elsif (defined $cal_dispatch{$triple->predicate->uri}
156             and ref($cal_dispatch{$triple->predicate->uri}) eq 'CODE')
157             {
158             my $code = $cal_dispatch{$triple->predicate->uri};
159             my $r = $code->($self, $model, $triple);
160             if (blessed($r) and $r->isa('RDF::iCalendar::Line'))
161             { $ical->add($r); }
162             elsif (blessed($r) and $r->isa('RDF::iCalendar::Entity'))
163             { $ical->add_component($r); }
164             }
165             elsif ((substr($triple->predicate->uri, 0, length(&I)) eq &I
166             or substr($triple->predicate->uri, 0, length(&IX)) eq &IX))
167             {
168             $ical->add($self->_prop_export_simple($model, $triple))
169             unless $triple->object->is_blank;
170             }
171             }
172            
173             $ical->add(
174             RDF::iCalendar::Line->new(
175             property => 'version',
176             value => '2.0',
177             )
178             );
179            
180             $ical->add(
181             RDF::iCalendar::Line->new(
182             property => 'prodid',
183             value => (defined $options{prodid} ? $options{prodid} : $PRODID),
184             )
185             ) unless exists $options{prodid} && !defined $options{prodid};
186              
187             $ical->add(
188             RDF::iCalendar::Line->new(
189             property => 'source',
190             value => $options{source},
191             type_parameters => {value=>'URI'},
192             )
193             ) if defined $options{source};
194              
195             return $ical;
196             }
197              
198             sub export_component
199             {
200             my ($self, $model, $subject, %options) = @_;
201             $model = RDF::TrineShortcuts::rdf_parse($model)
202             unless blessed($model) && $model->isa('RDF::Trine::Model');
203            
204             my $profile = 'VEVENT';
205             $profile = 'VTIMEZONE'
206             if $model->count_statements($subject, rdf_resource(RDF('type')), rdf_resource(I('Vtimezone')));
207             $profile = 'VFREEBUSY'
208             if $model->count_statements($subject, rdf_resource(RDF('type')), rdf_resource(I('Vfreebusy')));
209             $profile = 'VALARM'
210             if $model->count_statements($subject, rdf_resource(RDF('type')), rdf_resource(I('Valarm')));
211             $profile = 'VJOURNAL'
212             if $model->count_statements($subject, rdf_resource(RDF('type')), rdf_resource(I('Vjournal')));
213             $profile = 'VTODO'
214             if $model->count_statements($subject, rdf_resource(RDF('type')), rdf_resource(I('Vtodo')));
215             $profile = 'VEVENT'
216             if $model->count_statements($subject, rdf_resource(RDF('type')), rdf_resource(I('Vevent')));
217            
218             my $c = RDF::iCalendar::Entity->new( profile=>$profile );
219            
220             $self->_debug("COMPONENT: %s", flatten_node($subject));
221            
222             my $lists = {};
223            
224             my $triples = $model->get_statements($subject, undef, undef);
225             while (my $triple = $triples->next)
226             {
227             $self->_debug(" %s %s", $triple->predicate->sse, $triple->object->sse);
228            
229             if (defined $dispatch{$triple->predicate->uri}
230             and ref($dispatch{$triple->predicate->uri}) eq 'CODE')
231             {
232             $self->_debug(" -> dispatch");
233             my $code = $dispatch{$triple->predicate->uri};
234             my $r = $code->($self, $model, $triple);
235             if (blessed($r) and $r->isa('RDF::iCalendar::Line'))
236             { $c->add($r); }
237             elsif (blessed($r) and $r->isa('RDF::iCalendar::Entity'))
238             { $c->add_component($r); }
239             }
240             elsif (defined $list_dispatch{$triple->predicate->uri}
241             and ref($list_dispatch{$triple->predicate->uri}) eq 'ARRAY')
242             {
243             $self->_debug(" -> list_dispatch");
244             my ($listname, $code) = @{ $list_dispatch{$triple->predicate->uri} };
245             push @{ $lists->{$listname} }, $code->($self, $model, $triple);
246             }
247             elsif ((substr($triple->predicate->uri, 0, length(&I)) eq &I
248             or substr($triple->predicate->uri, 0, length(&IX)) eq &IX))
249             {
250             $self->_debug(" -> default");
251             $c->add($self->_prop_export_simple($model, $triple))
252             unless $triple->object->is_blank;
253             }
254             else
255             {
256             $self->_debug(" -> NO ACTION");
257             }
258             }
259            
260             foreach my $listname (keys %$lists)
261             {
262             $c->add(RDF::iCalendar::Line->new(
263             property => $listname,
264             value => [[ sort keys %{{ map { $_ => 1 } @{$lists->{$listname}} }} ]],
265             ));
266             }
267            
268             return $c;
269             }
270              
271             sub _prop_export_valarm
272             {
273             my ($self, $model, $triple) = @_;
274            
275             unless ($triple->object->is_literal)
276             {
277             return $self->export_component($model, $triple->object);
278             }
279             }
280              
281             sub _prop_export_simple
282             {
283             my ($self, $model, $triple) = @_;
284             my $rv = $self->SUPER::_prop_export_simple($model, $triple);
285             return $self->_rebless($rv);
286             }
287              
288             # iCalendar forces different datetime/date formats than
289             # the generalised text/directory ones used by vCard...
290             sub _prop_export_DateTime
291             {
292             my ($self, $model, $triple) = @_;
293              
294             my $prop = 'x-data';
295             if ($triple->predicate->uri =~ m/([^\#\/]+)$/)
296             {
297             $prop = $1;
298             }
299            
300             my $val = undef;
301             my $params = undef;
302              
303             my ($dt, $has_time) = $self->_node2dt($triple->object, $model);
304             my $tz = $dt->time_zone;
305              
306             if ($dt and $has_time)
307             {
308             $params = { value=>'DATE-TIME' };
309            
310             unless ($tz->is_floating ||
311             $tz->is_utc ||
312             $tz->is_olson )
313             {
314             $dt = $dt->clone->set_time_zone('UTC');
315             $tz = $dt->time_zone;
316             }
317            
318             $val = sprintf('%04d%02d%02dT%02d%02d%02d',
319             $dt->year, $dt->month, $dt->day, $dt->hour, $dt->minute, $dt->second);
320            
321             if ($tz->is_utc)
322             {
323             $val .= "Z";
324             }
325             elsif (!$tz->is_floating)
326             {
327             $params->{tzid} = $tz->name;
328             }
329             }
330             elsif ($dt)
331             {
332             $params = { value=>'DATE' };
333            
334             unless ($tz->is_floating ||
335             $tz->is_utc ||
336             $tz->is_olson )
337             {
338             $dt = $dt->clone->set_time_zone('UTC');
339             $tz = $dt->time_zone;
340             }
341            
342             $val = sprintf('%04d%02d%02d',
343             $dt->year, $dt->month, $dt->day);
344             }
345              
346             return RDF::iCalendar::Line->new(
347             property => $prop,
348             value => $val,
349             type_parameters => $params,
350             );
351             }
352              
353             sub _node2dt
354             {
355             my ($self, $node, $model) = @_;
356            
357             # Shouldn't happen!
358             return DateTime->now unless $node->is_literal;
359            
360             my ($date, $time) = split /T/i, $node->literal_value;
361             my $has_time = (defined $time and length $time);
362              
363             my $dt;
364             if ($date =~ m'^([0-9]{4})\-?([0-9]{2})\-?([0-9]{2})$')
365             {
366             $dt = DateTime->new(year => $1, month => $2, day => $3);
367             }
368             elsif ($date =~ m'^([0-9]{1,4})\-([0-9]{1,2})\-([0-9]{1,2})$')
369             {
370             $dt = DateTime->new(year => $1, month => $2, day => $3);
371             }
372             else
373             {
374             $dt = DateTime->now;
375             }
376            
377             my $zone;
378             if ($time =~ /^(.+)(Z|[\+\-]00\:?00])$/i)
379             {
380             $time = $1;
381             $zone = DateTime::TimeZone->new(name => 'UTC');
382             }
383             elsif ($time =~ /^(.+)([\+\-][0-9][0-9]\:?[0-9][0-9])$/i)
384             {
385             $time = $1;
386             $zone = DateTime::TimeZone->new(name => $2);
387             }
388             elsif ($node->has_datatype
389             and $node->literal_datatype =~ m'^http://www\.w3\.org/2002/12/cal/tzd/(.+)#tz$')
390             {
391             $zone = DateTime::TimeZone->new(name => $1);
392             }
393             elsif ($node->has_datatype
394             and $node->literal_datatype !~ m'^http://www\.w3\.org/2001/XMLSchema#'
395             and defined $model)
396             {
397             # Some funny datatype; let's try our best!
398             my @locations = grep
399             { $_->is_literal }
400             $model->objects(
401             rdf_resource($node->literal_datatype),
402             rdf_resource('http://www.w3.org/2002/12/cal/prod/Ximian_NON_de8f2a9bed573980#location'),
403             rdf_resource(RDF('value')),
404             rdf_resource(RDFS('label')),
405             );
406             $zone = DateTime::TimeZone->new(name => $locations[0]->literal_value)
407             if @locations;
408             }
409              
410             $dt->set_time_zone($zone) if $zone;
411              
412             if ($time =~ m'^([0-2][0-9])\:?([0-5][0-9])\:?([0-6][0-9](\.[0-9]*)?)?$')
413             {
414             $dt->set_hour($1)->set_minute($2);
415             $dt->set_second($3) if defined $3;
416             }
417             elsif ($time =~ m'^([0-2]?[0-9])\:([0-5]?[0-9])\:([0-6]?[0-9](\.[0-9]*)?)$')
418             {
419             $dt->set_hour($1)->set_minute($2)->set_second($3);
420             }
421             elsif ($time =~ m'^([0-2]?[0-9])\:([0-5]?[0-9])\:?$')
422             {
423             $dt->set_hour($1)->set_minute($2);
424             }
425              
426             return ($dt, $has_time);
427             }
428              
429             sub _prop_export_contact
430             {
431             my ($self, $model, $triple) = @_;
432              
433             if ($triple->object->is_literal)
434             {
435             return $self->_prop_export_simple($model, $triple);
436             }
437            
438             my $card = $self->export_card($model, $triple->object);
439             my $uri = URI->new('data:');
440             $uri->media_type('text/directory');
441             $uri->data("$card");
442              
443             my $label = '';
444             my ($fn) = $card->get('fn');
445             my ($email) = $card->get('email');
446             if ($fn and $email)
447             {
448             $label = sprintf('%s <%s>',
449             $fn->_unescape_value($fn->value_to_string),
450             $email->_unescape_value($email->value_to_string),
451             );
452             }
453             elsif ($fn)
454             {
455             $label = $fn->_unescape_value($fn->value_to_string);
456             }
457             elsif ($email)
458             {
459             $label = $email->_unescape_value($email->value_to_string);
460             }
461              
462             return RDF::iCalendar::Line->new(
463             property => 'contact',
464             value => $label,
465             type_parameters => {
466             altrep => "\"$uri\"",
467             },
468             );
469             }
470              
471             sub _prop_export_location
472             {
473             my ($self, $model, $triple) = @_;
474              
475             $self->_debug(" Location: %s", flatten_node($triple->object));
476              
477             if ($triple->object->is_literal)
478             {
479             $self->_debug(" -> literal");
480             return $self->_prop_export_simple($model, $triple);
481             }
482              
483             if ($model->count_statements(
484             $triple->object,
485             rdf_resource(RDF('type')),
486             rdf_resource(V('VCard')),
487             )
488             or $model->count_statements(
489             $triple->object,
490             rdf_resource(V('fn')),
491             undef,
492             )
493             )
494             {
495             $self->_debug(" -> vcard");
496             my $card = $self->export_card($model, $triple->object);
497             return RDF::iCalendar::Line->new(
498             property => 'location',
499             value => "$card",
500             type_parameters => {
501             value => "VCARD",
502             },
503             );
504             }
505              
506             elsif ($model->count_statements(
507             $triple->object,
508             rdf_resource(RDF('type')),
509             rdf_resource(V('Address')),
510             )
511             or $model->count_statements(
512             $triple->object,
513             rdf_resource(V('locality')),
514             undef,
515             )
516             or $model->count_statements(
517             $triple->object,
518             rdf_resource(V('street-address')),
519             undef,
520             )
521             )
522             {
523             $self->_debug(" -> adr");
524             my $line = $self->_rebless( $self->_prop_export_adr($model, $triple) );
525             $line->{property} = 'location';
526             return $line;
527             }
528              
529             elsif ($model->count_statements(
530             $triple->object,
531             rdf_resource(RDF('type')),
532             rdf_resource(V('Location')),
533             )
534             or $model->count_statements(
535             $triple->object,
536             rdf_resource(V('latitude')),
537             undef,
538             )
539             )
540             {
541             $self->_debug(" -> geo");
542             my $line = $self->_rebless( $self->SUPER::_prop_export_geo($model, $triple) );
543             $line->{property} = 'location';
544             return $line;
545             }
546              
547             return $self->_prop_export_simple($model, $triple);
548             }
549              
550              
551             sub _prop_export_geo
552             {
553             my ($self, $model, $triple) = @_;
554            
555             if ($triple->object->is_literal)
556             {
557             return $self->_prop_export_simple($model, $triple);
558             }
559             elsif ($triple->object->is_resource
560             and $triple->object->uri =~ /^geo:(.+)$/i)
561             {
562             my $g = $1;
563             return RDF::iCalendar::Line->new(
564             property => 'geo',
565             value => [ split /[,;]/, $g, 2 ],
566             );
567             }
568            
569             my ($lat, $lon);
570             {
571             my @latitudes = grep
572             { $_->is_literal }
573             $model->objects($triple->object, rdf_resource(RDF('first')));
574             $lat = $latitudes[0]->literal_value if @latitudes;
575            
576             my @nodes = grep
577             { !$_->is_literal }
578             $model->objects($triple->object, rdf_resource(RDF('next')));
579             if (@nodes)
580             {
581             my @longitudes = grep
582             { $_->is_literal }
583             $model->objects($nodes[0], rdf_resource(RDF('first')));
584             $lon = $longitudes[0]->literal_value if @longitudes;
585             }
586             }
587            
588             return RDF::iCalendar::Line->new(
589             property => 'geo',
590             value => [$lat||0, $lon||0],
591             );
592             }
593              
594             sub _prop_export_Person
595             {
596             my ($self, $model, $triple) = @_;
597              
598             if ($triple->object->is_literal)
599             {
600             return $self->_prop_export_simple($model, $triple);
601             }
602            
603             my $property = {
604             I('organizer') => 'organizer',
605             IX('organizer') => 'organizer',
606             I('attendee') => 'attendee',
607             IX('attendee') => 'attendee',
608             }->{ $triple->predicate->uri };
609            
610             my ($name, $email, $role, $partstat, $rsvp, $cutype, %thing_values);
611            
612             my %thing_meta = (
613             'sent-by' => [map {rdf_resource($_)} IX('sentBy'), I('sent-by')],
614             'delegated-to' => [map {rdf_resource($_)} IX('delegatedTo'), I('delegated-to')],
615             'delegated-from' => [map {rdf_resource($_)} IX('delegatedFrom'), I('delegated-from')],
616             );
617            
618             if ($triple->object->is_resource
619             and $triple->object->uri =~ /^mailto:.+$/i)
620             {
621             $email = $triple->object->uri;
622             }
623             else
624             {
625             ($name) = grep
626             { $_->is_literal }
627             $model->objects_for_predicate_list($triple->object, rdf_resource(IX('cn')), rdf_resource(V('fn')));
628              
629             ($role) = grep
630             { $_->is_literal }
631             $model->objects_for_predicate_list($triple->object, rdf_resource(V('role')), rdf_resource(I('role')), rdf_resource(IX('role')));
632              
633             ($partstat) = grep
634             { $_->is_literal }
635             $model->objects_for_predicate_list($triple->object, rdf_resource(I('partstat')), rdf_resource(IX('partstat')));
636              
637             ($rsvp) = grep
638             { $_->is_literal }
639             $model->objects_for_predicate_list($triple->object, rdf_resource(I('rsvp')), rdf_resource(IX('rsvp')));
640              
641             ($cutype) = grep
642             { $_->is_literal }
643             $model->objects_for_predicate_list($triple->object, rdf_resource(VX('kind')), rdf_resource(I('cutype')), rdf_resource(IX('cutype')));
644              
645             ($email) = $model->objects($triple->object, rdf_resource(V('email')));
646             if ($email
647             and ($email->is_blank or ($email->is_resource and $email->uri !~ /^mailto:/i)))
648             {
649             ($email) = grep
650             { !$_->is_blank }
651             $model->objects($email, rdf_resource(RDF('value')));
652             }
653              
654             # This bit doesn't just work for sent-by, but also delegated-from/delegated-to
655             while (my ($P, $X) = each %thing_meta)
656             {
657             my ($sentby) = $model->objects_for_predicate_list($triple->object, @$X);
658             # if $sentby isn't an email address
659             if (!defined $sentby) {}
660             elsif ($sentby->is_blank or $sentby->is_resource && $sentby->uri !~ /^mailto:/i)
661             {
662             # Maybe it's a vcard:Email resource; if so, then get the rdf:value.
663             my ($value) = grep
664             { !$_->is_blank }
665             $model->objects($triple->object, rdf_resource(RDF('value')));
666             if ($value)
667             {
668             $sentby = $value;
669             }
670             # If it's not then it might be a vcard:VCard...
671             else
672             {
673             my ($sb_email) = $model->objects($sentby, rdf_resource(V('email')));
674             if (!defined $sb_email) {}
675             elsif ($sb_email->is_literal or $sb_email->is_resource && $sb_email->uri !~ /^mailto:/i)
676             {
677             $sentby = $sb_email;
678             }
679             else
680             {
681             my ($value) = grep
682             { !$_->is_blank }
683             $model->objects($sb_email, rdf_resource(RDF('value')));
684             if ($value)
685             {
686             $sentby = $value;
687             }
688             }
689             }
690             }
691            
692             $thing_values{$P} = $sentby if $sentby;
693             }
694             }
695            
696             my %params = ();
697             $params{'cn'} = flatten_node($name)
698             if defined $name;
699            
700             foreach my $P (keys %thing_meta)
701             {
702             $params{$P} = flatten_node($thing_values{$P})
703             if defined $thing_values{$P};
704             }
705              
706             $params{'cutype'} = flatten_node($cutype)
707             if (defined $cutype and $property eq 'attendee');
708             $params{'partstat'} = flatten_node($partstat)
709             if (defined $partstat and $property eq 'attendee');
710             $params{'role'} = flatten_node($role)
711             if (defined $role and $property eq 'attendee');
712             $params{'rsvp'} = flatten_node($rsvp)
713             if (defined $rsvp and $property eq 'attendee');
714              
715             $params{'value'} = 'CAL-ADDRESS';
716              
717             if (!$email)
718             {
719             $email = $name;
720             $params{'value'} = 'TEXT';
721             }
722            
723             return RDF::iCalendar::Line->new(
724             property => $property,
725             value => flatten_node($email),
726             type_parameters => \%params,
727             );
728             }
729              
730             sub _value_export_simple
731             {
732             my ($self, $model, $triple) = @_;
733             my $rv = $self->_prop_export_simple($model, $triple);
734             return $rv->_unescape_value($rv->value_to_string);
735             }
736              
737             sub _value_export_DateTime
738             {
739             my ($self, $model, $triple) = @_;
740             my $rv = $self->_prop_export_DateTime($model, $triple);
741             return $rv->_unescape_value($rv->value_to_string);
742             }
743              
744             sub _value_export_category
745             {
746             my ($self, $model, $triple) = @_;
747              
748             if ($triple->object->is_literal)
749             {
750             return uc $triple->object->literal_value;
751             }
752              
753             my @labels = grep
754             { $_->is_literal }
755             $model->objects_for_predicate_list(
756             $triple->object,
757             rdf_resource('http://www.w3.org/2004/02/skos/core#prefLabel'),
758             rdf_resource('http://www.holygoat.co.uk/owl/redwood/0.1/tags/name'),
759             rdf_resource('http://www.w3.org/2000/01/rdf-schema#label'),
760             rdf_resource('http://www.w3.org/2004/02/skos/core#altLabel'),
761             rdf_resource('http://www.w3.org/2004/02/skos/core#notation'),
762             rdf_resource(RDF('value')),
763             );
764            
765             if (@labels)
766             {
767             return uc $labels[0]->literal_value;
768             }
769             elsif ($triple->object->is_resource)
770             {
771             return $triple->object->uri;
772             }
773             }
774              
775             sub _prop_export_Recur
776             {
777             my ($self, $model, $triple) = @_;
778              
779             my $prop = 'x-data';
780             if ($triple->predicate->uri =~ m/([^\#\/]+)$/)
781             {
782             $prop = $1;
783             }
784              
785             if ($triple->object->is_literal)
786             {
787             return $self->_prop_export_simple($model, $triple);
788             }
789            
790             my (%bits, @bits);
791            
792             my $iter = $model->get_statements($triple->object, undef, undef);
793             while (my $st = $iter->next)
794             {
795             if ($st->predicate->uri =~ m'^http://www\.w3\.org/2002/12/cal/icaltzd#(.+)$')
796             {
797             my $p = uc $1;
798             my $v = ($p eq 'UNITL') ? $self->_value_export_DateTime($model, $st) : flatten_node($st->object);
799             push @{ $bits{$p} }, $v;
800             }
801             }
802            
803             while (my ($k, $v) = each %bits)
804             {
805             push @bits, sprintf('%s=%s', $k, join(',', @$v));
806             }
807            
808             return RDF::iCalendar::Line->new(
809             property => $prop,
810             value => [ map { [ split /,/, $_ ] } @bits ],
811             type_parameters => { value => 'RECUR' },
812             );
813             }
814              
815             sub _prop_export_freebusy
816             {
817             my ($self, $model, $triple) = @_;
818              
819             if ($triple->object->is_literal)
820             {
821             return RDF::iCalendar::Line->new(
822             property => 'freebusy',
823             value => $triple->object->literal_value,
824             type_parameters => { fbtype => 'BUSY' },
825             );
826             }
827              
828             my @values = sort map
829             { $_->literal_value }
830             grep
831             { $_->is_literal }
832             $model->objects_for_predicate_list(
833             $triple->object,
834             rdf_resource(RDF('value')),
835             );
836            
837             my ($fbtype) = map
838             { uc $_->literal_value }
839             grep
840             { $_->is_literal }
841             $model->objects_for_predicate_list(
842             $triple->object,
843             rdf_resource(I('fbtype')),
844             rdf_resource(IX('fbtype')),
845             );
846              
847             return RDF::iCalendar::Line->new(
848             property => 'freebusy',
849             value => [[ @values ]],
850             type_parameters => { fbtype => $fbtype || 'BUSY' },
851             );
852              
853             }
854              
855              
856             1;
857              
858             __END__
859              
860             =head1 NAME
861              
862             RDF::iCalendar::Exporter - export RDF data to iCalendar format
863              
864             =head1 SYNOPSIS
865              
866             use RDF::iCalendar;
867            
868             my $input = "http://example.com/calendar-data.ics";
869             my $exporter = RDF::iCalendar::Exporter->new;
870            
871             print $_ foreach $exporter->export_calendars($input);
872              
873             =head1 DESCRIPTION
874              
875             This module reads RDF and writes iCalendar files.
876              
877             This is a subclass of RDF::vCard::Exporter, so it can also export vCards.
878              
879             =head2 Constructor
880              
881             =over
882              
883             =item * C<< new(%options) >>
884              
885             Returns a new RDF::iCalendar::Exporter object.
886              
887             There are no valid options at the moment - the hash is reserved
888             for future use.
889              
890             =back
891              
892             =head2 Methods
893              
894             =over
895              
896             =item * C<< export_calendars($input, %options) >>
897              
898             Returns a list of iCalendars found in the input, in no particular order.
899              
900             The input may be a URI, file name, L<RDF::Trine::Model> or anything else
901             that can be handled by the C<rdf_parse> method of L<RDF::TrineShortcuts>.
902              
903             Each item in the list returned is an L<RDF::iCalendar::Entity>, though
904             that class overloads stringification, so you can just treat each item
905             as a string mostly.
906              
907             =item * C<< export_calendar($input, $subject, %options) >>
908              
909             As per C<export_calendars> but exports just a single calendar.
910              
911             The subject provided must be an RDF::Trine::Node::Blank or
912             RDF::Trine::Node::Resource of type icaltzd:Vcalendar.
913              
914             =item * C<< export_component($input, $subject, %options) >>
915              
916             Exports a component from a calendar - e.g. a single VEVENT
917              
918             The subject provided must be an RDF::Trine::Node::Blank or
919             RDF::Trine::Node::Resource of type icaltzd:Vevent, icaltzd:Vtodo
920             or similar.
921              
922             =item * C<< export_cards($input, %options) >>
923              
924             See L<RDF::vCard::Exporter>.
925              
926             =item * C<< export_card($input, $subject, %options) >>
927              
928             See L<RDF::vCard::Exporter>.
929              
930             =back
931              
932             =head2 RDF Input
933              
934             Input is expected to use the newer of the 2005 revision of the W3C's
935             vCard vocabulary L<http://www.w3.org/TR/rdfcal/>. (Note that even
936             though this was revised in 2005, the term URIs include "2002" in
937             them.)
938              
939             Some extensions from the namespace L<http://buzzword.org.uk/rdf/icaltzdx#>
940             are also supported.
941              
942             =head2 iCalendar Output
943              
944             The output of this module aims at iCalendar (RFC 2445) compliance.
945             In the face of weird input data though, (e.g. an DTSTART property that is a
946             URI instead of a literal) it can pretty easily descend into exporting
947             junk, non-compliant iCalendars.
948              
949             The output has barely been tested in any iCalendar-supporting software,
950             so beware.
951              
952             =head1 SEE ALSO
953              
954             L<RDF::iCalendar>.
955              
956             L<RDF::vCard>, L<HTML::Microformats>, L<RDF::TrineShortcuts>.
957              
958             L<http://www.w3.org/TR/rdfcal/>.
959              
960             L<http://www.perlrdf.org/>.
961              
962             =head1 AUTHOR
963              
964             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
965              
966             =head1 COPYRIGHT
967              
968             Copyright 2011, 2013 Toby Inkster
969              
970             This is free software; you can redistribute it and/or modify it under
971             the same terms as the Perl 5 programming language system itself.
972              
973             =head1 DISCLAIMER OF WARRANTIES
974              
975             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
976             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
977             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.