diff --git a/graph/src/class.rs b/graph/src/class.rs index 5b6853d..24a537a 100644 --- a/graph/src/class.rs +++ b/graph/src/class.rs @@ -1,5 +1,5 @@ use std::fmt::Display; -use oxigraph::model::{Graph, Literal, NamedNode, NamedNodeRef, NamedOrBlankNode, Term, Triple}; +use oxigraph::model::{Graph, Literal, LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNode, Term, TermRef, Triple}; use oxilangtag::LanguageTag; use url::Url; use gl_search::Schema; @@ -125,28 +125,53 @@ impl Class { } pub fn template(&self, subject: NamedNode) -> Graph { - let empty_english_string_term = Term::Literal(Literal::new_language_tagged_literal_unchecked("", "en")); - let empty_string_term = Term::Literal(Literal::new_simple_literal("")); - let empty_node = NamedNode::new_unchecked(""); - let hash_node = NamedNode::new_unchecked("#"); + let subject = subject.as_ref(); + let empty_english_string_literal = LiteralRef::new_language_tagged_literal_unchecked("", "en"); + let empty_string_literal = LiteralRef::new_simple_literal(""); + let empty_node = NamedNodeRef::new_unchecked(""); + let hash_node = NamedNodeRef::new_unchecked("#"); match self { Class::RdfProperty => Graph::new(), Class::RdfsClass => Graph::new(), Class::SkosConcept => Graph::new(), Class::Work => Graph::from_iter([ - Triple::new(NamedOrBlankNode::NamedNode(subject), vocab::rdawd::TITLE_OF_WORK, empty_english_string_term) + Triple::new(subject, vocab::rdawd::TITLE_OF_WORK, empty_english_string_literal), + Triple::new(subject, vocab::rdawo::IDENTIFIER_FOR_WORK, empty_node), + Triple::new(subject, vocab::rdawo::EXPRESSION_OF_WORK, empty_node), ]), Class::Person => Graph::from_iter([ - Triple::new(NamedOrBlankNode::NamedNode(subject.clone()), vocab::rdaad::GIVEN_NAME, empty_english_string_term.clone()), - Triple::new(NamedOrBlankNode::NamedNode(subject.clone()), vocab::rdaad::SURNAME, empty_english_string_term), - Triple::new(NamedOrBlankNode::NamedNode(subject.clone()), vocab::rdaao::IDENTIFIER_FOR_PERSON, Term::NamedNode(hash_node.clone())), - Triple::new(NamedOrBlankNode::NamedNode(hash_node.clone()), vocab::rdano::SCHEME_OF_NOMEN, Term::NamedNode(empty_node)), - Triple::new(NamedOrBlankNode::NamedNode(subject.clone()), vocab::rdand::NOMEN_STRING, empty_string_term), + Triple::new(subject, vocab::rdaad::GIVEN_NAME, empty_english_string_literal), + Triple::new(subject, vocab::rdaad::SURNAME, empty_english_string_literal), + Triple::new(subject, vocab::rdaao::IDENTIFIER_FOR_PERSON, hash_node), + Triple::new(hash_node, vocab::rdano::SCHEME_OF_NOMEN, empty_node), + Triple::new(hash_node, vocab::rdand::NOMEN_STRING, empty_string_literal), + ]), + Class::CorporateBody => Graph::from_iter([ + Triple::new(subject, vocab::rdaao::IDENTIFIER_FOR_CORPORATE_BODY, hash_node), + Triple::new(subject, vocab::rdaad::NAME_OF_CORPORATE_BODY, empty_english_string_literal), + Triple::new(hash_node, vocab::rdano::SCHEME_OF_NOMEN, empty_node), + Triple::new(hash_node, vocab::rdand::NOMEN_STRING, empty_string_literal), + ]), + Class::Expression => Graph::from_iter([ + Triple::new(subject, vocab::rdaeo::WORK_EXPRESSED, empty_node), + Triple::new(subject, vocab::rdaeo::LANGUAGE_OF_EXPRESSION, vocab::lclang::ENGLISH), + Triple::new(subject, vocab::rdaeo::TITLE_OF_EXPRESSION, empty_english_string_literal), + Triple::new(subject, vocab::rdaeo::CONTENT_TYPE, empty_node), + Triple::new(subject, vocab::rdaeo::MANIFESTATION_OF_EXPRESSION, empty_node), + ]), + Class::Manifestation => Graph::from_iter([ + Triple::new(subject, vocab::rdamd::TITLE_OF_MANIFESTATION, empty_english_string_literal), + Triple::new(subject, vocab::rdamd::UNIFORM_RESOURCE_LOCATOR, empty_string_literal), + Triple::new(subject, vocab::rdamo::CARRIER_TYPE, vocab::rdact::ONLINE_RESOURCE), + Triple::new(subject, vocab::rdamo::MEDIA_TYPE, empty_node), + Triple::new(subject, vocab::rdamo::FILE_TYPE, empty_node), + Triple::new(subject, vocab::rdamo::CATEGORY_OF_MANIFESTATION, empty_node), + Triple::new(subject, vocab::rdamo::EXPRESSION_MANIFESTED, empty_node), + Triple::new(subject, vocab::rdamo::IDENTIFIER_FOR_MANIFESTATION, hash_node), + Triple::new(hash_node, vocab::rdano::SCHEME_OF_NOMEN, empty_node), + Triple::new(hash_node, vocab::rdand::NOMEN_STRING, empty_string_literal), ]), - Class::CorporateBody => Graph::new(), - Class::Expression => Graph::new(), - Class::Manifestation => Graph::new(), } } } \ No newline at end of file diff --git a/graph/src/ontology.rs b/graph/src/ontology.rs index 4037765..4eb7538 100644 --- a/graph/src/ontology.rs +++ b/graph/src/ontology.rs @@ -106,11 +106,11 @@ PREFIX rdfs: PREFIX xsd: PREFIX gl: -SELECT ?subject ?class WHERE {{ - VALUES ?class {{ rdf:Property rdfs:Class }} +SELECT ?subject ?class WHERE { + VALUES ?class { rdf:Property rdfs:Class } ?subject a ?class ; gl:readOnly "true"^^xsd:boolean . -}}"#.to_owned(); +}"#.to_owned(); let request = OntologyQueryRequest { sparql_query: Some(sparql_query), diff --git a/graph/src/vocab.rs b/graph/src/vocab.rs index 72dd69b..64d3a8e 100644 --- a/graph/src/vocab.rs +++ b/graph/src/vocab.rs @@ -14,11 +14,11 @@ pub mod glo { NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/nextUrl"); } -pub mod skos { +pub mod lclang { use oxigraph::model::NamedNodeRef; - pub const CONCEPT: NamedNodeRef = - NamedNodeRef::new_unchecked("http://www.w3.org/2004/02/skos/core#Concept"); + pub const ENGLISH: NamedNodeRef = + NamedNodeRef::new_unchecked("http://id.loc.gov/vocabulary/languages/eng"); } pub mod owl { @@ -47,6 +47,13 @@ pub mod rdac { NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/c/C10007"); } +pub mod rdact { + use oxigraph::model::NamedNodeRef; + + pub const ONLINE_RESOURCE: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/termList/RDACarrierType/1018"); +} + pub mod rdaad { use oxigraph::model::NamedNodeRef; @@ -65,6 +72,9 @@ pub mod rdaao { pub const IDENTIFIER_FOR_PERSON: NamedNodeRef = NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/a/object/P50094"); + + pub const IDENTIFIER_FOR_CORPORATE_BODY: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/a/object/P50006"); } pub mod rdawd { @@ -74,6 +84,16 @@ pub mod rdawd { NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/w/datatype/P10088"); } +pub mod rdawo { + use oxigraph::model::NamedNodeRef; + + pub const IDENTIFIER_FOR_WORK: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/w/object/P10002"); + + pub const EXPRESSION_OF_WORK: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/w/object/P10078"); +} + pub mod rdaed { use oxigraph::model::NamedNodeRef; @@ -81,11 +101,55 @@ pub mod rdaed { NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/e/datatype/P20312"); } +pub mod rdaeo { + use oxigraph::model::NamedNodeRef; + + pub const WORK_EXPRESSED: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/e/object/P20231"); + + pub const LANGUAGE_OF_EXPRESSION: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/e/object/P20006"); + + pub const TITLE_OF_EXPRESSION: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/e/datatype/P20312"); + + pub const CONTENT_TYPE: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/e/object/P20001"); + + pub const MANIFESTATION_OF_EXPRESSION: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/e/object/P20059"); +} + pub mod rdamd { use oxigraph::model::NamedNodeRef; pub const TITLE_OF_MANIFESTATION: NamedNodeRef = NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/m/datatype/P30335"); + + pub const UNIFORM_RESOURCE_LOCATOR: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/m/datatype/P30154"); +} + +pub mod rdamo { + use oxigraph::model::NamedNodeRef; + + pub const CARRIER_TYPE: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/m/object/P30001"); + + pub const MEDIA_TYPE: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/m/object/P30002"); + + pub const IDENTIFIER_FOR_MANIFESTATION: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/m/object/P30004"); + + pub const FILE_TYPE: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/m/object/P30018"); + + pub const CATEGORY_OF_MANIFESTATION: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/m/object/P30335"); + + pub const EXPRESSION_MANIFESTED: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/m/object/P30139"); } pub mod rdand { @@ -100,4 +164,11 @@ pub mod rdano { pub const SCHEME_OF_NOMEN: NamedNodeRef = NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/n/object/P80069"); +} + +pub mod skos { + use oxigraph::model::NamedNodeRef; + + pub const CONCEPT: NamedNodeRef = + NamedNodeRef::new_unchecked("http://www.w3.org/2004/02/skos/core#Concept"); } \ No newline at end of file diff --git a/publish/src/rdf/ontology.rs b/publish/src/rdf/ontology.rs deleted file mode 100644 index c6e94c7..0000000 --- a/publish/src/rdf/ontology.rs +++ /dev/null @@ -1,396 +0,0 @@ -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, 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 - } - } - } -} \ No newline at end of file