diff --git a/server/Cargo.toml b/server/Cargo.toml index 48d8829b..b663c435 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -21,3 +21,5 @@ mime = "0.3" failure = "0.1" url = "1" clap = "2" +tera = "0.11" +lazy_static = "1" diff --git a/server/src/main.rs b/server/src/main.rs index 68c3a93d..516c99eb 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,17 +1,21 @@ +extern crate clap; +#[macro_use] +extern crate failure; +extern crate futures; extern crate gotham; #[macro_use] extern crate gotham_derive; -extern crate futures; extern crate hyper; +#[macro_use] +extern crate lazy_static; extern crate mime; extern crate rudf; extern crate serde; -extern crate url; #[macro_use] extern crate serde_derive; #[macro_use] -extern crate failure; -extern crate clap; +extern crate tera; +extern crate url; use clap::App; use clap::Arg; @@ -35,6 +39,7 @@ use hyper::Body; use hyper::HeaderMap; use hyper::Response; use hyper::StatusCode; +use mime::Mime; use rudf::model::Graph; use rudf::rio::ntriples::read_ntriples; use rudf::sparql::algebra::QueryResult; @@ -46,9 +51,26 @@ use rudf::store::MemoryGraph; use rudf::store::RocksDbDataset; use std::fs::File; use std::panic::RefUnwindSafe; +use std::str::FromStr; use std::sync::Arc; +use tera::Context; +use tera::Tera; 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> { let matches = App::new("Rudf SPARQL server") .arg( @@ -72,15 +94,15 @@ pub fn main() -> Result<(), failure::Error> { let file = matches.value_of("file").map(|v| v.to_string()); if let Some(file) = file { - main_with_dataset(Arc::new(RocksDbDataset::open(file)?), matches) + main_with_dataset(Arc::new(RocksDbDataset::open(file)?), &matches) } else { - main_with_dataset(Arc::new(MemoryDataset::default()), matches) + main_with_dataset(Arc::new(MemoryDataset::default()), &matches) } } fn main_with_dataset( dataset: Arc, - matches: ArgMatches, + matches: &ArgMatches, ) -> Result<(), failure::Error> { if let Some(nt_file) = matches.value_of("ntriples") { println!("Loading NTriples file {}", nt_file); @@ -92,16 +114,32 @@ fn main_with_dataset( let addr = matches.value_of("bind").unwrap_or("127.0.0.1:7878"); 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(()) } -fn router(dataset: Arc) -> Router { - let store = SparqlStore(dataset); - let middleware = StateMiddleware::new(store); +fn router( + dataset: Arc, + base: String, +) -> Router { + let middleware = StateMiddleware::new(GothamState { dataset, base }); let pipeline = single_middleware(middleware); let (chain, pipelines) = single_pipeline(pipeline); build_router(chain, pipelines, |route| { + route + .get("/") + .to(|mut state: State| -> (State, Response) { + let gotham_state: GothamState = 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| { assoc .get() @@ -118,50 +156,48 @@ fn router(dataset: Arc .concat2() .then(|body| match body { Ok(body) => { - let response = match HeaderMap::borrow_from(&state) + let content_type: Option> = HeaderMap::borrow_from(&state) .get(CONTENT_TYPE) - .cloned() - { - Some(content_type) => { - if content_type == "application/sparql-query" { - evaluate_sparql_query::( - &mut state, - &body.into_bytes(), - ) - } else if content_type - == "application/x-www-form-urlencoded" - { - match parse_urlencoded_query_request(&body.into_bytes()) - { - Ok(parsed_request) => evaluate_sparql_query::( + .map(|content_type| Ok(Mime::from_str(content_type.to_str()?)?)); + let response = match content_type { + Some(Ok(content_type)) => match (content_type.type_(), content_type.subtype()) { + (mime::APPLICATION, subtype) if subtype == APPLICATION_SPARQL_QUERY_UTF_8.subtype() => { + evaluate_sparql_query::( &mut state, - &parsed_request.query.as_bytes(), - ), - Err(error) => error_to_response( + &body.into_bytes(), + ) + }, + (mime::APPLICATION, mime::WWW_FORM_URLENCODED) => { + match parse_urlencoded_query_request(&body.into_bytes()) + { + Ok(parsed_request) => evaluate_sparql_query::( + &mut state, + &parsed_request.query.as_bytes(), + ), + Err(error) => error_to_response( + &state, + &error, + StatusCode::BAD_REQUEST, + ), + } + }, + _ => error_to_response( &state, - &error, + &format_err!("Unsupported Content-Type: {:?}", content_type), StatusCode::BAD_REQUEST, - ), - } - } else { - error_to_response( - &state, - &format_err!( - "Unsupported Content-Type: {:?}", - content_type - ), - StatusCode::BAD_REQUEST, - ) + ) } - } - None => error_to_response( - &state, - &format_err!( - "The request should contain a Content-Type header" + Some(Err(error)) => error_to_response( + &state, + &format_err!("The request contains an invalid Content-Type header: {}", error), + StatusCode::BAD_REQUEST, ), - StatusCode::BAD_REQUEST, - ), - }; + None => error_to_response( + &state, + &format_err!("The request should contain a Content-Type header"), + StatusCode::BAD_REQUEST, + ), + }; future::ok((state, response)) } Err(e) => future::err((state, e.into_handler_error())), @@ -173,17 +209,17 @@ fn router(dataset: Arc } #[derive(StateData)] -struct SparqlStore(Arc); - -impl Clone for SparqlStore { - fn clone(&self) -> Self { - SparqlStore(self.0.clone()) - } +struct GothamState { + dataset: Arc, + base: String, } -impl AsRef for SparqlStore { - fn as_ref(&self) -> &D { - &*self.0 +impl Clone for GothamState { + fn clone(&self) -> Self { + Self { + dataset: self.dataset.clone(), + base: self.base.clone(), + } } } @@ -204,22 +240,22 @@ fn evaluate_sparql_query Response { - let dataset: SparqlStore = SparqlStore::take_from(state); - match dataset.as_ref().prepare_query(query) { + let gotham_state: GothamState = GothamState::take_from(state); + match gotham_state.dataset.prepare_query(query) { Ok(query) => match query.exec() { Ok(QueryResult::Graph(triples)) => { let triples: Result = triples.collect(); create_response( &state, StatusCode::OK, - "application/n-triples".parse().unwrap(), + APPLICATION_N_TRIPLES_UTF_8.clone(), triples.unwrap().to_string(), ) } Ok(result) => create_response( &state, StatusCode::OK, - "application/sparql-results+xml".parse().unwrap(), + APPLICATION_SPARQL_RESULTS_UTF_8.clone(), write_xml_results(result, Vec::default()).unwrap(), ), Err(error) => error_to_response(&state, &error, StatusCode::INTERNAL_SERVER_ERROR), @@ -239,9 +275,22 @@ mod tests { use mime::Mime; 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] 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 .client() .get("http://localhost/query?query=SELECT+*+WHERE+{+?s+?p+?o+}") @@ -252,7 +301,8 @@ mod tests { #[test] 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 .client() .post( diff --git a/server/templates/query.html b/server/templates/query.html new file mode 100644 index 00000000..9db97dc2 --- /dev/null +++ b/server/templates/query.html @@ -0,0 +1,28 @@ + + + + + Rudf server + + + +
+
+ + + +