/* Evolution calendar - Live search query implementation
*
* Copyright (C) 2001 Ximian, Inc.
*
* Author: Federico Mena-Quintero <federico@ximian.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public
* License as published by the Free Software Foundation.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
#include <glib.h>
#include <gtk/gtkmain.h>
#include <libgnome/gnome-i18n.h>
#include <bonobo/bonobo-exception.h>
#include <gal/widgets/e-unicode.h>
#include <e-util/e-component-listener.h>
#include <e-util/e-sexp.h>
#include <cal-util/cal-recur.h>
#include <cal-util/timeutil.h>
#include "cal-backend.h"
#include "query.h"
#include "query-backend.h"
typedef struct {
Query *query;
GNOME_Evolution_Calendar_QueryListener ql;
guint tid;
} StartCachedQueryInfo;
/* States of a query */
typedef enum {
QUERY_WAIT_FOR_BACKEND, /* the query is not populated and the backend is not loaded */
QUERY_START_PENDING, /* the query is not populated yet, but the backend is loaded */
QUERY_IN_PROGRESS, /* the query is populated; components are still being processed */
QUERY_DONE, /* the query is done, but still accepts object changes */
QUERY_PARSE_ERROR /* a parse error occurred when initially creating the ESexp */
} QueryState;
/* Private part of the Query structure */
struct _QueryPrivate {
/* The backend we are monitoring */
CalBackend *backend;
/* The cache backend */
QueryBackend *qb;
/* The default timezone for the calendar. */
icaltimezone *default_zone;
/* Listeners to which we report changes in the live query */
GList *listeners;
GList *component_listeners;
/* Sexp that defines the query */
char *sexp;
ESExp *esexp;
/* Timeout handler ID for asynchronous queries and current state of the query */
guint timeout_id;
QueryState state;
GList *cached_timeouts;
/* List of UIDs that we still have to process */
GList *pending_uids;
int n_pending;
int pending_total;
/* Table of the UIDs we know do match the query */
GHashTable *uids;
/* The next component that will be handled in e_sexp_eval(); put here
* just because the query object itself is the esexp context.
*/
CalComponent *next_comp;
};
static void query_class_init (QueryClass *class);
static void query_init (Query *query, QueryClass *class);
static void query_finalize (GObject *object);
static BonoboObjectClass *parent_class;
static GList *cached_queries = NULL;
BONOBO_TYPE_FUNC_FULL (Query,
GNOME_Evolution_Calendar_Query,
BONOBO_TYPE_OBJECT,
query);
/* Class initialization function for the live search query */
static void
query_class_init (QueryClass *class)
{
GObjectClass *object_class;
object_class = (GObjectClass *) class;
parent_class = g_type_class_peek_parent (class);
object_class->finalize = query_finalize;
/* The Query interface (ha ha! query interface!) has no methods, so we
* don't need to fiddle with the epv.
*/
}
/* Object initialization function for the live search query */
static void
query_init (Query *query, QueryClass *class)
{
QueryPrivate *priv;
priv = g_new0 (QueryPrivate, 1);
query->priv = priv;
priv->backend = NULL;
priv->qb = NULL;
priv->default_zone = NULL;
priv->listeners = NULL;
priv->component_listeners = NULL;
priv->sexp = NULL;
priv->timeout_id = 0;
priv->state = QUERY_WAIT_FOR_BACKEND;
priv->cached_timeouts = NULL;
priv->pending_uids = NULL;
priv->uids = g_hash_table_new (g_str_hash, g_str_equal);
priv->next_comp = NULL;
}
/* Used from g_hash_table_foreach(); frees a UID */
static void
free_uid_cb (gpointer key, gpointer value, gpointer data)
{
char *uid;
uid = key;
g_free (uid);
}
/* Finalize handler for the live search query */
static void
query_finalize (GObject *object)
{
Query *query;
QueryPrivate *priv;
g_return_if_fail (object != NULL);
g_return_if_fail (IS_QUERY (object));
query = QUERY (object);
priv = query->priv;
if (priv->backend) {
/* If we are waiting for the backend to be opened, we'll be
* connected to its "opened" signal. If we are in the middle of
* a query or if we are just waiting for object update
* notifications, we'll have the "obj_removed" and "obj_updated"
* connections. Otherwise, we are either in a parse error state
* or waiting for the query to be populated, and in both cases
* we have no signal connections.
*/
if (priv->state == QUERY_WAIT_FOR_BACKEND
|| priv->state == QUERY_IN_PROGRESS || priv->state == QUERY_DONE)
g_signal_handlers_disconnect_matched (G_OBJECT (priv->backend),
G_SIGNAL_MATCH_DATA,
0, 0, NULL, NULL, query);
g_object_unref (priv->backend);
priv->backend = NULL;
}
priv->qb = NULL;
if (priv->listeners != NULL) {
CORBA_Environment ev;
GList *l;
CORBA_exception_init (&ev);
for (l = priv->listeners; l != NULL; l = l->next) {
bonobo_object_release_unref (l->data, &ev);
if (BONOBO_EX (&ev))
g_message ("query_destroy(): Could not unref the listener\n");
}
CORBA_exception_free (&ev);
g_list_free (priv->listeners);
priv->listeners = NULL;
}
if (priv->component_listeners != NULL) {
g_list_foreach (priv->component_listeners, (GFunc) g_object_unref, NULL);
g_list_free (priv->component_listeners);
priv->component_listeners = NULL;
}
if (priv->sexp) {
g_free (priv->sexp);
priv->sexp = NULL;
}
if (priv->esexp) {
e_sexp_unref (priv->esexp);
priv->esexp = NULL;
}
if (priv->timeout_id) {
g_source_remove (priv->timeout_id);
priv->timeout_id = 0;
}
if (priv->cached_timeouts) {
GList *l;
for (l = priv->cached_timeouts; l != NULL; l = l->next)
g_source_remove (GPOINTER_TO_INT (l->data));
g_list_free (priv->cached_timeouts);
priv->cached_timeouts = NULL;
}
if (priv->pending_uids) {
GList *l;
for (l = priv->pending_uids; l; l = l->next) {
char *uid;
uid = l->data;
g_assert (uid != NULL);
g_free (uid);
}
g_list_free (priv->pending_uids);
priv->pending_uids = NULL;
priv->n_pending = 0;
}
g_hash_table_foreach (priv->uids, free_uid_cb, NULL);
g_hash_table_destroy (priv->uids);
priv->uids = NULL;
g_free (priv);
query->priv = NULL;
if (G_OBJECT_CLASS (parent_class)->finalize)
(* G_OBJECT_CLASS (parent_class)->finalize) (object);
}
/* E-Sexp functions */
/* (time-now)
*
* Returns a time_t of time (NULL).
*/
static ESExpResult *
func_time_now (ESExp *esexp, int argc, ESExpResult **argv, void *data)
{
ESExpResult *result;
if (argc != 0) {
e_sexp_fatal_error (esexp, _("time-now expects 0 arguments"));
return NULL;
}
result = e_sexp_result_new (esexp, ESEXP_RES_TIME);
result->value.time = time (NULL);
return result;
}
/* (make-time ISODATE)
*
* ISODATE - string, ISO 8601 date/time representation
*
* Constructs a time_t value for the specified date.
*/
static ESExpResult *
func_make_time (ESExp *esexp, int argc, ESExpResult **argv, void *data)
{
const char *str;
time_t t;
ESExpResult *result;
if (argc != 1) {
e_sexp_fatal_error (esexp, _("make-time expects 1 argument"));
return NULL;
}
if (argv[0]->type != ESEXP_RES_STRING) {
e_sexp_fatal_error (esexp, _("make-time expects argument 1 "
"to be a string"));
return NULL;
}
str = argv[0]->value.string;
t = time_from_isodate (str);
if (t == -1) {
e_sexp_fatal_error (esexp, _("make-time argument 1 must be an "
"ISO 8601 date/time string"));
return NULL;
}
result = e_sexp_result_new (esexp, ESEXP_RES_TIME);
result->value.time = t;
return result;
}
/* (time-add-day TIME N)
*
* TIME - time_t, base time
* N - int, number of days to add
*
* Adds the specified number of days to a time value.
*
* FIXME: TIMEZONES - need to use a timezone or daylight saving changes will
* make the result incorrect.
*/
static ESExpResult *
func_time_add_day (ESExp *esexp, int argc, ESExpResult **argv, void *data)
{
ESExpResult *result;
time_t t;
int n;
if (argc != 2) {
e_sexp_fatal_error (esexp, _("time-add-day expects 2 arguments"));
return NULL;
}
if (argv[0]->type != ESEXP_RES_TIME) {
e_sexp_fatal_error (esexp, _("time-add-day expects argument 1 "
"to be a time_t"));
return NULL;
}
t = argv[0]->value.time;
if (argv[1]->type != ESEXP_RES_INT) {
e_sexp_fatal_error (esexp, _("time-add-day expects argument 2 "
"to be an integer"));
return NULL;
}
n = argv[1]->value.number;
result = e_sexp_result_new (esexp, ESEXP_RES_TIME);
result->value.time = time_add_day (t, n);
return result;
}
/* (time-day-begin TIME)
*
* TIME - time_t, base time
*
* Returns the start of the day, according to the local time.
*
* FIXME: TIMEZONES - this uses the current Unix timezone.
*/
static ESExpResult *
func_time_day_begin (ESExp *esexp, int argc, ESExpResult **argv, void *data)
{
time_t t;
ESExpResult *result;
if (argc != 1) {
e_sexp_fatal_error (esexp, _("time-day-begin expects 1 argument"));
return NULL;
}
if (argv[0]->type != ESEXP_RES_TIME) {
e_sexp_fatal_error (esexp, _("time-day-begin expects argument 1 "
"to be a time_t"));
return NULL;
}
t = argv[0]->value.time;
result = e_sexp_result_new (esexp, ESEXP_RES_TIME);
result->value.time = time_day_begin (t);
return result;
}
/* (time-day-end TIME)
*
* TIME - time_t, base time
*
* Returns the end of the day, according to the local time.
*
* FIXME: TIMEZONES - this uses the current Unix timezone.
*/
static ESExpResult *
func_time_day_end (ESExp *esexp, int argc, ESExpResult **argv, void *data)
{
time_t t;
ESExpResult *result;
if (argc != 1) {
e_sexp_fatal_error (esexp, _("time-day-end expects 1 argument"));
return NULL;
}
if (argv[0]->type != ESEXP_RES_TIME) {
e_sexp_fatal_error (esexp, _("time-day-end expects argument 1 "
"to be a time_t"));
return NULL;
}
t = argv[0]->value.time;
result = e_sexp_result_new (esexp, ESEXP_RES_TIME);
result->value.time = time_day_end (t);
return result;
}
/* (get-vtype)
*
* Returns a string indicating the type of component (VEVENT, VTODO, VJOURNAL,
* VFREEBUSY, VTIMEZONE, UNKNOWN).
*/
static ESExpResult *
func_get_vtype (ESExp *esexp, int argc, ESExpResult **argv, void *data)
{
Query *query;
QueryPrivate *priv;
CalComponent *comp;
CalComponentVType vtype;
char *str;
ESExpResult *result;
query = QUERY (data);
priv = query->priv;
g_assert (priv->next_comp != NULL);
comp = priv->next_comp;
/* Check argument types */
if (argc != 0) {
e_sexp_fatal_error (esexp, _("get-vtype expects 0 arguments"));
return NULL;
}
/* Get the type */
vtype = cal_component_get_vtype (comp);
switch (vtype) {
case CAL_COMPONENT_EVENT:
str = g_strdup ("VEVENT");
break;
case CAL_COMPONENT_TODO:
str = g_strdup ("VTODO");
break;
case CAL_COMPONENT_JOURNAL:
str = g_strdup ("VJOURNAL");
break;
case CAL_COMPONENT_FREEBUSY:
str = g_strdup ("VFREEBUSY");
break;
case CAL_COMPONENT_TIMEZONE:
str = g_strdup ("VTIMEZONE");
break;
default:
str = g_strdup ("UNKNOWN");
break;
}
result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
result->value.string = str;
return result;
}
/* Sets a boolean value in the data to TRUE; called from
* cal_recur_generate_instances() to indicate that at least one instance occurs
* in the sought time range. We always return FALSE because we want the
* recurrence engine to finish as soon as possible.
*/
static gboolean
instance_occur_cb (CalComponent *comp, time_t start, time_t end, gpointer data)
{
gboolean *occurs;
occurs = data;
*occurs = TRUE;
return FALSE;
}
/* Call the backend function to get a timezone from a TZID. */
static icaltimezone*
resolve_tzid (const char *tzid, gpointer data)
{
Query *query = data;
if (!tzid || !tzid[0])
return NULL;
else
return cal_backend_get_timezone (query->priv->backend, tzid);
}
/* (occur-in-time-range? START END)
*
* START - time_t, start of the time range
* END - time_t, end of the time range
*
* Returns a boolean indicating whether the component has any occurrences in the
* specified time range.
*/
static ESExpResult *
func_occur_in_time_range (ESExp *esexp, int argc, ESExpResult **argv, void *data)
{
Query *query;
QueryPrivate *priv;
CalComponent *comp;
time_t start, end;
gboolean occurs;
ESExpResult *result;
query = QUERY (data);
priv = query->priv;
g_assert (priv->next_comp != NULL);
comp = priv->next_comp;
/* Check argument types */
if (argc != 2) {
e_sexp_fatal_error (esexp, _("occur-in-time-range? expects 2 arguments"));
return NULL;
}
if (argv[0]->type != ESEXP_RES_TIME) {
e_sexp_fatal_error (esexp, _("occur-in-time-range? expects argument 1 "
"to be a time_t"));
return NULL;
}
start = argv[0]->value.time;
if (argv[1]->type != ESEXP_RES_TIME) {
e_sexp_fatal_error (esexp, _("occur-in-time-range? expects argument 2 "
"to be a time_t"));
return NULL;
}
end = argv[1]->value.time;
/* See if there is at least one instance in that range */
occurs = FALSE;
cal_recur_generate_instances (comp, start, end,
instance_occur_cb, &occurs,
resolve_tzid, query, priv->default_zone);
result = e_sexp_result_new (esexp, ESEXP_RES_BOOL);
result->value.bool = occurs;
return result;
}
/* Returns whether a list of CalComponentText items matches the specified string */
static gboolean
matches_text_list (GSList *text_list, const char *str)
{
GSList *l;
gboolean matches;
matches = FALSE;
for (l = text_list; l; l = l->next) {
CalComponentText *text;
text = l->data;
g_assert (text->value != NULL);
if (e_utf8_strstrcasedecomp (text->value, str) != NULL) {
matches = TRUE;
break;
}
}
return matches;
}
/* Returns whether the comments in a component matches the specified string */
static gboolean
matches_comment (CalComponent *comp, const char *str)
{
GSList *list;
gboolean matches;
cal_component_get_comment_list (comp, &list);
matches = matches_text_list (list, str);
cal_component_free_text_list (list);
return matches;
}
/* Returns whether the description in a component matches the specified string */
static gboolean
matches_description (CalComponent *comp, const char *str)
{
GSList *list;
gboolean matches;
cal_component_get_description_list (comp, &list);
matches = matches_text_list (list, str);
cal_component_free_text_list (list);
return matches;
}
/* Returns whether the summary in a component matches the specified string */
static gboolean
matches_summary (CalComponent *comp, const char *str)
{
CalComponentText text;
cal_component_get_summary (comp, &text);
if (!text.value)
return FALSE;
return e_utf8_strstrcasedecomp (text.value, str) != NULL;
}
/* Returns whether any text field in a component matches the specified string */
static gboolean
matches_any (CalComponent *comp, const char *str)
{
/* As an optimization, and to make life easier for the individual
* predicate functions, see if we are looking for the empty string right
* away.
*/
if (strlen (str) == 0)
return TRUE;
return (matches_comment (comp, str)
|| matches_description (comp, str)
|| matches_summary (comp, str));
}
/* (contains? FIELD STR)
*
* FIELD - string, name of field to match (any, comment, description, summary)
* STR - string, match string
*
* Returns a boolean indicating whether the specified field contains the
* specified string.
*/
static ESExpResult *
func_contains (ESExp *esexp, int argc, ESExpResult **argv, void *data)
{
Query *query;
QueryPrivate *priv;
CalComponent *comp;
const char *field;
const char *str;
gboolean matches;
ESExpResult *result;
query = QUERY (data);
priv = query->priv;
g_assert (priv->next_comp != NULL);
comp = priv->next_comp;
/* Check argument types */
if (argc != 2) {
e_sexp_fatal_error (esexp, _("contains? expects 2 arguments"));
return NULL;
}
if (argv[0]->type != ESEXP_RES_STRING) {
e_sexp_fatal_error (esexp, _("contains? expects argument 1 "
"to be a string"));
return NULL;
}
field = argv[0]->value.string;
if (argv[1]->type != ESEXP_RES_STRING) {
e_sexp_fatal_error (esexp, _("contains? expects argument 2 "
"to be a string"));
return NULL;
}
str = argv[1]->value.string;
/* See if it matches */
if (strcmp (field, "any") == 0)
matches = matches_any (comp, str);
else if (strcmp (field, "comment") == 0)
matches = matches_comment (comp, str);
else if (strcmp (field, "description") == 0)
matches = matches_description (comp, str);
else if (strcmp (field, "summary") == 0)
matches = matches_summary (comp, str);
else {
e_sexp_fatal_error (esexp, _("contains? expects argument 1 to "
"be one of \"any\", \"summary\", \"description\""));
return NULL;
}
result = e_sexp_result_new (esexp, ESEXP_RES_BOOL);
result->value.bool = matches;
return result;
}
/* (has-categories? STR+)
* (has-categories? #f)
*
* STR - At least one string specifying a category
* Or you can specify a single #f (boolean false) value for components
* that have no categories assigned to them ("unfiled").
*
* Returns a boolean indicating whether the component has all the specified
* categories.
*/
static ESExpResult *
func_has_categories (ESExp *esexp, int argc, ESExpResult **argv, void *data)
{
Query *query;
QueryPrivate *priv;
CalComponent *comp;
gboolean unfiled;
int i;
GSList *categories;
gboolean matches;
ESExpResult *result;
query = QUERY (data);
priv = query->priv;
g_assert (priv->next_comp != NULL);
comp = priv->next_comp;
/* Check argument types */
if (argc < 1) {
e_sexp_fatal_error (esexp, _("has-categories? expects at least 1 argument"));
return NULL;
}
if (argc == 1 && argv[0]->type == ESEXP_RES_BOOL)
unfiled = TRUE;
else
unfiled = FALSE;
if (!unfiled)
for (i = 0; i < argc; i++)
if (argv[i]->type != ESEXP_RES_STRING) {
e_sexp_fatal_error (esexp, _("has-categories? expects all arguments "
"to be strings or one and only one "
"argument to be a boolean false (#f)"));
return NULL;
}
/* Search categories. First, if there are no categories we return
* whether unfiled components are supposed to match.
*/
cal_component_get_categories_list (comp, &categories);
if (!categories) {
result = e_sexp_result_new (esexp, ESEXP_RES_BOOL);
result->value.bool = unfiled;
return result;
}
/* Otherwise, we *do* have categories but unfiled components were
* requested, so this component does not match.
*/
if (unfiled) {
result = e_sexp_result_new (esexp, ESEXP_RES_BOOL);
result->value.bool = FALSE;
return result;
}
matches = TRUE;
for (i = 0; i < argc; i++) {
const char *sought;
GSList *l;
gboolean has_category;
sought = argv[i]->value.string;
has_category = FALSE;
for (l = categories; l; l = l->next) {
const char *category;
category = l->data;
if (strcmp (category, sought) == 0) {
has_category = TRUE;
break;
}
}
if (!has_category) {
matches = FALSE;
break;
}
}
cal_component_free_categories_list (categories);
result = e_sexp_result_new (esexp, ESEXP_RES_BOOL);
result->value.bool = matches;
return result;
}
/* (is-completed?)
*
* Returns a boolean indicating whether the component is completed (i.e. has
* a COMPLETED property. This is really only useful for TODO components.
*/
static ESExpResult *
func_is_completed (ESExp *esexp, int argc, ESExpResult **argv, void *data)
{
Query *query;
QueryPrivate *priv;
CalComponent *comp;
ESExpResult *result;
struct icaltimetype *t;
gboolean complete = FALSE;
query = QUERY (data);
priv = query->priv;
g_assert (priv->next_comp != NULL);
comp = priv->next_comp;
/* Check argument types */
if (argc != 0) {
e_sexp_fatal_error (esexp, _("is-completed? expects 0 arguments"));
return NULL;
}
cal_component_get_completed (comp, &t);
if (t) {
complete = TRUE;
cal_component_free_icaltimetype (t);
}
result = e_sexp_result_new (esexp, ESEXP_RES_BOOL);
result->value.bool = complete;
return result;
}
/* (completed-before? TIME)
*
* TIME - time_t
*
* Returns a boolean indicating whether the component was completed on or
* before the given time (i.e. it checks the COMPLETED property).
* This is really only useful for TODO components.
*/
static ESExpResult *
func_completed_before (ESExp *esexp, int argc, ESExpResult **argv, void *data)
{
Query *query;
QueryPrivate *priv;
CalComponent *comp;
ESExpResult *result;
struct icaltimetype *tt;
icaltimezone *zone;
gboolean retval = FALSE;
time_t before_time, completed_time;
query = QUERY (data);
priv = query->priv;
g_assert (priv->next_comp != NULL);
comp = priv->next_comp;
/* Check argument types */
if (argc != 1) {
e_sexp_fatal_error (esexp, _("completed-before? expects 1 argument"));
return NULL;
}
if (argv[0]->type != ESEXP_RES_TIME) {
e_sexp_fatal_error (esexp, _("completed-before? expects argument 1 "
"to be a time_t"));
return NULL;
}
before_time = argv[0]->value.time;
cal_component_get_completed (comp, &tt);
if (tt) {
/* COMPLETED must be in UTC. */
zone = icaltimezone_get_utc_timezone ();
completed_time = icaltime_as_timet_with_zone (*tt, zone);
#if 0
g_print ("Query Time : %s", ctime (&before_time));
g_print ("Completed Time: %s", ctime (&completed_time));
#endif
/* We want to return TRUE if before_time is after
completed_time. */
if (difftime (before_time, completed_time) > 0) {
#if 0
g_print (" Returning TRUE\n");
#endif
retval = TRUE;
}
cal_component_free_icaltimetype (tt);
}
result = e_sexp_result_new (esexp, ESEXP_RES_BOOL);
result->value.bool = retval;
return result;
}
/* Adds a component to our the UIDs hash table and notifies the client */
static void
add_component (Query *query, const char *uid, gboolean query_in_progress, int n_scanned, int total)
{
QueryPrivate *priv;
char *old_uid;
CORBA_Environment ev;
GList *l;
if (query_in_progress)
g_assert (n_scanned > 0 || n_scanned <= total);
priv = query->priv;
if (g_hash_table_lookup_extended (priv->uids, uid, (gpointer *) &old_uid, NULL)) {
g_hash_table_remove (priv->uids, old_uid);
g_free (old_uid);
}
g_hash_table_insert (priv->uids, g_strdup (uid), NULL);
CORBA_exception_init (&ev);
for (l = priv->listeners; l != NULL; l = l->next) {
GNOME_Evolution_Calendar_CalObjUIDSeq *corba_uids;
corba_uids = GNOME_Evolution_Calendar_CalObjUIDSeq__alloc ();
CORBA_sequence_set_release (corba_uids, TRUE);
corba_uids->_buffer = CORBA_sequence_GNOME_Evolution_Calendar_CalObjUID_allocbuf (1);
corba_uids->_length = 1;
corba_uids->_buffer[0] = CORBA_string_dup (uid);
GNOME_Evolution_Calendar_QueryListener_notifyObjUpdated (
l->data,
corba_uids,
query_in_progress,
n_scanned,
total,
&ev);
if (BONOBO_EX (&ev))
g_message ("add_component(): Could not notify the listener of an "
"updated component");
CORBA_free (corba_uids);
}
CORBA_exception_free (&ev);
}
/* Removes a component from our the UIDs hash table and notifies the client */
static void
remove_component (Query *query, const char *uid)
{
QueryPrivate *priv;
char *old_uid;
CORBA_Environment ev;
GList *l;
priv = query->priv;
if (!g_hash_table_lookup_extended (priv->uids, uid, (gpointer *) &old_uid, NULL))
return;
/* The component did match the query before but it no longer does, so we
* have to notify the client.
*/
g_hash_table_remove (priv->uids, old_uid);
g_free (old_uid);
CORBA_exception_init (&ev);
for (l = priv->listeners; l != NULL; l = l->next) {
GNOME_Evolution_Calendar_QueryListener_notifyObjRemoved (
l->data,
(char *) uid,
&ev);
if (BONOBO_EX (&ev))
g_message ("remove_component(): Could not notify the listener of a "
"removed component");
}
CORBA_exception_free (&ev);
}
/* Removes a component from the list of pending UIDs */
static void
remove_from_pending (Query *query, const char *remove_uid)
{
QueryPrivate *priv;
GList *l;
priv = query->priv;
for (l = priv->pending_uids; l; l = l->next) {
char *uid;
g_assert (priv->n_pending > 0);
uid = l->data;
if (strcmp (remove_uid, uid))
continue;
g_free (uid);
priv->pending_uids = g_list_remove_link (priv->pending_uids, l);
g_list_free_1 (l);
priv->n_pending--;
g_assert ((priv->pending_uids && priv->n_pending != 0)
|| (!priv->pending_uids && priv->n_pending == 0));
break;
}
}
static struct {
char *name;
ESExpFunc *func;
} functions[] = {
/* Time-related functions */
{ "time-now", func_time_now },
{ "make-time", func_make_time },
{ "time-add-day", func_time_add_day },
{ "time-day-begin", func_time_day_begin },
{ "time-day-end", func_time_day_end },
/* Component-related functions */
{ "get-vtype", func_get_vtype },
{ "occur-in-time-range?", func_occur_in_time_range },
{ "contains?", func_contains },
{ "has-categories?", func_has_categories },
{ "is-completed?", func_is_completed },
{ "completed-before?", func_completed_before }
};
/* Initializes a sexp by interning our own symbols */
static ESExp *
create_sexp (Query *query)
{
ESExp *esexp;
int i;
esexp = e_sexp_new ();
for (i = 0; i < (sizeof (functions) / sizeof (functions[0])); i++)
e_sexp_add_function (esexp, 0, functions[i].name, functions[i].func, query);
return esexp;
}
/* Creates the ESexp and parses the esexp. If a parse error occurs, it sets the
* query state to QUERY_PARSE_ERROR and returns FALSE.
*/
static gboolean
parse_sexp (Query *query)
{
QueryPrivate *priv;
priv = query->priv;
/* Compile the query string */
priv->esexp = create_sexp (query);
g_assert (priv->sexp != NULL);
e_sexp_input_text (priv->esexp, priv->sexp, strlen (priv->sexp));
if (e_sexp_parse (priv->esexp) == -1) {
const char *error_str;
CORBA_Environment ev;
GList *l;
priv->state = QUERY_PARSE_ERROR;
/* Report the error to the listeners */
error_str = e_sexp_error (priv->esexp);
g_assert (error_str != NULL);
CORBA_exception_init (&ev);
for (l = priv->listeners; l != NULL; l = l->next) {
GNOME_Evolution_Calendar_QueryListener_notifyQueryDone (
l->data,
GNOME_Evolution_Calendar_QueryListener_PARSE_ERROR,
error_str,
&ev);
if (BONOBO_EX (&ev))
g_message ("parse_sexp(): Could not notify the listener of "
"a parse error");
}
CORBA_exception_free (&ev);
e_sexp_unref (priv->esexp);
priv->esexp = NULL;
/* remove the query from the list of cached queries */
cached_queries = g_list_remove (cached_queries, query);
bonobo_object_unref (BONOBO_OBJECT (query));
return FALSE;
}
return TRUE;
}
/* Evaluates the query sexp on the specified component and notifies the listener
* as appropriate.
*/
static void
match_component (Query *query, const char *uid,
gboolean query_in_progress, int n_scanned, int total)
{
QueryPrivate *priv;
CalComponent *comp;
ESExpResult *result;
priv = query->priv;
g_assert (priv->state == QUERY_IN_PROGRESS || priv->state == QUERY_DONE);
g_assert (priv->esexp != NULL);
comp = query_backend_get_object_component (priv->qb, uid);
if (!comp)
return;
/* Eval the sexp */
g_assert (priv->next_comp == NULL);
priv->next_comp = comp;
result = e_sexp_eval (priv->esexp);
priv->next_comp = NULL;
if (!result) {
const char *error_str;
CORBA_Environment ev;
GList *l;
error_str = e_sexp_error (priv->esexp);
g_assert (error_str != NULL);
CORBA_exception_init (&ev);
for (l = priv->listeners; l != NULL; l = l->next) {
GNOME_Evolution_Calendar_QueryListener_notifyEvalError (
l->data,
error_str,
&ev);
if (BONOBO_EX (&ev))
g_message ("match_component(): Could not notify the listener of "
"an evaluation error");
}
CORBA_exception_free (&ev);
return;
} else if (result->type != ESEXP_RES_BOOL) {
CORBA_Environment ev;
GList *l;
CORBA_exception_init (&ev);
for (l = priv->listeners; l != NULL; l = l->next) {
GNOME_Evolution_Calendar_QueryListener_notifyEvalError (
l->data,
_("Evaluation of the search expression did not yield a boolean value"),
&ev);
if (BONOBO_EX (&ev))
g_message ("match_component(): Could not notify the listener of "
"an unexpected result value type when evaluating the "
"search expression");
}
CORBA_exception_free (&ev);
} else {
/* Success; process the component accordingly */
if (result->value.bool)
add_component (query, uid, query_in_progress, n_scanned, total);
else
remove_component (query, uid);
}
e_sexp_result_free (priv->esexp, result);
}
/* Processes all components that are queued in the list */
static gboolean
process_components_cb (gpointer data)
{
Query *query;
QueryPrivate *priv;
char *uid;
GList *l;
CORBA_Environment ev;
query = QUERY (data);
priv = query->priv;
g_source_remove (priv->timeout_id);
priv->timeout_id = 0;
bonobo_object_ref (BONOBO_OBJECT (query));
while (priv->pending_uids) {
g_assert (priv->n_pending > 0);
/* Fetch the component */
l = priv->pending_uids;
priv->pending_uids = g_list_remove_link (priv->pending_uids, l);
priv->n_pending--;
g_assert ((priv->pending_uids && priv->n_pending != 0)
|| (!priv->pending_uids && priv->n_pending == 0));
uid = l->data;
g_assert (uid != NULL);
g_list_free_1 (l);
match_component (query, uid,
TRUE,
priv->pending_total - priv->n_pending,
priv->pending_total);
g_free (uid);
/* run the main loop, for not blocking */
if (gtk_events_pending ())
gtk_main_iteration ();
}
bonobo_object_unref (BONOBO_OBJECT (query));
if (!priv || !priv->listeners)
return FALSE;
/* notify listeners that the query ended */
priv->state = QUERY_DONE;
CORBA_exception_init (&ev);
for (l = priv->listeners; l != NULL; l = l->next) {
GNOME_Evolution_Calendar_QueryListener_notifyQueryDone (
l->data,
GNOME_Evolution_Calendar_QueryListener_SUCCESS,
"",
&ev);
if (BONOBO_EX (&ev))
g_message ("start_query(): Could not notify the listener of "
"a finished query");
}
CORBA_exception_free (&ev);
return FALSE;
}
/* Callback used when a component changes in the backend */
static void
backend_obj_updated_cb (CalBackend *backend, const char *uid, gpointer data)
{
Query *query;
QueryPrivate *priv;
query = QUERY (data);
priv = query->priv;
g_assert (priv->state == QUERY_IN_PROGRESS || priv->state == QUERY_DONE);
bonobo_object_ref (BONOBO_OBJECT (query));
match_component (query, uid, FALSE, 0, 0);
remove_from_pending (query, uid);
bonobo_object_unref (BONOBO_OBJECT (query));
}
/* Callback used when a component is removed from the backend */
static void
backend_obj_removed_cb (CalBackend *backend, const char *uid, gpointer data)
{
Query *query;
QueryPrivate *priv;
query = QUERY (data);
priv = query->priv;
g_assert (priv->state == QUERY_IN_PROGRESS || priv->state == QUERY_DONE);
bonobo_object_ref (BONOBO_OBJECT (query));
remove_component (query, uid);
remove_from_pending (query, uid);
bonobo_object_unref (BONOBO_OBJECT (query));
}
/* Actually starts the query */
static void
start_query (Query *query)
{
QueryPrivate *priv;
priv = query->priv;
if (!parse_sexp (query))
return;
/* Populate the query with UIDs so that we can process them asynchronously */
priv->state = QUERY_IN_PROGRESS;
priv->pending_uids = query_backend_get_uids (priv->qb, CALOBJ_TYPE_ANY);
priv->pending_total = g_list_length (priv->pending_uids);
priv->n_pending = priv->pending_total;
g_signal_connect (G_OBJECT (priv->backend), "obj_updated",
G_CALLBACK (backend_obj_updated_cb),
query);
g_signal_connect (G_OBJECT (priv->backend), "obj_removed",
G_CALLBACK (backend_obj_removed_cb),
query);
priv->timeout_id = g_timeout_add (100, (GSourceFunc) process_components_cb, query);
}
/* Idle handler for starting a query */
static gboolean
start_query_cb (gpointer data)
{
Query *query;
QueryPrivate *priv;
query = QUERY (data);
priv = query->priv;
g_source_remove (priv->timeout_id);
priv->timeout_id = 0;
if (priv->state == QUERY_START_PENDING) {
priv->state = QUERY_IN_PROGRESS;
start_query (query);
}
return FALSE;
}
static void
listener_died_cb (EComponentListener *cl, gpointer data)
{
QueryPrivate *priv;
Query *query = QUERY (data);
GNOME_Evolution_Calendar_QueryListener ql;
CORBA_Environment ev;
priv = query->priv;
ql = e_component_listener_get_component (cl);
priv->listeners = g_list_remove (priv->listeners, ql);
priv->component_listeners = g_list_remove (priv->component_listeners, cl);
g_object_unref (cl);
CORBA_exception_init (&ev);
bonobo_object_release_unref (ql, &ev);
if (BONOBO_EX (&ev))
g_message ("query_destroy(): Could not unref the listener\n");
CORBA_exception_free (&ev);
}
static void
add_uid_cb (gpointer key, gpointer value, gpointer data)
{
char *uid = (char *) key;
GList **uidlist = (GList **) data;
*uidlist = g_list_append (*uidlist, uid);
}
/* Idle handler for starting a cached query */
static gboolean
start_cached_query_cb (gpointer data)
{
CORBA_Environment ev;
QueryPrivate *priv;
EComponentListener *cl;
StartCachedQueryInfo *info = (StartCachedQueryInfo *) data;
priv = info->query->priv;
g_source_remove (info->tid);
priv->cached_timeouts = g_list_remove (priv->cached_timeouts,
GINT_TO_POINTER (info->tid));
/* if the query hasn't started yet, we add the listener */
if (priv->state == QUERY_START_PENDING ||
priv->state == QUERY_WAIT_FOR_BACKEND) {
priv->listeners = g_list_append (priv->listeners, info->ql);
cl = e_component_listener_new (info->ql, 0);
priv->component_listeners = g_list_append (priv->component_listeners, cl);
g_signal_connect (G_OBJECT (cl), "component_died",
G_CALLBACK (listener_died_cb), info->query);
} else if (priv->state == QUERY_IN_PROGRESS) {
/* if it's in progress, we re-add the timeout */
info->tid = g_timeout_add (100, (GSourceFunc) start_cached_query_cb, info);
priv->cached_timeouts = g_list_append (priv->cached_timeouts,
GINT_TO_POINTER (info->tid));
return FALSE;
} else if (priv->state == QUERY_PARSE_ERROR) {
/* notify listener of error */
CORBA_exception_init (&ev);
GNOME_Evolution_Calendar_QueryListener_notifyQueryDone (
info->ql,
GNOME_Evolution_Calendar_QueryListener_PARSE_ERROR,
_("Parse error"),
&ev);
if (BONOBO_EX (&ev))
g_message ("start_cached_query_cb(): Could not notify the listener of "
"a parse error");
CORBA_exception_free (&ev);
/* remove all traces of this query */
cached_queries = g_list_remove (cached_queries, info->query);
bonobo_object_unref (BONOBO_OBJECT (info->query));
} else if (priv->state == QUERY_DONE) {
int len;
GList *uid_list = NULL, *l;
CORBA_exception_init (&ev);
/* if the query is done, then we just notify the listener of all the
* UIDS we've got so far, all at once */
g_hash_table_foreach (priv->uids, (GHFunc) add_uid_cb, &uid_list);
len = g_list_length (uid_list);
if (len > 0) {
int n;
GNOME_Evolution_Calendar_CalObjUIDSeq *corba_uids;
corba_uids = GNOME_Evolution_Calendar_CalObjUIDSeq__alloc ();
corba_uids->_length = len;
corba_uids->_maximum = len;
corba_uids->_buffer = CORBA_sequence_GNOME_Evolution_Calendar_CalObjUID_allocbuf (len);
CORBA_sequence_set_release (corba_uids, TRUE);
for (l = uid_list, n = 0; l != NULL; l = l->next, n++)
corba_uids->_buffer[n] = CORBA_string_dup ((CORBA_char *) l->data);
GNOME_Evolution_Calendar_QueryListener_notifyObjUpdated (
info->ql,
corba_uids,
TRUE,
len,
len, &ev);
if (BONOBO_EX (&ev))
g_message ("start_cached_query_cb(): Could not notify the listener of all "
"cached components");
CORBA_free (corba_uids);
g_list_free (uid_list);
}
/* setup private data and notify listener that the query ended */
priv->listeners = g_list_append (priv->listeners, info->ql);
cl = e_component_listener_new (info->ql, 0);
priv->component_listeners = g_list_append (priv->component_listeners, cl);
g_signal_connect (G_OBJECT (cl), "component_died",
G_CALLBACK (listener_died_cb), info->query);
GNOME_Evolution_Calendar_QueryListener_notifyQueryDone (
info->ql,
GNOME_Evolution_Calendar_QueryListener_SUCCESS,
"",
&ev);
if (BONOBO_EX (&ev))
g_message ("start_cached_query_cb(): Could not notify the listener of "
"a finished query");
CORBA_exception_free (&ev);
}
g_free (info);
return FALSE;
}
/* Callback used when the backend gets loaded; we just queue the query to be
* started later.
*/
static void
backend_opened_cb (CalBackend *backend, CalBackendOpenStatus status, gpointer data)
{
Query *query;
QueryPrivate *priv;
query = QUERY (data);
priv = query->priv;
g_assert (priv->state == QUERY_WAIT_FOR_BACKEND);
g_signal_handlers_disconnect_matched (G_OBJECT (priv->backend),
G_SIGNAL_MATCH_DATA,
0, 0, NULL, NULL, query);
priv->state = QUERY_START_PENDING;
if (status == CAL_BACKEND_OPEN_SUCCESS) {
g_assert (cal_backend_is_loaded (backend));
priv->timeout_id = g_timeout_add (100, (GSourceFunc) start_query_cb, query);
}
}
/* Callback used when the backend for a cached query is destroyed */
static void
backend_destroyed_cb (GObject *object, gpointer data)
{
Query *query;
query = QUERY (data);
cached_queries = g_list_remove (cached_queries, query);
bonobo_object_unref (BONOBO_OBJECT (query));
}
/**
* query_construct:
* @query: A live search query.
* @backend: Calendar backend that the query object will monitor.
* @ql: Listener for query results.
* @sexp: Sexp that defines the query.
*
* Constructs a #Query object by binding it to a calendar backend and a query
* listener. The @query object will start to populate itself asynchronously and
* call the listener as appropriate.
*
* Return value: The same value as @query, or NULL if the query could not
* be constructed.
**/
Query *
query_construct (Query *query,
CalBackend *backend,
GNOME_Evolution_Calendar_QueryListener ql,
const char *sexp)
{
QueryPrivate *priv;
CORBA_Environment ev;
EComponentListener *cl;
g_return_val_if_fail (query != NULL, NULL);
g_return_val_if_fail (IS_QUERY (query), NULL);
g_return_val_if_fail (backend != NULL, NULL);
g_return_val_if_fail (IS_CAL_BACKEND (backend), NULL);
g_return_val_if_fail (ql != CORBA_OBJECT_NIL, NULL);
g_return_val_if_fail (sexp != NULL, NULL);
priv = query->priv;
CORBA_exception_init (&ev);
priv->listeners = g_list_append (NULL, CORBA_Object_duplicate (ql, &ev));
if (BONOBO_EX (&ev)) {
g_message ("query_construct(): Could not duplicate the listener");
priv->listeners = NULL;
CORBA_exception_free (&ev);
return NULL;
}
CORBA_exception_free (&ev);
cl = e_component_listener_new (ql, 0);
priv->component_listeners = g_list_append (priv->component_listeners, cl);
g_signal_connect (G_OBJECT (cl), "component_died",
G_CALLBACK (listener_died_cb), query);
priv->backend = backend;
g_object_ref (priv->backend);
priv->qb = query_backend_new (query, backend);
priv->default_zone = cal_backend_get_default_timezone (backend);
priv->sexp = g_strdup (sexp);
/* Queue the query to be started asynchronously */
if (cal_backend_is_loaded (priv->backend)) {
priv->state = QUERY_START_PENDING;
priv->timeout_id = g_timeout_add (100, (GSourceFunc) start_query_cb, query);
} else
g_signal_connect (G_OBJECT (priv->backend), "opened",
G_CALLBACK (backend_opened_cb),
query);
return query;
}
/**
* query_new:
* @backend: Calendar backend that the query object will monitor.
* @ql: Listener for query results.
* @sexp: Sexp that defines the query.
*
* Creates a new query engine object that monitors a calendar backend.
*
* Return value: A newly-created query object, or NULL on failure.
**/
Query *
query_new (CalBackend *backend,
GNOME_Evolution_Calendar_QueryListener ql,
const char *sexp)
{
Query *query;
GList *l;
/* first, see if we've got this query in our cache */
for (l = cached_queries; l != NULL; l = l->next) {
query = QUERY (l->data);
g_assert (query != NULL);
if (query->priv->backend == backend &&
!strcmp (query->priv->sexp, sexp)) {
StartCachedQueryInfo *info;
CORBA_Environment ev;
info = g_new0 (StartCachedQueryInfo, 1);
info->query = query;
CORBA_exception_init (&ev);
info->ql = CORBA_Object_duplicate (ql, &ev);
if (BONOBO_EX (&ev)) {
g_message ("query_new(): Could not duplicate listener object");
g_free (info);
return NULL;
}
CORBA_exception_free (&ev);
info->tid = g_timeout_add (100, (GSourceFunc) start_cached_query_cb, info);
query->priv->cached_timeouts = g_list_append (query->priv->cached_timeouts,
GINT_TO_POINTER (info->tid));
bonobo_object_ref (BONOBO_OBJECT (query));
return query;
}
}
/* not found, so create a new one */
query = QUERY (g_object_new (QUERY_TYPE, NULL));
if (!query_construct (query, backend, ql, sexp)) {
bonobo_object_unref (BONOBO_OBJECT (query));
return NULL;
}
/* add the new query to our cache */
g_signal_connect (G_OBJECT (query->priv->backend), "destroy",
G_CALLBACK (backend_destroyed_cb), query);
bonobo_object_ref (BONOBO_OBJECT (query));
cached_queries = g_list_append (cached_queries, query);
return query;
}