rdf-n3 3.0.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +198 -76
  3. data/UNLICENSE +1 -1
  4. data/VERSION +1 -1
  5. data/lib/rdf/n3/algebra/builtin.rb +79 -0
  6. data/lib/rdf/n3/algebra/formula.rb +446 -0
  7. data/lib/rdf/n3/algebra/list/append.rb +42 -0
  8. data/lib/rdf/n3/algebra/list/first.rb +24 -0
  9. data/lib/rdf/n3/algebra/list/in.rb +48 -0
  10. data/lib/rdf/n3/algebra/list/iterate.rb +96 -0
  11. data/lib/rdf/n3/algebra/list/last.rb +24 -0
  12. data/lib/rdf/n3/algebra/list/length.rb +24 -0
  13. data/lib/rdf/n3/algebra/list/member.rb +44 -0
  14. data/lib/rdf/n3/algebra/list_operator.rb +96 -0
  15. data/lib/rdf/n3/algebra/log/conclusion.rb +65 -0
  16. data/lib/rdf/n3/algebra/log/conjunction.rb +36 -0
  17. data/lib/rdf/n3/algebra/log/content.rb +34 -0
  18. data/lib/rdf/n3/algebra/log/dtlit.rb +41 -0
  19. data/lib/rdf/n3/algebra/log/equal_to.rb +34 -0
  20. data/lib/rdf/n3/algebra/log/implies.rb +102 -0
  21. data/lib/rdf/n3/algebra/log/includes.rb +70 -0
  22. data/lib/rdf/n3/algebra/log/langlit.rb +41 -0
  23. data/lib/rdf/n3/algebra/log/n3_string.rb +34 -0
  24. data/lib/rdf/n3/algebra/log/not_equal_to.rb +23 -0
  25. data/lib/rdf/n3/algebra/log/not_includes.rb +27 -0
  26. data/lib/rdf/n3/algebra/log/output_string.rb +40 -0
  27. data/lib/rdf/n3/algebra/log/parsed_as_n3.rb +36 -0
  28. data/lib/rdf/n3/algebra/log/semantics.rb +40 -0
  29. data/lib/rdf/n3/algebra/math/absolute_value.rb +36 -0
  30. data/lib/rdf/n3/algebra/math/acos.rb +26 -0
  31. data/lib/rdf/n3/algebra/math/acosh.rb +26 -0
  32. data/lib/rdf/n3/algebra/math/asin.rb +26 -0
  33. data/lib/rdf/n3/algebra/math/asinh.rb +26 -0
  34. data/lib/rdf/n3/algebra/math/atan.rb +26 -0
  35. data/lib/rdf/n3/algebra/math/atanh.rb +26 -0
  36. data/lib/rdf/n3/algebra/math/ceiling.rb +28 -0
  37. data/lib/rdf/n3/algebra/math/cos.rb +40 -0
  38. data/lib/rdf/n3/algebra/math/cosh.rb +38 -0
  39. data/lib/rdf/n3/algebra/math/difference.rb +40 -0
  40. data/lib/rdf/n3/algebra/math/equal_to.rb +54 -0
  41. data/lib/rdf/n3/algebra/math/exponentiation.rb +35 -0
  42. data/lib/rdf/n3/algebra/math/floor.rb +28 -0
  43. data/lib/rdf/n3/algebra/math/greater_than.rb +41 -0
  44. data/lib/rdf/n3/algebra/math/less_than.rb +41 -0
  45. data/lib/rdf/n3/algebra/math/negation.rb +38 -0
  46. data/lib/rdf/n3/algebra/math/not_equal_to.rb +25 -0
  47. data/lib/rdf/n3/algebra/math/not_greater_than.rb +25 -0
  48. data/lib/rdf/n3/algebra/math/not_less_than.rb +25 -0
  49. data/lib/rdf/n3/algebra/math/product.rb +20 -0
  50. data/lib/rdf/n3/algebra/math/quotient.rb +36 -0
  51. data/lib/rdf/n3/algebra/math/remainder.rb +35 -0
  52. data/lib/rdf/n3/algebra/math/rounded.rb +26 -0
  53. data/lib/rdf/n3/algebra/math/sin.rb +40 -0
  54. data/lib/rdf/n3/algebra/math/sinh.rb +38 -0
  55. data/lib/rdf/n3/algebra/math/sum.rb +40 -0
  56. data/lib/rdf/n3/algebra/math/tan.rb +40 -0
  57. data/lib/rdf/n3/algebra/math/tanh.rb +38 -0
  58. data/lib/rdf/n3/algebra/not_implemented.rb +13 -0
  59. data/lib/rdf/n3/algebra/resource_operator.rb +122 -0
  60. data/lib/rdf/n3/algebra/str/concatenation.rb +27 -0
  61. data/lib/rdf/n3/algebra/str/contains.rb +33 -0
  62. data/lib/rdf/n3/algebra/str/contains_ignoring_case.rb +33 -0
  63. data/lib/rdf/n3/algebra/str/ends_with.rb +33 -0
  64. data/lib/rdf/n3/algebra/str/equal_ignoring_case.rb +34 -0
  65. data/lib/rdf/n3/algebra/str/format.rb +17 -0
  66. data/lib/rdf/n3/algebra/str/greater_than.rb +38 -0
  67. data/lib/rdf/n3/algebra/str/less_than.rb +33 -0
  68. data/lib/rdf/n3/algebra/str/matches.rb +37 -0
  69. data/lib/rdf/n3/algebra/str/not_equal_ignoring_case.rb +17 -0
  70. data/lib/rdf/n3/algebra/str/not_greater_than.rb +17 -0
  71. data/lib/rdf/n3/algebra/str/not_less_than.rb +17 -0
  72. data/lib/rdf/n3/algebra/str/not_matches.rb +18 -0
  73. data/lib/rdf/n3/algebra/str/replace.rb +35 -0
  74. data/lib/rdf/n3/algebra/str/scrape.rb +35 -0
  75. data/lib/rdf/n3/algebra/str/starts_with.rb +33 -0
  76. data/lib/rdf/n3/algebra/time/day.rb +35 -0
  77. data/lib/rdf/n3/algebra/time/day_of_week.rb +27 -0
  78. data/lib/rdf/n3/algebra/time/gm_time.rb +29 -0
  79. data/lib/rdf/n3/algebra/time/hour.rb +35 -0
  80. data/lib/rdf/n3/algebra/time/in_seconds.rb +59 -0
  81. data/lib/rdf/n3/algebra/time/local_time.rb +29 -0
  82. data/lib/rdf/n3/algebra/time/minute.rb +35 -0
  83. data/lib/rdf/n3/algebra/time/month.rb +35 -0
  84. data/lib/rdf/n3/algebra/time/second.rb +35 -0
  85. data/lib/rdf/n3/algebra/time/timezone.rb +36 -0
  86. data/lib/rdf/n3/algebra/time/year.rb +29 -0
  87. data/lib/rdf/n3/algebra.rb +210 -0
  88. data/lib/rdf/n3/extensions.rb +221 -0
  89. data/lib/rdf/n3/format.rb +66 -1
  90. data/lib/rdf/n3/list.rb +630 -0
  91. data/lib/rdf/n3/reader.rb +774 -497
  92. data/lib/rdf/n3/reasoner.rb +282 -0
  93. data/lib/rdf/n3/refinements.rb +178 -0
  94. data/lib/rdf/n3/repository.rb +332 -0
  95. data/lib/rdf/n3/terminals.rb +78 -0
  96. data/lib/rdf/n3/vocab.rb +36 -3
  97. data/lib/rdf/n3/writer.rb +461 -250
  98. data/lib/rdf/n3.rb +11 -8
  99. metadata +177 -49
  100. data/AUTHORS +0 -1
  101. data/History.markdown +0 -99
  102. data/lib/rdf/n3/patches/array_hacks.rb +0 -53
  103. data/lib/rdf/n3/reader/meta.rb +0 -641
  104. data/lib/rdf/n3/reader/parser.rb +0 -237
