1   package eu.fbk.knowledgestore.triplestore;
2   
3   import java.io.ObjectStreamException;
4   import java.io.Serializable;
5   
6   import javax.annotation.Nullable;
7   
8   import com.google.common.base.Objects;
9   import com.google.common.base.Preconditions;
10  import com.google.common.cache.Cache;
11  import com.google.common.cache.CacheBuilder;
12  
13  import org.openrdf.model.Value;
14  import org.openrdf.query.BindingSet;
15  import org.openrdf.query.Dataset;
16  import org.openrdf.query.MalformedQueryException;
17  import org.openrdf.query.QueryLanguage;
18  import org.openrdf.query.algebra.TupleExpr;
19  import org.openrdf.query.algebra.Var;
20  import org.openrdf.query.algebra.helpers.QueryModelVisitorBase;
21  import org.openrdf.query.parser.ParsedTupleQuery;
22  import org.openrdf.query.parser.QueryParserUtil;
23  
24  import eu.fbk.knowledgestore.data.ParseException;
25  
26  /**
27   * A SPARQL SELECT query.
28   * <p>
29   * This class models the specification of a SPARQL SELECT query, combining in a single object both
30   * its query string representation (property {@link #getString()}) and its Sesame algebraic
31   * representation (properties {@link #getExpression()}, {@link #getDataset()}).
32   * </p>
33   * <p>
34   * A <tt>SelectQuery</tt> can be created either providing its string representation (method
35   * {@link #from(String)}) or its algebraic expression together with the query dataset (method
36   * {@link #from(TupleExpr, Dataset)}). In both case, the dual representation is automatically
37   * derived, either via parsing or rendering to SPARQL language. The two <tt>from</tt> factory
38   * methods provide for the caching and reuse of already created objects, thus reducing parsing
39   * overhead. A {@link MalformedQueryException} is thrown in case the supplied representation
40   * (either the query string or the algebraic expression) does not denote a valid SPARQL SELECT
41   * query.
42   * </p>
43   * <p>
44   * Serialization is supported, with deserialization attempting to reuse existing objects from the
45   * cache. Note that only the string representation is serialized, with the algebraic expression
46   * obtained at deserialization time via parsing.
47   * </p>
48   * <p>
49   * Instances have to considered to be immutable: while it is not possible to (efficiently) forbid
50   * modifying the query tuple expression ({@link #getExpression()}), THE ALGEBRAIC EXPRESSION MUST
51   * NOT BE MODIFIED, as this will interfere with caching.
52   * </p>
53   */
54  public final class SelectQuery implements Serializable {
55  
56      /** Version identification code for serialization. */
57      private static final long serialVersionUID = -3361485014094610488L;
58  
59      /**
60       * A cache keeping track of created instances, which may be reclaimed by the GC. Instances are
61       * indexed by their query string.
62       */
63      private static final Cache<String, SelectQuery> CACHE = CacheBuilder.newBuilder().softValues()
64              .build();
65  
66      /** The query string. */
67      private final String string;
68  
69      /** The algebraic tuple expression. */
70      private final TupleExpr expression;
71  
72      /** The optional dataset associated to the query. */
73      @Nullable
74      private final Dataset dataset;
75  
76      /**
77       * Returns a <tt>SelectQuery</tt> for the specified SPARQL SELECT query string.
78       * 
79       * @param string
80       *            the query string, in SPARQL and without relative URIs
81       * @return the corresponding <tt>SelectQuery</tt>
82       * @throws ParseException
83       *             in case the string does not denote a valid SPARQL SELECT query
84       */
85      public static SelectQuery from(final String string) throws ParseException {
86  
87          Preconditions.checkNotNull(string);
88  
89          SelectQuery query = CACHE.getIfPresent(string);
90          if (query == null) {
91              final ParsedTupleQuery parsedQuery;
92              try {
93                  parsedQuery = QueryParserUtil.parseTupleQuery(QueryLanguage.SPARQL, string, null);
94              } catch (final IllegalArgumentException ex) {
95                  throw new ParseException(string, "SPARQL query not in SELECT form", ex);
96              } catch (final MalformedQueryException ex) {
97                  throw new ParseException(string, "Invalid SPARQL query: " + ex.getMessage(), ex);
98              }
99              query = new SelectQuery(string, parsedQuery.getTupleExpr(), parsedQuery.getDataset());
100             CACHE.put(string, query);
101         }
102         return query;
103     }
104 
105     /**
106      * Returns an <tt>SelectQuery</tt> for the algebraic expression and optional dataset
107      * specified.
108      * 
109      * @param expression
110      *            the algebraic expression for the query
111      * @param dataset
112      *            the dataset optionally associated to the query
113      * @return the corresponding <tt>SelectQuery</tt> object
114      * @throws ParseException
115      *             in case the supplied algebraic expression does not denote a valid SPARQL SELECT
116      *             query
117      */
118     public static SelectQuery from(final TupleExpr expression, @Nullable final Dataset dataset)
119             throws ParseException {
120 
121         Preconditions.checkNotNull(expression);
122 
123         try {
124             // Sesame rendering facilities are definitely broken, so we use our own
125             final String string = new SPARQLRenderer(null, true).render(expression, dataset);
126             SelectQuery query = CACHE.getIfPresent(string);
127             if (query == null) {
128                 query = new SelectQuery(string, expression, dataset);
129                 CACHE.put(string, query);
130             }
131             return query;
132 
133         } catch (final Exception ex) {
134             throw new ParseException(expression.toString(),
135                     "The supplied algebraic expression does not denote a valid SPARQL query", ex);
136         }
137     }
138 
139     /**
140      * Private constructor, accepting parameters for all the object properties.
141      * 
142      * @param string
143      *            the query string
144      * @param expression
145      *            the algebraic expression
146      * @param dataset
147      *            the query dataset, null if unspecified
148      */
149     private SelectQuery(final String string, final TupleExpr expression,
150             @Nullable final Dataset dataset) {
151         this.string = string;
152         this.expression = expression;
153         this.dataset = dataset;
154     }
155 
156     /**
157      * Returns the query string. The is possibly automatically rendered from a supplied algebraic
158      * expression.
159      * 
160      * @return the query string
161      */
162     public String getString() {
163         return this.string;
164     }
165 
166     /**
167      * Returns the algebraic expression for the query - DON'T MODIFY THE RESULT. As a query
168      * expression must be cached for performance reasons, modifying it would affect all subsequent
169      * operations on the same <tt>SelectQuery</tt> object, so CLONE THE EXPRESSION BEFORE
170      * MODIFYING IT.
171      * 
172      * @return the algebraic expression for this query
173      */
174     public TupleExpr getExpression() {
175         return this.expression;
176     }
177 
178     /**
179      * Returns the dataset expressed by the FROM and FROM NAMED clauses of the query, or
180      * <tt>null</tt> if there are no such clauses.
181      * 
182      * @return the dataset, possibly null
183      */
184     @Nullable
185     public Dataset getDataset() {
186         return this.dataset;
187     }
188 
189     /**
190      * Replaces the dataset of this query with the one specified, returning the resulting
191      * <tt>SelectQuery</tt> object.
192      * 
193      * @param dataset
194      *            the new dataset; as usual, <tt>null</tt> denotes the default dataset (all the
195      *            graphs)
196      * @return the resulting <tt>SelectQuery</tt> object (possibly <tt>this</tt> if no change is
197      *         required)
198      */
199     public SelectQuery replaceDataset(@Nullable final Dataset dataset) {
200         if (Objects.equal(this.dataset, dataset)) {
201             return this;
202         } else {
203             try {
204                 return from(this.expression, dataset);
205             } catch (final ParseException ex) {
206                 throw new Error("Unexpected error - replacing dataset made the query invalid (!)",
207                         ex);
208             }
209         }
210     }
211 
212     /**
213      * Replaces some variables of this queries with the constant values specified, returning the
214      * resulting <tt>SelectQuery</tt> object.
215      * 
216      * @param bindings
217      *            the bindings to apply
218      * @return the resulting <tt>SelectQuery</tt> object (possibly <tt>this</tt> if no change is
219      *         required).
220      */
221     public SelectQuery replaceVariables(final BindingSet bindings) {
222 
223         if (bindings.size() == 0) {
224             return this;
225         }
226 
227         // TODO: check whether the visitor code (taken from BindingAssigner) is enough, especially
228         // w.r.t. variables appearing in projection nodes (= SELECT clause).
229         final TupleExpr newExpression = this.expression.clone();
230         newExpression.visit(new QueryModelVisitorBase<RuntimeException>() {
231 
232             @Override
233             public void meet(final Var var) {
234                 if (!var.hasValue() && bindings.hasBinding(var.getName())) {
235                     final Value value = bindings.getValue(var.getName());
236                     var.setValue(value);
237                 }
238             }
239 
240         });
241 
242         try {
243             return from(newExpression, this.dataset);
244         } catch (final ParseException ex) {
245             throw new Error("Unexpected error - replacing variables made the query invalid (!)",
246                     ex);
247         }
248     }
249 
250     /**
251      * {@inheritDoc} Two instances are equal if they have the same string representation.
252      */
253     @Override
254     public boolean equals(final Object object) {
255         if (object == this) {
256             return true;
257         }
258         if (!(object instanceof SelectQuery)) {
259             return false;
260         }
261         final SelectQuery other = (SelectQuery) object;
262         return this.string.equals(other.string);
263     }
264 
265     /**
266      * {@inheritDoc} The returned hash code depends only on the string representation.
267      */
268     @Override
269     public int hashCode() {
270         return this.string.hashCode();
271     }
272 
273     /**
274      * {@inheritDoc} Returns the query string.
275      */
276     @Override
277     public String toString() {
278         return this.string;
279     }
280 
281     private Object writeReplace() throws ObjectStreamException {
282         return new SerializedForm(this.string);
283     }
284 
285     private static final class SerializedForm {
286 
287         private final String string;
288 
289         SerializedForm(final String string) {
290             this.string = string;
291         }
292 
293         private Object readResolve() throws ObjectStreamException {
294             SelectQuery query = CACHE.getIfPresent(this.string);
295             if (query == null) {
296                 try {
297                     query = SelectQuery.from(this.string);
298                 } catch (final ParseException ex) {
299                     throw new Error("Serialized form denotes an invalid SPARQL queries (!)", ex);
300                 }
301             }
302             return query;
303         }
304 
305     }
306 
307 }