gtksearchenginetracker.c 12.5 KB
Newer Older
1
/*
2
 * Copyright (C) 2009-2011 Nokia <ivan.frade@nokia.com>
3 4 5 6 7 8 9 10 11 12 13 14
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
Javier Jardón's avatar
Javier Jardón committed
15
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16
 *
17
 * Authors: Jürg Billeter <juerg.billeter@codethink.co.uk>
18
 *          Martyn Russell <martyn@lanedo.com>
19 20 21 22
 *
 * Based on nautilus-search-engine-tracker.c
 */

23
#include "config.h"
24

25 26
#include <string.h>

27 28
#include <gio/gio.h>
#include <gmodule.h>
Benjamin Otte's avatar
Benjamin Otte committed
29
#include <gdk/gdk.h>
30
#include <gtk/gtk.h>
31

32
#include "gtksearchenginetracker.h"
33

34 35 36 37 38 39 40 41 42 43 44 45 46 47
#define DBUS_SERVICE_RESOURCES   "org.freedesktop.Tracker1"
#define DBUS_PATH_RESOURCES      "/org/freedesktop/Tracker1/Resources"
#define DBUS_INTERFACE_RESOURCES "org.freedesktop.Tracker1.Resources"

#define DBUS_SERVICE_STATUS      "org.freedesktop.Tracker1"
#define DBUS_PATH_STATUS         "/org/freedesktop/Tracker1/Status"
#define DBUS_INTERFACE_STATUS    "org.freedesktop.Tracker1.Status"

/* Time in second to wait for service before deciding it's not available */
#define WAIT_TIMEOUT_SECONDS 1

/* Time in second to wait for query results to come back */
#define QUERY_TIMEOUT_SECONDS 10

48 49
/* If defined, we use fts:match, this has to be enabled in Tracker to
 * work which it usually is. The alternative is to undefine it and
50
 * use filename matching instead. This doesn’t use the content of the
51 52
 * file however.
 */
Matthias Clasen's avatar
Matthias Clasen committed
53
#define FTS_MATCHING
54 55 56 57

/*
 * GtkSearchEngineTracker object
 */
58
struct _GtkSearchEngineTrackerPrivate
59
{
60 61
  GDBusConnection *connection;
  GCancellable *cancellable;
62 63
  GtkQuery *query;
  gboolean query_pending;
64 65
};

66
G_DEFINE_TYPE_WITH_PRIVATE (GtkSearchEngineTracker, _gtk_search_engine_tracker, GTK_TYPE_SEARCH_ENGINE)
67 68 69 70 71

static void
finalize (GObject *object)
{
  GtkSearchEngineTracker *tracker;
72

73 74
  g_debug ("Finalizing GtkSearchEngineTracker");

75
  tracker = GTK_SEARCH_ENGINE_TRACKER (object);
76

77
  if (tracker->priv->cancellable)
78 79 80 81 82
    {
      g_cancellable_cancel (tracker->priv->cancellable);
      g_object_unref (tracker->priv->cancellable);
      tracker->priv->cancellable = NULL;
    }
83

84
  if (tracker->priv->query)
85 86 87 88 89
    {
      g_object_unref (tracker->priv->query);
      tracker->priv->query = NULL;
    }

90
  if (tracker->priv->connection)
91 92 93 94
    {
      g_object_unref (tracker->priv->connection);
      tracker->priv->connection = NULL;
    }
95 96 97 98

  G_OBJECT_CLASS (_gtk_search_engine_tracker_parent_class)->finalize (object);
}