data/lib/rdf/n3/writer.rb CHANGED
@@ -1,42 +1,39 @@
1
1
  # coding: utf-8
2
2
  module RDF::N3
3
3
  ##
4
- # A Turtle serialiser in Ruby
4
+ # A Notation-3 serialiser in Ruby
5
5
  #
6
6
  # Note that the natural interface is to write a whole graph at a time.
7
7
  # Writing statements or Triples will create a graph to add them to
8
8
  # and then serialize the graph.
9
9
  #
10
- # @example Obtaining a Turtle writer class
10
+ # @example Obtaining a N3 writer class
11
11
  # RDF::Writer.for(:n3) #=> RDF::N3::Writer
12
12
  # RDF::Writer.for("etc/test.n3")
13
- # RDF::Writer.for("etc/test.ttl")
14
13
  # RDF::Writer.for(file_name: "etc/test.n3")
15
- # RDF::Writer.for(file_name: "etc/test.ttl")
16
14
  # RDF::Writer.for(file_extension: "n3")
17
- # RDF::Writer.for(file_extension: "ttl")
18
15
  # RDF::Writer.for(content_type: "text/n3")
19
16
  #
20
- # @example Serializing RDF graph into an Turtle file
17
+ # @example Serializing RDF graph into an N3 file
21
18
  # RDF::N3::Writer.open("etc/test.n3") do |writer|
22
19
  # writer << graph
23
20
  # end
24
21
  #
25
- # @example Serializing RDF statements into an Turtle file
22
+ # @example Serializing RDF statements into an N3 file
26
23
  # RDF::N3::Writer.open("etc/test.n3") do |writer|
27
24
  # graph.each_statement do |statement|
28
25
  # writer << statement
29
26
  # end
30
27
  # end
31
28
  #
32
- # @example Serializing RDF statements into an Turtle string
29
+ # @example Serializing RDF statements into an N3 string
33
30
  # RDF::N3::Writer.buffer do |writer|
34
31
  # graph.each_statement do |statement|
35
32
  # writer << statement
36
33
  # end
37
34
  # end
38
35
  #
39
- # The writer will add prefix definitions, and use them for creating @prefix definitions, and minting QNames
36
+ # The writer will add prefix definitions, and use them for creating @prefix definitions, and minting pnames
40
37
  #
41
38
  # @example Creating @base and @prefix definitions in output
42
39
  # RDF::N3::Writer.buffer(base_uri: "http://example.com/", prefixes: {
@@ -52,12 +49,17 @@ module RDF::N3
52
49
  class Writer < RDF::Writer
53
50
  format RDF::N3::Format
54
51
  include RDF::Util::Logger
55
- QNAME = Meta::REGEXPS[:"http://www.w3.org/2000/10/swap/grammar/n3#qname"]
52
+ include Terminals
53
+ using Refinements
56
54
 
57
- # @return [Graph] Graph of statements serialized
55
+ # @return [RDF::Repository] Repository of statements serialized
56
+ attr_accessor :repo
57
+
58
+ # @return [RDF::Graph] Graph being serialized
58
59
  attr_accessor :graph
59
- # @return [URI] Base URI used for relativizing URIs
60
- attr_accessor :base_uri
60
+
61
+ # @return [Array<RDF::Node>] formulae names
62
+ attr_accessor :formula_names
61
63
 
62
64
  ##
63
65
  # N3 Writer options
@@ -78,7 +80,7 @@ module RDF::N3
78
80
  end
79
81
 
80
82
  ##
81
- # Initializes the Turtle writer instance.
83
+ # Initializes the N3 writer instance.
82
84
  #
83
85
  # @param [IO, File] output
84
86
  # the output stream
@@ -105,11 +107,15 @@ module RDF::N3
105
107
  # @yieldreturn [void]
106
108
  # @yield [writer]
107
109
  # @yieldparam [RDF::Writer] writer
