[libgdata] calendar: Port to the Calendar API version 3



commit 83d215b9d2bb7e7b1547986c23277b370fce215f
Author: Philip Withnall <philip tecnocode co uk>
Date:   Mon May 4 00:08:50 2015 +0100

    calendar: Port to the Calendar API version 3
    
    Deprecated API:
     • GDataCalendarFeed:timezone, gdata_calendar_feed_get_timezone()
     • GDataCalendarFeed:times-cleaned,
       gdata_calendar_feed_get_times_cleaned()
     • GDataBatchable implementation in GDataCalendarService
     • gdata_calendar_service_insert_event(),
       gdata_calendar_service_insert_event_async()
     • GDataCalendarCalendar:times-cleaned,
       gdata_calendar_calendar_get_times_cleaned()
     • GDataCalendarCalendar:edited, gdata_calendar_calendar_get_edited()
    
    API changes:
     • GDataBatchable implementation in GDataCalendarService will now always
       fail
     • ACLs are temporarily not supported; support for them will be re-added
       in future when they have been ported to the new API
     • Add gdata_calendar_service_insert_calendar_event(),
       gdata_calendar_service_insert_calendar_event_async()
    
    This includes a partial update of the unit tests, but most of them have
    been disabled pending updates.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=664353

 docs/reference/gdata-sections.txt                 |    2 +
 gdata/gdata.symbols                               |    2 +
 gdata/services/calendar/gdata-calendar-calendar.c |  311 +++++---
 gdata/services/calendar/gdata-calendar-calendar.h |    9 +-
 gdata/services/calendar/gdata-calendar-event.c    |  846 +++++++++++++++++----
 gdata/services/calendar/gdata-calendar-feed.c     |   91 +--
 gdata/services/calendar/gdata-calendar-feed.h     |    8 +-
 gdata/services/calendar/gdata-calendar-query.c    |    3 +-
 gdata/services/calendar/gdata-calendar-service.c  |  514 ++++++++++---
 gdata/services/calendar/gdata-calendar-service.h  |   36 +-
 gdata/tests/calendar.c                            |  696 ++++++++++-------
 11 files changed, 1775 insertions(+), 743 deletions(-)
---
diff --git a/docs/reference/gdata-sections.txt b/docs/reference/gdata-sections.txt
index 62d5409..813b4ee 100644
--- a/docs/reference/gdata-sections.txt
+++ b/docs/reference/gdata-sections.txt
@@ -470,6 +470,8 @@ gdata_calendar_service_query_events
 gdata_calendar_service_query_events_async
 gdata_calendar_service_insert_event
 gdata_calendar_service_insert_event_async
+gdata_calendar_service_insert_calendar_event
+gdata_calendar_service_insert_calendar_event_async
 <SUBSECTION Standard>
 gdata_calendar_service_get_type
 GDATA_CALENDAR_SERVICE
diff --git a/gdata/gdata.symbols b/gdata/gdata.symbols
index 830f0bf..ffb308b 100644
--- a/gdata/gdata.symbols
+++ b/gdata/gdata.symbols
@@ -239,6 +239,7 @@ gdata_calendar_service_query_own_calendars
 gdata_calendar_service_query_own_calendars_async
 gdata_calendar_service_query_events
 gdata_calendar_service_insert_event
+gdata_calendar_service_insert_calendar_event
 gdata_operation_type_get_type
 gdata_service_error_get_type
 gdata_media_expression_get_type
@@ -857,6 +858,7 @@ gdata_youtube_service_upload_video
 gdata_youtube_service_finish_video_upload
 gdata_calendar_service_query_events_async
 gdata_calendar_service_insert_event_async
+gdata_calendar_service_insert_calendar_event_async
 gdata_contacts_contact_get_photo_async
 gdata_contacts_contact_get_photo_finish
 gdata_contacts_contact_set_photo_async
diff --git a/gdata/services/calendar/gdata-calendar-calendar.c 
b/gdata/services/calendar/gdata-calendar-calendar.c
index 93f1453..9d7204b 100644
--- a/gdata/services/calendar/gdata-calendar-calendar.c
+++ b/gdata/services/calendar/gdata-calendar-calendar.c
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2009–2010 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2009, 2010, 2014, 2015 <philip tecnocode co uk>
  *
  * GData Client is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -29,7 +29,7 @@
  * access roles defined for the base #GDataAccessRule (e.g. %GDATA_ACCESS_ROLE_NONE), #GDataCalendarCalendar 
has its own, such as
  * %GDATA_CALENDAR_ACCESS_ROLE_EDITOR and %GDATA_CALENDAR_ACCESS_ROLE_FREE_BUSY.
  *
- * For more details of Google Calendar's GData API, see the <ulink type="http" 
url="http://code.google.com/apis/calendar/docs/2.0/reference.html";>
+ * For more details of Google Calendar's GData API, see the <ulink type="http" 
url="https://developers.google.com/google-apps/calendar/v3/reference/";>
  * online documentation</ulink>.
  *
  * <example>
@@ -77,7 +77,6 @@
 #include <config.h>
 #include <glib.h>
 #include <glib/gi18n-lib.h>
-#include <libxml/parser.h>
 #include <string.h>
 
 #include "gdata-calendar-calendar.h"
@@ -89,23 +88,19 @@
 #include "gdata-calendar-service.h"
 
 static void gdata_calendar_calendar_access_handler_init (GDataAccessHandlerIface *iface);