99 100
static GDBusConnection *
get_connection (void)
101
{
102
  GDBusConnection *connection;
103
  GError *error = NULL;
104
  GVariant *reply;
105

106 107 108 109
  /* Normally I hate sync calls with UIs, but we need to return NULL
   * or a GtkSearchEngine as a result of this function.
   */
  connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
110

111 112
  if (error)
    {
113
      g_debug ("Couldn't connect to D-Bus session bus, %s", error->message);
114
      g_error_free (error);
115
      return NULL;
116 117
    }

118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
  /* If connection is set, we know it worked. */
  g_debug ("Finding out if Tracker is available via D-Bus...");

  /* We only wait 1 second max, we expect it to be very fast. If we
   * don't get a response by then, clearly we're replaying a journal
   * or cleaning up the DB internally. Either way, services is not
   * available.
   *
   * We use the sync call here because we don't expect to be waiting
   * long enough to block UI painting.
   */
  reply = g_dbus_connection_call_sync (connection,
                                       DBUS_SERVICE_STATUS,
                                       DBUS_PATH_STATUS,
                                       DBUS_INTERFACE_STATUS,
                                       "Wait",
                                       NULL,
                                       NULL,
                                       G_DBUS_CALL_FLAGS_NONE,
                                       WAIT_TIMEOUT_SECONDS * 1000,
                                       NULL,
                                       &error);
140

141 142 143 144 145 146 147
  if (error)
    {
      g_debug ("Tracker is not available, %s", error->message);
      g_error_free (error);
      g_object_unref (connection);
      return NULL;
    }
148

149
  g_variant_unref (reply);
150

151
  g_debug ("Tracker is ready");
152

153
  return connection;
154
}
155

156
static void
157 158 159 160
get_query_results (GtkSearchEngineTracker *engine,
                   const gchar            *sparql,
                   GAsyncReadyCallback     callback,
                   gpointer                user_data)
161
{
162 163 164 165 166 167 168 169 170 171 172 173 174
  g_dbus_connection_call (engine->priv->connection,
                          DBUS_SERVICE_RESOURCES,
                          DBUS_PATH_RESOURCES,
                          DBUS_INTERFACE_RESOURCES,
                          "SparqlQuery",
                          g_variant_new ("(s)", sparql),
                          NULL,
                          G_DBUS_CALL_FLAGS_NONE,
                          QUERY_TIMEOUT_SECONDS * 1000,
                          engine->priv->cancellable,
                          callback,
                          user_data);
}
175

176 177 178 179 180 181
/* Stolen from libtracker-common */
static GList *
string_list_to_gslist (gchar **strv)
{
  GList *list;
  gsize i;
182

183
  list = NULL;
184

185 186
  for (i = 0; strv[i]; i++)
    list = g_list_prepend (list, g_strdup (strv[i]));
187

188
  return g_list_reverse (list);
189
}
190

191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
/* Stolen from libtracker-sparql */
static gchar *
sparql_escape_string (const gchar *literal)
{
  GString *str;
  const gchar *p;

  g_return_val_if_fail (literal != NULL, NULL);

  str = g_string_new ("");
  p = literal;

  while (TRUE)
     {
      gsize len;

      if (!((*p) != '\0'))
        break;

      len = strcspn ((const gchar *) p, "\t\n\r\b\f\"\\");
      g_string_append_len (str, (const gchar *) p, (gssize) ((glong) len));
      p = p + len;

      switch (*p)
        {
        case '\t':
          g_string_append (str, "\\t");
          break;
        case '\n':
          g_string_append (str, "\\n");
          break;
        case '\r':
          g_string_append (str, "\\r");
          break;
        case '\b':
          g_string_append (str, "\\b");
          break;
        case '\f':
          g_string_append (str, "\\f");
          break;
        case '"':
          g_string_append (str, "\\\"");
          break;
        case '\\':
          g_string_append (str, "\\\\");
          break;
        default:
          continue;
        }

      p++;
     }
  return g_string_free (str, FALSE);
 }

246 247
static void
sparql_append_string_literal (GString     *sparql,
248 249
                              const gchar *str,
                              gboolean     glob)