108
- def initialize(output = $stdout, options = {}, &block)
110
+ def initialize(output = $stdout, **options, &block)
111
+ @repo = RDF::N3::Repository.new
112
+ @uri_to_pname = {}
113
+ @uri_to_prefix = {}
109
114
  super do
110
- @graph = RDF::Graph.new
111
- @uri_to_qname = {}
112
- @uri_to_prefix = {}
115
+ if base_uri
116
+ @uri_to_prefix[base_uri.to_s.end_with?('#', '/') ? base_uri : RDF::URI("#{base_uri}#")] = nil
117
+ end
118
+ reset
113
119
  if block_given?
114
120
  case block.arity
115
121
  when 0 then instance_eval(&block)
@@ -128,7 +134,19 @@ module RDF::N3
128
134
  # @raise [NotImplementedError] unless implemented in subclass
129
135
  # @abstract
130
136
  def write_triple(subject, predicate, object)
131
- @graph.insert(RDF::Statement(subject, predicate, object))
137
+ repo.insert(RDF::Statement(subject, predicate, object))
138
+ end
139
+
140
+ ##
141
+ # Adds a quad to be serialized
142
+ # @param [RDF::Resource] subject
143
+ # @param [RDF::URI] predicate
144
+ # @param [RDF::Value] object
145
+ # @param [RDF::Resource] graph_name
146
+ # @return [void]
147
+ def write_quad(subject, predicate, object, graph_name)
148
+ statement = RDF::Statement.new(subject, predicate, object, graph_name: graph_name)
149
+ repo.insert(statement)
132
150
  end
133
151
 
134
152
  ##
@@ -138,28 +156,51 @@ module RDF::N3
138
156
  # @see #write_triple
139
157
  def write_epilogue
140
158
  @max_depth = @options[:max_depth] || 3
141
- @base_uri = RDF::URI(@options[:base_uri])
142
159
 
143
160
  self.reset
144
161
 
145
- log_debug {"\nserialize: graph: #{@graph.size}"}
162
+ log_debug("\nserialize: repo:") {repo.size}
146
163
 
147
164
  preprocess
165
+
148
166
  start_document
149
167
 
150
- order_subjects.each do |subject|
151
- unless is_done?(subject)
152
- statement(subject)
168
+ @formula_names = repo.graph_names(unique: true)
169
+
170
+ with_graph(nil) do
171
+ count = 0
172
+ order_subjects.each do |subject|
173
+ unless is_done?(subject)
174
+ statement(subject, count)
175
+ count += 1
176
+ end
177
+ end
178
+
179
+ # Output any formulae not already serialized using owl:sameAs
180
+ formula_names.each do |graph_name|
181
+ next if graph_done?(graph_name)
182
+
183
+ # Add graph_name to @formulae
184
+ @formulae[graph_name] = true
185
+
186
+ log_debug {"formula(#{graph_name})"}
187
+ @output.write("\n#{indent}")
188
+ p_term(graph_name, :subject)
189
+ @output.write(" ")
190
+ predicate(RDF::OWL.sameAs)
191
+ @output.write(" ")
192
+ formula(graph_name, :graph_name)
193
+ @output.write(" .\n")
153
194
  end
154
195
  end
155
196
 
156
197
  super
157
198
  end
158
-
159
- # Return a QName for the URI, or nil. Adds namespace of QName to defined prefixes
199
+
200
+ # Return a pname for the URI, or nil. Adds namespace of pname to defined prefixes
160
201
  # @param [RDF::Resource] resource
161
202
  # @return [String, nil] value to use to identify URI
162
- def get_qname(resource)
203
+ def get_pname(resource)
163
204
  case resource
164
205
  when RDF::Node
165
206
  return options[:unique_bnodes] ? resource.to_unique_base : resource.to_base
@@ -169,56 +210,55 @@ module RDF::N3
169
210
  return nil
170
211
  end
171
212
 
172
- log_debug {"get_qname(#{resource}), std?}"}
173
- qname = case
174
- when @uri_to_qname.has_key?(uri)
175
- return @uri_to_qname[uri]
213
+ #log_debug {"get_pname(#{resource}), std?}"}
214
+ pname = case
215
+ when @uri_to_pname.key?(uri)
216
+ return @uri_to_pname[uri]
176
217
  when u = @uri_to_prefix.keys.detect {|u| uri.index(u.to_s) == 0}
177
218
  # Use a defined prefix
178
219
  prefix = @uri_to_prefix[u]
179
- prefix(prefix, u) unless u.to_s.empty? # Define for output
180
- log_debug {"get_qname: add prefix #{prefix.inspect} => #{u}"}
181
- uri.sub(u.to_s, "#{prefix}:")
220
+ unless u.to_s.empty?
221
+ prefix(prefix, u) unless u.to_s.empty?
222
+ #log_debug("get_pname") {"add prefix #{prefix.inspect} => #{u}"}
223
+ uri.sub(u.to_s, "#{prefix}:")
224
+ end
182
225
  when @options[:standard_prefixes] && vocab = RDF::Vocabulary.each.to_a.detect {|v| uri.index(v.to_uri.to_s) == 0}
183
226
  prefix = vocab.__name__.to_s.split('::').last.downcase
184
227
  @uri_to_prefix[vocab.to_uri.to_s] = prefix
185
228
  prefix(prefix, vocab.to_uri) # Define for output
186
- log_debug {"get_qname: add standard prefix #{prefix.inspect} => #{vocab.to_uri}"}
229
+ #log_debug {"get_pname: add standard prefix #{prefix.inspect} => #{vocab.to_uri}"}
187
230
  uri.sub(vocab.to_uri.to_s, "#{prefix}:")
188
231
  else
189
232
  nil
190
233
  end
191
-
192
- # Make sure qname is a valid qname
193
- if qname
194
- md = QNAME.match(qname)
195
- qname = nil unless md.to_s.length == qname.length
234
+
235
+ # Make sure pname is a valid pname
236
+ if pname
237
+ md = PNAME_LN.match(pname) || PNAME_NS.match(pname)
238
+ pname = nil unless md.to_s.length == pname.length
196
239
  end
