parent
c30dd409c1
commit
e3be82e7e7
@ -1,5 +1,6 @@ |
||||
[workspace] |
||||
members = [ |
||||
"lib", |
||||
"python" |
||||
"python", |
||||
"server" |
||||
] |
@ -0,0 +1,23 @@ |
||||
[package] |
||||
name = "rudf_server" |
||||
version = "0.1.0" |
||||
authors = ["Tpt <thomas@pellissier-tanon.fr>"] |
||||
license = "MIT/Apache-2.0" |
||||
readme = "../README.md" |
||||
repository = "https://github.com/Tpt/rudf" |
||||
description = """ |
||||
Rudf based SPARQL server |
||||
""" |
||||
|
||||
[dependencies] |
||||
rudf = {path = "../lib"} |
||||
gotham = "0.3" |
||||
gotham_derive = "0.3" |
||||
hyper = "0.12" |
||||
futures = "0.1" |
||||
serde = "1" |
||||
serde_derive = "1" |
||||
mime = "0.3" |
||||
failure = "0.1" |
||||
url = "1" |
||||
clap = "2" |
@ -0,0 +1,230 @@ |
||||
extern crate gotham; |
||||
#[macro_use] |
||||
extern crate gotham_derive; |
||||
extern crate futures; |
||||
extern crate hyper; |
||||
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; |
||||
|
||||
use clap::App; |
||||
use clap::Arg; |
||||
use futures::future; |
||||
use futures::Future; |
||||
use futures::Stream; |
||||
use gotham::handler::{HandlerFuture, IntoHandlerError}; |
||||
use gotham::helpers::http::response::create_response; |
||||
use gotham::middleware::state::StateMiddleware; |
||||
use gotham::pipeline::single::single_pipeline; |
||||
use gotham::pipeline::single_middleware; |
||||
use gotham::router::builder::build_router; |
||||
use gotham::router::builder::DefineSingleRoute; |
||||
use gotham::router::builder::DrawRoutes; |
||||
use gotham::router::Router; |
||||
use gotham::state::FromState; |
||||
use gotham::state::State; |
||||
use hyper::header::CONTENT_TYPE; |
||||
use hyper::Body; |
||||
use hyper::HeaderMap; |
||||
use hyper::Response; |
||||
use hyper::StatusCode; |
||||
use rudf::model::Dataset; |
||||
use rudf::model::Graph; |
||||
use rudf::rio::ntriples::read_ntriples; |
||||
use rudf::sparql::xml_results::write_xml_results; |
||||
use rudf::sparql::PreparedQuery; |
||||
use rudf::sparql::SparqlDataset; |
||||
use rudf::store::MemoryDataset; |
||||
use std::fs::File; |
||||
use std::sync::Arc; |
||||
use url::form_urlencoded; |
||||
|
||||
pub fn main() -> Result<(), failure::Error> { |
||||
let matches = App::new("Rudf SPARQL server") |
||||
.arg( |
||||
Arg::with_name("bind") |
||||
.short("b") |
||||
.long("bind") |
||||
.help("Specify a server socket to bind using the format $(HOST):$(PORT)") |
||||
.takes_value(true), |
||||
).arg( |
||||
Arg::with_name("ntriples") |
||||
.long("ntriples") |
||||
.help("Load a N-Triples file in the server at startup") |
||||
.takes_value(true), |
||||
).get_matches(); |
||||
|
||||
let dataset = MemoryDataset::default(); |
||||
if let Some(nt_file) = matches.value_of("ntriples") { |
||||
println!("Loading NTriples file {}", nt_file); |
||||
let default_graph = dataset.default_graph(); |
||||
for quad in read_ntriples(File::open(nt_file)?) { |
||||
default_graph.insert(&quad?)? |
||||
} |
||||
} |
||||
|
||||
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)); |
||||
Ok(()) |
||||
} |
||||
|
||||
fn router(dataset: MemoryDataset) -> Router { |
||||
let store = SparqlStore::new(dataset); |
||||
let middleware = StateMiddleware::new(store); |
||||
let pipeline = single_middleware(middleware); |
||||
let (chain, pipelines) = single_pipeline(pipeline); |
||||
build_router(chain, pipelines, |route| { |
||||
route.associate("/query", |assoc| { |
||||
assoc |
||||
.get() |
||||
.with_query_string_extractor::<QueryRequest>() |
||||
.to(|mut state: State| -> (State, Response<Body>) { |
||||
let parsed_request = QueryRequest::take_from(&mut state); |
||||
let response = |
||||
evaluate_sparql_query(&mut state, &parsed_request.query.as_bytes()); |
||||
(state, response) |
||||
}); |
||||
assoc.post().to(|mut state: State| -> Box<HandlerFuture> { |
||||
Box::new( |
||||
Body::take_from(&mut state) |
||||
.concat2() |
||||
.then(|body| match body { |
||||
Ok(body) => { |
||||
let response = match 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( |
||||
&mut state, |
||||
&parsed_request.query.as_bytes(), |
||||
), |
||||
Err(error) => error_to_response( |
||||
&state, |
||||
&error, |
||||
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" |
||||
), |
||||
StatusCode::BAD_REQUEST, |
||||
), |
||||
}; |
||||
future::ok((state, response)) |
||||
} |
||||
Err(e) => future::err((state, e.into_handler_error())), |
||||
}), |
||||
) |
||||
}); |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
#[derive(Clone, StateData)] |
||||
struct SparqlStore(Arc<MemoryDataset>); |
||||
|
||||
impl SparqlStore { |
||||
fn new(dataset: MemoryDataset) -> Self { |
||||
SparqlStore(Arc::new(dataset)) |
||||
} |
||||
} |
||||
|
||||
impl AsRef<MemoryDataset> for SparqlStore { |
||||
fn as_ref(&self) -> &MemoryDataset { |
||||
&*self.0 |
||||
} |
||||
} |
||||
|
||||
#[derive(Deserialize, StateData, StaticResponseExtender)] |
||||
struct QueryRequest { |
||||
query: String, |
||||
} |
||||
|
||||
fn parse_urlencoded_query_request(query: &[u8]) -> Result<QueryRequest, failure::Error> { |
||||
form_urlencoded::parse(query) |
||||
.find(|(key, _)| key == "query") |
||||
.map(|(_, value)| QueryRequest { |
||||
query: value.to_string(), |
||||
}).ok_or_else(|| format_err!("'query' parameter not found")) |
||||
} |
||||
|
||||
fn evaluate_sparql_query(state: &mut State, query: &[u8]) -> Response<Body> { |
||||
let dataset = SparqlStore::take_from(state); |
||||
match dataset.as_ref().prepare_query(query) { |
||||
Ok(query) => match query.exec() { |
||||
Ok(result) => create_response( |
||||
&state, |
||||
StatusCode::OK, |
||||
"application/sparql-results+xml".parse().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::BAD_REQUEST), |
||||
} |
||||
} |
||||
|
||||
fn error_to_response(state: &State, error: &failure::Error, code: StatusCode) -> Response<Body> { |
||||
create_response(state, code, mime::TEXT_PLAIN_UTF_8, error.to_string()) |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use super::*; |
||||
use gotham::test::TestServer; |
||||
use mime::Mime; |
||||
use std::str::FromStr; |
||||
|
||||
#[test] |
||||
fn get_query() { |
||||
let test_server = TestServer::new(router(MemoryDataset::default())).unwrap(); |
||||
let response = test_server |
||||
.client() |
||||
.get("http://localhost/query?query=SELECT+*+WHERE+{+?s+?p+?o+}") |
||||
.perform() |
||||
.unwrap(); |
||||
assert_eq!(response.status(), StatusCode::OK); |
||||
} |
||||
|
||||
#[test] |
||||
fn post_query() { |
||||
let test_server = TestServer::new(router(MemoryDataset::default())).unwrap(); |
||||
let response = test_server |
||||
.client() |
||||
.post( |
||||
"http://localhost/query", |
||||
"SELECT * WHERE { ?s ?p ?o }", |
||||
Mime::from_str("application/sparql-query").unwrap(), |
||||
).perform() |
||||
.unwrap(); |
||||
assert_eq!(response.status(), StatusCode::OK); |
||||
} |
||||
} |
Loading…
Reference in new issue