-static GObject *gdata_calendar_calendar_constructor (GType type, guint n_construct_params, 
GObjectConstructParam *construct_params);
 static void gdata_calendar_calendar_finalize (GObject *object);
 static void gdata_calendar_calendar_get_property (GObject *object, guint property_id, GValue *value, 
GParamSpec *pspec);
 static void gdata_calendar_calendar_set_property (GObject *object, guint property_id, const GValue *value, 
GParamSpec *pspec);
-static void get_xml (GDataParsable *parsable, GString *xml_string);
-static gboolean parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError 
**error);
-static void get_namespaces (GDataParsable *parsable, GHashTable *namespaces);
+static void get_json (GDataParsable *parsable, JsonBuilder *builder);
+static gboolean parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError **error);
+static const gchar *get_content_type (void);
 
 struct _GDataCalendarCalendarPrivate {
        gchar *timezone;
-       guint times_cleaned;
        gboolean is_hidden;
        GDataColor colour;
        gboolean is_selected;
        gchar *access_level;
-
-       gint64 edited;
 };
 
 enum {
@@ -131,16 +126,15 @@ gdata_calendar_calendar_class_init (GDataCalendarCalendarClass *klass)
 
        g_type_class_add_private (klass, sizeof (GDataCalendarCalendarPrivate));
 
-       gobject_class->constructor = gdata_calendar_calendar_constructor;
        gobject_class->set_property = gdata_calendar_calendar_set_property;
        gobject_class->get_property = gdata_calendar_calendar_get_property;
        gobject_class->finalize = gdata_calendar_calendar_finalize;
 
-       parsable_class->parse_xml = parse_xml;
-       parsable_class->get_xml = get_xml;
-       parsable_class->get_namespaces = get_namespaces;
+       parsable_class->parse_json = parse_json;
+       parsable_class->get_json = get_json;
+       parsable_class->get_content_type = get_content_type;
 
-       entry_class->kind_term = "http://schemas.google.com/gCal/2005#calendarmeta";;
+       entry_class->kind_term = "calendar#calendarListEntry";
 
        /**
         * GDataCalendarCalendar:timezone:
@@ -158,12 +152,18 @@ gdata_calendar_calendar_class_init (GDataCalendarCalendarClass *klass)
         * GDataCalendarCalendar:times-cleaned:
         *
         * The number of times the calendar has been cleared of events.
+        *
+        * Deprecated: UNRELEASED: Unsupported by the online API any more. There
+        *   is no replacement; this will always return
+        *   <code class="literal">0</code>.
         **/
        g_object_class_install_property (gobject_class, PROP_TIMES_CLEANED,
                                         g_param_spec_uint ("times-cleaned",
                                                            "Times cleaned", "The number of times the 
calendar has been cleared of events.",
                                                            0, G_MAXUINT, 0,
-                                                           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+                                                           G_PARAM_DEPRECATED |
+                                                           G_PARAM_READABLE |
+                                                           G_PARAM_STATIC_STRINGS));
 
        /**
         * GDataCalendarCalendar:is-hidden:
@@ -181,12 +181,13 @@ gdata_calendar_calendar_class_init (GDataCalendarCalendarClass *klass)
        /**
         * GDataCalendarCalendar:color:
         *
-        * The color used to highlight the calendar in the user's browser. This must be one of a limited set 
of colors listed in the
-        * <ulink type="http" 
url="http://code.google.com/apis/calendar/data/2.0/reference.html#gCalcolor";>online documentation</ulink>.
+        * The background color used to highlight the calendar in the user’s
+        * browser. This used to be restricted to a limited set of colours, but
+        * since UNRELEASED may be any RGB colour.
         **/
        g_object_class_install_property (gobject_class, PROP_COLOR,
                                         g_param_spec_boxed ("color",
-                                                            "Color", "The color used to highlight the 
calendar in the user's browser.",
+                                                            "Color", "The background color used to highlight 
the calendar in the user's browser.",
                                                             GDATA_TYPE_COLOR,
                                                             G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
 
@@ -223,12 +224,17 @@ gdata_calendar_calendar_class_init (GDataCalendarCalendarClass *klass)
         *
         * For more information, see the <ulink type="http" 
url="http://www.atomenabled.org/developers/protocol/#appEdited";>
         * Atom Publishing Protocol specification</ulink>.
+        *
+        * Deprecated: UNRELEASED: Unsupported by the online API any more. There
+        * is no replacement; this will always return -1.
         **/
        g_object_class_install_property (gobject_class, PROP_EDITED,
                                         g_param_spec_int64 ("edited",
                                                             "Edited", "The last time the calendar was 
edited.",
                                                             -1, G_MAXINT64, -1,
-                                                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+                                                            G_PARAM_DEPRECATED |
+                                                            G_PARAM_READABLE |
+                                                            G_PARAM_STATIC_STRINGS));
 
        /* Override the ETag property since ETags don't seem to be supported for calendars. */
        g_object_class_override_property (gobject_class, PROP_ETAG, "etag");
@@ -257,28 +263,6 @@ static void
 gdata_calendar_calendar_init (GDataCalendarCalendar *self)
 {
        self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GDATA_TYPE_CALENDAR_CALENDAR, 
GDataCalendarCalendarPrivate);
-       self->priv->edited = -1;
-}
-
-static GObject *
-gdata_calendar_calendar_constructor (GType type, guint n_construct_params, GObjectConstructParam 
*construct_params)
-{
-       GObject *object;
-
-       /* Chain up to the parent class */
-       object = G_OBJECT_CLASS (gdata_calendar_calendar_parent_class)->constructor (type, 
n_construct_params, construct_params);
-
-       if (_gdata_parsable_is_constructed_from_xml (GDATA_PARSABLE (object)) == FALSE) {
-               GDataCalendarCalendarPrivate *priv = GDATA_CALENDAR_CALENDAR (object)->priv;
-               GTimeVal time_val;
-
-               /* Set the edited property to the current time (creation time). We don't do this in *_init() 
since that would cause
-                * setting it from parse_xml() to fail (duplicate element). */
-               g_get_current_time (&time_val);
-               priv->edited = time_val.tv_sec;
-       }
-
-       return object;
 }
 
 static void
@@ -296,14 +280,17 @@ gdata_calendar_calendar_finalize (GObject *object)
 static void
 gdata_calendar_calendar_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
 {
-       GDataCalendarCalendarPrivate *priv = GDATA_CALENDAR_CALENDAR (object)->priv;
+       GDataCalendarCalendar *self = GDATA_CALENDAR_CALENDAR (object);
+       GDataCalendarCalendarPrivate *priv = self->priv;
 
        switch (property_id) {
                case PROP_TIMEZONE:
                        g_value_set_string (value, priv->timezone);
                        break;
                case PROP_TIMES_CLEANED:
-                       g_value_set_uint (value, priv->times_cleaned);
+                       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+                       g_value_set_uint (value, gdata_calendar_calendar_get_times_cleaned (self));
+                       G_GNUC_END_IGNORE_DEPRECATIONS
                        break;
                case PROP_IS_HIDDEN:
                        g_value_set_boolean (value, priv->is_hidden);
@@ -318,7 +305,10 @@ gdata_calendar_calendar_get_property (GObject *object, guint property_id, GValue
                        g_value_set_string (value, priv->access_level);
                        break;
                case PROP_EDITED:
-                       g_value_set_int64 (value, priv->edited);
+                       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+                       g_value_set_int64 (value,
+                                          gdata_calendar_calendar_get_edited (self));
+                       G_GNUC_END_IGNORE_DEPRECATIONS
                        break;
                case PROP_ETAG:
                        /* Never return an ETag */
@@ -360,113 +350,176 @@ gdata_calendar_calendar_set_property (GObject *object, guint property_id, const
 }
 
 static gboolean
-parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error)
+parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError **error)
 {
        gboolean success;
        GDataCalendarCalendar *self = GDATA_CALENDAR_CALENDAR (parsable);
 
-       if (gdata_parser_is_namespace (node, "http://www.w3.org/2007/app";) == TRUE &&
-           gdata_parser_int64_time_from_element (node, "edited", P_REQUIRED | P_NO_DUPES, 
&(self->priv->edited), &success, error) == TRUE) {
+       /* FIXME: Unimplemented:
+        *  - location
+        *  - summaryOverride
+        *  - colorId
+        *  - foregroundColor
+        *  - defaultReminders
+        *  - notificationSettings
+        *  - primary
+        *  - deleted
+        */
+
+       if (gdata_parser_string_from_json_member (reader, "timeZone", P_DEFAULT, &self->priv->timezone, 
&success, error) ||
+           gdata_parser_color_from_json_member (reader, "backgroundColor", P_DEFAULT, &self->priv->colour, 
&success, error) ||
+           gdata_parser_boolean_from_json_member (reader, "hidden", P_DEFAULT, &self->priv->is_hidden, 
&success, error) ||
+           gdata_parser_boolean_from_json_member (reader, "selected", P_DEFAULT, &self->priv->is_selected, 
&success, error)) {
+               return success;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "summary") == 0) {
+               gchar *summary = NULL;
+
+               g_assert (gdata_parser_string_from_json_member (reader,
+                                                               "summary",
+                                                               P_DEFAULT,
+                                                               &summary,
+                                                               &success,
+                                                               error));
+
+               if (summary != NULL) {
+                       gdata_entry_set_title (GDATA_ENTRY (parsable), summary);
+               }
+
+               g_free (summary);
+
+               return success;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "description") == 0) {
+               gchar *description = NULL;
+
+               g_assert (gdata_parser_string_from_json_member (reader,
+                                                               "description",
+                                                               P_DEFAULT,
+                                                               &description,
+                                                               &success,
+                                                               error));
+
+               if (description != NULL) {
+                       gdata_entry_set_summary (GDATA_ENTRY (parsable),
+                                                description);
+               }
+
+               g_free (description);
+
                return success;
-       } else if (gdata_parser_is_namespace (node, "http://schemas.google.com/gCal/2005";) == TRUE) {
-               if (xmlStrcmp (node->name, (xmlChar*) "timezone") == 0) {
-                       /* gCal:timezone */
-                       xmlChar *_timezone = xmlGetProp (node, (xmlChar*) "value");
-                       if (_timezone == NULL)
-                               return gdata_parser_error_required_property_missing (node, "value", error);
-                       self->priv->timezone = (gchar*) _timezone;
-               } else if (xmlStrcmp (node->name, (xmlChar*) "timesCleaned") == 0) {
-                       /* gCal:timesCleaned */
-                       xmlChar *times_cleaned = xmlGetProp (node, (xmlChar*) "value");
-                       if (times_cleaned == NULL)
-                               return gdata_parser_error_required_property_missing (node, "value", error);
-                       self->priv->times_cleaned = g_ascii_strtoull ((gchar*) times_cleaned, NULL, 10);
-                       xmlFree (times_cleaned);
-               } else if (xmlStrcmp (node->name, (xmlChar*) "hidden") == 0) {
-                       /* gCal:hidden */
-                       if (gdata_parser_boolean_from_property (node, "value", &(self->priv->is_hidden), -1, 
error) == FALSE)
-                               return FALSE;
-               } else if (xmlStrcmp (node->name, (xmlChar*) "color") == 0) {
-                       /* gCal:color */
-                       xmlChar *value;
-                       GDataColor colour;
-
-                       value = xmlGetProp (node, (xmlChar*) "value");
-                       if (value == NULL)
-                               return gdata_parser_error_required_property_missing (node, "value", error);
-                       if (gdata_color_from_hexadecimal ((gchar*) value, &colour) == FALSE) {
-                               /* Error */
-                               g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
-                                            /* Translators: the first parameter is the name of an XML 
element (including the angle brackets
-                                             * ("<" and ">"), and the second parameter is the erroneous 
value (which was not in hexadecimal
-                                             * RGB format).
-                                             *
-                                             * For example:
-                                             *  The content of a <entry/gCal:color> element ("00FG56") was 
not in hexadecimal RGB format. */
-                                            _("The content of a %s element (\"%s\") was not in hexadecimal 
RGB format."),
-                                            "<entry/gCal:color>", value);
-                               xmlFree (value);
-
-                               return FALSE;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "accessRole") == 0) {
+               gchar *access_role = NULL;
+
+               g_assert (gdata_parser_string_from_json_member (reader,
+                                                               "accessRole",
+                                                               P_DEFAULT,
+                                                               &access_role,
+                                                               &success,
+                                                               error));
+
+               if (access_role != NULL) {
+                       const gchar *level;
+
+                       /* Convert from v3 format to v2. */
+                       if (g_strcmp0 (access_role, "freeBusyReader") == 0) {
+                               level = GDATA_CALENDAR_ACCESS_ROLE_FREE_BUSY;
+                       } else if (g_strcmp0 (access_role, "reader") == 0) {
+                               level = GDATA_CALENDAR_ACCESS_ROLE_READ;
+                       } else if (g_strcmp0 (access_role, "writer") == 0) {
+                               level = GDATA_CALENDAR_ACCESS_ROLE_EDITOR;
+                       } else if (g_strcmp0 (access_role, "owner") == 0) {
+                               level = GDATA_CALENDAR_ACCESS_ROLE_OWNER;
+                       } else {
+                               level = access_role;
                        }
 
-                       gdata_calendar_calendar_set_color (self, &colour);
-                       xmlFree (value);
-               } else if (xmlStrcmp (node->name, (xmlChar*) "selected") == 0) {
-                       /* gCal:selected */
-                       if (gdata_parser_boolean_from_property (node, "value", &(self->priv->is_selected), 
-1, error) == FALSE)
-                               return FALSE;
-               } else if (xmlStrcmp (node->name, (xmlChar*) "accesslevel") == 0) {
-                       /* gCal:accesslevel */
-                       self->priv->access_level = (gchar*) xmlGetProp (node, (xmlChar*) "value");
-                       if (self->priv->access_level == NULL)
-                               return gdata_parser_error_required_property_missing (node, "value", error);
-               } else {
-                       return GDATA_PARSABLE_CLASS (gdata_calendar_calendar_parent_class)->parse_xml 
(parsable, doc, node, user_data, error);
+                       self->priv->access_level = g_strdup (level);
+               }
+
+               g_free (access_role);
+
+               return success;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "id") == 0) {
+               GDataLink *_link;
+               const gchar *id;
+               gchar *uri;
+
+               /* Calendar entries don’t contain their own selfLink, so we have
+                * to add one manually. */
+               id = json_reader_get_string_value (reader);
+               if (id != NULL && *id != '\0') {
+                       uri = g_strconcat ("https://www.googleapis.com/calendar/v3/calendars/";, id, NULL);
+                       _link = gdata_link_new (uri, GDATA_LINK_SELF);
+                       gdata_entry_add_link (GDATA_ENTRY (parsable), _link);
+                       g_object_unref (_link);
+                       g_free (uri);
                }
+
+               return GDATA_PARSABLE_CLASS (gdata_calendar_calendar_parent_class)->parse_json (parsable, 
reader, user_data, error);
        } else {
-               return GDATA_PARSABLE_CLASS (gdata_calendar_calendar_parent_class)->parse_xml (parsable, doc, 
node, user_data, error);
+               return GDATA_PARSABLE_CLASS (gdata_calendar_calendar_parent_class)->parse_json (parsable, 
reader, user_data, error);
        }
 
        return TRUE;
 }
 
 static void
-get_xml (GDataParsable *parsable, GString *xml_string)
+get_json (GDataParsable *parsable, JsonBuilder *builder)
 {
+       const gchar *id, *etag, *title, *description;
        gchar *colour;
        GDataCalendarCalendarPrivate *priv = GDATA_CALENDAR_CALENDAR (parsable)->priv;
 
-       /* Chain up to the parent class */
-       GDATA_PARSABLE_CLASS (gdata_calendar_calendar_parent_class)->get_xml (parsable, xml_string);
+       id = gdata_entry_get_id (GDATA_ENTRY (parsable));
+       if (id != NULL) {
+               json_builder_set_member_name (builder, "id");
+               json_builder_add_string_value (builder, id);
+       }
+
+       json_builder_set_member_name (builder, "kind");
+       json_builder_add_string_value (builder, "calendar#calendar");
 
-       /* Add all the Calendar-specific XML */
-       if (priv->timezone != NULL)
-               gdata_parser_string_append_escaped (xml_string, "<gCal:timezone value='", priv->timezone, 
"'/>");
+       /* Add the ETag, if available. */
+       etag = gdata_entry_get_etag (GDATA_ENTRY (parsable));
+       if (etag != NULL) {
+               json_builder_set_member_name (builder, "etag");
+               json_builder_add_string_value (builder, etag);
+       }
+
+       /* Calendar labels titles as ‘summary’. */
+       title = gdata_entry_get_title (GDATA_ENTRY (parsable));
+       if (title != NULL) {
+               json_builder_set_member_name (builder, "summary");
+               json_builder_add_string_value (builder, title);
+       }
+
+       description = gdata_entry_get_summary (GDATA_ENTRY (parsable));
+       if (description != NULL) {
+               json_builder_set_member_name (builder, "description");
+               json_builder_add_string_value (builder, description);
+       }
 
-       if (priv->is_hidden == TRUE)
-               g_string_append (xml_string, "<gCal:hidden value='true'/>");
-       else
-               g_string_append (xml_string, "<gCal:hidden value='false'/>");
+       /* Add all the calendar-specific JSON */
+       if (priv->timezone != NULL) {
+               json_builder_set_member_name (builder, "timeZone");
+               json_builder_add_string_value (builder, priv->timezone);
+       }
+
+       json_builder_set_member_name (builder, "hidden");
+       json_builder_add_boolean_value (builder, priv->is_hidden);
 
-       colour = gdata_color_to_hexadecimal (&(priv->colour));
-       g_string_append_printf (xml_string, "<gCal:color value='%s'/>", colour);
+       colour = gdata_color_to_hexadecimal (&priv->colour);
+       json_builder_set_member_name (builder, "backgroundColor");
+       json_builder_add_string_value (builder, colour);
        g_free (colour);
 
-       if (priv->is_selected == TRUE)
-               g_string_append (xml_string, "<gCal:selected value='true'/>");
-       else
-               g_string_append (xml_string, "<gCal:selected value='false'/>");
+       json_builder_set_member_name (builder, "selected");
+       json_builder_add_boolean_value (builder, priv->is_selected);
 }
 
-static void
-get_namespaces (GDataParsable *parsable, GHashTable *namespaces)
+static const gchar *
+get_content_type (void)
 {
-       /* Chain up to the parent class */
-       GDATA_PARSABLE_CLASS (gdata_calendar_calendar_parent_class)->get_namespaces (parsable, namespaces);
-
-       g_hash_table_insert (namespaces, (gchar*) "gCal", (gchar*) "http://schemas.google.com/gCal/2005";);
-       g_hash_table_insert (namespaces, (gchar*) "app", (gchar*) "http://www.w3.org/2007/app";);
+       return "application/json";
 }
 
 /**
@@ -525,12 +578,14 @@ gdata_calendar_calendar_set_timezone (GDataCalendarCalendar *self, const gchar *
  * Gets the #GDataCalendarCalendar:times-cleaned property.
  *
  * Return value: the number of times the calendar has been totally emptied
+ * Deprecated: UNRELEASED: Unsupported by the online API any more. There is no
+ *   replacement; this will always return <code class="literal">0</code>.
  **/
 guint
 gdata_calendar_calendar_get_times_cleaned (GDataCalendarCalendar *self)
 {
        g_return_val_if_fail (GDATA_IS_CALENDAR_CALENDAR (self), 0);
-       return self->priv->times_cleaned;
+       return 0;
 }
 
 /**
@@ -654,10 +709,12 @@ gdata_calendar_calendar_get_access_level (GDataCalendarCalendar *self)
  * Gets the #GDataCalendarCalendar:edited property. If the property is unset, <code 
class="literal">-1</code> will be returned.
  *
  * Return value: the UNIX timestamp for the time the calendar was last edited, or <code 
class="literal">-1</code>
+ * Deprecated: UNRELEASED: Unsupported by the online API any more. There is no
+ *   replacement; this will always return <code class="literal">-1</code>.
  **/
 gint64
 gdata_calendar_calendar_get_edited (GDataCalendarCalendar *self)
 {
        g_return_val_if_fail (GDATA_IS_CALENDAR_CALENDAR (self), -1);
-       return self->priv->edited;
+       return -1;
 }
diff --git a/gdata/services/calendar/gdata-calendar-calendar.h 
b/gdata/services/calendar/gdata-calendar-calendar.h
index 6bfceb0..37a368f 100644
--- a/gdata/services/calendar/gdata-calendar-calendar.h
+++ b/gdata/services/calendar/gdata-calendar-calendar.h
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2009 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2009, 2015 <philip tecnocode co uk>
  *
  * GData Client is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -114,7 +114,6 @@ GDataCalendarCalendar *gdata_calendar_calendar_new (const gchar *id) G_GNUC_WARN
 
 const gchar *gdata_calendar_calendar_get_timezone (GDataCalendarCalendar *self) G_GNUC_PURE;
 void gdata_calendar_calendar_set_timezone (GDataCalendarCalendar *self, const gchar *_timezone);
-guint gdata_calendar_calendar_get_times_cleaned (GDataCalendarCalendar *self) G_GNUC_PURE;
 gboolean gdata_calendar_calendar_is_hidden (GDataCalendarCalendar *self) G_GNUC_PURE;
 void gdata_calendar_calendar_set_is_hidden (GDataCalendarCalendar *self, gboolean is_hidden);
 void gdata_calendar_calendar_get_color (GDataCalendarCalendar *self, GDataColor *color);
@@ -122,7 +121,11 @@ void gdata_calendar_calendar_set_color (GDataCalendarCalendar *self, const GData
 gboolean gdata_calendar_calendar_is_selected (GDataCalendarCalendar *self) G_GNUC_PURE;
 void gdata_calendar_calendar_set_is_selected (GDataCalendarCalendar *self, gboolean is_selected);
 const gchar *gdata_calendar_calendar_get_access_level (GDataCalendarCalendar *self) G_GNUC_PURE;
-gint64 gdata_calendar_calendar_get_edited (GDataCalendarCalendar *self);
+
+#ifndef LIBGDATA_DISABLE_DEPRECATED
+guint gdata_calendar_calendar_get_times_cleaned (GDataCalendarCalendar *self) G_GNUC_PURE G_GNUC_DEPRECATED;
+gint64 gdata_calendar_calendar_get_edited (GDataCalendarCalendar *self) G_GNUC_DEPRECATED;
+#endif /* !LIBGDATA_DISABLE_DEPRECATED */
 
 G_END_DECLS
 
diff --git a/gdata/services/calendar/gdata-calendar-event.c b/gdata/services/calendar/gdata-calendar-event.c
index 069bf33..a6dfdc6 100644
--- a/gdata/services/calendar/gdata-calendar-event.c
+++ b/gdata/services/calendar/gdata-calendar-event.c
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2009–2010 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2009, 2010, 2014, 2015 <philip tecnocode co uk>
  *
  * GData Client is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -25,7 +25,8 @@
  *
  * #GDataCalendarEvent is a subclass of #GDataEntry to represent an event on a calendar from Google Calendar.
  *
- * For more details of Google Calendar's GData API, see the <ulink type="http" 
url="http://code.google.com/apis/calendar/docs/2.0/reference.html";>
+ * For more details of Google Calendar's GData API, see the
+ * <ulink type="http" url="https://developers.google.com/google-apps/calendar/v3/reference/";>
  * online documentation</ulink>.
  *
  * <example>
@@ -84,7 +85,6 @@
 #include <config.h>
 #include <glib.h>
 #include <glib/gi18n-lib.h>
-#include <libxml/parser.h>
 #include <string.h>
 
 #include "gdata-calendar-event.h"
@@ -99,9 +99,9 @@ static void gdata_calendar_event_dispose (GObject *object);
 static void gdata_calendar_event_finalize (GObject *object);
 static void gdata_calendar_event_get_property (GObject *object, guint property_id, GValue *value, GParamSpec 
*pspec);
 static void gdata_calendar_event_set_property (GObject *object, guint property_id, const GValue *value, 
GParamSpec *pspec);
-static void get_xml (GDataParsable *parsable, GString *xml_string);
-static gboolean parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError 
**error);
-static void get_namespaces (GDataParsable *parsable, GHashTable *namespaces);
+static void get_json (GDataParsable *parsable, JsonBuilder *builder);
+static gboolean parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError **error);
+static const gchar *get_content_type (void);
 
 struct _GDataCalendarEventPrivate {
        gint64 edited;
@@ -109,17 +109,27 @@ struct _GDataCalendarEventPrivate {
        gchar *visibility;
        gchar *transparency;
        gchar *uid;
-       guint sequence;
+       gint64 sequence;
        GList *times; /* GDataGDWhen */
-       guint guests_can_modify : 1;
-       guint guests_can_invite_others : 1;
-       guint guests_can_see_guests : 1;
-       guint anyone_can_add_self : 1;
+       gboolean guests_can_modify;
+       gboolean guests_can_invite_others;
+       gboolean guests_can_see_guests;
+       gboolean anyone_can_add_self;
        GList *people; /* GDataGDWho */
        GList *places; /* GDataGDWhere */
        gchar *recurrence;
        gchar *original_event_id;
        gchar *original_event_uri;
+
+       /* Parsing state. */
+       struct {
+               gint64 start_time;
+               gint64 end_time;
+               gboolean seen_start;
+               gboolean seen_end;
+               gboolean start_is_date;
+               gboolean end_is_date;
+       } parser;
 };
 
 enum {
@@ -155,11 +165,11 @@ gdata_calendar_event_class_init (GDataCalendarEventClass *klass)
        gobject_class->dispose = gdata_calendar_event_dispose;
        gobject_class->finalize = gdata_calendar_event_finalize;
 
-       parsable_class->parse_xml = parse_xml;
-       parsable_class->get_xml = get_xml;
-       parsable_class->get_namespaces = get_namespaces;
+       parsable_class->parse_json = parse_json;
+       parsable_class->get_json = get_json;
+       parsable_class->get_content_type = get_content_type;
 
-       entry_class->kind_term = "http://schemas.google.com/g/2005#event";;
+       entry_class->kind_term = "calendar#event";
 
        /**
         * GDataCalendarEvent:edited:
@@ -250,7 +260,8 @@ gdata_calendar_event_class_init (GDataCalendarEventClass *klass)
         * Indicates whether attendees may modify the original event, so that changes are visible to 
organizers and other attendees.
         * Otherwise, any changes made by attendees will be restricted to that attendee's calendar.
         *
-        * For more information, see the <ulink type="http" 
url="http://code.google.com/apis/calendar/docs/2.0/reference.html#gCalguestsCanModify";>
+        * For more information, see the
+        * <ulink type="http" 
url="https://developers.google.com/google-apps/calendar/v3/reference/events#guestsCanInviteOthers";>
         * GData specification</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_GUESTS_CAN_MODIFY,
@@ -265,7 +276,7 @@ gdata_calendar_event_class_init (GDataCalendarEventClass *klass)
         * Indicates whether attendees may invite others to the event.
         *
         * For more information, see the <ulink type="http"
-        * url="http://code.google.com/apis/calendar/docs/2.0/reference.html#gCalguestsCanInviteOthers";>GData 
specification</ulink>.
+        * 
url="https://developers.google.com/google-apps/calendar/v3/reference/events#guestsCanInviteOthers";>GData 
specification</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_GUESTS_CAN_INVITE_OTHERS,
                                         g_param_spec_boolean ("guests-can-invite-others",
@@ -278,7 +289,8 @@ gdata_calendar_event_class_init (GDataCalendarEventClass *klass)
         *
         * Indicates whether attendees can see other people invited to the event.
         *
-        * For more information, see the <ulink type="http" 
url="http://code.google.com/apis/calendar/docs/2.0/reference.html#gCalguestsCanSeeGuests";>
+        * For more information, see the
+        * <ulink type="http" 
url="https://developers.google.com/google-apps/calendar/v3/reference/events#guestsCanSeeOtherGuests";>
         * GData specification</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_GUESTS_CAN_SEE_GUESTS,
@@ -438,7 +450,7 @@ gdata_calendar_event_get_property (GObject *object, guint property_id, GValue *v
                        g_value_set_string (value, priv->uid);
                        break;
                case PROP_SEQUENCE:
-                       g_value_set_uint (value, priv->sequence);
+                       g_value_set_uint (value, CLAMP (priv->sequence, 0, G_MAXUINT));
                        break;
                case PROP_GUESTS_CAN_MODIFY:
                        g_value_set_boolean (value, priv->guests_can_modify);
@@ -512,182 +524,698 @@ gdata_calendar_event_set_property (GObject *object, guint property_id, const GVa
 }
 
 static gboolean
-parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error)
+date_object_from_json (JsonReader *reader,
+                       const gchar *member_name,
+                       GDataParserOptions options,
+                       gint64 *date_time_output,
+                       gboolean *is_date_output,
+                       gboolean *success,
+                       GError **error)
+{
+       gint64 date_time;
+       gboolean is_date = FALSE;
+       gboolean found_member = FALSE;
+
+       /* Check if there’s such an element */
+       if (g_strcmp0 (json_reader_get_member_name (reader), member_name) != 0) {
+               return FALSE;
+       }
+
+       /* Check that it’s an object. */
+       if (!json_reader_is_object (reader)) {
+               const GError *child_error;
+
+               /* Manufacture an error. */
+               json_reader_read_member (reader, "dateTime");
+               child_error = json_reader_get_error (reader);
+               g_assert (child_error != NULL);
+               *success = gdata_parser_error_from_json_error (reader,
+                                                              child_error,
+                                                              error);
+               json_reader_end_member (reader);
+
+               return TRUE;
+       }
+
+       /* Try to parse either the dateTime or date member. */
+       if (json_reader_read_member (reader, "dateTime")) {
+               const gchar *date_string;
+               const GError *child_error;
+               GTimeVal time_val;
+
+               date_string = json_reader_get_string_value (reader);
+               child_error = json_reader_get_error (reader);
+
+               if (child_error != NULL) {
+                       *success = gdata_parser_error_from_json_error (reader,
+                                                                      child_error,
+                                                                      error);
+                       json_reader_end_member (reader);
+                       return TRUE;
+               }
+
+               if (!g_time_val_from_iso8601 (date_string, &time_val)) {
+                       *success = gdata_parser_error_not_iso8601_format_json (reader, date_string, error);
+                       json_reader_end_member (reader);
+                       return TRUE;
+               }
+
+               date_time = time_val.tv_sec;
+               is_date = FALSE;
+               found_member = TRUE;
+       }
+       json_reader_end_member (reader);
+
+       if (json_reader_read_member (reader, "date")) {
+               const gchar *date_string;
+               const GError *child_error;
+
+               date_string = json_reader_get_string_value (reader);
+               child_error = json_reader_get_error (reader);
+
+               if (child_error != NULL) {
+                       *success = gdata_parser_error_from_json_error (reader,
+                                                                      child_error,
+                                                                      error);
+                       json_reader_end_member (reader);
+                       return TRUE;
+               }
+
+               if (!gdata_parser_int64_from_date (date_string, &date_time)) {
+                       *success = gdata_parser_error_not_iso8601_format_json (reader, date_string, error);
+                       json_reader_end_member (reader);
+                       return TRUE;
+               }
+
+               is_date = TRUE;
+               found_member = TRUE;
+       }
+       json_reader_end_member (reader);
+
+       /* Ignore timeZone; it should be specified in dateTime. */
+       if (!found_member) {
+               *success = gdata_parser_error_required_json_content_missing (reader, error);
+               return TRUE;
+       }
+
+       *date_time_output = date_time;
+       *is_date_output = is_date;
+       *success = TRUE;
+
+       return TRUE;
+}
+
+/* Convert between v2 and v3 versions of various enum values. v2 uses a URI
+ * style with a constant prefix; v3 simply drops this prefix, and changes the
+ * spelling of ‘canceled’ to ‘cancelled’. */
+#define V2_PREFIX "http://schemas.google.com/g/2005#event.";
+
+static gchar *
+add_v2_prefix (const gchar *in)
+{
+       return g_strconcat (V2_PREFIX, in, NULL);
+}
+
+static const gchar *
+strip_v2_prefix (const gchar *uri)
+{
+       /* Convert to v3 format. */
+       if (g_str_has_prefix (uri, V2_PREFIX)) {
+               return uri + strlen (V2_PREFIX);
+       } else {
+               return uri;
+       }
+}
+
+static gboolean
+parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError **error)
 {
        gboolean success;
        GDataCalendarEvent *self = GDATA_CALENDAR_EVENT (parsable);
+       GDataCalendarEventPrivate *priv = self->priv;
+
+       /* FIXME: Currently unsupported:
+        *  - htmlLink
+        *  - colorId
+        *  - endTimeUnspecified
+        *  - originalStartTime
+        *  - attendeesOmitted
+        *  - extendedProperties
+        *  - hangoutLink
+        *  - gadget
+        *  - privateCopy
+        *  - locked
+        *  - reminders
+        *  - source
+        */
+
+       if (g_strcmp0 (json_reader_get_member_name (reader), "start") == 0) {
+               self->priv->parser.seen_start = TRUE;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "end") == 0) {
+               self->priv->parser.seen_end = TRUE;
+       }
+
+       if (gdata_parser_string_from_json_member (reader, "recurringEventId", P_DEFAULT, 
&self->priv->original_event_id, &success, error) ||
+           gdata_parser_boolean_from_json_member (reader, "guestsCanModify", P_DEFAULT, 
&self->priv->guests_can_modify, &success, error) ||
+           gdata_parser_boolean_from_json_member (reader, "guestsCanInviteOthers", P_DEFAULT, 
&self->priv->guests_can_invite_others, &success, error) ||
+           gdata_parser_boolean_from_json_member (reader, "guestsCanSeeOtherGuests", P_DEFAULT, 
&self->priv->guests_can_see_guests, &success, error) ||
+           gdata_parser_boolean_from_json_member (reader, "anyoneCanAddSelf", P_DEFAULT, 
&self->priv->anyone_can_add_self, &success, error) ||
+           gdata_parser_string_from_json_member (reader, "iCalUID", P_DEFAULT, &self->priv->uid, &success, 
error) ||
+           gdata_parser_int_from_json_member (reader, "sequence", P_DEFAULT, &self->priv->sequence, 
&success, error) ||
+           gdata_parser_int64_time_from_json_member (reader, "updated", P_DEFAULT, &self->priv->edited, 
&success, error) ||
+           date_object_from_json (reader, "start", P_DEFAULT, &self->priv->parser.start_time, 
&self->priv->parser.start_is_date, &success, error) ||
+           date_object_from_json (reader, "end", P_DEFAULT, &self->priv->parser.end_time, 
&self->priv->parser.end_is_date, &success, error)) {
+               if (success) {
+                       if (self->priv->edited != -1) {
+                               _gdata_entry_set_updated (GDATA_ENTRY (parsable),
+                                                         self->priv->edited);
+                       }
+
+                       if (self->priv->original_event_id != NULL) {
+                               g_free (self->priv->original_event_uri);
+                               self->priv->original_event_uri = g_strconcat 
("https://www.googleapis.com/calendar/v3/events/";,
+                                                                             self->priv->original_event_id, 
NULL);
+                       }
+
+                       if (self->priv->parser.seen_start && self->priv->parser.seen_end) {
+                               GDataGDWhen *when;
+
+                               when = gdata_gd_when_new (self->priv->parser.start_time,
+                                                         self->priv->parser.end_time,
+                                                         self->priv->parser.start_is_date ||
+                                                         self->priv->parser.end_is_date);
+                               self->priv->times = g_list_prepend (self->priv->times, when);  /* transfer 
ownership */
+
+                               self->priv->parser.seen_start = FALSE;
+                               self->priv->parser.seen_end = FALSE;
+                       }
+               }
 
-       if (gdata_parser_is_namespace (node, "http://www.w3.org/2007/app";) == TRUE &&
-           gdata_parser_int64_time_from_element (node, "edited", P_REQUIRED | P_NO_DUPES, 
&(self->priv->edited), &success, error) == TRUE) {
                return success;
-       } else if (gdata_parser_is_namespace (node, "http://schemas.google.com/g/2005";) == TRUE) {
-               if (gdata_parser_object_from_element_setter (node, "when", P_REQUIRED, GDATA_TYPE_GD_WHEN,
-                                                            gdata_calendar_event_add_time, self, &success, 
error) == TRUE ||
-                   gdata_parser_object_from_element_setter (node, "who", P_REQUIRED, GDATA_TYPE_GD_WHO,
-                                                            gdata_calendar_event_add_person, self, &success, 
error) == TRUE ||
-                   gdata_parser_object_from_element_setter (node, "where", P_REQUIRED, GDATA_TYPE_GD_WHERE,
-                                                            gdata_calendar_event_add_place, self, &success, 
error) == TRUE) {
-                       return success;
-               } else if (xmlStrcmp (node->name, (xmlChar*) "eventStatus") == 0) {
-                       /* gd:eventStatus */
-                       xmlChar *value = xmlGetProp (node, (xmlChar*) "value");
-                       if (value == NULL)
-                               return gdata_parser_error_required_property_missing (node, "value", error);
-                       self->priv->status = (gchar*) value;
-               } else if (xmlStrcmp (node->name, (xmlChar*) "visibility") == 0) {
-                       /* gd:visibility */
-                       xmlChar *value = xmlGetProp (node, (xmlChar*) "value");
-                       if (value == NULL)
-                               return gdata_parser_error_required_property_missing (node, "value", error);
-                       self->priv->visibility = (gchar*) value;
-               } else if (xmlStrcmp (node->name, (xmlChar*) "transparency") == 0) {
-                       /* gd:transparency */
-                       xmlChar *value = xmlGetProp (node, (xmlChar*) "value");
-                       if (value == NULL)
-                               return gdata_parser_error_required_property_missing (node, "value", error);
-                       self->priv->transparency = (gchar*) value;
-               } else if (xmlStrcmp (node->name, (xmlChar*) "recurrence") == 0) {
-                       /* gd:recurrence */
-                       self->priv->recurrence = (gchar*) xmlNodeListGetString (doc, node->children, TRUE);
-               } else if (xmlStrcmp (node->name, (xmlChar*) "originalEvent") == 0) {
-                       /* gd:originalEvent */
-                       self->priv->original_event_id = (gchar*) xmlGetProp (node, (xmlChar*) "id");
-                       self->priv->original_event_uri = (gchar*) xmlGetProp (node, (xmlChar*) "href");
-               } else {
-                       return GDATA_PARSABLE_CLASS (gdata_calendar_event_parent_class)->parse_xml (parsable, 
doc, node, user_data, error);
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "transparency") == 0) {
+               gchar *transparency = NULL;  /* owned */
+
+               g_assert (gdata_parser_string_from_json_member (reader,
+                                                               "transparency",
+                                                               P_DEFAULT,
+                                                               &transparency,
+                                                               &success,
+                                                               error));
+
+               if (success) {
+                       priv->transparency = add_v2_prefix (transparency);
                }
-       } else if (gdata_parser_is_namespace (node, "http://schemas.google.com/gCal/2005";) == TRUE) {
-               if (xmlStrcmp (node->name, (xmlChar*) "uid") == 0) {
-                       /* gCal:uid */
-                       xmlChar *value = xmlGetProp (node, (xmlChar*) "value");
-                       if (value == NULL)
-                               return gdata_parser_error_required_property_missing (node, "value", error);
-                       self->priv->uid = (gchar*) value;
-               } else if (xmlStrcmp (node->name, (xmlChar*) "sequence") == 0) {
-                       /* gCal:sequence */
-                       xmlChar *value;
-                       guint value_uint;
-
-                       value = xmlGetProp (node, (xmlChar*) "value");
-                       if (value == NULL)
-                               return gdata_parser_error_required_property_missing (node, "value", error);
-                       else
-                               value_uint = g_ascii_strtoull ((gchar*) value, NULL, 10);
-                       xmlFree (value);
-
-                       gdata_calendar_event_set_sequence (self, value_uint);
-               } else if (xmlStrcmp (node->name, (xmlChar*) "guestsCanModify") == 0) {
-                       /* gCal:guestsCanModify */
-                       gboolean guests_can_modify;
-                       if (gdata_parser_boolean_from_property (node, "value", &guests_can_modify, -1, error) 
== FALSE)
-                               return FALSE;
-                       gdata_calendar_event_set_guests_can_modify (self, guests_can_modify);
-               } else if (xmlStrcmp (node->name, (xmlChar*) "guestsCanInviteOthers") == 0) {
-                       /* gCal:guestsCanInviteOthers */
-                       gboolean guests_can_invite_others;
-                       if (gdata_parser_boolean_from_property (node, "value", &guests_can_invite_others, -1, 
error) == FALSE)
-                               return FALSE;
-                       gdata_calendar_event_set_guests_can_invite_others (self, guests_can_invite_others);
-               } else if (xmlStrcmp (node->name, (xmlChar*) "guestsCanSeeGuests") == 0) {
-                       /* gCal:guestsCanSeeGuests */
-                       gboolean guests_can_see_guests;
-                       if (gdata_parser_boolean_from_property (node, "value", &guests_can_see_guests, -1, 
error) == FALSE)
+
+               g_free (transparency);
+
+               return success;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "visibility") == 0) {
+               gchar *visibility = NULL;  /* owned */
+
+               g_assert (gdata_parser_string_from_json_member (reader,
+                                                               "visibility",
+                                                               P_DEFAULT,
+                                                               &visibility,
+                                                               &success,
+                                                               error));
+
+               if (success) {
+                       priv->visibility = add_v2_prefix (visibility);
+               }
+
+               g_free (visibility);
+
+               return success;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "status") == 0) {
+               gchar *status = NULL;  /* owned */
+
+               g_assert (gdata_parser_string_from_json_member (reader,
+                                                               "status",
+                                                               P_DEFAULT,
+                                                               &status,
+                                                               &success,
+                                                               error));
+
+               if (success) {
+                       if (g_strcmp0 (status, "cancelled") == 0) {
+                               /* Those damned British Englishes. */
+                               priv->status = add_v2_prefix ("canceled");
+                       } else {
+                               priv->status = add_v2_prefix (status);
+                       }
+               }
+
+               g_free (status);
+
+               return success;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "summary") == 0) {
+               const gchar *summary;
+               const GError *child_error = NULL;
+
+               summary = json_reader_get_string_value (reader);
+               child_error = json_reader_get_error (reader);
+
+               if (child_error != NULL) {
+                       gdata_parser_error_from_json_error (reader,
+                                                           child_error, error);
+                       return FALSE;
+               }
+
+               gdata_entry_set_title (GDATA_ENTRY (parsable), summary);
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "description") == 0) {
+               const gchar *description;
+               const GError *child_error = NULL;
+
+               description = json_reader_get_string_value (reader);
+               child_error = json_reader_get_error (reader);
+
+               if (child_error != NULL) {
+                       gdata_parser_error_from_json_error (reader,
+                                                           child_error, error);
+                       return FALSE;
+               }
+
+               gdata_entry_set_content (GDATA_ENTRY (parsable), description);
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "location") == 0) {
+               const gchar *location;
+               GDataGDWhere *where = NULL;  /* owned */
+               const GError *child_error = NULL;
+
+               location = json_reader_get_string_value (reader);
+               child_error = json_reader_get_error (reader);
+
+               if (child_error != NULL) {
+                       gdata_parser_error_from_json_error (reader,
+                                                           child_error, error);
+                       return FALSE;
+               }
+
+               where = gdata_gd_where_new (GDATA_GD_WHERE_EVENT,
+                                           location, NULL);
+               priv->places = g_list_prepend (priv->places, where);  /* transfer ownership */
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "created") == 0) {
+               gint64 created;
+
+               g_assert (gdata_parser_int64_time_from_json_member (reader,
+                                                                   "created",
+                                                                   P_DEFAULT,
+                                                                   &created,
+                                                                   &success,
+                                                                   error));
+
+               if (success) {
+                       _gdata_entry_set_published (GDATA_ENTRY (parsable),
+                                                   created);
+               }
+
+               return success;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "recurrence") == 0) {
+               guint i, j;
+               GString *recurrence = NULL;  /* owned */
+
+               /* In the JSON API, the recurrence is given as an array of
+                * strings, each giving an RFC 2445 property such as RRULE,
+                * EXRULE, RDATE or EXDATE. Concatenate them all to form a
+                * recurrence string as used in v2 of the API. */
+               if (self->priv->recurrence != NULL) {
+                       return gdata_parser_error_duplicate_json_element (reader,
+                                                                         error);
+               }
+
+               recurrence = g_string_new ("");
+
+               for (i = 0, j = json_reader_count_elements (reader); i < j; i++) {
+                       const gchar *line;
+                       const GError *child_error;
+
+                       json_reader_read_element (reader, i);
+
+                       line = json_reader_get_string_value (reader);
+                       child_error = json_reader_get_error (reader);
+                       if (child_error != NULL) {
+                               gdata_parser_error_from_json_error (reader, child_error, error);
+                               json_reader_end_element (reader);
                                return FALSE;
-                       gdata_calendar_event_set_guests_can_see_guests (self, guests_can_see_guests);
-               } else if (xmlStrcmp (node->name, (xmlChar*) "anyoneCanAddSelf") == 0) {
-                       /* gCal:anyoneCanAddSelf */
-                       gboolean anyone_can_add_self;
-                       if (gdata_parser_boolean_from_property (node, "value", &anyone_can_add_self, -1, 
error) == FALSE)
+                       }
+
+                       g_string_append (recurrence, line);
+                       g_string_append (recurrence, "\n");
+
+                       json_reader_end_element (reader);
+               }
+
+               g_assert (self->priv->recurrence == NULL);
+               self->priv->recurrence = g_string_free (recurrence, FALSE);
+
+               return TRUE;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "attendees") == 0) {
+               guint i, j;
+
+               if (priv->people != NULL) {
+                       return gdata_parser_error_duplicate_json_element (reader,
+                                                                         error);
+               }
+
+               for (i = 0, j = json_reader_count_elements (reader); i < j; i++) {
+                       GDataGDWho *who = NULL;  /* owned */
+                       const gchar *email_address, *value_string;
+                       const gchar *relation_type;
+                       gboolean is_organizer, is_resource;
+                       const GError *child_error;
+
+                       json_reader_read_element (reader, i);
+
+                       json_reader_read_member (reader, "responseStatus");
+                       child_error = json_reader_get_error (reader);
+                       if (child_error != NULL) {
+                               gdata_parser_error_from_json_error (reader,
+                                                                   child_error,
+                                                                   error);
+                               json_reader_end_member (reader);
                                return FALSE;
-                       gdata_calendar_event_set_anyone_can_add_self (self, anyone_can_add_self);
-               } else {
-                       return GDATA_PARSABLE_CLASS (gdata_calendar_event_parent_class)->parse_xml (parsable, 
doc, node, user_data, error);
+                       }
+                       json_reader_end_member (reader);
+
+                       json_reader_read_member (reader, "email");
+                       email_address = json_reader_get_string_value (reader);
+                       json_reader_end_member (reader);
+
+                       json_reader_read_member (reader, "displayName");
+                       value_string = json_reader_get_string_value (reader);
+                       json_reader_end_member (reader);
+
+                       json_reader_read_member (reader, "organizer");
+                       is_organizer = json_reader_get_boolean_value (reader);
+                       json_reader_end_member (reader);
+
+                       json_reader_read_member (reader, "resource");
+                       is_resource = json_reader_get_boolean_value (reader);
+                       json_reader_end_member (reader);
+
+                       /* FIXME: Currently unsupported:
+                        *  - id
+                        *  - self
+                        *  - optional (writeble)
+                        *  - responseStatus (writeble)
+                        *  - comment (writeble)
+                        *  - additionalGuests (writeble)
+                        */
+
+                       if (is_organizer) {
+                               relation_type = GDATA_GD_WHO_EVENT_ORGANIZER;
+                       } else if (!is_resource) {
+                               relation_type = GDATA_GD_WHO_EVENT_ATTENDEE;
+                       } else {
+                               /* FIXME: Add support for resources. */
+                               relation_type = NULL;
+                       }
+
+                       who = gdata_gd_who_new (relation_type, value_string,
+                                               email_address);
+                       priv->people = g_list_prepend (priv->people, who);  /* transfer ownership */
+
+                       json_reader_end_element (reader);
                }
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "creator") == 0 ||
+                  g_strcmp0 (json_reader_get_member_name (reader), "organizer") == 0) {
+               /* These are read-only and already handled as part of
+                * ‘attendees’, so ignore them. */
+               return TRUE;
        } else {
-               return GDATA_PARSABLE_CLASS (gdata_calendar_event_parent_class)->parse_xml (parsable, doc, 
node, user_data, error);
+               return GDATA_PARSABLE_CLASS (gdata_calendar_event_parent_class)->parse_json (parsable, 
reader, user_data, error);
        }
 
        return TRUE;
 }
 
 static void