197
240
 
198
- @uri_to_qname[uri] = qname
199
- rescue Addressable::URI::InvalidURIError => e
200
- raise RDF::WriterError, "Invalid URI #{resource.inspect}: #{e.message}"
241
+ @uri_to_pname[uri] = pname
201
242
  end
202
-
243
+
203
244
  # Take a hash from predicate uris to lists of values.
204
245
  # Sort the lists of values. Return a sorted list of properties.
205
- # @param [Hash{String => Array<Resource>}] properties A hash of Property to Resource mappings
206
- # @return [Array<String>}] Ordered list of properties. Uses predicate_order.
246
+ # @param [Hash{RDF::Term => Array<RDF::Term>}] properties A hash of Property to Resource mappings
247
+ # @return [Array<RDF::Term>}] Ordered list of properties. Uses predicate_order.
207
248
  def sort_properties(properties)
208
249
  # Make sorted list of properties
209
250
  prop_list = []
210
-
251
+
211
252
  predicate_order.each do |prop|
212
- next unless properties[prop.to_s]
213
- prop_list << prop.to_s
253
+ next unless properties.key?(prop)
254
+ prop_list << prop
214
255
  end
215
-
256
+
216
257
  properties.keys.sort.each do |prop|
217
- next if prop_list.include?(prop.to_s)
218
- prop_list << prop.to_s
258
+ next if prop_list.include?(prop)
259
+ prop_list << prop
219
260
  end
220
-
221
- log_debug {"sort_properties: #{prop_list.join(', ')}"}
261
+
222
262
  prop_list
223
263
  end
224
264
 
@@ -228,15 +268,32 @@ module RDF::N3
228
268
  # @param [RDF::Literal, String, #to_s] literal
229
269
  # @param [Hash{Symbol => Object}] options
230
270
  # @return [String]
231
- def format_literal(literal, options = {})
271
+ def format_literal(literal, **options)
232
272
  literal = literal.dup.canonicalize! if @options[:canonicalize]
233
273
  case literal
234
274
  when RDF::Literal
235
- case literal.datatype
236
- when RDF::XSD.boolean, RDF::XSD.integer, RDF::XSD.decimal
237
- literal.to_s
275
+ case literal.valid? ? literal.datatype : false
276
+ when RDF::XSD.boolean
277
+ %w(true false).include?(literal.value) ? literal.value : literal.canonicalize.to_s
278
+ when RDF::XSD.integer
279
+ literal.value.match?(/^[\+\-]?\d+$/) && !canonicalize? ? literal.value : literal.canonicalize.to_s
280
+ when RDF::XSD.decimal
281
+ literal.value.match?(/^[\+\-]?\d+\.\d+?$/) && !canonicalize? ?
282
+ literal.value :
283
+ literal.canonicalize.to_s
238
284
  when RDF::XSD.double
239
- literal.to_s.sub('E', 'e') # Favor lower case exponent
285
+ if literal.nan? || literal.infinite?
286
+ quoted(literal.value) + "^^#{format_uri(literal.datatype)}"
287
+ else
288
+ in_form = case literal.value
289
+ when /[\+\-]?\d+\.\d*E[\+\-]?\d+$/i then true
290
+ when /[\+\-]?\.\d+E[\+\-]?\d+$/i then true
291
+ when /[\+\-]?\d+E[\+\-]?\d+$/i then true
292
+ else false
293
+ end && !canonicalize?
294
+
295
+ in_form ? literal.value : literal.canonicalize.to_s.sub('E', 'e')
296
+ end
240
297
  else
241
298
  text = quoted(literal.value)
242
299
  text << "@#{literal.language}" if literal.has_language?
@@ -247,47 +304,47 @@ module RDF::N3
247
304
  quoted(literal.to_s)
248
305
  end
249
306
  end
250
-
307
+
251
308
  ##
252
- # Returns the Turtle/N3 representation of a URI reference.
309
+ # Returns the N3 representation of a URI reference.
253
310
  #
254
311
  # @param [RDF::URI] uri
255
312
  # @param [Hash{Symbol => Object}] options
256
313
  # @return [String]
257
- def format_uri(uri, options = {})
258
- md = relativize(uri)
259
- log_debug {"relativize(#{uri.inspect}) => #{md.inspect}"} if md != uri.to_s
260
- md != uri.to_s ? "<#{md}>" : (get_qname(uri) || "<#{uri}>")
314
+ def format_uri(uri, **options)
315
+ md = uri == base_uri ? '' : uri.relativize(base_uri)
316
+ log_debug("relativize") {"#{uri.to_sxp} => <#{md.inspect}>"} if md != uri.to_s
317
+ md != uri.to_s ? "<#{md}>" : (get_pname(uri) || "<#{uri}>")
261
318
  end
262
-
319
+
263
320
  ##
264
- # Returns the Turtle/N3 representation of a blank node.
321
+ # Returns the N3 representation of a blank node.
265
322
  #
266
323
  # @param [RDF::Node] node
267
324
  # @param [Hash{Symbol => Object}] options
268
325
  # @return [String]
269
- def format_node(node, options = {})
270
- options[:unique_bnodes] ? node.to_unique_base : node.to_base
326
+ def format_node(node, **options)
327
+ if node.id.match(/^([^_]+)_[^_]+_([^_]+)$/)
328
+ sn, seq = $1, $2.to_i
329
+ seq = nil if seq == 0
330
+ "_:#{sn}#{seq}"
331
+ elsif options[:unique_bnodes]
332
+ node.to_unique_base
333
+ else
334
+ node.to_base
335
+ end
271
336
  end
272
-
337
+
273
338
  protected
274
339
  # Output @base and @prefix definitions
275
340
  def start_document
276
- @output.write("#{indent}@base <#{base_uri}> .\n") unless base_uri.to_s.empty?
277
-
278
- log_debug {"start_document: #{prefixes.inspect}"}
341
+ @output.write("@base <#{base_uri}> .\n") unless base_uri.to_s.empty?
342
+
343
+ log_debug("start_document: prefixes") { prefixes.inspect}
279
344
  prefixes.keys.sort_by(&:to_s).each do |prefix|
