evolution-data-server r9023 - in branches/gnome-2-22: . calendar/libecal



Author: pohly
Date: Sun Jun 22 11:46:47 2008
New Revision: 9023
URL: http://svn.gnome.org/viewvc/evolution-data-server?rev=9023&view=rev

Log:
GNOME Bugzilla #52890: improved time zone handling

This patch adds a new utility function e_cal_check_timezones() to
libecal. The library version of libecal is bumped in a backwards
compatible way: code compiled against previous releases still works
with the extended library.

e_cal_check_timezones() can be used by calendar backends to fix two
problems:

1. Importing a new meeting invitation into a calendar which already
contains an old time zone definition ignores the new time zone
definition. The new meeting is then displayed using the old
definition.

2. There is no mechanism to update the time zone rules used by
existing meetings if their time zone changes.

If possible, time zone definitions are mapped to the system time zone
definitions used by Evolution. Currently this only works for time zone
definitions which have a known location at the end of their time zone
identifiers (i.e., older Evolution releases and other programs
following the Olson database naming scheme). If this mapping works,
then problem 1 and 2 no longer occur (but see the caveat below).

If the mapping fails, then problem 1 is avoided by checking for an
existing, conflicting time zone definition first and importing items
with a renamed time zone definition in such a case.

The caveat with regards to time zone handling is that Evolution only
uses a subset of the information available for a system time
zone. Events using a system time zone are only guaranteed to be
displayed correctly starting at the current time and one year into the
future. Past events and events even further into the future may be
displayed incorrectly. 

Due to this limitation, past meetings which would have been displayed
correctly using the time zone information they came with might be
displayed incorrectly using Evolution's stripped system time zones.
This limitation needs to be addressed separately in further patches.

e-cal-check-timezones.[ch] are 1:1 copies of
SyncEvolution/trunk revision 625.


Added:
   branches/gnome-2-22/calendar/libecal/e-cal-check-timezones.c
   branches/gnome-2-22/calendar/libecal/e-cal-check-timezones.h
Modified:
   branches/gnome-2-22/calendar/libecal/Makefile.am
   branches/gnome-2-22/configure.in

Modified: branches/gnome-2-22/calendar/libecal/Makefile.am
==============================================================================
--- branches/gnome-2-22/calendar/libecal/Makefile.am	(original)
+++ branches/gnome-2-22/calendar/libecal/Makefile.am	Sun Jun 22 11:46:47 2008
@@ -48,6 +48,7 @@
 	e-cal-listener.h	\
 	e-cal-recur.c		\
 	e-cal-time-util.c	\
+        e-cal-check-timezones.c \
 	e-cal-util.c		\
 	e-cal-view.c		\
 	e-cal-view-listener.c	\
@@ -71,6 +72,7 @@
 	e-cal-component.h	\
 	e-cal-recur.h		\
 	e-cal-time-util.h	\
+        e-cal-check-timezones.h \
 	e-cal-types.h		\
 	e-cal-util.h		\
 	e-cal-view.h

