Adds a UI to the SPARQL server

pull/10/head
Tpt 6 years ago
parent de34f6e833
commit 4e74db73ab
  1. 2
      server/Cargo.toml
  2. 136
      server/src/main.rs
  3. 28
      server/templates/query.html

@ -21,3 +21,5 @@ mime = "0.3"
failure = "0.1" failure = "0.1"
url = "1" url = "1"
clap = "2" clap = "2"
tera = "0.11"
lazy_static = "1"

@ -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(

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Rudf server</title>
<link href='http://cdn.jsdelivr.net/g/yasqe@2.2(yasqe.min.css),yasr@2.4(yasr.min.css)' rel='stylesheet' type='text/css'/>
</head>
<body>
<div id="yasqe"></div>
<div id="yasr"></div>
<script src='http://cdn.jsdelivr.net/yasr/2.4/yasr.bundled.min.js'></script>
<script src='http://cdn.jsdelivr.net/yasqe/2.2/yasqe.bundled.min.js'></script>
<script>
var yasqe = YASQE(document.getElementById("yasqe"), {
sparql: {
showQueryButton: true,
endpoint: '{{endpoint}}'
}
});
var yasr = YASR(document.getElementById("yasr"), {
//this way, the URLs in the results are prettified using the defined prefixes in the query
getUsedPrefixes: yasqe.getPrefixesFromQuery
});
//link both together
yasqe.options.sparql.callbacks.complete = yasr.setResponse;
</script>
</body>
Loading…
Cancel
Save