diff --git a/Cargo.lock b/Cargo.lock index d8046ef..d004b42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1633,6 +1633,7 @@ dependencies = [ "oxigraph", "oxilangtag", "rayon", + "spargebra", "thiserror 2.0.20", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index 5356ab6..06d477a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ rand = "0.10" rayon = "1.12" rfd = "0.17" slotmap = "1.1" +spargebra = "0.4" subtitler = "2.6" tantivy = "0.26" tar = "0.4" diff --git a/graph/Cargo.toml b/graph/Cargo.toml index 60a82db..3e4b7c6 100644 --- a/graph/Cargo.toml +++ b/graph/Cargo.toml @@ -10,5 +10,6 @@ gl-search.workspace = true oxigraph.workspace = true oxilangtag.workspace = true rayon.workspace = true +spargebra.workspace = true thiserror.workspace = true tracing.workspace = true \ No newline at end of file diff --git a/graph/src/class.rs b/graph/src/class.rs index d4a08fa..7cb5a3a 100644 --- a/graph/src/class.rs +++ b/graph/src/class.rs @@ -1,6 +1,12 @@ -use oxigraph::model::NamedNodeRef; +use std::collections::HashMap; +use std::fmt::Display; +use oxigraph::model::{NamedNode, NamedNodeRef}; +use oxilangtag::LanguageTag; +use gl_search::Schema; +use crate::language::LanguageCondition; use crate::vocab; +#[derive(Clone, Debug, Eq, PartialEq)] pub enum Class { RdfProperty = 0, RdfsClass = 1, @@ -12,7 +18,33 @@ pub enum Class { Manifestation = 10007, } +impl Display for Class { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Class::RdfProperty => f.write_str("RDF Property"), + Class::RdfsClass => f.write_str("RDFS Class"), + Class::SkosConcept => f.write_str("SKOS Concept"), + Class::Work => f.write_str("Work"), + Class::Person => f.write_str("Person"), + Class::CorporateBody => f.write_str("Corporate Body"), + Class::Expression => f.write_str("Expression"), + Class::Manifestation => f.write_str("Manifestation"), + } + } +} + impl Class { + pub const ALL: &[Class] = &[ + Class::RdfProperty, + Class::RdfsClass, + Class::SkosConcept, + Class::Work, + Class::Person, + Class::CorporateBody, + Class::Expression, + Class::Manifestation, + ]; + pub fn to_named_node(&self) -> NamedNodeRef<'_> { match self { Class::RdfProperty => vocab::rdf::PROPERTY, @@ -40,4 +72,45 @@ impl Class { _ => None, } } + + pub fn fields(&self, language: LanguageTag<&str>) -> Vec<(gl_search::Field, String)> { + match self { + Class::RdfProperty => vec![ + (Schema::curie_field(), String::from("CURIE")), + (Schema::field("label", Some(language.primary_language())), String::from("Label")), + (Schema::field("definition", Some(language.primary_language())), String::from("Definition")), + ], + Class::RdfsClass => vec![ + (Schema::curie_field(), String::from("CURIE")), + (Schema::field("label", Some(language.primary_language())), String::from("Label")), + (Schema::field("definition", Some(language.primary_language())), String::from("Definition")), + ], + Class::SkosConcept => vec![ + (Schema::curie_field(), String::from("CURIE")), + (Schema::field("label", Some(language.primary_language())), String::from("Label")), + (Schema::field("definition", Some(language.primary_language())), String::from("Definition")), + ], + Class::Work => vec![ + (Schema::curie_field(), String::from("CURIE")), + (Schema::field("title", Some(language.primary_language())), String::from("Title")), + ], + Class::Person => vec![ + (Schema::curie_field(), String::from("CURIE")), + (Schema::field("given_name", Some(language.primary_language())), String::from("Given Name")), + (Schema::field("surname", Some(language.primary_language())), String::from("Surname")), + ], + Class::CorporateBody => vec![ + (Schema::curie_field(), String::from("CURIE")), + (Schema::field("corporate_body", Some(language.primary_language())), String::from("Corporate Body")), + ], + Class::Expression => vec![ + (Schema::curie_field(), String::from("CURIE")), + (Schema::field("title", Some(language.primary_language())), String::from("Title")), + ], + Class::Manifestation => vec![ + (Schema::curie_field(), String::from("CURIE")), + (Schema::field("title", Some(language.primary_language())), String::from("Title")), + ], + } + } } \ No newline at end of file diff --git a/graph/src/error.rs b/graph/src/error.rs index 3e49a4f..a1e42de 100644 --- a/graph/src/error.rs +++ b/graph/src/error.rs @@ -4,6 +4,9 @@ pub type Result = std::result::Result; #[derive(Error, Debug)] pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] IriParse(#[from] oxigraph::model::IriParseError), @@ -22,6 +25,9 @@ pub enum Error { #[error(transparent)] UpdateEvaluation(#[from] oxigraph::sparql::UpdateEvaluationError), + #[error(transparent)] + TonicTransport(#[from] gl_inference::tonic::transport::Error), + #[error(transparent)] TonicStatus(#[from] gl_inference::tonic::Status), } diff --git a/graph/src/helpers.rs b/graph/src/helpers.rs index bdd5146..faaf8cc 100644 --- a/graph/src/helpers.rs +++ b/graph/src/helpers.rs @@ -1,4 +1,4 @@ -use oxigraph::model::{NamedNode, NamedNodeRef, Term, TermRef}; +use oxigraph::model::{NamedNode, Term, TermRef}; use oxigraph::model::vocab::xsd; use crate::vocab::rdf; diff --git a/graph/src/indexer.rs b/graph/src/indexer.rs index e3e1be6..0802af6 100644 --- a/graph/src/indexer.rs +++ b/graph/src/indexer.rs @@ -74,6 +74,15 @@ impl<'a> Indexer<'a> { ]) } + fn manifestation(&self, graph: &Graph, subject: NamedNodeRef) -> HashMap { + HashMap::from_iter([ + ( + Schema::field("title", self.language.primary_language()), + self.extract_string(graph, subject, vocab::rdamd::TITLE_OF_MANIFESTATION), + ), + ]) + } + pub fn graph(&self, graph: &Graph) -> crate::Result>> { let mut entities_and_types = HashSet::new(); let triples = graph.triples_for_predicate(vocab::rdf::TYPE); @@ -95,7 +104,7 @@ impl<'a> Indexer<'a> { Class::Person => Some(self.person(graph, *subject)), Class::CorporateBody => Some(self.corporate_body(graph, *subject)), Class::Expression => Some(self.expression(graph, *subject)), - Class::Manifestation => Some(self.person(graph, *subject)), + Class::Manifestation => Some(self.manifestation(graph, *subject)), }.map(|mut document| { document.insert(Schema::discriminant_field(), OwnedValue::from(class as u64)); document.insert(Schema::iri_field(), OwnedValue::Str(subject.as_str().to_string())); diff --git a/graph/src/language.rs b/graph/src/language.rs index e236941..ddcbbf2 100644 --- a/graph/src/language.rs +++ b/graph/src/language.rs @@ -1,6 +1,9 @@ -use oxigraph::model::TermRef; +use oxigraph::model::{Literal, TermRef}; use oxilangtag::LanguageTag; use std::sync::LazyLock; +use spargebra::algebra::{Expression, Function, GraphPattern}; +use spargebra::term::{NamedNode, NamedNodePattern, TermPattern, TriplePattern, Variable}; +use tracing::debug; pub const ENGLISH_PRIMARY: &str = "en"; @@ -10,7 +13,7 @@ pub static ENGLISH_OR_UNTAGGED: LazyLock = LazyLock::new(|| { ) }); -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum LanguageCondition { ExactMatchOnly(LanguageTag), ExactMatchOrUntagged(LanguageTag), @@ -49,7 +52,51 @@ impl LanguageCondition { } } - pub fn to_filter_expression(&self, var: &str) -> String { + pub fn filter(conditions: impl IntoIterator) -> String { + let exact_expression = |language, variable| Expression::FunctionCall( + Function::LangMatches, vec![ + Expression::FunctionCall(Function::Lang, vec![ + Expression::Variable(Variable::new_unchecked(variable)) + ]), + Expression::Literal(Literal::new_simple_literal(language)) + ], + ); + + let untagged_expression = |variable| Expression::Not( + Box::new(Expression::FunctionCall(Function::HasLang, vec![ + Expression::Variable(Variable::new_unchecked(variable)) + ])) + ); + + let condition_to_expression = |variable, condition| match condition { + LanguageCondition::ExactMatchOnly(language) => + Some(exact_expression(language.to_string(), variable)), + LanguageCondition::ExactMatchOrUntagged(language) => { + Some(Expression::Or( + Box::new(exact_expression(language.to_string(), variable.clone())), + Box::new(untagged_expression(variable)), + )) + } + LanguageCondition::UntaggedOnly => Some(untagged_expression(variable)), + LanguageCondition::AnyOrNone => None, + }; + + let mut iter = conditions.into_iter(); + let first_expression = iter.next() + .and_then(|(variable, condition)| condition_to_expression(variable, condition)); + let remaining_expressions = iter.filter_map(|(variable, condition)| condition_to_expression(variable, condition)) + .collect::>(); + let concatenated_expressions = if let Some(expression) = first_expression { + if remaining_expressions.is_empty() { + //Expression:: + } + } else { String::default() } + } + + pub fn to_sparql_expression(&self, var: &str) -> String { + let foo = + debug!("Test: {foo}"); + match self { LanguageCondition::ExactMatchOnly(language) => { format!(r#"FILTER(langMATCHES(LANG(?{var}), "{language}"))"#) diff --git a/graph/src/lib.rs b/graph/src/lib.rs index dc0d661..4a64274 100644 --- a/graph/src/lib.rs +++ b/graph/src/lib.rs @@ -7,6 +7,7 @@ pub mod category; pub mod language; mod helpers; pub mod indexer; +pub mod ontology; pub use curie::{CurieHelper, PREFIXES}; pub use error::{Error, Result}; \ No newline at end of file diff --git a/graph/src/ontology.rs b/graph/src/ontology.rs new file mode 100644 index 0000000..b9834d1 --- /dev/null +++ b/graph/src/ontology.rs @@ -0,0 +1,214 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; +use oxigraph::io::{RdfFormat, RdfParser, RdfSerializer}; +use oxigraph::model::{Graph, NamedNode, Triple, TripleRef}; +use oxigraph::sparql::results::{QueryResultsFormat, QueryResultsParser, SliceQueryResultsParserOutput}; +use gl_inference::proto::ontology_client::OntologyClient; +use gl_inference::proto::OntologyQueryRequest; +use gl_inference::tonic::codegen::StdError; +use gl_inference::tonic::transport::{Channel, Endpoint}; +use crate::helpers::{term_as_str, term_to_named_node}; +use crate::language::LanguageCondition; +use crate::vocab::{rdf, rdfs}; + +#[derive(Clone, Debug, Default)] +pub struct ResourceDescription { + pub label: Option, + pub description: Option, +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum ReadOnlyEntity { + Property(NamedNode), + Class(NamedNode), +} + +#[derive(Clone)] +pub struct Ontology { + client: OntologyClient, + language: LanguageCondition, +} + +impl Debug for Ontology { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("ontology") + } +} + +impl Ontology { + pub async fn new(endpoint: D, language: LanguageCondition) -> crate::Result + where + D: TryInto, + D::Error: Into, + { + let client = OntologyClient::connect(endpoint).await?; + Ok(Self { + client, + language, + }) + } + + pub async fn run_inference(&mut self, graph: &Graph) -> crate::Result { + let mut output_buffer = Vec::new(); + let mut serializer = RdfSerializer::from_format(RdfFormat::Turtle) + .for_writer(output_buffer); + for triple in graph { + serializer.serialize_triple(triple)?; + } + output_buffer = serializer.finish()?; + let turtle = String::from_utf8_lossy(&output_buffer).to_string(); + + let mut request = OntologyQueryRequest::default(); + request.sparql_query = None; + request.turtle = Some(turtle); + request.base = None; + request.inferences_only = true; + + let response = self.client.query(request).await?; + Ok(RdfParser::from_format(RdfFormat::Turtle) + .for_slice(&response.get_ref().results) + .filter_map(Result::ok) + .map(Triple::from) + .collect::()) + } + + pub async fn list_read_only(&mut self) -> crate::Result> { + let mut request = OntologyQueryRequest::default(); + request.sparql_query = Some(format!(r#"PREFIX rdf: +PREFIX rdfs: +PREFIX xsd: +PREFIX gl: + +SELECT ?subject ?class WHERE {{ + VALUES ?class {{ rdf:Property rdfs:Class }} + ?subject a ?class ; + gl:readOnly "true"^^xsd:boolean . +}}"#)); + + let response = self.client.query(request).await?; + let parser_output = QueryResultsParser::from_format(QueryResultsFormat::Json) + .for_slice(&response.get_ref().results)?; + + let mut results = HashSet::new(); + if let SliceQueryResultsParserOutput::Solutions(solutions) = parser_output { + for solution in solutions.filter_map(Result::ok) { + let subject = solution + .get("subject") + .and_then(term_to_named_node) + .map(|node| node.clone()); + + let class = solution + .get("class") + .and_then(term_to_named_node) + .map(|node| node.clone()); + + if let Some(subject) = subject && + let Some(class) = class { + let entry = match class.as_ref() { + rdf::PROPERTY => ReadOnlyEntity::Property(subject), + rdfs::CLASS => ReadOnlyEntity::Class(subject), + _ => continue, + }; + results.insert(entry); + } + } + } + Ok(results) + } + + pub async fn datatypes(&mut self) -> crate::Result> { + let label_filter = self.language.to_filter_expression("label"); + let description_filter = self.language.to_filter_expression("description"); + + let mut request = OntologyQueryRequest::default(); + request.sparql_query = Some(format!(r#"PREFIX rdf: +PREFIX rdfs: +PREFIX skos: + +SELECT ?subject ?label ?description WHERE {{ + ?subject a rdfs:Datatype + OPTIONAL {{ ?subject rdfs:label ?label }} + {label_filter} + + OPTIONAL {{ ?subject rdfs:comment ?comment }} + OPTIONAL {{ ?subject skos:definition ?definition }} + BIND(COALESCE(?definition, ?comment) AS ?description) + {description_filter} +}}"#)); + + let response = self.client.query(request).await?; + let parser_output = QueryResultsParser::from_format(QueryResultsFormat::Json) + .for_slice(&response.get_ref().results)?; + + let mut results = HashMap::new(); + if let SliceQueryResultsParserOutput::Solutions(solutions) = parser_output { + for solution in solutions.filter_map(Result::ok) { + let subject = solution + .get("subject") + .and_then(term_to_named_node) + .map(|node| node.clone()); + + if let Some(subject) = subject { + let label = solution + .get("label") + .and_then(term_as_str) + .map(String::from); + + let description = solution + .get("description") + .and_then(term_as_str) + .map(String::from); + + results.insert(subject, ResourceDescription { label, description }); + } + } + } + Ok(results) + } + + pub async fn resource_description(&mut self, subject: NamedNode) -> crate::Result { + let label_filter = self.language.to_filter_expression("label"); + let description_filter = self.language.to_filter_expression("description"); + + let mut request = OntologyQueryRequest::default(); + request.sparql_query = Some(format!(r#"PREFIX rdf: +PREFIX rdfs: +PREFIX skos: + +SELECT ?label ?description WHERE {{ + OPTIONAL {{ {subject} rdfs:label ?label }} + {label_filter} + + OPTIONAL {{ {subject} rdfs:comment ?comment }} + OPTIONAL {{ {subject} skos:definition ?definition }} + BIND(COALESCE(?definition, ?comment) AS ?description) + {description_filter} +}}"#)); + + let response = self.client.query(request).await?; + let parser_output = QueryResultsParser::from_format(QueryResultsFormat::Json) + .for_slice(&response.get_ref().results)?; + + if let SliceQueryResultsParserOutput::Solutions(solutions) = parser_output { + Ok(solutions.filter_map(Result::ok) + .next() + .map(|solution| { + let label = solution + .get("label") + .and_then(term_as_str) + .map(String::from); + + let description = solution + .get("description") + .and_then(term_as_str) + .map(String::from); + + ResourceDescription { + label, + description, + } + }).unwrap_or(ResourceDescription::default()) + ) + } else { unreachable!() } + } +} \ No newline at end of file diff --git a/graph/src/vocab.rs b/graph/src/vocab.rs index 9e7173d..6341306 100644 --- a/graph/src/vocab.rs +++ b/graph/src/vocab.rs @@ -69,4 +69,11 @@ pub mod rdaed { pub const TITLE_OF_EXPRESSION: NamedNodeRef = NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/e/datatype/P20312"); +} + +pub mod rdamd { + use oxigraph::model::NamedNodeRef; + + pub const TITLE_OF_MANIFESTATION: NamedNodeRef = + NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/m/datatype/P30335"); } \ No newline at end of file diff --git a/inference/src/service.rs b/inference/src/service.rs index 2f3a4a7..c7e3dc2 100644 --- a/inference/src/service.rs +++ b/inference/src/service.rs @@ -5,7 +5,7 @@ use oxigraph::sparql::{QueryResults, SparqlEvaluator}; use oxigraph::sparql::results::{QueryResultsFormat, QueryResultsSerializer}; use oxigraph::store::Store; use tonic::{Request, Response, Status}; -use tracing::{debug_span, field}; +use tracing::{debug, debug_span, field}; use gl_inference::proto::ontology_server::Ontology; use gl_inference::proto::{OntologyClearResponse, OntologyLoadRequest, OntologyLoadResponse, OntologyQueryRequest, OntologyQueryResponse}; @@ -96,10 +96,11 @@ impl Ontology for OntologyService { let request = request.get_ref(); let mut response = OntologyQueryResponse::default(); + let provided_dataset; let graph_name = if let Some(turtle) = &request.turtle { let random_graph_identifier = BlankNode::default(); let graph_name = GraphName::NamedNode(NamedNode::new_unchecked(format!("https://graphofliberty.org/inference/{}", random_graph_identifier.as_str()))); - let dataset = RdfParser::from_format(RdfFormat::Turtle) + provided_dataset = RdfParser::from_format(RdfFormat::Turtle) .for_slice(&turtle) .filter_map(Result::ok) .map(|mut quad| { @@ -107,7 +108,7 @@ impl Ontology for OntologyService { quad }).collect::(); - self.ontology.extend(&dataset) + self.ontology.extend(&provided_dataset) .map_err(|err| Status::internal(err.to_string()))?; let inferences = crate::logic::infer(0, &self.ontology, graph_name.as_ref()) @@ -115,11 +116,14 @@ impl Ontology for OntologyService { span.record("inferences", inferences); graph_name } else { + provided_dataset = Dataset::new(); ONTOLOGY_GRAPH.into_owned() }; let mut output_buffer = Vec::new(); if let Some(sparql_query) = &request.sparql_query { + debug!("SPARQL Query: {sparql_query}"); + let mut evaluator = SparqlEvaluator::new(); for (name, iri) in &request.prefixes { evaluator = evaluator.with_prefix(name, iri) @@ -185,11 +189,13 @@ impl Ontology for OntologyService { } let mut serializer = serializer.for_writer(output_buffer); - let graph = self.ontology + let resulting_dataset = self.ontology .quads_for_pattern(None, None, None, Some(graph_name.as_ref())) .filter_map(Result::ok); - for triple in graph { - serializer.serialize_triple(triple.as_ref())?; + for quad in resulting_dataset { + if !(request.inferences_only && provided_dataset.contains(quad.as_ref())) { + serializer.serialize_triple(quad.as_ref())?; + } } output_buffer = serializer.finish()?; } diff --git a/proto/inference.proto b/proto/inference.proto index 490f344..60a5273 100644 --- a/proto/inference.proto +++ b/proto/inference.proto @@ -24,6 +24,7 @@ message OntologyQueryRequest { optional string sparql_query = 2; map prefixes = 3; optional string base = 4; + bool inferences_only = 5; } message OntologyQueryResponse { diff --git a/publish/src/app.rs b/publish/src/app.rs index 2666e1e..cdac080 100644 --- a/publish/src/app.rs +++ b/publish/src/app.rs @@ -1,22 +1,21 @@ +use std::collections::{HashMap, HashSet}; use crate::navigator::Navigator; -use crate::rdf::ontology::{LabeledIri, Ontology}; use crate::rdf::term_helper::{TermHelper, TermHelperMut}; use crate::widget::iri_input::iri_input; use crate::widget::navigation_area::navigation_area; use gl_graph::CurieHelper; -use gl_graph::language; use gl_search::{Schema, SearchIndex}; use http::StatusCode; use iced::alignment::Horizontal; use iced::keyboard::{Event, key}; use iced::widget::button::Style; -use iced::widget::grid::Sizing; use iced::widget::{ button, center, column, combo_box, container, grid, mouse_area, opaque, operation, pick_list, row, scrollable, space, stack, table, text, text_input, toggler, }; use iced::window::Settings; use iced::{Background, Color, Element, Length, Subscription, Task, color, keyboard, window}; +use iced::advanced::text::Wrapping; use ldp::middleware::BasicAuthMiddleware; use ldp::model::{KeyedDataset, QuadKey}; use ldp::reqwest::{Client, Url}; @@ -24,15 +23,23 @@ use ldp::reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; use ldp::{RdfSource, RdfSourceUpdateResponse, ResourceRequestBuilder, SerializationOptions}; use oxigraph::io::RdfFormat; use oxigraph::model::vocab::{rdf, rdfs}; -use oxigraph::model::{ - BaseDirection, Dataset, Graph, NamedNode, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term, - TripleRef, -}; -use tracing::{debug_span, error, trace}; +use oxigraph::model::{BaseDirection, Dataset, Graph, NamedNode, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term, TermRef, Triple, TripleRef}; +use tracing::{debug, error, trace}; +use gl_graph::class::Class; +use gl_graph::ontology::{Ontology, ReadOnlyEntity, ResourceDescription}; +use gl_search::tantivy::schema::Value; +use crate::tasks; #[derive(Clone, Debug)] pub(crate) enum Message { None, + ConnectToOntologyService(String), + ConnectedToOntologyService(Ontology), + CacheDatatypes(HashMap), + CacheReadOnlyEntities(HashSet), + PopulateCaches, + LookupResourceDescription(NamedNode), + CacheResourceDescription(NamedNode, ResourceDescription), WindowClosed(window::Id), URLInputChanged(String), URLInputSubmitted, @@ -49,8 +56,8 @@ pub(crate) enum Message { HoverRow(QuadKey), UnhoverRow(QuadKey), QueryUpdated(String), - SetSearchResults(Vec<()>), - QueryTypeUpdated(LabeledIri), + SetSearchResults(Vec>), + UpdateQueryClass(Class), SearchResultClicked(NamedNode), DatatypeUpdated(QuadKey, Option), LanguageUpdated(QuadKey, Option), @@ -68,11 +75,10 @@ pub(crate) enum Message { NavigateBack, NavigateForward, ResetState, - SetReadOnly(bool), - SetInferredTypes(bool), - SetInferredProperties(bool), + SetShowReadOnly(bool), + SetShowInferredTriples(bool), RunInference, - SetInferredTriples(Dataset), + SetInferredTriples(Graph), Event(Event), } @@ -92,16 +98,18 @@ pub(crate) enum SearchResultClickAction { struct SearchState { window_id: window::Id, action: SearchResultClickAction, - entity_selection: Option, + entity_class_selection: Option, query: String, - results: Vec<()>, + results: Vec>, } pub(crate) struct Publisher { http_client: ClientWithMiddleware, curie_helper: CurieHelper, - ontology: Ontology, + ontology: Option, + read_only_entities: HashSet, abbreviated_datatypes: Vec, + resource_descriptions: HashMap, window_id: window::Id, url_input: String, url_input_valid: bool, @@ -109,44 +117,34 @@ pub(crate) struct Publisher { document: RdfSource>, inferred_triples: Graph, show_read_only: bool, - show_inferred_types: bool, - show_inferred_properties: bool, + show_inferred_triples: bool, hovered_row: Option, search_state: Option, index: SearchIndex, - traversal: Option, show_overwrite_confirmation: bool, modified: bool, show_new_document_buttons: bool, } impl Publisher { - pub(crate) fn new() -> (Self, Task) { - let curie_helper = CurieHelper::new(Ontology::prefixes().clone()); + fn is_read_only<'a>(&'a self, triple: impl Into>) -> bool { + let triple = triple.into(); + if triple.predicate == rdf::TYPE && + let TermRef::NamedNode(node) = triple.object { + self.read_only_entities.contains(&ReadOnlyEntity::Class(node.into_owned())) + } else { + self.read_only_entities.contains(&ReadOnlyEntity::Property(triple.predicate.into_owned())) + } + } - let ontology = debug_span!("Ontology Creation").in_scope(|| { - Ontology::builder() - .with_path("/home/alex/.local/share/org.graphofliberty.desktop/ontology") - .build() - .expect("Failed to build ontology") - }); + pub(crate) fn new() -> (Self, Task) { + let curie_helper = CurieHelper::new(gl_graph::PREFIXES.clone()); let index = SearchIndex::builder() .with_path("/home/alex/.local/share/org.graphofliberty.desktop/index") .build() .expect("Failed to build search index"); - let mut abbreviated_datatypes = ontology - .datatypes() - .into_iter() - .map(|node| { - curie_helper - .abbreviate(None, node.as_str()) - .unwrap_or(node.as_str().to_string()) - }) - .collect::>(); - abbreviated_datatypes.sort(); - let (id, task) = window::open(Settings::default()); let client = Client::new(); @@ -165,8 +163,10 @@ impl Publisher { Self { http_client, curie_helper, - ontology, - abbreviated_datatypes, + ontology: None, + read_only_entities: HashSet::new(), + abbreviated_datatypes: Vec::new(), + resource_descriptions: HashMap::new(), window_id: id, url_input: starting_url.to_string(), url_input_valid: true, @@ -174,17 +174,15 @@ impl Publisher { document, inferred_triples: Graph::new(), show_read_only: false, - show_inferred_types: false, - show_inferred_properties: false, + show_inferred_triples: false, hovered_row: None, search_state: None, index, - traversal: None, show_overwrite_confirmation: false, modified: false, show_new_document_buttons: false, }, - task.map(|_| Message::None), + task.map(|_| Message::ConnectToOntologyService("http://[::1]:3000".to_string())), ) } @@ -193,6 +191,39 @@ impl Publisher { trace!(?message); match message { + Message::ConnectToOntologyService(endpoint) => { + task = tasks::connect_to_ontology_service(endpoint) + .chain(Task::done(Message::PopulateCaches)); + } + Message::ConnectedToOntologyService(ontology) => { + self.ontology = Some(ontology); + } + Message::PopulateCaches => { + if let Some(ontology) = &mut self.ontology { + task = Task::batch([ + tasks::list_datatypes(ontology.clone()), + tasks::list_read_only_entities(ontology.clone()), + ]) + } + } + Message::CacheDatatypes(datatypes) => { + self.abbreviated_datatypes = datatypes.keys() + .map(|node| { + self.curie_helper.abbreviate(None, node.as_str()) + .unwrap_or(node.as_str().to_string()) + }).collect(); + } + Message::CacheReadOnlyEntities(entities) => { + self.read_only_entities = entities; + } + Message::LookupResourceDescription(node) => { + if let Some(ontology) = &mut self.ontology { + task = tasks::lookup_resource_description(ontology.clone(), node) + } + } + Message::CacheResourceDescription(node, description) => { + self.resource_descriptions.insert(node, description); + } Message::WindowClosed(id) => { if self.window_id == id { task = iced::exit(); @@ -248,16 +279,16 @@ impl Publisher { }); } Message::LoadDocument(document) => { - let messages = document.dataset().quads.iter().map(|(key, quad)| { + let add_row_tasks = document.dataset().quads.iter().map(|(key, quad)| { let datatype_state = combo_box::State::new(self.abbreviated_datatypes.clone()); - let state = RowState { - read_only: self.ontology.is_read_only(quad.as_ref()), + read_only: self.is_read_only(quad.as_ref()), datatype_state, }; - Message::AddRow(Some((key, state))) + Task::done(Message::AddRow(Some((key, state)))) + .chain(Task::done(Message::LookupResourceDescription(quad.predicate.clone()))) }); - task = Task::batch(messages.map(Task::done)) + task = Task::batch(add_row_tasks) .chain(Task::done(Message::RunInference)) .chain(Task::done(Message::ResetState)); @@ -318,33 +349,29 @@ impl Publisher { self.search_state = Some(SearchState { window_id: id, action, - entity_selection: None, + entity_class_selection: None, query: String::new(), results: Vec::new(), }); task = window_task.then(|_| operation::focus("query")); } - if let Some(selected_entity_class) = selected_entity_class { - let selection = self.ontology.info( - &selected_entity_class.into_owned(), - &*language::ENGLISH_OR_UNTAGGED, - ); - task = task.chain(Task::done(Message::QueryTypeUpdated(selection))); + if let Some(selected_entity_class) = selected_entity_class && + let Some(class) = Class::try_from_named_node(selected_entity_class) { + task = task.chain(Task::done(Message::UpdateQueryClass(class))) } } - Message::QueryTypeUpdated(type_) => { + Message::UpdateQueryClass(type_) => { if let Some(search_state) = &mut self.search_state { - search_state.entity_selection = Some(type_); + search_state.entity_class_selection = Some(type_); task = Task::done(Message::QueryUpdated(search_state.query.clone())); } } Message::QueryUpdated(new_query) => { if let Some(search_state) = &mut self.search_state { - let category_id = search_state - .entity_selection - .clone() - .and_then(|info| self.ontology.category_id(&info.iri)); + let category_id = search_state.entity_class_selection + .as_ref() + .map(|selection| selection.to_owned() as u64); let query = new_query.clone(); search_state.query = new_query; @@ -352,7 +379,7 @@ impl Publisher { .index .query(category_id, query.as_str(), Schema::all_fields(), 25) .expect("Unable to complete search"); - task = Task::done(Message::SetSearchResults(vec![])); + task = Task::done(Message::SetSearchResults(results)); }; } Message::SetSearchResults(results) => { @@ -449,8 +476,16 @@ impl Publisher { } Message::SaveGraph(overwrite) => { let client = self.http_client.clone(); + let read_only_entities = self.read_only_entities.clone(); let options = SerializationOptions::from_format(RdfFormat::Turtle) - .with_filter(self.ontology.exclude_read_only()); + .with_filter(move |triple| { + if triple.predicate == rdf::TYPE && + let TermRef::NamedNode(node) = triple.object { + read_only_entities.contains(&ReadOnlyEntity::Class(node.into_owned())) + } else { + read_only_entities.contains(&ReadOnlyEntity::Property(triple.predicate.into_owned())) + } + }); let request = self .document .to_update(options) @@ -538,19 +573,33 @@ impl Publisher { task = Task::done(Message::URLInputChanged(url.to_string())) .chain(Task::done(Message::NavigateTo(url))); } - Message::SetReadOnly(value) => { + Message::SetShowReadOnly(value) => { self.show_read_only = value; } - Message::SetInferredTypes(value) => { - self.show_inferred_types = value; - } - Message::SetInferredProperties(value) => { - self.show_inferred_properties = value; + Message::SetShowInferredTriples(value) => { + self.show_inferred_triples = value; } Message::RunInference => { + if let Some(ontology) = &mut self.ontology { + let graph = self.document + .dataset() + .quads + .iter() + .map(|(_, quad)| Triple::from(quad.clone())) + .collect(); + + task = tasks::run_inference(ontology.clone(), graph); + } } - Message::SetInferredTriples(dataset) => { - self.inferred_triples = Graph::from_iter(&dataset); + Message::SetInferredTriples(graph) => { + let messages = graph.iter() + .map(|triple| { + Message::LookupResourceDescription(triple.predicate.into_owned()) + }).map(Task::done) + .collect::>(); + task = Task::batch(messages); + + self.inferred_triples = graph; } Message::Event(Event::KeyPressed { key: keyboard::Key::Named(key::Named::Tab), @@ -582,7 +631,7 @@ impl Publisher { fn view_row<'a>( &'a self, key: QuadKey, - triple: &'a Quad, + quad: &'a Quad, state: &'a RowState, ) -> Element<'a, Message> { const BUTTON_WIDTH: Length = Length::Fixed(35.0); @@ -598,13 +647,13 @@ impl Publisher { let base = self.document.origin().as_str(); - let subject = match &triple.subject { + let subject = match &quad.subject { NamedOrBlankNode::NamedNode(subject) => subject.as_str(), _ => "", }; let subject_input_base = iri_input(&self.curie_helper, "Subject", Some(&base), subject); - let subject_input = if self.ontology.is_read_only(triple.as_ref()) { + let subject_input = if state.read_only { subject_input_base } else { subject_input_base.on_input(move |value| Message::SubjectUpdated(key, value)) @@ -614,9 +663,9 @@ impl Publisher { &self.curie_helper, "Predicate", Some(&base), - triple.predicate.as_str(), + quad.predicate.as_str(), ); - let predicate_input = if self.ontology.is_read_only(triple.as_ref()) { + let predicate_input = if state.read_only { predicate_input_base } else { predicate_input_base @@ -627,19 +676,17 @@ impl Publisher { .on_shift_click(Message::NavigateToPredicate(key)) }; - let predicate_info = self - .ontology - .info(&triple.predicate, &*language::ENGLISH_OR_UNTAGGED); - let predicate_label = container(text(predicate_info.label)); + let predicate_label = self.resource_descriptions + .get(&quad.predicate) + .and_then(|description| description.label.clone()) + .map(|label| container(text(label))); - let term = TermHelper::new(&triple.object); + let term = TermHelper::new(&quad.object); - let value_label = term.value_as_named_node().and_then(|node| { - let info = self - .ontology - .info(&node.into_owned(), &*language::ENGLISH_OR_UNTAGGED); - Some(container(text(info.label))) - }); + let value_label = term.value_as_named_node() + .and_then(|node| self.resource_descriptions.get(&node.into_owned())) + .and_then(|description| description.label.clone()) + .map(|label| container(text(label))); let value = term .value_as_named_node() @@ -658,7 +705,7 @@ impl Publisher { // text_input as opposed to an iri_input. let object_input: Element = if term.datatype().is_some() { let base = text_input("Object", value.clone()); - if self.ontology.is_read_only(triple.as_ref()) { + if state.read_only { base.into() } else { base.on_input(move |value| Message::ObjectUpdated(key, value)) @@ -667,7 +714,7 @@ impl Publisher { } else { let base = iri_input(&self.curie_helper, "Object", Some(&base), value.as_str()) .on_shift_click(Message::NavigateToObject(key)); - if self.ontology.is_read_only(triple.as_ref()) { + if state.read_only { base.into() } else { base.align_x(value_alignment) @@ -751,13 +798,13 @@ impl Publisher { fn view_new_entity_buttons( &self, - entities: impl IntoIterator, + entities: impl IntoIterator, ) -> Element<'_, Message> { - let buttons = entities.into_iter().map(|entity| { + /*let buttons = entities.into_iter().map(|entity| { let abbreviation = self .curie_helper - .abbreviate(None, entity.iri.as_str()) - .unwrap_or_else(|| entity.iri.as_str().to_string()); + .abbreviate(None, entity.label.as_str()) + .unwrap_or_else(|| entity.as_str().to_string()); let label = format!("{} ({})", entity.label, abbreviation); button(text(label)) @@ -767,7 +814,8 @@ impl Publisher { grid(buttons) .height(Sizing::EvenlyDistribute(Length::Shrink)) - .into() + .into()*/ + text("to do").into() } pub(crate) fn view(&self, window: window::Id) -> Element<'_, Message> { @@ -778,44 +826,27 @@ impl Publisher { .on_input(Message::QueryUpdated) .id("query"); - let entities = Vec::new(); /*self.ontology - .searchable_classes(&*language::ENGLISH_OR_UNTAGGED) - .into_iter() - .collect::>();*/ - let type_selector = pick_list( - search_state.entity_selection.as_ref(), - entities, + search_state.entity_class_selection.as_ref(), + Class::ALL, ToString::to_string, - ) - .on_select(|selection| Message::QueryTypeUpdated(selection)); + ).on_select(|selection| Message::UpdateQueryClass(selection)); - let mut columns = vec![table::column(text("CURIE"), |_| { - /*let iri = document - .get_first(Schema::iri_field()) + let mut columns = vec![table::column(text("CURIE"), |document: &HashMap| { + let iri = document.get(&Schema::iri_field()) .and_then(|value| value.as_str()) .unwrap_or_default(); - let abbreviated_iri = self - .curie_helper + let abbreviated_iri = self.curie_helper .abbreviate(None, iri) .unwrap_or_else(|| iri.to_string()); button(text(abbreviated_iri).wrapping(Wrapping::Word)) .on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri))) - .style(button::text)*/ - text("") + .style(button::text) })]; - let fields = if let Some(selection) = search_state.entity_selection.as_ref() { - self.ontology - .fields_for_class(&selection.iri, &*language::ENGLISH_OR_UNTAGGED) - .expect("Unable to load fields for class") - } else { - vec![] - }; - - /*for field in fields.into_iter() { + /*for field in { let field_name = field.name.clone(); columns.push( table::column(text(field.label), move |document: &SearchDocument| { @@ -847,9 +878,7 @@ impl Publisher { } let back_button = button("\u{1f870}").on_press(Message::NavigateBack); - let forward_button = button("\u{1f872}").on_press(Message::NavigateForward); - let add_row_button = button("Add row").on_press(Message::AddRow(None)); let address_input = iri_input(&self.curie_helper, "URL", None, &self.url_input) @@ -861,28 +890,29 @@ impl Publisher { )); let mut rows: Vec> = vec![]; - rows = self - .document + rows = self.document .dataset() .iter_both() .filter(|(_, _, state)| self.show_read_only || !state.read_only) .map(|(key, quad, state)| self.view_row(key, quad, state)) .collect(); - let body: Element = if self.show_new_document_buttons { + let body: Element = /*if self.show_new_document_buttons { let subclasses = Vec::new(); // TODO self.ontology.subclasses_of(vocab::rda::ENTITY.into_owned()); let labeled_subclasses = subclasses .iter() .map(|iri| self.ontology.info(iri, &*language::ENGLISH_OR_UNTAGGED)); column![self.view_new_entity_buttons(labeled_subclasses)].into() - } else { - column(rows).into() - }; + } else {*/ + column(rows).into(); + //}; - let inference_table = if self.show_inferred_properties { + let inference_table = if self.show_inferred_triples { let subject_column = table::column("Subject", |triple: TripleRef| { let curie = if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject { - self.curie_helper.abbreviate(None, subject.as_str()) + Some(self.curie_helper + .abbreviate(None, subject.as_str()) + .unwrap_or_else(|| subject.as_str().to_string())) } else { None }; @@ -891,18 +921,19 @@ impl Publisher { }); let predicate_column = table::column("Predicate", |triple: TripleRef| { - let predicate_info = self.ontology.info( - &triple.predicate.into_owned(), - &*language::ENGLISH_OR_UNTAGGED, - ); - let label = if predicate_info.label.is_empty() { - self.curie_helper - .abbreviate(None, triple.predicate.as_str()) - .unwrap_or(triple.predicate.as_str().to_string()) + let curie = self.curie_helper + .abbreviate(None, triple.predicate.as_str()) + .unwrap_or_else(|| triple.predicate.as_str().to_string()); + + let label = self.resource_descriptions + .get(&triple.predicate.into_owned()) + .and_then(|description| description.label.clone()); + + if let Some(label) = label { + text(format!("{curie} ({label})")) } else { - predicate_info.label - }; - text(label) + text(curie) + } }); let object_column = table::column("Object", |triple: TripleRef| { @@ -923,21 +954,15 @@ impl Publisher { save_button_base }; - let inferred_type_toggle = - toggler(self.show_inferred_types).on_toggle(Message::SetInferredTypes); + let inferred_triples_toggle = + toggler(self.show_inferred_triples).on_toggle(Message::SetShowInferredTriples); - let inferred_property_toggle = - toggler(self.show_inferred_properties).on_toggle(Message::SetInferredProperties); - - let read_only_toggle = toggler(self.show_read_only).on_toggle(Message::SetReadOnly); + let read_only_toggle = toggler(self.show_read_only).on_toggle(Message::SetShowReadOnly); let footer = row![ space::horizontal(), - text("Show Inferred Types"), - inferred_type_toggle, - space::horizontal(), - text("Show Inferred Properties"), - inferred_property_toggle, + text("Show Inferred Triples"), + inferred_triples_toggle, space::horizontal(), text("Show Read Only Triples"), read_only_toggle diff --git a/publish/src/args.rs b/publish/src/args.rs index b2d6d31..3935865 100644 --- a/publish/src/args.rs +++ b/publish/src/args.rs @@ -7,10 +7,13 @@ pub(crate) struct QueryArgs { pub(crate) dataset_path: Option, #[arg(short, long, value_name = "QUERY PATH")] - pub(crate) query_path: PathBuf, + pub(crate) query_path: Option, #[arg(short, long, value_name = "BASE IRI")] pub(crate) base: Option, + + #[arg(short, long)] + pub(crate) inferences_only: bool, } #[derive(Args)] @@ -18,6 +21,9 @@ pub(crate) struct SearchArgs { #[arg(short, long, value_name = "DOC TYPE")] pub(crate) discriminant: Option, + #[arg(short, long, value_name = "LIMIT")] + pub(crate) limit: Option, + #[arg(value_name = "QUERY")] pub(crate) query: String, } diff --git a/publish/src/main.rs b/publish/src/main.rs index 573aa23..b9d05c8 100644 --- a/publish/src/main.rs +++ b/publish/src/main.rs @@ -5,7 +5,7 @@ mod navigator; mod rdf; mod theme; mod widget; -mod windows; +mod tasks; use std::collections::HashMap; use crate::app::Publisher; @@ -42,25 +42,23 @@ fn main() -> color_eyre::Result<()> { .init(); color_eyre::install()?; - let mut index = SearchIndex::builder() - .with_path("/home/alex/.local/share/org.graphofliberty.desktop/index") - .build() - .expect("Failed to build search index"); - let args = AppArgs::parse(); match args.command { Some(Command::Query(args)) => { - let raw_query = String::from_utf8(std::fs::read(&args.query_path)?)?; + let raw_query = if let Some(query) = &args.query_path { + Some(String::from_utf8(std::fs::read(query)?)?) + } else { None }; let graph = if let Some(dataset_path) = &args.dataset_path { Some(String::from_utf8(std::fs::read(dataset_path)?)?) } else { None }; let mut request = OntologyQueryRequest::default(); - request.sparql_query = Some(raw_query); + request.sparql_query = raw_query; request.turtle = graph; request.prefixes = HashMap::from_iter(gl_graph::PREFIXES.iter().map(|(name, iri)| (name.clone(), iri.clone()))); request.base = args.base.clone(); + request.inferences_only = args.inferences_only; let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -74,11 +72,22 @@ fn main() -> color_eyre::Result<()> { }); } Some(Command::Search(args)) => { - for document in index.query(args.discriminant, &args.query, Schema::all_fields(), 5000)? { + let mut index = SearchIndex::builder() + .with_path("/home/alex/.local/share/org.graphofliberty.desktop/index") + .build() + .expect("Failed to build search index"); + + let limit = args.limit.unwrap_or(5); + for document in index.query(args.discriminant, &args.query, Schema::all_fields(), limit)? { println!("{}", gl_search::to_json(document)); } } Some(Command::Reindex) => { + let mut index = SearchIndex::builder() + .with_path("/home/alex/.local/share/org.graphofliberty.desktop/index") + .build() + .expect("Failed to build search index"); + let mut writer = index.writer()?; debug_span!("Clear Index").in_scope(|| { writer.delete_all_documents()?; @@ -147,6 +156,7 @@ fn main() -> color_eyre::Result<()> { request.turtle = Some(turtle); request.prefixes = HashMap::new(); request.base = None; + request.inferences_only = false; let response = client.query(request).await?; let graph_with_inferences = RdfParser::from_format(RdfFormat::Turtle) diff --git a/publish/src/navigator.rs b/publish/src/navigator.rs index bfc94cd..b6af0a7 100644 --- a/publish/src/navigator.rs +++ b/publish/src/navigator.rs @@ -33,4 +33,4 @@ impl Navigator { } self.current() } -} +} \ No newline at end of file diff --git a/publish/src/rdf/mod.rs b/publish/src/rdf/mod.rs index 0ff046c..95ba451 100644 --- a/publish/src/rdf/mod.rs +++ b/publish/src/rdf/mod.rs @@ -1,3 +1,3 @@ pub(crate) mod conversion; -pub(crate) mod ontology; +//pub(crate) mod ontology; pub(crate) mod term_helper; diff --git a/publish/src/tasks.rs b/publish/src/tasks.rs new file mode 100644 index 0000000..ab7e89f --- /dev/null +++ b/publish/src/tasks.rs @@ -0,0 +1,52 @@ +use iced::Task; +use oxigraph::model::{Graph, NamedNode}; +use gl_graph::language; +use gl_graph::ontology::Ontology; +use crate::app::Message; + +pub(crate) fn connect_to_ontology_service(endpoint: String) -> Task { + Task::future(Ontology::new(endpoint, (&*language::ENGLISH_OR_UNTAGGED).clone())) + .then(|result| { + match result { + Ok(ontology) => Task::done(Message::ConnectedToOntologyService(ontology)), + Err(err) => Task::done(Message::ShowError(err.to_string())) + } + }) +} + +pub(crate) fn lookup_resource_description(mut ontology: Ontology, subject: NamedNode) -> Task { + let subject_clone = subject.clone(); + Task::perform(async move { ontology.resource_description(subject_clone).await }, |result| { + match result { + Ok(description) => Message::CacheResourceDescription(subject, description), + Err(err) => Message::ShowError(err.to_string()), + } + }) +} + +pub(crate) fn list_datatypes(mut ontology: Ontology) -> Task { + Task::perform(async move { ontology.datatypes().await }, |result| { + match result { + Ok(datatypes) => Message::CacheDatatypes(datatypes), + Err(err) => Message::ShowError(err.to_string()), + } + }) +} + +pub(crate) fn list_read_only_entities(mut ontology: Ontology) -> Task { + Task::perform(async move { ontology.list_read_only().await }, |result| { + match result { + Ok(entities) => Message::CacheReadOnlyEntities(entities), + Err(err) => Message::ShowError(err.to_string()), + } + }) +} + +pub(crate) fn run_inference(mut ontology: Ontology, graph: Graph) -> Task { + Task::perform(async move { ontology.run_inference(&graph).await }, |result| { + match result { + Ok(inferences) => Message::SetInferredTriples(inferences), + Err(err) => Message::ShowError(err.to_string()), + } + }) +} \ No newline at end of file diff --git a/publish/src/windows/mod.rs b/publish/src/windows/mod.rs deleted file mode 100644 index 4c0d52d..0000000 --- a/publish/src/windows/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/publish/src/windows/search.rs b/publish/src/windows/search.rs deleted file mode 100644 index 473a0f4..0000000