Added: branches/gnome-2-22/calendar/libecal/e-cal-check-timezones.c
==============================================================================
--- (empty file)
+++ branches/gnome-2-22/calendar/libecal/e-cal-check-timezones.c	Sun Jun 22 11:46:47 2008
@@ -0,0 +1,490 @@
+/*
+ * Copyright (C) 2008 Novell, Inc.
+ *
+ * Authors: Patrick Ohly <patrick ohly gmx de>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of version 2 of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef HANDLE_LIBICAL_MEMORY
+# define HANDLE_LIBICAL_MEMORY 1
+#endif
+#include <libical/ical.h>
+
+#ifdef LIBICAL_MEMFIXES
+/* avoid dependency on icalstrdup.h, works when compiling as part of EDS >= 2.22 */
+# define ical_strdup(_x) (_x)
+#else
+/* use icalstrdup.h to get runtime detection of memory fix patch */
+# include "libical/icalstrdup.h"
+#endif
+
+#include "e-cal-check-timezones.h"
+#include <libecal/e-cal.h>
+#include <string.h>
+#include <ctype.h>
+
+/**
+ * Matches a location to a system timezone definition via a fuzzy
+ * search and returns the matching TZID, or NULL if none found.
+ *
+ * Currently simply strips a suffix introduced by a hyphen,
+ * as in "America/Denver-(Standard)".
+ */
+static const char *e_cal_match_location(const char *location)
+{
+    icaltimezone *icomp;
+    const char *tail;
+    size_t len;
+    char *buffer;
+
+    icomp = icaltimezone_get_builtin_timezone (location);
+    if (icomp) {
+        return icaltimezone_get_tzid(icomp);
+    }
+
+    /* try a bit harder by stripping trailing suffix */
+    tail = strrchr(location, '-');
+    len = tail ? (tail - location) : strlen(location);
+    buffer = g_malloc(len + 1);
+
+    if (buffer) {
+        memcpy(buffer, location, len);
+        buffer[len] = 0;
+        icomp = icaltimezone_get_builtin_timezone (buffer);
+        g_free(buffer);
+        if (icomp) {
+            return icaltimezone_get_tzid(icomp);
+        }
+    }
+
+    return NULL;
+}
+
+/**
+ * e_cal_match_tzid:
+ * matches a TZID against the system timezone definitions
+ * and returns the matching TZID, or NULL if none found
+ */
+const char *e_cal_match_tzid(const char *tzid)
+{
+    const char *location;
+    const char *systzid;
+    size_t len = strlen(tzid);
+    ssize_t eostr;
+
+    /*
+     * Try without any trailing spaces/digits: they might have been added
+     * by e_cal_check_timezones() in order to distinguish between
+     * different incompatible definitions. At that time mapping
+     * to system time zones must have failed, but perhaps now
+     * we have better code and it succeeds...
+     */
+    eostr = len - 1;
+    while (eostr >= 0 &&
+           isdigit(tzid[eostr])) {
+        eostr--;
+    }
+    while (eostr >= 0 &&
+           isspace(tzid[eostr])) {
+        eostr--;
+    }
+    if (eostr + 1 < len) {
+        char *strippedtzid = g_strndup(tzid, eostr + 1);
+        if (strippedtzid) {
+            systzid = e_cal_match_tzid(strippedtzid);
+            g_free(strippedtzid);
+            if (systzid) {
+                return systzid;
+            }
+        }
+    }
+
+    /*
+     * old-style Evolution: /softwarestudio.org/Olson_20011030_5/America/Denver
+     *
+     * jump from one slash to the next and check whether the remainder
+     * is a known location; start with the whole string (just in case)
+     */
+    for (location = tzid;
+         location && location[0];
+         location = strchr(location + 1, '/')) {
+        systzid = e_cal_match_location(location[0] == '/' ?
+                                       location + 1 :
+                                       location);
+        if (systzid) {
+            return systzid;
+        }
+    }
+
+    /* TODO: lookup table for Exchange TZIDs */
+
+    return NULL;
+}
+
+static void patch_tzids(icalcomponent *subcomp,
+                        GHashTable *mapping)
+{
+    char *tzid = NULL;
+
+    if (icalcomponent_isa(subcomp) != ICAL_VTIMEZONE_COMPONENT) {
+        icalproperty *prop = icalcomponent_get_first_property(subcomp,
+                                                              ICAL_ANY_PROPERTY);
+        while (prop) {
+            icalparameter *param = icalproperty_get_first_parameter(prop,
+                                                                    ICAL_TZID_PARAMETER);
+            while (param) {
+                const char *oldtzid;
+                const char *newtzid;
+
+                g_free(tzid);
+                tzid = g_strdup(icalparameter_get_tzid(param));
+
+                if (!g_hash_table_lookup_extended(mapping,
+                                                  tzid,
+                                                  (gpointer *)&oldtzid,
+                                                  (gpointer *)&newtzid)) {
+                    /* Corresponding VTIMEZONE not seen before! */
+                    newtzid = e_cal_match_tzid(tzid);
+                }
+                if (newtzid) {
+                    icalparameter_set_tzid(param, newtzid);
+                }
+                param = icalproperty_get_next_parameter(prop,
+                                                        ICAL_TZID_PARAMETER);
+            }
+            prop = icalcomponent_get_next_property(subcomp,
+                                                   ICAL_ANY_PROPERTY);
+        }
+    }
+
+    g_free(tzid);
+}
+
+static void addsystemtz(gpointer key,
+                        gpointer value,
+                        gpointer user_data)
+{
+    const char *tzid = key;
+    icalcomponent *comp = user_data;
+    icaltimezone *zone;
+
+    zone = icaltimezone_get_builtin_timezone_from_tzid(tzid);
+    if (zone) {
+        icalcomponent_add_component(comp,
+                                    icalcomponent_new_clone(icaltimezone_get_component(zone)));
+    }
+}
+
+/**
+ * e_cal_check_timezones:
+ * @comp:     a VCALENDAR containing a list of
+ *            VTIMEZONE and arbitrary other components, in
+ *            arbitrary order: these other components are
+ *            modified by this call
+ * @comps:    a list of icalcomponent instances which
+ *            also have to be patched; may be NULL
+ * @tzlookup: a callback function which is called to retrieve
+ *            a calendar's VTIMEZONE definition; the returned
+ *            definition is *not* freed by e_cal_check_timezones()
+ *            (to be compatible with e_cal_get_timezone());
+ *            NULL indicates that no such timezone exists
+ *            or an error occurred
+ * @custom:   an arbitrary pointer which is passed through to
+ *            the tzlookup function
+ * @error:    an error description in case of a failure
+ *
+ * This function cleans up VEVENT, VJOURNAL, VTODO and VTIMEZONE
+ * items which are to be imported into Evolution.
+ *
+ * Using VTIMEZONE definitions is problematic because they cannot be
+ * updated properly when timezone definitions change. They are also
+ * incomplete (for compatibility reason only one set of rules for
+ * summer saving changes can be included, even if different rules
+ * apply in different years). This function looks for matches of the
+ * used TZIDs against system timezones and replaces such TZIDs with
+ * the corresponding system timezone. This works for TZIDs containing
+ * a location (found via a fuzzy string search) and for Outlook TZIDs
+ * (via a hard-coded lookup table).
+ *
+ * Some programs generate broken meeting invitations with TZID, but
+ * without including the corresponding VTIMEZONE. Importing such
+ * invitations unchanged causes problems later on (meeting displayed
+ * incorrectly, #e_cal_get_component_as_string fails). The situation
+ * where this occurred in the past (found by a SyncEvolution user) is
+ * now handled via the location based mapping.
+ *
+ * If this mapping fails, this function also deals with VTIMEZONE
+ * conflicts: such conflicts occur when the calendar already contains
+ * an old VTIMEZONE definition with the same TZID, but different
+ * summer saving rules. Replacing the VTIMEZONE potentially breaks
+ * displaying of old events, whereas not replacing it breaks the new
+ * events (the behavior in Evolution <= 2.22.1).
+ *
+ * The way this problem is resolved is by renaming the new VTIMEZONE
+ * definition until the TZID is unique. A running count is appended to
+ * the TZID. All items referencing the renamed TZID are adapted
+ * accordingly.
+ *
+ * Return value: TRUE if successful, FALSE otherwise.
+ */
+gboolean e_cal_check_timezones(icalcomponent *comp,
+                               GList *comps,
+                               icaltimezone *(*tzlookup)(const char *tzid,
+                                                         const void *custom,
+                                                         GError **error),
+                               const void *custom,
+                               GError **error)
+{
+    gboolean success = TRUE;
+    icalcomponent *subcomp = NULL;
+    icaltimezone *zone = icaltimezone_new();
+    char *key = NULL, *value = NULL;
+    char *buffer = NULL;
+    char *zonestr = NULL;
+    char *tzid = NULL;
+    GList *l;
+
+    /** a hash from old to new tzid; strings dynamically allocated */
+    GHashTable *mapping = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+
+    /** a hash of all system time zone IDs which have to be added; strings are shared with mapping hash */
+    GHashTable *systemtzids = g_hash_table_new(g_str_hash, g_str_equal);
+
+    *error = NULL;
+
+    if (!mapping || !zone) {
+        goto nomem;
+    }
+
+    /* iterate over all VTIMEZONE definitions */
+    subcomp = icalcomponent_get_first_component(comp,
+                                                ICAL_VTIMEZONE_COMPONENT);
+    while (subcomp) {
+        if (icaltimezone_set_component(zone, subcomp)) {
+            g_free(tzid);
+            tzid = g_strdup(icaltimezone_get_tzid(zone));
+            if (tzid) {
+                const char *newtzid = e_cal_match_tzid(tzid);
+                if (newtzid) {
+                    /* matched against system time zone */
+                    g_free(key);
+                    key = g_strdup(tzid);
+                    if (!key) {
+                        goto nomem;
+                    }
+
+                    g_free(value);
+                    value = g_strdup(newtzid);
+                    if (!value) {
+                        goto nomem;
+                    }
+
+                    g_hash_table_insert(mapping, key, value);
+                    g_hash_table_insert(systemtzids, value, NULL);
+                    key =
+                        value = NULL;
+                } else {
+                    zonestr = ical_strdup(icalcomponent_as_ical_string(subcomp));
+
+                    /* check for collisions with existing timezones */
+                    int counter;
+                    for (counter = 0;
+                         counter < 100 /* sanity limit */;
+                         counter++) {
+                        icaltimezone *existing_zone;
+
+                        if (counter) {
+                            g_free(value);
+                            value = g_strdup_printf("%s %d", tzid, counter);
+                        }
+                        existing_zone = tzlookup(counter ? value : tzid,
+                                                 custom,
+                                                 error);
+                        if (!existing_zone) {
+                            if (*error) {
+                                goto failed;
+                            } else {
+                                break;
+                            }
+                        }
+                        g_free(buffer);
+                        buffer = ical_strdup(icalcomponent_as_ical_string(icaltimezone_get_component(existing_zone)));
+
+                        if (counter) {
+                            char *fulltzid = g_strdup_printf("TZID:%s", value);
+                            size_t baselen = strlen("TZID:") + strlen(tzid);
+                            size_t fulllen = strlen(fulltzid);
+                            char *tzidprop;
+                            /*
+                             * Map TZID with counter suffix back to basename.
+                             */
+                            tzidprop = strstr(buffer, fulltzid);
+                            if (tzidprop) {
+                                memmove(tzidprop + baselen,
+                                        tzidprop + fulllen,
+                                        strlen(tzidprop + fulllen) + 1);
+                            }
+                            g_free(fulltzid);
+                        }
+                            
+
+                        /*
+                         * If the strings are identical, then the
+                         * VTIMEZONE definitions are identical.  If
+                         * they are not identical, then VTIMEZONE
+                         * definitions might still be semantically
+                         * correct and we waste some space by
+                         * needlesly duplicating the VTIMEZONE. This
+                         * is expected to occur rarely (if at all) in
+                         * practice.
+                         */
+                        if (!strcmp(zonestr, buffer)) {
+                            break;
+                        }
+                    }
+
+                    if (!counter) {
+                        /* does not exist, nothing to do */
+                    } else {
+                        /* timezone renamed */
+                        icalproperty *prop = icalcomponent_get_first_property(subcomp,
+                                                                              ICAL_TZID_PROPERTY);
+                        while (prop) {
+                            icalproperty_set_value_from_string(prop, value, "NO");
+                            prop = icalcomponent_get_next_property(subcomp,
+                                                                   ICAL_ANY_PROPERTY);
+                        }
+                        g_free(key);
+                        key = g_strdup(tzid);
+                        g_hash_table_insert(mapping, key, value);
+                        key =
+                            value = NULL;
+                    }
+                }
+            }
+        }
+
+        subcomp = icalcomponent_get_next_component(comp,
+                                                   ICAL_VTIMEZONE_COMPONENT);
+    }
+
+    /*
+     * now replace all TZID parameters in place
+     */
+    subcomp = icalcomponent_get_first_component(comp,
+                                                ICAL_ANY_COMPONENT);
+    while (subcomp) {
+        /*
+         * Leave VTIMEZONE unchanged, iterate over properties of
+         * everything else.
+         *
+         * Note that no attempt is made to remove unused VTIMEZONE
+         * definitions. That would just make the code more complex for
+         * little additional gain. However, newly used time zones are
+         * added below.
+         */
+        patch_tzids (subcomp, mapping);
+        subcomp = icalcomponent_get_next_component(comp,
+                                                   ICAL_ANY_COMPONENT);
+    }
+
+    for (l = comps; l; l = l->next) {
+        patch_tzids (l->data, mapping);
+    }
+
+    /*
+     * add system time zones that we mapped to: adding them ensures
+     * that the VCALENDAR remains consistent
+     */
+    g_hash_table_foreach(systemtzids, addsystemtz, comp);
+    
+    goto done;
+ nomem:
+    /* set gerror for "out of memory" if possible, otherwise abort via g_error() */
+    *error = g_error_new(E_CALENDAR_ERROR, E_CALENDAR_STATUS_OTHER_ERROR, "out of memory");
+    if (!*error) {
+        g_error("e_cal_check_timezones(): out of memory, cannot proceed - sorry!");
+    }
+ failed:
+    /* gerror should have been set already */
+    success = FALSE;
+ done:
+    if (mapping) {
+        g_hash_table_destroy(mapping);
+    }
+    if (systemtzids) {
+        g_hash_table_destroy(systemtzids);
+    }
+    if (zone) {
+        icaltimezone_free(zone, 1);
+    }
+    g_free(tzid);
+    g_free(zonestr);
+    g_free(buffer);
+    g_free(key);
+    g_free(value);
+    
+    return success;
+}
+
+/**
+ * e_cal_tzlookup_ecal:
+ * @custom: must be a valid ECal pointer
+ *
+ * An implementation of the tzlookup callback which clients
+ * can use. Calls #e_cal_get_timezone.
+ */
+icaltimezone *e_cal_tzlookup_ecal(const char *tzid,
+                                  const void *custom,
+                                  GError **error)
+{
+    ECal *ecal = (ECal *)custom;
+    icaltimezone *zone = NULL;
+
+    if (e_cal_get_timezone(ecal, tzid, &zone, error)) {
+        g_assert(*error == NULL);
+        return zone;
+    } else {
+        g_assert(*error);
+        if ((*error)->domain == E_CALENDAR_ERROR &&
+            (*error)->code == E_CALENDAR_STATUS_OBJECT_NOT_FOUND) {
+            /*
+             * we had to trigger this error to check for the timezone existance,
+             * clear it and return NULL
+             */
+            g_clear_error(error);
+        }
+        return NULL;
+    }
+}
+
+/**
+ * e_cal_tzlookup_icomp:
+ * @custom: must be a icalcomponent pointer which contains
+ *          either a VCALENDAR with VTIMEZONEs or VTIMEZONES
+ *          directly
+ *
+ * An implementation of the tzlookup callback which backends
+ * like the file backend can use. Searches for the timezone
+ * in the component list.
+ */
+icaltimezone *e_cal_tzlookup_icomp(const char *tzid,
+                                   const void *custom,
+                                   GError **error)
+{
+    icalcomponent *icomp = (icalcomponent *)custom;
+
+    return icalcomponent_get_timezone(icomp, (char *)tzid);
+}