250 251
{
  gchar *s;
Jürg Billeter's avatar
Jürg Billeter committed
252

253
  s = sparql_escape_string (str);
254

255 256
  g_string_append_c (sparql, '"');
  g_string_append (sparql, s);
257 258 259

  if (glob)
    g_string_append_c (sparql, '*');
260
  g_string_append_c (sparql, '"');
261

262
  g_free (s);
263 264
}

265 266 267 268 269 270 271
static void
sparql_append_string_literal_lower_case (GString     *sparql,
                                         const gchar *str)
{
  gchar *s;

  s = g_utf8_strdown (str, -1);
272
  sparql_append_string_literal (sparql, s, FALSE);
273 274 275
  g_free (s);
}

276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
static void
query_callback (GObject      *object,
                GAsyncResult *res,
                gpointer      user_data)
{
  GtkSearchEngineTracker *tracker;
  GList *hits;
  GVariant *reply;
  GVariant *r;
  GVariantIter iter;
  gchar **result;
  GError *error = NULL;
  gint i, n;

  tracker = GTK_SEARCH_ENGINE_TRACKER (user_data);

  tracker->priv->query_pending = FALSE;

  reply = g_dbus_connection_call_finish (tracker->priv->connection, res, &error);
  if (error)
    {
      _gtk_search_engine_error (GTK_SEARCH_ENGINE (tracker), error->message);
      g_error_free (error);
299
      g_object_unref (tracker);
300 301 302 303 304 305
      return;
    }

  if (!reply)
    {
      _gtk_search_engine_finished (GTK_SEARCH_ENGINE (tracker));
306
      g_object_unref (tracker);
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
      return;
    }

  r = g_variant_get_child_value (reply, 0);
  g_variant_iter_init (&iter, r);
  n = g_variant_iter_n_children (&iter);
  result = g_new0 (gchar *, n + 1);
  for (i = 0; i < n; i++)
    {
      GVariant *v;
      const gchar **strv;

      v = g_variant_iter_next_value (&iter);
      strv = g_variant_get_strv (v, NULL);
      result[i] = (gchar*)strv[0];
      g_free (strv);
    }

  /* We iterate result by result, not n at a time. */
  hits = string_list_to_gslist (result);
  _gtk_search_engine_hits_added (GTK_SEARCH_ENGINE (tracker), hits);
  _gtk_search_engine_finished (GTK_SEARCH_ENGINE (tracker));
  g_list_free (hits);
  g_free (result);
  g_variant_unref (reply);
  g_variant_unref (r);

334
  g_object_unref (tracker);
335 336
}

337 338 339 340
static void
gtk_search_engine_tracker_start (GtkSearchEngine *engine)
{
  GtkSearchEngineTracker *tracker;
341 342
  gchar *search_text;
  gchar *location_uri;
343
  GString *sparql;
344 345 346 347

  tracker = GTK_SEARCH_ENGINE_TRACKER (engine);

  if (tracker->priv->query_pending)
348 349 350 351
    {
      g_debug ("Attempt to start a new search while one is pending, doing nothing");
      return;
    }
352 353

  if (tracker->priv->query == NULL)
354 355 356 357
    {
      g_debug ("Attempt to start a new search with no GtkQuery, doing nothing");
      return;
    }
358

359
  search_text = _gtk_query_get_text (tracker->priv->query);
360
  location_uri = _gtk_query_get_location (tracker->priv->query);
361

362 363 364
  sparql = g_string_new ("SELECT nie:url(?urn) "
                         "WHERE {"
                         "  ?urn a nfo:FileDataObject ;"
365 366 367 368 369 370 371 372 373 374
                         "  tracker:available true ; ");

#ifdef FTS_MATCHING
  /* Using FTS: */
  g_string_append (sparql, "fts:match ");
  sparql_append_string_literal (sparql, search_text, TRUE);
#endif

  g_string_append (sparql, ". FILTER (fn:contains(fn:lower-case(nfo:fileName(?urn)),");
  sparql_append_string_literal_lower_case (sparql, search_text);
375 376

  if (location_uri)
377
    {
378 379
      g_string_append (sparql, ") && fn:starts-with(nie:url(?urn),");
      sparql_append_string_literal (sparql, location_uri, FALSE);
380
    }
381

382
  g_string_append (sparql, "))");
383

384 385 386 387
#ifdef FTS_MATCHING
  g_string_append (sparql, " } ORDER BY DESC(fts:rank(?urn)) DESC(nie:url(?urn))");
#else  /* FTS_MATCHING */
  g_string_append (sparql, "} ORDER BY DESC(nie:url(?urn)) DESC(nfo:fileName(?urn))");
388 389
#endif /* FTS_MATCHING */

390
  tracker->priv->query_pending = TRUE;
391

Matthias Clasen's avatar
Matthias Clasen committed
392 393
  g_debug ("SearchEngineTracker: query: %s", sparql->str);

394
  get_query_results (tracker, sparql->str, query_callback, g_object_ref (tracker));
395 396

  g_string_free (sparql, TRUE);
397 398 399 400 401 402 403
  g_free (search_text);
}

