Class: RDF::RDFXML::Writer

Inherits:
Writer show all
Includes:
Util::Logger
Defined in:
vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb

Overview

An RDF/XML serialiser in Ruby

Note that the natural interface is to write a whole graph at a time. Writing statements or Triples will create a graph to add them to and then serialize the graph.

The writer will add prefix definitions, and use them for creating @prefix definitions, and minting QNames

Examples:

Obtaining a RDF/XML writer class

RDF::Writer.for(:rdf)         #=> RDF::RDFXML::Writer
RDF::Writer.for("etc/test.rdf")
RDF::Writer.for(file_name: "etc/test.rdf")
RDF::Writer.for(file_extension: "rdf")
RDF::Writer.for(content_type: "application/rdf+xml")

Serializing RDF graph into an RDF/XML file

RDF::RDFXML::Write.open("etc/test.rdf") do |writer|
  writer << graph
end

Serializing RDF statements into an RDF/XML file

RDF::RDFXML::Writer.open("etc/test.rdf") do |writer|
  graph.each_statement do |statement|
    writer << statement
  end
end

Serializing RDF statements into an RDF/XML string

RDF::RDFXML::Writer.buffer do |writer|
  graph.each_statement do |statement|
    writer << statement
  end
end

Creating @base and @prefix definitions in output

RDF::RDFXML::Writer.buffer(base_uri: "http://example.com/", prefixes: {
    nil => "http://example.com/ns#",
    foaf: "http://xmlns.com/foaf/0.1/"}
) do |writer|
  graph.each_statement do |statement|
    writer << statement
  end
end

Author:

Constant Summary collapse

VALID_ATTRIBUTES =
[:none, :untyped, :typed]

Constants included from Util::Logger

Util::Logger::IOWrapper

Instance Attribute Summary collapse

Attributes inherited from Writer

#options

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Util::Logger

#log_debug, #log_depth, #log_error, #log_fatal, #log_info, #log_recover, #log_recovering?, #log_statistics, #log_warn, #logger

Methods inherited from Writer

accept?, buffer, #canonicalize?, dump, each, #encoding, #escaped, #flush, for, format, #format_list, #format_literal, #format_node, #format_term, #format_tripleTerm, #format_uri, #node_id, open, #prefix, #prefixes, #prefixes=, #puts, #quoted, #to_sym, to_sym, #uri_for, #validate?, #write_comment, #write_prologue, #write_statement, #write_triples

Methods included from Util::Aliasing::LateBound

#alias_method

Methods included from Writable

#<<, #insert, #insert_graph, #insert_reader, #insert_statement, #insert_statements, #writable?

Methods included from Util::Coercions

#coerce_statements

Constructor Details

#initialize(output = $stdout, **options) {|writer| ... } ⇒ Writer

Initializes the RDF/XML writer instance.

Parameters:

  • output (IO, File) (defaults to: $stdout)

    the output stream

  • options (Hash{Symbol => Object})

    any additional options