Added: branches/gnome-2-22/calendar/libecal/e-cal-check-timezones.h
==============================================================================
--- (empty file)
+++ branches/gnome-2-22/calendar/libecal/e-cal-check-timezones.h	Sun Jun 22 11:46:47 2008
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2008 Novell, Inc.
+ *
+ * Authors: Patrick Ohly <patrick ohly gmx de>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of version 2 of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef E_CAL_CHECK_TIMEZONES_H
+#define E_CAL_CHECK_TIMEZONES_H
+
+#include <libical/ical.h>
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+gboolean e_cal_check_timezones(icalcomponent *comp,
+                               GList *comps,
+                               icaltimezone *(*tzlookup)(const char *tzid,
+                                                         const void *custom,
+                                                         GError **error),
+                               const void *custom,
+                               GError **error);
+
+icaltimezone *e_cal_tzlookup_ecal(const char *tzid,
+                                  const void *custom,
+                                  GError **error);
+
+icaltimezone *e_cal_tzlookup_icomp(const char *tzid,
+                                   const void *custom,
+                                   GError **error);
+
+const char *e_cal_match_tzid(const char *tzid);
+
+G_END_DECLS
+
+#endif /* E_CAL_CHECK_TIMEZONES_H */

Modified: branches/gnome-2-22/configure.in
==============================================================================
--- branches/gnome-2-22/configure.in	(original)
+++ branches/gnome-2-22/configure.in	Sun Jun 22 11:46:47 2008
@@ -47,9 +47,9 @@
 LIBEDATASERVERUI_REVISION=0
 LIBEDATASERVERUI_AGE=1
 
-LIBECAL_CURRENT=8
+LIBECAL_CURRENT=9
 LIBECAL_REVISION=0
-LIBECAL_AGE=1
+LIBECAL_AGE=2
 
 LIBEDATACAL_CURRENT=6
 LIBEDATACAL_REVISION=2



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]