
com.metreeca.rdf4j.assets.Graph Maven / Gradle / Ivy
/*
* Copyright © 2013-2021 Metreeca srl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.metreeca.rdf4j.assets;
import com.metreeca.rest.*;
import org.eclipse.rdf4j.model.*;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.query.*;
import org.eclipse.rdf4j.repository.Repository;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.rio.helpers.StatementCollector;
import java.time.Instant;
import java.util.*;
import java.util.function.*;
import static com.metreeca.json.Values.*;
import static com.metreeca.rest.Context.asset;
import static org.eclipse.rdf4j.query.QueryLanguage.SPARQL;
import static java.time.ZoneOffset.UTC;
import static java.time.temporal.ChronoUnit.MILLIS;
/**
* Graph store.
*
* Manages task execution on an RDF {@linkplain Repository repository}.
*
* Nested task executions on the same graph store from the same thread will share the same connection to the backing
* RDF repository through a {@link ThreadLocal} context variable.
*/
public final class Graph implements AutoCloseable {
/**
* Retrieves the default graph factory.
*
* @return the default graph factory, which throws an exception reporting the asset as undefined
*/
public static Supplier graph() {
return () -> { throw new IllegalStateException("undefined graph service"); };
}
//// Graph-Based Functions ////////////////////////////////////////////////////////////////////////////////////////
/**
* Creates a SPARQL query message filter.
*
* @param query the SPARQL graph query (describe/construct) to be executed by the new filter on target
* messages; empty scripts are ignored
* @param customizers optional custom configuration setters for the SPARQL query operation
* @param the type of the target message for the new filter
*
* @return a message filter executing the SPARQL graph {@code query} on target messages with {@linkplain
* #configure(Message, Operation, BiConsumer[]) standard bindings} and optional custom
* configurations;
* returns the input model extended with the statements returned by {@code query}
*
* @throws NullPointerException if any argument is null or if {@code customizers} contains null values
*/
@SafeVarargs public static > BiFunction, Collection> query(
final String query, final BiConsumer... customizers
) {
if ( query == null ) {
throw new NullPointerException("null query");
}
if ( customizers == null || Arrays.stream(customizers).anyMatch(Objects::isNull) ) {
throw new NullPointerException("null customizers");
}
final Graph graph=asset(graph());
return query.isEmpty() ? (message, model) -> model : (message, model) -> graph.exec(connection -> {
configure(
message, connection.prepareGraphQuery(SPARQL, query, message.request().base()), customizers
).evaluate(
new StatementCollector(model)
);
return model;
});
}
/**
* Creates a SPARQL update housekeeping task.
*
* @param update the SPARQL update script to be executed by the new housekeeping filter on target messages;
* empty scripts are ignored
* @param customizers optional custom configuration setters for the SPARQL update operation
* @param the type of the target message for the new filter
*
* @return a housekeeping task executing the SPARQL {@code update} script on target messages with {@linkplain
* #configure(Message, Operation, BiConsumer[]) standard bindings} and optional custom
* configurations;
* returns the
* input message without altering it
*
* @throws NullPointerException if any argument is null or if {@code customizers} contains null values
*/
@SafeVarargs public static > Function update(
final String update, final BiConsumer... customizers
) {
if ( update == null ) {
throw new NullPointerException("null update");
}
if ( customizers == null || Arrays.stream(customizers).anyMatch(Objects::isNull) ) {
throw new NullPointerException("null customizers");
}
final Graph graph=asset(graph());
return update.isEmpty() ? message -> message : message -> graph.exec(txn(connection -> {
configure(message, connection.prepareUpdate(SPARQL, update, message.request().base()), customizers).execute();
return message;
}));
}
/**
* Configures standard bindings for SPARQL operations.
*
* Configures the following pre-defined bindings for the target SPARQL operation:
*
*
*
*
*
*
* variable
* value
*
*
*
*
*
*
*
* {@code ?time}
* an {@code xsd:dateTime} literal representing the execution system time with millisecond precision
*
*
*
* {@code ?this}
* the {@linkplain Message#item() focus item} of the filtered message
*
*
*
* {@code ?stem}
* the {@linkplain IRI#getNamespace() namespace} of the IRI bound to the {@code this} variable
*
*
*
* {@code ?name}
* the local {@linkplain IRI#getLocalName() name} of the IRI bound to the {@code this} variable
*
*
*
* {@code ?task}
* the HTTP {@linkplain Request#method() method} of the original request
*
*
*
* {@code ?base}
* the {@linkplain Request#base() base} IRI of the original request
*
*
*
* {@code ?item}
* the {@linkplain Message#item() focus item} of the original request
*
*
*
* {@code ?user}
* the IRI identifying the {@linkplain Request#user() user} submitting the original
* request or
* {@linkplain RDF#NIL} if no user is authenticated
*
*
*
*
*
*
* If the target message is a {@linkplain Response response}, the following additional
* bindings are
* configured:
*
*
*
*
*
*
* variable
* value
*
*
*
*
*
*
*
* {@code ?code}
* the HTTP {@linkplain Response#status() status code} of the filtered response
*
*
*
*
*
*
* @param message the message to be filtered
* @param operation the SPARQL operation executed by the filter
* @param customizers optional custom configuration setters for the SPARQL {@code operation}
* @param the type f the {@code message} to be filtered
* @param the type of the SPARQL {@code operation} to be configured
*
* @return the input {@code operation} with standard bindings and optional custom configurations applied
*
* @throws NullPointerException if any argument is null or if {@code customizers} contains null values
*/
@SafeVarargs public static , O extends Operation> O configure(
final M message, final O operation, final BiConsumer... customizers
) {
if ( message == null ) {
throw new NullPointerException("null message");
}
if ( operation == null ) {
throw new NullPointerException("null operation");
}
if ( customizers == null ) {
throw new NullPointerException("null customizers");
}
if ( Arrays.stream(customizers).anyMatch(Objects::isNull) ) {
throw new NullPointerException("null customizer");
}
operation.setBinding("time", literal(Instant.now().truncatedTo(MILLIS).atZone(UTC)));
final IRI item=iri(message.item());
operation.setBinding("this", item);
operation.setBinding("stem", iri(item.getNamespace()));
operation.setBinding("name", literal(item.getLocalName()));
final Request request=message.request();
operation.setBinding("task", literal(request.method()));
operation.setBinding("base", iri(request.base()));
operation.setBinding("item", iri(request.item()));
operation.setBinding("user",
request.user().map(v -> v instanceof Value ? (Value)v : literal(v.toString())).orElse(RDF.NIL));
if ( message instanceof Response ) {
operation.setBinding("code", literal(integer(((Response)message).status())));
}
for (final BiConsumer customizer : customizers) {
customizer.accept(message, operation);
}
return operation;
}
/**
* Connects a graph-based supplier.
*
* @param supplier the graph-based supplier
* @param the type of the value generated by {@code supplier}
*
* @return a plain supplier that creates a read-only on demand connection to the to shared system {@linkplain
* #graph() Graph} and delegates value generation to the connected graph-based {@code supplier}
*
* @throws NullPointerException if {@code supplier} is null
*/
public static Supplier connect(final Function super RepositoryConnection, R> supplier) {
if ( supplier == null ) {
throw new NullPointerException("null supplier");
}
final Graph graph=asset(graph());
return () -> graph.exec(supplier::apply);
}
/**
* Connects a graph-based function.
*
* @param function the graph-based function
* @param the type of the argument accepted by {@code function}
* @param the type of the value returned by {@code function}
*
* @return a plain function that creates a read-only on demand connection to the to shared system {@linkplain
* #graph() Graph} and delegates processing to the connected graph-based {@code function}
*
* @throws NullPointerException if {@code function} is null
*/
public static Function connect(final BiFunction super RepositoryConnection, ? super V, R> function) {
if ( function == null ) {
throw new NullPointerException("null function");
}
final Graph graph=asset(graph());
return v -> graph.exec(connection -> { return function.apply(connection, v); });
}
/**
* Executes a graph-based task inside a transaction.
*
*
*
* - if a transaction is not already active on the underlying storage, begins one and commits it on
* successful
* task completion;
*
* - if the task throws an exception, rolls back the transaction and rethrows the exception;
*
* - in either case, no action is taken if the transaction was already terminated inside the task.
*
*
*
* @param task the graph-based task to be executed
*
* @return a graph-based task that executes the target {@code task} inside a graph transaction
*
* @throws NullPointerException if {@code task} is {@code null}
*/
public static Consumer txn(final Consumer task) {
if ( task == null ) {
throw new NullPointerException("null task");
}
return txn(connection -> {
task.accept(connection);
return connection;
})::apply;
}
/**
* Executes a graph-based task inside a transaction.
*
*
*
* - if a transaction is not already active on the underlying storage, begins one and commits it on
* successful
* task completion;
*
* - if the task throws an exception, rolls back the transaction and rethrows the exception;
*
* - in either case, no action is taken if the transaction was already terminated inside the task.
*
*
*
* @param task the graph-based task to be executed
* @param the type of the value returned by {@code task}
*
* @return a graph-based task that returns the value returned by the target {@code task} when executed inside a
* graph transaction
*
* @throws NullPointerException if {@code task} is {@code null}
*/
public static Function txn(final Function task) {
if ( task == null ) {
throw new NullPointerException("null task");
}
return connection -> {
if ( connection.isActive() ) {
return task.apply(connection);
} else {
try {
connection.begin();
final V value=task.apply(connection);
connection.commit();
return value;
} finally {
if ( connection.isActive() ) { connection.rollback(); }
}
}
};
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private Repository repository;
private final ThreadLocal context=new ThreadLocal<>();
/**
* Creates a graph store.
*
* @param repository the backing RDF repository
*
* @throws NullPointerException if {@code repository} is null
*/
public Graph(final Repository repository) {
if ( repository == null ) {
throw new NullPointerException("null repository");
}
this.repository=repository;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override public void close() {
try {
if ( repository != null && repository.isInitialized() ) { repository.shutDown(); }
} finally {
repository=null;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Executes a task inside a transaction on this graph store.
*
* If a transaction is not already active on the shared repository connection, begins one and commits it on
* successful task completion; if the task throws an exception, the transaction is rolled back and the exception
* rethrown; in either case, no action is taken if the transaction was already terminated inside the task.
*
* @param task the task to be executed; takes as argument a connection to the backing repository of this graph
* store
*
* @return this graph store
*
* @throws NullPointerException if {@code task} is null
*/
public Graph exec(final Consumer task) {
if ( task == null ) {
throw new NullPointerException("null task");
}
return exec(connection -> {
task.accept(connection);
return this;
});
}
/**
* Executes a task inside a transaction on this graph store.
*
* If a transaction is not already active on the shared repository connection, begins one and commits it on
* successful task completion; if the task throws an exception, the transaction is rolled back and the exception
* rethrown; in either case no action is taken if the transaction was already closed inside the task.
*
* @param task the task to be executed; takes as argument a connection to the backing repository of this graph
* store
* @param the type of the value returned by {@code task}
*
* @return the value returned by {@code task}
*
* @throws NullPointerException if {@code task} is {@code null}
*/
public V exec(final Function task) {
if ( task == null ) {
throw new NullPointerException("null task");
}
if ( repository == null ) {
throw new IllegalStateException("closed graph store");
}
final RepositoryConnection shared=context.get();
if ( shared != null ) {
return task.apply(shared);
} else {
if ( !repository.isInitialized() ) { repository.init(); }
try ( final RepositoryConnection connection=repository.getConnection() ) {
context.set(connection);
return task.apply(connection);
} finally {
context.remove();
}
}
}
}