From e7ebb355565e64b3e22b980c1fdafb7c18101228 Mon Sep 17 00:00:00 2001 From: Florian Kleedorfer Date: Sun, 2 Jun 2024 15:26:25 +0200 Subject: [PATCH] GH-4999: improve UpdateWithModelBuilder to support statement removal --- .../dao/support/UpdateWithModelBuilder.java | 131 ++++++++++++++++-- .../spring/dao/support/ServiceLayerTests.java | 15 +- .../rdf4j/spring/domain/dao/PaintingDao.java | 8 ++ .../spring/domain/service/ArtService.java | 15 ++ 4 files changed, 157 insertions(+), 12 deletions(-) diff --git a/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/UpdateWithModelBuilder.java b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/UpdateWithModelBuilder.java index e13a9d3aac0..53aa8142951 100644 --- a/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/UpdateWithModelBuilder.java +++ b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/UpdateWithModelBuilder.java @@ -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; /** + *

+ * An {@link Operation} that holds a {@link Model} internally and exposes a {@link ModelBuilder} for adding to it. + * Moreover it allows for deleting statements. + *

+ *

+ * 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.) + *

+ * 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 */ @@ -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 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) + * + *

+ * 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) { @@ -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()); + } } } diff --git a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/dao/support/ServiceLayerTests.java b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/dao/support/ServiceLayerTests.java index 9cfc3b007d3..ffc52185888 100644 --- a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/dao/support/ServiceLayerTests.java +++ b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/dao/support/ServiceLayerTests.java @@ -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; @@ -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( @@ -71,7 +85,6 @@ public void testCreatePaintingWithoutArtist() { null)); } - // TODO @Test public void testRollbackOnException() { transactionTemplate.execute(status -> { diff --git a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/PaintingDao.java b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/PaintingDao.java index 0cd1bd72034..8acb4921c0e 100644 --- a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/PaintingDao.java +++ b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/PaintingDao.java @@ -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; @@ -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(); + } } diff --git a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/service/ArtService.java b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/service/ArtService.java index 6dd563489c5..588e4f30c99 100644 --- a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/service/ArtService.java +++ b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/service/ArtService.java @@ -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(); @@ -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); + } + }