RDF4JTemplate.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.support;

import static org.eclipse.rdf4j.spring.util.TypeMappingUtils.toIri;

import java.io.IOException;
import java.io.StringWriter;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.eclipse.rdf4j.common.annotation.Experimental;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.query.GraphQuery;
import org.eclipse.rdf4j.query.TupleQuery;
import org.eclipse.rdf4j.query.Update;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.Rio;
import org.eclipse.rdf4j.sail.shacl.ShaclSailValidationException;
import org.eclipse.rdf4j.sparqlbuilder.constraint.Expressions;
import org.eclipse.rdf4j.sparqlbuilder.constraint.propertypath.PropertyPath;
import org.eclipse.rdf4j.sparqlbuilder.core.SparqlBuilder;
import org.eclipse.rdf4j.sparqlbuilder.core.Variable;
import org.eclipse.rdf4j.sparqlbuilder.core.query.ModifyQuery;
import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries;
import org.eclipse.rdf4j.sparqlbuilder.graphpattern.TriplePattern;
import org.eclipse.rdf4j.sparqlbuilder.rdf.RdfValue;
import org.eclipse.rdf4j.spring.dao.exception.RDF4JDaoException;
import org.eclipse.rdf4j.spring.dao.support.UpdateWithModelBuilder;
import org.eclipse.rdf4j.spring.dao.support.opbuilder.GraphQueryEvaluationBuilder;
import org.eclipse.rdf4j.spring.dao.support.opbuilder.TupleQueryEvaluationBuilder;
import org.eclipse.rdf4j.spring.dao.support.opbuilder.UpdateExecutionBuilder;
import org.eclipse.rdf4j.spring.dao.support.sparql.NamedSparqlSupplier;
import org.eclipse.rdf4j.spring.support.connectionfactory.RepositoryConnectionFactory;
import org.eclipse.rdf4j.spring.util.TypeMappingUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ResourceLoader;

/**
 * @author Florian Kleedorfer
 * @author Gabriel Pickl
 * @since 4.0.0
 */
@Experimental
public class RDF4JTemplate {
	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
	private final RepositoryConnectionFactory repositoryConnectionFactory;
	private final OperationInstantiator operationInstantiator;
	private final ResourceLoader resourceLoader;
	private final UUIDSource uuidSource;

	public RDF4JTemplate(
			@Autowired RepositoryConnectionFactory repositoryConnectionFactory,
			@Autowired OperationInstantiator operationInstantiator,
			@Autowired ResourceLoader resourceLoader,
			@Autowired(required = false) UUIDSource uuidSource) {
		this.repositoryConnectionFactory = repositoryConnectionFactory;
		this.operationInstantiator = operationInstantiator;
		this.resourceLoader = resourceLoader;
		if (uuidSource == null) {
			this.uuidSource = new DefaultUUIDSource();
		} else {
			this.uuidSource = uuidSource;
		}
	}

	public void consumeConnection(final Consumer<RepositoryConnection> fun) {
		RepositoryConnection con = getRepositoryConnection();
		if (logger.isDebugEnabled()) {
			logger.debug(
					"using connection {} of type {}",
					con.hashCode(),
					con.getClass().getSimpleName());
		}
		try {
			fun.accept(con);
		} catch (Exception e) {
			logIfShaclValidationFailure(e);
			throw e;
		}
	}

	public <T> T applyToConnection(final Function<RepositoryConnection, T> fun) {
		RepositoryConnection con = getRepositoryConnection();
		if (logger.isDebugEnabled()) {
			logger.debug(
					"using connection {} of type {}",
					con.hashCode(),
					con.getClass().getSimpleName());
		}
		try {
			return fun.apply(con);
		} catch (Exception e) {
			logIfShaclValidationFailure(e);
			throw e;
		}
	}

	/**
	 * Bypassing any caches, generates a new Update from the specified SPARQL string and returns a Builder for its
	 * execution. Should be avoided in favor of one of the methods that apply caching unless the update is not reusable.
	 */
	public UpdateExecutionBuilder update(String updateString) {
		return applyToConnection(
				con -> new UpdateExecutionBuilder(
						operationInstantiator.getUpdate(con, updateString), this));
	}

	/**
	 * Uses a cached {@link Update} if one is available under the specified <code>operationName
	 * </code> for the {@link RepositoryConnection} that is used, otherwise the query string is obtained from the
	 * specified supplier, a new Update is instantiated and cached for future calls to this method.
	 * <p>
	 * Note: this call is equivalent to {@link #update(String)} if operation caching is disabled.
	 *
	 * @param owner                the class of the client requesting the update, used to generate a cache key in
	 *                             combination with the operation name
	 * @param operationName        name of the operation that, within the scope of the client, identifies the update
	 * @param updateStringSupplier supplies the sparql of the update if needed
	 */
	public UpdateExecutionBuilder update(
			Class<?> owner, String operationName, Supplier<String> updateStringSupplier) {
		return applyToConnection(
				con -> new UpdateExecutionBuilder(
						operationInstantiator.getUpdate(
								con, owner, operationName, updateStringSupplier),
						this));
	}

