1use std::{
2 borrow::Cow,
3 collections::{HashMap, HashSet},
4};
5
6use bevy_ecs::{error::warn, prelude::*, world::CommandQueue};
7use serde::Deserialize;
8use sophia_api::{
9 prelude::{Any, Dataset},
10 quad::Quad,
11 term::Term as _,
12};
13use tracing::{debug, error, span};
14
15use super::setup::FromPrefix;
16use crate::{
17 lsp_types::{TextDocumentItem, Url},
18 prelude::*,
19 util::{fs::Fs, ns::owl},
20};
21
22#[derive(Deserialize, Debug)]
23struct Version {
24 #[serde(rename = "fileURL")]
25 file_url: Option<String>,
26 issued: chrono::DateTime<chrono::Utc>,
27}
28
29#[derive(Deserialize, Debug)]
30struct Vocab {
31 versions: Vec<Version>,
32}
33
34async fn extract_file_url(prefix: &str, client: &impl Client) -> Option<String> {
35 let url = format!(
36 "https://lov.linkeddata.es/dataset/lov/api/v2/vocabulary/info?vocab={}",
37 prefix
38 );
39 match client.fetch(&url, &std::collections::HashMap::new()).await {
40 Ok(resp) if resp.status == 200 => match serde_json::from_str::<Vocab>(&resp.body) {
41 Ok(x) => {
42 let versions: Vec<_> = x.versions.iter().flat_map(|x| &x.file_url).collect();
43 debug!(
44 "Found lov response ({} versions) {:?}",
45 x.versions.len(),
46 versions
47 );
48 x.versions
49 .into_iter()
50 .flat_map(|x| x.file_url.map(|url| (url, x.issued)))
51 .max_by_key(|x| x.1)
52 .map(|x| x.0)
53 }
54 Err(e) => {
55 error!("Deserialize failed ({}) {:?}", url, e);
56 None
57 }
58 },
59 Ok(resp) => {
60 error!("Fetch ({}) failed status {}", url, resp.status);
61 None
62 }
63 Err(e) => {
64 error!("Fetch ({}) failed {:?}", url, e);
65 None
66 }
67 }
68}
69
70pub fn open_imports<C: Client + Resource>(
71 query: Query<(&Triples, &RopeC), Changed<Triples>>,
72 mut opened: Local<HashSet<String>>,
73 sender: Res<CommandSender>,
74 fs: Res<Fs>,
75 client: Res<C>,
76) {
77 for (triples, _) in &query {
78 for object in triples
79 .quads_matching(Any, [owl::imports], Any, Any)
80 .flatten()
81 .flat_map(|s| s.o().iri())
82 .flat_map(|s| Url::parse(s.as_str()))
83 {
84 if opened.contains(object.as_str()) {
85 continue;
86 }
87 opened.insert(object.as_str().to_string());
88
89 let fs = fs.clone();
90 let sender = sender.clone();
91 let fut = async move {
92 if let Some(content) = fs.0.read_file(&object).await {
93 spawn_document(object, content, &sender.0, |_, _| {});
94
95 let mut command_queue = CommandQueue::default();
96 command_queue.push(move |world: &mut World| {
97 world.run_schedule(SaveLabel);
98 });
99 let _ = sender.unbounded_send(command_queue);
100 } else {
101 tracing::warn!("No content found for {}", object);
102 }
103 };
104 client.spawn(fut);
105 }
106 }
107}
108
109pub fn fetch_lov_properties<C: Client + Resource>(
112 sender: Res<CommandSender>,
113 query: Query<&Prefixes, (Or<((Changed<Prefixes>, With<Open>), Changed<Open>)>,)>,
114 ontologies: Query<(Entity, &swls_lov::LocalPrefix)>,
115 mut prefixes: Local<HashSet<String>>,
116 client: Res<C>,
117 fs: Res<Fs>,
118) {
119 for prefs in &query {
120 for prefix in prefs.0.iter() {
121 if !prefixes.contains(prefix.url.as_str()) {
122 prefixes.insert(prefix.url.to_string());
123 let mut found = false;
124 for (_e, local) in ontologies
125 .iter()
126 .filter(|(_, x)| x.namespace == prefix.url.as_str())
127 {
128 debug!(
129 "Local lov for Prefix {} {} is entry {} {} {} {}",
130 prefix.prefix,
131 prefix.url.as_str(),
132 local.name,
133 local.namespace,
134 local.location,
135 local.content.is_empty()
136 );
137
138 let Some(label) = fs.0.lov_url(prefix.url.as_str(), &prefix.prefix) else {
139 continue;
140 };
141
142 found = true;
143 let c = client.as_ref().clone();
144 let sender = sender.0.clone();
145 client.spawn(local_lov::<C>(local.clone(), label, sender, fs.clone(), c));
146 }
147
148 if !found {
149 if let Some(url) = fs.0.lov_url(prefix.url.as_str(), &prefix.prefix) {
150 tracing::debug!(
151 "Remote lov for prefix {} {}",
152 prefix.prefix,
153 prefix.url.as_str()
154 );
155 let sender = sender.0.clone();
156 let c = client.as_ref().clone();
157 client.spawn(fetch_lov(
158 prefix.clone(),
159 url.clone(),
160 c,
161 sender,
162 fs.clone(),
163 ));
164 }
165 }
166 }
167 }
168 }
169}
170
171type Sender = futures::channel::mpsc::UnboundedSender<CommandQueue>;
172
173pub fn spawn_document(
174 url: Url,
175 content: String,
176 sender: &Sender,
177 extra: impl FnOnce(Entity, &mut World) -> () + Send + Sync + 'static,
178) {
179 let mut command_queue = CommandQueue::default();
180 let item = TextDocumentItem {
181 version: 1,
182 uri: url.clone(),
183 language_id: String::from("turtle"),
184 text: String::new(),
185 };
186
187 let spawn = spawn_or_insert(
188 url.clone(),
189 (
190 RopeC(LineIndex::new(&content)),
191 Source(content.clone()),
192 Label(url.clone()),
193 Wrapped(item),
194 Types(HashMap::new()),
195 ),
196 Some("turtle".into()),
197 (),
198 );
199
200 command_queue.push(move |world: &mut World| {
201 let span = span!(tracing::Level::INFO, "span lov");
202 let _enter = span.enter();
203 let e = spawn(world);
204
205 extra(e, world);
206
207 world.run_schedule(ParseLabel);
208 });
209
210 let _ = sender.unbounded_send(command_queue);
211}
212
213fn extra_from_lov<C: Client + Resource>(
214 from: FromPrefix,
215 content: String,
216 url: Url,
217 fs: Fs,
218) -> impl FnOnce(Entity, &mut World) + Send + Sync + 'static {
219 move |e, world| {
220 world.entity_mut(e).insert(from);
221
222 let client = world.resource::<C>();
223 client.spawn(async move {
224 fs.0.write_file(&url, &content).await;
225 });
226 }
227}
228
229pub(super) async fn fetch_lov_body<C: Client + Resource>(
230 prefix: &str,
231 namespace: &str,
232 c: C,
233) -> Option<String> {
234 let mirror_url = format!(
236 "https://ajuvercr.github.io/lov-mirror/by-prefix/{}/ontology.ttl",
237 prefix
238 );
239 match c
240 .fetch(&mirror_url, &std::collections::HashMap::new())
241 .await
242 {
243 Ok(resp) if resp.status == 200 => {
244 tracing::warn!("Mirror hit for prefix {}", prefix);
245 return Some(resp.body);
246 }
247 Ok(resp) => {
248 tracing::warn!(
249 "Mirror miss for prefix {} (status {}), falling back to LOV API",
250 prefix,
251 resp.status
252 );
253 }
254 Err(e) => {
255 tracing::warn!(
256 "Mirror fetch failed for prefix {} ({:?}), falling back to LOV API",
257 prefix,
258 e
259 );
260 }
261 }
262
263 if namespace.ends_with('#') || namespace.ends_with('/') {
265 if let Some(url) = extract_file_url(&prefix, &c).await {
266 match c.fetch(&url, &std::collections::HashMap::new()).await {
267 Ok(resp) if resp.status == 200 => return Some(resp.body),
268 Ok(resp) => {
269 error!("Fetch ({}) failed status {}", url, resp.status);
270 }
271 Err(e) => {
272 error!("Fetch ({}) failed {:?}", url, e);
273 }
274 }
275 }
276 }
277 None
278}
279
280async fn fetch_lov<C: Client + Resource>(prefix: Prefix, label: Url, c: C, sender: Sender, fs: Fs) {
281 tracing::debug!("Fetching LOV for prefix {}", prefix.prefix);
282 if prefix.prefix.to_ascii_lowercase() == prefix.prefix {
283 if let Some(body) = fetch_lov_body(&prefix.prefix, prefix.url.as_str(), c).await {
284 let extra = extra_from_lov::<C>(FromPrefix(prefix), body.clone(), label.clone(), fs);
285 spawn_document(label, body, &sender, extra);
286 }
287 }
288}
289
290async fn local_lov<C: Client + Resource>(
291 local: swls_lov::LocalPrefix,
292 label: Url,
293 sender: Sender,
294 fs: Fs,
295 c: C,
296) {
297 tracing::debug!(
298 "Using local {} {} Label {}",
299 local.name,
300 local.namespace,
301 label.as_str()
302 );
303 let content = if local.content.is_empty() {
304 tracing::debug!("Fetching from LOV {}", local.name);
305 if let Some(body) = fetch_lov_body(&local.name, &local.namespace, c).await {
306 Cow::Owned(body)
307 } else {
308 return;
309 }
310 } else {
311 local.content
312 };
313
314 let from = FromPrefix(Prefix {
315 prefix: local.name.to_string(),
316 url: Url::parse(&local.namespace).unwrap(),
317 });
318
319 let extra = extra_from_lov::<C>(from, content.to_string(), label.clone(), fs);
320 spawn_document(label, content.to_string(), &sender, extra);
321}