package eu.fbk.knowledgestore.triplestore;
import java.io.ObjectStreamException;
import java.io.Serializable;
import javax.annotation.Nullable;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.openrdf.model.Value;
import org.openrdf.query.BindingSet;
import org.openrdf.query.Dataset;
import org.openrdf.query.MalformedQueryException;
import org.openrdf.query.QueryLanguage;
import org.openrdf.query.algebra.TupleExpr;
import org.openrdf.query.algebra.Var;
import org.openrdf.query.algebra.helpers.QueryModelVisitorBase;
import org.openrdf.query.parser.ParsedTupleQuery;
import org.openrdf.query.parser.QueryParserUtil;
import eu.fbk.knowledgestore.data.ParseException;
/**
* A SPARQL SELECT query.
* <p>
* This class models the specification of a SPARQL SELECT query, combining in a single object both
* its query string representation (property {@link #getString()}) and its Sesame algebraic
* representation (properties {@link #getExpression()}, {@link #getDataset()}).
* </p>
* <p>
* A <tt>SelectQuery</tt> can be created either providing its string representation (method
* {@link #from(String)}) or its algebraic expression together with the query dataset (method
* {@link #from(TupleExpr, Dataset)}). In both case, the dual representation is automatically
* derived, either via parsing or rendering to SPARQL language. The two <tt>from</tt> factory
* methods provide for the caching and reuse of already created objects, thus reducing parsing
* overhead. A {@link MalformedQueryException} is thrown in case the supplied representation
* (either the query string or the algebraic expression) does not denote a valid SPARQL SELECT
* query.
* </p>
* <p>
* Serialization is supported, with deserialization attempting to reuse existing objects from the
* cache. Note that only the string representation is serialized, with the algebraic expression
* obtained at deserialization time via parsing.
* </p>
* <p>
* Instances have to considered to be immutable: while it is not possible to (efficiently) forbid
* modifying the query tuple expression ({@link #getExpression()}), THE ALGEBRAIC EXPRESSION MUST
* NOT BE MODIFIED, as this will interfere with caching.
* </p>
*/
public final class SelectQuery implements Serializable {
/** Version identification code for serialization. */
private static final long serialVersionUID = -3361485014094610488L;
/**
* A cache keeping track of created instances, which may be reclaimed by the GC. Instances are
* indexed by their query string.
*/
private static final Cache<String, SelectQuery> CACHE = CacheBuilder.newBuilder().softValues()
.build();
/** The query string. */
private final String string;
/** The algebraic tuple expression. */
private final TupleExpr expression;
/** The optional dataset associated to the query. */
@Nullable
private final Dataset dataset;
/**
* Returns a <tt>SelectQuery</tt> for the specified SPARQL SELECT query string.
*
* @param string
* the query string, in SPARQL and without relative URIs
* @return the corresponding <tt>SelectQuery</tt>
* @throws ParseException
* in case the string does not denote a valid SPARQL SELECT query
*/
public static SelectQuery from(final String string) throws ParseException {
Preconditions.checkNotNull(string);
SelectQuery query = CACHE.getIfPresent(string);
if (query == null) {
final ParsedTupleQuery parsedQuery;
try {
parsedQuery = QueryParserUtil.parseTupleQuery(QueryLanguage.SPARQL, string, null);
} catch (final IllegalArgumentException ex) {
throw new ParseException(string, "SPARQL query not in SELECT form", ex);
} catch (final MalformedQueryException ex) {
throw new ParseException(string, "Invalid SPARQL query: " + ex.getMessage(), ex);
}
query = new SelectQuery(string, parsedQuery.getTupleExpr(), parsedQuery.getDataset());
CACHE.put(string, query);
}
return query;
}
/**
* Returns an <tt>SelectQuery</tt> for the algebraic expression and optional dataset
* specified.
*
* @param expression
* the algebraic expression for the query
* @param dataset
* the dataset optionally associated to the query
* @return the corresponding <tt>SelectQuery</tt> object
* @throws ParseException
* in case the supplied algebraic expression does not denote a valid SPARQL SELECT
* query
*/
public static SelectQuery from(final TupleExpr expression, @Nullable final Dataset dataset)
throws ParseException {
Preconditions.checkNotNull(expression);
try {
// Sesame rendering facilities are definitely broken, so we use our own
final String string = new SPARQLRenderer(null, true).render(expression, dataset);
SelectQuery query = CACHE.getIfPresent(string);
if (query == null) {
query = new SelectQuery(string, expression, dataset);
CACHE.put(string, query);
}
return query;
} catch (final Exception ex) {
throw new ParseException(expression.toString(),
"The supplied algebraic expression does not denote a valid SPARQL query", ex);
}
}
/**
* Private constructor, accepting parameters for all the object properties.
*
* @param string
* the query string
* @param expression
* the algebraic expression
* @param dataset
* the query dataset, null if unspecified
*/
private SelectQuery(final String string, final TupleExpr expression,
@Nullable final Dataset dataset) {
this.string = string;
this.expression = expression;
this.dataset = dataset;
}
/**
* Returns the query string. The is possibly automatically rendered from a supplied algebraic
* expression.
*
* @return the query string
*/
public String getString() {
return this.string;
}
/**
* Returns the algebraic expression for the query - DON'T MODIFY THE RESULT. As a query
* expression must be cached for performance reasons, modifying it would affect all subsequent
* operations on the same <tt>SelectQuery</tt> object, so CLONE THE EXPRESSION BEFORE
* MODIFYING IT.
*
* @return the algebraic expression for this query
*/
public TupleExpr getExpression() {
return this.expression;
}
/**
* Returns the dataset expressed by the FROM and FROM NAMED clauses of the query, or
* <tt>null</tt> if there are no such clauses.
*
* @return the dataset, possibly null
*/
@Nullable
public Dataset getDataset() {
return this.dataset;
}
/**
* Replaces the dataset of this query with the one specified, returning the resulting
* <tt>SelectQuery</tt> object.
*
* @param dataset
* the new dataset; as usual, <tt>null</tt> denotes the default dataset (all the
* graphs)
* @return the resulting <tt>SelectQuery</tt> object (possibly <tt>this</tt> if no change is
* required)
*/
public SelectQuery replaceDataset(@Nullable final Dataset dataset) {
if (Objects.equal(this.dataset, dataset)) {
return this;
} else {
try {
return from(this.expression, dataset);
} catch (final ParseException ex) {
throw new Error("Unexpected error - replacing dataset made the query invalid (!)",
ex);
}
}
}
/**
* Replaces some variables of this queries with the constant values specified, returning the
* resulting <tt>SelectQuery</tt> object.
*
* @param bindings
* the bindings to apply
* @return the resulting <tt>SelectQuery</tt> object (possibly <tt>this</tt> if no change is
* required).
*/
public SelectQuery replaceVariables(final BindingSet bindings) {
if (bindings.size() == 0) {
return this;
}
// TODO: check whether the visitor code (taken from BindingAssigner) is enough, especially
// w.r.t. variables appearing in projection nodes (= SELECT clause).
final TupleExpr newExpression = this.expression.clone();
newExpression.visit(new QueryModelVisitorBase<RuntimeException>() {
@Override
public void meet(final Var var) {
if (!var.hasValue() && bindings.hasBinding(var.getName())) {
final Value value = bindings.getValue(var.getName());
var.setValue(value);
}
}
});
try {
return from(newExpression, this.dataset);
} catch (final ParseException ex) {
throw new Error("Unexpected error - replacing variables made the query invalid (!)",
ex);
}
}
/**
* {@inheritDoc} Two instances are equal if they have the same string representation.
*/
@Override
public boolean equals(final Object object) {
if (object == this) {
return true;
}
if (!(object instanceof SelectQuery)) {
return false;
}
final SelectQuery other = (SelectQuery) object;
return this.string.equals(other.string);
}
/**
* {@inheritDoc} The returned hash code depends only on the string representation.
*/
@Override
public int hashCode() {
return this.string.hashCode();
}
/**
* {@inheritDoc} Returns the query string.
*/
@Override
public String toString() {
return this.string;
}
private Object writeReplace() throws ObjectStreamException {
return new SerializedForm(this.string);
}
private static final class SerializedForm {
private final String string;
SerializedForm(final String string) {
this.string = string;
}
private Object readResolve() throws ObjectStreamException {
SelectQuery query = CACHE.getIfPresent(this.string);
if (query == null) {
try {
query = SelectQuery.from(this.string);
} catch (final ParseException ex) {
throw new Error("Serialized form denotes an invalid SPARQL queries (!)", ex);
}
}
return query;
}
}
}