	/**
	 * Reads the update from the specified resource and provides it through a {@link Supplier <String>} in
	 * {@link #update(Class, String, Supplier)}, using the <code>resourceName
	 * </code> as the <code>operationName</code>.
	 */
	public UpdateExecutionBuilder updateFromResource(Class<?> owner, String resourceName) {
		return update(
				owner,
				resourceName,
				() -> getStringSupplierFromResourceContent(resourceName).get());
	}

	/**
	 * Uses the provided {@link NamedSparqlSupplier} for calling {@link #update(Class, String, Supplier)}.
	 */
	public UpdateExecutionBuilder update(Class<?> owner, NamedSparqlSupplier namedSparqlSupplier) {
		return update(
				owner, namedSparqlSupplier.getName(), namedSparqlSupplier.getSparqlSupplier());
	}

	public UpdateExecutionBuilder updateWithoutCachingStatement(String updateString) {
		return applyToConnection(
				con -> new UpdateExecutionBuilder(con.prepareUpdate(updateString), this));
	}

	public UpdateWithModelBuilder updateWithBuilder() {
		return new UpdateWithModelBuilder(getRepositoryConnection());
	}

	/**
	 * Bypassing any caches, generates a new TupleQuery from the specified SPARQL string and returns a Builder for its
	 * evaluation. Should be avoided in favor of one of the methods that apply caching unless the query is not reusable.
	 */
	public TupleQueryEvaluationBuilder tupleQuery(String queryString) {
		return new TupleQueryEvaluationBuilder(
				applyToConnection(con -> operationInstantiator.getTupleQuery(con, queryString)),
				this);
	}

	/**
	 * Uses a cached {@link TupleQuery} if one is available under the specified <code>operationName
	 * </code> for the {@link RepositoryConnection} that is used, otherwise the query string is obtained from the
	 * specified supplier, a new TupleQuery is instantiated and cached for future calls to this method.
	 */
	public TupleQueryEvaluationBuilder tupleQuery(
			Class<?> owner, String operationName, Supplier<String> queryStringSupplier) {
		return new TupleQueryEvaluationBuilder(
				applyToConnection(
						con -> operationInstantiator.getTupleQuery(
								con, owner, operationName, queryStringSupplier)),
				this);
	}

	/**
	 * Reads the query from the specified resource and provides it through a {@link Supplier <String>} in
	 * {@link #tupleQuery(Class, String, Supplier)}, using the <code>
	 * resourceName</code> as the <code>operationName</code>.
	 */
	public TupleQueryEvaluationBuilder tupleQueryFromResource(Class<?> owner, String resourceName) {
		return tupleQuery(
				owner,
				resourceName,
				() -> getStringSupplierFromResourceContent(resourceName).get());
	}

	/**
	 * Uses the provided {@link NamedSparqlSupplier} for calling {@link #tupleQuery(Class, String, Supplier)}.
	 */
	public TupleQueryEvaluationBuilder tupleQuery(
			Class<?> owner, NamedSparqlSupplier namedSparqlSupplier) {
		return tupleQuery(
				owner, namedSparqlSupplier.getName(), namedSparqlSupplier.getSparqlSupplier());
	}

	/**
	 * Bypassing any caches, generates a new GraphQuery from the specified SPARQL string and returns a Builder for its
	 * evaluation. Should be avoided in favor of one of the methods that apply caching unless the query is not reusable.
	 */
	public GraphQueryEvaluationBuilder graphQuery(String graphQueryString) {
		return new GraphQueryEvaluationBuilder(
				applyToConnection(
						con -> operationInstantiator.getGraphQuery(con, graphQueryString)),
				this);
	}

	/**
	 * Uses a cached {@link GraphQuery} if one is available under the specified <code>operationName
	 * </code> for the {@link RepositoryConnection} that is used, otherwise the query string is obtained from the
	 * specified supplier, a new GraphQuery is instantiated and cached for future calls to this method.
	 */
	public GraphQueryEvaluationBuilder graphQuery(
			Class<?> owner, String operationName, Supplier<String> queryStringSupplier) {
		return new GraphQueryEvaluationBuilder(
				applyToConnection(
						con -> operationInstantiator.getGraphQuery(
								con, owner, operationName, queryStringSupplier)),
				this);
	}

	/**
	 * Reads the query from the specified resource and provides it through a {@link Supplier <String>} in
	 * {@link #graphQuery(Class, String, Supplier)}, using the <code>
	 * resourceName</code> as the <code>operationName</code>.
	 */
	public GraphQueryEvaluationBuilder graphQueryFromResource(Class<?> owner, String resourceName) {
		return graphQuery(
				owner,
				resourceName,
				() -> getStringSupplierFromResourceContent(resourceName).get());
	}

	/**
	 * Uses the provided {@link NamedSparqlSupplier} for calling {@link #graphQuery(Class, String, Supplier)}.
	 */
	public GraphQueryEvaluationBuilder graphQuery(
			Class<?> owner, NamedSparqlSupplier namedSparqlSupplier) {
		return graphQuery(
				owner, namedSparqlSupplier.getName(), namedSparqlSupplier.getSparqlSupplier());
	}

