View source with formatted comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        J.Wielemaker@vu.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2010-2020, University of Amsterdam
    7                              CWI, Amsterdam
    8    All rights reserved.
    9
   10    Redistribution and use in source and binary forms, with or without
   11    modification, are permitted provided that the following conditions
   12    are met:
   13
   14    1. Redistributions of source code must retain the above copyright
   15       notice, this list of conditions and the following disclaimer.
   16
   17    2. Redistributions in binary form must reproduce the above copyright
   18       notice, this list of conditions and the following disclaimer in
   19       the documentation and/or other materials provided with the
   20       distribution.
   21
   22    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   23    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   24    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   25    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   26    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   27    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   28    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   29    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   30    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   31    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   32    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   33    POSSIBILITY OF SUCH DAMAGE.
   34*/
   35
   36:- module(rdf_write,
   37          [ rdf_write_xml/2             % +Stream, +Triples
   38          ]).   39:- autoload(library(assoc),
   40	    [empty_assoc/1,put_assoc/4,assoc_to_keys/2,get_assoc/3]).   41:- autoload(library(debug),[assertion/1]).   42:- autoload(library(lists),[member/2,append/3,select/3]).   43:- autoload(library(sgml),
   44	    [xml_quote_attribute/3, xml_name/1, xml_quote_cdata/3, xml_is_dom/1]).   45:- autoload(library(sgml_write),[xml_write/3]).   46:- autoload(library(semweb/rdf_db),
   47	    [rdf_global_id/2, rdf_register_ns/2, rdf_is_bnode/1, rdf_equal/2]).   48
   49
   50/** <module> Write RDF/XML from a list of triples
   51
   52This module writes an RDF/XML document  from   a  list of triples of the
   53format rdf(Subject, Predicate, Object).  It   is  primarily intended for
   54communicating computed RDF model fragments   to  external programs using
   55RDF/XML.
   56
   57When used from the HTTP library, use the following code:
   58
   59==
   60reply_graph(RDF) :-
   61        format('Content-type: application/rdf+xml; charset=UTF-8~n~n'),
   62        rdf_write_xml(current_output, RDF).
   63==
   64
   65@author Jan Wielemaker
   66@see    library(semweb/rdf_db) offers saving a named graph directly from
   67        the RDF database.
   68*/
   69
   70
   71                 /*******************************
   72                 *           WRITE RDFXML       *
   73                 *******************************/
   74
   75%!  rdf_write_xml(+Out:stream, +Triples:list(rdf(S,P,O))) is det.
   76%
   77%   Write an RDF/XML serialization of Triples to Out.
   78
   79rdf_write_xml(Out, Triples) :-
   80    sort(Triples, Unique),
   81    rdf_write_header(Out, Unique),
   82    node_id_map(Unique, AnonIDs),
   83    rdf_write_triples(Unique, AnonIDs, Out),
   84    rdf_write_footer(Out).
   85
   86
   87                 /*******************************
   88                 *        HEADER/FOOTER         *
   89                 *******************************/
   90
   91%!  rdf_write_header(+Out, +Triples)
   92%
   93%   Save XML document header, doctype and open the RDF environment.
   94%   This predicate also sets up the namespace notation.
   95
   96rdf_write_header(Out, Triples) :-
   97    xml_encoding(Out, Enc, Encoding),
   98    format(Out, '<?xml version=\'1.0\' encoding=\'~w\'?>~n', [Encoding]),
   99    format(Out, '<!DOCTYPE rdf:RDF [', []),
  100    used_namespaces(Triples, NSList),
  101    (   member(Id, NSList),
  102        ns(Id, NS),
  103        xml_quote_attribute(NS, NSText0, Enc),
  104        xml_escape_parameter_entity(NSText0, NSText),
  105        format(Out, '~N    <!ENTITY ~w \'~w\'>', [Id, NSText]),
  106        fail
  107    ;   true
  108    ),
  109    format(Out, '~N]>~n~n', []),
  110    format(Out, '<rdf:RDF', []),
  111    (   member(Id, NSList),
  112        format(Out, '~N    xmlns:~w="&~w;"~n', [Id, Id]),
  113        fail
  114    ;   true
  115    ),
  116    format(Out, '>~n', []).
  117
  118
  119xml_encoding(Out, Enc, Encoding) :-
  120    stream_property(Out, encoding(Enc)),
  121    (   xml_encoding_name(Enc, Encoding)
  122    ->  true
  123    ;   throw(error(domain_error(rdf_encoding, Enc), _))
  124    ).
  125
  126xml_encoding_name(ascii,       'US-ASCII').
  127xml_encoding_name(iso_latin_1, 'ISO-8859-1').
  128xml_encoding_name(utf8,        'UTF-8').
  129
  130%!  xml_escape_parameter_entity(+In, -Out) is det.
  131%
  132%   Escape % as &#37; for entity declarations.
  133
  134xml_escape_parameter_entity(In, Out) :-
  135    sub_atom(In, _, _, _, '%'),
  136    !,
  137    atom_codes(In, Codes),
  138    phrase(escape_parent(Codes), OutCodes),
  139    atom_codes(Out, OutCodes).
  140xml_escape_parameter_entity(In, In).
  141
  142escape_parent([]) --> [].
  143escape_parent([H|T]) -->
  144    (   { H == 37 }
  145    ->  "&#37;"
  146    ;   [H]
  147    ),
  148    escape_parent(T).
  149
  150%!  used_namespaces(+Triples:list(rdf(S,P,O)), -List:atom) is det.
  151%
  152%   Return the list of namespace abbreviations used in a set of
  153%   triples.
  154
  155used_namespaces(Triples, NSList) :-
  156    decl_used_predicate_ns(Triples),
  157    resources(Triples, Resources),
  158    empty_assoc(A0),
  159    put_assoc(rdf, A0, *, A1),      % needed for rdf:RDF
  160    res_used_namespaces(Resources, _NoNS, A1, A),
  161    assoc_to_keys(A, NSList).
  162
  163
  164res_used_namespaces([], [], A, A).
  165res_used_namespaces([Resource|T], NoNS, A0, A) :-
  166    ns(NS, Full),
  167    Full \== '',
  168    atom_concat(Full, Local, Resource),
  169    xml_name(Local),
  170    !,
  171    put_assoc(NS, A0, *, A1),
  172    res_used_namespaces(T, NoNS, A1, A).
  173res_used_namespaces([R|T0], [R|T], A0, A) :-
  174    res_used_namespaces(T0, T, A0, A).
  175
  176%!  resources(+Triples:list(rdf(S,P,O)), -Resources:list(atom)) is det.
  177%
  178%   Resources is the set of resources referenced in Triples.
  179
  180resources(Triples, Resources) :-
  181    phrase(resources(Triples), Raw),
  182    sort(Raw, Resources).
  183
  184resources([]) -->
  185    [].
  186resources([rdf(S,P,O)|T]) -->
  187    [S,P],
  188    object_resources(O),
  189    resources(T).
  190
  191object_resources(Atom) -->
  192    { atom(Atom) },
  193    !,
  194    [ Atom ].
  195object_resources(literal(type(Type, _))) -->
  196    !,
  197    [ Type ].
  198object_resources(_) -->
  199    [].
  200
  201%!  decl_used_predicate_ns(+Triples:list(rdf(S,P,O)))
  202%
  203%   For every URL used as a predicate   we *MUST* define a namespace
  204%   as we cannot use names holding /, :, etc. as XML identifiers.
  205
  206:- thread_local
  207    predicate_ns/2.  208
  209decl_used_predicate_ns(Triples) :-
  210    retractall(predicate_ns(_,_)),
  211    (   member(rdf(_,P,_), Triples),
  212        decl_predicate_ns(P),
  213        fail
  214    ;   true
  215    ).
  216
  217decl_predicate_ns(Pred) :-
  218    predicate_ns(Pred, _),
  219    !.
  220decl_predicate_ns(Pred) :-
  221    rdf_global_id(NS:Local, Pred),
  222    xml_name(Local),
  223    !,
  224    assert(predicate_ns(Pred, NS)).
  225decl_predicate_ns(Pred) :-
  226    is_bag_li_predicate(Pred),
  227    !.
  228decl_predicate_ns(Pred) :-
  229    atom_codes(Pred, Codes),
  230    append(NSCodes, LocalCodes, Codes),
  231    xml_codes(LocalCodes),
  232    !,
  233    (   NSCodes \== []
  234    ->  atom_codes(NS, NSCodes),
  235        (   ns(Id, NS)
  236        ->  assert(predicate_ns(Pred, Id))
  237        ;   between(1, infinite, N),
  238            atom_concat(ns, N, Id),
  239            \+ ns(Id, _)
  240        ->  rdf_register_ns(Id, NS),
  241            print_message(informational,
  242                          rdf(using_namespace(Id, NS)))
  243        ),
  244        assert(predicate_ns(Pred, Id))
  245    ;   assert(predicate_ns(Pred, -)) % no namespace used
  246    ).
  247
  248xml_codes([]).
  249xml_codes([H|T]) :-
  250    xml_code(H),
  251    xml_codes(T).
  252
  253xml_code(X) :-
  254    code_type(X, csym),
  255    !.
  256xml_code(0'-).                          % '
  257
  258
  259rdf_write_footer(Out) :-
  260    format(Out, '</rdf:RDF>~n', []).
  261
  262
  263                 /*******************************
  264                 *          ANONYMOUS IDS       *
  265                 *******************************/
  266
  267%!  node_id_map(+Triples, -IdMap) is det.
  268%
  269%   Create an assoc Resource -> NodeID for those anonymous resources
  270%   in Triples that need  a  NodeID.   This  implies  all  anonymous
  271%   resources that are used multiple times as object value.
  272
  273node_id_map(Triples, IdMap) :-
  274    anonymous_objects(Triples, Objs),
  275    msort(Objs, Sorted),
  276    empty_assoc(IdMap0),
  277    nodeid_map(Sorted, 0, IdMap0, IdMap).
  278
  279anonymous_objects([], []).
  280anonymous_objects([rdf(_,_,O)|T0], Anon) :-
  281    rdf_is_bnode(O),
  282    !,
  283    Anon = [O|T],
  284    anonymous_objects(T0, T).
  285anonymous_objects([_|T0], T) :-
  286    anonymous_objects(T0, T).
  287
  288nodeid_map([], _, Map, Map).
  289nodeid_map([H,H|T0], Id, Map0, Map) :-
  290    !,
  291    remove_leading(H, T0, T),
  292    atom_concat(bn, Id, NodeId),
  293    put_assoc(H, Map0, NodeId, Map1),
  294    Id2 is Id + 1,
  295    nodeid_map(T, Id2, Map1, Map).
  296nodeid_map([_|T], Id, Map0, Map) :-
  297    nodeid_map(T, Id, Map0, Map).
  298
  299remove_leading(H, [H|T0], T) :-
  300    !,
  301    remove_leading(H, T0, T).
  302remove_leading(_, T, T).
  303
  304
  305                 /*******************************
  306                 *            TRIPLES           *
  307                 *******************************/
  308
  309rdf_write_triples(Triples, NodeIDs, Out) :-
  310    rdf_write_triples(Triples, NodeIDs, Out, [], Anon),
  311    rdf_write_anon(Anon, NodeIDs, Out, Anon).
  312
  313rdf_write_triples([], _, _, Anon, Anon).
  314rdf_write_triples([H|T0], NodeIDs, Out, Anon0, Anon) :-
  315    arg(1, H, S),
  316    subject_triples(S, [H|T0], T, OnSubject),
  317    (   rdf_is_bnode(S)
  318    ->  rdf_write_triples(T, NodeIDs, Out, [anon(S,_,OnSubject)|Anon0], Anon)
  319    ;   rdf_write_subject(OnSubject, S, NodeIDs, Out, Anon0),
  320        rdf_write_triples(T, NodeIDs, Out, Anon0, Anon)
  321    ).
  322
  323subject_triples(S, [H|T0], T, [H|M]) :-
  324    arg(1, H, S),
  325    !,
  326    subject_triples(S, T0, T, M).
  327subject_triples(_, T, T, []).
  328
  329
  330rdf_write_anon([], _, _, _).
  331rdf_write_anon([anon(Subject, Done, Triples)|T], NodeIDs, Out, Anon) :-
  332    Done \== true,
  333    !,
  334    Done = true,
  335    rdf_write_subject(Triples, Subject, NodeIDs, Out, Anon),
  336    rdf_write_anon(T, NodeIDs, Out, Anon).
  337rdf_write_anon([_|T], NodeIDs, Out, Anon) :-
  338    rdf_write_anon(T, NodeIDs, Out, Anon).
  339
  340rdf_write_subject(Triples, Subject, NodeIDs, Out, Anon) :-
  341    rdf_write_subject(Triples, Out, Subject, NodeIDs, -, 0, Anon),
  342    !,
  343    format(Out, '~n', []).
  344rdf_write_subject(_, Subject, _, _, _) :-
  345    throw(error(rdf_save_failed(Subject), 'Internal error')).
  346
  347rdf_write_subject(Triples, Out, Subject, NodeIDs, DefNS, Indent, Anon) :-
  348    rdf_equal(rdf:type, RdfType),
  349    select(rdf(_, RdfType,Type), Triples, Triples1),
  350    \+ rdf_is_bnode(Type),
  351    rdf_id(Type, DefNS, TypeId),
  352    xml_is_name(TypeId),
  353    !,
  354    format(Out, '~*|<', [Indent]),
  355    rdf_write_id(Out, TypeId),
  356    save_about(Out, Subject, NodeIDs),
  357    save_attributes(Triples1, DefNS, Out, NodeIDs, TypeId, Indent, Anon).
  358rdf_write_subject(Triples, Out, Subject, NodeIDs, _DefNS, Indent, Anon) :-
  359    format(Out, '~*|<rdf:Description', [Indent]),
  360    save_about(Out, Subject, NodeIDs),
  361    save_attributes(Triples, rdf, Out, NodeIDs, rdf:'Description', Indent, Anon).
  362
  363xml_is_name(_NS:Atom) :-
  364    !,
  365    xml_name(Atom).
  366xml_is_name(Atom) :-
  367    xml_name(Atom).
  368
  369save_about(Out, Subject, NodeIDs) :-
  370    rdf_is_bnode(Subject),
  371    !,
  372    (   get_assoc(Subject, NodeIDs, NodeID)
  373    ->  format(Out,' rdf:nodeID="~w"', [NodeID])
  374    ;   true
  375    ).
  376save_about(Out, Subject, _) :-
  377    stream_property(Out, encoding(Encoding)),
  378    rdf_value(Subject, QSubject, Encoding),
  379    format(Out, ' rdf:about="~w"', [QSubject]),
  380    !.
  381save_about(_, _, _) :-
  382    assertion(fail).
  383
  384%!  save_attributes(+List, +DefNS, +Out, +NodeIDs, Element, +Indent, +Anon)
  385%
  386%   Save the attributes.  Short literal attributes are saved in the
  387%   tag.  Others as the content of the description element.  The
  388%   begin tag has already been filled.
  389
  390save_attributes(Triples, DefNS, Out, NodeIDs, Element, Indent, Anon) :-
  391    split_attributes(Triples, InTag, InBody),
  392    SubIndent is Indent + 2,
  393    save_attributes2(InTag, DefNS, tag, Out, NodeIDs, SubIndent, Anon),
  394    (   InBody == []
  395    ->  format(Out, '/>~n', [])
  396    ;   format(Out, '>~n', []),
  397        save_attributes2(InBody, _, body, Out, NodeIDs, SubIndent, Anon),
  398        format(Out, '~N~*|</~w>~n', [Indent, Element])
  399    ).
  400
  401%       split_attributes(+Triples, -HeadAttrs, -BodyAttr)
  402%
  403%       Split attribute (Name=Value) list into attributes for the head
  404%       and body. Attributes can only be in the head if they are literal
  405%       and appear only one time in the attribute list.
  406
  407split_attributes(Triples, HeadAttr, BodyAttr) :-
  408    duplicate_attributes(Triples, Dupls, Singles),
  409    simple_literal_attributes(Singles, HeadAttr, Rest),
  410    append(Dupls, Rest, BodyAttr).
  411
  412%       duplicate_attributes(+Attrs, -Duplicates, -Singles)
  413%
  414%       Extract attributes that appear more than onces as we cannot
  415%       dublicate an attribute in the head according to the XML rules.
  416
  417duplicate_attributes([], [], []).
  418duplicate_attributes([H|T], Dupls, Singles) :-
  419    arg(2, H, Name),
  420    named_attributes(Name, T, D, R),
  421    D \== [],
  422    append([H|D], Dupls2, Dupls),
  423    !,
  424    duplicate_attributes(R, Dupls2, Singles).
  425duplicate_attributes([H|T], Dupls2, [H|Singles]) :-
  426    duplicate_attributes(T, Dupls2, Singles).
  427
  428named_attributes(_, [], [], []) :- !.
  429named_attributes(Name, [H|T], D, R) :-
  430    (   arg(2, H, Name)
  431    ->  D = [H|DT],
  432        named_attributes(Name, T, DT, R)
  433    ;   R = [H|RT],
  434        named_attributes(Name, T, D, RT)
  435    ).
  436
  437%       simple_literal_attributes(+Attributes, -Inline, -Body)
  438%
  439%       Split attributes for (literal) attributes to be used in the
  440%       begin-tag and ones that have to go into the body of the description.
  441
  442simple_literal_attributes([], [], []).
  443simple_literal_attributes([H|TA], [H|TI], B) :-
  444    in_tag_attribute(H),
  445    !,
  446    simple_literal_attributes(TA, TI, B).
  447simple_literal_attributes([H|TA], I, [H|TB]) :-
  448    simple_literal_attributes(TA, I, TB).
  449
  450in_tag_attribute(rdf(_,P,literal(Text))) :-
  451    atom(Text),                     % may not have lang qualifier
  452    atom_length(Text, Len),
  453    Len < 60,
  454    \+ is_bag_li_predicate(P).
  455
  456
  457%       save_attributes(+List, +DefNS, +TagOrBody, +Out, +NodeIDs, +Indent, +Anon)
  458%
  459%       Save a list of attributes.
  460
  461save_attributes2([], _, _, _, _, _, _).
  462save_attributes2([H|T], DefNS, Where, Out, NodeIDs, Indent, Anon) :-
  463    save_attribute(Where, H, DefNS, Out, NodeIDs, Indent, Anon),
  464    save_attributes2(T, DefNS, Where, Out, NodeIDs, Indent, Anon).
  465
  466%!  save_attribute(+Where, +Triple, +DefNS, +Out, +NodeIDs, +Indent, +Anon)
  467
  468save_attribute(tag, rdf(_, Name, literal(Value)), DefNS, Out, _, Indent, _Anon) :-
  469    AttIndent is Indent + 2,
  470    rdf_att_id(Name, DefNS, NameText),
  471    stream_property(Out, encoding(Encoding)),
  472    xml_quote_attribute(Value, QVal, Encoding),
  473    format(Out, '~N~*|', [AttIndent]),
  474    rdf_write_id(Out, NameText),
  475    format(Out, '="~w"', [QVal]).
  476save_attribute(body, rdf(_,Name,literal(Literal)), DefNS, Out, _, Indent, _) :-
  477    !,
  478    rdf_p_id(Name, DefNS, NameText),
  479    format(Out, '~N~*|<', [Indent]),
  480    rdf_write_id(Out, NameText),
  481    (   Literal = lang(Lang, Value)
  482    ->  rdf_id(Lang, DefNS, LangText),
  483        format(Out, ' xml:lang="~w">', [LangText])
  484    ;   Literal = type(Type, Value)
  485    ->  (   rdf_equal(Type, rdf:'XMLLiteral')
  486        ->  write(Out, ' rdf:parseType="Literal">'),
  487            Value = Literal
  488        ;   stream_property(Out, encoding(Encoding)),
  489            rdf_value(Type, QVal, Encoding),
  490            format(Out, ' rdf:datatype="~w">', [QVal])
  491        )
  492    ;   atomic(Literal)
  493    ->  write(Out, '>'),
  494        Value = Literal
  495    ;   write(Out, ' rdf:parseType="Literal">'),
  496        Value = Literal
  497    ),
  498    save_attribute_value(Value, Out, Indent),
  499    write(Out, '</'), rdf_write_id(Out, NameText), write(Out, '>').
  500save_attribute(body, rdf(_, Name, Value), DefNS, Out, NodeIDs, Indent, Anon) :-
  501    rdf_is_bnode(Value),
  502    !,
  503    (   memberchk(anon(Value, Done, ValueTriples), Anon)
  504    ->  true
  505    ;   ValueTriples = []
  506    ),
  507    rdf_p_id(Name, DefNS, NameText),
  508    format(Out, '~N~*|<', [Indent]),
  509    rdf_write_id(Out, NameText),
  510    (   var(Done)
  511    ->  Done = true,
  512        SubIndent is Indent + 2,
  513        (   rdf_equal(RdfType, rdf:type),
  514            rdf_equal(ListClass, rdf:'List'),
  515            memberchk(rdf(_, RdfType, ListClass), ValueTriples)
  516        ->  format(Out, ' rdf:parseType="Collection">~n', []),
  517            rdf_save_list(ValueTriples, Out, Value, NodeIDs, DefNS, SubIndent, Anon)
  518        ;   format(Out, '>~n', []),
  519            rdf_write_subject(ValueTriples, Out, Value, NodeIDs, DefNS, SubIndent, Anon)
  520        ),
  521        format(Out, '~N~*|</', [Indent]),
  522        rdf_write_id(Out, NameText),
  523        format(Out, '>~n', [])
  524    ;   get_assoc(Value, NodeIDs, NodeID)
  525    ->  format(Out, ' rdf:nodeID="~w"/>', [NodeID])
  526    ;   assertion(fail)
  527    ).
  528save_attribute(body, rdf(_, Name, Value), DefNS, Out, _, Indent, _Anon) :-
  529    stream_property(Out, encoding(Encoding)),
  530    rdf_value(Value, QVal, Encoding),
  531    rdf_p_id(Name, DefNS, NameText),
  532    format(Out, '~N~*|<', [Indent]),
  533    rdf_write_id(Out, NameText),
  534    format(Out, ' rdf:resource="~w"/>', [QVal]).
  535
  536save_attribute_value(Value, Out, _) :-  % strings
  537    atom(Value),
  538    !,
  539    stream_property(Out, encoding(Encoding)),
  540    xml_quote_cdata(Value, QVal, Encoding),
  541    write(Out, QVal).
  542save_attribute_value(Value, Out, _) :-  % numbers
  543    number(Value),
  544    !,
  545    writeq(Out, Value).             % quoted: preserve floats
  546save_attribute_value(Value, Out, Indent) :-
  547    xml_is_dom(Value),
  548    !,
  549    XMLIndent is Indent+2,
  550    xml_write(Out, Value,
  551              [ header(false),
  552                indent(XMLIndent)
  553              ]).
  554save_attribute_value(Value, _Out, _) :-
  555    throw(error(save_attribute_value(Value), _)).
  556
  557rdf_save_list(_, _, List, _, _, _, _) :-
  558    rdf_equal(List, rdf:nil),
  559    !.
  560rdf_save_list(ListTriples, Out, List, NodeIDs, DefNS, Indent, Anon) :-
  561    rdf_equal(RdfFirst, rdf:first),
  562    memberchk(rdf(List, RdfFirst, First), ListTriples),
  563    (   rdf_is_bnode(First),
  564        memberchk(anon(First, true, FirstTriples), Anon)
  565    ->  nl(Out),
  566        rdf_write_subject(FirstTriples, Out, First, NodeIDs, DefNS, Indent, Anon)
  567    ;   stream_property(Out, encoding(Encoding)),
  568        rdf_value(First, QVal, Encoding),
  569        format(Out, '~N~*|<rdf:Description about="~w"/>',
  570               [Indent, QVal])
  571    ),
  572    (   rdf_equal(RdfRest, rdf:rest),
  573        memberchk(rdf(List, RdfRest, List2), ListTriples),
  574        \+ rdf_equal(List2, rdf:nil),
  575        memberchk(anon(List2, true, List2Triples), Anon)
  576    ->  rdf_save_list(List2Triples, Out, List2, NodeIDs, DefNS, Indent, Anon)
  577    ;   true
  578    ).
  579
  580%!  rdf_p_id(+Resource, +DefNS, -NSLocal)
  581%
  582%   As rdf_id/3 for predicate names.  Maps _:<N> to rdf:li.
  583%
  584%   @tbd    Ensure we are talking about an rdf:Bag
  585
  586rdf_p_id(LI, _, 'rdf:li') :-
  587    is_bag_li_predicate(LI),
  588    !.
  589rdf_p_id(Resource, DefNS, NSLocal) :-
  590    rdf_id(Resource, DefNS, NSLocal).
  591
  592%!  is_bag_li_predicate(+Pred) is semidet.
  593%
  594%   True if Pred is _:N, as used  for members of an rdf:Bag, rdf:Seq
  595%   or rdf:Alt.
  596
  597is_bag_li_predicate(Pred) :-
  598    atom_concat('_:', AN, Pred),
  599    catch(atom_number(AN, N), _, true), integer(N), N >= 0,
  600    !.
  601
  602
  603%!  rdf_id(+Resource, +DefNS, -NSLocal)
  604%
  605%   Generate a NS:Local name for Resource given the indicated
  606%   default namespace.  This call is used for elements.
  607
  608rdf_id(Id, NS, NS:Local) :-
  609    ns(NS, Full),
  610    Full \== '',
  611    atom_concat(Full, Local, Id),
  612    xml_name(Local),
  613    !.
  614rdf_id(Id, _, NS:Local) :-
  615    ns(NS, Full),
  616    Full \== '',
  617    atom_concat(Full, Local, Id),
  618    xml_name(Local),
  619    !.
  620rdf_id(Id, _, Id).
  621
  622
  623%!  rdf_write_id(+Out, +NSLocal) is det.
  624%
  625%   Write an identifier. We cannot use native write on it as both NS
  626%   and Local can be operators.
  627
  628rdf_write_id(Out, NS:Local) :-
  629    !,
  630    format(Out, '~w:~w', [NS, Local]).
  631rdf_write_id(Out, Atom) :-
  632    write(Out, Atom).
  633
  634
  635%!  rdf_att_id(+URI, +DefNS, -ID)
  636
  637rdf_att_id(Id, _, NS:Local) :-
  638    ns(NS, Full),
  639    Full \== '',
  640    atom_concat(Full, Local, Id),
  641    xml_name(Local),
  642    !.
  643rdf_att_id(Id, _, Id).
  644
  645
  646%!  rdf_value(+Resource, -Text, +Encoding)
  647%
  648%   According  to  "6.4  RDF  URI  References"  of  the  RDF  Syntax
  649%   specification, a URI reference is  UNICODE string not containing
  650%   control sequences, represented as  UTF-8   and  then  as escaped
  651%   US-ASCII.
  652%
  653%   NOTE: the to_be_described/1 trick  ensures   entity  rewrite  in
  654%   resources that start with 'http://t-d-b.org?'. This   is  a of a
  655%   hack to save the artchive data   in  the MultimediaN project. We
  656%   should use a more general mechanism.
  657
  658rdf_value(V, Text, Encoding) :-
  659    to_be_described(Prefix),
  660    atom_concat(Prefix, V1, V),
  661    ns(NS, Full),
  662    atom_concat(Full, Local, V1),
  663    !,
  664    xml_quote_attribute(Local, QLocal, Encoding),
  665    atomic_list_concat([Prefix, '&', NS, (';'), QLocal], Text).
  666rdf_value(V, Text, Encoding) :-
  667    ns(NS, Full),
  668    atom_concat(Full, Local, V),
  669    !,
  670    xml_quote_attribute(Local, QLocal, Encoding),
  671    atomic_list_concat(['&', NS, (';'), QLocal], Text).
  672rdf_value(V, Q, Encoding) :-
  673    xml_quote_attribute(V, Q, Encoding).
  674
  675to_be_described('http://t-d-b.org?').
  676
  677
  678                 /*******************************
  679                 *             UTIL             *
  680                 *******************************/
  681
  682ns(Id, Full) :-
  683    rdf_db:ns(Id, Full)