-get_child_xml (GList *list, GString *xml_string)
+get_json (GDataParsable *parsable, JsonBuilder *builder)
 {
-       GList *i;
+       GList *l;
+       const gchar *id, *etag, *title, *description;
+       GDataGDWho *organiser_who = NULL;  /* unowned */
+       GDataCalendarEventPrivate *priv = GDATA_CALENDAR_EVENT (parsable)->priv;
 
-       for (i = list; i != NULL; i = i->next)
-               _gdata_parsable_get_xml (GDATA_PARSABLE (i->data), xml_string, FALSE);
-}
+       /* FIXME: Support:
+        *  - colorId
+        *  - attendeesOmitted
+        *  - extendedProperties
+        *  - gadget
+        *  - reminders
+        *  - source
+        */
 
-static void
-get_xml (GDataParsable *parsable, GString *xml_string)
-{
-       GDataCalendarEventPrivate *priv = GDATA_CALENDAR_EVENT (parsable)->priv;
+       id = gdata_entry_get_id (GDATA_ENTRY (parsable));
+       if (id != NULL) {
+               json_builder_set_member_name (builder, "id");
+               json_builder_add_string_value (builder, id);
+       }
 
-       /* Chain up to the parent class */
-       GDATA_PARSABLE_CLASS (gdata_calendar_event_parent_class)->get_xml (parsable, xml_string);
+       json_builder_set_member_name (builder, "kind");
+       json_builder_add_string_value (builder, "calendar#event");
 
-       /* Add all the Calendar-specific XML */
+       /* Add the ETag, if available. */
+       etag = gdata_entry_get_etag (GDATA_ENTRY (parsable));
+       if (etag != NULL) {
+               json_builder_set_member_name (builder, "etag");
+               json_builder_add_string_value (builder, etag);
+       }
 
-       /* TODO: gd:comments? */
+       /* Calendar labels titles as ‘summary’. */
+       title = gdata_entry_get_title (GDATA_ENTRY (parsable));
+       if (title != NULL) {
+               json_builder_set_member_name (builder, "summary");
+               json_builder_add_string_value (builder, title);
+       }
 
-       if (priv->status != NULL)
-               gdata_parser_string_append_escaped (xml_string, "<gd:eventStatus value='", priv->status, 
"'/>");
+       description = gdata_entry_get_content (GDATA_ENTRY (parsable));
+       if (description != NULL) {
+               json_builder_set_member_name (builder, "description");
+               json_builder_add_string_value (builder, description);
+       }
 
-       if (priv->visibility != NULL)
-               gdata_parser_string_append_escaped (xml_string, "<gd:visibility value='", priv->visibility, 
"'/>");
+       /* Add all the calendar-specific JSON */
+       json_builder_set_member_name (builder, "anyoneCanAddSelf");
+       json_builder_add_boolean_value (builder, priv->anyone_can_add_self);
 
-       if (priv->transparency != NULL)
-               gdata_parser_string_append_escaped (xml_string, "<gd:transparency value='", 
priv->transparency, "'/>");
+       json_builder_set_member_name (builder, "guestsCanInviteOthers");
+       json_builder_add_boolean_value (builder, priv->guests_can_invite_others);
 
-       if (priv->uid != NULL)
-               gdata_parser_string_append_escaped (xml_string, "<gCal:uid value='", priv->uid, "'/>");
+       json_builder_set_member_name (builder, "guestsCanModify");
+       json_builder_add_boolean_value (builder, priv->guests_can_modify);
 
-       if (priv->sequence != 0)
-               g_string_append_printf (xml_string, "<gCal:sequence value='%u'/>", priv->sequence);
+       json_builder_set_member_name (builder, "guestsCanSeeOtherGuests");
+       json_builder_add_boolean_value (builder, priv->guests_can_see_guests);
 
-       if (priv->guests_can_modify == TRUE)
-               g_string_append (xml_string, "<gCal:guestsCanModify value='true'/>");
-       else
-               g_string_append (xml_string, "<gCal:guestsCanModify value='false'/>");
+       if (priv->transparency != NULL) {
+               json_builder_set_member_name (builder, "transparency");
+               json_builder_add_string_value (builder,
+                                              strip_v2_prefix (priv->transparency));
+       }
 
-       if (priv->guests_can_invite_others == TRUE)
-               g_string_append (xml_string, "<gCal:guestsCanInviteOthers value='true'/>");
-       else
-               g_string_append (xml_string, "<gCal:guestsCanInviteOthers value='false'/>");
+       if (priv->visibility != NULL) {
+               json_builder_set_member_name (builder, "visibility");
+               json_builder_add_string_value (builder,
+                                              strip_v2_prefix (priv->visibility));
+       }
 
-       if (priv->guests_can_see_guests == TRUE)
-               g_string_append (xml_string, "<gCal:guestsCanSeeGuests value='true'/>");
-       else
-               g_string_append (xml_string, "<gCal:guestsCanSeeGuests value='false'/>");
+       if (priv->uid != NULL) {
+               json_builder_set_member_name (builder, "iCalUID");
+               json_builder_add_string_value (builder, priv->uid);
+       }
 
-       if (priv->anyone_can_add_self == TRUE)
-               g_string_append (xml_string, "<gCal:anyoneCanAddSelf value='true'/>");
-       else
-               g_string_append (xml_string, "<gCal:anyoneCanAddSelf value='false'/>");
+       if (priv->sequence > 0) {
+               json_builder_set_member_name (builder, "sequence");
+               json_builder_add_int_value (builder, priv->sequence);
+       }
 
-       if (priv->recurrence != NULL)
-               gdata_parser_string_append_escaped (xml_string, "<gd:recurrence>", priv->recurrence, 
"</gd:recurrence>");
+       if (priv->status != NULL) {
+               const gchar *status;
 
-       get_child_xml (priv->times, xml_string);
-       get_child_xml (priv->people, xml_string);
-       get_child_xml (priv->places, xml_string);
+               /* Convert to v3 format. */
+               status = strip_v2_prefix (priv->status);
+               if (g_strcmp0 (status, "canceled") == 0) {
+                       status = "cancelled";
+               }
 
-       /* TODO:
-        * - Finish supporting all tags
-        * - Check all tags here are valid for insertions and updates
-        */
+               json_builder_set_member_name (builder, "status");
+               json_builder_add_string_value (builder, status);
+       }
+
+       if (priv->recurrence != NULL) {
+               gchar **parts;
+               guint i;
+
+               json_builder_set_member_name (builder, "recurrence");
+               json_builder_begin_array (builder);
+
+               parts = g_strsplit (priv->recurrence, "\n", -1);
+
+               for (i = 0; parts[i] != NULL; i++) {
+                       json_builder_add_string_value (builder, parts[i]);
+               }
+
+               g_strfreev (parts);
+
+               json_builder_end_array (builder);
+       }
+
+       if (priv->original_event_id != NULL) {
+               json_builder_set_member_name (builder, "recurringEventId");
+               json_builder_add_string_value (builder, priv->original_event_id);
+       }
+
+       /* Times. */
+       for (l = priv->times; l != NULL; l = l->next) {
+               GDataGDWhen *when;  /* unowned */
+               gchar *val = NULL;  /* owned */
+               const gchar *member_name;
+               gint64 start_time, end_time;
+
+               when = l->data;
+
+               /* Start time. */
+               start_time = gdata_gd_when_get_start_time (when);
+               json_builder_set_member_name (builder, "start");
+               json_builder_begin_object (builder);
+
+               if (gdata_gd_when_is_date (when)) {
+                       member_name = "date";
+                       val = gdata_parser_date_from_int64 (start_time);
+               } else {
+                       member_name = "dateTime";
+                       val = gdata_parser_int64_to_iso8601 (start_time);
+               }
+
+               json_builder_set_member_name (builder, member_name);
+               json_builder_add_string_value (builder, val);
+               g_free (val);
+
+               json_builder_set_member_name (builder, "timeZone");
+               json_builder_add_string_value (builder, "UTC");
+
+               json_builder_end_object (builder);
+
+               /* End time. */
+               end_time = gdata_gd_when_get_end_time (when);
+
+               if (end_time > -1) {
+                       json_builder_set_member_name (builder, "end");
+                       json_builder_begin_object (builder);
+
+                       if (gdata_gd_when_is_date (when)) {
+                               member_name = "date";
+                               val = gdata_parser_date_from_int64 (end_time);
+                       } else {
+                               member_name = "dateTime";
+                               val = gdata_parser_int64_to_iso8601 (end_time);
+                       }
+
+                       json_builder_set_member_name (builder, member_name);
+                       json_builder_add_string_value (builder, val);
+                       g_free (val);
+
+                       json_builder_set_member_name (builder, "timeZone");
+                       json_builder_add_string_value (builder, "UTC");
+
+                       json_builder_end_object (builder);
+               } else {
+                       json_builder_set_member_name (builder, "endTimeUnspecified");
+                       json_builder_add_boolean_value (builder, TRUE);
+               }
+
+               /* Only use the first time. :-(
+                * FIXME: There must be a better solution. */
+               if (l->next != NULL) {
+                       g_warning ("Ignoring secondary times; they are no "
+                                  "longer supported by the server-side API.");
+                       break;
+               }
+       }
+
+       /* Locations. */
+       for (l = priv->places; l != NULL; l = l->next) {
+               GDataGDWhere *where;  /* unowned */
+               const gchar *location;
+
+               where = l->data;
+               location = gdata_gd_where_get_value_string (where);
+
+               json_builder_set_member_name (builder, "location");
+               json_builder_add_string_value (builder, location);
+
+               /* Only use the first location. :-(
+                * FIXME: There must be a better solution. */
+               if (l->next != NULL) {
+                       g_warning ("Ignoring secondary locations; they are no "
+                                  "longer supported by the server-side API.");
+                       break;
+               }
+       }
+
+       /* People. */
+       json_builder_set_member_name (builder, "attendees");
+       json_builder_begin_array (builder);
+
+       for (l = priv->people; l != NULL; l = l->next) {
+               GDataGDWho *who;  /* unowned */
+               const gchar *display_name, *email_address;
+
+               who = l->data;
+
+               json_builder_begin_object (builder);
+
+               display_name = gdata_gd_who_get_value_string (who);
+               if (display_name != NULL) {
+                       json_builder_set_member_name (builder, "displayName");
+                       json_builder_add_string_value (builder, display_name);
+               }
+
+               email_address = gdata_gd_who_get_email_address (who);
+               if (email_address != NULL) {
+                       json_builder_set_member_name (builder, "email");
+                       json_builder_add_string_value (builder, email_address);
+               }
+
+               if (g_strcmp0 (gdata_gd_who_get_relation_type (who),
+                              GDATA_GD_WHO_EVENT_ORGANIZER) == 0) {
+                       json_builder_set_member_name (builder, "organizer");
+                       json_builder_add_boolean_value (builder, TRUE);
+
+                       organiser_who = who;
+               }
+
+               json_builder_end_object (builder);
+       }
+
+       json_builder_end_array (builder);
+
+       if (organiser_who != NULL) {
+               const gchar *display_name, *email_address;
+
+               json_builder_set_member_name (builder, "organizer");
+               json_builder_begin_object (builder);
+
+               display_name = gdata_gd_who_get_value_string (organiser_who);
+               if (display_name != NULL) {
+                       json_builder_set_member_name (builder, "displayName");
+                       json_builder_add_string_value (builder, display_name);
+               }
+
+               email_address = gdata_gd_who_get_email_address (organiser_who);
+               if (email_address != NULL) {
+                       json_builder_set_member_name (builder, "email");
+                       json_builder_add_string_value (builder, email_address);
+               }
+
+               json_builder_end_object (builder);
+       }
 }
 
