package RDF::Server::Formatter::Atom; use Moose; with 'RDF::Server::Formatter'; use MooseX::Types::Moose qw(ArrayRef Str); use RDF::Server::Constants qw(:ns); use RDF::Server::XMLDoc; use XML::LibXML; use RDF::Server::Exception; use RDF::Server::Types qw( UUID ); use RDF::Server (); # we need a way to communicate the mime type sub wants_rdf { 1 } ### # Entry / Resource formatting ### sub _define_namespace { my($self, $e, $ns, $uri, $prefix) = @_; return if defined( $ns -> {$uri} ) && $ns -> {$uri} ne ''; if( defined $ns -> {$uri} ) { $e -> setNamespaceDeclPrefix( '', $prefix ); } else { $e -> setNamespace( $uri, $prefix, 1 ); } $ns -> {$uri} = $prefix; } my @atom_elements = ( [ ATOM_NS, 'category', ATOM_NS, 'category' ], [ ATOM_NS, 'contributor',DC_NS, 'contributor' ], [ ATOM_NS, 'author', DC_NS, 'creator' ], [ ATOM_NS, 'published', DC_NS, 'publisher' ], [ ATOM_NS, 'rights', DC_NS, 'rights' ], [ ATOM_NS, 'source', DC_NS, 'source' ], [ ATOM_NS, 'summary', DC_NS, 'description' ], [ ATOM_NS, 'title', DC_NS, 'title' ], [ ATOM_NS, 'updated', ATOM_NS, 'updated' ], ); sub resource { my($self, $rdf) = @_; # now we need to wrap this in whatever is needed for Atom =pod =for Atom spec atom:category (0 or more) atom:content (0 or 1) atom:contributor (0 or more) atom:id (1) atom:link (0 or more) atom:published (0 or 1) atom:rights (0 or 1) atom:source (0 or 1) atom:summary (0 or 1) atom:title (1) atom:updated (1) extensionElements: (0 or more) root element: atom:entry =end Atom spec =cut # categories... ? # we want to replace the with # we want to 'unserialize' atom:content elements my $doc = RDF::Server::XMLDoc -> new( $rdf ); my $root = $doc -> document -> documentElement(); my %namespaces = map { $_ -> declaredURI => $_ -> declaredPrefix } $root -> getNamespaces; $self -> _define_namespace( $root, \%namespaces, ATOM_NS, 'xxatom'); $self -> _define_namespace( $root, \%namespaces, APP_NS, 'xxapp'); $self -> _define_namespace( $root, \%namespaces, RDF_NS, 'xxrdf'); $self -> _define_namespace( $root, \%namespaces, DC_NS, 'xxdc'); $root -> setNodeName( 'entry' ); $root -> setNamespace( ATOM_NS, $namespaces{+ATOM_NS}, 1); my @content = $root -> findnodes( "/$namespaces{+ATOM_NS}:entry/$namespaces{+RDF_NS}:Description" ); if( @content ) { $content[0] -> setNodeName( 'content' ); $content[0] -> setNamespace( ATOM_NS, $namespaces{+ATOM_NS}, 1); $content[0] -> setAttribute( type => 'application/rdf+xml' ); } my($e, $a); foreach my $translation ( @atom_elements ) { foreach $e ( $root -> findnodes( "/$namespaces{+ATOM_NS}:entry/$namespaces{+ATOM_NS}:content/$namespaces{$translation->[2]}:$translation->[3]") ) { $e -> setNodeName( $translation->[1] ); $e -> setNamespace( $translation->[0], $namespaces{$translation->[0]}, 1); # check for rdf:resource attributes and change them to href if( $a = $e -> getAttributeNodeNS( RDF_NS, 'resource' ) ) { $e -> setAttribute( href => $a -> getValue ); $e -> removeAttributeNS( RDF_NS, 'resource' ); } $root -> insertBefore($e, $content[0]); } } if( $content[0] -> hasAttributeNS( RDF_NS, 'about' ) ) { my $id = $content[0] -> getAttributeNodeNS( RDF_NS, 'about' ); my $idv = $id -> getValue(); my $url = ''; if( is_UUID($idv) ) { $url = 'urn:uuid:' . $idv; } else { $url = $idv; } my $textnode = $doc -> document -> createElement( 'id' ); $textnode -> setNamespace( ATOM_NS, $namespaces{+ATOM_NS}, 1); $textnode -> appendText( $url ); $root -> insertBefore( $textnode, $content[0] ); $id -> unbindNode(); } return( 'application/atom+xml', $doc ); } sub to_rdf { my($self, $rdf) = @_; my $doc = RDF::Server::XMLDoc -> new( $rdf ); my $root = $doc -> document -> documentElement(); if($root -> localname ne 'entry' || $root -> namespaceURI() ne ATOM_NS) { throw RDF::Server::Exception::BadRequest( Content => 'Document is not an atom:entry!' ); } my %namespaces = map { $_ -> declaredURI => $_ -> declaredPrefix } $root -> getNamespaces; $self -> _define_namespace( $root, \%namespaces, ATOM_NS, 'xxatom'); $self -> _define_namespace( $root, \%namespaces, APP_NS, 'xxapp'); $self -> _define_namespace( $root, \%namespaces, RDF_NS, 'xxrdf'); $self -> _define_namespace( $root, \%namespaces, DC_NS, 'xxdc'); $root -> setNodeName( "$namespaces{+RDF_NS}:RDF" ); $root -> setNamespace( RDF_NS, $namespaces{+RDF_NS}, 1 ); my @content = $root -> findnodes( "$namespaces{+ATOM_NS}:content" ); foreach my $e (@content) { my $type = $e -> getAttribute('type'); confess "Undefined atom:content type" unless defined $type; if( $type ne 'application/rdf+xml' ) { confess "Unsupported atom:content type: $type"; } $e -> setNodeName( 'Description' ); $e -> setNamespace( RDF_NS, $namespaces{+RDF_NS}, 1); $e -> removeAttribute( 'type' ); } foreach my $translation ( @atom_elements ) { my @elems = $root -> findnodes( "/$namespaces{+RDF_NS}:RDF/$namespaces{$translation->[0]}:$translation->[1]" ); foreach my $e ( @elems ) { $e -> setNodeName( $translation->[3] ); # print STDERR "ns: ", join("; ", $translation->[2], $namespaces{$translation->[2]} ), "\n"; $e -> setNamespace( $translation->[2], $namespaces{$translation->[2]}, 1); if( $a = $e -> getAttributeNode( 'href' ) ) { $e -> setAttributeNS( RDF_NS, resource => $a -> getValue ); $e -> removeAttribute( 'href' ); } $content[0] -> appendChild( $e ); } } return $doc; } ### # List formatting ### sub _add_text_node { my($self, $doc, $root, $e, $t) = @_; #print STDERR "_add_text_node($e => $t)\n"; my $n = $doc -> createElement( $e ); $n -> appendTextNode( $t ); $root -> appendChild( $n ); } # # we expect: title, id, link # entries: iterator # sub feed { my($self, %c) = @_; my($doc, $root) = $self -> _new_xml_doc(ATOM_NS, 'feed'); $self -> _add_text_node( $doc -> document, $root, 'atom:title', $c{title} ); $self -> _add_text_node( $doc -> document, $root, 'atom:id', $c{id} ); $self -> _add_text_node( $doc -> document, $root, 'atom:generator', "RDF::Server " . $RDF::Server::VERSION ); my $n = $doc -> document -> createElement( 'atom:link' ); $n -> setAttribute( href => $c{link} ); $n -> setAttribute( rel => 'self' ); my $e; if( $c{entries} ) { while( $e = $c{entries} -> next ) { my $eroot = $doc -> document -> createElement( 'atom:entry' ); $self -> _add_text_node( $doc -> document, $eroot, 'atom:title', $e -> get_value(DC_NS, 'title') ); $n = $doc -> document -> createElement( 'atom:link' ); $n -> setAttribute( href => $e -> uri ); $eroot -> appendChild( $n ); my $id = $e -> id; if( is_UUID( $id ) ) { $id = "urn:uuid:$id"; } else { $id = $e -> uri; } $self -> _add_text_node( $doc -> document, $eroot, 'atom:id', $id ); $self -> _add_text_node( $doc -> document, $eroot, 'atom:updated', $e -> get_value(ATOM_NS, 'updated' ) || $e -> get_value(DC_NS, 'created') ); $self -> _add_text_node( $doc -> document, $eroot, 'atom:summary', 'rdf content' ); $root -> appendChild( $eroot ); } } return( 'application/atom+xml', $doc ); } sub category { my($self, %c) = @_; my($doc, $root) = $self -> _new_xml_doc(ATOM_NS, 'category'); $self -> _add_text_node( $doc -> document, $root, 'atom:title', $c{title} || $c{term} ); $root -> setAttribute( scheme => $c{scheme} ); $root -> setAttribute( term => $c{term} ); return( 'application/atom+xml', $doc ); } sub collection { my($self, %c) = @_; my($doc, $root) = $self -> _new_xml_doc('collection'); $self -> _add_text_node( $doc -> document, $root, 'atom:title', $c{title} ); foreach my $a ( @{ $c{accept} || [] }) { $self -> _add_text_node( $doc -> document, $root, 'app:accept', $a ); #$n = $doc -> document -> createElement( 'app:accept' ); #$n -> appendTextNode( $a ); #$root -> appendChild( $n ); } if( $c{categories} ) { my $cats_root = $doc -> document -> createElement( 'app:categories' ); if(is_ArrayRef( $c{categories} ) ) { foreach my $c ( @{$c{categories}} ) { my($t, $c_doc) = $self -> category(%$c); my $c_root = $self -> _import_as_child_of( $doc, $cats_root, $c_doc ); } } elsif(is_Str( $c{categories} ) ) { $cats_root -> setAttribute( href => $c{categories} ); } $root -> appendChild( $cats_root ); } return( 'application/atom+xml', $doc ); } sub workspace { my($self, %c) = @_; my($doc, $root) = $self -> _new_xml_doc('workspace'); $self -> _add_text_node( $doc -> document, $root, 'atom:title', $c{title} ); #my $n = $doc -> document -> createElement( 'atom:title'); #$n -> appendTextNode( $c{title} ); #$root -> appendChild( $n ); foreach my $c (@{$c{collections}}) { my($t, $c_doc) = $self -> collection(%$c); my $c_root = $self -> _import_as_child_of( $doc, $root, $c_doc ); $c_root -> setAttribute( href => $c -> {link} ); } return( 'application/atom+xml', $doc ); } sub service { my($self, %c) = @_; my($doc, $root) = $self -> _new_xml_doc('service'); foreach my $w ( @{$c{workspaces}} ) { my($t, $w_doc) = $self -> workspace(%$w); my $w_root = $self -> _import_as_child_of( $doc, $root, $w_doc ); } return( 'application/atomsvc+xml', $doc ); } sub _import_as_child_of { my($self, $doc, $root, $other_doc) = @_; my $o_root = $other_doc -> document -> documentElement(); $doc -> document -> importNode( $o_root ); $root -> addChild( $o_root ); return $o_root; } sub _new_xml_doc { my($self, $ns, $root_element); if( @_ == 2 ) { ($self, $root_element) = @_; $ns = APP_NS; } else { ($self, $ns, $root_element) = @_; } # produce an Atom document describing the workspaces (handlers) my $doc = XML::LibXML::Document -> new(); my $root = $doc -> createElement($root_element); $root -> setNamespace( APP_NS, 'app', $ns eq APP_NS); $root -> setNamespace( ATOM_NS, 'atom', $ns eq ATOM_NS); $root -> setNamespace( $ns, 'a', 1) if $ns ne APP_NS && $ns ne ATOM_NS; $doc -> setDocumentElement( $root ); return( RDF::Server::XMLDoc -> new($doc), $root ); } 1; __END__ =pod =head1 NAME RDF::Server::Formatter::Atom - Work with Atom documents =head1 SYNOPSIS package My::Server; protocol 'HTTP'; interface 'REST'; semantic 'Atom'; render xml => 'Atom'; =head1 DESCRIPTION =head1 METHODS =over 4 =item wants_rdf Returns true. The Atom formatter works with RDF documents. =item resource =item to_rdf =item feed =item category =item collection =item workspace =item service =back =head1 AUTHOR James Smith, C<< >> =head1 LICENSE Copyright (c) 2008 Texas A&M University. This library is free software. You can redistribute it and/or modify it under the same terms as Perl itself. =cut