Skip to content

Commit

Permalink
eclipse-rdf4jGH-4999: improve UpdateWithModelBuilder to support state…
Browse files Browse the repository at this point in the history
…ment removal
  • Loading branch information
fkleedorfer committed Jun 2, 2024
1 parent ac2accd commit e7ebb35
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,38 @@

import java.io.StringWriter;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;

import org.apache.commons.lang3.ObjectUtils;
import org.eclipse.rdf4j.model.BNode;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.Namespace;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.*;
import org.eclipse.rdf4j.model.base.AbstractStatement;
import org.eclipse.rdf4j.model.impl.LinkedHashModel;
import org.eclipse.rdf4j.model.util.ModelBuilder;
import org.eclipse.rdf4j.query.Operation;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.Rio;
import org.eclipse.rdf4j.spring.support.RDF4JTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* <p>
* An {@link Operation} that holds a {@link Model} internally and exposes a {@link ModelBuilder} for adding to it.
* Moreover it allows for deleting statements.
* </p>
* <p>
* Thus, the class provides a way of configuring an update to the repository incrementally, and no repository access
* happens until {@link #execute()} is called. (unless the client uses {@link #applyToConnection(Function)} and accesses
* the repository that way.)
* </p>
* Removing statements via {@link #remove} will remove them from the repository when {@link #execute()} is called;
* moreover, the statements will also be removed from the model at the time of the {@link #remove} call, such that a
* subsequent creation of some of the deleted statements to the model will result in those triples being first deleted
* and then added to the repository when {@link #execute()} is called.
*
* @author Florian Kleedorfer
* @since 4.0.0
*/
Expand All @@ -42,11 +53,55 @@ public class UpdateWithModelBuilder {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

private final RepositoryConnection con;

/** the model builder being exposed to clients */
private final ModelBuilder modelBuilder;
/** the model being built by the modelBuilder, and that is going to be added to the repository eventually */
private final Model addModel;

/**
* Set of Statements to be removed from the repository eventually. The Statement implementation used here is the
* {@link WildcardAllowingStatement}, which allows for using wildcards for deletion
*/
private final Set<Statement> removeStatements;

public UpdateWithModelBuilder(RepositoryConnection con) {
this.con = con;
this.modelBuilder = new ModelBuilder();
this.addModel = new LinkedHashModel();
this.removeStatements = new HashSet<>();
this.modelBuilder = new ModelBuilder(addModel);
}

public static UpdateWithModelBuilder fromTemplate(RDF4JTemplate template) {
return template.applyToConnection(con -> new UpdateWithModelBuilder(con));
}

/**
* Will remove statements upon update execution, before processing any additions. Statements that are removed here
* are also removed from the #addModel at the time of this call (not upon update execution)
*
* <p>
* The semantics of {@link RepositoryConnection#remove(Iterable, Resource...)} apply, i.e. the resource(s) specified
* here are used there, if any.
*
* @param subject the subject, or null to match any resource
* @param predicate the predicate, or null to match any IRI
* @param object the object, or null to match any value
* @param resources the context(s), if any
* @return this builder
*/
public UpdateWithModelBuilder remove(
Resource subject, IRI predicate, Value object, Resource... resources) {
addModel.remove(subject, predicate, object, resources);
if (resources.length == 0) {
removeStatements.add(new WildcardAllowingStatement(subject, predicate, object, null));
} else {
for (int i = 0; i < resources.length; i++) {
removeStatements.add(
new WildcardAllowingStatement(subject, predicate, object, resources[i]));
}
}
return this;
}

public UpdateWithModelBuilder setNamespace(Namespace ns) {
Expand Down Expand Up @@ -171,9 +226,63 @@ public void execute() {
Model model = modelBuilder.build();
if (logger.isDebugEnabled()) {
StringWriter sw = new StringWriter();
Rio.write(this.removeStatements, sw, RDFFormat.TURTLE);
logger.debug("removing the following triples:\n{}", sw.toString());
sw = new StringWriter();
Rio.write(model, sw, RDFFormat.TURTLE);
logger.debug("adding the following triples:\n{}", sw.toString());
}
con.add(model);
con.remove(this.removeStatements);
con.add(this.addModel);
}

static class WildcardAllowingStatement extends AbstractStatement {
private static final long serialVersionUID = -4116676621136121342L;
private final Resource subject;
private final IRI predicate;
private final Value object;
private final Resource context;

WildcardAllowingStatement(Resource subject, IRI predicate, Value object, Resource context) {
this.subject = subject;
this.predicate = predicate;
this.object = object;
this.context = context;
}

public Resource getSubject() {
return this.subject;
}

public IRI getPredicate() {
return this.predicate;
}

public Value getObject() {
return this.object;
}

public Resource getContext() {
return this.context;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
WildcardAllowingStatement that = (WildcardAllowingStatement) o;
return Objects.equals(getSubject(), that.getSubject())
&& Objects.equals(getPredicate(), that.getPredicate())
&& Objects.equals(getObject(), that.getObject())
&& Objects.equals(getContext(), that.getContext());
}

@Override
public int hashCode() {
return Objects.hash(
super.hashCode(), getSubject(), getPredicate(), getObject(), getContext());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import org.eclipse.rdf4j.spring.RDF4JSpringTestBase;
import org.eclipse.rdf4j.spring.domain.model.Artist;
import org.eclipse.rdf4j.spring.domain.model.EX;
import org.eclipse.rdf4j.spring.domain.model.Painting;
import org.eclipse.rdf4j.spring.domain.service.ArtService;
import org.eclipse.rdf4j.spring.support.RDF4JTemplate;
Expand Down Expand Up @@ -63,6 +64,19 @@ public void testCreatePainting() {
assertTrue(painting.getId().toString().startsWith("urn:uuid"));
}

@Test
public void testChangeArtist() {
Artist artist = artService.createArtist("Jan", "Vermeer");
Painting painting = artService.createPainting("Cypresses", "oil on canvas", artist.getId());
assertNotNull(painting.getId());
assertTrue(painting.getId().toString().startsWith("urn:uuid"));
assertEquals(artist.getId(), painting.getArtistId());
artService.changeArtist(painting.getId(), EX.VanGogh);
painting = artService.getPainting(painting.getId());
assertNotNull(painting);
assertEquals(EX.VanGogh, painting.getArtistId());
}

@Test
public void testCreatePaintingWithoutArtist() {
assertThrows(NullPointerException.class, () -> artService.createPainting(
Expand All @@ -71,7 +85,6 @@ public void testCreatePaintingWithoutArtist() {
null));
}

// TODO
@Test
public void testRollbackOnException() {
transactionTemplate.execute(status -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries;
import org.eclipse.rdf4j.spring.dao.SimpleRDF4JCRUDDao;
import org.eclipse.rdf4j.spring.dao.support.UpdateWithModelBuilder;
import org.eclipse.rdf4j.spring.dao.support.bindingsBuilder.MutableBindings;
import org.eclipse.rdf4j.spring.dao.support.sparql.NamedSparqlSupplier;
import org.eclipse.rdf4j.spring.domain.model.EX;
Expand Down Expand Up @@ -97,4 +98,11 @@ protected IRI getInputId(Painting painting) {
}
return painting.getId();
}

public void changeArtist(IRI painting, IRI newArtist) {
UpdateWithModelBuilder update = getRdf4JTemplate().updateWithBuilder();
update.remove(null, EX.creatorOf, painting);
update.add(newArtist, EX.creatorOf, painting);
update.execute();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ public class ArtService {
@Autowired
private PaintingDao paintingDao;

@Transactional
public Artist getArtist(IRI id) {
return this.artistDao.getById(id);
}

@Transactional
public Painting getPainting(IRI id) {
return this.paintingDao.getById(id);
}

@Transactional(propagation = Propagation.REQUIRED)
public Artist createArtist(String firstName, String lastName) {
Artist artist = new Artist();
Expand All @@ -46,4 +56,9 @@ public Painting createPainting(String title, String technique, IRI artist) {
return paintingDao.save(painting);
}

@Transactional(propagation = Propagation.REQUIRED)
public void changeArtist(IRI painting, IRI newArtist) {
paintingDao.changeArtist(painting, newArtist);
}

}

0 comments on commit e7ebb35

Please sign in to comment.