File Coverage

blib/lib/SemanticWeb/OAI/ORE/ReM.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package SemanticWeb::OAI::ORE::ReM;
2             #$Id: ReM.pm,v 1.32 2010-12-07 16:38:29 simeon Exp $
3              
4             =head1 NAME
5              
6             SemanticWeb::OAI::ORE::ReM - Module implementing OAI-ORE Resource Map object
7              
8             =head1 SYNPOSIS
9              
10             This class is designed to provide convenient ways to interact with
11             OAI-ORE resource maps from Perl code. It as based around a data model
12             class L which is the RDF model and may be
13             accessed directly via $rem->model. The access methods here are intended
14             to hide the RDF and instead work more naturally with the constraints
15             and language of OAI-ORE.
16              
17             Written against the v1.0 OAI-ORE specification
18             (L).
19              
20             =head1 DESCRIPTION
21              
22             An ORE Resource Map is comprised of two things:
23              
24             1) a URI indicating its location
25              
26             2) an RDF graph expressing the relationship between an aggregation and
27             aggreted resources
28              
29             This class encapsulates these two things ($rem->uri and $rem->model),
30             some other useful informatino about paring and serialization methods,
31             and provides routines to create/read, update, and write the
32             resource map.
33              
34             =head2 CREATION OF A RESOURCE MAP
35              
36             For simple case where we have simply a set of aggregated resources
37             and minimal metadata is required:
38              
39             use SemanticWeb::OAI::ORE::ReM;
40              
41             my $rem=SemanticWeb::OAI::ORE::ReM->new('ar'=>['uri:1','uri:2']);
42             print $rem->serialize('rdfxml');
43              
44             =head2 PARSING A RESOURCE MAP
45              
46             use SemanticWeb::OAI::ORE::ReM;
47             my $rem=SemanticWeb::OAI::ORE::ReM->new;
48             $rem->parse('rdfxml',$rdfxml_string);
49              
50             =cut
51              
52 7     7   11119 use warnings;
  7         17  
  7         273  
53 7     7   41 use strict;
  7         13  
  7         271  
54 7     7   40 use Carp;
  7         20  
  7         615  
55              
56 7     7   4766 use SemanticWeb::OAI::ORE::Agent;
  7         20  
  7         54  
57 7     7   3072 use SemanticWeb::OAI::ORE::Constant qw(:all);
  7         16  
  7         2636  
58 7     7   6030 use SemanticWeb::OAI::ORE::Model;
  0            
  0            