static void
gtk_search_engine_tracker_stop (GtkSearchEngine *engine)
{
  GtkSearchEngineTracker *tracker;
404

405
  tracker = GTK_SEARCH_ENGINE_TRACKER (engine);
406 407

  if (tracker->priv->query && tracker->priv->query_pending)
408
    {
409
      g_cancellable_cancel (tracker->priv->cancellable);
410 411 412 413 414 415 416 417 418 419 420
      tracker->priv->query_pending = FALSE;
    }
}

static gboolean
gtk_search_engine_tracker_is_indexed (GtkSearchEngine *engine)
{
  return TRUE;
}

static void
421
gtk_search_engine_tracker_set_query (GtkSearchEngine *engine,
422
                                     GtkQuery        *query)
423 424
{
  GtkSearchEngineTracker *tracker;
425

426
  tracker = GTK_SEARCH_ENGINE_TRACKER (engine);
427 428

  if (query)
429 430 431 432 433 434 435 436 437 438 439 440 441
    g_object_ref (query);

  if (tracker->priv->query)
    g_object_unref (tracker->priv->query);

  tracker->priv->query = query;
}

static void
_gtk_search_engine_tracker_class_init (GtkSearchEngineTrackerClass *class)
{
  GObjectClass *gobject_class;
  GtkSearchEngineClass *engine_class;
442

443 444
  gobject_class = G_OBJECT_CLASS (class);
  gobject_class->finalize = finalize;
445

446 447 448 449 450 451 452 453 454 455
  engine_class = GTK_SEARCH_ENGINE_CLASS (class);
  engine_class->set_query = gtk_search_engine_tracker_set_query;
  engine_class->start = gtk_search_engine_tracker_start;
  engine_class->stop = gtk_search_engine_tracker_stop;
  engine_class->is_indexed = gtk_search_engine_tracker_is_indexed;
}

static void
_gtk_search_engine_tracker_init (GtkSearchEngineTracker *engine)
{
456
  engine->priv = _gtk_search_engine_tracker_get_instance_private (engine);
457 458 459 460 461 462 463
}


GtkSearchEngine *
_gtk_search_engine_tracker_new (void)
{
  GtkSearchEngineTracker *engine;
464
  GDBusConnection *connection;
465

466
  g_debug ("--");
467

468 469 470
  connection = get_connection ();
  if (!connection)
    return NULL;
471

472
  g_debug ("Creating GtkSearchEngineTracker...");
473

474 475
  engine = g_object_new (GTK_TYPE_SEARCH_ENGINE_TRACKER, NULL);

476
  engine->priv->connection = connection;
477
  engine->priv->cancellable = g_cancellable_new ();
478
  engine->priv->query_pending = FALSE;
479

480 481
  return GTK_SEARCH_ENGINE (engine);
}