use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;
use oxigraph::io::{RdfParseError, RdfParser, RdfSyntaxError, ReaderQuadParser, SliceQuadParser};
use oxigraph::model::{GraphName, Quad};
use crate::io::Syntax;
use crate::io::bom::{BomStrippingReader, strip_utf8_bom_bytes, strip_utf8_bom_str};
use crate::io::location::SourceLocation;
use crate::world::{FeatureMap, FeatureValue};
use crate::{Error, Model, Result};
#[derive(Clone, Debug)]
pub struct Parser {
syntax: Syntax,
base_iri: Option<String>,
graph_target: GraphTarget,
features: FeatureMap,
}
impl Parser {
#[must_use]
pub fn for_syntax(syntax: Syntax) -> Self {
Self {
syntax,
base_iri: None,
graph_target: GraphTarget::DefaultGraph,
features: FeatureMap::new(),
}
}
pub fn set_feature(&self, iri: impl Into<String>, value: FeatureValue) {
self.features.set(iri, value);
}
#[must_use]
pub fn feature(&self, iri: &str) -> Option<FeatureValue> {
self.features.get(iri)
}
#[must_use]
pub fn syntax(&self) -> Syntax {
self.syntax
}
pub fn base_iri(mut self, base_iri: impl Into<String>) -> Result<Self> {
let base_iri = base_iri.into();
let _ = RdfParser::from_format(self.syntax.to_oxigraph())
.with_base_iri(&base_iri)
.map_err(|error| Error::InvalidRdf(error.to_string()))?;
self.base_iri = Some(base_iri);
Ok(self)
}
#[must_use]
pub fn graph_target(mut self, target: GraphTarget) -> Self {
self.graph_target = target;
self
}
pub fn parse_reader<R: Read>(&self, reader: R) -> Result<QuadStream<BomStrippingReader<R>>> {
Ok(QuadStream {
inner: QuadStreamInner::Reader(
self.build()?.for_reader(BomStrippingReader::new(reader)),
),
graph_target: self.graph_target.clone(),
})
}
pub fn parse_slice<'a>(
&self,
slice: &'a (impl AsRef<[u8]> + ?Sized),
) -> Result<SliceStream<'a>> {
let bytes = strip_utf8_bom_bytes(slice.as_ref());
Ok(SliceStream {
inner: self.build()?.for_slice(bytes),
graph_target: self.graph_target.clone(),
})
}
pub fn parse_str<'a>(&self, input: &'a str) -> Result<SliceStream<'a>> {
self.parse_slice(strip_utf8_bom_str(input).as_bytes())
}
pub fn parse_path(
&self,
path: impl AsRef<Path>,
) -> Result<QuadStream<BomStrippingReader<BufReader<File>>>> {
let path = path.as_ref();
let file = File::open(path).map_err(|error| io_with_path(error, path))?;
self.parse_reader(BufReader::new(file))
}
pub fn parse_path_with_extension(
path: impl AsRef<Path>,
) -> Result<(Syntax, QuadStream<BomStrippingReader<BufReader<File>>>)> {
let path = path.as_ref();
let extension = path
.extension()
.and_then(|ext| ext.to_str())
.ok_or_else(|| {
Error::Unsupported(format!(
"path '{}' has no file extension for syntax detection",
path.display()
))
})?;
let syntax = Syntax::from_extension(extension)?;
let mut parser = Parser::for_syntax(syntax);
if syntax.supports_datasets() {
parser = parser.graph_target(GraphTarget::Dataset);
}
let stream = parser.parse_path(path)?;
Ok((syntax, stream))
}
pub fn load_into(&self, model: &Model, reader: impl Read) -> Result<usize> {
let mut processed = 0usize;
let mut newly_inserted = 0usize;
for item in self.parse_reader(reader)? {
let quad = item.map_err(|error| annotate_partial_load(error, newly_inserted))?;
if model
.insert_quad(quad)
.map_err(|error| annotate_partial_load(error, newly_inserted))?
{
newly_inserted += 1;
}
processed += 1;
}
Ok(processed)
}
pub fn load_collecting(&self, model: &Model, reader: impl Read) -> Result<usize> {
let quads = self.parse_reader(reader)?.collect::<Result<Vec<_>>>()?;
let total = quads.len();
let mut newly_inserted = Vec::new();
for quad in quads {
match model.insert_quad(quad.clone()) {
Ok(true) => newly_inserted.push(quad),
Ok(false) => {}
Err(error) => {
let mut rollback_error: Option<Error> = None;
for inserted in newly_inserted.into_iter().rev() {
if let Err(remove_error) = model.remove_quad(&inserted) {
if rollback_error.is_none() {
rollback_error = Some(remove_error);
}
}
}
return Err(match rollback_error {
Some(remove_error) => Error::Storage(format!(
"insert failed ({error}); rollback also failed ({remove_error})"
)),
None => error,
});
}
}
}
Ok(total)
}
pub fn load_path_into(&self, model: &Model, path: impl AsRef<Path>) -> Result<usize> {
let path = path.as_ref();
let file = File::open(path).map_err(|error| io_with_path(error, path))?;
self.load_into(model, BufReader::new(file))
}
pub fn load_path_collecting(&self, model: &Model, path: impl AsRef<Path>) -> Result<usize> {
let path = path.as_ref();
let file = File::open(path).map_err(|error| io_with_path(error, path))?;
self.load_collecting(model, BufReader::new(file))
}
pub fn load_transactional(&self, model: &Model, reader: impl Read) -> Result<usize> {
let quads = self.parse_reader(reader)?.collect::<Result<Vec<_>>>()?;
let total = quads.len();
model.transaction(|tx| {
for quad in quads {
tx.insert_quad(quad)?;
}
Ok(total)
})
}
pub fn load_path_transactional(&self, model: &Model, path: impl AsRef<Path>) -> Result<usize> {
let path = path.as_ref();
let file = File::open(path).map_err(|error| io_with_path(error, path))?;
self.load_transactional(model, BufReader::new(file))
}
fn build(&self) -> Result<RdfParser> {
let mut parser = RdfParser::from_format(self.syntax.to_oxigraph()).rename_blank_nodes();
if let Some(base_iri) = &self.base_iri {
parser = parser
.with_base_iri(base_iri)
.map_err(|error| Error::InvalidRdf(error.to_string()))?;
}
match &self.graph_target {
GraphTarget::DefaultGraph => {
parser = parser
.with_default_graph(GraphName::DefaultGraph)
.without_named_graphs();
}
GraphTarget::Named(graph_name) => {
parser = parser.with_default_graph(graph_name.clone());
}
GraphTarget::Dataset => {
if !self.syntax.supports_datasets() {
return Err(Error::Unsupported(format!(
"syntax '{}' does not support dataset/named-graph input; use GraphTarget::DefaultGraph or Named",
self.syntax.name()
)));
}
}
}
Ok(parser)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum GraphTarget {
DefaultGraph,
Named(GraphName),
Dataset,
}
#[must_use]
pub struct QuadStream<R: Read> {
inner: QuadStreamInner<R>,
graph_target: GraphTarget,
}
enum QuadStreamInner<R: Read> {
Reader(ReaderQuadParser<R>),
}
impl<R: Read> Iterator for QuadStream<R> {
type Item = Result<Quad>;
fn next(&mut self) -> Option<Self::Item> {
match &mut self.inner {
QuadStreamInner::Reader(parser) => parser.next().map(|item| {
map_parse_result(item)
.and_then(|quad| enforce_graph_target(quad, &self.graph_target))
}),
}
}
}
#[must_use]
pub struct SliceStream<'a> {
inner: SliceQuadParser<'a>,
graph_target: GraphTarget,
}
impl Iterator for SliceStream<'_> {
type Item = Result<Quad>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|item| {
map_syntax_result(item).and_then(|quad| enforce_graph_target(quad, &self.graph_target))
})
}
}
fn enforce_graph_target(quad: Quad, target: &GraphTarget) -> Result<Quad> {
match target {
GraphTarget::DefaultGraph | GraphTarget::Dataset => Ok(quad),
GraphTarget::Named(expected) => {
if quad.graph_name == *expected {
Ok(quad)
} else {
Err(Error::parse(
format!(
"named graph '{}' is not allowed for GraphTarget::Named('{expected}')",
quad.graph_name
),
None,
))
}
}
}
}
fn map_parse_result(result: std::result::Result<Quad, RdfParseError>) -> Result<Quad> {
result.map_err(map_rdf_parse_error)
}
pub(crate) fn map_rdf_parse_error(error: RdfParseError) -> Error {
match error {
RdfParseError::Io(error) => Error::Io(error),
RdfParseError::Syntax(error) => map_syntax_error(error),
}
}
fn map_syntax_result(result: std::result::Result<Quad, RdfSyntaxError>) -> Result<Quad> {
result.map_err(map_syntax_error)
}
fn map_syntax_error(error: RdfSyntaxError) -> Error {
let location = error.location().map(SourceLocation::from_range);
Error::parse(strip_embedded_location(error.to_string()), location)
}
fn strip_embedded_location(message: String) -> String {
const MARKERS: &[&str] = &[
"Parser error at line ",
"parser error at line ",
"Error at line ",
"error at line ",
];
for marker in MARKERS {
if let Some(rest) = message.strip_prefix(marker) {
if let Some((_, semantic)) = rest.split_once(": ") {
return semantic.to_owned();
}
}
}
if let Some(idx) = message.find(" at line ") {
if let Some((_, semantic)) = message[idx..].split_once(": ") {
return semantic.to_owned();
}
}
message
}
fn annotate_partial_load(error: Error, newly_inserted: usize) -> Error {
if newly_inserted == 0 {
return error;
}
let note = format!(
"partial load newly inserted {newly_inserted} statement(s) that remain in the model (ADR-007 progressive load)"
);
match error {
Error::Parse(mut parse) => {
parse.message = format!("{}; {note}", parse.message);
Error::Parse(parse)
}
Error::Io(io_error) => Error::Io(std::io::Error::new(
io_error.kind(),
format!("{io_error}; {note}"),
)),
Error::Storage(message) => Error::Storage(format!("{message}; {note}")),
Error::InvalidRdf(message) => Error::InvalidRdf(format!("{message}; {note}")),
Error::Serialize(message) => Error::Serialize(format!("{message}; {note}")),
Error::Unsupported(message) => Error::Unsupported(format!("{message}; {note}")),
Error::SparqlParse(message) => Error::SparqlParse(format!("{message}; {note}")),
Error::SparqlEvaluation(message) => Error::SparqlEvaluation(format!("{message}; {note}")),
Error::OpenStore { path, message } => Error::OpenStore {
path,
message: format!("{message}; {note}"),
},
}
}
fn io_with_path(error: std::io::Error, path: &Path) -> Error {
Error::Io(std::io::Error::new(
error.kind(),
format!("{}: {}", path.display(), error),
))
}