59             use DateTime;
60             use IO::File;
61              
62             use base qw(Class::Accessor);
63             __PACKAGE__->mk_accessors(qw(default_format die_level warn_level));
64              
65             =head1 METHODS
66              
67             =head3 SemanticWeb::OAI::ORE::ReM->new(%args)
68              
69             Create a new Resource Map (ReM) object. The resource map is comprised
70             of a URI (URI-R) and a set of triples, the model.
71              
72             Any C<%args> supplied are used to set the object variables.
73             As a shorthand for construction argument C{ar} may be used to
74             provide an arrayref of aggregated resources which are added
75             using C<$rem->aggregated_resources($args{ar})>.
76              
77             =cut
78              
79             sub new {
80             my $class=shift;
81             my $self={'uri'=>undef,
82             'uri_agg'=>undef,
83             'model'=>undef,
84             'io'=>{#'atom' =>'SemanticWeb::OAI::ORE::Atom',
85             'rdfxml' =>'SemanticWeb::OAI::ORE::RDFXML',
86             'trix' =>'SemanticWeb::OAI::ORE::TriX',
87             'n3' =>'SemanticWeb::OAI::ORE::N3'},
88             'default_format'=>'rdfxml',
89             'die_level'=>FATAL,
90             'warn_level'=>WARN,
91             @_};
92             bless $self, (ref($class) || $class);
93              
94             # As a shorthand we accept the {ar} parameter which may be
95             # an arrayref to a list of aggregated resources
96             if ($self->{ar}) {
97             $self->aggregated_resources($self->{ar});
98             delete($self->{ar});
99             }
100              
101             return($self);
102             }
103              
104              
105             =head3 $rem->uri()
106              
107             Set or access the identity of the ReM (URI-R). This should be the first
108             thing set when building a Resource Map. The validity of the URI is checked
109             with C.
110              
111             =cut
112              
113             sub uri {
114             my $self=shift;
115             if (@_) {
116             my $uri=shift;
117             $self->check_valid_uri($uri,'Resource Map URI');
118             $self->{uri}=$uri;
119             }
120             return($self->{uri});
121             }
122              
123              
124             =head3 $rem->model()
125              
126             Set or access the model (see L) of this Resource
127             Map. It is usually expected that ORE specific accessor methods such
128             as L, L and such will be used to
129             add data to the model when building a resource map.
130              
131             See L for details of how the model may be
132             accessed and manipulated, and also methods that follow for convenient
133             accessors to ORE elements.
134              
135             Will always return the reference to the model object so one can do things
136             such as:
137              
138             $rem->model->add($ref_statement);
139              
140             =cut
141              
142             sub model {
143             my $self=shift;
144             if (@_ or not $self->{model}) {
145             my $model=$_[0];
146             if (ref($model) and $model->isa('SemanticWeb::OAI::ORE::Model')) {
147             $self->{model}=$model;
148             } else {
149             if ($self->{model}=SemanticWeb::OAI::ORE::Model->new(@_)) {
150             $self->{model}->{errobj}=$self;
151             }
152             }
153             }
154             return($self->{model});
155             }
156              
157              
158             =head3 $rem->aggregation()
159              
160             Set or access the URI of the Aggregation described by this ReM (URI-A).
161             Must be set after the URI of the Resource Map (URI-R) and would usually
162             be the second thing set when building a resource map from scratch.
163              
164             WARNING - this routine does not have the facility to update all occurrences
165             in the model if changed when other statements (e.g. aggregated resources
166             or metadata) have been added that reference the aggregation.
167              
168             =cut
169              
170             sub aggregation {
171             my $self=shift;
172             if (@_) {
173             my $aggregation=shift;
174             $self->check_valid_uri($aggregation,'Aggregation URI');
175             $self->{uri_agg}=$aggregation;
176             $self->model->add_rel_to_resource($self->{uri},DESCRIBES,$aggregation);
177             }
178             return($self->{uri_agg});
179             }
180              
181              
182             =head3 $rem->creators()
183              
184             Set or access the creator of the ReM. Returns If there is more than one creator then
185             the first will be returned. Returns nothing if there is no creator set.
186              
187             See L:
188              
189             The identity of the authoring authority (human, organization, or agent) of
190             the Resource Map, using the dcterms:creator predicate, with an object that MUST
191             be a reference to a Resource of type L. This MAY
192             then be the subject of the following triples:
193              
194             * A triple with the predicate foaf:name and an object that is a text string
195             containing some descriptive name of the authoring authority.
196              
197             * A triple with the predicate foaf:mbox and an object that is a URI that is
198             the email address of the authoring authority.
199              
200             =cut
201              
202             sub creators {
203             my $self=shift;
204             foreach my $creator (@_) {
205             $self->model->add_rel_to_resource($self->{uri},CREATOR,$creator);
206             }
207             my @creators=();
208             foreach my $creator ($self->model->creators($self->{uri})) {
209             push(@creators,SemanticWeb::OAI::ORE::Agent->new(uri=>$creator->getURI,
210             name=>$self->model->literal_matching($creator,FOAF_NAME),
211             mbox=>$self->model->literal_matching($creator,FOAF_MBOX)));
212             }
213             return(@creators);
214             }
215              
216              
217             =head3 $rem->creator()
218              
219             Assumes one creator. Wrapper around $rem->creators() that does the same thing
220             except in the case where there are multiple creators it will return just the first.
221              
222             =cut
223              
224             sub creator {
225             my $self=shift;
226             my @creators=$self->creators;
227             return(@creators ? shift @creators : () );
228             }
229              
230              
231             =head3 $rem->creator_name()
232              
233             Set or access the creator as a URI of the ReM.
234              
235             =cut
236              
237             sub creator_name {
238             my $self=shift;
239             if (@_) {
240             $self->{creator_name}=shift;
241             $self->model->add_rel_to_literal($self->{uri},CREATOR,$self->{creator_name});
242             } else {
243              
244             }
245             return($self->{creator_name});
246             }
247              
248              
249             =head3 $rem->timestamp_as_unix($type,$timestamp)
250              
251             Set or access the option creation timestamp of the ReM. Returns
252             now if not set. Type should be either CREATED or MODIFIED constant.
253             Usually called via created_as_unix() or modified_as_unix() wrappers.
254              
255             Will set if timestamp if C<$timestamp> is defined.
256              
257             =cut
258              
259             sub timestamp_as_unix {
260             my $self=shift;
261             my ($type,$timestamp)=@_;
262             if (not defined $type or ($type ne CREATED and $type ne MODIFIED)) {
263             confess "OOPS - call to timestamp_as_unix(type) without valid type (got: $type)";
264             }
265             if (defined $timestamp) {
266             if ($self->{timestamp_iso8601}) {
267             carp "WARNING - Already have ISO8601 timestamp set";
268             }
269             $self->{timestamp_unix}=$timestamp;
270             }
271             if ($self->{timestamp_iso8601}) {
272             croak "OOPS .. haven't implemented conversion of ISO8601 to unix time";
273             } else {
274             return($self->{timestamp_unix} || time());
275             }
276             }
277              
278             =head3 $rem->created_as_unix($timestamp)
279              
280             Set or access the creation timestamp of the ReM as a Unix timestamp.
281              
282             Will set timestamp if C<$timestamp> is defined.
283              
284             =cut
285              
286             sub created_as_unix {
287             my $self=shift;
288             return(timestamp_as_unix($self,CREATED,@_));
289             }
290              
291              
292             =head3 $rem->modified_as_unix($timestamp)
293              
294             Set or access the modification timestamp of the ReM as a Unix timestamp.
295              
296             Will set timestamp if C<$timestamp> is defined.
297              
298             =cut
299              
300             sub modified_as_unix {
301             my $self=shift;
302             return(timestamp_as_unix($self,MODIFIED,@_));
303             }
304              
305              
306            
307             =head3 $rem->timestamp_as_iso8601($type,$timestamp)
308              
309             Set or access the timestamp of the rem as an ISO8601 string.
310             Type should be either CREATED or MODIFIED constant.
311             Usually called via created_as_iso8601() or modified_as_iso8601() wrappers.
312              
313             Will set timetstamp if C<$timestamp> is defined.
314              
315             =cut
316              
317             sub timestamp_as_iso8601 {
318             my $self=shift;
319             my ($type,$timestamp)=@_;
320             if (not defined $type or ($type ne CREATED and $type ne MODIFIED)) {
321             confess "OOPS - call to timestamp_as_iso8601(type) without valid type (got: $type)";
322             }
323             return(undef) if ($type eq CREATED); #FIXME - not yet implemented
324             if (defined $timestamp) {
325             if ($self->{timestamp_unix}) {
326             carp "WARNING - Already have unix timestamp set";
327             }
328             $self->{timestamp_iso8601}=$timestamp;
329             }
330             return($self->{timestamp_iso8601});
331             }
332              
333              
334             =head3 $rem->now_as_iso8601()
335              
336             Returns the current system time as an iso8601 string
337              
338             =cut
339              
340             sub now_as_iso8601 {
341             my $self=shift;
342             my $dt=DateTime->from_epoch(time());
343             return($dt->iso8601().'Z');
344             }
345              
346              
347             =head3 $rem->created_as_iso8601($timestamp)
348              
349             Set or access the creation timestamp of the ReM as a ISO8601 timestamp.
350              
351             Will set timestamp if C<$timestamp> is defined.
352              
353             =cut
354              
355             sub created_as_iso8601 {
356             my $self=shift;
357             return(timestamp_as_iso8601($self,CREATED,@_));
358             }
359              
360              
361             =head3 $rem->modified_as_iso8601($timestamp)
362              
363             Set or access the creation timestamp of the ReM as a ISO8601 timestamp.
364              
365             Will set timestamp if C<$timestamp> is defined.
366              
367             =cut
368              
369             sub modified_as_iso8601 {
370             my $self=shift;
371             return(timestamp_as_iso8601($self,MODIFIED,@_));
372             }
373              
374              
375             =head3 $rem->aggregation_metadata($predicate,$only)
376              
377             If C<$only> is not specified then the objects matching will be returned.
378              
379             If C<$only> is C then only resource labels will be included, if
380             C then only literal labels will be returned.
381              
382             =cut
383              
384             sub aggregation_metadata {
385             my $self=shift;
386             my ($predicate,$only)=@_;
387             # Cannot do anything if we do not have an aggregation
388             return() if (not $self->aggregation);
389             # We do, search for all matching objects
390             return( $self->model->objects_matching($self->aggregation,$predicate,$only) );
391             }
392              
393              
394             =head3 $rem->aggregation_metadata_literal($predicate)
395              
396             Wrapper for C<$rem->aggregation_metadata> that will take just the first
397             matching literal, or return undef if there a no matches.
398              
399             =cut
400              
401             sub aggregation_metadata_literal {
402             my $self=shift;
403             my ($predicate)=@_;
404             # Cannot do anything if we donot have an aggregation
405             return() if (not $self->aggregation);
406             # We do, get first matching literal
407             return( $self->model->literal_matching($self->aggregation,$predicate) );
408             }
409              
410              
411             =head3 $rem->aggregated_resources()
412              
413             Set or access the aggregated resources list.
414              
415             $rem->aggregated_resources('uri:3');
416              
417             This method will never remove an aggregated resource from this ReM. Use
418             delete_aggregated_resources to remove or clear the set of
419             aggregated resources for this ReM. Returns a list of all the
420             aggregated resources.
421              
422             =cut
423              
424             sub aggregated_resources {
425             my $self=shift;
426             foreach my $ar (@_) {
427             $self->model->add_rel_to_resource($self->aggregation,AGGREGATES,$ar);
428             }
429             # now return the list of all aggregated resources
430             return($self->model->objects_matching($self->aggregation,AGGREGATES,RESOURCE));
431             }
432              
433              
434             =head3 $rem->rights()
435              
436             Set of access the rights for this resource map. Permits only
437             one rights statement to be associated with the resource map.
438             Returns undef if no rights value is set.
439              
440             =cut
441              
442             sub rights {
443             my $self=shift;
444             if (@_) {
445             my $rights=shift(@_);
446             $self->model->add_rel_to_resource($self->{uri},RIGHTS,$rights);
447             }
448             # now return the current rights URI
449             my @rights=$self->model->objects_matching($self->{uri},RIGHTS,RESOURCE);
450             return($rights[0] ? $rights[0] : undef);
451             }
452              
453              
454             =head3 $rem->is_valid
455              
456             Run validation checks on the resource map model. Returns true (1) on succes,
457             false (nothing) on failures. Errors set in errstr.
458              
459             =cut
460              
461             sub is_valid {
462             my $self=shift;
463              
464             eval {
465             $self->model->check_model($self->uri,$self);
466             };
467             if ($@) {
468             # Oops
469             $self->add_errstr("Invalid resource map: $@");
470             return;
471             }
472             return(1);
473             }
474              
475              
476              
477             #####################################################################
478              
479             =head2 INPUT AND OUTPUT METHODS
480              
481             =head3 $rem->parse($format,$src,$uri_rem)
482              
483             Parse resource C<$uri_rem>. Get it from C<$src>, where C<$src> may be
484             either a string containing the representation to be parsed,
485             or an open filehandle. If C<$src> is not set then attempt to
486             download from C<$uri_rem> using C.
487              
488             To parse a file directly, use the C wrapper. To parse
489             a URI directly, use the C wrapper.
490              
491             Will run validation checks on the resource map model obtained. Set
492             C<$rem->die_level(RECKLESS)> to ignore errors.
493              
494             Will return true (1) on success, false (undef) on failure. Will
495             have set errstr on failure.
496              
497             =cut
498              
499             sub parse {
500             my $self=shift;
501             my ($format,$src,$uri_rem)=@_;
502              
503             if (not defined $src and defined $uri_rem) {
504             # Try to get from URI
505             return($self->parseuri($format,$uri_rem));
506             }
507              
508             my $input_uri_rem=$uri_rem;
509             $uri_rem='http://unknown.example.org/' if (not defined $uri_rem);
510              
511             my $model=undef;
512             if (my $io_class=$self->{io}{$format}) {
513             eval {
514             if (not eval("require $io_class")) {
515             croak "Failed to load class $io_class: $@";
516             }
517             my $reader=$io_class->new(%$self,'rem'=>$self);
518             #print "DEBUG ".__PACKAGE__."::parse: using reader: $reader isa ".ref($reader)."\n";
519             $model=$reader->parse($src,$uri_rem);
520             } or do {
521             carp "Error trying to parse (".($uri_rem?$uri_rem:'ReM URI unknown').") in '$format' using class '$io_class': $@\n";
522             if (not $self->errstr) {
523             $self->errstr("Error parsing (".($uri_rem?$uri_rem:'ReM URI unknown').")");
524             }
525             };
526             } else {
527             croak "Unknown serialization format to parse '$format' in parse(..)\n";
528             }
529              
530             # Now have model in $model, connect as model of this Resource Map
531             # and check that it is valid.
532             $model=$self->model($model);
533              
534             # If we didn't know the ReM URI to start, introspect to find it
535             if ($input_uri_rem) {
536             $self->uri($input_uri_rem);
537             } else {
538             if (my $uri=$self->model->find_rem_uri) {
539             $self->uri($uri);
540             }
541             }
542              
543             # If we didn't know the Aggregation URI to start, introspect to find it
544             if (not $self->aggregation) {
545             if (my $agg=$self->model->find_aggregation_uri) {
546             $self->{uri_agg}=$agg;
547             }
548             }
549              
550             # Validate?
551             #$self->is_valid;
552              
553             return(1);
554             }
555              
556              
557             =head3 $rem->parsefile($format,$file,$uri_rem)
558              
559             Wrapper for X<$rem->parse($format,$uri_rem,$src)> which does nothing with
560             C<$format> and C<$uri_rem> but opens C<$file> and passes the reulting filehandle
561             on to C<$rem->parse(...)>. Returns C if the file cannot be opened, otherwise
562             return values as for C<$rem->parse(...)>.
563              
564             =cut
565              
566             sub parsefile {
567             my $self=shift;
568             my ($format,$file,$uri_rem)=@_;
569             my $srcfh=IO::File->new();
570             if ($srcfh->open($file,'<')) {
571             my $retval=$self->parse($format,$srcfh,$uri_rem);
572             close($srcfh);
573             return($retval);
574             }
575             $self->errstr("Can't open source file '$file'");
576             return(undef);
577             }
578              
579              
580             =head3 $rem->parseuri($format,$uri_rem)
581              
582             Simple wrapper for X<$rem->parse($format,$uri_rem,$src)> that
583             downloads C<$uri_rem> with L before passing it on.
584             Returns C if C<$uri_rem> cannot be downloaded, otherwise
585             return values as for C<$rem->parse(...)>.
586              
587             =cut
588              
589             sub parseuri {
590             my $self=shift;
591             my ($format,$uri_rem)=@_;
592              
593             if (not defined $uri_rem) {
594             croak("Attempt to call parseuri without a URI");
595             }
596              
597             # Wrap in eval so we can run this module without LWP::Simple if
598             # this method is not required
599             my $src_string;
600             eval {
601             use LWP::Simple (); #do not import
602             $src_string=LWP::Simple::get($uri_rem);
603             };
604             if ($@ or not defined $src_string) {
605             $self->errstr("Can't get ReM from $uri_rem: $@");
606             return(undef);
607             }
608            
609             return($self->parse($format,$src_string,$uri_rem));
610             }
611              
612              
613             =head3 $rem->serialize()
614              
615             Serialize in default format which has accessor C<$rem->default_format>.
616              
617             =head3 $rem->serialize($format)
618              
619             Serialize in C<$format>. This will use and call the appropriate
620             writer class.
621              
622             =cut
623              
624             sub serialize {
625             my $self=shift;
626             my $format=shift || $self->{default_format};
627             my $out='';
628             if (my $io_class=$self->{io}{$format}) {
629             eval {
630             eval("require $io_class");
631             my $writer=$io_class->new('rem'=>$self,@_);
632             #print "DEBUG ".__PACKAGE__."::serialize: using writer: $writer isa ".ref($writer)."\n";
633             $out=$writer->serialize();
634             } or do {
635             carp "Error trying to serialize in '$format' using class '$io_class': $@\n";
636             };
637             } else {
638             carp "Unknown serialization format '$format'\n";
639             }
640             return($out);
641             }
642              
643              
644             =head3 $rem->errstr($str) or $rem->errstr
645              
646             Resets the error string to C<$str> if C<$str> provided.
647              
648             Returns a string, either the error string if set, else ''.
649              
650             =cut
651              
652             sub errstr {
653             my $self=shift;
654             my $str=shift;
655             $self->{errstr}=$str if (defined $str);
656             return($self->{errstr}?$self->{errstr}:'');
657             }
658              
659              
660             =head3 $rem->add_errstr($str)
661              
662             Add to the error string. Will append C<\n> if not present in C<$str>.
663              
664             =cut
665              
666             sub add_errstr {
667             my $self=shift;
668             my ($str)=@_;
669             $str.="\n" if ($str!~/\n$/);
670             $self->{errstr}.=$str;
671             }
672              
673              
674             =head3 $rem->err($level,$msg)
675              
676             Log and/or report an error C<$msg> at level C<$level>. Intended mainly
677             for internal use and use by particular format classes.
678              
679             =cut
680              
681             sub err {
682             my $self=shift;
683             my ($level,$msg)=@_;
684             if ($self->die_level and $level>=$self->die_level) {
685             croak "ERROR: $msg";
686             } elsif ($self->warn_level and $level>=$self->warn_level) {
687             my $code=ERROR_LEVEL->[$level] || 'UNKNOWN';
688             $self->add_errstr("[$code] $msg");
689             } #else ignore!
690             }
691              
692              
693             =head3 $rem->check_valid_uri($uri,$description)
694              
695             Check that the supplied C<$uri> is valid and create C<$rem->err> if not.
696             Returns true if valid, false otherwise.
697              
698             =cut
699              
700             sub check_valid_uri {
701             my $self=shift;
702             my ($uri,$description)=@_;
703             if (not defined $uri) {
704             $self->err(WARN,"$description is not defined and thus not a valid URI");
705             return();
706             } elsif ($uri!~/^[a-z]+:\S+$/) {
707             $self->err(WARN,"$description ($uri) is not a valid URI");
708             return();
709             }
710             return(1);
711             }
712              
713              
714             =head1 SEE ALSO
715              
716             Details of the Open Archive Initiative, including both the OAI-ORE and
717             OAI-PMH specification are found at L.
718              
719             This module is the primary class for support of OAI-ORE resource maps.
720             Other parts include:
721             L
722             L
723             L
724              
725             Support for the OAI-PMH protocol is provided by other modules
726             including L.
727              
728             =head1 AUTHOR
729              
730             Simeon Warner, C<< >>
731              
732             =head1 BUGS
733              
734             Support for Atom format output is not yet provided, this should be
735             L.
736              
737             Please report any bugs or feature requests to
738             C, or through the web interface at
739             L.
740             I will be notified, and then you'll automatically be notified of progress on
741             your bug as I make changes.
742              
743             =head1 SUPPORT
744              
745             You can find documentation for this module with the perldoc command.
746              
747             perldoc SemanticWeb::OAI::ORE
748              
749             You can also look for information at:
750              
751             =over 4
752              
753             =item * AnnoCPAN: Annotated CPAN documentation
754              
755             L
756              
757             =item * CPAN Ratings
758              
759             L
760              
761             =item * RT: CPAN's request tracker
762              
763             L
764              
765             =item * Search CPAN
766              
767             L
768              
769             =back
770              
771             =head1 COPYRIGHT & LICENSE
772              
773             Copyright 2007-2010 Simeon Warner.
774              
775             This program is free software; you can redistribute it and/or modify it
776             under the same terms as Perl itself.
777              
778             =cut
779              
780             1;