	public void deleteTriplesWithSubject(IRI id) {
		consumeConnection(
				con -> {
					con.remove(id, null, null);
				});
	}

	/**
	 * Deletes the specified resource: all triples are deleted in which <code>id</code> is the subject or the object.
	 *
	 * @param id
	 */
	public void delete(IRI id) {
		consumeConnection(
				con -> {
					con.remove(id, null, null);
					con.remove((Resource) null, null, id);
				});
	}

	/**
	 * Deletes the specified resource and all resources <code>R</code> reached via any of the specified property paths.
	 * <p>
	 * Deletion means that all triples are removed in which <code>start</code> or any resource in <code>R</code> are the
	 * subject or the object.
	 *
	 * @param start         the initial resource to be deleted
	 * @param propertyPaths paths by which to reach more resources to be deleted.
	 */
	public void delete(IRI start, List<PropertyPath> propertyPaths) {
		List<Variable> targets = new ArrayList<>();
		int i = 0;
		Variable sp = SparqlBuilder.var("sp");
		Variable so = SparqlBuilder.var("so");
		Variable is = SparqlBuilder.var("is");
		Variable ip = SparqlBuilder.var("ip");
		ModifyQuery q = Queries.MODIFY()
				.delete(toIri(start).has(sp, so), is.has(ip, start))
				.where(toIri(start).has(sp, so).optional(), is.has(ip, start).optional());
		for (PropertyPath p : propertyPaths) {
			i++;
			Variable target = SparqlBuilder.var("target_" + i);
			Variable p1 = SparqlBuilder.var("p1_" + i);
			Variable o1 = SparqlBuilder.var("o2_" + i);
			Variable p2 = SparqlBuilder.var("p2_" + i);
			Variable s2 = SparqlBuilder.var("s2_" + i);
			q.delete(target.has(p1, o1), s2.has(p2, target))
					.where(
							toIri(start).has(p, target),
							target.has(p1, o1).optional(),
							s2.has(p2, target).optional());
		}
		update(q.getQueryString()).execute();
	}

	public void associate(
			IRI fromResource,
			IRI property,
			Collection<IRI> toResources,
			boolean deleteOtherOutgoing,
			boolean deleteOtherIcoming) {
		Variable from = SparqlBuilder.var("fromResource");
		Variable to = SparqlBuilder.var("toResource");
		if (deleteOtherOutgoing) {
			String query = Queries.MODIFY()
					.delete(from.has(property, to))
					.where(from.has(property, to))
					.getQueryString();
			update(query).withBinding(from, fromResource).execute();
		}
		if (deleteOtherIcoming) {
			String query = Queries.MODIFY()
					.delete(from.has(property, to))
					.where(
							from.has(property, to)
									.filter(
											Expressions.in(
													to,
													toResources.stream()
															.map(TypeMappingUtils::toIri)
															.collect(Collectors.toList())
															.toArray(
																	new RdfValue[toResources
																			.size()]))))
					.getQueryString();
			update(query).execute();
		}
		String query = Queries.INSERT_DATA(
				toResources.stream()
						.map(e -> toIri(fromResource).has(property, e))
						.collect(Collectors.toList())
						.toArray(new TriplePattern[toResources.size()]))
				.getQueryString();
		updateWithoutCachingStatement(query).execute();
	}

	/**
	 * Returns a {@link Supplier <String>} that returns the String content of the specified resource (as obtained by a
	 * {@link ResourceLoader}). The resource's content is read once when this method is called (revealing any problem
	 * reading the resource early on.
	 */
	public Supplier<String> getStringSupplierFromResourceContent(String resourceName) {
		Objects.requireNonNull(resourceName);
		try {
			org.springframework.core.io.Resource res = resourceLoader.getResource(resourceName);
			String contentAsString = new String(res.getInputStream().readAllBytes());
			return () -> contentAsString;
		} catch (IOException e) {
			throw new RDF4JDaoException(
					String.format("Cannot read String from resource %s", resourceName));
		}
	}

	private RepositoryConnection getRepositoryConnection() {
		return repositoryConnectionFactory.getConnection();
	}

	private void logIfShaclValidationFailure(Throwable t) {
		Throwable cause = t.getCause();
		if (cause instanceof ShaclSailValidationException) {
			logger.error("SHACL Validation failed!");
			Model report = ((ShaclSailValidationException) cause).validationReportAsModel();
			StringWriter out = new StringWriter();
			Rio.write(report, out, RDFFormat.TURTLE);
			logger.error("Validation report:\n{}", out.toString());
		}
	}

	/**
	 * Returns a UUID IRI (schema: 'urn:uuid'). Actual implementation depends on the {@link #uuidSource} that has been
	 * configured. See {@link UUIDSource} and {@link org.eclipse.rdf4j.spring.uuidsource} for details.
	 */
	public IRI getNewUUID() {
		return uuidSource.nextUUID();
	}
}