Skip to content

Commit

Permalink
[Bug #202] Modify Jena driver to remove all property assertions on it…
Browse files Browse the repository at this point in the history
…s update to prevent transaction concurrency issues.

Originally, all property values were selected and then these statements were removed on commit. However, if these changed in the meantime (e.g., when another transaction committed), it lead to stale data being in the repository.
  • Loading branch information
ledsoft committed Sep 19, 2023
1 parent 5d58e0d commit cb85679
Show file tree
Hide file tree
Showing 11 changed files with 242 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import cz.cvut.kbss.ontodriver.descriptor.AbstractAxiomDescriptor;
import cz.cvut.kbss.ontodriver.jena.connector.StorageConnector;
import cz.cvut.kbss.ontodriver.jena.connector.SubjectPredicateContext;
import cz.cvut.kbss.ontodriver.jena.util.JenaUtils;
import cz.cvut.kbss.ontodriver.model.Assertion;
import cz.cvut.kbss.ontodriver.model.NamedResource;
Expand All @@ -25,8 +26,11 @@
import org.apache.jena.rdf.model.ResourceFactory;

import java.net.URI;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
* This class performs an epistemic removal of statements.
Expand All @@ -51,14 +55,14 @@ class EpistemicAxiomRemover {
*/
void remove(AbstractAxiomDescriptor descriptor) {
final Resource subject = ResourceFactory.createResource(descriptor.getSubject().getIdentifier().toString());
final Collection<SubjectPredicateContext> toRemove = new HashSet<>();
descriptor.getAssertions().forEach(assertion -> {
final Property property = ResourceFactory.createProperty(assertion.getIdentifier().toString());
if (descriptor.getAssertionContexts(assertion).isEmpty()) {
connector.remove(subject, property, null, null);
}
descriptor.getAssertionContexts(assertion)
.forEach(context -> connector.remove(subject, property, null, context.toString()));
toRemove.add(new SubjectPredicateContext(subject, property, descriptor.getAssertionContexts(assertion)
.stream().map(URI::toString)
.collect(Collectors.toSet())));
});
connector.removeStatementsBySubjectAndPredicate(toRemove);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ private void mergeRemovedStatements() {
final Model model = removed.getNamedModel(context);
centralConnector.remove(model.listStatements().toList(), context);
});
centralConnector.removeStatementsBySubjectAndPredicate(localModel.getRemovedSubjectPredicateStatements());
}

private void mergeAddedStatements() {
Expand Down Expand Up @@ -135,6 +136,12 @@ public void remove(Resource subject, Property property, RDFNode object, String c
localModel.removeStatements(new ArrayList<>(find(subject, property, object, context != null ? Collections.singleton(context) : Collections.emptySet())), context);
}

@Override
public void removeStatementsBySubjectAndPredicate(Collection<SubjectPredicateContext> spc) {
transaction.verifyActive();
localModel.removeStatementsBySubjectAndPredicate(spc);
}

@Override
public AbstractResultSet executeSelectQuery(Query query, StatementOntology target) throws JenaDriverException {
Objects.requireNonNull(query);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
/**
* Copyright (C) 2023 Czech Technical University in Prague
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details. You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* <p>
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
* version.
* <p>
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details. You should have received a copy of the GNU General Public License along with this program. If not, see
* <http://www.gnu.org/licenses/>.
*/
package cz.cvut.kbss.ontodriver.jena.connector;

import org.apache.jena.query.Dataset;
import org.apache.jena.query.DatasetFactory;
import org.apache.jena.rdf.model.*;

import java.util.*;
import org.apache.jena.rdf.model.Model;
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.Statement;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
* Tracks transactional changes.
Expand All @@ -27,6 +36,7 @@ class LocalModel {

private final Dataset added;
private final Dataset removed;
private final Set<SubjectPredicateContext> removedSubjectPredicateStatements;

/**
* Whether default graph should be treated as union of all graphs.
Expand All @@ -40,18 +50,19 @@ enum Containment {
LocalModel(boolean defaultAsUnion) {
this.added = DatasetFactory.create();
this.removed = DatasetFactory.create();
this.removedSubjectPredicateStatements = new HashSet<>();
this.defaultAsUnion = defaultAsUnion;
}

Collection<Statement> enhanceStatements(Collection<Statement> statements, Resource subject, Property property,
RDFNode value, Collection<String> contexts) {
if (contexts.isEmpty()) {
return enhanceStatements(statements, subject, property, value, addedDefault(), removedDefault());
return enhanceStatements(statements, subject, property, value, addedDefault(), removedDefault(), null);
} else {
Collection<Statement> enhanced = statements;
for (String ctx : contexts) {
enhanced = enhanceStatements(statements, subject, property, value, added.getNamedModel(ctx),
removed.getNamedModel(ctx));
removed.getNamedModel(ctx), ctx);
}
return enhanced;
}
Expand All @@ -65,25 +76,31 @@ private Model removedDefault() {
return defaultAsUnion ? removed.getUnionModel().union(removed.getDefaultModel()) : removed.getDefaultModel();
}

private static Collection<Statement> enhanceStatements(Collection<Statement> toEnhance, Resource subject,
Property property, RDFNode value, Model addedModel,
Model removedModel) {
private Collection<Statement> enhanceStatements(Collection<Statement> toEnhance, Resource subject,
Property property, RDFNode value, Model addedModel,
Model removedModel, String context) {
final Set<Statement> statements = new HashSet<>(toEnhance);
statements.addAll(addedModel.listStatements(subject, property, value).toList());
removedModel.listStatements(subject, property, value).toList().forEach(statements::remove);
return statements;
return statements.stream()
.filter(s -> removedSubjectPredicateStatements.stream()
.noneMatch(spc -> spc.matches(s, context)))
.collect(Collectors.toSet());
}

Containment contains(Resource subject, Property property, RDFNode value, Collection<String> contexts) {
if (contexts.isEmpty()) {
if (removedDefault().contains(subject, property, value)) {
if (removedDefault().contains(subject, property, value) || removedSubjectPredicateStatements.contains(new SubjectPredicateContext(subject, property, Collections.emptySet()))) {
return Containment.REMOVED;
} else {
return addedDefault().contains(subject, property, value) ? Containment.ADDED :
Containment.UNKNOWN;
Containment.UNKNOWN;
}
} else {
Containment result = Containment.UNKNOWN;
if (removedSubjectPredicateStatements.contains(new SubjectPredicateContext(subject, property, new HashSet<>(contexts)))) {
return Containment.REMOVED;
}
for (String c : contexts) {
if (removed.getNamedModel(c).contains(subject, property, value)) {
return Containment.REMOVED;
Expand Down Expand Up @@ -116,6 +133,10 @@ void removeStatements(List<Statement> statements, String context) {
registerStatements(statements, context, removed, added);
}

void removeStatementsBySubjectAndPredicate(Collection<SubjectPredicateContext> toRemove) {
removedSubjectPredicateStatements.addAll(toRemove);
}

Dataset getAdded() {
return added;
}
Expand All @@ -124,6 +145,10 @@ Dataset getRemoved() {
return removed;
}

public Set<SubjectPredicateContext> getRemovedSubjectPredicateStatements() {
return removedSubjectPredicateStatements;
}

List<String> getContexts() {
final Iterator<String> it = added.listNames();
final List<String> contexts = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,20 @@ public void remove(Resource subject, Property property, RDFNode object, String c
}
}

@Override
public void removeStatementsBySubjectAndPredicate(Collection<SubjectPredicateContext> spc) {
ensureTransactionalState();
spc.forEach(s -> {
if (s.getContexts().isEmpty()) {
storage.remove(storage.getDefaultGraph()
.listStatements(s.getSubject(), s.getPredicate(), (RDFNode) null), null);
} else {
s.getContexts().forEach(c -> storage.remove(storage.getNamedGraph(c)
.listStatements(s.getSubject(), s.getPredicate(), (RDFNode) null), c));
}
});
}

@Override
public AbstractResultSet executeSelectQuery(Query query, StatementOntology target) throws JenaDriverException {
ensureOpen();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,19 @@ public void remove(Resource subject, Property property, RDFNode object, String c
remove(toRemove, context);
}

@Override
public void removeStatementsBySubjectAndPredicate(Collection<SubjectPredicateContext> spc) {
ensureOpen();
spc.forEach(s -> {
if (s.getContexts().isEmpty()) {
remove(s.getSubject(), s.getPredicate(), null, null);
} else {
s.getContexts().forEach(ctx -> remove(s.getSubject(), s.getPredicate(), null, ctx));
}
});
transactionalChanges.removeStatementsBySubjectAndPredicate(spc);
}

@Override
public AbstractResultSet executeSelectQuery(Query query, StatementOntology target) throws JenaDriverException {
ensureOpen();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ public interface StorageConnector extends Closeable, Wrapper, StatementExecutor
*/
void remove(Resource subject, Property property, RDFNode object, String context);

/**
* Removes statements that have the specified subject and predicate pairs.
*
* @param spc Subject-predicate-contexts tuples
*/
void removeStatementsBySubjectAndPredicate(Collection<SubjectPredicateContext> spc);

@Override
void close() throws JenaDriverException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package cz.cvut.kbss.ontodriver.jena.connector;

import org.apache.jena.rdf.model.Property;
import org.apache.jena.rdf.model.Resource;
import org.apache.jena.rdf.model.Statement;

import java.util.Objects;
import java.util.Set;

public final class SubjectPredicateContext {

private final Resource subject;

private final Property predicate;

private final Set<String> contexts;

public SubjectPredicateContext(Resource subject, Property predicate, Set<String> contexts) {
assert subject != null;
assert contexts != null;

this.subject = subject;
this.predicate = predicate;
this.contexts = contexts;
}

public Resource getSubject() {
return subject;
}

public Property getPredicate() {
return predicate;
}

public Set<String> getContexts() {
return contexts;
}

public boolean matches(Statement s, String context) {
return subject.equals(s.getSubject()) && predicate.equals(s.getPredicate()) &&
(contexts.isEmpty() || contexts.contains(context));
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SubjectPredicateContext)) return false;
SubjectPredicateContext that = (SubjectPredicateContext) o;
return subject.equals(that.subject) && Objects.equals(predicate, that.predicate) && contexts.equals(
that.contexts);
}

@Override
public int hashCode() {
return Objects.hash(subject, predicate, contexts);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import cz.cvut.kbss.ontodriver.descriptor.AxiomDescriptor;
import cz.cvut.kbss.ontodriver.jena.connector.StorageConnector;
import cz.cvut.kbss.ontodriver.jena.connector.SubjectPredicateContext;
import cz.cvut.kbss.ontodriver.jena.environment.Generator;
import cz.cvut.kbss.ontodriver.model.Assertion;
import cz.cvut.kbss.ontodriver.model.NamedResource;
Expand All @@ -28,6 +29,8 @@
import org.mockito.junit.jupiter.MockitoExtension;

import java.net.URI;
import java.util.Collections;
import java.util.Set;

import static org.apache.jena.rdf.model.ResourceFactory.createProperty;
import static org.mockito.Mockito.verify;
Expand Down Expand Up @@ -57,8 +60,10 @@ public void removeRemovesStatementsByProperty() {
descriptor.addAssertion(op);

remover.remove(descriptor);
verify(connectorMock).remove(SUBJECT_RESOURCE, createProperty(dp.getIdentifier().toString()), null, null);
verify(connectorMock).remove(SUBJECT_RESOURCE, createProperty(op.getIdentifier().toString()), null, null);
verify(connectorMock).removeStatementsBySubjectAndPredicate(Set.of(
new SubjectPredicateContext(SUBJECT_RESOURCE, createProperty(dp.getIdentifier().toString()), Collections.emptySet()),
new SubjectPredicateContext(SUBJECT_RESOURCE, createProperty(op.getIdentifier().toString()), Collections.emptySet())
));
}

@Test
Expand All @@ -74,9 +79,9 @@ public void removeRemovesStatementsByPropertyFromCorrectContexts() {
descriptor.addAssertionContext(ap, assertionContext);

remover.remove(descriptor);
verify(connectorMock)
.remove(SUBJECT_RESOURCE, createProperty(ca.getIdentifier().toString()), null, mainContext.toString());
verify(connectorMock).remove(SUBJECT_RESOURCE, createProperty(ap.getIdentifier().toString()), null,
assertionContext.toString());
verify(connectorMock).removeStatementsBySubjectAndPredicate(Set.of(
new SubjectPredicateContext(SUBJECT_RESOURCE, createProperty(ca.getIdentifier().toString()), Set.of(mainContext.toString())),
new SubjectPredicateContext(SUBJECT_RESOURCE, createProperty(ap.getIdentifier().toString()), Set.of(assertionContext.toString()))
));
}
}
Loading

0 comments on commit cb85679

Please sign in to comment.