From fb9d86a57d6ea2b71e112a65d1403f9358081310 Mon Sep 17 00:00:00 2001 From: Brian Lowe Date: Thu, 15 Jun 2023 13:55:23 +0300 Subject: [PATCH] Use same logic to remove blank nodes from reasoner's internal model as used in RDFService (#397) * Refactor blank-nodes-as-variables behavior from RDFServiceJena to JenaModelUtils; use this logic when removing triples from the TBox reasoner's internal assertions model. Add unit test. * Remove complication of datasets and remove blank nodes directly from models * Remove temporary debugging logging * Simplify original blank node tree logic * Remove unneeded imports --- .../vitro/webapp/dao/jena/JenaModelUtils.java | 195 +++++++++++++- .../rdfservice/impl/RDFServiceImpl.java | 4 +- .../rdfservice/impl/jena/RDFServiceJena.java | 239 +----------------- .../impl/jena/model/RDFServiceModel.java | 2 +- .../impl/sparql/RDFServiceSparql.java | 6 +- .../impl/jfact/JFactTBoxReasoner.java | 29 ++- .../impl/jfact/JFactTBoxReasonerTest.java | 102 ++++++++ 7 files changed, 326 insertions(+), 251 deletions(-) create mode 100644 api/src/test/java/edu/cornell/mannlib/vitro/webapp/tboxreasoner/impl/jfact/JFactTBoxReasonerTest.java diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaModelUtils.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaModelUtils.java index 78dfc104f..1ffb21ae8 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaModelUtils.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaModelUtils.java @@ -2,15 +2,17 @@ package edu.cornell.mannlib.vitro.webapp.dao.jena; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - +import org.apache.jena.graph.Triple; import org.apache.jena.ontology.Individual; import org.apache.jena.ontology.OntClass; import org.apache.jena.ontology.OntModel; @@ -21,12 +23,16 @@ import org.apache.jena.query.Query; import org.apache.jena.query.QueryExecution; import org.apache.jena.query.QueryExecutionFactory; import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.QuerySolution; +import org.apache.jena.query.ResultSet; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.Property; import org.apache.jena.rdf.model.RDFNode; import org.apache.jena.rdf.model.Resource; import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.rdf.model.Statement; +import org.apache.jena.rdf.model.StmtIterator; import org.apache.jena.shared.Lock; import org.apache.jena.vocabulary.OWL; import org.apache.jena.vocabulary.RDF; @@ -39,6 +45,8 @@ import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactoryConfig; public class JenaModelUtils { + public static final String BNODE_ROOT_QUERY = + "SELECT DISTINCT ?s WHERE { ?s ?p ?o OPTIONAL { ?ss ?pp ?s } FILTER (!isBlank(?s) || !bound(?ss)) }"; private static final Log log = LogFactory.getLog(JenaModelUtils.class.getName()); private static final Set nonIndividualTypeURIs ; @@ -398,5 +406,190 @@ public class JenaModelUtils { return aboxModel; } + + /** + * Remove statements from a model by separating statements + * containing blank nodes from those that have no blank nodes. + * The blank node statements are removed by treating blank nodes as variables and + * constructing the matching subgraphs for deletion. + * The other statements are removed normally. + * @param toRemove containing statements to be removed + * @param removeFrom from which statements should be removed + */ + public static void removeWithBlankNodesAsVariables(Model toRemove, Model removeFrom) { + List blankNodeStatements = new ArrayList(); + List nonBlankNodeStatements = new ArrayList(); + StmtIterator stmtIt = toRemove.listStatements(); + while (stmtIt.hasNext()) { + Statement stmt = stmtIt.nextStatement(); + if (stmt.getSubject().isAnon() || stmt.getObject().isAnon()) { + blankNodeStatements.add(stmt); + } else { + nonBlankNodeStatements.add(stmt); + } + } + if(!blankNodeStatements.isEmpty()) { + Model blankNodeModel = ModelFactory.createDefaultModel(); + blankNodeModel.add(blankNodeStatements); + removeBlankNodesUsingSparqlConstruct(blankNodeModel, removeFrom); + } + if(!nonBlankNodeStatements.isEmpty()) { + try { + removeFrom.enterCriticalSection(Lock.WRITE); + removeFrom.remove(nonBlankNodeStatements); + } finally { + removeFrom.leaveCriticalSection(); + } + } + } + + private static void removeBlankNodesUsingSparqlConstruct(Model blankNodeModel, + Model removeFrom) { + log.debug("blank node model size " + blankNodeModel.size()); + if (blankNodeModel.size() == 1) { + log.debug("Deleting single triple with blank node: " + blankNodeModel); + log.debug("This could result in the deletion of multiple triples" + + " if multiple blank nodes match the same triple pattern."); + } + Query rootFinderQuery = QueryFactory.create(BNODE_ROOT_QUERY); + QueryExecution qe = QueryExecutionFactory.create(rootFinderQuery, blankNodeModel); + try { + ResultSet rs = qe.execSelect(); + while (rs.hasNext()) { + QuerySolution qs = rs.next(); + Resource s = qs.getResource("s"); + String treeFinder = makeDescribe(s); + Query treeFinderQuery = QueryFactory.create(treeFinder); + QueryExecution qee = QueryExecutionFactory.create(treeFinderQuery, blankNodeModel); + try { + Model tree = qee.execDescribe(); + JenaModelUtils.removeUsingSparqlConstruct(tree, removeFrom); + } finally { + qee.close(); + } + } + } finally { + qe.close(); + } + } + + private static String makeDescribe(Resource s) { + StringBuilder query = new StringBuilder("DESCRIBE <") ; + if (s.isAnon()) { + query.append("_:").append(s.getId().toString()); + } else { + query.append(s.getURI()); + } + query.append(">"); + return query.toString(); + } + + private static final boolean WHERE_CLAUSE = true; + + /** + * Remove statements from a model by first constructing + * the statements to be removed with a SPARQL query that treats + * each blank node ID as a variable. + * This allows matching blank node structures to be removed even though + * the internal blank node IDs are different. + * @param toRemove containing statements to be removed + * @param removeFrom from which statements should be removed + */ + public static void removeUsingSparqlConstruct(Model toRemove, Model removeFrom) { + if(toRemove.isEmpty()) { + return; + } + List stmts = toRemove.listStatements().toList(); + stmts = sort(stmts); + StringBuffer queryBuff = new StringBuffer(); + queryBuff.append("CONSTRUCT { \n"); + addStatementPatterns(stmts, queryBuff, !WHERE_CLAUSE); + queryBuff.append("} WHERE { \n"); + addStatementPatterns(stmts, queryBuff, WHERE_CLAUSE); + queryBuff.append("} \n"); + String queryStr = queryBuff.toString(); + log.debug(queryBuff.toString()); + Query construct = QueryFactory.create(queryStr); + QueryExecution qe = QueryExecutionFactory.create(construct, removeFrom); + try { + Model constructedRemovals = qe.execConstruct(); + try { + removeFrom.enterCriticalSection(Lock.WRITE); + removeFrom.remove(constructedRemovals); + } finally { + removeFrom.leaveCriticalSection(); + } + } finally { + qe.close(); + } + } + + private static List sort(List stmts) { + List output = new ArrayList(); + int originalSize = stmts.size(); + if(originalSize == 1) { + return stmts; + } + List remaining = stmts; + ConcurrentLinkedQueue subjQueue = new ConcurrentLinkedQueue(); + for(Statement stmt : remaining) { + if(stmt.getSubject().isURIResource()) { + subjQueue.add(stmt.getSubject()); + break; + } + } + if (subjQueue.isEmpty()) { + log.warn("No named subject in statement patterns"); + return stmts; + } + while(remaining.size() > 0) { + if(subjQueue.isEmpty()) { + subjQueue.add(remaining.get(0).getSubject()); + } + while(!subjQueue.isEmpty()) { + Resource subj = subjQueue.poll(); + List temp = new ArrayList(); + for (Statement stmt : remaining) { + if(stmt.getSubject().equals(subj)) { + output.add(stmt); + if (stmt.getObject().isResource()) { + subjQueue.add((Resource) stmt.getObject()); + } + } else { + temp.add(stmt); + } + } + remaining = temp; + } + } + if(output.size() != originalSize) { + throw new RuntimeException("original list size was " + originalSize + + " but sorted size is " + output.size()); + } + return output; + } + + private static void addStatementPatterns(List stmts, + StringBuffer patternBuff, boolean whereClause) { + for(Statement stmt : stmts) { + Triple t = stmt.asTriple(); + patternBuff.append(SparqlGraph.sparqlNodeDelete(t.getSubject(), null)); + patternBuff.append(" "); + patternBuff.append(SparqlGraph.sparqlNodeDelete(t.getPredicate(), null)); + patternBuff.append(" "); + patternBuff.append(SparqlGraph.sparqlNodeDelete(t.getObject(), null)); + patternBuff.append(" .\n"); + if (whereClause) { + if (t.getSubject().isBlank()) { + patternBuff.append(" FILTER(isBlank(").append( + SparqlGraph.sparqlNodeDelete(t.getSubject(), null)).append(")) \n"); + } + if (t.getObject().isBlank()) { + patternBuff.append(" FILTER(isBlank(").append( + SparqlGraph.sparqlNodeDelete(t.getObject(), null)).append(")) \n"); + } + } + } + } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/RDFServiceImpl.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/RDFServiceImpl.java index 82c3ec1db..92c7329ab 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/RDFServiceImpl.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/RDFServiceImpl.java @@ -45,9 +45,7 @@ import edu.cornell.mannlib.vitro.webapp.utils.logging.ToString; public abstract class RDFServiceImpl implements RDFService { private static final Log log = LogFactory.getLog(RDFServiceImpl.class); - protected static final String BNODE_ROOT_QUERY = - "SELECT DISTINCT ?s WHERE { ?s ?p ?o OPTIONAL { ?ss ?pp ?s } FILTER (!isBlank(?s) || !bound(?ss)) }"; - + protected String defaultWriteGraphURI; protected List registeredListeners = new CopyOnWriteArrayList(); protected List registeredJenaListeners = new CopyOnWriteArrayList(); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/jena/RDFServiceJena.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/jena/RDFServiceJena.java index 09a98a647..de5171113 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/jena/RDFServiceJena.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/jena/RDFServiceJena.java @@ -10,46 +10,40 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import java.util.concurrent.ConcurrentLinkedQueue; -import edu.cornell.mannlib.vitro.webapp.rdfservice.ResultSetConsumer; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.apache.jena.query.QuerySolutionMap; -import org.apache.jena.query.Syntax; -import org.apache.jena.rdf.model.Literal; -import org.apache.jena.riot.RDFDataMgr; -import org.apache.log4j.lf5.util.StreamUtils; - import org.apache.jena.graph.Triple; import org.apache.jena.query.Dataset; -import org.apache.jena.query.DatasetFactory; import org.apache.jena.query.Query; import org.apache.jena.query.QueryExecution; import org.apache.jena.query.QueryExecutionFactory; import org.apache.jena.query.QueryFactory; import org.apache.jena.query.QuerySolution; +import org.apache.jena.query.QuerySolutionMap; import org.apache.jena.query.ResultSet; import org.apache.jena.query.ResultSetFormatter; +import org.apache.jena.query.Syntax; +import org.apache.jena.rdf.model.Literal; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.RDFNode; -import org.apache.jena.rdf.model.Resource; -import org.apache.jena.rdf.model.Statement; -import org.apache.jena.rdf.model.StmtIterator; +import org.apache.jena.riot.RDFDataMgr; import org.apache.jena.sdb.SDB; import org.apache.jena.shared.Lock; import org.apache.jena.sparql.core.Quad; +import org.apache.log4j.lf5.util.StreamUtils; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.dao.jena.DatasetWrapper; +import edu.cornell.mannlib.vitro.webapp.dao.jena.JenaModelUtils; import edu.cornell.mannlib.vitro.webapp.dao.jena.RDFServiceDataset; -import edu.cornell.mannlib.vitro.webapp.dao.jena.SparqlGraph; import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeSet; import edu.cornell.mannlib.vitro.webapp.rdfservice.ModelChange; import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ResultSetConsumer; import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceImpl; import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils; import edu.cornell.mannlib.vitro.webapp.utils.logging.ToString; @@ -91,7 +85,7 @@ public abstract class RDFServiceJena extends RDFServiceImpl implements RDFServic Model model = (modelChange.getGraphURI() == null) ? dataset.getDefaultModel() : dataset.getNamedModel(modelChange.getGraphURI()); - operateOnModel(model, modelChange, dataset); + operateOnModel(model, modelChange); } finally { dataset.getLock().leaveCriticalSection(); } @@ -104,7 +98,7 @@ public abstract class RDFServiceJena extends RDFServiceImpl implements RDFServic } } - protected void operateOnModel(Model model, ModelChange modelChange, Dataset dataset) { + protected void operateOnModel(Model model, ModelChange modelChange) { model.enterCriticalSection(Lock.WRITE); try { if (log.isDebugEnabled()) { @@ -115,10 +109,7 @@ public abstract class RDFServiceJena extends RDFServiceImpl implements RDFServic model.add(addition); } else if (modelChange.getOperation() == ModelChange.Operation.REMOVE) { Model removal = parseModel(modelChange); - model.remove(removal); - if (dataset != null) { - removeBlankNodesWithSparqlUpdate(dataset, removal, modelChange.getGraphURI()); - } + JenaModelUtils.removeWithBlankNodesAsVariables(removal, model); } else { log.error("unrecognized operation type"); } @@ -179,216 +170,6 @@ public abstract class RDFServiceJena extends RDFServiceImpl implements RDFServic ToString.modelToString(model))); } - private void removeBlankNodesWithSparqlUpdate(Dataset dataset, Model model, String graphURI) { - - List blankNodeStatements = new ArrayList(); - StmtIterator stmtIt = model.listStatements(); - while (stmtIt.hasNext()) { - Statement stmt = stmtIt.nextStatement(); - if (stmt.getSubject().isAnon() || stmt.getObject().isAnon()) { - blankNodeStatements.add(stmt); - } - } - - if(blankNodeStatements.size() == 0) { - return; - } - - Model blankNodeModel = ModelFactory.createDefaultModel(); - blankNodeModel.add(blankNodeStatements); - - log.debug("removal model size " + model.size()); - log.debug("blank node model size " + blankNodeModel.size()); - - if (blankNodeModel.size() == 1) { - log.debug("Deleting single triple with blank node: " + blankNodeModel); - log.debug("This could result in the deletion of multiple triples if multiple blank nodes match the same triple pattern."); - } - - Query rootFinderQuery = QueryFactory.create(BNODE_ROOT_QUERY); - QueryExecution qe = QueryExecutionFactory.create(rootFinderQuery, blankNodeModel); - try { - ResultSet rs = qe.execSelect(); - if (!rs.hasNext()) { - log.warn("No rooted blank node trees; deletion is not possible."); - } - while (rs.hasNext()) { - QuerySolution qs = rs.next(); - Resource s = qs.getResource("s"); - String treeFinder = makeDescribe(s); - Query treeFinderQuery = QueryFactory.create(treeFinder); - QueryExecution qee = QueryExecutionFactory.create(treeFinderQuery, blankNodeModel); - try { - Model tree = qee.execDescribe(); - Dataset ds = DatasetFactory.createMem(); - if (graphURI == null) { - ds.setDefaultModel(dataset.getDefaultModel()); - } else { - ds.addNamedModel(graphURI, dataset.getNamedModel(graphURI)); - } - if (s.isAnon()) { - removeUsingSparqlUpdate(ds, tree, graphURI); - } else { - StmtIterator sit = tree.listStatements(s, null, (RDFNode) null); - while (sit.hasNext()) { - Statement stmt = sit.nextStatement(); - RDFNode n = stmt.getObject(); - Model m2 = ModelFactory.createDefaultModel(); - if (n.isResource()) { - Resource s2 = (Resource) n; - // now run yet another describe query - String smallerTree = makeDescribe(s2); - log.debug(smallerTree); - Query smallerTreeQuery = QueryFactory.create(smallerTree); - QueryExecution qe3 = QueryExecutionFactory.create( - smallerTreeQuery, tree); - try { - qe3.execDescribe(m2); - } finally { - qe3.close(); - } - } - m2.add(stmt); - removeUsingSparqlUpdate(ds, m2, graphURI); - } - } - } finally { - qee.close(); - } - } - } finally { - qe.close(); - } - } - - private String makeDescribe(Resource s) { - StringBuilder query = new StringBuilder("DESCRIBE <") ; - if (s.isAnon()) { - query.append("_:").append(s.getId().toString()); - } else { - query.append(s.getURI()); - } - query.append(">"); - return query.toString(); - } - - private void removeUsingSparqlUpdate(Dataset dataset, Model model, String graphURI) { - StmtIterator stmtIt = model.listStatements(); - - if (!stmtIt.hasNext()) { - stmtIt.close(); - return; - } - - StringBuffer queryBuff = new StringBuffer(); - queryBuff.append("CONSTRUCT { \n"); - List stmts = stmtIt.toList(); - stmts = sort(stmts); - addStatementPatterns(stmts, queryBuff, !WHERE_CLAUSE); - queryBuff.append("} WHERE { \n"); - if (graphURI != null) { - queryBuff.append(" GRAPH <").append(graphURI).append("> { \n"); - } - stmtIt = model.listStatements(); - stmts = stmtIt.toList(); - stmts = sort(stmts); - addStatementPatterns(stmts, queryBuff, WHERE_CLAUSE); - if (graphURI != null) { - queryBuff.append(" } \n"); - } - queryBuff.append("} \n"); - - log.debug(queryBuff.toString()); - - Query construct = QueryFactory.create(queryBuff.toString()); - // make a plain dataset to force the query to be run in a way that - // won't overwhelm MySQL with too many joins - Dataset ds = DatasetFactory.createMem(); - if (graphURI == null) { - ds.setDefaultModel(dataset.getDefaultModel()); - } else { - ds.addNamedModel(graphURI, dataset.getNamedModel(graphURI)); - } - QueryExecution qe = QueryExecutionFactory.create(construct, ds); - try { - Model m = qe.execConstruct(); - if (graphURI != null) { - dataset.getNamedModel(graphURI).remove(m); - } else { - dataset.getDefaultModel().remove(m); - } - } finally { - qe.close(); - } - } - - private List sort(List stmts) { - List output = new ArrayList(); - int originalSize = stmts.size(); - if(originalSize == 1) { - return stmts; - } - List remaining = stmts; - ConcurrentLinkedQueue subjQueue = new ConcurrentLinkedQueue(); - for(Statement stmt : remaining) { - if(stmt.getSubject().isURIResource()) { - subjQueue.add(stmt.getSubject()); - break; - } - } - if (subjQueue.isEmpty()) { - log.warn("No named subject in statement patterns"); - return stmts; - } - while(remaining.size() > 0) { - if(subjQueue.isEmpty()) { - subjQueue.add(remaining.get(0).getSubject()); - } - while(!subjQueue.isEmpty()) { - Resource subj = subjQueue.poll(); - List temp = new ArrayList(); - for (Statement stmt : remaining) { - if(stmt.getSubject().equals(subj)) { - output.add(stmt); - if (stmt.getObject().isResource()) { - subjQueue.add((Resource) stmt.getObject()); - } - } else { - temp.add(stmt); - } - } - remaining = temp; - } - } - if(output.size() != originalSize) { - throw new RuntimeException("original list size was " + originalSize + - " but sorted size is " + output.size()); - } - return output; - } - - private static final boolean WHERE_CLAUSE = true; - - private void addStatementPatterns(List stmts, StringBuffer patternBuff, boolean whereClause) { - for(Statement stmt : stmts) { - Triple t = stmt.asTriple(); - patternBuff.append(SparqlGraph.sparqlNodeDelete(t.getSubject(), null)); - patternBuff.append(" "); - patternBuff.append(SparqlGraph.sparqlNodeDelete(t.getPredicate(), null)); - patternBuff.append(" "); - patternBuff.append(SparqlGraph.sparqlNodeDelete(t.getObject(), null)); - patternBuff.append(" .\n"); - if (whereClause) { - if (t.getSubject().isBlank()) { - patternBuff.append(" FILTER(isBlank(").append(SparqlGraph.sparqlNodeDelete(t.getSubject(), null)).append(")) \n"); - } - if (t.getObject().isBlank()) { - patternBuff.append(" FILTER(isBlank(").append(SparqlGraph.sparqlNodeDelete(t.getObject(), null)).append(")) \n"); - } - } - } - } - private Model parseModel(ModelChange modelChange) { Model model = ModelFactory.createDefaultModel(); model.read(modelChange.getSerializedModel(), null, diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/jena/model/RDFServiceModel.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/jena/model/RDFServiceModel.java index 4b910b568..9bbc71453 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/jena/model/RDFServiceModel.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/jena/model/RDFServiceModel.java @@ -95,7 +95,7 @@ public class RDFServiceModel extends RDFServiceJena implements RDFService { m = dataset.getDefaultModel(); } } - operateOnModel(m, modelChange, null); + operateOnModel(m, modelChange); } // notify listeners of triple changes diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/sparql/RDFServiceSparql.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/sparql/RDFServiceSparql.java index 42aefb920..c3fec0c1b 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/sparql/RDFServiceSparql.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/sparql/RDFServiceSparql.java @@ -38,8 +38,6 @@ import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; -import org.apache.jena.riot.RDFDataMgr; - import org.apache.jena.graph.Triple; import org.apache.jena.query.Query; import org.apache.jena.query.QueryExecution; @@ -55,8 +53,10 @@ import org.apache.jena.rdf.model.RDFNode; import org.apache.jena.rdf.model.Resource; import org.apache.jena.rdf.model.Statement; import org.apache.jena.rdf.model.StmtIterator; +import org.apache.jena.riot.RDFDataMgr; import org.apache.jena.sparql.core.Quad; +import edu.cornell.mannlib.vitro.webapp.dao.jena.JenaModelUtils; import edu.cornell.mannlib.vitro.webapp.dao.jena.RDFServiceDataset; import edu.cornell.mannlib.vitro.webapp.dao.jena.SparqlGraph; import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeListener; @@ -691,7 +691,7 @@ public class RDFServiceSparql extends RDFServiceImpl implements RDFService { log.warn("This likely indicates a problem; excessive data may be deleted."); } - Query rootFinderQuery = QueryFactory.create(BNODE_ROOT_QUERY); + Query rootFinderQuery = QueryFactory.create(JenaModelUtils.BNODE_ROOT_QUERY); QueryExecution qe = QueryExecutionFactory.create(rootFinderQuery, blankNodeModel); try { ResultSet rs = qe.execSelect(); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/tboxreasoner/impl/jfact/JFactTBoxReasoner.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/tboxreasoner/impl/jfact/JFactTBoxReasoner.java index 617f6829b..3f3bea92a 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/tboxreasoner/impl/jfact/JFactTBoxReasoner.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/tboxreasoner/impl/jfact/JFactTBoxReasoner.java @@ -2,6 +2,8 @@ package edu.cornell.mannlib.vitro.webapp.tboxreasoner.impl.jfact; +import static org.semanticweb.owlapi.vocab.OWLRDFVocabulary.OWL_AXIOM; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.ArrayList; @@ -9,17 +11,6 @@ import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.semanticweb.owlapi.apibinding.OWLManager; -import org.semanticweb.owlapi.model.OWLOntology; -import org.semanticweb.owlapi.model.OWLOntologyCreationException; -import org.semanticweb.owlapi.reasoner.InferenceType; -import org.semanticweb.owlapi.reasoner.OWLReasoner; -import org.semanticweb.owlapi.reasoner.OWLReasonerConfiguration; -import org.semanticweb.owlapi.reasoner.OWLReasonerFactory; -import org.semanticweb.owlapi.reasoner.SimpleConfiguration; - -import uk.ac.manchester.cs.jfact.JFactFactory; - import org.apache.jena.ontology.DatatypeProperty; import org.apache.jena.ontology.ObjectProperty; import org.apache.jena.ontology.OntModel; @@ -32,13 +23,21 @@ import org.apache.jena.rdf.model.ResourceFactory; import org.apache.jena.rdf.model.Statement; import org.apache.jena.rdf.model.StmtIterator; import org.apache.jena.vocabulary.RDF; +import org.semanticweb.owlapi.apibinding.OWLManager; +import org.semanticweb.owlapi.model.OWLOntology; +import org.semanticweb.owlapi.model.OWLOntologyCreationException; +import org.semanticweb.owlapi.reasoner.InferenceType; +import org.semanticweb.owlapi.reasoner.OWLReasoner; +import org.semanticweb.owlapi.reasoner.OWLReasonerConfiguration; +import org.semanticweb.owlapi.reasoner.OWLReasonerFactory; +import org.semanticweb.owlapi.reasoner.SimpleConfiguration; +import edu.cornell.mannlib.vitro.webapp.dao.jena.JenaModelUtils; import edu.cornell.mannlib.vitro.webapp.tboxreasoner.ReasonerStatementPattern; import edu.cornell.mannlib.vitro.webapp.tboxreasoner.TBoxChanges; import edu.cornell.mannlib.vitro.webapp.tboxreasoner.TBoxReasoner; import edu.cornell.mannlib.vitro.webapp.tboxreasoner.impl.TBoxInferencesAccumulator; - -import static org.semanticweb.owlapi.vocab.OWLRDFVocabulary.OWL_AXIOM; +import uk.ac.manchester.cs.jfact.JFactFactory; /** * An implementation of the JFact reasoner for the TBox. @@ -81,7 +80,9 @@ public class JFactTBoxReasoner implements log.debug("Adding " + changes.getAddedStatements().size() + ", removing " + changes.getRemovedStatements().size()); filteredAssertionsModel.add(changes.getAddedStatements()); - filteredAssertionsModel.remove(changes.getRemovedStatements()); + Model removals = ModelFactory.createDefaultModel(); + removals.add(changes.getRemovedStatements()); + JenaModelUtils.removeWithBlankNodesAsVariables(removals, filteredAssertionsModel); clearEmptyAxiomStatements(); } diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/tboxreasoner/impl/jfact/JFactTBoxReasonerTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/tboxreasoner/impl/jfact/JFactTBoxReasonerTest.java new file mode 100644 index 000000000..4ee666b8b --- /dev/null +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/tboxreasoner/impl/jfact/JFactTBoxReasonerTest.java @@ -0,0 +1,102 @@ +package edu.cornell.mannlib.vitro.webapp.tboxreasoner.impl.jfact; + +import java.io.StringReader; + +import org.apache.jena.ontology.OntModel; +import org.apache.jena.ontology.OntModelSpec; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.RDFNode; +import org.junit.Assert; +import org.junit.Test; + +import edu.cornell.mannlib.vitro.webapp.dao.jena.JenaModelUtils; +import edu.cornell.mannlib.vitro.webapp.dao.jena.event.EditEvent; +import edu.cornell.mannlib.vitro.webapp.tboxreasoner.ReasonerConfiguration; +import edu.cornell.mannlib.vitro.webapp.tboxreasoner.impl.BasicTBoxReasonerDriver; + +public class JFactTBoxReasonerTest { + + private final static String axioms = "@prefix obo: .\r\n" + + "@prefix owl: .\r\n" + + "@prefix rdf: .\r\n" + + "@prefix xml: .\r\n" + + "@prefix xsd: .\r\n" + + "@prefix rdfs: .\r\n" + + "\r\n" + + " rdf:type owl:Class ;\r\n" + + " \r\n" + + " rdfs:subClassOf [ rdf:type owl:Class ;\r\n" + + " owl:intersectionOf ( \r\n" + + " \r\n" + + " )\r\n" + + " ] .\r\n" + + "\r\n" + + "\r\n" + + " a owl:Class ;\r\n" + + " rdfs:label \"Class A\"@en-US .\r\n" + + "\r\n" + + "\r\n" + + " a owl:Class ;\r\n" + + " rdfs:label \"Class B\"@en-US .\r\n" + + "\r\n" + + "\r\n" + + " a owl:Class ;\r\n" + + " rdfs:label \"Class C\"@en-US .\r\n"; + + /** + * Test that axioms containing blank nodes can be removed from the reasoner + * even if the internal blank node IDs are different from when first added. + */ + @Test + public void testRemoveAxiomsWithBlankNodes() { + OntModel tboxAssertions = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM); + OntModel tboxInferences = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM); + OntModel tboxUnion = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, + ModelFactory.createUnion(tboxAssertions, tboxInferences)); + JFactTBoxReasoner reasoner = new JFactTBoxReasoner(); + BasicTBoxReasonerDriver driver = new BasicTBoxReasonerDriver( + tboxAssertions, tboxInferences.getBaseModel(), tboxUnion, reasoner, + ReasonerConfiguration.DEFAULT); + Model additions = ModelFactory.createDefaultModel(); + additions.read(new StringReader(axioms), null, "TTL"); + // Reading again will generate new internal blank node IDs + Model subtractions = ModelFactory.createDefaultModel(); + subtractions.read(new StringReader(axioms), null, "TTL"); + // Confirm that directly subtracting the models doesn't work because + // the blank node IDs do not match + Model incorrectSubtraction = additions.difference(subtractions); + Assert.assertFalse(incorrectSubtraction.isEmpty()); + tboxAssertions.getBaseModel().add(additions); + tboxAssertions.getBaseModel().notifyEvent(new EditEvent(null, false)); + waitForTBoxReasoning(driver); + // Confirm that union model now contains inferred triples + Assert.assertTrue(tboxUnion.size() > additions.size()); + JenaModelUtils.removeWithBlankNodesAsVariables(subtractions, tboxAssertions.getBaseModel()); + tboxAssertions.getBaseModel().notifyEvent(new EditEvent(null, false)); + waitForTBoxReasoning(driver); + // Confirm that no statements related to classes a, b or c remain in the + // TBox union model. (The inference model may not be completely empty, because + // the reasoner may supply unrelated triples related to OWL and RDFS vocabulary.) + Assert.assertFalse(tboxUnion.contains(tboxUnion.getResource( + "http://vivo.mydomain.edu/individual/class_a"), null, (RDFNode) null)); + Assert.assertFalse(tboxUnion.contains(tboxUnion.getResource( + "http://vivo.mydomain.edu/individual/class_b"), null, (RDFNode) null)); + Assert.assertFalse(tboxUnion.contains(tboxUnion.getResource( + "http://vivo.mydomain.edu/individual/class_c"), null, (RDFNode) null)); + } + + private void waitForTBoxReasoning(BasicTBoxReasonerDriver driver) { + int sleeps = 0; + // sleep at least once to make sure the TBox reasoning gets started + while ((0 == sleeps) || ((sleeps < 1000) && driver.getStatus().isReasoning())) { + try { + Thread.sleep(200); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + sleeps++; + } + } + +}