-static void
-get_namespaces (GDataParsable *parsable, GHashTable *namespaces)
+static const gchar *
+get_content_type (void)
 {
-       /* Chain up to the parent class */
-       GDATA_PARSABLE_CLASS (gdata_calendar_event_parent_class)->get_namespaces (parsable, namespaces);
-
-       g_hash_table_insert (namespaces, (gchar*) "gd", (gchar*) "http://schemas.google.com/g/2005";);
-       g_hash_table_insert (namespaces, (gchar*) "gCal", (gchar*) "http://schemas.google.com/gCal/2005";);
-       g_hash_table_insert (namespaces, (gchar*) "app", (gchar*) "http://www.w3.org/2007/app";);
+       return "application/json";
 }
 
 /**
@@ -872,7 +1400,7 @@ guint
 gdata_calendar_event_get_sequence (GDataCalendarEvent *self)
 {
        g_return_val_if_fail (GDATA_IS_CALENDAR_EVENT (self), 0);
-       return self->priv->sequence;
+       return CLAMP (self->priv->sequence, 0, G_MAXUINT);
 }
 
 /**
diff --git a/gdata/services/calendar/gdata-calendar-feed.c b/gdata/services/calendar/gdata-calendar-feed.c
index ce0ca91..b1bf4ff 100644
--- a/gdata/services/calendar/gdata-calendar-feed.c
+++ b/gdata/services/calendar/gdata-calendar-feed.c
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2009–2010 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2009, 2010, 2014, 2015 <philip tecnocode co uk>
  *
  * GData Client is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -34,14 +34,7 @@
 #include "gdata-feed.h"
 #include "gdata-private.h"
 
-static void gdata_calendar_feed_finalize (GObject *object);
 static void gdata_calendar_feed_get_property (GObject *object, guint property_id, GValue *value, GParamSpec 
*pspec);
-static gboolean parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError 
**error);
-
-struct _GDataCalendarFeedPrivate {
-       gchar *timezone;
-       guint times_cleaned;
-};
 
 enum {
        PROP_TIMEZONE = 1,
@@ -54,14 +47,8 @@ static void
 gdata_calendar_feed_class_init (GDataCalendarFeedClass *klass)
 {
        GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
-       GDataParsableClass *parsable_class = GDATA_PARSABLE_CLASS (klass);
-
-       g_type_class_add_private (klass, sizeof (GDataCalendarFeedPrivate));
 
        gobject_class->get_property = gdata_calendar_feed_get_property;
-       gobject_class->finalize = gdata_calendar_feed_finalize;
-
-       parsable_class->parse_xml = parse_xml;
 
        /**
         * GDataCalendarFeed:timezone:
@@ -70,12 +57,16 @@ gdata_calendar_feed_class_init (GDataCalendarFeedClass *klass)
         * url="http://en.wikipedia.org/wiki/Tz_database#Names_of_time_zones";>reference</ulink>.
         *
         * Since: 0.3.0
+        * Deprecated: UNRELEASED: Unsupported by the online API any more. There
+        *   is no replacement; this will always return %NULL.
         **/
        g_object_class_install_property (gobject_class, PROP_TIMEZONE,
                                         g_param_spec_string ("timezone",
                                                              "Timezone", "The timezone in which the feed's 
times are given.",
                                                              NULL,
-                                                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+                                                             G_PARAM_DEPRECATED |
+                                                             G_PARAM_READABLE |
+                                                             G_PARAM_STATIC_STRINGS));
 
        /**
         * GDataCalendarFeed:times-cleaned:
@@ -83,42 +74,41 @@ gdata_calendar_feed_class_init (GDataCalendarFeedClass *klass)
         * The number of times the feed has been completely cleared of entries.
         *
         * Since: 0.3.0
+        * Deprecated: UNRELEASED: Unsupported by the online API any more. There
+        *   is no replacement; this will always return 0.
         **/
        g_object_class_install_property (gobject_class, PROP_TIMES_CLEANED,
                                         g_param_spec_uint ("times-cleaned",
                                                            "Times cleaned", "The number of times the feed 
has been completely cleared of entries.",
                                                            0, G_MAXUINT, 0,
-                                                           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+                                                           G_PARAM_DEPRECATED |
+                                                           G_PARAM_READABLE |
+                                                           G_PARAM_STATIC_STRINGS));
 }
 
 static void
 gdata_calendar_feed_init (GDataCalendarFeed *self)
 {
-       self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GDATA_TYPE_CALENDAR_FEED, GDataCalendarFeedPrivate);
-}
-
-static void
-gdata_calendar_feed_finalize (GObject *object)
-{
-       GDataCalendarFeedPrivate *priv = GDATA_CALENDAR_FEED (object)->priv;
-
-       g_free (priv->timezone);
-
-       /* Chain up to the parent class */
-       G_OBJECT_CLASS (gdata_calendar_feed_parent_class)->finalize (object);
+       /* Nothing to see here. */
 }
 
 static void
 gdata_calendar_feed_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
 {
-       GDataCalendarFeedPrivate *priv = GDATA_CALENDAR_FEED (object)->priv;
+       GDataCalendarFeed *self = GDATA_CALENDAR_FEED (object);
 
        switch (property_id) {
                case PROP_TIMEZONE:
-                       g_value_set_string (value, priv->timezone);
+                       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+                       g_value_set_string (value,
+                                           gdata_calendar_feed_get_timezone (self));
+                       G_GNUC_END_IGNORE_DEPRECATIONS
                        break;
                case PROP_TIMES_CLEANED:
-                       g_value_set_uint (value, priv->times_cleaned);
+                       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+                       g_value_set_uint (value,
+                                         gdata_calendar_feed_get_times_cleaned (self));
+                       G_GNUC_END_IGNORE_DEPRECATIONS
                        break;
                default:
                        /* We don't have any other property... */
@@ -127,35 +117,6 @@ gdata_calendar_feed_get_property (GObject *object, guint property_id, GValue *va
        }
 }
 
-static gboolean
-parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error)
-{
-       GDataCalendarFeed *self = GDATA_CALENDAR_FEED (parsable);
-
-       if (gdata_parser_is_namespace (node, "http://schemas.google.com/gCal/2005";) == FALSE)
-               return GDATA_PARSABLE_CLASS (gdata_calendar_feed_parent_class)->parse_xml (parsable, doc, 
node, user_data, error);
-
-       if (xmlStrcmp (node->name, (xmlChar*) "timezone") == 0) {
-               /* gCal:timezone */
-               xmlChar *_timezone = xmlGetProp (node, (xmlChar*) "value");
-               if (_timezone == NULL)
-                       return gdata_parser_error_required_property_missing (node, "value", error);
-               g_free (self->priv->timezone);
-               self->priv->timezone = (gchar*) _timezone;
-       } else if (xmlStrcmp (node->name, (xmlChar*) "timesCleaned") == 0) {
-               /* gCal:timesCleaned */
-               xmlChar *times_cleaned = xmlGetProp (node, (xmlChar*) "value");
-               if (times_cleaned == NULL)
-                       return gdata_parser_error_required_property_missing (node, "value", error);
-               self->priv->times_cleaned = g_ascii_strtoull ((gchar*) times_cleaned, NULL, 10);
-               xmlFree (times_cleaned);
-       } else {
-               return GDATA_PARSABLE_CLASS (gdata_calendar_feed_parent_class)->parse_xml (parsable, doc, 
node, user_data, error);
-       }
-
-       return TRUE;
-}
-
 /**
  * gdata_calendar_feed_get_timezone:
  * @self: a #GDataCalendarFeed
@@ -165,12 +126,15 @@ parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_da
  * Return value: the feed's timezone, or %NULL
  *
  * Since: 0.3.0
+ * Deprecated: UNRELEASED: Unsupported by the online API any more. There is no
+ *   replacement; this will always return %NULL.
  **/
 const gchar *
 gdata_calendar_feed_get_timezone (GDataCalendarFeed *self)
 {
+       /* Not supported any more by version 3 of the API. */
        g_return_val_if_fail (GDATA_IS_CALENDAR_FEED (self), NULL);
-       return self->priv->timezone;
+       return NULL;
 }
 
 /**
@@ -182,10 +146,13 @@ gdata_calendar_feed_get_timezone (GDataCalendarFeed *self)
  * Return value: the number of times the feed has been totally emptied
  *
  * Since: 0.3.0
+ * Deprecated: UNRELEASED: Unsupported by the online API any more. There is no
+ *   replacement; this will always return %NULL.
  **/
 guint
 gdata_calendar_feed_get_times_cleaned (GDataCalendarFeed *self)
 {
+       /* Not supported any more by version 3 of the API. */
        g_return_val_if_fail (GDATA_IS_CALENDAR_FEED (self), 0);
-       return self->priv->times_cleaned;
+       return 0;
 }