Options Hash (**options):

  • :attributes (Symbol) — default: nil

    How to use XML attributes when serializing, one of :none, :untyped, :typed. The default is :none.

  • :base_uri (#to_s) — default: nil

    the base URI to use when constructing relative URIs

  • :canonicalize (Boolean) — default: false

    whether to canonicalize literals when serializing

  • :default_namespace (String) — default: nil

    URI to use as default namespace, same as prefix(nil)

  • :lang (#to_s) — default: nil

    Output as root xml:lang attribute, and avoid generation xml:lang where possible

  • :max_depth (Integer) — default: 10

    Maximum depth for recursively defining resources

  • :prefixes (Hash) — default: Hash.new

    the prefix mappings to use (not supported by all writers)

  • :standard_prefixes (Boolean) — default: false

    Add standard prefixes to prefixes, if necessary.

  • :stylesheet (String) — default: nil

    URI to use as @href for output stylesheet processing instruction.

  • :top_classes (Array<RDF::URI>) — default: [RDF::RDFS.Class]

    Defines rdf:type of subjects to be emitted at the beginning of the document.

Yields:

  • (writer)

Yield Parameters:



134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 134

def initialize(output = $stdout, **options, &block)
  super do
    @graph = RDF::Graph.new
    @uri_to_prefix = {}
    @uri_to_qname = {}
    @top_classes = options[:top_classes] || [RDF::RDFS.Class]

    # FIXME: If version is specified in media type, use it to set an explicit version
    @version = nil

    block.call(self) if block_given?
  end
end

Instance Attribute Details

#base_uriRDF::URI

Returns Base URI used for relativizing URIs.

Returns:

  • (RDF::URI)

    Base URI used for relativizing URIs



64
65
66
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 64

def base_uri
  @base_uri
end

#graphGraph

Returns Graph of statements serialized.

Returns:

  • (Graph)

    Graph of statements serialized



61
62
63
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 61

def graph
  @graph
end

#has_directionBoolean

Returns Set to true if any literal includes a base direction.

Returns:

  • (Boolean)

    Set to true if any literal includes a base direction



70
71
72
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 70

def has_direction
  @has_direction
end

#top_classesArray<URI> (readonly)

Defines rdf:type of subjects to be emitted at the beginning of the document.

Returns:



58
59
60
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 58

def top_classes
  @top_classes
end

#versionString

Returns RDF Version to output, if any.

Returns:

  • (String)

    RDF Version to output, if any



67
68
69
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 67

def version
  @version
end

Class Method Details

.optionsObject

RDF/XML Writer options



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 75

def self.options
  super + [
    RDF::CLI::Option.new(
      symbol: :attributes,
      datatype: %w(none untyped typed),
      on: ["--attributes ATTRIBUTES",  %w(none untyped typed)],
      description: "How to use XML attributes when serializing, one of :none, :untyped, :typed. The default is :none.") {|arg| arg.to_sym},
    RDF::CLI::Option.new(
      symbol: :default_namespace,
      datatype: RDF::URI,
      on: ["--default-namespace URI", :REQUIRED],
      description: "URI to use as default namespace, same as prefixes.") {|arg| RDF::URI(arg)},
    RDF::CLI::Option.new(
      symbol: :lang,
      datatype: String,
      on: ["--lang LANG", :REQUIRED],
      description: "Output as root xml:lang attribute, and avoid generation xml:lang, where possible.") {|arg| RDF::URI(arg)},
    RDF::CLI::Option.new(
      symbol: :max_depth,
      datatype: Integer,
      on: ["--max-depth"],
      description: "Maximum depth for recursively defining resources, defaults to 3.") {|arg| arg.to_i},
    RDF::CLI::Option.new(
      symbol: :stylesheet,
      datatype: RDF::URI,
      on: ["--stylesheet URI", :REQUIRED],
      description: "URI to use as @href for output stylesheet processing instruction.") {|arg| RDF::URI(arg)},
  ]
end

Instance Method Details

#prefix_attrsHash{String => String} (protected)

XML namespace attributes for defined prefixes

Returns:



443
444
445
446
447
448
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 443

def prefix_attrs
  prefixes.inject({}) do |memo, (k, v)|
    memo[(k ? "xmlns:#{k}" : "xmlns").to_sym] = v.to_s
    memo
  end
end

#preprocessignored (protected)

Perform any preprocessing of statements required

Returns:

  • (ignored)


452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 452

def preprocess
  # Load defined prefixes
  (@options[:prefixes] || {}).each_pair do |k, v|
    @uri_to_prefix[v.to_s] = k
  end
  @options[:prefixes] = {}  # Will define actual used when matched

  prefix(:rdf, RDF.to_uri)
  @uri_to_prefix[RDF.to_uri.to_s] = :rdf
  if base_uri || @options[:lang]
    prefix(:xml, RDF::XML)
    @uri_to_prefix[RDF::XML.to_s] = :xml
  end

  if @options[:default_namespace]
    @uri_to_prefix[@options[:default_namespace]] = nil
    prefix(nil, @options[:default_namespace])
  end

  # Process each statement to establish QNames and Terms
  @graph.each {|statement| preprocess_statement(statement)}
end

#preprocess_statement(statement) ⇒ Object (protected)

Perform any statement preprocessing required. This is used to perform reference counts and determine required prefixes.

For RDF/XML, make sure that all predicates have QNames

Parameters:



479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 479

def preprocess_statement(statement)
  #log_debug {"preprocess: #{statement.inspect}"}
  bump_reference(statement.object)
  @subjects[statement.subject] = true
  get_qname(statement.subject)
  ensure_qname(statement.predicate)
  statement.predicate == RDF.type && statement.object.uri? ? ensure_qname(statement.object) : get_qname(statement.object)
  get_qname(statement.object.datatype) if statement.object.literal? && statement.object.datatype?

  # Base direction requires a prefix, used to set the its:version in the document
  if statement.object.literal? && statement.object.direction?
    prefix(:its, RDF::ITS.to_s)
    @has_direction = true # Indirectly adds its:version to document element

    # It's an error if version is frozen and not at least "1.2-basic"
    if version && version.frozen?
      log_error("Literal direction is incompatible with required version #{version}: #{statement.object.direction}") if
        version == "1.1"
    elsif version.nil?
      @version = "1.2-basic"
    end
  elsif statement.object.statement?
    # It's an error if version is frozen and not at least "1.2-basic"
    if version && version.frozen?
      log_error("Triple terms are incompatible with required version #{version}") if
        version != "1.2"
    else
      @version = "1.2"
    end

    bump_reference(statement.subject)

    # If this statement is also asserted, note it as an annotation
    
    # Also count references of triple terms
    preprocess_statement(statement.object) if statement.object.statement?

    # If it fits, allow this to be rendered as an annotation
    if statement.predicate == RDF.reifies
      @reification[statement.subject] ||= []
      @reification[statement.subject] << statement.object unless
        @reification[statement.subject].include?(statement.object)
    end
  end
end

#render_collection(property, list, builder, **options, &block) ⇒ Object (protected)

Render a collection, which may be included in a property declaration, or may be recursive within another collection

Parameters:

Returns:

  • String The rendered collection is returned as a string



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 423

def render_collection(property, list, builder, **options, &block)
  builder.tag!(property, "rdf:parseType": "Collection") do |b|
    list.each do |object|
      if log_depth <= @max_depth && !is_done?(object)
        render_subject(object, b)
      elsif object.node?
        if ref_count(object) > 1
          b.tag!("rdf:Description", "rdf:nodeID": object.id)
        else
          b.tag!("rdf:Description")
        end
      else
        b.tag!("rdf:Description", "rdf:about": object.relativize(base_uri))
      end
    end
  end
end

#render_document(subjects, lang: nil, base: nil, **options) {|subject| ... } ⇒ Object (protected)

Render document. Yields each subject to be rendered separately.

Parameters:

  • subjects (Array<RDF::Resource>)

    Ordered list of subjects. Template must yield to each subject, which returns the serialization of that subject (@see #subject_template)

  • options (Hash{Symbol => Object})

    Rendering options.

Options Hash (**options):

  • base (RDF::URI) — default: nil

    Base URI added to document, used for shortening URIs within the document.

  • language (Symbol, String) — default: nil

    Value of @lang attribute in document, also allows included literals to omit an @lang attribute if it is equivalent to that of the document.

  • title (String) — default: nil

    Value of html>head>title element.

  • prefix (String) — default: nil

    Value of @prefix attribute.

Yields:

  • (subject)

    Yields each subject

Yield Parameters:

Yield Returns:

  • (:ignored)

Returns:

  • String The rendered document is returned as a string



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 225

def render_document(subjects, lang: nil, base: nil, **options, &block)
  builder = Builder::RdfXml.new(indent: 2)
  builder.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
  builder.instruct! :'xml-stylesheet', type: 'text/xsl', href: options[:stylesheet] if options[:stylesheet]
  attrs = prefix_attrs
  attrs[:"xml:lang"] = lang if lang
  attrs[:"xml:base"] = base if base
  attrs[:"rdf:version"] = version.freeze if version
  attrs[:"its:version"] = "2.0" if has_direction

  builder.rdf(:RDF, **attrs) do |b|
    subjects.each do |subject|
      render_subject(subject, b, **options)
    end
  end
end

#render_property(subject, property, objects, builder, **options) ⇒ Object (protected)

Render a single- or multi-valued property. Yields each object for optional rendering. The block should only render for recursive subject definitions (i.e., where the object is also a subject and is rendered underneath the first referencing subject).

If a multi-valued property definition is not found within the template, the writer will use the single-valued property definition multiple times.

Parameters:

  • subject (RDF::Resource)
  • property (String)

    Property to render, already in QName form.

  • objects (Array<RDF::Resource>)

    List of objects to render. If the list contains only a single element, the :property_value template will be used. Otherwise, the :property_values template is used.

  • builder (Builder::RdfXml)
  • options (Hash{Symbol => Object})

    Rendering options.



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 318

def render_property(subject, property, objects, builder, **options)
  log_debug {"render_property(#{property}): #{objects.inspect}"}
  property = get_qname(property) if property.is_a?(RDF::URI)

  # Separate out the objects which are lists and render separately
  lists = objects.
    select(&:node?).
    map {|o| RDF::List.new(subject: o, graph: @graph)}.
    select {|l| l.valid? && l.none?(&:literal?)}

  objects = objects - lists.map(&:subject)

  unless lists.empty?
    # Render non-list objects
    log_debug(depth: log_depth + 1) {"properties with lists: #{lists} non-lists: #{objects - lists.map(&:subject)}"}

    unless objects.empty?
      render_property(subject, property,  objects, builder, **options)
    end

    # Render each list
    lists.each do |list|
      # Render each list as multiple properties and set :inlist to true
      list.each_statement {|st| subject_done(st.subject)}

      log_depth do
        log_debug {"list: #{list.inspect} #{list.to_a}"}
        render_collection(property, list, builder, **options)
      end
    end
  end

  if objects.length == 1
    recurse = log_depth <= @max_depth
    object = objects.first
    attrs = {}

    # If there is a single reifier for this statement, write out annotation
    tt = RDF::Statement(subject, @uri_to_qname.invert[property], object)
    reifs = @reification.select {|k, v| v.include?(tt)}.keys
    if reifs.length == 1
      reif = reifs.first
      @as_annotation[reif] = tt
      if reif.iri?
        attrs['rdf:annotation'] = reif.relativize(base_uri)
      else
        attrs['rdf:annotationNodeID'] = reif.id
      end
    end

    if recurse && !is_done?(object)
      builder.tag!(property, **attrs) do |b|
        render_subject(object, b, **options)
      end
    elsif object.literal? && object.datatype == RDF.XMLLiteral
      builder.tag!(property, "rdf:parseType": "Literal", no_whitespace: true, **attrs) do |b|
        b << object.value
      end
    elsif object.literal?
      attrs[:"xml:lang"] = object.language if object.language?
      attrs[:"rdf:datatype"] = object.datatype if object.datatype?
      attrs[:"its:dir"] = object.direction if object.direction?
      builder.tag!(property, object.value.to_s, **attrs)
    elsif object.statement?
      # Just write out the triple term, unless it is annotated
      builder.tag!(property, "rdf:parseType": "Triple") do |b|
        render_triple_term(object, b, **options)
      end unless @as_annotation.key?(subject)
    elsif object.node?
       builder.tag!(property, "rdf:nodeID": object.id, **attrs)
    elsif object
      builder.tag!(property, "rdf:resource": object.relativize(base_uri), **attrs)
    end
  else
    # Render each property using property_value template
    objects.each do |object|
      log_depth do
        render_property(subject, property, [object], builder, **options)
      end
    end
  end
end

#render_subject(subject, builder, **options) {|predicate| ... } ⇒ Object (protected)

Render a subject.

The subject template may be called either as a top-level element, or recursively under another element if the rel local is not nil.

For RDF/XML, removes from predicates those that can be rendered as attributes, and adds the :attr_props local, which includes all attributes to be rendered as properties.

Yields each property to be rendered separately.

Parameters:

Options Hash (**options):

  • about (String) — default: nil

    About description, a QName, URI or Node definition. May be nil if no @about is rendered (e.g. unreferenced Nodes)

  • resource (String) — default: nil

    Resource description, a QName, URI or Node definition. May be nil if no @resource is rendered

  • rel (String) — default: nil

    Optional @rel property description, a QName, URI or Node definition.

  • typeof (String) — default: nil

    RDF type as a QName, URI or Node definition. If :about is nil, this defaults to the empty string ("").

  • element (:li, nil) — default: nil

    Render with <li>, otherwise with template default.

Yields:

  • (predicate)

    Yields each predicate

Yield Parameters:

Yield Returns:

  • (:ignored)

Returns:

  • Builder::RdfXml



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 272

def render_subject(subject, builder, **options, &block)
  return nil if is_done?(subject)

  attr_props, embed_props, types = prop_partition(properties_for_subject(subject))

  # The first type is used for
  first_type = types.shift
  type_qname = get_qname(first_type) if first_type && !first_type.node?
  type_qname = nil unless type_qname.is_a?(String)
  types.unshift(first_type) if first_type && !type_qname
  type_qname ||= "rdf:Description"

  attr_props = attr_props.merge("rdf:nodeID": subject.id) if subject.node? && ref_count(subject) >= 1
  attr_props = attr_props.merge("rdf:about": subject.relativize(base_uri)) if subject.uri?

  log_debug {"render_subject(#{subject.inspect})"}
  subject_done(subject)

  builder.tag!(type_qname, **attr_props) do |b|
    types.each do |type|
      if type.node?
        b.tag!("rdf:type",  "rdf:nodeID": type.id)
      else
        b.tag!("rdf:type",  "rdf:resource": type.to_s)
      end
    end

    log_depth do
      embed_props.each do |p, objects|
        render_property(subject, p, objects, b, **options)
      end
    end
  end
end

#render_triple_term(term, builder, **options) ⇒ Object (protected)

Render a triple term, which may be recursive



403
404
405
406
407
408
409
410
411
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 403

def render_triple_term(term, builder, **options)
  attr_props = {}
  attr_props = attr_props.merge("rdf:nodeID": term.subject.id) if term.subject.node?
  attr_props = attr_props.merge("rdf:about": term.subject.relativize(base_uri)) if term.subject.uri?

  builder.tag!("rdf:Description", **attr_props) do |b|
    render_property(term.subject, term.predicate, [term.object], b)
  end
end

#resetObject (protected)

Reset parser to run again



194
195
196
197
198
199
200
201
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 194

def reset
  @options[:log_depth] = 0
  @references = {}
  @serialized = {}
  @subjects = {}
  @reification = {}
  @as_annotation = {}
end

#write_epilogueObject



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 161

def write_epilogue
  @max_depth = @options.fetch(:max_depth, 10)
  @attributes = @options.fetch(:attributes, :none)
  @base_uri = RDF::URI(@options[:base_uri]) if @options[:base_uri]
  @lang = @options[:lang]
  self.reset

  log_debug {"\nserialize: graph size: #{@graph.size}"}

  preprocess

  # Prefixes
  prefix = prefixes.keys.map {|pk| "#{pk}: #{prefixes[pk]}"}.sort.join(" ") unless prefixes.empty?
  log_debug {"\nserialize: prefixes: #{prefix.inspect}"}

  @subjects = order_subjects

  # Generate document
  doc = render_document(@subjects,
    lang:       @lang,
    base:       base_uri,
    prefix:     prefix,
    stylesheet: @options[:stylesheet]) do |s|
    subject(s)
  end
  @output.write(doc)

  super
end

#write_triple(subject, predicate, object)

This method is abstract.

This method returns an undefined value.

Addes a triple to be serialized

Parameters:

Raises:

  • (NotImplementedError)

    unless implemented in subclass

  • (RDF::WriterError)

    if validating and attempting to write an invalid Term.



157
158
159
# File 'vendor/bundler/ruby/3.4.0/bundler/gems/rdf-rdfxml-14ee5432437b/lib/rdf/rdfxml/writer.rb', line 157

def write_triple(subject, predicate, object)
  @graph.insert(RDF::Statement(subject, predicate, object))
end