use crate::error; use crate::rdf::conversion; use gl_graph::language::LanguageCondition; use gl_graph::vocab::gl; use iced::futures::TryFutureExt; use oxigraph::model::vocab::{rdf, rdfs, xsd}; use oxigraph::model::{ Dataset, Graph, GraphNameRef, LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Term, TermRef, Triple, TripleRef, }; use oxigraph::sparql::{QueryResults, SparqlEvaluator}; use oxigraph::store::Store; use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fmt::Display; use std::path::{Path, PathBuf}; use std::sync::LazyLock; use tracing::{debug_span, field}; const ONTOLOGY_PREFIX: &str = "https://graphofliberty.org/2026/04/ont/"; const ONTOLOGY_GRAPH_NAME: GraphNameRef = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked( "https://graphofliberty.org/2026/04/ont", )); static PREFIXES: LazyLock> = LazyLock::new(|| { BTreeMap::from_iter( [ ("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"), ("rdfs", "http://www.w3.org/2000/01/rdf-schema#"), ("owl", "http://www.w3.org/2002/07/owl#"), ("xsd", "http://www.w3.org/2001/XMLSchema#"), ("ldp", "http://www.w3.org/ns/ldp#"), ("dc", "http://purl.org/dc/elements/1.1/"), ("posix", "http://www.w3.org/ns/posix/stat#"), ( "ebucore", "http://www.ebu.ch/metadata/ontologies/ebucore/ebucore#", ), ("prov", "http://www.w3.org/ns/prov#"), ("locid", "http://id.loc.gov/vocabulary/identifiers/"), ("loclang", "http://id.loc.gov/vocabulary/languages/"), ("premis", "http://www.loc.gov/premis/rdf/v1#"), ("premis3", "http://www.loc.gov/premis/rdf/v3/"), ("dcterms", "http://purl.org/dc/terms/"), ("fedora", "http://fedora.info/definitions/v4/repository#"), ("rdaa", "http://rdaregistry.info/Elements/a/"), ("rdac", "http://rdaregistry.info/Elements/c/"), ("rdae", "http://rdaregistry.info/Elements/e/"), ("rdai", "http://rdaregistry.info/Elements/i/"), ("rdam", "http://rdaregistry.info/Elements/m/"), ("rdan", "http://rdaregistry.info/Elements/n/"), ("rdap", "http://rdaregistry.info/Elements/p/"), ("rdaf", "http://rdaregistry.info/Elements/rof/"), ("rdat", "http://rdaregistry.info/Elements/t/"), ("rdau", "http://rdaregistry.info/Elements/u/"), ("rdaw", "http://rdaregistry.info/Elements/w/"), ("rdax", "http://rdaregistry.info/Elements/x/"), ("rdaco", "http://rdaregistry.info/termList/RDAContentType/"), ("rdact", "http://rdaregistry.info/termList/RDACarrierType/"), ("rdamt", "http://rdaregistry.info/termList/RDAMediaType/"), ("rdaft", "http://rdaregistry.info/termList/fileType/"), ("schema", "https://schema.org/"), ("gl", "http://fedora.quill.lan/rest/"), ("glo", ONTOLOGY_PREFIX), ] .map(|(k, v)| (k.to_string(), v.to_string())), ) }); pub struct OntologyBuilder { path: Option, } impl OntologyBuilder { pub fn with_path(mut self, path: impl AsRef) -> Self { let path = path.as_ref().to_owned(); self.path = Some(path); self } pub fn build(self) -> error::Result { let store = if let Some(path) = self.path { Store::open(path) } else { Store::new() }?; let prefixes = PREFIXES .iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect::>(); /*let mut indexed_by = HashMap::new(); for quad in store .quads_for_pattern(None, Some(gl::INDEXED_BY_FIELD), None, None) .filter_map(Result::ok) { if let NamedOrBlankNode::NamedNode(subject) = quad.subject && let Term::NamedNode(field) = quad.object { indexed_by.insert(subject, field); } }*/ Ok(Ontology { store, prefixes }) } } #[derive(Clone, Debug)] pub struct IndexEntry { pub category_id: u64, pub fields: HashMap, } #[derive(Clone, Debug, Eq)] pub struct LabeledIri { pub iri: NamedNode, pub label: String, } impl PartialEq for LabeledIri { fn eq(&self, other: &Self) -> bool { self.iri == other.iri } } impl PartialOrd for LabeledIri { fn partial_cmp(&self, other: &Self) -> Option { self.label.partial_cmp(&other.label) } } impl Ord for LabeledIri { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.label.cmp(&other.label) } } impl Display for LabeledIri { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.label) } } #[derive(Clone, Debug)] pub struct IndexField { pub name: String, pub label: String, } pub struct Ontology { store: Store, prefixes: HashMap, } impl Ontology { pub fn builder() -> OntologyBuilder { OntologyBuilder { path: None } } pub fn prefixes() -> &'static BTreeMap { &*PREFIXES } pub fn to_dataset(&self) -> Dataset { self.store .quads_for_pattern(None, None, None, Some(ONTOLOGY_GRAPH_NAME)) .filter_map(Result::ok) .collect::() } pub fn store(&self) -> Store { self.store.clone() } pub fn query_for_indexable_triples( &self, language: &LanguageCondition, source: Option, ) -> impl Future>> + 'static { let language_filter = language.to_filter_expression("fieldValue"); let query = format!( r#"SELECT ?individual ?categoryId ?fieldName ?fieldValue WHERE {{ ?class a gl:SearchableClass ; gl:categoryId ?categoryId ; gl:associatedProperty ?property . ?property gl:indexedByField/gl:fieldName ?fieldName . ?individual a ?class ; ?property ?fieldValue . {language_filter} }}"# ); let mut sparql = SparqlEvaluator::new() .with_prefix("gl", ONTOLOGY_PREFIX) .unwrap() .parse_query(&query) .expect("Unable to parse query"); sparql.dataset_mut().set_default_graph_as_union(); let store = self.store.clone(); tokio::task::spawn_blocking(move || { let span = debug_span!("Indexable Triples Query", solutions = field::Empty).entered(); let query_results = if let Some(source) = &source { sparql.on_queryable_dataset(source).execute() } else { sparql.on_store(&store).execute() } .expect("Unable to execute indexing query"); let mut results = HashMap::new(); if let QueryResults::Solutions(solutions) = query_results { let mut counter = 0usize; for solution in solutions.filter_map(Result::ok) { let individual = solution .get("individual") .and_then(conversion::term_to_named_node); let catalog_id = solution.get("categoryId").and_then(conversion::term_to_u64); let field_name = solution.get("fieldName").and_then(conversion::term_as_str); let field_value = solution.get("fieldValue").and_then(conversion::term_as_str); if let ( Some(individual), Some(catalog_id), Some(field_name), Some(field_value), ) = (individual, catalog_id, field_name, field_value) { results .entry(individual.to_owned()) .and_modify(|entry: &mut IndexEntry| { entry .fields .insert(field_name.to_owned(), field_value.to_owned()); }) .or_insert(IndexEntry { category_id: catalog_id, fields: HashMap::from_iter([( field_name.to_owned(), field_value.to_owned(), )]), }); } counter += 1; } span.record("solutions", counter); } else { unreachable!() } results }) .map_err(error::Error::from) } pub fn category_id(&self, class: &NamedNode) -> Option { None /*self.store .quads_for_pattern(Some(class.into()), Some(gl::CATEGORY_ID), None, None) .filter_map(Result::ok) .map(conversion::quad_into_term) .filter_map(conversion::term_into_u64) .next()*/ } pub fn fields_for_class( &self, class: &NamedNode, language: &LanguageCondition, ) -> error::Result> { let language_filter = language.to_filter_expression("label"); let query = format!( r#"SELECT DISTINCT ?name ?label {{ {class} gl:associatedProperty/gl:indexedByField ?field . ?field gl:fieldName ?name ; gl:fieldLabel ?label . {language_filter} }}"# ); let mut sparql = SparqlEvaluator::new() .with_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#")? .with_prefix("gl", ONTOLOGY_PREFIX)? .parse_query(&query)?; sparql.dataset_mut().set_default_graph_as_union(); let mut results = Vec::new(); if let QueryResults::Solutions(solutions) = sparql.on_store(&self.store).execute()? { for solution in solutions.filter_map(Result::ok) { let name = solution.get("name").and_then(conversion::term_as_str); let label = solution.get("label").and_then(conversion::term_as_str); if let Some(name) = name && let Some(label) = label { results.push(IndexField { name: name.to_string(), label: label.to_string(), }); } } } Ok(results) } pub fn info(&self, iri: &NamedNode, language: &LanguageCondition) -> LabeledIri { let label = self .store .quads_for_pattern(Some(iri.as_ref().into()), Some(rdfs::LABEL), None, None) .filter_map(Result::ok) .map(conversion::quad_into_term) .filter(|term| language.primary_matches_term(term)) .filter_map(conversion::term_into_string) .next() .unwrap_or_default(); LabeledIri { iri: iri.clone(), label, } } pub fn subclasses_of(&self, class: &NamedNode) -> BTreeSet { self.store .quads_for_pattern(None, Some(rdfs::SUB_CLASS_OF), Some(class.into()), None) .filter_map(Result::ok) .filter_map(|quad| { if let NamedOrBlankNode::NamedNode(subject) = quad.subject { Some(subject) } else { None } }) .collect() } pub fn datatypes(&self) -> BTreeSet { self.store .quads_for_pattern( None, Some(rdf::TYPE), Some(TermRef::NamedNode(rdfs::DATATYPE)), None, ) .filter_map(Result::ok) .filter_map(|quad| match quad.subject { NamedOrBlankNode::NamedNode(subject) => Some(subject), _ => None, }) .collect() } pub fn is_read_only<'a>(&self, triple: impl Into>) -> bool { let triple = triple.into(); let true_term = TermRef::Literal(LiteralRef::new_typed_literal("true", xsd::BOOLEAN)); let subject = match (triple.predicate, triple.object) { (rdf::TYPE, TermRef::NamedNode(class)) => class, (predicate, _) => predicate, }; self.store .quads_for_pattern( Some(NamedOrBlankNodeRef::NamedNode(subject)), Some(gl::READ_ONLY), Some(true_term), None, ) .filter_map(Result::ok) .count() >= 1 } pub fn exclude_read_only(&self) -> impl Fn(TripleRef<'_>) -> bool + 'static { let true_term = TermRef::Literal(LiteralRef::new_typed_literal("true", xsd::BOOLEAN)); let read_only_graph = self .store .quads_for_pattern(None, Some(gl::READ_ONLY), Some(true_term), None) .filter_map(Result::ok) .map(Triple::from) .collect::(); move |triple| { if triple.predicate == rdf::TYPE && let TermRef::NamedNode(class) = triple.object { read_only_graph.triples_for_subject(class).count() == 0 } else { read_only_graph .triples_for_subject(triple.predicate) .count() == 0 } } } }