|
|
@ -1,17 +1,21 @@ |
|
|
|
|
|
|
|
extern crate clap; |
|
|
|
|
|
|
|
#[macro_use] |
|
|
|
|
|
|
|
extern crate failure; |
|
|
|
|
|
|
|
extern crate futures; |
|
|
|
extern crate gotham; |
|
|
|
extern crate gotham; |
|
|
|
#[macro_use] |
|
|
|
#[macro_use] |
|
|
|
extern crate gotham_derive; |
|
|
|
extern crate gotham_derive; |
|
|
|
extern crate futures; |
|
|
|
|
|
|
|
extern crate hyper; |
|
|
|
extern crate hyper; |
|
|
|
|
|
|
|
#[macro_use] |
|
|
|
|
|
|
|
extern crate lazy_static; |
|
|
|
extern crate mime; |
|
|
|
extern crate mime; |
|
|
|
extern crate rudf; |
|
|
|
extern crate rudf; |
|
|
|
extern crate serde; |
|
|
|
extern crate serde; |
|
|
|
extern crate url; |
|
|
|
|
|
|
|
#[macro_use] |
|
|
|
#[macro_use] |
|
|
|
extern crate serde_derive; |
|
|
|
extern crate serde_derive; |
|
|
|
#[macro_use] |
|
|
|
#[macro_use] |
|
|
|
extern crate failure; |
|
|
|
extern crate tera; |
|
|
|
extern crate clap; |
|
|
|
extern crate url; |
|
|
|
|
|
|
|
|
|
|
|
use clap::App; |
|
|
|
use clap::App; |
|
|
|
use clap::Arg; |
|
|
|
use clap::Arg; |
|
|
@ -35,6 +39,7 @@ use hyper::Body; |
|
|
|
use hyper::HeaderMap; |
|
|
|
use hyper::HeaderMap; |
|
|
|
use hyper::Response; |
|
|
|
use hyper::Response; |
|
|
|
use hyper::StatusCode; |
|
|
|
use hyper::StatusCode; |
|
|
|
|
|
|
|
use mime::Mime; |
|
|
|
use rudf::model::Graph; |
|
|
|
use rudf::model::Graph; |
|
|
|
use rudf::rio::ntriples::read_ntriples; |
|
|
|
use rudf::rio::ntriples::read_ntriples; |
|
|
|
use rudf::sparql::algebra::QueryResult; |
|
|
|
use rudf::sparql::algebra::QueryResult; |
|
|
@ -46,9 +51,26 @@ use rudf::store::MemoryGraph; |
|
|
|
use rudf::store::RocksDbDataset; |
|
|
|
use rudf::store::RocksDbDataset; |
|
|
|
use std::fs::File; |
|
|
|
use std::fs::File; |
|
|
|
use std::panic::RefUnwindSafe; |
|
|
|
use std::panic::RefUnwindSafe; |
|
|
|
|
|
|
|
use std::str::FromStr; |
|
|
|
use std::sync::Arc; |
|
|
|
use std::sync::Arc; |
|
|
|
|
|
|
|
use tera::Context; |
|
|
|
|
|
|
|
use tera::Tera; |
|
|
|
use url::form_urlencoded; |
|
|
|
use url::form_urlencoded; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lazy_static! { |
|
|
|
|
|
|
|
static ref TERA: Tera = { |
|
|
|
|
|
|
|
let mut tera = compile_templates!("templates/**/*"); |
|
|
|
|
|
|
|
tera.autoescape_on(vec![]); |
|
|
|
|
|
|
|
tera |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
static ref APPLICATION_SPARQL_QUERY_UTF_8: Mime = |
|
|
|
|
|
|
|
"application/sparql-query; charset=utf-8".parse().unwrap(); |
|
|
|
|
|
|
|
static ref APPLICATION_SPARQL_RESULTS_UTF_8: Mime = |
|
|
|
|
|
|
|
"application/sparql-results; charset=utf-8".parse().unwrap(); |
|
|
|
|
|
|
|
static ref APPLICATION_N_TRIPLES_UTF_8: Mime = |
|
|
|
|
|
|
|
"application/n-triples; charset=utf-8".parse().unwrap(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
pub fn main() -> Result<(), failure::Error> { |
|
|
|
pub fn main() -> Result<(), failure::Error> { |
|
|
|
let matches = App::new("Rudf SPARQL server") |
|
|
|
let matches = App::new("Rudf SPARQL server") |
|
|
|
.arg( |
|
|
|
.arg( |
|
|
@ -72,15 +94,15 @@ pub fn main() -> Result<(), failure::Error> { |
|
|
|
|
|
|
|
|
|
|
|
let file = matches.value_of("file").map(|v| v.to_string()); |
|
|
|
let file = matches.value_of("file").map(|v| v.to_string()); |
|
|
|
if let Some(file) = file { |
|
|
|
if let Some(file) = file { |
|
|
|
main_with_dataset(Arc::new(RocksDbDataset::open(file)?), matches) |
|
|
|
main_with_dataset(Arc::new(RocksDbDataset::open(file)?), &matches) |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
main_with_dataset(Arc::new(MemoryDataset::default()), matches) |
|
|
|
main_with_dataset(Arc::new(MemoryDataset::default()), &matches) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fn main_with_dataset<D: SparqlDataset + Send + Sync + RefUnwindSafe + 'static>( |
|
|
|
fn main_with_dataset<D: SparqlDataset + Send + Sync + RefUnwindSafe + 'static>( |
|
|
|
dataset: Arc<D>, |
|
|
|
dataset: Arc<D>, |
|
|
|
matches: ArgMatches, |
|
|
|
matches: &ArgMatches, |
|
|
|
) -> Result<(), failure::Error> { |
|
|
|
) -> Result<(), failure::Error> { |
|
|
|
if let Some(nt_file) = matches.value_of("ntriples") { |
|
|
|
if let Some(nt_file) = matches.value_of("ntriples") { |
|
|
|
println!("Loading NTriples file {}", nt_file); |
|
|
|
println!("Loading NTriples file {}", nt_file); |
|
|
@ -92,16 +114,32 @@ fn main_with_dataset<D: SparqlDataset + Send + Sync + RefUnwindSafe + 'static>( |
|
|
|
|
|
|
|
|
|
|
|
let addr = matches.value_of("bind").unwrap_or("127.0.0.1:7878"); |
|
|
|
let addr = matches.value_of("bind").unwrap_or("127.0.0.1:7878"); |
|
|
|
println!("Listening for requests at http://{}", addr); |
|
|
|
println!("Listening for requests at http://{}", addr); |
|
|
|
gotham::start(addr.to_string(), router(dataset)); |
|
|
|
gotham::start(addr.to_string(), router(dataset, addr.to_string())); |
|
|
|
Ok(()) |
|
|
|
Ok(()) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fn router<D: SparqlDataset + Send + Sync + RefUnwindSafe + 'static>(dataset: Arc<D>) -> Router { |
|
|
|
fn router<D: SparqlDataset + Send + Sync + RefUnwindSafe + 'static>( |
|
|
|
let store = SparqlStore(dataset); |
|
|
|
dataset: Arc<D>, |
|
|
|
let middleware = StateMiddleware::new(store); |
|
|
|
base: String, |
|
|
|
|
|
|
|
) -> Router { |
|
|
|
|
|
|
|
let middleware = StateMiddleware::new(GothamState { dataset, base }); |
|
|
|
let pipeline = single_middleware(middleware); |
|
|
|
let pipeline = single_middleware(middleware); |
|
|
|
let (chain, pipelines) = single_pipeline(pipeline); |
|
|
|
let (chain, pipelines) = single_pipeline(pipeline); |
|
|
|
build_router(chain, pipelines, |route| { |
|
|
|
build_router(chain, pipelines, |route| { |
|
|
|
|
|
|
|
route |
|
|
|
|
|
|
|
.get("/") |
|
|
|
|
|
|
|
.to(|mut state: State| -> (State, Response<Body>) { |
|
|
|
|
|
|
|
let gotham_state: GothamState<D> = GothamState::take_from(&mut state); |
|
|
|
|
|
|
|
let mut context = Context::new(); |
|
|
|
|
|
|
|
context.insert("endpoint", &format!("//{}/query", gotham_state.base)); |
|
|
|
|
|
|
|
let response = create_response( |
|
|
|
|
|
|
|
&state, |
|
|
|
|
|
|
|
StatusCode::OK, |
|
|
|
|
|
|
|
mime::TEXT_HTML_UTF_8, |
|
|
|
|
|
|
|
TERA.render("query.html", &context).unwrap(), |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
(state, response) |
|
|
|
|
|
|
|
}); |
|
|
|
route.associate("/query", |assoc| { |
|
|
|
route.associate("/query", |assoc| { |
|
|
|
assoc |
|
|
|
assoc |
|
|
|
.get() |
|
|
|
.get() |
|
|
@ -118,19 +156,18 @@ fn router<D: SparqlDataset + Send + Sync + RefUnwindSafe + 'static>(dataset: Arc |
|
|
|
.concat2() |
|
|
|
.concat2() |
|
|
|
.then(|body| match body { |
|
|
|
.then(|body| match body { |
|
|
|
Ok(body) => { |
|
|
|
Ok(body) => { |
|
|
|
let response = match HeaderMap::borrow_from(&state) |
|
|
|
let content_type: Option<Result<Mime,failure::Error>> = HeaderMap::borrow_from(&state) |
|
|
|
.get(CONTENT_TYPE) |
|
|
|
.get(CONTENT_TYPE) |
|
|
|
.cloned() |
|
|
|
.map(|content_type| Ok(Mime::from_str(content_type.to_str()?)?)); |
|
|
|
{ |
|
|
|
let response = match content_type { |
|
|
|
Some(content_type) => { |
|
|
|
Some(Ok(content_type)) => match (content_type.type_(), content_type.subtype()) { |
|
|
|
if content_type == "application/sparql-query" { |
|
|
|
(mime::APPLICATION, subtype) if subtype == APPLICATION_SPARQL_QUERY_UTF_8.subtype() => { |
|
|
|
evaluate_sparql_query::<D>( |
|
|
|
evaluate_sparql_query::<D>( |
|
|
|
&mut state, |
|
|
|
&mut state, |
|
|
|
&body.into_bytes(), |
|
|
|
&body.into_bytes(), |
|
|
|
) |
|
|
|
) |
|
|
|
} else if content_type |
|
|
|
}, |
|
|
|
== "application/x-www-form-urlencoded" |
|
|
|
(mime::APPLICATION, mime::WWW_FORM_URLENCODED) => { |
|
|
|
{ |
|
|
|
|
|
|
|
match parse_urlencoded_query_request(&body.into_bytes()) |
|
|
|
match parse_urlencoded_query_request(&body.into_bytes()) |
|
|
|
{ |
|
|
|
{ |
|
|
|
Ok(parsed_request) => evaluate_sparql_query::<D>( |
|
|
|
Ok(parsed_request) => evaluate_sparql_query::<D>( |
|
|
@ -143,22 +180,21 @@ fn router<D: SparqlDataset + Send + Sync + RefUnwindSafe + 'static>(dataset: Arc |
|
|
|
StatusCode::BAD_REQUEST, |
|
|
|
StatusCode::BAD_REQUEST, |
|
|
|
), |
|
|
|
), |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
}, |
|
|
|
error_to_response( |
|
|
|
_ => error_to_response( |
|
|
|
&state, |
|
|
|
&state, |
|
|
|
&format_err!( |
|
|
|
&format_err!("Unsupported Content-Type: {:?}", content_type), |
|
|
|
"Unsupported Content-Type: {:?}", |
|
|
|
|
|
|
|
content_type |
|
|
|
|
|
|
|
), |
|
|
|
|
|
|
|
StatusCode::BAD_REQUEST, |
|
|
|
StatusCode::BAD_REQUEST, |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
Some(Err(error)) => error_to_response( |
|
|
|
None => error_to_response( |
|
|
|
|
|
|
|
&state, |
|
|
|
&state, |
|
|
|
&format_err!( |
|
|
|
&format_err!("The request contains an invalid Content-Type header: {}", error), |
|
|
|
"The request should contain a Content-Type header" |
|
|
|
StatusCode::BAD_REQUEST, |
|
|
|
), |
|
|
|
), |
|
|
|
|
|
|
|
None => error_to_response( |
|
|
|
|
|
|
|
&state, |
|
|
|
|
|
|
|
&format_err!("The request should contain a Content-Type header"), |
|
|
|
StatusCode::BAD_REQUEST, |
|
|
|
StatusCode::BAD_REQUEST, |
|
|
|
), |
|
|
|
), |
|
|
|
}; |
|
|
|
}; |
|
|
@ -173,17 +209,17 @@ fn router<D: SparqlDataset + Send + Sync + RefUnwindSafe + 'static>(dataset: Arc |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[derive(StateData)] |
|
|
|
#[derive(StateData)] |
|
|
|
struct SparqlStore<D: SparqlDataset + Send + Sync + RefUnwindSafe + 'static>(Arc<D>); |
|
|
|
struct GothamState<D: SparqlDataset + Send + Sync + RefUnwindSafe + 'static> { |
|
|
|
|
|
|
|
dataset: Arc<D>, |
|
|
|
|
|
|
|
base: String, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
impl<D: SparqlDataset + Send + Sync + RefUnwindSafe + 'static> Clone for SparqlStore<D> { |
|
|
|
impl<D: SparqlDataset + Send + Sync + RefUnwindSafe + 'static> Clone for GothamState<D> { |
|
|
|
fn clone(&self) -> Self { |
|
|
|
fn clone(&self) -> Self { |
|
|
|
SparqlStore(self.0.clone()) |
|
|
|
Self { |
|
|
|
|
|
|
|
dataset: self.dataset.clone(), |
|
|
|
|
|
|
|
base: self.base.clone(), |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl<D: SparqlDataset + Send + Sync + RefUnwindSafe + 'static> AsRef<D> for SparqlStore<D> { |
|
|
|
|
|
|
|
fn as_ref(&self) -> &D { |
|
|
|
|
|
|
|
&*self.0 |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -204,22 +240,22 @@ fn evaluate_sparql_query<D: SparqlDataset + Send + Sync + RefUnwindSafe + 'stati |
|
|
|
state: &mut State, |
|
|
|
state: &mut State, |
|
|
|
query: &[u8], |
|
|
|
query: &[u8], |
|
|
|
) -> Response<Body> { |
|
|
|
) -> Response<Body> { |
|
|
|
let dataset: SparqlStore<D> = SparqlStore::take_from(state); |
|
|
|
let gotham_state: GothamState<D> = GothamState::take_from(state); |
|
|
|
match dataset.as_ref().prepare_query(query) { |
|
|
|
match gotham_state.dataset.prepare_query(query) { |
|
|
|
Ok(query) => match query.exec() { |
|
|
|
Ok(query) => match query.exec() { |
|
|
|
Ok(QueryResult::Graph(triples)) => { |
|
|
|
Ok(QueryResult::Graph(triples)) => { |
|
|
|
let triples: Result<MemoryGraph, failure::Error> = triples.collect(); |
|
|
|
let triples: Result<MemoryGraph, failure::Error> = triples.collect(); |
|
|
|
create_response( |
|
|
|
create_response( |
|
|
|
&state, |
|
|
|
&state, |
|
|
|
StatusCode::OK, |
|
|
|
StatusCode::OK, |
|
|
|
"application/n-triples".parse().unwrap(), |
|
|
|
APPLICATION_N_TRIPLES_UTF_8.clone(), |
|
|
|
triples.unwrap().to_string(), |
|
|
|
triples.unwrap().to_string(), |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
Ok(result) => create_response( |
|
|
|
Ok(result) => create_response( |
|
|
|
&state, |
|
|
|
&state, |
|
|
|
StatusCode::OK, |
|
|
|
StatusCode::OK, |
|
|
|
"application/sparql-results+xml".parse().unwrap(), |
|
|
|
APPLICATION_SPARQL_RESULTS_UTF_8.clone(), |
|
|
|
write_xml_results(result, Vec::default()).unwrap(), |
|
|
|
write_xml_results(result, Vec::default()).unwrap(), |
|
|
|
), |
|
|
|
), |
|
|
|
Err(error) => error_to_response(&state, &error, StatusCode::INTERNAL_SERVER_ERROR), |
|
|
|
Err(error) => error_to_response(&state, &error, StatusCode::INTERNAL_SERVER_ERROR), |
|
|
@ -239,9 +275,22 @@ mod tests { |
|
|
|
use mime::Mime; |
|
|
|
use mime::Mime; |
|
|
|
use std::str::FromStr; |
|
|
|
use std::str::FromStr; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
|
|
|
fn get_ui() { |
|
|
|
|
|
|
|
let test_server = |
|
|
|
|
|
|
|
TestServer::new(router(Arc::new(MemoryDataset::default()), "".to_string())).unwrap(); |
|
|
|
|
|
|
|
let response = test_server |
|
|
|
|
|
|
|
.client() |
|
|
|
|
|
|
|
.get("http://localhost/") |
|
|
|
|
|
|
|
.perform() |
|
|
|
|
|
|
|
.unwrap(); |
|
|
|
|
|
|
|
assert_eq!(response.status(), StatusCode::OK); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
#[test] |
|
|
|
fn get_query() { |
|
|
|
fn get_query() { |
|
|
|
let test_server = TestServer::new(router(Arc::new(MemoryDataset::default()))).unwrap(); |
|
|
|
let test_server = |
|
|
|
|
|
|
|
TestServer::new(router(Arc::new(MemoryDataset::default()), "".to_string())).unwrap(); |
|
|
|
let response = test_server |
|
|
|
let response = test_server |
|
|
|
.client() |
|
|
|
.client() |
|
|
|
.get("http://localhost/query?query=SELECT+*+WHERE+{+?s+?p+?o+}") |
|
|
|
.get("http://localhost/query?query=SELECT+*+WHERE+{+?s+?p+?o+}") |
|
|
@ -252,7 +301,8 @@ mod tests { |
|
|
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
#[test] |
|
|
|
fn post_query() { |
|
|
|
fn post_query() { |
|
|
|
let test_server = TestServer::new(router(Arc::new(MemoryDataset::default()))).unwrap(); |
|
|
|
let test_server = |
|
|
|
|
|
|
|
TestServer::new(router(Arc::new(MemoryDataset::default()), "".to_string())).unwrap(); |
|
|
|
let response = test_server |
|
|
|
let response = test_server |
|
|
|
.client() |
|
|
|
.client() |
|
|
|
.post( |
|
|
|
.post( |
|
|
|