Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug#202 concurrent attribute modification #203

Merged
merged 7 commits into from
Sep 22, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import cz.cvut.kbss.jopa.vocabulary.RDF;
import cz.cvut.kbss.jopa.vocabulary.RDFS;
import cz.cvut.kbss.ontodriver.owlapi.OwlapiAdapter;
import cz.cvut.kbss.ontodriver.owlapi.util.MutableAddAxiom;
import cz.cvut.kbss.ontodriver.owlapi.change.MutableAddAxiom;
import cz.cvut.kbss.ontodriver.owlapi.util.OwlapiUtils;
import org.semanticweb.owlapi.model.*;
import org.semanticweb.owlapi.search.EntitySearcher;
Expand All @@ -39,7 +39,7 @@ public void persistTestData(Collection<Quad> data, EntityManager em) {
final OwlapiAdapter owlapiAdapter = em.unwrap(OwlapiAdapter.class);
for (Quad t : data) {
final OWLNamedIndividual ind = df.getOWLNamedIndividual(IRI.create(t.getSubject()));
final AddAxiom axiom;
final MutableAddAxiom axiom;
if (t.getProperty().toString().equals(RDF.TYPE)) {
final OWLClass cls = df.getOWLClass(IRI.create(t.getValue().toString()));
axiom = new MutableAddAxiom(ontology, df.getOWLClassAssertionAxiom(cls, ind));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import cz.cvut.kbss.jopa.exceptions.IntegrityConstraintViolatedException;
import cz.cvut.kbss.jopa.exceptions.RollbackException;
import cz.cvut.kbss.jopa.model.EntityManager;
import cz.cvut.kbss.jopa.model.descriptors.Descriptor;
import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor;
import cz.cvut.kbss.jopa.oom.exceptions.UnpersistedChangeException;
Expand Down Expand Up @@ -1376,4 +1377,29 @@ void mergeHandlesCascadingAndReferencedObjects() {
// The incorrect merge messes up the cached value of the original entityA
assertEquals(entityA.getUri(), aResult.getUri());
}

/**
* Bug #202
*/
@Test
void concurrentTransactionsLeaveDataInConsistentState() {
final String a1String = "a1String";
final String a2String = "a2String";
this.em= getEntityManager("concurrentTransactionsLeaveDataInConsistentState", false);
final EntityManager secondEm = em.getEntityManagerFactory().createEntityManager();
persist(entityA);
em.getTransaction().begin();
final OWLClassA a1 = em.find(OWLClassA.class, entityA.getUri());

secondEm.getTransaction().begin();
final OWLClassA a2 = secondEm.find(OWLClassA.class, entityA.getUri());

a1.setStringAttribute(a1String);
a2.setStringAttribute(a2String);
em.getTransaction().commit();
secondEm.getTransaction().commit();

final OWLClassA result = em.find(OWLClassA.class, entityA.getUri());
assertEquals(a2String, result.getStringAttribute());
}
}
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.removePropertyValues(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.removePropertyValues(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 removePropertyValues(Collection<SubjectPredicateContext> spc) {
transaction.verifyActive();
localModel.removePropertyValues(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,19 @@ void removeStatements(List<Statement> statements, String context) {
registerStatements(statements, context, removed, added);
}

void removePropertyValues(Collection<SubjectPredicateContext> toRemove) {
removedSubjectPredicateStatements.addAll(toRemove);
toRemove.forEach(spc -> {
if (spc.getContexts().isEmpty()) {
added.getDefaultModel().remove(added.getDefaultModel()
.listStatements(spc.getSubject(), spc.getPredicate(), (RDFNode) null));
} else {
spc.getContexts().forEach(c -> added.getNamedModel(c).remove(added.getNamedModel(c)
.listStatements(spc.getSubject(), spc.getPredicate(), (RDFNode) null)));
}
});
}

Dataset getAdded() {
return added;
}
Expand All @@ -124,6 +154,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 removePropertyValues(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 removePropertyValues(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.removePropertyValues(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 all property values specified by the provided argument for the specified subject(s).
*
* @param spc Subject-predicate-contexts tuples
*/
void removePropertyValues(Collection<SubjectPredicateContext> spc);

@Override
void close() throws JenaDriverException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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;

/**
* Represents the subject, predicate and context(s) of a statement.
*
* Used to indicate what property values to remove from the repository.
*/
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);
}
}
Loading