Skip to content

Commit c5040e7

Browse files
authored
GH-4999: improve UpdateWithModelBuilder to support statement removal (#5008)
2 parents 8fed00e + e7ebb35 commit c5040e7

File tree

4 files changed

+157
-12
lines changed

4 files changed

+157
-12
lines changed

spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/UpdateWithModelBuilder.java

Lines changed: 120 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,38 @@
1313

1414
import java.io.StringWriter;
1515
import java.lang.invoke.MethodHandles;
16-
import java.util.ArrayList;
17-
import java.util.Collection;
18-
import java.util.List;
16+
import java.util.*;
1917
import java.util.function.Consumer;
2018
import java.util.function.Function;
2119

2220
import org.apache.commons.lang3.ObjectUtils;
23-
import org.eclipse.rdf4j.model.BNode;
24-
import org.eclipse.rdf4j.model.IRI;
25-
import org.eclipse.rdf4j.model.Model;
26-
import org.eclipse.rdf4j.model.Namespace;
27-
import org.eclipse.rdf4j.model.Resource;
28-
import org.eclipse.rdf4j.model.Statement;
21+
import org.eclipse.rdf4j.model.*;
22+
import org.eclipse.rdf4j.model.base.AbstractStatement;
23+
import org.eclipse.rdf4j.model.impl.LinkedHashModel;
2924
import org.eclipse.rdf4j.model.util.ModelBuilder;
25+
import org.eclipse.rdf4j.query.Operation;
3026
import org.eclipse.rdf4j.repository.RepositoryConnection;
3127
import org.eclipse.rdf4j.rio.RDFFormat;
3228
import org.eclipse.rdf4j.rio.Rio;
29+
import org.eclipse.rdf4j.spring.support.RDF4JTemplate;
3330
import org.slf4j.Logger;
3431
import org.slf4j.LoggerFactory;
3532

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

4455
private final RepositoryConnection con;
56+
57+
/** the model builder being exposed to clients */
4558
private final ModelBuilder modelBuilder;
59+
/** the model being built by the modelBuilder, and that is going to be added to the repository eventually */
60+
private final Model addModel;
61+
62+
/**
63+
* Set of Statements to be removed from the repository eventually. The Statement implementation used here is the
64+
* {@link WildcardAllowingStatement}, which allows for using wildcards for deletion
65+
*/
66+
private final Set<Statement> removeStatements;
4667

4768
public UpdateWithModelBuilder(RepositoryConnection con) {
4869
this.con = con;
49-
this.modelBuilder = new ModelBuilder();
70+
this.addModel = new LinkedHashModel();
71+
this.removeStatements = new HashSet<>();
72+
this.modelBuilder = new ModelBuilder(addModel);
73+
}
74+
75+
public static UpdateWithModelBuilder fromTemplate(RDF4JTemplate template) {
76+
return template.applyToConnection(con -> new UpdateWithModelBuilder(con));
77+
}
78+
79+
/**
80+
* Will remove statements upon update execution, before processing any additions. Statements that are removed here
81+
* are also removed from the #addModel at the time of this call (not upon update execution)
82+
*
83+
* <p>
84+
* The semantics of {@link RepositoryConnection#remove(Iterable, Resource...)} apply, i.e. the resource(s) specified
85+
* here are used there, if any.
86+
*
87+
* @param subject the subject, or null to match any resource
88+
* @param predicate the predicate, or null to match any IRI
89+
* @param object the object, or null to match any value
90+
* @param resources the context(s), if any
91+
* @return this builder
92+
*/
93+
public UpdateWithModelBuilder remove(
94+
Resource subject, IRI predicate, Value object, Resource... resources) {
95+
addModel.remove(subject, predicate, object, resources);
96+
if (resources.length == 0) {
97+
removeStatements.add(new WildcardAllowingStatement(subject, predicate, object, null));
98+
} else {
99+
for (int i = 0; i < resources.length; i++) {
100+
removeStatements.add(
101+
new WildcardAllowingStatement(subject, predicate, object, resources[i]));
102+
}
103+
}
104+
return this;
50105
}
51106

52107
public UpdateWithModelBuilder setNamespace(Namespace ns) {
@@ -171,9 +226,63 @@ public void execute() {
171226
Model model = modelBuilder.build();
172227
if (logger.isDebugEnabled()) {
173228
StringWriter sw = new StringWriter();
229+
Rio.write(this.removeStatements, sw, RDFFormat.TURTLE);
230+
logger.debug("removing the following triples:\n{}", sw.toString());
231+
sw = new StringWriter();
174232
Rio.write(model, sw, RDFFormat.TURTLE);
175233
logger.debug("adding the following triples:\n{}", sw.toString());
176234
}
177-
con.add(model);
235+
con.remove(this.removeStatements);
236+
con.add(this.addModel);
237+
}
238+
239+
static class WildcardAllowingStatement extends AbstractStatement {
240+
private static final long serialVersionUID = -4116676621136121342L;
241+
private final Resource subject;
242+
private final IRI predicate;
243+
private final Value object;
244+
private final Resource context;
245+
246+
WildcardAllowingStatement(Resource subject, IRI predicate, Value object, Resource context) {
247+
this.subject = subject;
248+
this.predicate = predicate;
249+
this.object = object;
250+
this.context = context;
251+
}
252+
253+
public Resource getSubject() {
254+
return this.subject;
255+
}
256+
257+
public IRI getPredicate() {
258+
return this.predicate;
259+
}
260+
261+
public Value getObject() {
262+
return this.object;
263+
}
264+
265+
public Resource getContext() {
266+
return this.context;
267+
}
268+
269+
@Override
270+
public boolean equals(Object o) {
271+
if (this == o)
272+
return true;
273+
if (o == null || getClass() != o.getClass())
274+
return false;
275+
WildcardAllowingStatement that = (WildcardAllowingStatement) o;
276+
return Objects.equals(getSubject(), that.getSubject())
277+
&& Objects.equals(getPredicate(), that.getPredicate())
278+
&& Objects.equals(getObject(), that.getObject())
279+
&& Objects.equals(getContext(), that.getContext());
280+
}
281+
282+
@Override
283+
public int hashCode() {
284+
return Objects.hash(
285+
super.hashCode(), getSubject(), getPredicate(), getObject(), getContext());
286+
}
178287
}
179288
}

spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/dao/support/ServiceLayerTests.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import org.eclipse.rdf4j.spring.RDF4JSpringTestBase;
2121
import org.eclipse.rdf4j.spring.domain.model.Artist;
22+
import org.eclipse.rdf4j.spring.domain.model.EX;
2223
import org.eclipse.rdf4j.spring.domain.model.Painting;
2324
import org.eclipse.rdf4j.spring.domain.service.ArtService;
2425
import org.eclipse.rdf4j.spring.support.RDF4JTemplate;
@@ -63,6 +64,19 @@ public void testCreatePainting() {
6364
assertTrue(painting.getId().toString().startsWith("urn:uuid"));
6465
}
6566

67+
@Test
68+
public void testChangeArtist() {
69+
Artist artist = artService.createArtist("Jan", "Vermeer");
70+
Painting painting = artService.createPainting("Cypresses", "oil on canvas", artist.getId());
71+
assertNotNull(painting.getId());
72+
assertTrue(painting.getId().toString().startsWith("urn:uuid"));
73+
assertEquals(artist.getId(), painting.getArtistId());
74+
artService.changeArtist(painting.getId(), EX.VanGogh);
75+
painting = artService.getPainting(painting.getId());
76+
assertNotNull(painting);
77+
assertEquals(EX.VanGogh, painting.getArtistId());
78+
}
79+
6680
@Test
6781
public void testCreatePaintingWithoutArtist() {
6882
assertThrows(NullPointerException.class, () -> artService.createPainting(
@@ -71,7 +85,6 @@ public void testCreatePaintingWithoutArtist() {
7185
null));
7286
}
7387

74-
// TODO
7588
@Test
7689
public void testRollbackOnException() {
7790
transactionTemplate.execute(status -> {

spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/PaintingDao.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.eclipse.rdf4j.query.BindingSet;
2323
import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries;
2424
import org.eclipse.rdf4j.spring.dao.SimpleRDF4JCRUDDao;
25+
import org.eclipse.rdf4j.spring.dao.support.UpdateWithModelBuilder;
2526
import org.eclipse.rdf4j.spring.dao.support.bindingsBuilder.MutableBindings;
2627
import org.eclipse.rdf4j.spring.dao.support.sparql.NamedSparqlSupplier;
2728
import org.eclipse.rdf4j.spring.domain.model.EX;
@@ -97,4 +98,11 @@ protected IRI getInputId(Painting painting) {
9798
}
9899
return painting.getId();
99100
}
101+
102+
public void changeArtist(IRI painting, IRI newArtist) {
103+
UpdateWithModelBuilder update = getRdf4JTemplate().updateWithBuilder();
104+
update.remove(null, EX.creatorOf, painting);
105+
update.add(newArtist, EX.creatorOf, painting);
106+
update.execute();
107+
}
100108
}

spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/service/ArtService.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ public class ArtService {
2929
@Autowired
3030
private PaintingDao paintingDao;
3131

32+
@Transactional
33+
public Artist getArtist(IRI id) {
34+
return this.artistDao.getById(id);
35+
}
36+
37+
@Transactional
38+
public Painting getPainting(IRI id) {
39+
return this.paintingDao.getById(id);
40+
}
41+
3242
@Transactional(propagation = Propagation.REQUIRED)
3343
public Artist createArtist(String firstName, String lastName) {
3444
Artist artist = new Artist();
@@ -46,4 +56,9 @@ public Painting createPainting(String title, String technique, IRI artist) {
4656
return paintingDao.save(painting);
4757
}
4858

59+
@Transactional(propagation = Propagation.REQUIRED)
60+
public void changeArtist(IRI painting, IRI newArtist) {
61+
paintingDao.changeArtist(painting, newArtist);
62+
}
63+
4964
}

0 commit comments

Comments
 (0)