280
- @output.write("#{indent}@prefix #{prefix}: <#{prefixes[prefix]}> .\n")
345
+ @output.write("@prefix #{prefix}: <#{prefixes[prefix]}> .\n")
281
346
  end
282
347
  end
283
-
284
- # If base_uri is defined, use it to try to make uri relative
285
- # @param [#to_s] uri
286
- # @return [String]
287
- def relativize(uri)
288
- uri = uri.to_s
289
- base_uri ? uri.sub(base_uri.to_s, "") : uri
290
- end
291
348
 
292
349
  # Defines rdf:type of subjects to be emitted at the beginning of the graph. Defaults to rdfs:Class
293
350
  # @return [Array<URI>]
@@ -296,8 +353,18 @@ module RDF::N3
296
353
  # Defines order of predicates to to emit at begninning of a resource description. Defaults to
297
354
  # [rdf:type, rdfs:label, dc:title]
298
355
  # @return [Array<URI>]
299
- def predicate_order; [RDF.type, RDF::RDFS.label, RDF::URI("http://purl.org/dc/terms/title")]; end
300
-
356
+ def predicate_order
357
+ [
358
+ RDF.type,
359
+ RDF::RDFS.label,
360
+ RDF::RDFS.comment,
361
+ RDF::URI("http://purl.org/dc/terms/title"),
362
+ RDF::URI("http://purl.org/dc/terms/description"),
363
+ RDF::OWL.sameAs,
364
+ RDF::N3::Log.implies
365
+ ]
366
+ end
367
+
301
368
  # Order subjects for output. Override this to output subjects in another order.
302
369
  #
303
370
  # Uses #top_classes and #base_uri.
@@ -305,46 +372,46 @@ module RDF::N3
305
372
  def order_subjects
306
373
  seen = {}
307
374
  subjects = []
308
-
375
+
309
376
  # Start with base_uri
310
- if base_uri && @subjects.keys.include?(base_uri)
377
+ if base_uri && @subjects.keys.select(&:uri?).include?(base_uri)
311
378
  subjects << base_uri
312
379
  seen[base_uri] = true
313
380
  end
314
- log_debug {"subjects1: #{subjects.inspect}"}
315
-
381
+
316
382
  # Add distinguished classes
317
383
  top_classes.each do |class_uri|
318
- graph.query(predicate: RDF.type, object: class_uri).
319
- map {|st| st.subject}.
320
- sort.
321
- uniq.
322
- each do |subject|
323
- log_debug("order_subjects") {subject.to_ntriples}
324
- subjects << subject
325
- seen[subject] = true
326
- end
384
+ graph.query({predicate: RDF.type, object: class_uri}).
385
+ map {|st| st.subject}.sort.uniq.each do |subject|
386
+ log_debug("order_subjects") {subject.to_sxp}
387
+ subjects << subject
388
+ seen[subject] = true
389
+ end
390
+ end
391
+
392
+ # Add formulae which are subjects in this graph
393
+ @formulae.each_key do |bn|
394
+ next unless @subjects.key?(bn)
395
+ subjects << bn
396
+ seen[bn] = true
327
397
  end
328
- log_debug {"subjects2: #{subjects.inspect}"}
329
398
 
330
399
  # Mark as seen lists that are part of another list
331
- @lists.values.map(&:statements).
332
- flatten.each do |st|
333
- seen[st.object] if @lists.has_key?(st.object)
334
- end
400
+ @lists.values.flatten.each do |v|
401
+ seen[v] = true if @lists.key?(v)
402
+ end
403
+
404
+ list_elements = [] # Lists may be top-level elements
335
405
 
336
406
  # Sort subjects by resources over bnodes, ref_counts and the subject URI itself
337
- recursable = @subjects.keys.
407
+ recursable = (@subjects.keys - list_elements).
338
408
  select {|s| !seen.include?(s)}.
339
409
  map {|r| [r.node? ? 1 : 0, ref_count(r), r]}.
340
410
  sort
341
-
342
- log_debug {"subjects3: #{subjects.inspect}"}
411
+
343
412
  subjects += recursable.map{|r| r.last}
344
- log_debug {"subjects4: #{subjects.inspect}"}
345
- subjects
346
413
  end
347
-
414
+
348
415
  # Perform any preprocessing of statements required
349
416
  def preprocess
350
417
  # Load defined prefixes
@@ -355,57 +422,46 @@ module RDF::N3
355
422
 
356
423
  prefix(nil, @options[:default_namespace]) if @options[:default_namespace]
357
424
 
358
- @graph.each {|statement| preprocess_statement(statement)}
425
+ @options[:prefixes] = {} # Will define actual used when matched
426
+ repo.each {|statement| preprocess_statement(statement)}
359
427
  end
360
-
428
+
361
429
  # Perform any statement preprocessing required. This is used to perform reference counts and determine required
362
430
  # prefixes.
363
431
  # @param [Statement] statement
364
432
  def preprocess_statement(statement)
365
- #log_debug {"preprocess: #{statement.inspect}"}
366
- references = ref_count(statement.object) + 1
367
- @references[statement.object] = references
368
- @subjects[statement.subject] = true
369
-
370
- # Collect lists
371
- if statement.predicate == RDF.first
372
- @lists[statement.subject] = RDF::List.new(subject: statement.subject, graph: graph)
373
- end
433
+ #log_debug("preprocess") {statement.inspect}
374
434
 
