File Coverage

blib/lib/SemanticWeb/OAI/ORE/Model.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package SemanticWeb::OAI::ORE::Model;
2             #$Id: Model.pm,v 1.16 2010-12-06 14:44:15 simeon Exp $
3              
4             =head1 NAME
5              
6             SemanticWeb::OAI::ORE::Model - Module for model component of an OAI-ORE Resource Map
7              
8             =head1 SYNOPSIS
9              
10             In essence, the model is simply a set of triples and we thus store them in
11             a triple store provided by L, L etc..
12              
13             =cut
14              
15 7     7   45 use strict;
  7         14  
  7         257  
16 7     7   40 use warnings;
  7         11  
  7         213  
17 7     7   34 use Carp qw(croak carp);
  7         12  
  7         413  
18              
19 7     7   34 use SemanticWeb::OAI::ORE::Constant qw(:all);
  7         13  
  7         2086  
20 7     7   4856 use SemanticWeb::OAI::ORE::N3;
  0            
  0            
21              
22             use RDF::Core::Model;
23             use RDF::Core::Model::Serializer;
24             use RDF::Core::Storage;
25             use RDF::Core::Storage::Memory;
26             use RDF::Core::Resource;
27             use RDF::Core::Literal;
28             use RDF::Core::Statement;
29             use Class::Accessor;
30              
31             use base qw(Class::Accessor RDF::Core::Model);
32             SemanticWeb::OAI::ORE::Model->mk_accessors(qw(die_level));
33              
34             =head1 METHODS
35              
36             =head2 CREATION AND MODIFICATION
37              
38             =head3 SemanticWeb::OAI::ORE::Model->new(%args) or SemanticWeb::OAI::ORE::Model->new($rdf_model)
39              
40             Create new relationships object as part of a resource map.
41              
42             If supplied with a single argument that is a L then that
43             object is blessed into this class an returned. Otherwise a new
44             L object is created and any %args are passed to the
45             creator.
46              
47             =cut
48              
49             sub new {
50             my $class=shift;
51             my $self;
52             if (ref($_[0]) and $_[0]->isa('RDF::Core::Model')) {
53             $self=$_[0];
54             } else {
55             $self=RDF::Core::Model->new(Storage=>RDF::Core::Storage::Memory->new(),@_);
56             }
57             bless $self, $class;
58             $self->die_level(FATAL);
59             return($self);
60             }
61              
62              
63             =head3 $model->add($model_or_statement)
64              
65             Add either another model object or a single statement to this $model.
66             Returns the number of statements added.
67              
68             =cut
69              
70             sub add {
71             my $self=shift;
72             my $count=0;
73             foreach my $to_add (@_) {
74             if ($to_add->isa('RDF::Core::Model')) {
75             my $enum=$to_add->getStmts(undef,undef,undef);
76             my $statement=$enum->getFirst();
77             while ($statement) {
78             $self->addStmt($statement);
79             $count++;
80             $statement=$enum->getNext();
81             }
82             $enum->close();
83             } elsif ($to_add->isa('RDF::Core::Statement')) {
84             $self->addStmt($to_add);
85             $count++;
86             } else {
87             die "Don't know how to add a ".ref($to_add)." to the Model";
88             }
89             }
90             return($count);
91             }
92              
93              
94             =head3 $model->add_rel_to_resource($subject,$predicate,$object)
95              
96             Add relationship where the object is a resource (URI).
97              
98             =cut
99              
100             sub add_rel_to_resource {
101             my $self=shift;
102             my ($subject,$predicate,$object)=@_;
103             $subject=RDF::Core::Resource->new($subject);
104             $predicate=RDF::Core::Resource->new($predicate);
105             $object=RDF::Core::Resource->new($object);
106             $self->addStmt(RDF::Core::Statement->new($subject,$predicate,$object));
107             }
108              
109              
110             =head3 $model->add_rel_to_literal($subject,$predicate,$object)
111              
112             Add relationship where the object is a literal.
113              
114             =cut
115              
116             sub add_rel_to_literal {
117             my $self=shift;
118             my ($subject,$predicate,$object)=@_;
119             $subject=RDF::Core::Resource->new($subject);
120             $predicate=RDF::Core::Resource->new($predicate);
121             $object=RDF::Core::Literal->new($object);
122             $self->addStmt(RDF::Core::Statement->new($subject,$predicate,$object));
123             }
124              
125              
126             =head3 $model->count()
127              
128             Returns the number of statements or relationships.
129              
130             =cut
131              
132             sub count {
133             my $self=shift;
134             return($self->countStmts(undef,undef,undef));
135             }
136              
137              
138             =head3 $model->as_array()
139              
140             Return an array reference to all triples each as a four element array with
141             [subject, predicate, object, object_is_literal] for each statement.
142              
143             FIXME - should perhaps implement iterator or similar...
144              
145             =cut
146              
147             sub as_array {
148             my $self=shift;
149             my $enum=$self->getStmts(undef,undef,undef);
150             my $statement=$enum->getFirst();
151             my @triples=();
152             while ($statement) {
153             push(@triples,[$statement->getSubject()->getLabel(),
154             $statement->getPredicate()->getLabel(),
155             $statement->getObject()->getLabel(),
156             $statement->getObject()->isLiteral()]);
157             $statement=$enum->getNext();
158             }
159             return(\@triples);
160             }
161              
162              
163             =head3 $model->objects_matching($subject,$predicate,$only)
164              
165             Return an array of objects from triples where the subject and predicate
166             are as specified. Will return an empty array if there are no matches.
167              
168             If $only is not specified then the objects matching will be returned.
169              
170             If $only is RESOURCE then only resource labels will be included, if
171             LITERAL then only literal labels will be returned.
172              
173             =cut
174              
175             sub objects_matching {
176             my $self=shift;
177             my ($subject,$predicate,$only)=@_;
178              
179             if (not defined($subject)) {
180             #empty list
181             return([]);
182             } elsif (not ref($subject)) {
183             $subject=RDF::Core::Resource->new($subject);
184             }
185             if (not defined($predicate)) {
186             #leave undef so we match any
187             } elsif (not ref($predicate)) {
188             $predicate=expand_qname($predicate);
189             $predicate=RDF::Core::Resource->new($predicate);
190             }
191              
192             my $enum=$self->getStmts($subject,$predicate,undef);
193             my $statement=$enum->getFirst();
194             my @matching=();
195             while ($statement) {
196             my $obj=$statement->getObject();
197             if ($only) {
198             if ($only==RESOURCE) {
199             next if ($obj->isLiteral());
200             } elsif ($only==LITERAL) {
201             next if (not $obj->isLiteral());
202             }
203             push(@matching,$obj->getLabel());
204             } else {
205             push(@matching,$obj);
206             }
207             $statement=$enum->getNext();
208             }
209             return(@matching);
210             }
211              
212              
213              
214             =head3 $model->literal_matching($subject,$predicate)
215              
216             Wrapper around objects_matching to get the first literal matching the
217             specified condition, else retursn undef. Ignores any other matches.
218              
219             =cut
220              
221             sub literal_matching {
222             my $self=shift;
223             my ($subject,$predicate)=@_;
224             my @objects=$self->objects_matching($subject,$predicate,LITERAL);
225             return(@objects ? $objects[0] : undef );
226             }
227              
228              
229              
230             # Return URI or literal based on whether string looks like a URI
231             #
232             sub __uri_or_literal {
233             my ($str)=@_;
234             my $ul;
235             if ($str=~/^[a-z]+:\S+$/) {
236             $ul=RDF::Core::Resource->new($str);
237             } else {
238             $ul=RDF::Core::Literal->new($str);
239             }
240             return($ul);
241             }
242              
243              
244             =head2 VALIDATION
245              
246             =head3 $model->check_model($uri_rem,$rem)
247              
248             Take an RDF model of type RDF::Core::Model in $self and a Resource
249             Map URI $uri_rem. Attempt to parse/interpret it as a resource map. Will
250             croak if parsing fails so usual call would be to wrap in an eval:
251              
252             eval {
253             $model->check_model($uri_rem,$rem);
254             };
255             if ($@) {
256             # oops
257             }
258              
259             If $rem is supplied then this is expected to be a SemanticWeb::OAI::ORE::ReM object
260             with methods uri(), aggregation(), creator() and
261             timestamp_as_iso8601() which are used to set these values for easy reference.
262              
263             The requirements are based mainly on the table given in
264             L.
265              
266             =cut
267              
268             sub check_model {
269             my $self=shift;
270             my ($uri_rem,$rem)=@_;
271              
272             my $resource_map=RDF::Core::Resource->new(RESOURCE_MAP);
273             my $aggregation=RDF::Core::Resource->new(AGGREGATION);
274             my $has_type=RDF::Core::Resource->new(HAS_TYPE);
275             my $describes=RDF::Core::Resource->new(DESCRIBES);
276             my $aggregates=RDF::Core::Resource->new(AGGREGATES);
277              
278             # First, work out what the Resource Map URI (URI-R) is
279             {
280             my $statement=undef;
281             my $uri=undef;
282             my $cnt=$self->countStmts(undef,$has_type,$resource_map);
283             if ($cnt==0) {
284             $self->err(FATAL,"No resource map node defined as such and not URI-R supplied") if (not defined $uri_rem);
285             #if FATAL turned off or $uri_rem supplied then just assume $uri_rem as given
286             $self->err(WARN,"Using supplied URI-R ($uri_rem) as resource map URI") if (defined $uri_rem);
287             $uri=$uri_rem;
288             } elsif ($cnt==1) {
289             my $enum=$self->getStmts(undef,$has_type,$resource_map);
290             $statement=$enum->getFirst;
291             $enum->close();
292             $uri=$statement->getSubject->getURI;
293             } else {
294             # more than one match, can't handle that yet so barf.
295             # can probably work it out by looking for an AGGREGATES arc from the same Subject
296             $self->err(FATAL,"Got $cnt candidates for resourceMap node");
297             return(0); #if FATAL turned off
298             }
299             # Only get here if we found $statement and extracted $uri
300             if (defined $rem) {
301             $rem->uri($uri);
302             }
303             if (defined $uri_rem and $uri_rem ne $uri) {
304             $self->err(WARN,"URI for ReM supplied ($uri_rem) but does not match that inside object ($uri)");
305             }
306             $uri_rem=$uri;
307             }
308              
309             # Second, work out what the Aggregation URI (URI-A) is. First look for a DESCRIBES
310             # predicate, look for a node typed as an aggregation if that fails.
311             my $uri_agg=undef;
312             {
313             my $statement=undef;
314             my $rem_resource=RDF::Core::Resource->new($uri_rem);
315             my $cnt=$self->countStmts($rem_resource,$describes,undef);
316             if ($cnt==1) {
317             my $enum=$self->getStmts($rem_resource,$describes,undef);
318             $statement=$enum->getFirst();
319             $enum->close();
320             $uri_agg=$statement->getObject()->getURI();
321             } elsif ($cnt==0) {
322             # Any describes statement..
323             my $cnt=$self->countStmts(undef,$describes,undef);
324             if ($cnt==1) {
325             my $enum=$self->getStmts(undef,$describes,undef);
326             $statement=$enum->getFirst();
327             $enum->close();
328             $uri_agg=$statement->getObject()->getURI();
329             }
330             }
331             # If that did not work, try typed node
332             if (not defined $uri_agg) {
333             my $cnt=$self->countStmts(undef,$has_type,$aggregation);
334             if ($cnt==0) {
335             $self->err(FATAL,"Failed to find an Aggregation node!");
336             return(0);
337             } elsif ($cnt==1) {
338             my $enum=$self->getStmts(undef,$has_type,$aggregation);
339             $statement=$enum->getFirst();
340             $enum->close();
341             $uri_agg=$statement->getSubject()->getURI();
342             } else {
343             # more than one match, can't handle that yet so barf.
344             $self->err(FATAL,"Got $cnt candidates for Aggregation node");
345             return(0);
346             }
347             }
348             # Only get here if we found $statement and extracted $uri_agg, record
349             # in model.
350             if (defined $rem) {
351             $rem->aggregation($uri_agg);
352             }
353             }
354              
355             # Now look for $uri_agg AGGREGATES statements and add to
356             # the list of aggregated resources
357             my $uri_agg_resource=RDF::Core::Resource->new($uri_agg);
358             {
359             my $cnt=$self->countStmts($uri_agg_resource,$aggregates,undef);
360             if ($cnt==0) {
361             $self->err(WARN,"No resources aggregated by Aggregation $uri_agg. This is legal but perhaps not what is intended.");
362             } else {
363             carp "Found $cnt aggregated resources" if ($self->{debug});
364             }
365             }
366              
367             # Now look for essential metadata: creator and modified
368             {
369             if (scalar($self->creators($uri_rem))==0) {
370             $self->err(FATAL,"Resource map must have at least one ".CREATOR);
371             return(0);
372             }
373             }
374              
375             my $uri_rem_resource=RDF::Core::Resource->new($uri_rem);
376             if (my $timestamp=$self->get_timestamp($uri_rem_resource,1)) {
377             $rem->timestamp_as_iso8601(MODIFIED,$timestamp);
378             } else {
379             # Will have already thrown error
380             return(0);
381             }
382            
383             return(1);
384             }
385              
386              
387             =head3 $model->creators($uri)
388              
389             Find all the CREATOR objects (resources or literals) for $uri.
390              
391             =cut
392              
393             sub creators {
394             my $self=shift;
395             my ($uri_rem)=@_;
396             my $uri_rem_resource=RDF::Core::Resource->new($uri_rem);
397             return($self->objects_matching($uri_rem_resource,CREATOR));
398             }
399              
400              
401             =head3 $model->get_timestamp($uri_rem,$throw_error)
402              
403             Return timestamp literal associated with $uri_rem. There must be
404             just one otherwise nothing (error) will be returned.
405              
406             =cut
407              
408             sub get_timestamp {
409             my $self=shift;
410             my ($uri_rem,$throw_error)=@_;
411             my @timestamps=$self->objects_matching($uri_rem,MODIFIED);
412             if (scalar(@timestamps)!=1) {
413             if ($throw_error) {
414             $self->err(FATAL,"Resource map must have one and only one ".MODIFIED);
415             }
416             return();
417             }
418             my $timestamp=$timestamps[0];
419             if (not $timestamp->isLiteral()) {
420             if ($throw_error) {
421             $self->err(FATAL,"Resource map timestamp must be a literal value");
422             }
423             return();
424             }
425             return($timestamp->getLabel());
426             }
427              
428              
429             =head3 $model->err($level,$msg)
430              
431             Error handling. Will use similar error method of $self->{errobj} if
432             that is set. Otherwise handles here.
433              
434             =cut
435              
436             sub err {
437             my $self=shift;
438             if ($self->{errobj}) {
439             return($self->{errobj}->err(@_));
440             }
441             my ($level,$msg)=@_;
442             if ($level>=$self->die_level) {
443             croak "ERROR: $msg";
444             }
445             $self->add_errstr($msg);
446             }
447              
448              
449             =head2 INTROSPECTION
450              
451             These routines support examination of the model to pull out key reference
452             points and information such as the Resource Map URI or the Aggregation URI.
453              
454             =head3 $model->find_rem
455              
456             Attempt to find the Resource Map. Returns the appropriate Resource object
457             if successful, nothing otherwise.
458              
459             =cut
460              
461             sub find_rem {
462             my $self=shift;
463              
464             my $rem=undef;
465              
466             my $resource_map=RDF::Core::Resource->new(RESOURCE_MAP);
467             my $has_type=RDF::Core::Resource->new(HAS_TYPE);
468             my $enum=$self->getStmts(undef,$has_type,$resource_map);
469             if (my $statement=$enum->getFirst) {
470             # If more than one match, recklessly pick the 'first'
471             # FIXME - could look for one with describes link
472             $rem=$statement->getSubject;
473             $enum->close;
474             } else {
475             # None found from that test, try looking for something
476             # that DESCRIBES
477             my $describes=RDF::Core::Resource->new(DESCRIBES);
478             my $cnt=$self->countStmts(undef,$describes,undef);
479             if ($cnt==1) {
480             # Just one so we take it
481             $enum=$self->getStmts(undef,$describes,undef);
482             $rem=$enum->getFirst->getSubject;
483             $enum->close;
484             } else {
485             # FIXME - look for one with other matches
486             }
487             }
488              
489             # Now have Resource in $rem if we found it
490             return( $rem || () );
491             }
492              
493              
494             =head3 $model->find_rem_uri(%opts)
495              
496             Wrapper around $model->find_rem that returns a URI on
497             success, nothing otherwise.
498              
499             =cut
500              
501             sub find_rem_uri {
502             my $self=shift;
503             my $agg=$self->find_rem(@_);
504             return($agg ? $agg->getURI : () );
505             }
506              
507              
508             =head3 $model->find_aggregation(%opts)
509              
510             Find the Aggregation in this Resource Map. Returns the appropriate
511             Resource object if successful, nothing otherwise.
512              
513             Valid options are:
514              
515             uri_rem -> Resurce Map URI,
516              
517             =cut
518              
519             sub find_aggregation {
520             my $self=shift;
521              
522             my $agg=undef;
523              
524             my $aggregation=RDF::Core::Resource->new(AGGREGATION);
525             my $has_type=RDF::Core::Resource->new(HAS_TYPE);
526             my $cnt=$self->countStmts(undef,$has_type,$aggregation);
527             if ($cnt==1) {
528             my $enum=$self->getStmts(undef,$has_type,$aggregation);
529             $agg=$enum->getFirst->getSubject;
530             $enum->close;
531             } elsif ($cnt>1) {
532             # FIXME - do something smarter than taking the first
533             my $enum=$self->getStmts(undef,$has_type,$aggregation);
534             $agg=$enum->getFirst->getSubject;
535             $enum->close;
536             } else { # ($cnt==0)
537             # None found from that test, try looking for something
538             # that the rem DESCRIBES
539             my $describes=RDF::Core::Resource->new(DESCRIBES);
540             my $cnt=$self->countStmts(undef,$describes,undef);
541             if ($cnt==1) {
542             # Just one so we take it
543             my $enum=$self->getStmts(undef,$describes,undef);
544             $agg=$enum->getFirst->getObject;
545             $enum->close;
546             } else {
547             # FIXME - look for one with other matches
548             }
549             # ???
550             }
551              
552             return( $agg || () );
553             }
554              
555              
556             =head3 $model->find_aggregation_uri(%opts)
557              
558             Wrapper around $model->find_aggregation that returns a URI on
559             success, nothing otherwise.
560              
561             =cut
562              
563             sub find_aggregation_uri {
564             my $self=shift;
565             my $agg=$self->find_aggregation(@_);
566             return($agg ? $agg->getURI : () );
567             }
568              
569              
570             =head2 DATA DUMP
571              
572             These are low-level data dump methods. It is expected that normally
573             the methods provided via L::serialize will be used.
574              
575             =head3 $model->as_n3($unsorted)
576              
577             Very simple dump of this object as N3. No prefixes are used and the triples
578             are sorted alphabetically by line unless $unsorted is set true (in which case
579             the output will be essentially random).
580              
581             See L for "pretty printing" methods.
582              
583             =cut
584              
585             sub as_n3 {
586             my $self=shift;
587             my ($unsorted)=@_;
588              
589             my @triples=();
590             my $enum=$self->getStmts(undef,undef,undef);
591             my $statement=$enum->getFirst();
592             while ($statement) {
593             my $subject='<'.$statement->getSubject()->getLabel().'>';
594             my $predicate='<'.$statement->getPredicate()->getLabel().'>';
595             my $obj=$statement->getObject();
596             my $object=$obj->getLabel();
597             if ($obj->isa('RDF::Core::Resource')) {
598             $object='<'.$object.'>';
599             } else {
600             $object='"'.SemanticWeb::OAI::ORE::N3::_n3_escape($object).'"';
601             }
602             push(@triples,"$subject $predicate $object.\n");
603             $statement=$enum->getNext();
604             }
605              
606             my $str="# Dump of OAI-ORE Resource Map model as N3\n";
607             if ($unsorted) {
608             $str.=join('',@triples);
609             } else {
610             $str.=join('',sort(@triples));
611             }
612             return($str);
613             }
614              
615              
616             =head3 $model->as_rdfxml
617              
618             Simple RDF XML dump, returns string. For more sophisticated output
619             see L.
620              
621             =cut
622              
623             sub as_rdfxml {
624             my $self=shift;
625             my $xml = '';
626             my $serializer = new RDF::Core::Model::Serializer(Model=>$self,
627             Output=>\$xml,
628             BaseURI => 'http://example.com/',
629             );
630             $serializer->serialize;
631             return($xml);
632             }
633              
634              
635             =head1 SEE ALSO
636              
637             L
638              
639             =head1 AUTHOR
640              
641             Simeon Warner
642              
643             =head1 LICENSE AND COPYRIGHT
644              
645             Copyright 2007-2010 Simeon Warner.
646              
647             This module is free software; you can redistribute it and/or
648             modify it under the same terms as Perl itself. See L.
649              
650             =cut
651              
652             1;