diff --git a/gdata/services/calendar/gdata-calendar-feed.h b/gdata/services/calendar/gdata-calendar-feed.h
index 0cf062a..f9b2bc1 100644
--- a/gdata/services/calendar/gdata-calendar-feed.h
+++ b/gdata/services/calendar/gdata-calendar-feed.h
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2009 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2009, 2015 <philip tecnocode co uk>
  *
  * GData Client is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -68,8 +68,10 @@ typedef struct {
 
 GType gdata_calendar_feed_get_type (void);
 
-const gchar *gdata_calendar_feed_get_timezone (GDataCalendarFeed *self) G_GNUC_PURE;
-guint gdata_calendar_feed_get_times_cleaned (GDataCalendarFeed *self) G_GNUC_PURE;
+#ifndef LIBGDATA_DISABLE_DEPRECATED
+const gchar *gdata_calendar_feed_get_timezone (GDataCalendarFeed *self) G_GNUC_PURE G_GNUC_DEPRECATED;
+guint gdata_calendar_feed_get_times_cleaned (GDataCalendarFeed *self) G_GNUC_PURE G_GNUC_DEPRECATED;
+#endif /* !LIBGDATA_DISABLE_DEPRECATED */
 
 G_END_DECLS
 
diff --git a/gdata/services/calendar/gdata-calendar-query.c b/gdata/services/calendar/gdata-calendar-query.c
index 85da917..8198b89 100644
--- a/gdata/services/calendar/gdata-calendar-query.c
+++ b/gdata/services/calendar/gdata-calendar-query.c
@@ -27,7 +27,8 @@
  * those catered for by #GDataQuery.
  *
  * For more information on the custom GData query parameters supported by #GDataCalendarQuery, see the 
<ulink type="http"
- * url="http://code.google.com/apis/calendar/docs/2.0/reference.html#Parameters";>online 
documentation</ulink>.
+ * url="https://developers.google.com/google-apps/calendar/v3/reference/events/list";>online
+ * documentation</ulink>.
  *
  * <example>
  *     <title>Querying for Events</title>
diff --git a/gdata/services/calendar/gdata-calendar-service.c 
b/gdata/services/calendar/gdata-calendar-service.c
index e326eab..924fc5b 100644
--- a/gdata/services/calendar/gdata-calendar-service.c
+++ b/gdata/services/calendar/gdata-calendar-service.c
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2009 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2009, 2014, 2015 <philip tecnocode co uk>
  *
  * GData Client is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -26,12 +26,13 @@
  * #GDataCalendarService is a subclass of #GDataService for communicating with the GData API of Google 
Calendar. It supports querying
  * for, inserting, editing and deleting events from calendars, as well as operations on the calendars 
themselves.
  *
- * For more details of Google Calendar's GData API, see the <ulink type="http" 
url="http://code.google.com/apis/calendar/docs/2.0/reference.html";>
+ * For more details of Google Calendar's GData API, see the
+ * <ulink type="http" url="https://developers.google.com/google-apps/calendar/v3/reference/";>
  * online documentation</ulink>.
  *
  * Each calendar accessible through the service has an access control list (ACL) which defines the level of 
access to the calendar to each user, and
  * which users the calendar is shared with. For more information about ACLs for calendars, see the
- * <ulink type="http" 
url="http://code.google.com/apis/calendar/data/2.0/developers_guide_protocol.html#SharingACalendar";>online 
documentation on
+ * <ulink type="http" url="https://developers.google.com/google-apps/calendar/v3/reference/acl";>online 
documentation on
  * sharing calendars</ulink>.
  *
  * <example>
@@ -128,76 +129,9 @@
  *     </programlisting>
  * </example>
  *
- * The Calendar service can be manipulated using batch operations, too. See the
- * <ulink type="http" 
url="http://code.google.com/apis/calendar/data/2.0/developers_guide_protocol.html#Batch";>online documentation 
on batch
- * operations</ulink> for more information.
- *
- * <example>
- *     <title>Performing a Batch Operation on a Calendar</title>
- *     <programlisting>
- *     GDataCalendarService *service;
- *     GDataCalendarCalendar *calendar;
- *     GDataBatchOperation *operation;
- *     GDataLink *batch_link;
- *     GList *i;
- *     GError *error = NULL;
- *
- *     /<!-- -->* Create a service and retrieve a calendar to work on *<!-- -->/
- *     service = create_calendar_service ();
- *     calendar = get_calendar (service);
- *
- *     /<!-- -->* Create the batch operation *<!-- -->/
- *     batch_link = gdata_entry_look_up_link (GDATA_ENTRY (calendar), GDATA_LINK_BATCH);
- *     operation = gdata_batchable_create_operation (GDATA_BATCHABLE (service), gdata_link_get_uri 
(batch_link));
- *
- *     g_object_unref (calendar);
- *
- *     gdata_batch_operation_add_query (operation, event_entry_id_to_query, GDATA_TYPE_CALENDAR_EVENT,
- *                                      (GDataBatchOperationCallback) batch_query_cb, user_data);
- *     gdata_batch_operation_add_insertion (operation, new_entry, (GDataBatchOperationCallback) 
batch_insertion_cb, user_data);
- *     gdata_batch_operation_add_update (operation, old_entry, (GDataBatchOperationCallback) 
batch_update_cb, user_data);
- *     gdata_batch_operation_add_deletion (operation, entry_to_delete, (GDataBatchOperationCallback) 
batch_deletion_cb, user_data);
- *
- *     /<!-- -->* Run the batch operation and handle the results in the various callbacks *<!-- -->/
- *     gdata_test_batch_operation_run (operation, NULL, &error);
- *
- *     g_object_unref (operation);
- *     g_object_unref (service);
- *
- *     if (error != NULL) {
- *             g_error ("Error running batch operation: %s", error->message);
- *             g_error_free (error);
- *             return;
- *     }
- *
- *     static void
- *     batch_query_cb (guint operation_id, GDataBatchOperationType operation_type, GDataEntry *entry, GError 
*error, gpointer user_data)
- *     {
- *             /<!-- -->* operation_type == GDATA_BATCH_OPERATION_QUERY *<!-- -->/
- *             /<!-- -->* Reference and do something with the returned entry. *<!-- -->/
- *     }
- *
- *     static void
- *     batch_insertion_cb (guint operation_id, GDataBatchOperationType operation_type, GDataEntry *entry, 
GError *error, gpointer user_data)
- *     {
- *             /<!-- -->* operation_type == GDATA_BATCH_OPERATION_INSERTION *<!-- -->/
- *             /<!-- -->* Reference and do something with the returned entry. *<!-- -->/
- *     }
- *
- *     static void
- *     batch_update_cb (guint operation_id, GDataBatchOperationType operation_type, GDataEntry *entry, 
GError *error, gpointer user_data)
- *     {
- *             /<!-- -->* operation_type == GDATA_BATCH_OPERATION_UPDATE *<!-- -->/
- *             /<!-- -->* Reference and do something with the returned entry. *<!-- -->/
- *     }
- *
- *     static void
- *     batch_deletion_cb (guint operation_id, GDataBatchOperationType operation_type, GDataEntry *entry, 
GError *error, gpointer user_data)
- *     {
- *             /<!-- -->* operation_type == GDATA_BATCH_OPERATION_DELETION, entry == NULL *<!-- -->/
- *     }
- *     </programlisting>
- * </example>
+ * Before version UNRELEASED, the Calendar service could be manipulated using
+ * batch operations. That is no longer supported, and any batch operations
+ * created on the calendar will fail.
  **/
 
 #include <config.h>
@@ -213,8 +147,17 @@
 #include "gdata-query.h"
 #include "gdata-calendar-feed.h"
 
-/* Standards reference here: http://code.google.com/apis/calendar/docs/2.0/reference.html */
+/* Standards reference here:
+ * https://developers.google.com/google-apps/calendar/v3/reference/ */
 
+static void
+parse_error_response (GDataService *self,
+                      GDataOperationType operation_type,
+                      guint status,
+                      const gchar *reason_phrase,
+                      const gchar *response_body,
+                      gint length,
+                      GError **error);
 static GList *get_authorization_domains (void);
 
 _GDATA_DEFINE_AUTHORIZATION_DOMAIN (calendar, "cl", "https://www.google.com/calendar/feeds/";)
@@ -225,6 +168,7 @@ gdata_calendar_service_class_init (GDataCalendarServiceClass *klass)
 {
        GDataServiceClass *service_class = GDATA_SERVICE_CLASS (klass);
        service_class->feed_type = GDATA_TYPE_CALENDAR_FEED;
+       service_class->parse_error_response = parse_error_response;
        service_class->get_authorization_domains = get_authorization_domains;
 }
 
@@ -234,6 +178,212 @@ gdata_calendar_service_init (GDataCalendarService *self)
        /* Nothing to see here */
 }
 
+/* The error format used by the Google Calendar API doesn’t seem to be
+ * documented anywhere, which is a little frustrating. Here’s an example of it:
+ *     {
+ *      "error": {
+ *       "errors": [
+ *        {
+ *         "domain": "global",
+ *         "reason": "parseError",
+ *         "message": "Parse Error",
+ *        }
+ *       ],
+ *       "code": 400,
+ *       "message": "Parse Error"
+ *      }
+ *     }
+ * or:
+ *     {
+ *      "error": {
+ *       "errors": [
+ *        {
+ *         "domain": "global",
+ *         "reason": "required",
+ *         "message": "Missing end time."
+ *        }
+ *       ],
+ *       "code": 400,
+ *       "message": "Missing end time."
+ *      }
+ *     }
+ */
+static void
+parse_error_response (GDataService *self,
+                      GDataOperationType operation_type,
+                      guint status,
+                      const gchar *reason_phrase,
+                      const gchar *response_body,
+                      gint length,
+                      GError **error)
+{
+       JsonParser *parser = NULL;  /* owned */
+       JsonReader *reader = NULL;  /* owned */
+       gint i;
+       GError *child_error = NULL;
+
+       if (response_body == NULL) {
+               goto parent;
+       }
+
+       if (length == -1) {
+               length = strlen (response_body);
+       }
+
+       parser = json_parser_new ();
+       if (!json_parser_load_from_data (parser, response_body, length,
+                                        &child_error)) {
+               goto parent;
+       }
+
+       reader = json_reader_new (json_parser_get_root (parser));
+
+       /* Check that the outermost node is an object. */
+       if (!json_reader_is_object (reader)) {
+               goto parent;
+       }
+
+       /* Grab the ‘error’ member, then its ‘errors’ member. */
+       if (!json_reader_read_member (reader, "error") ||
+           !json_reader_is_object (reader) ||
+           !json_reader_read_member (reader, "errors") ||
+           !json_reader_is_array (reader)) {
+               goto parent;
+       }
+
+       /* Parse each of the errors. Return the first one, and print out any
+        * others. */
+       for (i = 0; i < json_reader_count_elements (reader); i++) {
+               const gchar *domain, *reason, *message, *extended_help;
+               const gchar *location_type, *location;
+
+               /* Parse the error. */
+               if (!json_reader_read_element (reader, i) ||
+                   !json_reader_is_object (reader)) {
+                       goto parent;
+               }
+
+               json_reader_read_member (reader, "domain");
+               domain = json_reader_get_string_value (reader);
+               json_reader_end_member (reader);
+
+               json_reader_read_member (reader, "reason");
+               reason = json_reader_get_string_value (reader);
+               json_reader_end_member (reader);
+
+               json_reader_read_member (reader, "message");
+               message = json_reader_get_string_value (reader);
+               json_reader_end_member (reader);
+
+               json_reader_read_member (reader, "extendedHelp");
+               extended_help = json_reader_get_string_value (reader);
+               json_reader_end_member (reader);
+
+               json_reader_read_member (reader, "locationType");
+               location_type = json_reader_get_string_value (reader);
+               json_reader_end_member (reader);
+
+               json_reader_read_member (reader, "location");
+               location = json_reader_get_string_value (reader);
+               json_reader_end_member (reader);
+
+               /* End the error element. */
+               json_reader_end_element (reader);
+
+               /* Create an error message, but only for the first error */
+               if (error == NULL || *error == NULL) {
+                       if (g_strcmp0 (domain, "usageLimits") == 0 &&
+                           g_strcmp0 (reason,
+                                      "dailyLimitExceededUnreg") == 0) {
+                               /* Daily Limit for Unauthenticated Use
+                                * Exceeded. */
+                               g_set_error (error, GDATA_SERVICE_ERROR,
+                                            GDATA_SERVICE_ERROR_API_QUOTA_EXCEEDED,
+                                            _("You have made too many API "
+                                              "calls recently. Please wait a "
+                                              "few minutes and try again."));
+                       } else if (g_strcmp0 (domain, "global") == 0 &&
+                                  g_strcmp0 (reason, "notFound") == 0) {
+                               /* Calendar not found. */
+                               g_set_error (error, GDATA_SERVICE_ERROR,
+                                            GDATA_SERVICE_ERROR_NOT_FOUND,
+                                            /* Translators: the parameter is an
+                                             * error message returned by the
+                                             * server. */
+                                            _("The requested resource was not found: %s"),
+                                            message);
+                       } else if ((g_strcmp0 (domain, "global") == 0 &&
+                                   g_strcmp0 (reason, "required") == 0) ||
+                                  (g_strcmp0 (domain, "global") == 0 &&
+                                   g_strcmp0 (reason, "conditionNotMet") == 0)) {
+                               /* Client-side protocol error. */
+                               g_set_error (error, GDATA_SERVICE_ERROR,
+                                            GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
+                                            /* Translators: the parameter is an
+                                             * error message returned by the
+                                             * server. */
+                                            _("Invalid request URI or header, "
+                                              "or unsupported nonstandard "
+                                              "parameter: %s"), message);
+                       } else if (g_strcmp0 (domain, "global") == 0 &&
+                                  (g_strcmp0 (reason, "authError") == 0 ||
+                                   g_strcmp0 (reason, "required") == 0)) {
+                               /* Authentication problem */
+                               g_set_error (error, GDATA_SERVICE_ERROR,
+                                            GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED,
+                                            _("You must be authenticated to "
+                                              "do this."));
+                       } else if (g_strcmp0 (domain, "global") == 0 &&
+                                  g_strcmp0 (reason, "forbidden") == 0) {
+                               g_set_error (error, GDATA_SERVICE_ERROR,
+                                            GDATA_SERVICE_ERROR_FORBIDDEN,
+                                            _("Access was denied by the user "
+                                              "or server."));
+                       } else {
+                               /* Unknown or validation (protocol) error. Fall
+                                * back to working off the HTTP status code. */
+                               g_warning ("Unknown error code ‘%s’ in domain "
+                                          "‘%s’ received with location type "
+                                          "‘%s’, location ‘%s’, extended help "
+                                          "‘%s’ and message ‘%s’.",
+                                          reason, domain, location_type,
+                                          location, extended_help, message);
+
+                               goto parent;
+                       }
+               } else {
+                       /* For all errors after the first, log the error in the
+                        * terminal. */
+                       g_debug ("Error message received in response: domain "
+                                "‘%s’, reason ‘%s’, extended help ‘%s’, "
+                                "message ‘%s’, location type ‘%s’, location "
+                                "‘%s’.",
+                                domain, reason, extended_help, message,
+                                location_type, location);
+               }
+       }
+
+       /* End the ‘errors’ and ‘error’ members. */
+       json_reader_end_element (reader);
+       json_reader_end_element (reader);
+
+       g_clear_object (&reader);
+       g_clear_object (&parser);
+
+       /* Ensure we’ve actually set an error message. */
+       g_assert (error == NULL || *error != NULL);
+
+       return;
+
+parent:
+       g_clear_object (&reader);
+       g_clear_object (&parser);
+
+       /* Chain up to the parent class */
+       GDATA_SERVICE_CLASS (gdata_calendar_service_parent_class)->parse_error_response (self, 
operation_type, status, reason_phrase,
+                                                                                        response_body, 
length, error);
+}
+
 static GList *
 get_authorization_domains (void)
 {
@@ -316,7 +466,7 @@ gdata_calendar_service_query_all_calendars (GDataCalendarService *self, GDataQue
                return NULL;
        }
 
-       request_uri = g_strconcat (_gdata_service_get_scheme (), 
"://www.google.com/calendar/feeds/default/allcalendars/full", NULL);
+       request_uri = g_strconcat (_gdata_service_get_scheme (), 
"://www.googleapis.com/calendar/v3/users/me/calendarList", NULL);
        feed = gdata_service_query (GDATA_SERVICE (self), get_calendar_authorization_domain (), request_uri, 
query, GDATA_TYPE_CALENDAR_CALENDAR,
                                    cancellable, progress_callback, progress_user_data, error);
        g_free (request_uri);
@@ -370,7 +520,7 @@ gdata_calendar_service_query_all_calendars_async (GDataCalendarService *self, GD
                return;
        }
 
-       request_uri = g_strconcat (_gdata_service_get_scheme (), 
"://www.google.com/calendar/feeds/default/allcalendars/full", NULL);
+       request_uri = g_strconcat (_gdata_service_get_scheme (), 
"://www.googleapis.com/calendar/v3/users/me/calendarList", NULL);
        gdata_service_query_async (GDATA_SERVICE (self), get_calendar_authorization_domain (), request_uri, 
query, GDATA_TYPE_CALENDAR_CALENDAR,
                                   cancellable, progress_callback, progress_user_data, 
destroy_progress_user_data, callback, user_data);
        g_free (request_uri);
@@ -413,7 +563,7 @@ gdata_calendar_service_query_own_calendars (GDataCalendarService *self, GDataQue
                return NULL;
        }
 
-       request_uri = g_strconcat (_gdata_service_get_scheme (), 
"://www.google.com/calendar/feeds/default/owncalendars/full", NULL);
+       request_uri = g_strconcat (_gdata_service_get_scheme (), 
"://www.googleapis.com/calendar/v3/users/me/calendarList?minAccessRole=owner", NULL);
        feed = gdata_service_query (GDATA_SERVICE (self), get_calendar_authorization_domain (), request_uri, 
query, GDATA_TYPE_CALENDAR_CALENDAR,
                                    cancellable, progress_callback, progress_user_data, error);
        g_free (request_uri);
@@ -467,12 +617,62 @@ gdata_calendar_service_query_own_calendars_async (GDataCalendarService *self, GD
                return;
        }
 
-       request_uri = g_strconcat (_gdata_service_get_scheme (), 
"://www.google.com/calendar/feeds/default/owncalendars/full", NULL);
+       request_uri = g_strconcat (_gdata_service_get_scheme (), 
"://www.googleapis.com/calendar/v3/users/me/calendarList?minAccessRole=owner", NULL);
        gdata_service_query_async (GDATA_SERVICE (self), get_calendar_authorization_domain (), request_uri, 
query, GDATA_TYPE_CALENDAR_CALENDAR,
                                   cancellable, progress_callback, progress_user_data, 
destroy_progress_user_data, callback, user_data);
        g_free (request_uri);
 }
 
+static gchar *
+build_events_uri (GDataCalendarCalendar *calendar)
+{
+       GString *uri;
+       const gchar *calendar_id;
+
+       calendar_id = (calendar != NULL) ? gdata_entry_get_id (GDATA_ENTRY (calendar)) : "default";
+
+       uri = g_string_new (_gdata_service_get_scheme ());
+       g_string_append (uri, "://www.googleapis.com/calendar/v3/calendars/");
+       g_string_append_uri_escaped (uri, calendar_id, NULL, FALSE);
+       g_string_append (uri, "/events");
+
+       return g_string_free (uri, FALSE);
+}
+
+static void
+set_event_id (GDataCalendarEvent *event,
+              GDataCalendarCalendar *calendar)
+{
+       GDataLink *_link = NULL;  /* owned */
+       const gchar *id, *calendar_id;
+       gchar *uri = NULL;  /* owned */
+
+       id = gdata_entry_get_id (GDATA_ENTRY (event));
+       calendar_id = gdata_entry_get_id (GDATA_ENTRY (calendar));
+
+       uri = g_strconcat ("https://www.googleapis.com/calendar/v3/calendars/";,
+                          calendar_id, "/events/", id, NULL);
+       _link = gdata_link_new (uri, GDATA_LINK_SELF);
+       gdata_entry_add_link (GDATA_ENTRY (event), _link);
+       g_object_unref (_link);
+       g_free (uri);
+}
+
+static void
+set_event_ids (GDataFeed *feed,
+               GDataCalendarCalendar *calendar)
+{
+       GList/*<unowned GDataEntry>*/ *entries, *i;  /* unowned */
+
+       /* Add a selfLink to each event, since they don’t contain one by
+        * default in the data returned by the server. */
+       entries = gdata_feed_get_entries (feed);
+
+       for (i = entries; i != NULL; i = i->next) {
+               set_event_id (GDATA_CALENDAR_EVENT (i->data), calendar);
+       }
+}
+
 /**
  * gdata_calendar_service_query_events:
  * @self: a #GDataCalendarService
@@ -493,7 +693,8 @@ GDataFeed *
 gdata_calendar_service_query_events (GDataCalendarService *self, GDataCalendarCalendar *calendar, GDataQuery 
*query, GCancellable *cancellable,
                                      GDataQueryProgressCallback progress_callback, gpointer 
progress_user_data, GError **error)
 {
-       const gchar *uri;
+       gchar *request_uri;
+       GDataFeed *feed;
 
        g_return_val_if_fail (GDATA_IS_CALENDAR_SERVICE (self), NULL);
        g_return_val_if_fail (GDATA_IS_CALENDAR_CALENDAR (calendar), NULL);
@@ -509,18 +710,20 @@ gdata_calendar_service_query_events (GDataCalendarService *self, GDataCalendarCa
                return NULL;
        }
 
-       /* Use the calendar's content src */
-       uri = gdata_entry_get_content_uri (GDATA_ENTRY (calendar));
-       if (uri == NULL) {
-               /* Erroring out is probably the safest thing to do */
-               g_set_error_literal (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
-                                    _("The calendar did not have a content URI."));
-               return NULL;
-       }
+       /* Execute the query. */
+       request_uri = build_events_uri (calendar);
+       feed = gdata_service_query (GDATA_SERVICE (self),
+                                   get_calendar_authorization_domain (),
+                                   request_uri, query,
+                                   GDATA_TYPE_CALENDAR_EVENT, cancellable,
+                                   progress_callback, progress_user_data,
+                                   error);
+       g_free (request_uri);
+
+       /* Set the events’ IDs. */
+       set_event_ids (feed, calendar);
 