375
- if statement.object == RDF.nil || statement.subject == RDF.nil
376
- # Add an entry for the list tail
377
- @lists[RDF.nil] ||= RDF::List[]
378
- end
379
-
380
- # Pre-fetch qnames, to fill prefixes
381
- get_qname(statement.subject)
382
- get_qname(statement.predicate)
383
- get_qname(statement.object)
384
- get_qname(statement.object.datatype) if statement.object.literal? && statement.object.datatype
385
-
386
- @references[statement.predicate] = ref_count(statement.predicate) + 1
435
+ # Pre-fetch pnames, to fill prefixes
436
+ get_pname(statement.subject)
437
+ get_pname(statement.predicate)
438
+ get_pname(statement.object)
439
+ get_pname(statement.object.datatype) if statement.object.literal? && statement.object.datatype
387
440
  end
388
-
389
- # Return the number of times this node has been referenced in the object position
390
- # @return [Integer]
391
- def ref_count(node)
392
- @references.fetch(node, 0)
441
+
442
+ # Perform graph-specific preprocessing
443
+ # @param [Statement] statement
444
+ def preprocess_graph_statement(statement)
445
+ bump_reference(statement.object)
446
+ # Count properties of this subject
447
+ @subjects[statement.subject] ||= {}
448
+ @subjects[statement.subject][statement.predicate] ||= 0
449
+ @subjects[statement.subject][statement.predicate] += 1
393
450
  end
394
451
 
395
452
  # Returns indent string multiplied by the depth
396
453
  # @param [Integer] modifier Increase depth by specified amount
397
454
  # @return [String] A number of spaces, depending on current depth
398
455
  def indent(modifier = 0)
399
- " " * (@depth + modifier)
456
+ " " * (@options.fetch(:log_depth, log_depth) * 2 + modifier)
400
457
  end
401
458
 
402
459
  # Reset internal helper instance variables
403
460
  def reset
404
- @depth = 0
405
461
  @lists = {}
406
-
407
462
  @references = {}
408
463
  @serialized = {}
464
+ @graphs = {}
409
465
  @subjects = {}
410
466
  end
411
467
 
@@ -416,7 +472,7 @@ module RDF::N3
416
472
  # @return [String]
417
473
  def quoted(string)
418
474
  if string.to_s.match(/[\t\n\r]/)
419
- string = string.gsub('\\', '\\\\\\\\').gsub('"""', '\\"""')
475
+ string = string.gsub('\\', '\\\\\\\\').gsub('"""', '\\"\\"\\"')
420
476
  %("""#{string}""")
421
477
  else
422
478
  "\"#{escaped(string)}\""
@@ -424,154 +480,309 @@ module RDF::N3
424
480
  end
425
481
 
426
482
  private
427
-
483
+
428
484
  # Checks if l is a valid RDF list, i.e. no nodes have other properties.
429
- def is_valid_list(l)
430
- #log_debug {"is_valid_list: #{l.inspect}"}
431
- return @lists[l] && @lists[l].valid?
432
- end
433
-
434
- def do_list(l)
435
- list = @lists[l]
436
- log_debug {"do_list: #{list.inspect}"}
437
- position = :subject
438
- list.each_statement do |st|
439
- next unless st.predicate == RDF.first
440
- log_debug {" list this: #{st.subject} first: #{st.object}[#{position}]"}
441
- path(st.object, position)
442
- subject_done(st.subject)
443
- position = :object
444
- end
485
+ def collection?(l)
486
+ return @lists.key?(l) || l.list?
445
487
  end
446
-
447
- def p_list(node, position)
448
- return false if !is_valid_list(node)
449
- #log_debug {"p_list: #{node.inspect}, #{position}"}
450
488
 
451
- @output.write(position == :subject ? "(" : " (")
452
- @depth += 2
453
- do_list(node)
454
- @depth -= 2
489
+ def collection(node, position)
490
+ return false if !collection?(node)
491
+ log_debug("collection") do
492
+ "#{node.to_sxp}, " +
493
+ "pos: #{position}, " +
494
+ "rc: #{ref_count(node)}"
495
+ end
496
+
497
+ @output.write("(")
498
+ log_depth do
499
+ list = node.list? ? node : @lists[node]
500
+ log_debug("collection") {list.inspect}
501
+ subject_done(RDF.nil)
502
+ subject_done(node)
503
+ index = 0
504
+ list.each do |li|
505
+ log_debug("(list first)") {"#{li}[#{position}]"}
506
+ @output.write(" ") if index > 0
507
+ path(li, :object)
508
+ subject_done(li)
509
+ index += 1
510
+ end
511
+ end
455
512
  @output.write(')')
456
513
  end
457
-
458
- def p_squared?(node, position)
459
- node.node? &&
460
- !@serialized.has_key?(node) &&
461
- ref_count(node) <= 1
462
- end
463
-
464
- def p_squared(node, position)
465
- return false unless p_squared?(node, position)
466
-
467
- #log_debug {"p_squared: #{node.inspect}, #{position}"}
468
- subject_done(node)
469
- @output.write(position == :subject ? '[' : ' [')
470
- @depth += 2
471
- predicate_list(node)
472
- @depth -= 2
473
- @output.write(']')
474
-
475
- true
476
- end
477
-
478
- def p_default(node, position)
479
- #log_debug {"p_default: #{node.inspect}, #{position}"}
480
- l = (position == :subject ? "" : " ") + format_term(node, options)
514
+
515
+ # Default singular resource representation.
516
+ def p_term(resource, position)
517
+ #log_debug("p_term") {"#{resource.to_sxp}, #{position}"}
518
+ l = if resource.is_a?(RDF::Query::Variable)
519
+ "?#{resource.name}"
520
+ elsif resource == RDF.nil
521
+ "()"
522
+ else
523
+ format_term(resource, **options)
524
+ end
481
525
  @output.write(l)
482
526
  end
483
-
484
- def path(node, position)
485
- log_debug do
486
- "path: #{node.inspect}, " +
527
+
528
+ # Represent a resource in subject, predicate or object position.
529
+ # Use either collection, blankNodePropertyList or singular resource notation.
530
+ def path(resource, position)
531
+ log_debug("path") do
532
+ "#{resource.to_sxp}, " +
487
533
  "pos: #{position}, " +
