build,core,plugins: Port to GDBus and GVariant
[profile/ivi/rygel.git] / src / plugins / tracker / rygel-tracker-search-container.vala
1 /*
2  * Copyright (C) 2008 Zeeshan Ali <zeenix@gmail.com>.
3  * Copyright (C) 2008 Nokia Corporation.
4  *
5  * Author: Zeeshan Ali <zeenix@gmail.com>
6  *         Ivan Frade <ivan.frade@nokia.com>
7  *
8  * This file is part of Rygel.
9  *
10  * Rygel is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU Lesser General Public License as published by
12  * the Free Software Foundation; either version 2 of the License, or
13  * (at your option) any later version.
14  *
15  * Rygel is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU Lesser General Public License for more details.
19  *
20  * You should have received a copy of the GNU Lesser General Public License
21  * along with this program; if not, write to the Free Software Foundation,
22  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23  */
24
25 using GUPnP;
26 using Gee;
27
28 /**
29  * A container listing a Tracker search result.
30  */
31 public class Rygel.Tracker.SearchContainer : Rygel.MediaContainer {
32     /* class-wide constants */
33     private const string TRACKER_SERVICE = "org.freedesktop.Tracker1";
34     private const string RESOURCES_PATH = "/org/freedesktop/Tracker1/Resources";
35
36     private const string ITEM_VARIABLE = "?item";
37     private const string MODIFIED_PREDICATE = "nfo:fileLastModified";
38     private const string MODIFIED_VARIABLE = "?modified";
39     private const string URL_PREDICATE = "nie:url";
40     private const string URL_VARIABLE = "?url";
41
42     public SelectionQuery query;
43     public ItemFactory item_factory;
44
45     private ResourcesIface resources;
46
47     public SearchContainer (string             id,
48                             MediaContainer     parent,
49                             string             title,
50                             ItemFactory        item_factory,
51                             QueryTriplets?     triplets = null,
52                             ArrayList<string>? filters = null) {
53         base (id, parent, title, 0);
54
55         this.item_factory = item_factory;
56
57         var variables = new ArrayList<string> ();
58         variables.add (ITEM_VARIABLE);
59         variables.add (URL_VARIABLE);
60
61         QueryTriplets our_triplets;
62         if (triplets != null) {
63             our_triplets = triplets;
64         } else {
65             our_triplets = new QueryTriplets ();
66         }
67
68         our_triplets.add_triplet (new QueryTriplet (ITEM_VARIABLE,
69                                                     "a",
70                                                     item_factory.category));
71         our_triplets.add_triplet (new QueryTriplet (ITEM_VARIABLE,
72                                                     MODIFIED_PREDICATE,
73                                                     MODIFIED_VARIABLE));
74         our_triplets.add_triplet (new QueryTriplet (ITEM_VARIABLE,
75                                                     URL_PREDICATE,
76                                                     URL_VARIABLE));
77
78         foreach (var chain in this.item_factory.key_chains) {
79             var variable = ITEM_VARIABLE;
80
81             foreach (var key in chain) {
82                 variable = key + "(" + variable + ")";
83             }
84
85             variables.add (variable);
86         }
87
88         this.query = new SelectionQuery (variables,
89                                          our_triplets,
90                                          filters,
91                                          MODIFIED_VARIABLE);
92
93         try {
94             this.resources = Bus.get_proxy_sync (BusType.SESSION,
95                                                  TRACKER_SERVICE,
96                                                  RESOURCES_PATH);
97
98             this.get_children_count.begin ();
99         } catch (IOError error) {
100             critical (_("Failed to connect to session bus: %s"), error.message);
101         }
102     }
103
104     public override async MediaObjects? get_children (uint         offset,
105                                                       uint         max_count,
106                                                       Cancellable? cancellable)
107                                                       throws GLib.Error {
108         var expression = new RelationalExpression ();
109         expression.op = SearchCriteriaOp.EQ;
110         expression.operand1 = "@parentID";
111         expression.operand2 = this.id;
112
113         uint total_matches;
114
115         return yield this.search (expression,
116                                   offset,
117                                   max_count,
118                                   out total_matches,
119                                   cancellable);
120     }
121
122     public override async MediaObjects? search (SearchExpression? expression,
123                                                 uint              offset,
124                                                 uint              max_count,
125                                                 out uint          total_matches,
126                                                 Cancellable?      cancellable)
127                                                 throws GLib.Error {
128         var results = new MediaObjects ();
129
130         if (expression == null || !(expression is RelationalExpression)) {
131             return yield base.search (expression,
132                                       offset,
133                                       max_count,
134                                       out total_matches,
135                                       cancellable);
136         }
137
138         var query = this.create_query (expression as RelationalExpression,
139                                        (int) offset,
140                                        (int) max_count);
141         if (query != null) {
142             yield query.execute (this.resources);
143
144             /* Iterate through all items */
145             for (uint i = 0; i < query.result.length[0]; i++) {
146                 var id = this.create_child_id_for_urn (query.result[i, 0]);
147                 var uri = query.result[i, 1];
148                 string[] metadata = this.slice_strvv_tail (query.result, i, 2);
149
150                 var item = this.item_factory.create (id, uri, this, metadata);
151                 results.add (item);
152             }
153         }
154
155         total_matches = results.size;
156
157         return results;
158     }
159
160     public override async MediaObject? find_object (string       id,
161                                                     Cancellable? cancellable)
162                                                     throws GLib.Error {
163         if (this.is_our_child (id)) {
164             return yield base.find_object (id, cancellable);
165         } else {
166             return null;
167         }
168     }
169
170     public string create_child_id_for_urn (string urn) {
171         return this.id + "," + urn;
172     }
173
174     private bool is_our_child (string id) {
175         return id.has_prefix (this.id + ",");
176     }
177
178     private async void get_children_count () {
179         try {
180             var query = new SelectionQuery.clone (this.query);
181
182             query.variables = new ArrayList<string> ();
183             query.variables.add ("COUNT(" + ITEM_VARIABLE + ") AS x");
184
185             yield query.execute (this.resources);
186
187             this.child_count = query.result[0,0].to_int ();
188             this.updated ();
189         } catch (GLib.Error error) {
190             critical (_("Error getting item count under category '%s': %s"),
191                       this.item_factory.category,
192                       error.message);
193
194             return;
195         }
196     }
197
198     private SelectionQuery? create_query (RelationalExpression? expression,
199                                           int                   offset,
200                                           int                   max_count) {
201         if (expression.operand1 == "upnp:class" &&
202             !this.item_factory.upnp_class.has_prefix (expression.operand2)) {
203             return null;
204         }
205
206         var query = new SelectionQuery.clone (this.query);
207
208         if (expression.operand1 == "@parentID") {
209             if (!expression.compare_string (this.id)) {
210                 return null;
211             }
212         } else if (expression.operand1 != "upnp:class") {
213             var filter = create_filter_for_child (expression);
214             if (filter != null) {
215                 query.filters.insert (0, filter);
216             } else {
217                 return null;
218             }
219         }
220
221         query.offset = offset;
222         query.max_count = max_count;
223
224         return query;
225     }
226
227     private string? create_filter_for_child (RelationalExpression expression) {
228         string filter = null;
229         string variable = null;
230         string value = null;
231
232         if (expression.operand1 == "@id") {
233             variable = ITEM_VARIABLE;
234
235             string parent_id;
236
237             var urn = this.get_item_info (expression.operand2, out parent_id);
238             if (urn == null || parent_id == null || parent_id != this.id) {
239                 return null;
240             }
241
242             switch (expression.op) {
243                 case SearchCriteriaOp.EQ:
244                     value = "<" + urn + ">";
245                     break;
246                 case SearchCriteriaOp.CONTAINS:
247                     value = expression.operand2;
248                     break;
249             }
250         }
251
252         if (variable == null || value == null) {
253             return null;
254         }
255
256         switch (expression.op) {
257             case SearchCriteriaOp.EQ:
258                 filter = variable + " = " + value;
259                 break;
260             case SearchCriteriaOp.CONTAINS:
261                 // We need to escape this twice for Tracker
262                 var regex = this.escape_string (Regex.escape_string (value));
263
264                 filter = "regex(" + variable + ", \"" + regex + "\", \"i\")";
265                 break;
266         }
267
268         return filter;
269     }
270
271     // Returns the URN and the ID of the parent this item belongs to, or null
272     // if item_id is invalid
273     private string? get_item_info (string     item_id,
274                                    out string parent_id) {
275         var tokens = item_id.split (",", 2);
276
277         if (tokens[0] != null && tokens[1] != null) {
278             parent_id = tokens[0];
279
280             return tokens[1];
281         } else {
282             return null;
283         }
284     }
285
286     /**
287      * Chops the tail of a particular row in a 2-dimensional string array.
288      *
289      * param strvv the 2-dimenstional string array to chop the tail of.
290      * param row the row whose tail needs to be chopped off.
291      * param index index of the first element in the tail.
292      *
293      * FIXME: Stop using it once vala supports array slicing syntax for
294      *        multi-dimentional arrays.
295      */
296     private string[] slice_strvv_tail (string[,] strvv, uint row, uint index) {
297         var slice = new string[strvv.length[1] - index];
298
299         for (var i = 0; i < slice.length; i++) {
300             slice[i] = strvv[row, i + index];
301         }
302
303         return slice;
304     }
305
306     /**
307      * tracker_sparql_escape_string:
308      * @literal: a string to escape
309      *
310      * Escapes a string so that it can be used in a SPARQL query. Copied from
311      * Tracker project.
312      *
313      * Returns: a newly-allocated string with the escaped version of @literal.
314      *  The returned string should be freed with g_free() when no longer needed.
315      */
316     private string escape_string (string literal) {
317         StringBuilder str = new StringBuilder ();
318         char *p = literal;
319
320         while (*p != '\0') {
321             size_t len = Posix.strcspn ((string) p, "\t\n\r\b\f\"\\");
322             str.append_len ((string) p, (long) len);
323             p += len;
324
325             switch (*p) {
326                 case '\t':
327                     str.append ("\\t");
328                     break;
329                 case '\n':
330                     str.append ("\\n");
331                     break;
332                 case '\r':
333                     str.append ("\\r");
334                     break;
335                 case '\b':
336                     str.append ("\\b");
337                     break;
338                 case '\f':
339                     str.append ("\\f");
340                     break;
341                 case '"':
342                     str.append ("\\\"");
343                     break;
344                 case '\\':
345                     str.append ("\\\\");
346                     break;
347                 default:
348                     continue;
349             }
350
351             p++;
352         }
353
354         return str.str;
355     }
356 }
357