redlander 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +59 -0
- data/Rakefile +5 -0
- data/ext/README +1 -0
- data/ext/extconf.rb +17 -0
- data/ext/redland-pre.i +6 -0
- data/ext/redland-types.i +9 -0
- data/ext/redland_wrap.c +8052 -0
- data/lib/redlander.rb +61 -0
- data/lib/redlander/error_container.rb +41 -0
- data/lib/redlander/model.rb +33 -0
- data/lib/redlander/model_proxy.rb +85 -0
- data/lib/redlander/node.rb +90 -0
- data/lib/redlander/parser.rb +92 -0
- data/lib/redlander/parser_proxy.rb +18 -0
- data/lib/redlander/serializer.rb +80 -0
- data/lib/redlander/statement.rb +153 -0
- data/lib/redlander/statement_iterator.rb +37 -0
- data/lib/redlander/storage.rb +79 -0
- data/lib/redlander/version.rb +3 -0
- data/spec/redlander/model_spec.rb +239 -0
- data/spec/redlander/node_spec.rb +85 -0
- data/spec/redlander/parser_spec.rb +96 -0
- data/spec/redlander/serializer_spec.rb +52 -0
- data/spec/redlander/statement_spec.rb +86 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- metadata +127 -0
@@ -0,0 +1,153 @@
|
|
1
|
+
module Redlander
|
2
|
+
|
3
|
+
class Statement
|
4
|
+
|
5
|
+
include ErrorContainer
|
6
|
+
|
7
|
+
attr_reader :rdf_statement
|
8
|
+
|
9
|
+
# Create an RDF statement.
|
10
|
+
# Options are:
|
11
|
+
# :subject
|
12
|
+
# :predicate
|
13
|
+
# :object
|
14
|
+
def initialize(options = {})
|
15
|
+
@rdf_statement = if options.is_a?(SWIG::TYPE_p_librdf_statement_s)
|
16
|
+
# A special case, where you can pass an instance of SWIG::TYPE_p_librdf_statement_s
|
17
|
+
# in order to create a Statement from an internal RDF statement representation.
|
18
|
+
options
|
19
|
+
else
|
20
|
+
s = options[:subject] && Node.new(options[:subject]).rdf_node
|
21
|
+
p = options[:predicate] && Node.new(options[:predicate]).rdf_node
|
22
|
+
o = options[:object] && Node.new(options[:object]).rdf_node
|
23
|
+
Redland.librdf_new_statement_from_nodes(Redlander.rdf_world, s, p, o)
|
24
|
+
end
|
25
|
+
|
26
|
+
raise RedlandError.new("Failed to create a new statement") unless @rdf_statement
|
27
|
+
ObjectSpace.define_finalizer(@rdf_statement, proc { Redland.librdf_free_statement(@rdf_statement) })
|
28
|
+
end
|
29
|
+
|
30
|
+
def subject
|
31
|
+
rdf_node = Redland.librdf_statement_get_subject(@rdf_statement)
|
32
|
+
rdf_node && Node.new(rdf_node)
|
33
|
+
end
|
34
|
+
|
35
|
+
def predicate
|
36
|
+
rdf_node = Redland.librdf_statement_get_predicate(@rdf_statement)
|
37
|
+
rdf_node && Node.new(rdf_node)
|
38
|
+
end
|
39
|
+
|
40
|
+
def object
|
41
|
+
rdf_node = Redland.librdf_statement_get_object(@rdf_statement)
|
42
|
+
rdf_node && Node.new(rdf_node)
|
43
|
+
end
|
44
|
+
|
45
|
+
# set the subject of the statement
|
46
|
+
def subject=(node)
|
47
|
+
Redland.librdf_statement_set_subject(@rdf_statement, rdf_node_from(node))
|
48
|
+
end
|
49
|
+
|
50
|
+
# set the predicate of the statement
|
51
|
+
def predicate=(node)
|
52
|
+
Redland.librdf_statement_set_predicate(@rdf_statement, rdf_node_from(node))
|
53
|
+
end
|
54
|
+
|
55
|
+
# set the object of the statement
|
56
|
+
def object=(node)
|
57
|
+
Redland.librdf_statement_set_object(@rdf_statement, rdf_node_from(node))
|
58
|
+
end
|
59
|
+
|
60
|
+
def model
|
61
|
+
@model
|
62
|
+
end
|
63
|
+
|
64
|
+
# Add the statement to the given model.
|
65
|
+
#
|
66
|
+
# Returns the model on success, or nil.
|
67
|
+
# NOTE: Duplicate statements are not added to the model.
|
68
|
+
# However, this doesn't result in an error here.
|
69
|
+
def model=(model)
|
70
|
+
if model.nil?
|
71
|
+
@model = nil
|
72
|
+
else
|
73
|
+
if self.valid?
|
74
|
+
if Redland.librdf_model_add_statement(model.rdf_model, @rdf_statement).zero?
|
75
|
+
@model = model
|
76
|
+
else
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Destroy the statement (remove it from the model, if possible).
|
84
|
+
#
|
85
|
+
# Returns true if successfully removed from the model, or false.
|
86
|
+
# If the statement is not bound to a model, false is returned.
|
87
|
+
def destroy
|
88
|
+
if @model
|
89
|
+
if Redland.librdf_model_remove_statement(@model.rdf_model, @rdf_statement).zero?
|
90
|
+
self.model = nil
|
91
|
+
true
|
92
|
+
else
|
93
|
+
false
|
94
|
+
end
|
95
|
+
else
|
96
|
+
false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def eql?(other_statement)
|
101
|
+
model == other_statement.model &&
|
102
|
+
subject == other_statement.subject &&
|
103
|
+
predicate == other_statement.predicate &&
|
104
|
+
object == other_statement.object
|
105
|
+
end
|
106
|
+
alias_method :==, :eql?
|
107
|
+
|
108
|
+
def hash
|
109
|
+
self.class.hash + to_s.hash
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_s
|
113
|
+
Redland.librdf_statement_to_string(@rdf_statement)
|
114
|
+
end
|
115
|
+
|
116
|
+
# A valid statement satisfies the following:
|
117
|
+
# URI or blank subject, URI predicate and URI or blank or literal object (i.e. anything).
|
118
|
+
def valid?
|
119
|
+
if is_valid = attributes_satisfy?
|
120
|
+
errors.clear
|
121
|
+
else
|
122
|
+
errors.add("is invalid")
|
123
|
+
end
|
124
|
+
is_valid
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def rdf_node_from(node)
|
131
|
+
if node.nil?
|
132
|
+
nil
|
133
|
+
else
|
134
|
+
# According to Redland docs,
|
135
|
+
# the node here becomes a part of the statement
|
136
|
+
# and must not be used by the caller!
|
137
|
+
if node.frozen?
|
138
|
+
raise RedlandError.new("The node is already bound to a statement and cannot be added.")
|
139
|
+
else
|
140
|
+
node.freeze.rdf_node
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def attributes_satisfy?
|
146
|
+
!subject.nil? && (subject.resource? || subject.blank?) &&
|
147
|
+
!predicate.nil? && predicate.resource? &&
|
148
|
+
!object.nil?
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Redlander
|
2
|
+
|
3
|
+
# Wrapper for a librdf_stream object.
|
4
|
+
module StatementIterator
|
5
|
+
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# Iterate over statements in the stream.
|
9
|
+
def each
|
10
|
+
# TODO: The options specify matching criteria: subj, pred, obj;
|
11
|
+
# if an option is not specified, it matches any value,
|
12
|
+
# so with no options given, all statements will be returned.
|
13
|
+
if block_given?
|
14
|
+
while Redland.librdf_stream_end(@rdf_stream).zero?
|
15
|
+
yield current
|
16
|
+
Redland.librdf_stream_next(@rdf_stream).zero?
|
17
|
+
end
|
18
|
+
else
|
19
|
+
raise ::LocalJumpError.new("no block given")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Get the current Statement in the stream.
|
27
|
+
def current
|
28
|
+
rdf_statement = Redland.librdf_stream_get_object(@rdf_stream)
|
29
|
+
statement = Statement.new(rdf_statement)
|
30
|
+
# not using Statement#model= in order to avoid re-adding the statement to the model
|
31
|
+
statement.instance_variable_set(:@model, @model)
|
32
|
+
statement
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Redlander
|
2
|
+
|
3
|
+
module Storage
|
4
|
+
|
5
|
+
VALID_STORAGE_TYPES = [:memory, :hashes, :file, :uri, :tstore, :mysql, :sqlite, :postgresql]
|
6
|
+
|
7
|
+
# Creates a store of the given type
|
8
|
+
#
|
9
|
+
# Store types (:storage option) are:
|
10
|
+
# :memory
|
11
|
+
# :hashes
|
12
|
+
# :file - memory model initialized from RDF/XML file
|
13
|
+
# :uri - read-only memory model with URI provided in 'name' arg
|
14
|
+
# :mysql
|
15
|
+
# :sqlite
|
16
|
+
# :postgresql
|
17
|
+
# :tstore
|
18
|
+
# Options are:
|
19
|
+
# :name - ?
|
20
|
+
# :host - database host name (for store types: :postgres, :mysql, :tstore)
|
21
|
+
# :port - database host port (for store types: :postgres, :mysql, :tstore)
|
22
|
+
# :database - database name (for store types: :postgres, :mysql, :tstore)
|
23
|
+
# :user - database user name (for store types: :postgres, :mysql, :tstore)
|
24
|
+
# :password - database user password (for store types: :postgres, :mysql, :tstore)
|
25
|
+
# :hash_type - hash type (for store types: :bdb)
|
26
|
+
# can be either 'memory' or 'bdb'
|
27
|
+
# :new - force creation of a new store
|
28
|
+
# :dir - directory path (for store types: :hashes)
|
29
|
+
# :contexts - support contexts (for store types: :hashes, :memory)
|
30
|
+
# :write - allow writing data to the store (for store types: :hashes)
|
31
|
+
#
|
32
|
+
# NOTE: When dealing with databases,
|
33
|
+
# Redland (1.0.7) just crashes when the required tables aren't available!
|
34
|
+
def self.initialize_storage(options = {})
|
35
|
+
storage_type, storage_options = split_options(options)
|
36
|
+
storage_type ||= :memory
|
37
|
+
|
38
|
+
unless VALID_STORAGE_TYPES.include?(storage_type)
|
39
|
+
raise RedlandError.new("Unknown storage type: #{storage_type}")
|
40
|
+
end
|
41
|
+
|
42
|
+
rdf_storage = Redland.librdf_new_storage(Redlander.rdf_world,
|
43
|
+
storage_type.to_s,
|
44
|
+
storage_options.delete(:name).to_s,
|
45
|
+
Redlander.to_rdf_options(storage_options))
|
46
|
+
raise RedlandError.new("Failed to initialize storage") unless rdf_storage
|
47
|
+
ObjectSpace.define_finalizer(rdf_storage, proc { Redland.librdf_free_storage(rdf_storage) })
|
48
|
+
|
49
|
+
rdf_storage
|
50
|
+
end
|
51
|
+
|
52
|
+
# Wrap changes to the given model in a transaction.
|
53
|
+
# If an exception is raised in the block, the transaction is rolled back.
|
54
|
+
# (Does not work for all storages, in which case the changes are instanteous).
|
55
|
+
def self.transaction(model, &block)
|
56
|
+
Redland.librdf_model_transaction_start(model.rdf_model).zero? || RedlandError.new("Failed to initialize a transaction")
|
57
|
+
block.call
|
58
|
+
Redland.librdf_model_transaction_commit(model.rdf_model).zero? || RedlandError.new("Failed to commit the transaction")
|
59
|
+
rescue
|
60
|
+
rollback(model)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Rollback a latest transaction for the given model.
|
64
|
+
def self.rollback(model)
|
65
|
+
Redland.librdf_model_transaction_rollback(model.rdf_model).zero? || RedlandError.new("Failed to rollback the latest transaction")
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def self.split_options(options = {})
|
72
|
+
storage_options = options.dup
|
73
|
+
storage_type = storage_options.delete(:storage)
|
74
|
+
[storage_type, storage_options]
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Model do
|
4
|
+
|
5
|
+
it "should be created with default options" do
|
6
|
+
lambda { Model.new }.should_not raise_exception
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "statements" do
|
10
|
+
|
11
|
+
before :each do
|
12
|
+
@model = Model.new
|
13
|
+
end
|
14
|
+
|
15
|
+
it do
|
16
|
+
@model.statements.should be_an_instance_of(ModelProxy)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should be created in the model" do
|
20
|
+
lambda {
|
21
|
+
@model.statements.create(statement_attributes)
|
22
|
+
}.should change(@model.statements, :size).by(1)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should be bound to the model" do
|
26
|
+
statement = @model.statements.create(statement_attributes)
|
27
|
+
statement.model.should be(@model)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should be iterated over" do
|
31
|
+
statement = @model.statements.create(statement_attributes)
|
32
|
+
statements = []
|
33
|
+
lambda {
|
34
|
+
@model.statements.each do |s|
|
35
|
+
statements << s
|
36
|
+
end
|
37
|
+
}.should change(statements, :size).by(1)
|
38
|
+
statements.first.should eql(statement)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should be added to the model" do
|
42
|
+
statement = Statement.new(statement_attributes)
|
43
|
+
lambda {
|
44
|
+
lambda {
|
45
|
+
@model.statements.add(statement)
|
46
|
+
}.should change(@model.statements, :size).by(1)
|
47
|
+
}.should change(statement, :model).from(nil).to(@model)
|
48
|
+
@model.statements.should include(statement)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should be found without a block" do
|
52
|
+
statement = @model.statements.create(statement_attributes)
|
53
|
+
@model.statements.find(:first).should eql(statement)
|
54
|
+
@model.statements.first.should eql(statement)
|
55
|
+
@model.statements.find(:all).should eql([statement])
|
56
|
+
@model.statements.all.should eql([statement])
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should be found with a block" do
|
60
|
+
statements = []
|
61
|
+
statement = @model.statements.create(statement_attributes)
|
62
|
+
lambda {
|
63
|
+
@model.statements.find(:all) do |st|
|
64
|
+
statements << st
|
65
|
+
end
|
66
|
+
}.should change(statements, :size).by(1)
|
67
|
+
statements.first.should eql(statement)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should be found by an attribute" do
|
71
|
+
statements = []
|
72
|
+
statement = @model.statements.create(statement_attributes)
|
73
|
+
lambda {
|
74
|
+
@model.statements.find(:all, :object => statement.object) do |st|
|
75
|
+
statements << st
|
76
|
+
end
|
77
|
+
}.should change(statements, :size).by(1)
|
78
|
+
statements.first.should eql(statement)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should not be found with a block" do
|
82
|
+
statements = []
|
83
|
+
statement = @model.statements.create(statement_attributes)
|
84
|
+
lambda {
|
85
|
+
@model.statements.find(:all, :object => "another object") do |st|
|
86
|
+
statements << st
|
87
|
+
end
|
88
|
+
}.should_not change(statements, :size)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should be removed from the model" do
|
92
|
+
statement = @model.statements.create(statement_attributes)
|
93
|
+
lambda {
|
94
|
+
statement.destroy
|
95
|
+
}.should change(@model.statements, :size).by(-1)
|
96
|
+
@model.statements.should_not include(statement)
|
97
|
+
statement.model.should be_nil
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def statement_attributes
|
104
|
+
s = URI.parse('http://example.com/concepts#subject')
|
105
|
+
p = URI.parse('http://example.com/concepts#label')
|
106
|
+
o = "subject!"
|
107
|
+
{
|
108
|
+
:subject => s,
|
109
|
+
:predicate => p,
|
110
|
+
:object => o
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "serialization" do
|
117
|
+
|
118
|
+
before :each do
|
119
|
+
@model = Model.new
|
120
|
+
s = URI.parse("http://example.com/concepts#two-dimensional_seismic_imaging")
|
121
|
+
p = URI.parse("http://www.w3.org/2000/01/rdf-schema#label")
|
122
|
+
o = "2-D seismic imaging@en"
|
123
|
+
@model.statements.create(:subject => s, :predicate => p, :object => o)
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should produce RDF/XML content" do
|
127
|
+
content = @model.to_rdfxml
|
128
|
+
content.should be_an_instance_of(String)
|
129
|
+
content.should include('2-D seismic imaging')
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should produce N-Triples content" do
|
133
|
+
content = @model.to_ntriples
|
134
|
+
content.should be_an_instance_of(String)
|
135
|
+
content.should include('2-D seismic imaging@en')
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should produce Turtle content" do
|
139
|
+
content = @model.to_turtle
|
140
|
+
content.should be_an_instance_of(String)
|
141
|
+
content.should include('2-D seismic imaging@en')
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should produce JSON content" do
|
145
|
+
content = @model.to_json
|
146
|
+
content.should be_an_instance_of(String)
|
147
|
+
content.should include('2-D seismic imaging@en')
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should produce DOT content" do
|
151
|
+
content = @model.to_dot
|
152
|
+
content.should be_an_instance_of(String)
|
153
|
+
content.should include('2-D seismic imaging@en')
|
154
|
+
end
|
155
|
+
|
156
|
+
describe "file source" do
|
157
|
+
|
158
|
+
before :each do
|
159
|
+
content = '<?xml version="1.0" encoding="utf-8"?><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="http://example.com/concepts#two-dimensional_seismic_imaging"><ns0:label xmlns:ns0="http://www.w3.org/2000/01/rdf-schema#" xml:lang="en">2-D seismic imaging</ns0:label></rdf:Description></rdf:RDF>'
|
160
|
+
reference_model = Model.new
|
161
|
+
reference_model.from_rdfxml(content, :base_uri => 'http://example.com/concepts')
|
162
|
+
reference_model.to_file(filename)
|
163
|
+
end
|
164
|
+
|
165
|
+
after :each do
|
166
|
+
cleanup
|
167
|
+
end
|
168
|
+
|
169
|
+
it "should be loaded from a file" do
|
170
|
+
lambda {
|
171
|
+
@model.from_file(filename, :base_uri => 'http://example.com/concepts')
|
172
|
+
}.should change(@model.statements, :size).by(1)
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
describe "file destination" do
|
178
|
+
|
179
|
+
before :each do
|
180
|
+
cleanup
|
181
|
+
end
|
182
|
+
|
183
|
+
after :each do
|
184
|
+
cleanup
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should produce a file" do
|
188
|
+
@model.to_file(filename)
|
189
|
+
File.should be_exists(filename)
|
190
|
+
File.size(filename).should_not be_zero
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
def cleanup
|
198
|
+
File.delete(filename) if File.exists?(filename)
|
199
|
+
end
|
200
|
+
|
201
|
+
def filename
|
202
|
+
"test_model.rdf"
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
describe "deserialization" do
|
208
|
+
|
209
|
+
before :each do
|
210
|
+
@model = Model.new
|
211
|
+
end
|
212
|
+
|
213
|
+
it "should be successful for RDF/XML data" do
|
214
|
+
content = '<?xml version="1.0" encoding="utf-8"?><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="http://example.com/concepts#two-dimensional_seismic_imaging"><ns0:label xmlns:ns0="http://www.w3.org/2000/01/rdf-schema#" xml:lang="en">2-D seismic imaging</ns0:label></rdf:Description></rdf:RDF>'
|
215
|
+
lambda {
|
216
|
+
@model.from_rdfxml(content, :base_uri => 'http://example.com/concepts')
|
217
|
+
}.should change(@model.statements, :size).by(1)
|
218
|
+
end
|
219
|
+
|
220
|
+
it "should be successful for N-Triples data" do
|
221
|
+
content = '<http://example.org/ns/a2> <http://example.org/ns/b2> <http://example.org/ns/c2> .'
|
222
|
+
lambda {
|
223
|
+
@model.from_ntriples(content)
|
224
|
+
}.should change(@model.statements, :size).by(1)
|
225
|
+
end
|
226
|
+
|
227
|
+
it "should be successful for Turtle data" do
|
228
|
+
content = '# this is a complete turtle document
|
229
|
+
@prefix foo: <http://example.org/ns#> .
|
230
|
+
@prefix : <http://other.example.org/ns#> .
|
231
|
+
foo:bar foo: : .'
|
232
|
+
lambda {
|
233
|
+
@model.from_turtle(content, :base_uri => 'http://example.com/concepts')
|
234
|
+
}.should change(@model.statements, :size).by(1)
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|