UpdateWithModelBuilder.java
/*******************************************************************************
* Copyright (c) 2021 Eclipse RDF4J contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*******************************************************************************/
package org.eclipse.rdf4j.spring.dao.support;
import java.io.StringWriter;
import java.lang.invoke.MethodHandles;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import org.apache.commons.lang3.ObjectUtils;
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
*/
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.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) {
modelBuilder.setNamespace(ns);
return this;
}
public UpdateWithModelBuilder setNamespace(String prefix, String namespace) {
modelBuilder.setNamespace(prefix, namespace);
return this;
}
public UpdateWithModelBuilder subject(Resource subject) {
modelBuilder.subject(subject);
return this;
}
public UpdateWithModelBuilder subject(String prefixedNameOrIri) {
modelBuilder.subject(prefixedNameOrIri);
return this;
}
public UpdateWithModelBuilder namedGraph(Resource namedGraph) {
modelBuilder.namedGraph(namedGraph);
return this;
}
public UpdateWithModelBuilder namedGraph(String prefixedNameOrIRI) {
modelBuilder.namedGraph(prefixedNameOrIRI);
return this;
}
public UpdateWithModelBuilder defaultGraph() {
modelBuilder.defaultGraph();
return this;
}
public UpdateWithModelBuilder addMaybe(Resource subject, IRI predicate, Object object) {
if (ObjectUtils.allNotNull(subject, predicate, object)) {
return add(subject, predicate, object);
}
return this;
}
public UpdateWithModelBuilder add(Resource subject, IRI predicate, Object object) {
modelBuilder.add(subject, predicate, object);
return this;
}
public UpdateWithModelBuilder addMaybe(String subject, IRI predicate, Object object) {
if (ObjectUtils.allNotNull(subject, predicate, object)) {
return add(subject, predicate, object);
}
return this;
}
public UpdateWithModelBuilder add(String subject, IRI predicate, Object object) {
modelBuilder.add(subject, predicate, object);
return this;
}
public UpdateWithModelBuilder addMaybe(String subject, String predicate, Object object) {
if (ObjectUtils.allNotNull(subject, predicate, object)) {
return add(subject, predicate, object);
}
return this;
}
public UpdateWithModelBuilder add(String subject, String predicate, Object object) {
modelBuilder.add(subject, predicate, object);
return this;
}
public UpdateWithModelBuilder addMaybe(IRI predicate, Object object) {
if (ObjectUtils.allNotNull(predicate, object)) {
return add(predicate, object);
}
return this;
}
public UpdateWithModelBuilder add(IRI predicate, Object object) {
modelBuilder.add(predicate, object);
return this;
}
public UpdateWithModelBuilder addMaybe(String predicate, Object object) {
if (ObjectUtils.allNotNull(predicate, object)) {
return add(predicate, object);
}
return this;
}
public UpdateWithModelBuilder add(String predicate, Object object) {
modelBuilder.add(predicate, object);
return this;
}
public void acceptConnection(Consumer<RepositoryConnection> connectionConsumer) {
connectionConsumer.accept(this.con);
}
public <T> T applyToConnection(Function<RepositoryConnection, T> function) {
return function.apply(con);
}
public BNode createBNode() {
return con.getValueFactory().createBNode();
}
public UpdateWithModelBuilder withSink(Consumer<Collection<Statement>> consumer) {
List<Statement> sink = new ArrayList<>();
consumer.accept(sink);
if (!sink.isEmpty()) {
sink.stream()
.forEach(
s -> modelBuilder.add(s.getSubject(), s.getPredicate(), s.getObject()));
}
return this;
}
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.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());
}
}
}