-       /* Execute the query */
-       return gdata_service_query (GDATA_SERVICE (self), get_calendar_authorization_domain (), uri, query, 
GDATA_TYPE_CALENDAR_EVENT, cancellable,
-                                   progress_callback, progress_user_data, error);
+       return feed;
 }
 
 /**
@@ -552,7 +755,7 @@ gdata_calendar_service_query_events_async (GDataCalendarService *self, GDataCale
                                            GDestroyNotify destroy_progress_user_data,
                                            GAsyncReadyCallback callback, gpointer user_data)
 {
-       const gchar *uri;
+       gchar *request_uri;
 
        g_return_if_fail (GDATA_IS_CALENDAR_SERVICE (self));
        g_return_if_fail (GDATA_IS_CALENDAR_CALENDAR (calendar));
@@ -572,22 +775,16 @@ gdata_calendar_service_query_events_async (GDataCalendarService *self, GDataCale
                return;
        }
 
-       /* Use the calendar's content src */
-       uri = gdata_entry_get_content_uri (GDATA_ENTRY (calendar));
-       if (uri == NULL) {
-               /* Erroring out is probably the safest thing to do */
-               GSimpleAsyncResult *result = g_simple_async_result_new (G_OBJECT (self), callback, user_data, 
gdata_service_query_async);
-               g_simple_async_result_set_error (result, GDATA_SERVICE_ERROR, 
GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED, "%s",
-                                                _("The calendar did not have a content URI."));
-               g_simple_async_result_complete_in_idle (result);
-               g_object_unref (result);
-
-               return;
-       }
-
-       /* Execute the query */
-       gdata_service_query_async (GDATA_SERVICE (self), get_calendar_authorization_domain (), uri, query, 
GDATA_TYPE_CALENDAR_EVENT, cancellable,
-                                  progress_callback, progress_user_data, destroy_progress_user_data, 
callback, user_data);
+       /* Execute the query. */
+       request_uri = build_events_uri (calendar);
+       gdata_service_query_async (GDATA_SERVICE (self),
+                                  get_calendar_authorization_domain (),
+                                  request_uri, query,
+                                  GDATA_TYPE_CALENDAR_EVENT, cancellable,
+                                  progress_callback, progress_user_data,
+                                  destroy_progress_user_data, callback,
+                                  user_data);
+       g_free (request_uri);
 }
 
 /**
@@ -604,11 +801,13 @@ gdata_calendar_service_query_events_async (GDataCalendarService *self, GDataCale
  * Return value: (transfer full): an updated #GDataCalendarEvent, or %NULL; unref with g_object_unref()
  *
  * Since: 0.2.0
+ * Deprecated: UNRELEASED: Use gdata_calendar_service_insert_calendar_event()
+ *   instead to be able to specify the calendar to add the event to; otherwise
+ *   the default calendar will be used.
  **/
 GDataCalendarEvent *
 gdata_calendar_service_insert_event (GDataCalendarService *self, GDataCalendarEvent *event, GCancellable 
*cancellable, GError **error)
 {
-       /* TODO: How do we choose which calendar? */
        gchar *uri;
        GDataEntry *entry;
 
@@ -617,7 +816,7 @@ gdata_calendar_service_insert_event (GDataCalendarService *self, GDataCalendarEv
        g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
        g_return_val_if_fail (error == NULL || *error == NULL, NULL);
 
-       uri = g_strconcat (_gdata_service_get_scheme (), 
"://www.google.com/calendar/feeds/default/private/full", NULL);
+       uri = build_events_uri (NULL);
        entry = gdata_service_insert_entry (GDATA_SERVICE (self), get_calendar_authorization_domain (), uri, 
GDATA_ENTRY (event), cancellable, error);
        g_free (uri);
 
@@ -625,6 +824,52 @@ gdata_calendar_service_insert_event (GDataCalendarService *self, GDataCalendarEv
 }
 
 /**
+ * gdata_calendar_service_insert_calendar_event:
+ * @self: a #GDataCalendarService
+ * @calendar: the #GDataCalendarCalendar to insert the event into
+ * @event: the #GDataCalendarEvent to insert
+ * @cancellable: (allow-none): optional #GCancellable object, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Inserts @event by uploading it to the online calendar service, adding it to
+ * the specified @calendar.
+ *
+ * For more details, see gdata_service_insert_entry().
+ *
+ * Return value: (transfer full): an updated #GDataCalendarEvent, or %NULL;
+ * unref with g_object_unref()
+ *
+ * Since: UNRELEASED
+ */
+GDataCalendarEvent *
+gdata_calendar_service_insert_calendar_event (GDataCalendarService *self,
+                                              GDataCalendarCalendar *calendar,
+                                              GDataCalendarEvent *event,
+                                              GCancellable *cancellable,
+                                              GError **error)
+{
+       gchar *uri;
+       GDataEntry *entry;
+
+       g_return_val_if_fail (GDATA_IS_CALENDAR_SERVICE (self), NULL);
+       g_return_val_if_fail (GDATA_IS_CALENDAR_CALENDAR (calendar), NULL);
+       g_return_val_if_fail (GDATA_IS_CALENDAR_EVENT (event), NULL);
+       g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
+       g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+       uri = build_events_uri (calendar);
+       entry = gdata_service_insert_entry (GDATA_SERVICE (self),
+                                           get_calendar_authorization_domain (),
+                                           uri, GDATA_ENTRY (event),
+                                           cancellable, error);
+       g_free (uri);
+
+       set_event_id (GDATA_CALENDAR_EVENT (entry), calendar);
+
+       return GDATA_CALENDAR_EVENT (entry);
+}
+
+/**
  * gdata_calendar_service_insert_event_async:
  * @self: a #GDataCalendarService
  * @event: the #GDataCalendarEvent to insert
@@ -642,6 +887,10 @@ gdata_calendar_service_insert_event (GDataCalendarService *self, GDataCalendarEv
  * gdata_service_insert_entry_async(), which is the base asynchronous insertion function.
  *
  * Since: 0.8.0
+ * Deprecated: UNRELEASED: Use
+ *   gdata_calendar_service_insert_calendar_event_async() instead to be able to
+ *   specify the calendar to add the event to; otherwise the default calendar
+ *   will be used.
  **/
 void
 gdata_calendar_service_insert_event_async (GDataCalendarService *self, GDataCalendarEvent *event, 
GCancellable *cancellable,
@@ -653,8 +902,53 @@ gdata_calendar_service_insert_event_async (GDataCalendarService *self, GDataCale
        g_return_if_fail (GDATA_IS_CALENDAR_EVENT (event));
        g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
 
-       uri = g_strconcat (_gdata_service_get_scheme (), 
"://www.google.com/calendar/feeds/default/private/full", NULL);
+       uri = build_events_uri (NULL);
        gdata_service_insert_entry_async (GDATA_SERVICE (self), get_calendar_authorization_domain (), uri, 
GDATA_ENTRY (event), cancellable,
                                          callback, user_data);
        g_free (uri);
 }
+
+/**
+ * gdata_calendar_service_insert_calendar_event_async:
+ * @self: a #GDataCalendarService
+ * @calendar: the #GDataCalendarCalendar to insert the event into
+ * @event: the #GDataCalendarEvent to insert
+ * @cancellable: (allow-none): optional #GCancellable object, or %NULL
+ * @callback: a #GAsyncReadyCallback to call when insertion is finished
+ * @user_data: (closure): data to pass to the @callback function
+ *
+ * Inserts @event by uploading it to the online calendar service, adding it to
+ * the specified @calendar. @self and @event are both reffed when this function
+ * is called, so can safely be unreffed after this function returns.
+ *
+ * @callback should call gdata_service_insert_entry_finish() to obtain a
+ * #GDataCalendarEvent representing the inserted event and to check for possible
+ * errors.
+ *
+ * For more details, see gdata_calendar_service_insert_event(), which is the
+ * synchronous version of this function, and gdata_service_insert_entry_async(),
+ * which is the base asynchronous insertion function.
+ *
+ * Since: UNRELEASED
+ */
+void
+gdata_calendar_service_insert_calendar_event_async (GDataCalendarService *self,
+                                                    GDataCalendarCalendar *calendar,
+                                                    GDataCalendarEvent *event,
+                                                    GCancellable *cancellable,
+                                                    GAsyncReadyCallback callback,
+                                                    gpointer user_data)
+{
+       gchar *uri;
+
+       g_return_if_fail (GDATA_IS_CALENDAR_SERVICE (self));
+       g_return_if_fail (GDATA_IS_CALENDAR_EVENT (event));
+       g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+       uri = build_events_uri (calendar);
+       gdata_service_insert_entry_async (GDATA_SERVICE (self),
+                                         get_calendar_authorization_domain (),
+                                         uri, GDATA_ENTRY (event), cancellable,
+                                         callback, user_data);
+       g_free (uri);
+}
diff --git a/gdata/services/calendar/gdata-calendar-service.h 
b/gdata/services/calendar/gdata-calendar-service.h
index 8177491..47f245d 100644
--- a/gdata/services/calendar/gdata-calendar-service.h
+++ b/gdata/services/calendar/gdata-calendar-service.h
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2009 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2009, 2015 <philip tecnocode co uk>
  *
  * GData Client is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -95,10 +95,36 @@ void gdata_calendar_service_query_events_async (GDataCalendarService *self, GDat
 
 #include <gdata/services/calendar/gdata-calendar-event.h>
 
-GDataCalendarEvent *gdata_calendar_service_insert_event (GDataCalendarService *self, GDataCalendarEvent 
*event,
-                                                         GCancellable *cancellable, GError **error) 
G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
-void gdata_calendar_service_insert_event_async (GDataCalendarService *self, GDataCalendarEvent *event, 
GCancellable *cancellable,
-                                                GAsyncReadyCallback callback, gpointer user_data);
+#ifndef LIBGDATA_DISABLE_DEPRECATED
+GDataCalendarEvent *
+gdata_calendar_service_insert_event (GDataCalendarService *self,
+                                     GDataCalendarEvent *event,
+                                     GCancellable *cancellable,
+                                     GError **error)
+                                     G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC
+                                     G_GNUC_DEPRECATED_FOR (gdata_calendar_service_insert_calendar_event);
+void
+gdata_calendar_service_insert_event_async (GDataCalendarService *self,
+                                           GDataCalendarEvent *event,
+                                           GCancellable *cancellable,
+                                           GAsyncReadyCallback callback,
+                                           gpointer user_data)
+                                           G_GNUC_DEPRECATED_FOR 
(gdata_calendar_service_insert_calendar_event_async);
+#endif /* !LIBGDATA_DISABLE_DEPRECATED */
+
+GDataCalendarEvent *
+gdata_calendar_service_insert_calendar_event (GDataCalendarService *self,
+                                              GDataCalendarCalendar *calendar,
+                                              GDataCalendarEvent *event,
+                                              GCancellable *cancellable,
+                                              GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
+void
+gdata_calendar_service_insert_calendar_event_async (GDataCalendarService *self,
+                                                    GDataCalendarCalendar *calendar,
+                                                    GDataCalendarEvent *event,
+                                                    GCancellable *cancellable,
+                                                    GAsyncReadyCallback callback,
+                                                    gpointer user_data);
 
 G_END_DECLS
 
diff --git a/gdata/tests/calendar.c b/gdata/tests/calendar.c
index c54ddda..9a30eac 100644
--- a/gdata/tests/calendar.c
+++ b/gdata/tests/calendar.c
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2009–2010 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2009, 2010, 2014, 2015 <philip tecnocode co uk>
  *
  * GData Client is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -23,9 +23,16 @@
 
 #include "gdata.h"
 #include "common.h"
+#include "gdata-dummy-authorizer.h"
 
 static UhmServer *mock_server = NULL;
 
+#undef CLIENT_ID  /* from common.h */
+
+#define CLIENT_ID "352818697630-nqu2cmt5quqd6lr17ouoqmb684u84l1f.apps.googleusercontent.com"
+#define CLIENT_SECRET "-fA4pHQJxR3zJ-FyAMPQsikg"
+#define REDIRECT_URI "urn:ietf:wg:oauth:2.0:oob"
+
 typedef struct {
        GDataCalendarCalendar *calendar;
 } TempCalendarData;
@@ -46,7 +53,7 @@ set_up_temp_calendar (TempCalendarData *data, gconstpointer service)
        gdata_calendar_calendar_set_color (calendar, &colour);
        data->calendar = GDATA_CALENDAR_CALENDAR (gdata_service_insert_entry (GDATA_SERVICE (service),
                                                                              
gdata_calendar_service_get_primary_authorization_domain (),
-                                                                             
"https://www.google.com/calendar/feeds/default/owncalendars/full";,
+                                                                             
"https://www.googleapis.com/calendar/v3/calendars";,
                                                                              GDATA_ENTRY (calendar), NULL, 
NULL));
        g_assert (GDATA_IS_CALENDAR_CALENDAR (data->calendar));
        g_object_unref (calendar);
@@ -70,75 +77,48 @@ tear_down_temp_calendar (TempCalendarData *data, gconstpointer service)
 static void
 test_authentication (void)
 {
-       gboolean retval;
-       GDataClientLoginAuthorizer *authorizer;
-       GError *error = NULL;
+       GDataOAuth2Authorizer *authorizer = NULL;  /* owned */
+       gchar *authentication_uri, *authorisation_code;
 
        gdata_test_mock_server_start_trace (mock_server, "authentication");
 
-       /* Create an authorizer */
-       authorizer = gdata_client_login_authorizer_new (CLIENT_ID, GDATA_TYPE_CALENDAR_SERVICE);
+       authorizer = gdata_oauth2_authorizer_new (CLIENT_ID, CLIENT_SECRET,
+                                                 REDIRECT_URI,
+                                                 GDATA_TYPE_CALENDAR_SERVICE);
 
-       g_assert_cmpstr (gdata_client_login_authorizer_get_client_id (authorizer), ==, CLIENT_ID);
+       /* Get an authentication URI. */
+       authentication_uri = gdata_oauth2_authorizer_build_authentication_uri (authorizer, NULL, FALSE);
+       g_assert (authentication_uri != NULL);
 
-       /* Log in */
-       retval = gdata_client_login_authorizer_authenticate (authorizer, USERNAME, PASSWORD, NULL, &error);
-       g_assert_no_error (error);
-       g_assert (retval == TRUE);
-       g_clear_error (&error);
+       /* Get the authorisation code off the user. */
+       if (uhm_server_get_enable_online (mock_server)) {
+               authorisation_code = gdata_test_query_user_for_verifier (authentication_uri);
+       } else {
+               /* Hard coded, extracted from the trace file. TODO */
+               authorisation_code = g_strdup 
("4/OEX-S1iMbOA_dOnNgUlSYmGWh3TK.QrR73axcNMkWoiIBeO6P2m_su7cwkQI");
+       }
 
-       /* Check all is as it should be */
-       g_assert_cmpstr (gdata_client_login_authorizer_get_username (authorizer), ==, USERNAME);
-       g_assert_cmpstr (gdata_client_login_authorizer_get_password (authorizer), ==, PASSWORD);
+       g_free (authentication_uri);
 
+       if (authorisation_code == NULL) {
+               /* Skip tests. */
+               goto skip_test;
+       }
+
+       /* Authorise the token */
+       g_assert (gdata_oauth2_authorizer_request_authorization (authorizer, authorisation_code, NULL, NULL) 
== TRUE);
+
+       /* Check all is as it should be */
        g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER (authorizer),
                                                             
gdata_calendar_service_get_primary_authorization_domain ()) == TRUE);
 
+skip_test:
+       g_free (authorisation_code);
        g_object_unref (authorizer);
 
        uhm_server_end_trace (mock_server);
 }
 
-GDATA_ASYNC_TEST_FUNCTIONS (authentication, void,
-G_STMT_START {
-       GDataClientLoginAuthorizer *authorizer;
-
-       /* Create an authorizer */
-       authorizer = gdata_client_login_authorizer_new (CLIENT_ID, GDATA_TYPE_CALENDAR_SERVICE);
-
-       g_assert_cmpstr (gdata_client_login_authorizer_get_client_id (authorizer), ==, CLIENT_ID);
-
-       gdata_client_login_authorizer_authenticate_async (authorizer, USERNAME, PASSWORD, cancellable, 
async_ready_callback, async_data);
-
-       g_object_unref (authorizer);
-} G_STMT_END,
-G_STMT_START {
-       gboolean retval;
-       GDataClientLoginAuthorizer *authorizer = GDATA_CLIENT_LOGIN_AUTHORIZER (obj);
-
-       retval = gdata_client_login_authorizer_authenticate_finish (authorizer, async_result, &error);
-
-       if (error == NULL) {
-               g_assert (retval == TRUE);
-
-               /* Check all is as it should be */
-               g_assert_cmpstr (gdata_client_login_authorizer_get_username (authorizer), ==, USERNAME);
-               g_assert_cmpstr (gdata_client_login_authorizer_get_password (authorizer), ==, PASSWORD);
-
-               g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER (authorizer),
-                                                                    
gdata_calendar_service_get_primary_authorization_domain ()) == TRUE);
-       } else {
-               g_assert (retval == FALSE);
-
-               /* Check nothing's changed */
-               g_assert_cmpstr (gdata_client_login_authorizer_get_username (authorizer), ==, NULL);
-               g_assert_cmpstr (gdata_client_login_authorizer_get_password (authorizer), ==, NULL);
-
-               g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER (authorizer),
-                                                                    
gdata_calendar_service_get_primary_authorization_domain ()) == FALSE);
-       }
-} G_STMT_END);
-
 typedef struct {
        GDataCalendarCalendar *calendar1;
        GDataCalendarCalendar *calendar2;
@@ -160,7 +140,7 @@ set_up_query_calendars (QueryCalendarsData *data, gconstpointer service)
        gdata_calendar_calendar_set_color (calendar, &colour);
        data->calendar1 = GDATA_CALENDAR_CALENDAR (gdata_service_insert_entry (GDATA_SERVICE (service),
                                                                               
gdata_calendar_service_get_primary_authorization_domain (),
-                                                                              
"https://www.google.com/calendar/feeds/default/owncalendars/full";,
+                                                                              
"https://www.googleapis.com/calendar/v3/calendars";,
                                                                               GDATA_ENTRY (calendar), NULL, 
NULL));
        g_assert (GDATA_IS_CALENDAR_CALENDAR (data->calendar1));
        g_object_unref (calendar);
@@ -170,7 +150,7 @@ set_up_query_calendars (QueryCalendarsData *data, gconstpointer service)
        gdata_calendar_calendar_set_color (calendar, &colour);
        data->calendar2 = GDATA_CALENDAR_CALENDAR (gdata_service_insert_entry (GDATA_SERVICE (service),
                                                                               
gdata_calendar_service_get_primary_authorization_domain (),
-                                                                              
"https://www.google.com/calendar/feeds/default/owncalendars/full";,
+                                                                              
"https://www.googleapis.com/calendar/v3/calendars";,
                                                                               GDATA_ENTRY (calendar), NULL, 
NULL));
        g_assert (GDATA_IS_CALENDAR_CALENDAR (data->calendar2));
        g_object_unref (calendar);
@@ -340,6 +320,8 @@ static void
 set_up_query_events (QueryEventsData *data, gconstpointer service)
 {
        GDataCalendarEvent *event;
+       GDataGDWhen *when;
+       GError *error = NULL;
 
        /* Set up a temporary calendar */
        set_up_temp_calendar ((TempCalendarData*) data, service);
@@ -349,19 +331,37 @@ set_up_query_events (QueryEventsData *data, gconstpointer service)
        /* Add some test events to it */
        event = gdata_calendar_event_new (NULL);
        gdata_entry_set_title (GDATA_ENTRY (event), "Test Event 1");
-       data->event1 = gdata_calendar_service_insert_event (GDATA_CALENDAR_SERVICE (service), event, NULL, 
NULL);
+
+       when = gdata_gd_when_new (1419113727, 1419113728, FALSE);
+       gdata_calendar_event_add_time (event, when);
+       g_object_unref (when);
+
+       data->event1 = gdata_calendar_service_insert_calendar_event (GDATA_CALENDAR_SERVICE (service), 
data->parent.calendar, event, NULL, &error);
+       g_assert_no_error (error);
        g_assert (GDATA_IS_CALENDAR_EVENT (data->event1));
        g_object_unref (event);
 
        event = gdata_calendar_event_new (NULL);
        gdata_entry_set_title (GDATA_ENTRY (event), "Test Event 2");
-       data->event2 = gdata_calendar_service_insert_event (GDATA_CALENDAR_SERVICE (service), event, NULL, 
NULL);
+
+       when = gdata_gd_when_new (1419113000, 1419114000, FALSE);
+       gdata_calendar_event_add_time (event, when);
+       g_object_unref (when);
+
+       data->event2 = gdata_calendar_service_insert_calendar_event (GDATA_CALENDAR_SERVICE (service), 
data->parent.calendar, event, NULL, &error);
+       g_assert_no_error (error);
        g_assert (GDATA_IS_CALENDAR_EVENT (data->event2));
        g_object_unref (event);
 
        event = gdata_calendar_event_new (NULL);
        gdata_entry_set_title (GDATA_ENTRY (event), "Test Event 3");
-       data->event3 = gdata_calendar_service_insert_event (GDATA_CALENDAR_SERVICE (service), event, NULL, 
NULL);
+
+       when = gdata_gd_when_new (1419110000, 1419120000, TRUE);
+       gdata_calendar_event_add_time (event, when);
+       g_object_unref (when);
+
+       data->event3 = gdata_calendar_service_insert_calendar_event (GDATA_CALENDAR_SERVICE (service), 
data->parent.calendar, event, NULL, &error);
+       g_assert_no_error (error);
        g_assert (GDATA_IS_CALENDAR_EVENT (data->event3));
        g_object_unref (event);
 
@@ -519,7 +519,8 @@ test_event_insert (InsertEventData *data, gconstpointer service)
        g_object_unref (when);
 
        /* Insert the event */
-       new_event = data->new_event = gdata_calendar_service_insert_event (GDATA_CALENDAR_SERVICE (service), 
event, NULL, &error);
+       new_event = data->new_event = gdata_calendar_service_insert_calendar_event (GDATA_CALENDAR_SERVICE 
(service),
+                                                                                   data->parent.calendar, 
event, NULL, &error);
        g_assert_no_error (error);
        g_assert (GDATA_IS_CALENDAR_EVENT (new_event));
        g_clear_error (&error);
@@ -561,7 +562,9 @@ G_STMT_START {
        g_object_unref (when);
 
        /* Insert the event */
-       gdata_calendar_service_insert_event_async (GDATA_CALENDAR_SERVICE (service), event, cancellable, 
async_ready_callback, async_data);
+       gdata_calendar_service_insert_calendar_event_async (GDATA_CALENDAR_SERVICE (service),
+                                                           data->parent.calendar, event, cancellable,
+                                                           async_ready_callback, async_data);
 
        g_object_unref (event);
 } G_STMT_END,
@@ -578,7 +581,7 @@ G_STMT_START {
 } G_STMT_END);
 
 static void
-test_event_xml (void)
+test_event_json (void)
 {
        GDataCalendarEvent *event;
        GDataGDWhere *where;
@@ -604,174 +607,204 @@ test_event_xml (void)
        gdata_calendar_event_add_time (event, when);
        g_object_unref (when);
 
-       /* Check the XML */
-       gdata_test_assert_xml (event,
-               "<?xml version='1.0' encoding='UTF-8'?>"
-               "<entry xmlns='http://www.w3.org/2005/Atom' "
-                      "xmlns:gd='http://schemas.google.com/g/2005' "
-                      "xmlns:gCal='http://schemas.google.com/gCal/2005' "
-                      "xmlns:app='http://www.w3.org/2007/app'>"
-                       "<title type='text'>Tennis with Beth</title>"
-                       "<content type='text'>Meet for a quick lesson.</content>"
-                       "<category term='http://schemas.google.com/g/2005#event' 
scheme='http://schemas.google.com/g/2005#kind'/>"
-                       "<gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'/>"
-                       "<gd:transparency value='http://schemas.google.com/g/2005#event.opaque'/>"
-                       "<gCal:guestsCanModify value='false'/>"
-                       "<gCal:guestsCanInviteOthers value='false'/>"
-                       "<gCal:guestsCanSeeGuests value='false'/>"
-                       "<gCal:anyoneCanAddSelf value='false'/>"
-                       "<gd:when startTime='2009-04-17T15:00:00Z' endTime='2009-04-17T17:00:00Z'/>"
-                       "<gd:who email='john smith example com' "
-                               "rel='http://schemas.google.com/g/2005#event.organizer' "
-                               "valueString='John Smith\342\200\275'/>"
-                       "<gd:where valueString='Rolling Lawn Courts'/>"
-               "</entry>");
+       /* Check the JSON */
+       gdata_test_assert_json (event, "{"
+               "'summary': 'Tennis with Beth',"
+               "'description': 'Meet for a quick lesson.',"
+               "'kind': 'calendar#event',"
+               "'status': 'confirmed',"
+               "'transparency': 'opaque',"
+               "'guestsCanModify': false,"
+               "'guestsCanInviteOthers': false,"
+               "'guestsCanSeeOtherGuests': false,"
+               "'anyoneCanAddSelf': false,"
+               "'start': {"
+                       "'dateTime': '2009-04-17T15:00:00Z',"
+                       "'timeZone': 'UTC'"
+               "},"
+               "'end': {"
+                       "'dateTime': '2009-04-17T17:00:00Z',"
+                       "'timeZone': 'UTC'"
+               "},"
+               "'attendees': ["
+                       "{"
+                               "'email': 'john smith example com',"
+                               "'displayName': 'John Smith‽',"
+                               "'organizer': true"
+                       "}"
+               "],"
+               "'organizer': {"
+                       "'email': 'john smith example com',"
+                       "'displayName': 'John Smith‽'"
+               "},"
+               "'location': 'Rolling Lawn Courts'"
+       "}");
 }
 
 static void
-test_event_xml_dates (void)
+test_event_json_dates (void)
 {
-       GDataCalendarEvent *event;
-       GList *i;
-       GDataGDWhen *when;
-       gint64 _time;
-       GError *error = NULL;
-
-       event = GDATA_CALENDAR_EVENT (gdata_parsable_new_from_xml (GDATA_TYPE_CALENDAR_EVENT,
-               "<entry xmlns='http://www.w3.org/2005/Atom' "
-                      "xmlns:gd='http://schemas.google.com/g/2005' "
-                      "xmlns:gCal='http://schemas.google.com/gCal/2005' "
-                      "xmlns:app='http://www.w3.org/2007/app'>"
-                       "<title type='text'>Tennis with Beth</title>"
-                       "<content type='text'>Meet for a quick lesson.</content>"
-                       "<category term='http://schemas.google.com/g/2005#event' 
scheme='http://schemas.google.com/g/2005#kind'/>"
-                       "<gd:when startTime='2009-04-17'/>"
-                       "<gd:when startTime='2009-04-17T15:00:00Z'/>"
-                       "<gd:when startTime='2009-04-27' endTime='20090506'/>"
-               "</entry>", -1, &error));
-       g_assert_no_error (error);
-       g_assert (GDATA_IS_ENTRY (event));
-       g_clear_error (&error);
-
-       /* Check the times */
-       i = gdata_calendar_event_get_times (event);
-
-       /* First time */
-       when = GDATA_GD_WHEN (i->data);
-       g_assert (i->next != NULL);
-       g_assert (gdata_gd_when_is_date (when) == TRUE);
-       _time = gdata_gd_when_get_start_time (when);
-       g_assert_cmpint (_time, ==, 1239926400);
-       _time = gdata_gd_when_get_end_time (when);
-       g_assert_cmpint (_time, ==, -1);
-       g_assert (gdata_gd_when_get_value_string (when) == NULL);
-       g_assert (gdata_gd_when_get_reminders (when) == NULL);
-
-       /* Second time */
-       i = i->next;
-       when = GDATA_GD_WHEN (i->data);
-       g_assert (i->next != NULL);
-       g_assert (gdata_gd_when_is_date (when) == FALSE);
-       _time = gdata_gd_when_get_start_time (when);
-       g_assert_cmpint (_time, ==, 1239926400 + 54000);
-       _time = gdata_gd_when_get_end_time (when);
-       g_assert_cmpint (_time, ==, -1);
-       g_assert (gdata_gd_when_get_value_string (when) == NULL);
-       g_assert (gdata_gd_when_get_reminders (when) == NULL);
-
-       /* Third time */
-       i = i->next;
-       when = GDATA_GD_WHEN (i->data);
-       g_assert (i->next == NULL);
-       g_assert (gdata_gd_when_is_date (when) == TRUE);
-       _time = gdata_gd_when_get_start_time (when);
-       g_assert_cmpint (_time, ==, 1239926400 + 864000);
-       _time = gdata_gd_when_get_end_time (when);
-       g_assert_cmpint (_time, ==, 1241568000);
-       g_assert (gdata_gd_when_get_value_string (when) == NULL);
-       g_assert (gdata_gd_when_get_reminders (when) == NULL);
-
-       /* Check the XML */
-       gdata_test_assert_xml (event,
-               "<?xml version='1.0' encoding='UTF-8'?>"
-               "<entry xmlns='http://www.w3.org/2005/Atom' "
-                      "xmlns:gd='http://schemas.google.com/g/2005' "
-                      "xmlns:gCal='http://schemas.google.com/gCal/2005' "
-                      "xmlns:app='http://www.w3.org/2007/app'>"
-                       "<title type='text'>Tennis with Beth</title>"
-                       "<content type='text'>Meet for a quick lesson.</content>"
-                       "<category term='http://schemas.google.com/g/2005#event' 
scheme='http://schemas.google.com/g/2005#kind'/>"
-                       "<gCal:guestsCanModify value='false'/>"
-                       "<gCal:guestsCanInviteOthers value='false'/>"
-                       "<gCal:guestsCanSeeGuests value='false'/>"
-                       "<gCal:anyoneCanAddSelf value='false'/>"
-                       "<gd:when startTime='2009-04-17'/>"
-                       "<gd:when startTime='2009-04-17T15:00:00Z'/>"
-                       "<gd:when startTime='2009-04-27' endTime='2009-05-06'/>"
-               "</entry>");
-
-       g_object_unref (event);
+       guint i;
+
+       const struct {
+               const gchar *json;
+               gboolean is_date;
+               gint64 start_time;
+               gint64 end_time;
+               const gchar *output_json;  /* NULL if equal to @json */
+       } test_vectors[] = {
+               /* Plain date, single day. */
+               { "'start': {"
+                       "'date': '2009-04-17',"
+                       "'timeZone': 'UTC'"
+               "},"
+               "'end': {"
+                       "'date': '2009-04-18',"
+                       "'timeZone': 'UTC'"
+               "}", TRUE, 1239926400, 1239926400 + 86400, NULL },
+               /* Full date and time. */
+               { "'start': {"
+                       "'dateTime': '2009-04-17T15:00:00Z',"
+                       "'timeZone': 'UTC'"
+               "},"
+               "'end': {"
+                       "'dateTime': '2009-04-17T16:00:00Z',"
+                       "'timeZone': 'UTC'"
+               "}", FALSE, 1239926400 + 54000, 1239926400 + 54000 + 3600, NULL },
+               /* Start and end time. */
+               { "'start': {"
+                       "'date': '2009-04-27',"
+                       "'timeZone': 'UTC'"
+               "},"
+               "'end': {"
+                       "'date': '20090506',"
+                       "'timeZone': 'UTC'"
+               "}", TRUE, 1239926400 + 864000, 1241568000, "'start': {"
+                       "'date': '2009-04-27',"
+                       "'timeZone': 'UTC'"
+               "},"
+               "'end': {"
+                       "'date': '2009-05-06',"
+                       "'timeZone': 'UTC'"
+               "}" },
+       };
+
+       for (i = 0; i < G_N_ELEMENTS (test_vectors); i++) {
+               gchar *json = NULL, *output_json = NULL;  /* owned */
+               GDataCalendarEvent *event;
+               GList *j;
+               GDataGDWhen *when;
+               gint64 _time;
+               GError *error = NULL;
+
+               json = g_strdup_printf ("{"
+                       "'summary': 'Tennis with Beth',"
+                       "'description': 'Meet for a quick lesson.',"
+                       "'kind': 'calendar#event',"
+                       "%s"
+               "}", test_vectors[i].json);
+               output_json = g_strdup_printf ("{"
+                       "'summary': 'Tennis with Beth',"
+                       "'description': 'Meet for a quick lesson.',"
+                       "'kind': 'calendar#event',"
+                       "'guestsCanModify': false,"
+                       "'guestsCanInviteOthers': false,"
+                       "'guestsCanSeeOtherGuests': false,"
+                       "'anyoneCanAddSelf': false,"
+                       "'attendees': [],"
+                       "%s"
+               "}", (test_vectors[i].output_json != NULL) ? test_vectors[i].output_json : 
test_vectors[i].json);
+
+               event = GDATA_CALENDAR_EVENT (gdata_parsable_new_from_json (GDATA_TYPE_CALENDAR_EVENT, json, 
-1, &error));
+               g_assert_no_error (error);
+               g_assert (GDATA_IS_ENTRY (event));
+               g_clear_error (&error);
+
+               /* Check the times */
+               j = gdata_calendar_event_get_times (event);
+               g_assert (j != NULL);
+
+               when = GDATA_GD_WHEN (j->data);
+               g_assert (gdata_gd_when_is_date (when) == test_vectors[i].is_date);
+               _time = gdata_gd_when_get_start_time (when);
+               g_assert_cmpint (_time, ==, test_vectors[i].start_time);
+               _time = gdata_gd_when_get_end_time (when);
+               g_assert_cmpint (_time, ==, test_vectors[i].end_time);
+               g_assert (gdata_gd_when_get_value_string (when) == NULL);
+               g_assert (gdata_gd_when_get_reminders (when) == NULL);
+
+               /* Should be no other times. */
+               g_assert (j->next == NULL);
+
+               /* Check the JSON */
+               gdata_test_assert_json (event, output_json);
+
+               g_object_unref (event);
+               g_free (output_json);
+               g_free (json);
+       }
 }
 
 static void
-test_event_xml_recurrence (void)
+test_event_json_recurrence (void)
 {
        GDataCalendarEvent *event;
        GError *error = NULL;
        gchar *id, *uri;
 
-       event = GDATA_CALENDAR_EVENT (gdata_parsable_new_from_xml (GDATA_TYPE_CALENDAR_EVENT,
-               "<entry xmlns='http://www.w3.org/2005/Atom' "
-                      "xmlns:gd='http://schemas.google.com/g/2005' "
-                      "xmlns:gCal='http://schemas.google.com/gCal/2005' "
-                      "xmlns:app='http://www.w3.org/2007/app'>"
-                       "<id>http://www.google.com/calendar/feeds/libgdata test googlemail 
com/events/g5928e82rrch95b25f8ud0dlsg_20090429T153000Z</id>"
-                       "<published>2009-04-25T15:22:47.000Z</published>"
-                       "<updated>2009-04-27T17:54:10.000Z</updated>"
-                       "<app:edited 
xmlns:app='http://www.w3.org/2007/app'>2009-04-27T17:54:10.000Z</app:edited>"
-                       "<category scheme='http://schemas.google.com/g/2005#kind' 
term='http://schemas.google.com/g/2005#event'/>"
-                       "<title>Test daily instance event</title>"
-                       "<content></content>"
-                       "<link rel='http://www.iana.org/assignments/relation/alternate' type='text/html' "
-                             "href='http://www.google.com/calendar/event?";
-                                   
"eid=ZzU5MjhlODJycmNoOTViMjVmOHVkMGRsc2dfMjAwOTA0MjlUMTUzMDAwWiBsaWJnZGF0YS50ZXN0QGdvb2dsZW1haWwuY29t' "
-                             "title='alternate'/>"
-                       "<link rel='http://www.iana.org/assignments/relation/self' 
type='application/atom+xml' "
-                             "href='http://www.google.com/calendar/feeds/libgdata test googlemail 
com/private/full/"
-                                   "g5928e82rrch95b25f8ud0dlsg_20090429T153000Z'/>"
-                       "<link rel='http://www.iana.org/assignments/relation/edit' 
type='application/atom+xml' "
-                             "href='http://www.google.com/calendar/feeds/libgdata test googlemail 
com/private/full/"
-                                   "g5928e82rrch95b25f8ud0dlsg_20090429T153000Z'/>"
-                       "<author>"
-                               "<name>GData Test</name>"
-                               "<email>libgdata test googlemail com</email>"
-                       "</author>"
-                       "<gd:originalEvent id='g5928e82rrch95b25f8ud0dlsg' "
-                                         "href='http://www.google.com/calendar/feeds/libgdata test 
googlemail com/private/full/"
-                                               "g5928e82rrch95b25f8ud0dlsg'>"
-                               "<gd:when startTime='2009-04-29T16:30:00.000+01:00'/>"
-                       "</gd:originalEvent>"
-                       "<gCal:guestsCanModify value='false'/>"
-                       "<gCal:guestsCanInviteOthers value='false'/>"
-                       "<gCal:guestsCanSeeGuests value='false'/>"
-                       "<gCal:anyoneCanAddSelf value='false'/>"
-                       "<gd:comments>"
-                               "<gd:feedLink href='http://www.google.com/calendar/feeds/libgdata test 
googlemail com/private/full/"
-                                                  "g5928e82rrch95b25f8ud0dlsg_20090429T153000Z/comments'/>"
-                       "</gd:comments>"
-                       "<gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'/>"
-                       "<gd:visibility value='http://schemas.google.com/g/2005#event.private'/>"
-                       "<gd:transparency value='http://schemas.google.com/g/2005#event.opaque'/>"
-                       "<gCal:uid value='g5928e82rrch95b25f8ud0dlsg google com'/>"
-                       "<gCal:sequence value='0'/>"
-                       "<gd:when startTime='2009-04-29T17:30:00.000+01:00' 
endTime='2009-04-29T17:30:00.000+01:00'>"
-                               "<gd:reminder minutes='10' method='email'/>"
-                               "<gd:reminder minutes='10' method='alert'/>"
-                       "</gd:when>"
-                       "<gd:who rel='http://schemas.google.com/g/2005#event.organizer' valueString='GData 
Test' "
-                               "email='libgdata test googlemail com'/>"
-                       "<gd:where valueString=''/>"
-               "</entry>", -1, &error));
+       event = GDATA_CALENDAR_EVENT (gdata_parsable_new_from_json (GDATA_TYPE_CALENDAR_EVENT, "{"
+               "'id': 'https://www.googleapis.com/calendar/v3/calendars/libgdata test googlemail 
com/events/g5928e82rrch95b25f8ud0dlsg_20090429T153000Z',"
+               "'updated': '2009-04-27T17:54:10.000Z',"
+               "'summary': 'Test daily instance event',"
+               "'kind': 'calendar#event',"
+               "'creator': {"
+                       "'displayName': 'GData Test',"
+                       "'email': 'libgdata test googlemail com'"
+               "},"
+               "'recurringEventId': 'g5928e82rrch95b25f8ud0dlsg',"
+               "'originalStartTime': {"
+                       "'dateTime': '2009-04-29T16:30:00.000+01:00',"
+                       "'timeZone': 'UTC'"
+               "},"
+               "'guestsCanModify': false,"
+               "'guestsCanInviteOthers': false,"
+               "'guestsCanSeeOtherGuests': false,"
+               "'anyoneCanAddSelf': false,"
+               "'status': 'confirmed',"
+               "'visibility': 'private',"
+               "'transparency': 'opaque',"
+               "'iCalUID': 'g5928e82rrch95b25f8ud0dlsg google com',"
+               "'sequence': '0',"
+               "'start': {"
+                       "'dateTime': '2009-04-29T17:30:00.000+01:00',"
+                       "'timeZone': 'UTC'"
+               "},"
+               "'end': {"
+                       "'dateTime': '2009-04-29T17:30:00.000+01:00',"
+                       "'timeZone': 'UTC'"
+               "},"
+               "'reminders': {"
+                       "'overrides': [{"
+                               "'method': 'email',"
+                               "'minutes': 10"
+                       "}, {"
+                               "'method': 'popup',"
+                               "'minutes': 10"
+                       "}]"
+               "},"
+               "'attendees': ["
+                       "{"
+                               "'email': 'libgdata test googlemail com',"
+                               "'displayName': 'GData Test',"
+                               "'organizer': true,"
+                               "'responseStatus': 'needsAction'"
+                       "}"
+               "],"
+               "'organizer': {"
+                       "'email': 'libgdata test googlemail com',"
+                       "'displayName': 'GData Test'"
+               "}"
+       "}", -1, &error));
        g_assert_no_error (error);
        g_assert (GDATA_IS_ENTRY (event));
        g_clear_error (&error);
@@ -781,7 +814,7 @@ test_event_xml_recurrence (void)
 
        gdata_calendar_event_get_original_event_details (event, &id, &uri);
        g_assert_cmpstr (id, ==, "g5928e82rrch95b25f8ud0dlsg");
-       g_assert_cmpstr (uri, ==, "http://www.google.com/calendar/feeds/libgdata test googlemail 
com/private/full/g5928e82rrch95b25f8ud0dlsg");
+       g_assert_cmpstr (uri, ==, "https://www.googleapis.com/calendar/v3/events/g5928e82rrch95b25f8ud0dlsg";);
 
        g_free (id);
        g_free (uri);
@@ -796,18 +829,14 @@ test_calendar_escaping (void)
        calendar = gdata_calendar_calendar_new (NULL);
        gdata_calendar_calendar_set_timezone (calendar, "<timezone>");
 
-       /* Check the outputted XML is escaped properly */
-       gdata_test_assert_xml (calendar,
-               "<?xml version='1.0' encoding='UTF-8'?>"
-               "<entry xmlns='http://www.w3.org/2005/Atom' xmlns:gd='http://schemas.google.com/g/2005' "
-                      "xmlns:gCal='http://schemas.google.com/gCal/2005' 
xmlns:app='http://www.w3.org/2007/app'>"
-                       "<title type='text'></title>"
-                       "<category term='http://schemas.google.com/gCal/2005#calendarmeta' 
scheme='http://schemas.google.com/g/2005#kind'/>"
-                       "<gCal:timezone value='&lt;timezone&gt;'/>"
-                       "<gCal:hidden value='false'/>"
-                       "<gCal:color value='#000000'/>"
-                       "<gCal:selected value='false'/>"
-               "</entry>");
+       /* Check the outputted JSON is escaped properly */
+       gdata_test_assert_json (calendar, "{"
+               "'kind': 'calendar#calendar',"
+               "'timeZone': '<timezone>',"
+               "'hidden': false,"
+               "'backgroundColor': '#000000',"
+               "'selected': false"
+       "}");
        g_object_unref (calendar);
 }
 
@@ -817,29 +846,90 @@ test_event_escaping (void)
        GDataCalendarEvent *event;
 
        event = gdata_calendar_event_new (NULL);
-       gdata_calendar_event_set_status (event, "<status>");
-       gdata_calendar_event_set_visibility (event, "<visibility>");
-       gdata_calendar_event_set_transparency (event, "<transparency>");
-       gdata_calendar_event_set_uid (event, "<uid>");
-       gdata_calendar_event_set_recurrence (event, "<recurrence>");
-
-       /* Check the outputted XML is escaped properly */
-       gdata_test_assert_xml (event,
-               "<?xml version='1.0' encoding='UTF-8'?>"
-               "<entry xmlns='http://www.w3.org/2005/Atom' xmlns:gd='http://schemas.google.com/g/2005' "
-                      "xmlns:gCal='http://schemas.google.com/gCal/2005' 
xmlns:app='http://www.w3.org/2007/app'>"
-                       "<title type='text'></title>"
-                       "<category term='http://schemas.google.com/g/2005#event' 
scheme='http://schemas.google.com/g/2005#kind'/>"
-                       "<gd:eventStatus value='&lt;status&gt;'/>"
-                       "<gd:visibility value='&lt;visibility&gt;'/>"
-                       "<gd:transparency value='&lt;transparency&gt;'/>"
-                       "<gCal:uid value='&lt;uid&gt;'/>"
-                       "<gCal:guestsCanModify value='false'/>"
-                       "<gCal:guestsCanInviteOthers value='false'/>"
-                       "<gCal:guestsCanSeeGuests value='false'/>"
-                       "<gCal:anyoneCanAddSelf value='false'/>"
-                       "<gd:recurrence>&lt;recurrence&gt;</gd:recurrence>"
-               "</entry>");
+       gdata_calendar_event_set_status (event, "\"status\"");
+       gdata_calendar_event_set_visibility (event, "\"visibility\"");
+       gdata_calendar_event_set_transparency (event, "\"transparency\"");
+       gdata_calendar_event_set_uid (event, "\"uid\"");
+       gdata_calendar_event_set_recurrence (event, "\"recurrence\"");
+
+       /* Check the outputted JSON is escaped properly */
+       gdata_test_assert_json (event, "{"
+               "'kind': 'calendar#event',"
+               "'status': '\"status\"',"
+               "'transparency': '\"transparency\"',"
+               "'visibility': '\"visibility\"',"
+               "'iCalUID': '\"uid\"',"
+               "'recurrence': [ '\"recurrence\"' ],"
+               "'guestsCanModify': false,"
+               "'guestsCanInviteOthers': false,"
+               "'guestsCanSeeOtherGuests': false,"
+               "'anyoneCanAddSelf': false,"
+               "'attendees': []"
+       "}");
+       g_object_unref (event);
+}
+
+/* Test the event parser with the minimal number of properties specified. */
+static void
+test_calendar_event_parser_minimal (void)
+{
+       GDataCalendarEvent *event = NULL;  /* owned */
+       GDataEntry *entry;  /* unowned */
+       GDataLink *self_link;  /* unowned */
+       GError *error = NULL;
+
+       event = GDATA_CALENDAR_EVENT (gdata_parsable_new_from_json (GDATA_TYPE_CALENDAR_EVENT,
+               "{"
+                       "\"kind\": \"calendar#event\","
+                       "\"etag\": \"\\\"2838230136828000\\\"\","
+                       "\"id\": \"hsfgtc50u68vdai81t6634u7lg\","
+                       "\"status\": \"confirmed\","
+                       "\"htmlLink\": 
\"https://www.google.com/calendar/event?eid=aHNmZ3RjNTB1Njh2ZGFpODF0NjYzNHU3bGcgODk5MWkzNjM0YzRzN3Nwa3NrcjNjZjVuanNAZw\",";
+                       "\"created\": \"2014-12-20T22:37:48.000Z\","
+                       "\"updated\": \"2014-12-20T22:37:48.414Z\","
+                       "\"summary\": \"Test Event 1\","
+                       "\"creator\": {"
+                               "\"email\": \"libgdata test googlemail com\","
+                               "\"displayName\": \"GData Test\""
+                       "},"
+                       "\"organizer\": {"
+                               "\"email\": \"8991i3634c4s7spkskr3cf5njs group calendar google com\","
+                               "\"displayName\": \"Temp Test Calendar\","
+                               "\"self\": true"
+                       "},"
+                       "\"start\": {"
+                               "\"dateTime\": \"2014-12-20T22:15:27Z\","
+                               "\"timeZone\": \"UTC\""
+                       "},"
+                       "\"end\": {"
+                               "\"dateTime\": \"2014-12-20T22:15:28Z\","
+                               "\"timeZone\": \"UTC\""
+                       "},"
+                       "\"iCalUID\": \"hsfgtc50u68vdai81t6634u7lg google com\","
+                       "\"sequence\": 0,"
+                       "\"guestsCanInviteOthers\": false,"
+                       "\"guestsCanSeeOtherGuests\": false,"
+                       "\"reminders\": {"
+                               "\"useDefault\": true"
+                       "}"
+               "}", -1, &error));
+       g_assert_no_error (error);
+       g_assert (GDATA_IS_CALENDAR_EVENT (event));
+       gdata_test_compare_kind (GDATA_ENTRY (event), "calendar#event", NULL);
+
+       entry = GDATA_ENTRY (event);
+
+       /* Check the event’s properties. */
+       g_assert_cmpstr (gdata_entry_get_id (entry), ==,
+                        "hsfgtc50u68vdai81t6634u7lg");
+       g_assert_cmpstr (gdata_entry_get_etag (entry), ==,
+                        "\"2838230136828000\"");
+       g_assert_cmpstr (gdata_entry_get_title (entry), ==,
+                        "Test Event 1");
+       g_assert_cmpint (gdata_entry_get_updated (entry), ==, 1419115068);
+
+       /* TODO: check everything else */
+
        g_object_unref (event);
 }
 
@@ -861,7 +951,7 @@ test_access_rule_properties (void)
 }
 
 static void
