package net.fortytwo.sesametools.constrained;
import info.aduna.iteration.CloseableIteration;
import net.fortytwo.sesametools.CompoundCloseableIteration;
import net.fortytwo.sesametools.EmptyCloseableIteration;
import net.fortytwo.sesametools.SailConnectionTripleSource;
import org.openrdf.model.IRI;
import org.openrdf.model.Namespace;
import org.openrdf.model.Resource;
import org.openrdf.model.Statement;
import org.openrdf.model.Value;
import org.openrdf.model.ValueFactory;
import org.openrdf.query.BindingSet;
import org.openrdf.query.Dataset;
import org.openrdf.query.QueryEvaluationException;
import org.openrdf.query.algebra.TupleExpr;
import org.openrdf.query.algebra.evaluation.EvaluationStrategy;
import org.openrdf.query.algebra.evaluation.TripleSource;
import org.openrdf.query.algebra.evaluation.impl.SimpleEvaluationStrategy;
import org.openrdf.query.impl.SimpleDataset;
import org.openrdf.sail.SailConnection;
import org.openrdf.sail.SailException;
import org.openrdf.sail.helpers.SailConnectionWrapper;
import java.util.Collection;
import java.util.LinkedList;
/**
* @author Joshua Shinavier (http://fortytwo.net)
*/
public class ConstrainedSailConnection extends SailConnectionWrapper {
// For now, we won't allow inferred statements through when statements from
// specific contexts are requested. Inferred statements for queries using
// a wildcard context are allowed because they have to be checked
// after-the-fact anyway.
private static final boolean INCLUDE_INFERRED_STATEMENTS = false;
// For a wilcard removeStatements, query for all matching statements and
// remove only the ones which are in writable named graphs.
private static final boolean WILDCARD_REMOVE_FROM_ALL_CONTEXTS = true;
private boolean namespacesAreReadable;
private boolean namespacesAreWritable;
private boolean hideNonWritableContexts;
private ValueFactory valueFactory;
// For now, it is assumed that no requestor has permission to see the total
// number of statements in all contexts.
private static final boolean ALLOW_WILDCARD_SIZE = false;
// For now, it is assumed that no requestor has permission to clear
// statements from all contexts.
private static final boolean ALLOW_WILDCARD_CLEAR = false;
private Dataset readableSet;
private Dataset writableSet;
private Resource defaultWriteContext;
/**
* @param baseSailConnection a subordinate SailConnection. When this SailConnection is
* closed, it closes the subordinate collection as well.
* @param valueFactory ValueFactory from wrapped sail.
* @param readableSet all contexts from which the requestor is
* allowed to read (possibly including the null context)
* @param writableSet all contexts to or from which the
* requestor is allowed to add or delete statements. Note that while it's
* possible to add statements to the null context, it is not possible to
* remove statements from it
* @param defaultWriteContext the default context to or from which to add
* or remove statements when no other context is given. It must be a
* writable namespace to be of use
* @param namespacesAreReadable whether the requestor can see namespace
* definitions
* @param namespacesAreWritable whether the requestor can modify namespace
* definitions
* @param hideNonWritableContexts removes context information from non-writable graphs.
* @throws SailException If there is an error communicating with the base SAIL
*/
public ConstrainedSailConnection(final SailConnection baseSailConnection,
final ValueFactory valueFactory,
final Dataset readableSet,
final Dataset writableSet,
final Resource defaultWriteContext,
final boolean namespacesAreReadable,
final boolean namespacesAreWritable,
final boolean hideNonWritableContexts) throws SailException {
super(baseSailConnection);
this.valueFactory = valueFactory;
this.readableSet = readableSet;
this.writableSet = writableSet;
this.defaultWriteContext = defaultWriteContext;
this.namespacesAreReadable = namespacesAreReadable;
this.namespacesAreWritable = namespacesAreWritable;
this.hideNonWritableContexts = hideNonWritableContexts;
if (null != defaultWriteContext
&& (!writePermitted(defaultWriteContext) || !deletePermitted(defaultWriteContext))) {
this.defaultWriteContext = null;
}
}
/**
* Adds a statement to each of the given contexts for which the requestor
* has write access. If no context is given, statements will be written to
* the default write context, provided that it is writable.
*/
@Override
public void addStatement(final Resource subj,
final IRI pred,
final Value obj,
final Resource... contexts) throws SailException {
if (0 == contexts.length) {
if (writePermitted(defaultWriteContext)) {
if (null == defaultWriteContext) {
super.addStatement(subj, pred, obj);
} else {
super.addStatement(subj, pred, obj, defaultWriteContext);
}
}
} else {
for (Resource context : contexts) {
if (writePermitted(context)) {
super.addStatement(subj, pred, obj, context);
}
}
}
}
/**
* Clears the statements in all of the given contexts for which the
* requestor has write access. If no context is given, the default write
* context will be cleared, provided this is writable and not null.
*/
@Override
public void clear(final Resource... contexts) throws SailException {
if (0 == contexts.length) {
if (null != defaultWriteContext) {
if (deletePermitted(defaultWriteContext)) {
super.clear(defaultWriteContext);
}
} else if (ALLOW_WILDCARD_CLEAR) {
super.clear();
}
} else {
for (Resource context : contexts) {
if (writePermitted(context)) {
super.clear(context);
}
}
}
}
@Override
public void clearNamespaces() throws SailException {
if (namespacesAreWritable) {
super.clearNamespaces();
}
}
@Override
public CloseableIteration<? extends BindingSet, QueryEvaluationException> evaluate(
final TupleExpr tupleExpr,
final Dataset dataset,
final BindingSet bindings,
final boolean includeInferred) throws SailException {
return evaluateByGraphNames(tupleExpr, dataset, bindings, includeInferred);
//return evaluateByDecomposition(tupleExpr, dataset, bindings, includeInferred);
}
// TODO: more thorough testing involving both "FROM" and "FROM NAMED" clauses in SPARQL queries
public CloseableIteration<? extends BindingSet, QueryEvaluationException> evaluateByGraphNames(
final TupleExpr tupleExpr,
final Dataset dataset,
final BindingSet bindings,
final boolean includeInferred) throws SailException {
Dataset d;
if (null == dataset) {
d = this.readableSet;
} else {
SimpleDataset di = new SimpleDataset();
d = di;
for (IRI r : dataset.getDefaultGraphs()) {
if (this.readPermitted(r)) {
di.addDefaultGraph(r);
}
}
for (IRI r : dataset.getNamedGraphs()) {
if (this.readPermitted(r)) {
di.addNamedGraph(r);
}
}
}
return super.evaluate(tupleExpr, d, bindings, includeInferred);
}
private CloseableIteration<? extends BindingSet, QueryEvaluationException> evaluateByDecomposition(
final TupleExpr tupleExpr,
final Dataset dataset,
final BindingSet bindings,
final boolean includeInferred) throws SailException {
try {
TripleSource tripleSource = new SailConnectionTripleSource(this, valueFactory, includeInferred);
EvaluationStrategy strategy = new SimpleEvaluationStrategy(tripleSource, dataset, null);
return strategy.evaluate(tupleExpr, bindings);
} catch (QueryEvaluationException e) {
throw new SailException(e);
}
}
/**
* @return an iterator containing only those context IDs to which the
* requestor has read access (excluding the null context).
*/
@Override
public CloseableIteration<? extends Resource, SailException> getContextIDs()
throws SailException {
return new ReadableContextIteration(super.getContextIDs());
}
@Override
public String getNamespace(String prefix) throws SailException {
return namespacesAreReadable
? super.getNamespace(prefix)
: null;
}
@Override
public CloseableIteration<? extends Namespace, SailException> getNamespaces()
throws SailException {
return namespacesAreReadable
? super.getNamespaces()
: new EmptyCloseableIteration<Namespace, SailException>();
}
@Override
public CloseableIteration<? extends Statement, SailException> getStatements(
final Resource subj,
final IRI pred,
final Value obj,
final boolean includeInferred,
final Resource... contexts) throws SailException {
// Get statements from a wildcard context --> filter after retrieving
// statements.
if (0 == contexts.length) {
return new ReadableStatementIteration(
super.getStatements(subj, pred, obj, includeInferred));
}
// Get statements in specific contexts --> filter before retrieving
// statements. The statements retrieved are assumed to be in one of the
// requested contexts.
else {
Collection<CloseableIteration<? extends Statement, SailException>>
iterations = new LinkedList<>();
for (Resource context : contexts) {
if (readPermitted(context)) {
iterations.add(super.getStatements(
subj, pred, obj, INCLUDE_INFERRED_STATEMENTS, context));
}
}
return new CompoundCloseableIteration(iterations);
}
}
@Override
public void removeNamespace(final String prefix) throws SailException {
if (namespacesAreWritable) {
super.removeNamespace(prefix);
}
}
/**
* @param contexts if supplied, matching statements will be removed from
* given contexts to which the requestor has delete access. If absent,
* matching statements will be removed from the designated writeable context.
*/
@Override
public void removeStatements(final Resource subj, final IRI pred, final Value obj,
final Resource... contexts) throws SailException {
if (0 == contexts.length) {
if (WILDCARD_REMOVE_FROM_ALL_CONTEXTS) {
Collection<Resource> toRemove = new LinkedList<>();
try (CloseableIteration<? extends Statement, SailException> iter
= super.getStatements(subj, pred, obj, false)) {
while (iter.hasNext()) {
Resource context = iter.next().getContext();
if (null != context && writePermitted(context)) {
toRemove.add(context);
}
}
}
if (!toRemove.isEmpty()) {
Resource[] ctxArray = new Resource[toRemove.size()];
toRemove.toArray(ctxArray);
super.removeStatements(subj, pred, obj, ctxArray);
}
} else {
if (null != defaultWriteContext && deletePermitted(defaultWriteContext)) {
super.removeStatements(subj, pred, obj, defaultWriteContext);
}
}
// Note: there is no way to remove statements from *only* the null context
} else {
for (Resource context : contexts) {
if (deletePermitted(context)) {
super.removeStatements(subj, pred, obj, context);
}
}
}
}
@Override
public void setNamespace(final String prefix, final String name) throws SailException {
if (namespacesAreWritable) {
super.setNamespace(prefix, name);
}
}
/**
* Returns the number of readable statements in the given contexts.
*/
@Override
public long size(final Resource... contexts) throws SailException {
if (0 == contexts.length) {
return ALLOW_WILDCARD_SIZE
? super.size()
: 0;
} else {
long count = 0;
for (Resource context : contexts) {
if (readPermitted(context)) {
count += super.size(context);
}
}
return count;
}
}
public boolean readPermitted(final Resource context) throws SailException {
return context instanceof IRI && readableSet.getDefaultGraphs().contains(context);
}
public boolean writePermitted(final Resource context) throws SailException {
return context instanceof IRI && writableSet.getDefaultGraphs().contains(context);
}
public boolean deletePermitted(final Resource context) throws SailException {
return writePermitted(context);
}
private class ReadableContextIteration implements CloseableIteration<Resource, SailException> {
private CloseableIteration<? extends Resource, SailException> baseIteration;
private Resource nextContext = null;
private boolean finished = false;
public ReadableContextIteration(final CloseableIteration<? extends Resource, SailException> baseIteration) {
this.baseIteration = baseIteration;
}
public void close() throws SailException {
baseIteration.close();
}
public boolean hasNext() throws SailException {
if (finished) {
return false;
} else if (null != nextContext) {
return true;
} else {
// Break out when a query-permitted context is found or the
// end of the base iteration is reached.
while (true) {
if (!baseIteration.hasNext()) {
finished = true;
return false;
} else {
Resource r = baseIteration.next();
if (readPermitted(r)) {
nextContext = r;
return true;
}
}
}
}
}
public Resource next() throws SailException {
Resource context = nextContext;
nextContext = null;
return context;
}
/**
* Has no effect.
*/
public void remove() throws SailException {
}
}
private class ReadableStatementIteration implements CloseableIteration<Statement, SailException> {
private CloseableIteration<? extends Statement, SailException> baseIteration;
private Statement nextStatement = null;
private boolean finished = false;
public ReadableStatementIteration(final CloseableIteration<? extends Statement, SailException> baseIteration) {
this.baseIteration = baseIteration;
}
public void close() throws SailException {
baseIteration.close();
}
public boolean hasNext() throws SailException {
if (finished) {
return false;
} else if (null != nextStatement) {
return true;
} else {
// Break out when a query-permitted statement is found or the
// end of the base iteration is reached.
while (true) {
if (!baseIteration.hasNext()) {
finished = true;
return false;
} else {
Statement r = baseIteration.next();
if (readPermitted(r.getContext())) {
nextStatement = r;
return true;
}
}
}
}
}
public Statement next() throws SailException {
Statement st = nextStatement;
nextStatement = null;
if (hideNonWritableContexts) {
Resource c = st.getContext();
if (null != c && !writePermitted(c)) {
st = valueFactory.createStatement(st.getSubject(), st.getPredicate(), st.getObject());
}
}
return st;
}
/**
* Has no effect.
*/
public void remove() throws SailException {
}
}
}