488
- "[]: #{is_valid_list(node)}, " +
489
- "p2?: #{p_squared?(node, position)}, " +
490
- "rc: #{ref_count(node)}"
534
+ "{}?: #{formula?(resource, position).inspect}, " +
535
+ "()?: #{collection?(resource).inspect}, " +
536
+ "[]?: #{blankNodePropertyList?(resource, position).inspect}, " +
537
+ "rc: #{ref_count(resource)}"
491
538
  end
492
- raise RDF::WriterError, "Cannot serialize node '#{node}'" unless p_list(node, position) || p_squared(node, position) || p_default(node, position)
539
+ raise RDF::WriterError, "Cannot serialize resource '#{resource}'" unless
540
+ formula(resource, position) ||
541
+ collection(resource, position) ||
542
+ blankNodePropertyList(resource, position) ||
543
+ p_term(resource, position)
493
544
  end
494
-
495
- def verb(node)
496
- log_debug {"verb: #{node.inspect}"}
497
- if node == RDF.type
498
- @output.write(" a")
545
+
546
+ def predicate(resource)
547
+ log_debug("predicate") {resource.to_sxp}
548
+ case resource
549
+ when RDF.type
550
+ @output.write("a")
551
+ when RDF::OWL.sameAs
552
+ @output.write("=")
553
+ when RDF::N3::Log.implies
554
+ @output.write("=>")
499
555
  else
500
- path(node, :predicate)
556
+ log_depth {path(resource, :predicate)}
501
557
  end
502
558
  end
503
-
504
- def object_list(objects)
505
- log_debug {"object_list: #{objects.inspect}"}
559
+
560
+ # Render an objectList having a common subject and predicate
561
+ def objectList(objects)
562
+ log_debug("objectList") {objects.inspect}
506
563
  return if objects.empty?
507
564
 
508
- objects.each_with_index do |obj, i|
509
- @output.write(",\n#{indent(4)}") if i > 0
510
- path(obj, :object)
565
+ log_depth do
566
+ objects.each_with_index do |obj, i|
567
+ if i > 0 && (formula?(obj, :object) || blankNodePropertyList?(obj, :object))
568
+ @output.write ", "
569
+ elsif i > 0
570
+ @output.write ",\n#{indent(4)}"
571
+ end
572
+ path(obj, :object)
573
+ end
511
574
  end
512
575
  end
513
-
514
- def predicate_list(subject)
576
+
577
+ # Render a predicateObjectList having a common subject.
578
+ # @return [Integer] the number of properties serialized
579
+ def predicateObjectList(subject, from_bpl = false)
515
580
  properties = {}
516
- @graph.query(subject: subject) do |st|
517
- properties[st.predicate.to_s] ||= []
518
- properties[st.predicate.to_s] << st.object
581
+ @graph.enum_statement.select {|s| s.subject.sameTerm?(subject)}.each do |st|
582
+ (properties[st.predicate] ||= []) << st.object
519
583
  end
520
584
 
521
585
  prop_list = sort_properties(properties)
522
- prop_list -= [RDF.first.to_s, RDF.rest.to_s] if subject.node?
523
- log_debug {"predicate_list: #{prop_list.inspect}"}
524
- return if prop_list.empty?
525
-
526
- prop_list.each_with_index do |prop, i|
527
- begin
528
- @output.write(";\n#{indent(2)}") if i > 0
529
- verb(prop[0, 2] == "_:" ? RDF::Node.intern(prop.split(':').last) : RDF::URI.intern(prop))
530
- object_list(properties[prop])
531
- rescue Addressable::URI::InvalidURIError => e
532
- log_debug {"Predicate #{prop.inspect} is an invalid URI: #{e.message}"}
586
+ prop_list -= [RDF.first, RDF.rest] if @lists.key?(subject)
587
+ log_debug("predicateObjectList") { "subject: #{subject.to_sxp}, properties: #{prop_list.join(', ')}" }
588
+ return 0 if prop_list.empty?
589
+
590
+ @output.write("\n#{indent(2)}") if properties.keys.length > 1 && from_bpl
591
+ log_depth do
592
+ prop_list.each_with_index do |prop, i|
593
+ begin
594
+ @output.write(";\n#{indent(2)}") if i > 0
595
+ predicate(prop)
596
+ @output.write(" ")
597
+ objectList(properties[prop])
598
+ end
533
599
  end
534
600
  end
601
+ properties.keys.length
602
+ end
603
+
604
+ # Can subject be represented as a blankNodePropertyList?
605
+ def blankNodePropertyList?(resource, position)
606
+ resource.node? &&
607
+ !formula?(resource, position) &&
608
+ !collection?(resource) &&
609
+ (!is_done?(resource) || position == :subject) &&
610
+ ref_count(resource) == (position == :object ? 1 : 0) &&
611
+ !repo.has_graph?(resource)
612
+ end
613
+
614
+ def blankNodePropertyList(resource, position)
615
+ return false unless blankNodePropertyList?(resource, position)
616
+
617
+ log_debug("blankNodePropertyList") {resource.to_sxp}
618
+ subject_done(resource)
619
+ @output.write((position == :subject ? "\n#{indent}[" : '['))
620
+ num_props = log_depth {predicateObjectList(resource, true)}
621
+ @output.write((num_props > 1 ? "\n#{indent(2)}" : "") + (position == :subject ? '] .' : ']'))
622
+ true
535
623
  end
536
-
537
- def s_squared?(subject)
538
- ref_count(subject) == 0 && subject.node? && !is_valid_list(subject)
539
- end
540
-
541
- def s_squared(subject)
542
- return false unless s_squared?(subject)
543
-
544
- log_debug {"s_squared: #{subject.inspect}"}
545
- @output.write("\n#{indent} [")
546
- @depth += 1
547
- predicate_list(subject)
548
- @depth -= 1
549
- @output.write("] .")
624
+
625
+ # Can subject be represented as a formula?
626
+ def formula?(resource, position)
627
+ !!@formulae[resource]
628
+ end
629
+
630
+ def formula(resource, position)
631
+ return false unless formula?(resource, position)
632
+
633
+ log_debug("formula") {resource.to_sxp}
634
+ subject_done(resource)
635
+ @output.write('{')
636
+ count = 0
637
+ log_depth do
638
+ with_graph(resource) do
639
+ order_subjects.each do |subject|
640
+ unless is_done?(subject)
641
+ statement(subject, count)
642
+ count += 1
643
+ end
644
+ end
645
+ end
646
+ end
647
+ @output.write((count > 0 ? "#{indent}" : "") + '}')
550
648
  true
