gtk2/gtk/gtksearchenginetracker.c
Carlos Garnacho 0d407bcd6c searchenginetracker: Fix prefix searches
With the FTS5 query syntax, when using quotes to delimit the search phrase
the '*' token must happen after the quote, or will otherwise be considered
a character to match, go through the tokenizer, and end up ignored in
result.
2016-06-07 00:56:38 +02:00

581 lines
16 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (C) 2009-2011 Nokia <ivan.frade@nokia.com>
*
* 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
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors: Jürg Billeter <juerg.billeter@codethink.co.uk>
* Martyn Russell <martyn@lanedo.com>
*
* Based on nautilus-search-engine-tracker.c
*/
#include "config.h"
#include <string.h>
#include <gio/gio.h>
#include <gmodule.h>
#include <gdk/gdk.h>
#include <gtk/gtk.h>
#include "gtksearchenginetracker.h"
#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
/* 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
* use filename matching instead. This doesnt use the content of the
* file however.
*/
#define FTS_MATCHING
struct _GtkSearchEngineTracker
{
GtkSearchEngine parent;
GDBusConnection *connection;
GCancellable *cancellable;
GtkQuery *query;
gboolean query_pending;
GPtrArray *indexed_locations;
};
struct _GtkSearchEngineTrackerClass
{
GtkSearchEngineClass parent_class;
};
G_DEFINE_TYPE (GtkSearchEngineTracker, _gtk_search_engine_tracker, GTK_TYPE_SEARCH_ENGINE)
static void
finalize (GObject *object)
{
GtkSearchEngineTracker *tracker;
g_debug ("Finalizing GtkSearchEngineTracker");
tracker = GTK_SEARCH_ENGINE_TRACKER (object);
if (tracker->cancellable)
{
g_cancellable_cancel (tracker->cancellable);
g_object_unref (tracker->cancellable);
}
g_clear_object (&tracker->query);
g_clear_object (&tracker->connection);
g_ptr_array_unref (tracker->indexed_locations);
G_OBJECT_CLASS (_gtk_search_engine_tracker_parent_class)->finalize (object);
}
static GDBusConnection *
get_connection (void)
{
GDBusConnection *connection;
GError *error = NULL;
GVariant *reply;
/* 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);
if (error)
{
g_debug ("Couldn't connect to D-Bus session bus, %s", error->message);
g_error_free (error);
return NULL;
}
/* 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);
if (error)
{
g_debug ("Tracker is not available, %s", error->message);
g_error_free (error);
g_object_unref (connection);
return NULL;
}
g_variant_unref (reply);
g_debug ("Tracker is ready");
return connection;
}
static void
get_query_results (GtkSearchEngineTracker *engine,
const gchar *sparql,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_dbus_connection_call (engine->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->cancellable,
callback,
user_data);
}
/* 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);
}
static void
sparql_append_string_literal (GString *sparql,
const gchar *str,
gboolean glob,
gboolean is_dir_uri,
gboolean quoted)
{
gchar *s;
s = sparql_escape_string (str);
g_string_append_c (sparql, '"');
if (quoted)
g_string_append (sparql, "\\\"");
g_string_append (sparql, s);
if (is_dir_uri)
g_string_append_c (sparql, '/');
if (quoted)
g_string_append (sparql, "\\\"");
if (glob)
g_string_append_c (sparql, '*');
g_string_append_c (sparql, '"');
g_free (s);
}
static void
sparql_append_string_literal_lower_case (GString *sparql,
const gchar *str)
{
gchar *s;
s = g_utf8_strdown (str, -1);
sparql_append_string_literal (sparql, s, FALSE, FALSE, FALSE);
g_free (s);
}
static void
query_callback (GObject *object,
GAsyncResult *res,
gpointer user_data)
{
GtkSearchEngineTracker *tracker;
GList *hits;
GVariant *reply;
GVariant *r;
GVariantIter iter;
GError *error = NULL;
gint i, n;
GtkSearchHit *hit;
tracker = GTK_SEARCH_ENGINE_TRACKER (user_data);
tracker->query_pending = FALSE;
reply = g_dbus_connection_call_finish (tracker->connection, res, &error);
if (error)
{
_gtk_search_engine_error (GTK_SEARCH_ENGINE (tracker), error->message);
g_error_free (error);
g_object_unref (tracker);
return;
}
if (!reply)
{
_gtk_search_engine_finished (GTK_SEARCH_ENGINE (tracker));
g_object_unref (tracker);
return;
}
r = g_variant_get_child_value (reply, 0);
g_variant_iter_init (&iter, r);
n = g_variant_iter_n_children (&iter);
hit = g_new (GtkSearchHit, n);
hits = NULL;
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);
hit[i].file = g_file_new_for_uri (strv[0]);
hit[i].info = NULL;
g_free (strv);
hits = g_list_prepend (hits, &hit[i]);
}
_gtk_search_engine_hits_added (GTK_SEARCH_ENGINE (tracker), hits);
_gtk_search_engine_finished (GTK_SEARCH_ENGINE (tracker));
g_list_free (hits);
g_free (hit);
g_variant_unref (reply);
g_variant_unref (r);
g_object_unref (tracker);
}
static void
gtk_search_engine_tracker_start (GtkSearchEngine *engine)
{
GtkSearchEngineTracker *tracker;
const gchar *search_text;
GFile *location;
GString *sparql;
gboolean recursive;
tracker = GTK_SEARCH_ENGINE_TRACKER (engine);
if (tracker->query_pending)
{
g_debug ("Attempt to start a new search while one is pending, doing nothing");
return;
}
if (tracker->query == NULL)
{
g_debug ("Attempt to start a new search with no GtkQuery, doing nothing");
return;
}
search_text = gtk_query_get_text (tracker->query);
location = gtk_query_get_location (tracker->query);
recursive = _gtk_search_engine_get_recursive (engine);
sparql = g_string_new ("SELECT nie:url(?urn) "
"WHERE {"
" ?urn a nfo:FileDataObject ;"
" tracker:available true ; "
" nfo:belongsToContainer ?parent; ");
#ifdef FTS_MATCHING
/* Using FTS: */
g_string_append (sparql, "fts:match ");
sparql_append_string_literal (sparql, search_text, TRUE, FALSE, TRUE);
#endif
g_string_append (sparql, ". FILTER (BOUND(nie:url(?urn)) && ");
g_string_append (sparql, "fn:contains(fn:lower-case(nfo:fileName(?urn)),");
sparql_append_string_literal_lower_case (sparql, search_text);
g_string_append (sparql, ")");
if (location)
{
gchar *location_uri = g_file_get_uri (location);
g_string_append (sparql, " && ");
if (recursive)
{
g_string_append (sparql, "fn:starts-with(nie:url(?urn),");
sparql_append_string_literal (sparql, location_uri, FALSE, TRUE, FALSE);
g_string_append (sparql, ")");
}
else
{
g_string_append (sparql, "nie:url(?parent) = ");
sparql_append_string_literal (sparql, location_uri, FALSE, FALSE, FALSE);
}
g_free (location_uri);
}
g_string_append (sparql, ")");
#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))");
#endif /* FTS_MATCHING */
tracker->query_pending = TRUE;
g_debug ("SearchEngineTracker: query: %s", sparql->str);
get_query_results (tracker, sparql->str, query_callback, g_object_ref (tracker));
g_string_free (sparql, TRUE);
}
static void
gtk_search_engine_tracker_stop (GtkSearchEngine *engine)
{
GtkSearchEngineTracker *tracker;
tracker = GTK_SEARCH_ENGINE_TRACKER (engine);
if (tracker->query && tracker->query_pending)
{
g_cancellable_cancel (tracker->cancellable);
tracker->query_pending = FALSE;
}
}
static void
gtk_search_engine_tracker_set_query (GtkSearchEngine *engine,
GtkQuery *query)
{
GtkSearchEngineTracker *tracker;
tracker = GTK_SEARCH_ENGINE_TRACKER (engine);
if (query)
g_object_ref (query);
if (tracker->query)
g_object_unref (tracker->query);
tracker->query = query;
}
static void
_gtk_search_engine_tracker_class_init (GtkSearchEngineTrackerClass *class)
{
GObjectClass *gobject_class;
GtkSearchEngineClass *engine_class;
gobject_class = G_OBJECT_CLASS (class);
gobject_class->finalize = finalize;
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;
}
static void get_indexed_locations (GtkSearchEngineTracker *engine);
static void
_gtk_search_engine_tracker_init (GtkSearchEngineTracker *engine)
{
engine->cancellable = g_cancellable_new ();
engine->query_pending = FALSE;
engine->indexed_locations = g_ptr_array_new_with_free_func (g_object_unref);
get_indexed_locations (engine);
}
GtkSearchEngine *
_gtk_search_engine_tracker_new (void)
{
GtkSearchEngineTracker *engine;
GDBusConnection *connection;
g_debug ("--");
connection = get_connection ();
if (!connection)
return NULL;
g_debug ("Creating GtkSearchEngineTracker...");
engine = g_object_new (GTK_TYPE_SEARCH_ENGINE_TRACKER, NULL);
engine->connection = connection;
return GTK_SEARCH_ENGINE (engine);
}
#define TRACKER_SCHEMA "org.freedesktop.Tracker.Miner.Files"
#define TRACKER_KEY_RECURSIVE_DIRECTORIES "index-recursive-directories"
static const gchar *
get_user_special_dir_if_not_home (GUserDirectory idx)
{
const gchar *path;
path = g_get_user_special_dir (idx);
if (g_strcmp0 (path, g_get_home_dir ()) == 0)
return NULL;
return path;
}
static const gchar *
path_from_tracker_dir (const gchar *value)
{
const gchar *path;
if (g_strcmp0 (value, "&DESKTOP") == 0)
path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_DESKTOP);
else if (g_strcmp0 (value, "&DOCUMENTS") == 0)
path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_DOCUMENTS);
else if (g_strcmp0 (value, "&DOWNLOAD") == 0)
path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_DOWNLOAD);
else if (g_strcmp0 (value, "&MUSIC") == 0)
path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_MUSIC);
else if (g_strcmp0 (value, "&PICTURES") == 0)
path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_PICTURES);
else if (g_strcmp0 (value, "&PUBLIC_SHARE") == 0)
path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_PUBLIC_SHARE);
else if (g_strcmp0 (value, "&TEMPLATES") == 0)
path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_TEMPLATES);
else if (g_strcmp0 (value, "&VIDEOS") == 0)
path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_VIDEOS);
else if (g_strcmp0 (value, "$HOME") == 0)
path = g_get_home_dir ();
else
path = value;
return path;
}
static void
get_indexed_locations (GtkSearchEngineTracker *engine)
{
GSettingsSchemaSource *source;
GSettingsSchema *schema;
GSettings *settings;
gchar **locations;
gint i;
GFile *location;
const gchar *path;
source = g_settings_schema_source_get_default ();
schema = g_settings_schema_source_lookup (source, TRACKER_SCHEMA, FALSE);
if (!schema)
return;
settings = g_settings_new_full (schema, NULL, NULL);
g_settings_schema_unref (schema);
locations = g_settings_get_strv (settings, TRACKER_KEY_RECURSIVE_DIRECTORIES);
for (i = 0; locations[i] != NULL; i++)
{
path = path_from_tracker_dir (locations[i]);
if (path == NULL)
continue;
location = g_file_new_for_path (path);
g_ptr_array_add (engine->indexed_locations, location);
}
g_strfreev (locations);
g_object_unref (settings);
}
gboolean
_gtk_search_engine_tracker_is_indexed (GFile *location,
gpointer data)
{
GtkSearchEngineTracker *engine = data;
gint i;
GFile *place;
for (i = 0; i < engine->indexed_locations->len; i++)
{
place = g_ptr_array_index (engine->indexed_locations, i);
if (g_file_equal (location, place) || g_file_has_prefix (location, place))
return TRUE;
}
return FALSE;
}