-test_access_rule_xml (void)
+test_access_rule_json (void)
 {
        GDataAccessRule *rule;
 
@@ -870,8 +960,8 @@ test_access_rule_xml (void)
        gdata_access_rule_set_role (rule, GDATA_CALENDAR_ACCESS_ROLE_EDITOR);
        gdata_access_rule_set_scope (rule, GDATA_ACCESS_SCOPE_USER, "darcy gmail com");
 
-       /* Check the XML */
-       gdata_test_assert_xml (rule,
+       /* Check the JSON */
+       gdata_test_assert_json (rule,
                "<?xml version='1.0' encoding='UTF-8'?>"
                "<entry xmlns='http://www.w3.org/2005/Atom' "
                       "xmlns:gd='http://schemas.google.com/g/2005' "
@@ -1442,7 +1532,66 @@ mock_server_notify_resolver_cb (GObject *object, GParamSpec *pspec, gpointer use
                const gchar *ip_address = uhm_server_get_address (server);
 
                uhm_resolver_add_A (resolver, "www.google.com", ip_address);
+               uhm_resolver_add_A (resolver, "www.googleapis.com", ip_address);
+               uhm_resolver_add_A (resolver,
+                                   "accounts.google.com", ip_address);
+       }
+}
+
+/* Set up a global GDataAuthorizer to be used for all the tests. Unfortunately,
+ * the Google Calendar API is limited to OAuth1 and OAuth2 authorisation, so
+ * this requires user interaction when online.
+ *
+ * If not online, use a dummy authoriser. */
+static GDataAuthorizer *
+create_global_authorizer (void)
+{
+       GDataOAuth2Authorizer *authorizer = NULL;  /* owned */
+       gchar *authentication_uri, *authorisation_code;
+       GError *error = NULL;
+
+       /* If not online, just return a dummy authoriser. */
+       if (!uhm_server_get_enable_online (mock_server)) {
+               return GDATA_AUTHORIZER (gdata_dummy_authorizer_new (GDATA_TYPE_CALENDAR_SERVICE));
+       }
+
+       /* Otherwise, go through the interactive OAuth dance. */
+       gdata_test_mock_server_start_trace (mock_server, "global-authentication");
+       authorizer = gdata_oauth2_authorizer_new (CLIENT_ID, CLIENT_SECRET,
+                                                 REDIRECT_URI,
+                                                 GDATA_TYPE_CALENDAR_SERVICE);
+
+       /* Get an authentication URI */
+       authentication_uri = gdata_oauth2_authorizer_build_authentication_uri (authorizer, NULL, FALSE);
+       g_assert (authentication_uri != NULL);
+
+       /* Get the authorisation code off the user. */
+       if (uhm_server_get_enable_online (mock_server)) {
+               authorisation_code = gdata_test_query_user_for_verifier (authentication_uri);
+       } else {
+               /* Hard coded, extracted from the trace file. TODO */
+               authorisation_code = g_strdup 
("4/hmXZtrXmXMqK1hwiWPZs9F_N6DK-.Ap4OICAUIe0WoiIBeO6P2m8IDoMxkQI");
+       }
+
+       g_free (authentication_uri);
+
+       if (authorisation_code == NULL) {
+               /* Skip tests. */
+               g_object_unref (authorizer);
+               authorizer = NULL;
+               goto skip_test;
        }
+
+       /* Authorise the token */
+       g_assert (gdata_oauth2_authorizer_request_authorization (authorizer, authorisation_code, NULL, 
&error));
+       g_assert_no_error (error);
+
+skip_test:
+       g_free (authorisation_code);
+
+       uhm_server_end_trace (mock_server);
+
+       return GDATA_AUTHORIZER (authorizer);
 }
 
 int
@@ -1460,19 +1609,12 @@ main (int argc, char *argv[])
        trace_directory = g_file_new_for_path (TEST_FILE_DIR "traces/calendar");
        uhm_server_set_trace_directory (mock_server, trace_directory);
        g_object_unref (trace_directory);
-
-       gdata_test_mock_server_start_trace (mock_server, "global-authentication");
-       authorizer = GDATA_AUTHORIZER (gdata_client_login_authorizer_new (CLIENT_ID, 
GDATA_TYPE_CALENDAR_SERVICE));
-       gdata_client_login_authorizer_authenticate (GDATA_CLIENT_LOGIN_AUTHORIZER (authorizer), USERNAME, 
PASSWORD, NULL, NULL);
-       uhm_server_end_trace (mock_server);
+#if 0
+       authorizer = create_global_authorizer ();
 
        service = GDATA_SERVICE (gdata_calendar_service_new (authorizer));
 
        g_test_add_func ("/calendar/authentication", test_authentication);
-       g_test_add ("/calendar/authentication/async", GDataAsyncTestData, NULL, gdata_set_up_async_test_data, 
test_authentication_async,
-                   gdata_tear_down_async_test_data);
-       g_test_add ("/calendar/authentication/async/cancellation", GDataAsyncTestData, NULL, 
gdata_set_up_async_test_data,
-                   test_authentication_async_cancellation, gdata_tear_down_async_test_data);
 
        g_test_add ("/calendar/query/all_calendars", QueryCalendarsData, service, set_up_query_calendars, 
test_query_all_calendars,
                    tear_down_query_calendars);
@@ -1514,21 +1656,29 @@ main (int argc, char *argv[])
                    tear_down_temp_calendar_acls);
        g_test_add ("/calendar/access-rule/delete", TempCalendarAclsData, service, set_up_temp_calendar_acls, 
test_access_rule_delete,
                    tear_down_temp_calendar_acls);
-
+#endif
+#if 0
+TODO
        g_test_add_data_func ("/calendar/batch", service, test_batch);
        g_test_add ("/calendar/batch/async", BatchAsyncData, service, setup_batch_async, test_batch_async, 
teardown_batch_async);
        g_test_add ("/calendar/batch/async/cancellation", BatchAsyncData, service, setup_batch_async, 
test_batch_async_cancellation,
                    teardown_batch_async);
+#endif
 
-       g_test_add_func ("/calendar/event/xml", test_event_xml);
-       g_test_add_func ("/calendar/event/xml/dates", test_event_xml_dates);
-       g_test_add_func ("/calendar/event/xml/recurrence", test_event_xml_recurrence);
+       g_test_add_func ("/calendar/event/json", test_event_json);
+       g_test_add_func ("/calendar/event/json/dates", test_event_json_dates);
+       g_test_add_func ("/calendar/event/json/recurrence", test_event_json_recurrence);
        g_test_add_func ("/calendar/event/escaping", test_event_escaping);
+       g_test_add_func ("/calendar/event/parser/minimal",
+                        test_calendar_event_parser_minimal);
 
        g_test_add_func ("/calendar/calendar/escaping", test_calendar_escaping);
 
+#if 0
+TODO
        g_test_add_func ("/calendar/access-rule/properties", test_access_rule_properties);
-       g_test_add_func ("/calendar/access-rule/xml", test_access_rule_xml);
+       g_test_add_func ("/calendar/access-rule/json", test_access_rule_json);
+#endif
 
        g_test_add_func ("/calendar/query/uri", test_query_uri);
        g_test_add_func ("/calendar/query/etag", test_query_etag);



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