package eu.fbk.knowledgestore.server;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import org.openrdf.model.BNode;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.query.BindingSet;
import org.openrdf.query.Dataset;
import org.openrdf.query.IncompatibleOperationException;
import org.openrdf.query.MalformedQueryException;
import org.openrdf.query.QueryEvaluationException;
import org.openrdf.query.algebra.ArbitraryLengthPath;
import org.openrdf.query.algebra.BinaryTupleOperator;
import org.openrdf.query.algebra.DescribeOperator;
import org.openrdf.query.algebra.Distinct;
import org.openrdf.query.algebra.Filter;
import org.openrdf.query.algebra.Group;
import org.openrdf.query.algebra.Order;
import org.openrdf.query.algebra.Projection;
import org.openrdf.query.algebra.Service;
import org.openrdf.query.algebra.Slice;
import org.openrdf.query.algebra.StatementPattern;
import org.openrdf.query.algebra.TupleExpr;
import org.openrdf.query.algebra.ZeroLengthPath;
import org.openrdf.query.algebra.evaluation.EvaluationStrategy;
import org.openrdf.query.algebra.evaluation.QueryBindingSet;
import org.openrdf.query.algebra.evaluation.impl.EvaluationStrategyImpl;
import org.openrdf.query.algebra.evaluation.iterator.DescribeIteration;
import org.openrdf.query.algebra.helpers.QueryModelVisitorBase;
import org.openrdf.query.impl.EmptyBindingSet;
import org.openrdf.query.parser.ParsedBooleanQuery;
import org.openrdf.query.parser.ParsedGraphQuery;
import org.openrdf.query.parser.ParsedQuery;
import org.openrdf.query.parser.ParsedTupleQuery;
import org.openrdf.query.parser.sparql.ASTVisitorBase;
import org.openrdf.query.parser.sparql.BaseDeclProcessor;
import org.openrdf.query.parser.sparql.BlankNodeVarProcessor;
import org.openrdf.query.parser.sparql.DatasetDeclProcessor;
import org.openrdf.query.parser.sparql.StringEscapesProcessor;
import org.openrdf.query.parser.sparql.TupleExprBuilder;
import org.openrdf.query.parser.sparql.WildcardProjectionProcessor;
import org.openrdf.query.parser.sparql.ast.ASTAskQuery;
import org.openrdf.query.parser.sparql.ast.ASTConstructQuery;
import org.openrdf.query.parser.sparql.ast.ASTDescribeQuery;
import org.openrdf.query.parser.sparql.ast.ASTIRI;
import org.openrdf.query.parser.sparql.ast.ASTOperationContainer;
import org.openrdf.query.parser.sparql.ast.ASTPrefixDecl;
import org.openrdf.query.parser.sparql.ast.ASTQName;
import org.openrdf.query.parser.sparql.ast.ASTQuery;
import org.openrdf.query.parser.sparql.ast.ASTQueryContainer;
import org.openrdf.query.parser.sparql.ast.ASTSelectQuery;
import org.openrdf.query.parser.sparql.ast.ASTServiceGraphPattern;
import org.openrdf.query.parser.sparql.ast.ParseException;
import org.openrdf.query.parser.sparql.ast.SyntaxTreeBuilder;
import org.openrdf.query.parser.sparql.ast.SyntaxTreeBuilderTreeConstants;
import org.openrdf.query.parser.sparql.ast.TokenMgrError;
import org.openrdf.query.parser.sparql.ast.VisitorException;
import info.aduna.iteration.CloseableIteration;
import info.aduna.iteration.ConvertingIteration;
import eu.fbk.knowledgestore.data.Data;
import eu.fbk.knowledgestore.triplestore.SelectQuery;
import eu.fbk.knowledgestore.triplestore.TripleTransaction;
final class SparqlHelper {
static ParsedQuery parse(final String queryStr, final String baseURI)
throws MalformedQueryException {
try {
final ASTQueryContainer qc = SyntaxTreeBuilder.parseQuery(queryStr);
StringEscapesProcessor.process(qc);
BaseDeclProcessor.process(qc, baseURI);
final Map<String, String> prefixes = parseHelper(qc); // [FC] changed
WildcardProjectionProcessor.process(qc);
BlankNodeVarProcessor.process(qc);
if (qc.containsQuery()) {
TupleExpr tupleExpr;
final TupleExprBuilder tupleExprBuilder = new TupleExprBuilder(
Data.getValueFactory()); // [FC] changed
try {
tupleExpr = (TupleExpr) qc.jjtAccept(tupleExprBuilder, null);
} catch (final VisitorException e) {
throw new MalformedQueryException(e.getMessage(), e);
}
ParsedQuery query;
final ASTQuery queryNode = qc.getQuery();
if (queryNode instanceof ASTSelectQuery) {
query = new ParsedTupleQuery(queryStr, tupleExpr);
} else if (queryNode instanceof ASTConstructQuery) {
query = new ParsedGraphQuery(queryStr, tupleExpr, prefixes);
} else if (queryNode instanceof ASTAskQuery) {
query = new ParsedBooleanQuery(queryStr, tupleExpr);
} else if (queryNode instanceof ASTDescribeQuery) {
query = new ParsedGraphQuery(queryStr, tupleExpr, prefixes);
} else {
throw new RuntimeException("Unexpected query type: " + queryNode.getClass());
}
final Dataset dataset = DatasetDeclProcessor.process(qc);
if (dataset != null) {
query.setDataset(dataset);
}
return query;
} else {
throw new IncompatibleOperationException(
"supplied string is not a query operation");
}
} catch (final ParseException e) {
throw new MalformedQueryException(e.getMessage(), e);
} catch (final TokenMgrError e) {
throw new MalformedQueryException(e.getMessage(), e);
}
}
private static Map<String, String> parseHelper(final ASTOperationContainer qc)
throws MalformedQueryException {
final List<ASTPrefixDecl> prefixDeclList = qc.getPrefixDeclList();
final Map<String, String> prefixMap = new LinkedHashMap<String, String>();
for (final ASTPrefixDecl prefixDecl : prefixDeclList) {
final String prefix = prefixDecl.getPrefix();
final String iri = prefixDecl.getIRI().getValue();
if (prefixMap.containsKey(prefix)) {
throw new MalformedQueryException("Multiple prefix declarations for prefix '"
+ prefix + "'");
}
prefixMap.put(prefix, iri);
}
final QNameProcessor visitor = new QNameProcessor(prefixMap);
try {
qc.jjtAccept(visitor, null);
} catch (final VisitorException e) {
throw new MalformedQueryException(e);
}
return prefixMap;
}
static CloseableIteration<BindingSet, QueryEvaluationException> evaluate(
final TripleTransaction transaction, final TupleExpr expr,
@Nullable final Dataset dataset, @Nullable final BindingSet bindings,
@Nullable final Long timeout) throws QueryEvaluationException {
Preconditions.checkNotNull(transaction);
final AtomicBoolean delegate = new AtomicBoolean(false);
expr.visit(new QueryModelVisitorBase<RuntimeException>() {
@Override
public void meet(final StatementPattern node) throws RuntimeException {
delegate.set(true);
}
});
final EvaluationStrategy strategy = delegate.get() ? new DelegatingEvaluationStrategy(
transaction, dataset, timeout, false) : new LocalEvaluationStrategy(transaction,
dataset, timeout);
return strategy
.evaluate(expr, bindings != null ? bindings : EmptyBindingSet.getInstance());
}
private static Value skolemize(final Value value) {
return value instanceof BNode ? Data.getValueFactory().createURI("bnode:",
((BNode) value).getID()) : value;
}
private static BindingSet skolemize(final BindingSet bindings) {
final QueryBindingSet result = new QueryBindingSet();
for (final String name : bindings.getBindingNames()) {
result.setBinding(name, skolemize(bindings.getValue(name)));
}
return result;
}
private static CloseableIteration<BindingSet, QueryEvaluationException> skolemize(
final CloseableIteration<BindingSet, QueryEvaluationException> iter) {
return new ConvertingIteration<BindingSet, BindingSet, QueryEvaluationException>(iter) {
@Override
protected BindingSet convert(final BindingSet bindings)
throws QueryEvaluationException {
return skolemize(bindings);
}
};
}
private static Value deskolemize(final Value value) {
if (value instanceof URI) {
final URI uri = (URI) value;
if (uri.getNamespace().equals("bnode:")) {
return Data.getValueFactory().createBNode(uri.getLocalName());
}
}
return value;
}
@Nullable
private static BindingSet deskolemize(@Nullable final BindingSet bindings) {
final QueryBindingSet result = new QueryBindingSet();
for (final String name : bindings.getBindingNames()) {
result.setBinding(name, deskolemize(bindings.getValue(name)));
}
return result;
}
private static CloseableIteration<BindingSet, QueryEvaluationException> deskolemize(
final CloseableIteration<BindingSet, QueryEvaluationException> iter) {
return new ConvertingIteration<BindingSet, BindingSet, QueryEvaluationException>(iter) {
@Override
protected BindingSet convert(final BindingSet bindings)
throws QueryEvaluationException {
return deskolemize(bindings);
}
};
}
private static final class LocalEvaluationStrategy extends EvaluationStrategyImpl {
private final TripleTransaction transaction;
@Nullable
private final Long timeout;
public LocalEvaluationStrategy(final TripleTransaction transaction, final Dataset dataset,
@Nullable final Long timeout) {
super(null, dataset, null);
this.transaction = Preconditions.checkNotNull(transaction);
this.timeout = timeout;
}
@Override
public CloseableIteration<BindingSet, QueryEvaluationException> evaluate(
final DescribeOperator expr, final BindingSet bindings)
throws QueryEvaluationException {
return skolemize(new DescribeIteration(//
deskolemize(evaluate(expr.getArg(), bindings)), //
new DelegatingEvaluationStrategy(this.transaction, this.dataset, this.timeout,
true), //
expr.getBindingNames(), //
bindings));
}
}
private static final class DelegatingEvaluationStrategy extends EvaluationStrategyImpl {
private final TripleTransaction transaction;
@Nullable
private final Long timeout;
private final boolean skolemize;
public DelegatingEvaluationStrategy(final TripleTransaction transaction,
final Dataset dataset, @Nullable final Long timeout, final boolean skolemize) {
super(null, dataset, null);
this.transaction = transaction;
this.timeout = timeout;
this.skolemize = skolemize;
}
@Override
public CloseableIteration<BindingSet, QueryEvaluationException> evaluate(
final DescribeOperator expr, final BindingSet bindings)
throws QueryEvaluationException {
return skolemize(new DescribeIteration(//
deskolemize(evaluate(expr.getArg(), bindings)), //
this.skolemize ? this : new DelegatingEvaluationStrategy(this.transaction,
this.dataset, this.timeout, true), //
expr.getBindingNames(), //
bindings));
}
@Override
public CloseableIteration<BindingSet, QueryEvaluationException> evaluate(
final Projection expr, final BindingSet bindings) throws QueryEvaluationException {
return delegate(expr, bindings);
}
@Override
public CloseableIteration<BindingSet, QueryEvaluationException> evaluate(
final BinaryTupleOperator expr, final BindingSet bindings)
throws QueryEvaluationException {
return delegate(expr, bindings);
}
@Override
public CloseableIteration<BindingSet, QueryEvaluationException> evaluate(
final StatementPattern expr, final BindingSet bindings)
throws QueryEvaluationException {
return delegate(expr, bindings);
}
@Override
public CloseableIteration<BindingSet, QueryEvaluationException> evaluate(
final Filter expr, final BindingSet bindings) throws QueryEvaluationException {
return delegate(expr, bindings);
}
@Override
public CloseableIteration<BindingSet, QueryEvaluationException> evaluate(
final ZeroLengthPath expr, final BindingSet bindings)
throws QueryEvaluationException {
return delegate(expr, bindings);
}
@Override
public CloseableIteration<BindingSet, QueryEvaluationException> evaluate(
final ArbitraryLengthPath expr, final BindingSet bindings)
throws QueryEvaluationException {
return delegate(expr, bindings);
}
@Override
public CloseableIteration<BindingSet, QueryEvaluationException> evaluate(
final Service expr, final BindingSet bindings) throws QueryEvaluationException {
return delegate(expr, bindings);
}
@Override
public CloseableIteration<BindingSet, QueryEvaluationException> evaluate(final Slice expr,
final BindingSet bindings) throws QueryEvaluationException {
return delegate(expr, bindings);
}
@Override
public CloseableIteration<BindingSet, QueryEvaluationException> evaluate(
final Distinct expr, final BindingSet bindings) throws QueryEvaluationException {
return delegate(expr, bindings);
}
@Override
public CloseableIteration<BindingSet, QueryEvaluationException> evaluate(final Group expr,
final BindingSet bindings) throws QueryEvaluationException {
return delegate(expr, bindings);
}
@Override
public CloseableIteration<BindingSet, QueryEvaluationException> evaluate(final Order expr,
final BindingSet bindings) throws QueryEvaluationException {
return delegate(expr, bindings);
}
private CloseableIteration<BindingSet, QueryEvaluationException> delegate(
final TupleExpr expr, final BindingSet bindings) throws QueryEvaluationException {
try {
// Skolemize bindings if necessary
final BindingSet actualBindings = this.skolemize ? skolemize(bindings) : bindings;
// Convert the algebraic sub-tree into a SelectQuery
final SelectQuery query = SelectQuery.from(expr, this.dataset);
// Delegate to TripleStore component
return this.transaction.query(query, actualBindings, this.timeout);
} catch (final IOException ex) {
throw new QueryEvaluationException(ex);
}
}
}
private static class QNameProcessor extends ASTVisitorBase {
private final Map<String, String> prefixMap;
public QNameProcessor(final Map<String, String> prefixMap) {
this.prefixMap = prefixMap;
}
@Override
public Object visit(final ASTServiceGraphPattern node, final Object data)
throws VisitorException {
node.setPrefixDeclarations(this.prefixMap);
return super.visit(node, data);
}
@Override
public Object visit(final ASTQName qnameNode, final Object data) throws VisitorException {
final String qname = qnameNode.getValue();
final int colonIdx = qname.indexOf(':');
assert colonIdx >= 0 : "colonIdx should be >= 0: " + colonIdx;
final String prefix = qname.substring(0, colonIdx);
String localName = qname.substring(colonIdx + 1);
String namespace = this.prefixMap.get(prefix);
if (namespace == null) { // [FC] added lookup of default namespace
namespace = Data.getNamespaceMap().get(prefix);
}
if (namespace == null) {
throw new VisitorException("QName '" + qname + "' uses an undefined prefix");
}
localName = processEscapesAndHex(localName);
final ASTIRI iriNode = new ASTIRI(SyntaxTreeBuilderTreeConstants.JJTIRI);
iriNode.setValue(namespace + localName);
qnameNode.jjtReplaceWith(iriNode);
return null;
}
private String processEscapesAndHex(final String localName) {
final StringBuffer unencoded = new StringBuffer();
final Pattern hexPattern = Pattern.compile("([^\\\\]|^)(%[A-F\\d][A-F\\d])",
Pattern.CASE_INSENSITIVE);
Matcher m = hexPattern.matcher(localName);
boolean result = m.find();
while (result) {
final String previousChar = m.group(1);
final String encoded = m.group(2);
final int codePoint = Integer.parseInt(encoded.substring(1), 16);
final String decoded = String.valueOf(Character.toChars(codePoint));
m.appendReplacement(unencoded, previousChar + decoded);
result = m.find();
}
m.appendTail(unencoded);
final StringBuffer unescaped = new StringBuffer();
final Pattern escapedCharPattern = Pattern
.compile("\\\\[_~\\.\\-!\\$\\&\\'\\(\\)\\*\\+\\,\\;\\=\\:\\/\\?#\\@\\%]");
m = escapedCharPattern.matcher(unencoded.toString());
result = m.find();
while (result) {
final String escaped = m.group();
m.appendReplacement(unescaped, escaped.substring(1));
result = m.find();
}
m.appendTail(unescaped);
return unescaped.toString();
}
}
}