package eu.fbk.knowledgestore.triplestore; import java.io.IOException; import java.util.List; import java.util.Set; import javax.annotation.Nullable; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import org.openrdf.model.Resource; import org.openrdf.model.Statement; import org.openrdf.model.URI; import org.openrdf.model.Value; import org.openrdf.query.BindingSet; import org.openrdf.query.QueryEvaluationException; import org.openrdf.query.algebra.BindingSetAssignment; import org.openrdf.query.algebra.TupleExpr; import org.openrdf.query.algebra.ValueConstant; import org.openrdf.query.algebra.Var; import org.openrdf.query.algebra.helpers.QueryModelVisitorBase; import org.openrdf.query.impl.ListBindingSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import info.aduna.iteration.CloseableIteration; import info.aduna.iteration.ConvertingIteration; import eu.fbk.knowledgestore.data.Data; import eu.fbk.knowledgestore.data.Handler; import eu.fbk.knowledgestore.internal.rdf.CompactBindingSet; /** * A {@code TripleStore} wrapper that rewrites URIs, adapting a prefix used externally in the KS * to a prefix used internally to the wrapped triple. * <p> * The wrapper intercepts calls to an underlying {@code TripleStore} and to the * {@code TripleTransaction}s it creates. It rewrites any incoming URI starting with a configured * <i>external</i> prefix, replacing that prefix with an <i>internal</i> replacement. At the same * time, the wrapper rewrites every outgoing URI starting with the <i>internal</i> prefix, * replacing that prefix with the <i>external</i> one. Rewrite occurs on every input / output data * item exchanged at the level of the {@code TripleStore} API, including RDF values, statements, * binding sets and SPARQL queries (query rewriting is done by manipulating their algebraic form). * </p> */ public final class RewritingTripleStore extends ForwardingTripleStore { private static final Logger LOGGER = LoggerFactory.getLogger(RewritingTripleStore.class); private final TripleStore delegate; private final Rewriter in; // rewriter for incoming data private final Rewriter out; // rewriter for outgoing data /** * Creates a new instance for the wrapped {@code TripleStore} specified. * * @param delegate * the wrapped {@code TripleStore} * @param internalPrefix * the internal (i.e., in the TripleStore) prefix of the URIs to rewrite * @param externalPrefix * the external (i.e., in the KS API) prefix of the URIs to rewrite */ public RewritingTripleStore(final TripleStore delegate, final String internalPrefix, final String externalPrefix) { this.delegate = Preconditions.checkNotNull(delegate); this.in = new Rewriter(externalPrefix, internalPrefix); this.out = new Rewriter(internalPrefix, externalPrefix); LOGGER.debug("{} configured", getClass().getSimpleName()); } @Override public TripleTransaction begin(final boolean readOnly) throws IOException { return new RewritingTripleTransaction(super.begin(readOnly)); } @Override protected TripleStore delegate() { return this.delegate; } private class RewritingTripleTransaction extends ForwardingTripleTransaction { private final TripleTransaction delegate; RewritingTripleTransaction(final TripleTransaction delegate) { this.delegate = Preconditions.checkNotNull(delegate); } @Override protected TripleTransaction delegate() { return this.delegate; } @Override public CloseableIteration<? extends Statement, ? extends Exception> get( @Nullable final Resource subject, @Nullable final URI predicate, @Nullable final Value object, @Nullable final Resource context) throws IOException, IllegalStateException { return RewritingTripleStore.this.out.rewriteStatements(delegate().get(// RewritingTripleStore.this.in.rewriteValue(subject), // RewritingTripleStore.this.in.rewriteValue(predicate), // RewritingTripleStore.this.in.rewriteValue(object), // RewritingTripleStore.this.in.rewriteValue(context))); } @Override public CloseableIteration<BindingSet, QueryEvaluationException> query( final SelectQuery query, @Nullable final BindingSet bindings, @Nullable final Long timeout) throws IOException, UnsupportedOperationException { return RewritingTripleStore.this.out.rewriteBindings(// ImmutableList.copyOf(query.getExpression().getBindingNames()), // super.query(RewritingTripleStore.this.in.rewriteQuery(query), RewritingTripleStore.this.in.rewriteBindings(bindings), timeout)); } @Override public void infer(@Nullable final Handler<? super Statement> handler) throws IOException, IllegalStateException { delegate().infer(RewritingTripleStore.this.out.rewriteStatements(handler)); } @Override public void add(final Iterable<? extends Statement> statements) throws IOException, IllegalStateException { delegate().add(RewritingTripleStore.this.in.rewriteStatements(statements)); } @Override public void remove(final Iterable<? extends Statement> statements) throws IOException, IllegalStateException { delegate().remove(RewritingTripleStore.this.in.rewriteStatements(statements)); } } private static class Rewriter { private final String fromPrefix; private final String toPrefix; Rewriter(final String fromPrefix, final String toPrefix) { this.fromPrefix = Preconditions.checkNotNull(fromPrefix); this.toPrefix = Preconditions.checkNotNull(toPrefix); } @Nullable SelectQuery rewriteQuery(@Nullable final SelectQuery query) { final TupleExpr expr = query.getExpression().clone(); expr.visit(new QueryModelVisitorBase<RuntimeException>() { @Override public void meet(final BindingSetAssignment node) throws RuntimeException { final List<BindingSet> bindingsList = Lists.newArrayList(); for (final BindingSet bindings : node.getBindingSets()) { bindingsList.add(rewriteBindings(bindings)); } node.setBindingSets(bindingsList); } @Override public void meet(final ValueConstant node) throws RuntimeException { node.setValue(rewriteValue(node.getValue())); } @Override public void meet(final Var node) throws RuntimeException { node.setValue(rewriteValue(node.getValue())); } // @Override // public void meet(FunctionCall node) throws RuntimeException { // } // @Override // public void meet(IRIFunction node) throws RuntimeException { // } // @Override // public void meet(Service node) throws RuntimeException { // } }); return SelectQuery.from(expr, query.getDataset()); } @Nullable <E extends Exception> CloseableIteration<BindingSet, E> rewriteBindings( final List<String> variables, @Nullable final CloseableIteration<? extends BindingSet, ? extends E> iteration) { if (iteration == null) { return null; } final CompactBindingSet.Builder builder = CompactBindingSet.builder(variables); return new ConvertingIteration<BindingSet, BindingSet, E>(iteration) { @Override protected BindingSet convert(final BindingSet bindings) throws E { for (int i = 0; i < variables.size(); ++i) { final String variable = variables.get(i); builder.set(i, rewriteValue(bindings.getValue(variable))); } return builder.build(); } }; } @Nullable BindingSet rewriteBindings(@Nullable final BindingSet bindings) { if (bindings != null) { final Set<String> names = bindings.getBindingNames(); final Value[] values = new Value[names.size()]; boolean changed = false; int index = 0; for (final String name : names) { final Value oldValue = bindings.getValue(name); final Value newValue = rewriteValue(oldValue); values[index++] = newValue; changed |= oldValue != newValue; } if (changed) { return new ListBindingSet(ImmutableList.copyOf(names), values); } } return bindings; } Handler<Statement> rewriteStatements(final Handler<? super Statement> handler) { return new Handler<Statement>() { @Override public void handle(final Statement statement) throws Throwable { handler.handle(statement == null ? null : rewriteStatement(statement)); } }; } @Nullable <E extends Exception> CloseableIteration<Statement, E> rewriteStatements( @Nullable final CloseableIteration<? extends Statement, ? extends E> iteration) { return iteration == null ? null : new ConvertingIteration<Statement, Statement, E>( iteration) { @Override protected Statement convert(final Statement statement) throws E { return rewriteStatement(statement); } }; } @Nullable Iterable<Statement> rewriteStatements( @Nullable final Iterable<? extends Statement> statements) { return statements == null ? null : Iterables.transform(statements, new Function<Statement, Statement>() { @Override @Nullable public Statement apply(@Nullable final Statement statement) { return rewriteStatement(statement); } }); } @Nullable Statement rewriteStatement(@Nullable final Statement statement) { if (statement == null) { return null; } final Resource oldSubj = statement.getSubject(); final URI oldPred = statement.getPredicate(); final Value oldObj = statement.getObject(); final Resource oldCtx = statement.getContext(); final Resource newSubj = rewriteValue(oldSubj); final URI newPred = rewriteValue(oldPred); final Value newObj = rewriteValue(oldObj); final Resource newCtx = rewriteValue(oldCtx); if (oldSubj == newSubj && oldPred == newPred && oldObj == newObj && oldCtx == newCtx) { return statement; } else if (newCtx != null) { return Data.getValueFactory().createStatement(newSubj, newPred, newObj, newCtx); } else { return Data.getValueFactory().createStatement(newSubj, newPred, newObj); } } @SuppressWarnings("unchecked") @Nullable <T extends Value> T rewriteValue(@Nullable final T value) { if (value instanceof URI) { final URI uri = (URI) value; final String string = uri.stringValue(); if (string.startsWith(this.fromPrefix)) { return (T) Data.getValueFactory().createURI( this.toPrefix + string.substring(this.fromPrefix.length())); } } return value; } } }