diff --git a/lib/src/sparql/json_results.rs b/lib/src/sparql/json_results.rs new file mode 100644 index 00000000..97dbef17 --- /dev/null +++ b/lib/src/sparql/json_results.rs @@ -0,0 +1,123 @@ +//! Implementation of [SPARQL Query Results XML Format](https://www.w3.org/TR/sparql11-results-json/) + +use crate::model::*; +use crate::sparql::model::*; +use crate::Result; +use failure::format_err; +use std::io::Write; + +pub fn write_json_results(results: QueryResult<'_>, mut sink: W) -> Result { + match results { + QueryResult::Boolean(value) => { + sink.write_all(b"{\"head\":{},\"boolean\":")?; + sink.write_all(if value { b"true" } else { b"false" })?; + sink.write_all(b"}")?; + } + QueryResult::Bindings(bindings) => { + let (variables, results) = bindings.destruct(); + sink.write_all(b"{\"head\":{\"vars\":[")?; + let mut start_vars = true; + for variable in &variables { + if start_vars { + start_vars = false; + } else { + sink.write_all(b",")?; + } + write_escaped_json_string(variable.name()?, &mut sink)?; + } + sink.write_all(b"]},\"results\":{\"bindings\":[")?; + let mut start_bindings = true; + for result in results { + if start_bindings { + start_bindings = false; + } else { + sink.write_all(b",")?; + } + sink.write_all(b"{")?; + + let result = result?; + let mut start_binding = true; + for (i, value) in result.into_iter().enumerate() { + if let Some(term) = value { + if start_binding { + start_binding = false; + } else { + sink.write_all(b",")?; + } + write_escaped_json_string(variables[i].name()?, &mut sink)?; + match term { + Term::NamedNode(uri) => { + sink.write_all(b":{\"type\":\"uri\",\"value\":")?; + write_escaped_json_string(uri.as_str(), &mut sink)?; + sink.write_all(b"}")?; + } + Term::BlankNode(bnode) => { + sink.write_all(b":{\"type\":\"bnode\",\"value\":")?; + sink.write_fmt(format_args!("{}", bnode.as_uuid().to_simple()))?; + sink.write_all(b"}")?; + } + Term::Literal(literal) => { + sink.write_all(b":{\"type\":\"literal\",\"value\":")?; + write_escaped_json_string(&literal.value(), &mut sink)?; + if let Some(language) = literal.language() { + sink.write_all(b",\"xml:lang\":")?; + write_escaped_json_string(language, &mut sink)?; + } else if !literal.is_plain() { + sink.write_all(b",\"datatype\":")?; + write_escaped_json_string( + literal.datatype().as_str(), + &mut sink, + )?; + } + sink.write_all(b"}")?; + } + } + } + } + sink.write_all(b"}")?; + } + sink.write_all(b"]}}")?; + } + QueryResult::Graph(_) => { + return Err(format_err!( + "Graphs could not be formatted to SPARQL query results XML format" + )); + } + } + Ok(sink) +} + +fn write_escaped_json_string(s: &str, sink: &mut impl Write) -> Result<()> { + sink.write_all(b"\"")?; + for c in s.chars() { + match c { + '\\' => sink.write_all(b"\\\\"), + '"' => sink.write_all(b"\\\""), + c => { + if c < char::from(32) { + match c { + '\u{08}' => sink.write_all(b"\\b"), + '\u{0C}' => sink.write_all(b"\\f"), + '\n' => sink.write_all(b"\\n"), + '\r' => sink.write_all(b"\\r"), + '\t' => sink.write_all(b"\\t"), + c => { + let mut c = c as u8; + let mut result = [b'\\', b'u', 0, 0, 0, 0]; + for i in (2..6).rev() { + let ch = c % 16; + result[i] = ch + if ch < 10 { b'0' } else { b'A' }; + c /= 16; + } + sink.write_all(&result) + } + } + } else { + sink.write_fmt(format_args!("{}", c)) + } + } + }?; + } + sink.write_all(b"\"")?; + Ok(()) +} diff --git a/lib/src/sparql/mod.rs b/lib/src/sparql/mod.rs index dabab272..6632a997 100644 --- a/lib/src/sparql/mod.rs +++ b/lib/src/sparql/mod.rs @@ -2,6 +2,7 @@ mod algebra; mod eval; +mod json_results; mod model; mod parser; mod plan; diff --git a/lib/src/sparql/model.rs b/lib/src/sparql/model.rs index 528dd40a..067b6721 100644 --- a/lib/src/sparql/model.rs +++ b/lib/src/sparql/model.rs @@ -1,6 +1,7 @@ use crate::model::*; +use crate::sparql::json_results::write_json_results; use crate::sparql::xml_results::{read_xml_results, write_xml_results}; -use crate::Result; +use crate::{FileSyntax, Result}; use failure::format_err; use std::fmt; use std::io::{BufRead, Write}; @@ -20,12 +21,14 @@ impl<'a> QueryResult<'a> { pub fn read(reader: impl BufRead + 'a, syntax: QueryResultSyntax) -> Result { match syntax { QueryResultSyntax::Xml => read_xml_results(reader), + QueryResultSyntax::Json => unimplemented!(), } } pub fn write(self, writer: W, syntax: QueryResultSyntax) -> Result { match syntax { QueryResultSyntax::Xml => write_xml_results(self, writer), + QueryResultSyntax::Json => write_json_results(self, writer), } } } @@ -35,6 +38,40 @@ impl<'a> QueryResult<'a> { pub enum QueryResultSyntax { /// [SPARQL Query Results XML Format](http://www.w3.org/TR/rdf-sparql-XMLres/) Xml, + /// [SPARQL Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/) + Json, +} + +impl FileSyntax for QueryResultSyntax { + fn iri(self) -> &'static str { + unimplemented!() + } + + fn media_type(self) -> &'static str { + match self { + QueryResultSyntax::Xml => "application/sparql-results+xml", + QueryResultSyntax::Json => "application/sparql-results+json", + } + } + + fn file_extension(self) -> &'static str { + match self { + QueryResultSyntax::Xml => "srx", + QueryResultSyntax::Json => "srj", + } + } + + fn from_mime_type(media_type: &str) -> Option { + if let Some(base_type) = media_type.split(';').next() { + match base_type { + "application/sparql-results+xml" => Some(QueryResultSyntax::Xml), + "application/sparql-results+json" => Some(QueryResultSyntax::Json), + _ => None, + } + } else { + None + } + } } /// An iterator over results bindings diff --git a/server/src/main.rs b/server/src/main.rs index c040175b..6cd6a791 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,6 +1,7 @@ use clap::App; use clap::Arg; use clap::ArgMatches; +use rouille::input::priority_header_preferred; use rouille::url::form_urlencoded; use rouille::{start_server, Request, Response}; use rudf::sparql::QueryResult; @@ -89,20 +90,22 @@ fn handle_request( Response::text("No content given").with_status_code(400) } } - ("/query", "GET") => { - evaluate_urlencoded_sparql_query(connection, request.raw_query_string().as_bytes()) - } + ("/query", "GET") => evaluate_urlencoded_sparql_query( + connection, + request.raw_query_string().as_bytes(), + request, + ), ("/query", "POST") => { if let Some(mut body) = request.data() { if let Some(content_type) = request.header("Content-Type") { if content_type.starts_with("application/sparql-query") { let mut buffer = String::default(); body.read_to_string(&mut buffer).unwrap(); - evaluate_sparql_query(connection, &buffer) + evaluate_sparql_query(connection, &buffer, request) } else if content_type.starts_with("application/x-www-form-urlencoded") { let mut buffer = Vec::default(); body.read_to_end(&mut buffer).unwrap(); - evaluate_urlencoded_sparql_query(connection, &buffer) + evaluate_urlencoded_sparql_query(connection, &buffer, request) } else { Response::text(format!( "No supported content Content-Type given: {}", @@ -124,31 +127,77 @@ fn handle_request( fn evaluate_urlencoded_sparql_query( connection: R, encoded: &[u8], + request: &Request, ) -> Response { if let Some((_, query)) = form_urlencoded::parse(encoded).find(|(k, _)| k == "query") { - evaluate_sparql_query(connection, &query) + evaluate_sparql_query(connection, &query, request) } else { Response::text("You should set the 'query' parameter").with_status_code(400) } } -fn evaluate_sparql_query(connection: R, query: &str) -> Response { +fn evaluate_sparql_query( + connection: R, + query: &str, + request: &Request, +) -> Response { //TODO: stream match connection.prepare_query(query, None) { Ok(query) => match query.exec().unwrap() { QueryResult::Graph(triples) => { + let supported_formats = [ + GraphSyntax::NTriples.media_type(), + GraphSyntax::Turtle.media_type(), + ]; + let format = if let Some(accept) = request.header("Accept") { + if let Some(media_type) = + priority_header_preferred(accept, supported_formats.iter().cloned()) + .and_then(|p| GraphSyntax::from_mime_type(supported_formats[p])) + { + media_type + } else { + return Response::text(format!( + "No supported Accept given: {}. Supported format: {:?}", + accept, supported_formats + )) + .with_status_code(415); + } + } else { + GraphSyntax::NTriples + }; let mut result = String::default(); for triple in triples { writeln!(&mut result, "{}", triple.unwrap()).unwrap() } - Response::from_data(GraphSyntax::NTriples.media_type(), result.into_bytes()) + Response::from_data(format.media_type(), result.into_bytes()) + } + result => { + let supported_formats = [ + QueryResultSyntax::Xml.media_type(), + QueryResultSyntax::Json.media_type(), + ]; + let format = if let Some(accept) = request.header("Accept") { + if let Some(media_type) = + priority_header_preferred(accept, supported_formats.iter().cloned()) + .and_then(|p| QueryResultSyntax::from_mime_type(supported_formats[p])) + { + media_type + } else { + return Response::text(format!( + "No supported Accept given: {}. Supported format: {:?}", + accept, supported_formats + )) + .with_status_code(415); + } + } else { + QueryResultSyntax::Xml + }; + + Response::from_data( + format.media_type(), + result.write(Vec::default(), format).unwrap(), + ) } - result => Response::from_data( - "application/sparql-results", - result - .write(Vec::default(), QueryResultSyntax::Xml) - .unwrap(), - ), }, Err(error) => Response::text(error.to_string()).with_status_code(400), }