use crate::model::OwnedSubject;
use crate::utils::*;
use quick_xml::events::*;
use quick_xml::Writer;
use rio_api::formatter::TriplesFormatter;
use rio_api::model::*;
use std::convert::TryInto;
use std::io;
use std::io::Write;
pub struct RdfXmlFormatter<W: Write> {
writer: Writer<W>,
current_subject: Option<OwnedSubject>,
}
impl<W: Write> RdfXmlFormatter<W> {
pub fn new(write: W) -> io::Result<Self> {
Self {
writer: Writer::new(write),
current_subject: None,
}
.write_start()
}
pub fn with_indentation(write: W, indentation_size: usize) -> io::Result<Self> {
Self {
writer: Writer::new_with_indent(write, b' ', indentation_size),
current_subject: None,
}
.write_start()
}
fn write_start(mut self) -> io::Result<Self> {
self.writer
.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
.map_err(map_err)?;
let mut rdf_open = BytesStart::new("rdf:RDF");
rdf_open.push_attribute(("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"));
self.writer
.write_event(Event::Start(rdf_open))
.map_err(map_err)?;
Ok(self)
}
pub fn finish(mut self) -> io::Result<W> {
if self.current_subject.is_some() {
self.writer
.write_event(Event::End(BytesEnd::new("rdf:Description")))
.map_err(map_err)?;
}
self.writer
.write_event(Event::End(BytesEnd::new("rdf:RDF")))
.map_err(map_err)?;
let mut inner = self.writer.into_inner();
inner.flush()?;
Ok(inner)
}
}
impl<W: Write> TriplesFormatter for RdfXmlFormatter<W> {
type Error = io::Error;
fn format(&mut self, triple: &Triple<'_>) -> io::Result<()> {
if self.current_subject.as_ref().map(|v| v.into()) != Some(triple.subject) {
if self.current_subject.is_some() {
self.writer
.write_event(Event::End(BytesEnd::new("rdf:Description")))
.map_err(map_err)?;
}
let mut description_open = BytesStart::new("rdf:Description");
match triple.subject {
Subject::NamedNode(n) => description_open.push_attribute(("rdf:about", n.iri)),
Subject::BlankNode(n) => description_open.push_attribute(("rdf:nodeID", n.id)),
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"RDF/XML only supports named or blank subject",
))
}
}
self.writer
.write_event(Event::Start(description_open))
.map_err(map_err)?;
}
let (prop_prefix, prop_value) = split_iri(triple.predicate.iri);
let (prop_qname, prop_xmlns) = if prop_value.is_empty() {
("prop:", ("xmlns:prop", prop_prefix))
} else {
(prop_value, ("xmlns", prop_prefix))
};
let mut property_open = BytesStart::new(prop_qname);
property_open.push_attribute(prop_xmlns);
let content = match triple.object {
Term::NamedNode(n) => {
property_open.push_attribute(("rdf:resource", n.iri));
None
}
Term::BlankNode(n) => {
property_open.push_attribute(("rdf:nodeID", n.id));
None
}
Term::Literal(l) => match l {
Literal::Simple { value } => Some(value),
Literal::LanguageTaggedString { value, language } => {
property_open.push_attribute(("xml:lang", language));
Some(value)
}
Literal::Typed { value, datatype } => {
property_open.push_attribute(("rdf:datatype", datatype.iri));
Some(value)
}
},
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"RDF/XML only supports named, blank or literal object",
))
}
};
if let Some(content) = content {
self.writer
.write_event(Event::Start(property_open))
.map_err(map_err)?;
self.writer
.write_event(Event::Text(BytesText::new(content)))
.map_err(map_err)?;
self.writer
.write_event(Event::End(BytesEnd::new(prop_qname)))
.map_err(map_err)?;
} else {
self.writer
.write_event(Event::Empty(property_open))
.map_err(map_err)?;
}
self.current_subject = Some(triple.subject.try_into()?);
Ok(())
}
}
fn map_err(error: quick_xml::Error) -> io::Error {
if let quick_xml::Error::Io(error) = error {
io::Error::new(error.kind(), error)
} else {
io::Error::new(io::ErrorKind::Other, error)
}
}
fn split_iri(iri: &str) -> (&str, &str) {
if let Some(position_base) = iri.rfind(|c| !is_name_char(c) || c == ':') {
if let Some(position_add) = iri[position_base..].find(|c| is_name_start_char(c) && c != ':')
{
(
&iri[..position_base + position_add],
&iri[position_base + position_add..],
)
} else {
(iri, "")
}
} else {
(iri, "")
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_split_iri() {
assert_eq!(
split_iri("http://schema.org/Person"),
("http://schema.org/", "Person")
);
assert_eq!(split_iri("http://schema.org/"), ("http://schema.org/", ""));
}
#[cfg(feature = "rio_api/star")]
#[test]
fn formatting_rdf_star_fails_cleanly() {
use rio_api::formatter::TriplesFormatter;
let iri = NamedNode { iri: "tag:iri" };
let triple = Triple {
subject: Triple {
subject: iri.into(),
predicate: iri,
object: iri.into(),
}
.into(),
predicate: iri,
object: iri.into(),
};
let mut fmt = RdfXmlFormatter::new(std::io::sink()).unwrap();
let res = fmt.format(&triple).and_then(|_| fmt.finish());
assert!(res.is_err());
}
}