1
use axum::{
2
    extract::{Query, State},
3
    response::{Html, IntoResponse},
4
    routing::get,
5
    Json, Router,
6
};
7
use http::{Method, StatusCode};
8
use serde::{Deserialize, Serialize};
9
use std::{
10
    collections::BTreeMap,
11
    error::Error,
12
    fmt::Display,
13
    path::PathBuf,
14
    sync::{Arc, RwLock},
15
};
16
use tokio::fs::read_to_string;
17
use tower_http::{
18
    cors::{Any, CorsLayer},
19
    services::{ServeDir, ServeFile},
20
};
21
use url::Url;
22

            
23
#[derive(Serialize, Deserialize, Clone)]
24
pub struct AnnotationServerConfig {
25
    assets_dir: PathBuf,
26
    sparql_endpoint: String,
27
    sparql_user: String,
28
    sparql_password: String,
29
    template_url: String,
30
    query_configs: BTreeMap<String, QueryConfig>,
31
}
32

            
33
#[derive(Serialize, Deserialize, Clone, Debug)]
34
pub struct QueryConfig {
35
    pub url_pattern: String,
36
    pub xpath: String,
37
    pub text_pattern: String,
38
    pub query_file: String,
39
    pub template_file: String,
40
}
41

            
42
type SharedState = Arc<RwLock<AppState>>;
43

            
44
struct AppState {
45
    config: AnnotationServerConfig,
46
}
47

            
48
#[derive(Debug)]
49
pub struct MyError {
50
    msg: String,
51
}
52
impl Display for MyError {
53
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54
        write!(f, "{}", self.msg)
55
    }
56
}
57
impl Error for MyError {}
58
impl IntoResponse for MyError {
59
    fn into_response(self) -> axum::response::Response {
60
        self.msg.into_response()
61
    }
62
}
63

            
64
async fn route_get_index() -> Result<Html<String>, MyError> {
65
    Ok(Html(String::new()))
66
}
67

            
68
async fn route_get_configuration(
69
    State(state): State<SharedState>,
70
) -> Result<Json<AnnotationServerConfig>, MyError> {
71
    let state = &mut state.read().unwrap();
72
    Ok(Json(state.config.clone()))
73
}
74

            
75
#[derive(Deserialize)]
76
struct QueryQuery {
77
    query: String,
78
    entity: String,
79
}
80

            
81
async fn run_sparql(
82
    config: &AnnotationServerConfig,
83
    query_config: &QueryConfig,
84
    entity: &str,
85
) -> Result<String, MyError> {
86
    let query_path = config.assets_dir.join(&query_config.query_file);
87
    println!("reading {}", query_path.display());
88
    let query = read_to_string(&query_path).await.map_err(|_| MyError {
89
        msg: format!("could not read {}", query_path.display()),
90
    })?;
91
    let query = query.replace("%%ENTITY%%", entity);
92
    println!("querying: {}", config.sparql_endpoint);
93
    println!("{}", query);
94
    let mut url = Url::parse(&config.sparql_endpoint).unwrap();
95
    url.query_pairs_mut().append_pair("query", &query);
96
    let client = reqwest::Client::new();
97
    let res = client
98
        .get(url)
99
        .basic_auth(&config.sparql_user, Some(&config.sparql_password))
100
        .send()
101
        .await
102
        .unwrap();
103
    let status = res.status();
104
    let text = res.text().await.unwrap();
105
    if status != StatusCode::OK {
106
        return Err(MyError { msg: text });
107
    }
108
    println!("sparql result: {}", status);
109
    println!("sparql result: {}", text);
110
    Ok(text)
111
}
112

            
113
async fn run_template(
114
    config: &AnnotationServerConfig,
115
    query_config: &QueryConfig,
116
    sparql_output: String,
117
) -> Result<String, MyError> {
118
    let template_path = config.assets_dir.join(&query_config.template_file);
119
    let template = read_to_string(&template_path).await.map_err(|_| MyError {
120
        msg: format!("could not read {}", template_path.display()),
121
    })?;
122
    println!("template {}", template);
123
    let client = reqwest::Client::new();
124
    let res = client
125
        .post(&config.template_url)
126
        .form(&[("data", sparql_output), ("query", template)])
127
        .send()
128
        .await
129
        .unwrap();
130
    let status = res.status();
131
    let text = res.text().await.unwrap();
132
    if status != StatusCode::OK {
133
        return Err(MyError { msg: text });
134
    }
135
    Ok(text)
136
}
137

            
138
async fn route_get_query(
139
    State(state): State<SharedState>,
140
    Query(query): Query<QueryQuery>,
141
) -> Result<Html<String>, MyError> {
142
    let config: AnnotationServerConfig = {
143
        let state = state.read().unwrap();
144
        state.config.clone()
145
    };
146
    let query_config = if let Some(query_config) = config.query_configs.get(&query.query) {
147
        query_config
148
    } else {
149
        return Err(MyError {
150
            msg: format!("{} is not known", query.query),
151
        });
152
    };
153
    println!("{} {} {:?}", query.query, query.entity, query_config);
154
    let sparql_output = run_sparql(&config, query_config, &query.entity).await?;
155
    let template_output = run_template(&config, query_config, sparql_output).await?;
156
    Ok(Html(template_output))
157
}
158

            
159
pub fn create_app(
160
    config: AnnotationServerConfig,
161
) -> Result<axum::routing::IntoMakeService<Router>, Box<dyn Error>> {
162
    let assets_dir = &config.assets_dir;
163
    let serve_dir =
164
        ServeDir::new(assets_dir).not_found_service(ServeFile::new(assets_dir.join("404.html")));
165

            
166
    let cors = CorsLayer::new()
167
        // allow `GET` and `POST` when accessing the resource
168
        .allow_methods([Method::GET, Method::POST])
169
        // allow requests from any origin
170
        .allow_origin(Any);
171
    let shared_state = Arc::new(RwLock::new(AppState { config }));
172
    Ok(Router::new()
173
        .route("/", get(route_get_index))
174
        .route("/query", get(route_get_query))
175
        .route("/configuration.json", get(route_get_configuration))
176
        .nest_service("/assets", serve_dir)
177
        .layer(cors)
178
        .with_state(Arc::clone(&shared_state))
179
        .into_make_service())
180
}