551
649
  end
552
-
553
- def s_default(subject)
650
+
651
+ # Render triples having the same subject using an explicit subject
652
+ def triples(subject)
554
653
  @output.write("\n#{indent}")
555
654
  path(subject, :subject)
556
- predicate_list(subject)
557
- @output.write(" .")
655
+ @output.write(" ")
656
+ num_props = predicateObjectList(subject)
657
+ @output.puts("#{num_props > 0 ? ' ' : ''}.")
558
658
  true
559
659
  end
560
-
561
- def statement(subject)
562
- log_debug {"statement: #{subject.inspect}, s2?: #{s_squared?(subject)}"}
660
+
661
+ def statement(subject, count)
662
+ log_debug("statement") do
663
+ "#{subject.to_sxp}, " +
664
+ "{}?: #{formula?(subject, :subject).inspect}, " +
665
+ "()?: #{collection?(subject).inspect}, " +
666
+ "[]?: #{blankNodePropertyList?(subject, :subject).inspect}, "
667
+ end
563
668
  subject_done(subject)
564
- s_squared(subject) || s_default(subject)
565
- @output.write("\n")
669
+ blankNodePropertyList(subject, :subject) || triples(subject)
670
+ @output.puts if count > 0 || graph.graph_name
671
+ end
672
+
673
+ # Return the number of times this node has been referenced in the object position
674
+ # @return [Integer]
675
+ def ref_count(node)
676
+ @references.fetch(node, 0)
566
677
  end
567
-
678
+
679
+ # Increase the reference count of this resource
680
+ # @param [RDF::Resource] resource
681
+ # @return [Integer] resulting reference count
682
+ def bump_reference(resource)
683
+ @references[resource] = ref_count(resource) + 1
684
+ end
685
+
568
686
  def is_done?(subject)
569
687
  @serialized.include?(subject)
570
688
  end
571
-
689
+
572
690
  # Mark a subject as done.
573
691
  def subject_done(subject)
574
692
  @serialized[subject] = true
575
693
  end
694
+
695
+ def graph_done?(subject)
696
+ @graphs.include?(subject)
697
+ end
698
+
699
+ # Mark a graph as done.
700
+ def graph_done(graph_name)
701
+ @graphs[graph_name] = true
702
+ end
703
+
704
+ # Process a graph projection
705
+ def with_graph(graph_name)
706
+ old_lists, @lists = @lists, {}
707
+ old_references, @references = @references, {}
708
+ old_serialized, @serialized = @serialized, {}
709
+ old_subjects, @subjects = @subjects, {}
710
+ old_graph, @graph = @graph, repo.project_graph(graph_name)
711
+ old_formulae, @formulae = @formulae, {}
712
+
713
+ graph_done(graph_name)
714
+
715
+ lists = {}
716
+ graph.each do |statement|
717
+ preprocess_graph_statement(statement)
718
+ [statement.subject, statement.object].each do |resource|
719
+ @formulae[resource] = true if
720
+ resource.node? &&
721
+ (formula_names.include?(resource) || resource.id.start_with?('_form_'))
722
+
723
+ # First-class list may have members which are formulae, and need reference counts
724
+ if resource.list?
725
+ resource.each_descendant do |term|
726
+ bump_reference(term)
727
+ @formulae[term] = true if
728
+ term.node? &&
729
+ (formula_names.include?(term) || term.id.start_with?('_form_'))
730
+ end
731
+ end
732
+ end
733
+
734
+ # Collect list elements
735
+ if [RDF.first, RDF.rest].include?(statement.predicate) && statement.subject.node?
736
+ lists[statement.subject] ||= {}
737
+ lists[statement.subject][statement.predicate] = statement.object
738
+ end
739
+ end
740
+
741
+ # Remove list entries after head with more than two properties (other than rdf:type)
742
+ rests = lists.values.map {|props| props[RDF.rest]}
743
+
744
+ # Remove non-head lists that have too many properties
745
+ rests.select do |bn|
746
+ pc = 0
747
+ @subjects.fetch(bn, {}).each do |pred, count|
748
+ next if pred == RDF.type
749
+ pc += count
750
+ end
751
+ lists.delete(bn) if pc > 2
752
+ end
753
+
754
+ # Values for this list element, recursive
755
+ def list_values(bn, lists)
756
+ raise "no list" unless lists.has_key?(bn)
757
+ first, rest = lists[bn][RDF.first], lists[bn][RDF.rest]
758
+ (rest == RDF.nil ? [] : list_values(rest, lists)).unshift(first)
759
+ rescue
760
+ lists.delete(bn)
761
+ raise $!
762
+ end
763
+
764
+ # Create value arrays for each entry
765
+ lists.each do |bn, props|
766
+ begin
767
+ @lists[bn] = list_values(bn, lists)
768
+ rescue
769
+ # Skip this list element, if it raises an exception
770
+ lists.delete(bn)
771
+ end
772
+ end
773
+
774
+ # Mark all remaining rests done
775
+ rests.each {|bn| subject_done(bn) if lists.include?(bn)}
776
+
777
+ # Remove entries that are referenced as rdf:rest of some entry
778
+ lists.each do |bn, props|
779
+ @lists.delete(props[RDF.rest])
780
+ end
781
+
782
+ # Record nodes in subject or object
783
+ yield
784
+ ensure
785
+ @graph, @lists, @references, @serialized, @subjects, @formulae = old_graph, old_lists, old_references, old_serialized, old_subjects, old_formulae
786
+ end
576
787
  end
577
788
  end