[libgdata] [core] Batch operation support



commit 5c32aadbddb89eea4d8fc15626e179ce8a8f2d13
Author: Philip Withnall <philip tecnocode co uk>
Date:   Tue Jan 26 13:20:41 2010 +0000

    [core] Batch operation support
    
    Add batch operation support. Batch operations are executed by creating a new
    GDataBatchOperation, adding the required operations to it, then running it
    (sync or async). Batch operations are only supported by services which
    implement GDataBatchable.
    
    This includes full test cases and documentation.
    
    Closes: bgo#579169

 Makefile.am                       |   11 +-
 docs/reference/Makefile.am        |    4 +-
 docs/reference/gdata-docs.xml     |    6 +
 docs/reference/gdata-sections.txt |   44 +++
 gdata/atom/gdata-link.c           |   10 +-
 gdata/gdata-batch-feed.c          |  167 +++++++++
 gdata/gdata-batch-feed.h          |   68 ++++
 gdata/gdata-batch-operation.c     |  696 +++++++++++++++++++++++++++++++++++++
 gdata/gdata-batch-operation.h     |  139 ++++++++
 gdata/gdata-batch-private.h       |   44 +++
 gdata/gdata-batchable.c           |   73 ++++
 gdata/gdata-batchable.h           |   64 ++++
 gdata/gdata-entry.c               |   76 ++++
 gdata/gdata-feed.c                |   64 ++++
 gdata/gdata-private.h             |    6 +
 gdata/gdata-service.c             |    6 +
 gdata/gdata-service.h             |    8 +-
 gdata/gdata.h                     |    2 +
 gdata/gdata.symbols               |   13 +
 gdata/tests/common.c              |  217 ++++++++++++
 gdata/tests/common.h              |    7 +
 21 files changed, 1718 insertions(+), 7 deletions(-)
---
diff --git a/Makefile.am b/Makefile.am
index 356892f..cae4f48 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -40,7 +40,7 @@ gdata/gdata-enums.h: $(gdata_headers) Makefile
 
 gdata/gdata-enums.c: $(gdata_headers) Makefile gdata/gdata-enums.h
 	$(AM_V_GEN)($(GLIB_MKENUMS) \
-			--fhead "#include \"gdata-service.h\"\n#include \"gdata-parsable.h\"\n#include \"gdata-enums.h\"" \
+			--fhead "#include \"gdata-service.h\"\n#include \"gdata-parsable.h\"\n#include \"gdata-batch-operation.h\"\n#include \"gdata-enums.h\"" \
 			--fprod "\n/* enumerations from \"@filename \" */" \
 			--vhead "GType\n enum_name@_get_type (void)\n{\n  static GType etype = 0;\n  if (etype == 0) {\n    static const G Type@Value values[] = {" \
 			--vprod "      { @VALUENAME@, \"@VALUENAME \", \"@valuenick \" }," \
@@ -156,10 +156,14 @@ gdata_headers = \
 	gdata/gdata-parsable.h		\
 	gdata/gdata-download-stream.h	\
 	gdata/gdata-upload-stream.h	\
-	gdata/gdata-comparable.h
+	gdata/gdata-comparable.h	\
+	gdata/gdata-batch-operation.h	\
+	gdata/gdata-batchable.h
 # The following headers are private, and shouldn't be installed:
 private_headers = \
 	gdata/gdata-private.h		\
+	gdata/gdata-batch-private.h	\
+	gdata/gdata-batch-feed.h	\
 	gdata/gdata-parser.h		\
 	gdata/gdata-buffer.h		\
 	gdata/exif/gdata-exif-tags.h	\
@@ -287,6 +291,9 @@ gdata_libgdata_la_SOURCES = \
 	gdata/gdata-upload-stream.c	\
 	gdata/gdata-buffer.c		\
 	gdata/gdata-comparable.c	\
+	gdata/gdata-batch-operation.c	\
+	gdata/gdata-batchable.c		\
+	gdata/gdata-batch-feed.c	\
 	\
 	gdata/atom/gdata-author.c	\
 	gdata/atom/gdata-category.c	\
diff --git a/docs/reference/Makefile.am b/docs/reference/Makefile.am
index bb87103..3ef40dd 100644
--- a/docs/reference/Makefile.am
+++ b/docs/reference/Makefile.am
@@ -63,7 +63,9 @@ IGNORE_HFILES = \
 	gdata-picasaweb-enums.h	\
 	gdata-exif-tags.h	\
 	gdata-georss-where.h	\
-	gdata-buffer.h
+	gdata-buffer.h		\
+	gdata-batch-private.h	\
+	gdata-batch-feed.h
 
 # Images to copy into HTML directory.
 # e.g. HTML_IMAGES=$(top_srcdir)/gtk/stock-icons/stock_about_24.png
diff --git a/docs/reference/gdata-docs.xml b/docs/reference/gdata-docs.xml
index 40d7890..0026c7c 100644
--- a/docs/reference/gdata-docs.xml
+++ b/docs/reference/gdata-docs.xml
@@ -41,6 +41,12 @@
 			<xi:include href="xml/gdata-access-handler.xml"/>
 			<xi:include href="xml/gdata-access-rule.xml"/>
 		</chapter>
+
+		<chapter>
+			<title>Batch Operation API</title>
+			<xi:include href="xml/gdata-batchable.xml"/>
+			<xi:include href="xml/gdata-batch-operation.xml"/>
+		</chapter>
 	</part>
 
 	<part>
diff --git a/docs/reference/gdata-sections.txt b/docs/reference/gdata-sections.txt
index b366b73..03dfb77 100644
--- a/docs/reference/gdata-sections.txt
+++ b/docs/reference/gdata-sections.txt
@@ -751,6 +751,7 @@ GDataCategoryPrivate
 <FILE>gdata-link</FILE>
 <TITLE>GDataLink</TITLE>
 GDATA_LINK_ALTERNATE
+GDATA_LINK_BATCH
 GDATA_LINK_EDIT
 GDATA_LINK_EDIT_MEDIA
 GDATA_LINK_ENCLOSURE
@@ -2040,3 +2041,46 @@ GDATA_COMPARABLE_GET_IFACE
 GDATA_IS_COMPARABLE
 GDATA_TYPE_COMPARABLE
 </SECTION>
+
+<SECTION>
+<FILE>gdata-batch-operation</FILE>
+<TITLE>GDataBatchOperation</TITLE>
+GDataBatchOperation
+GDataBatchOperationClass
+GDataBatchOperationType
+GDataBatchOperationCallback
+gdata_batch_operation_add_query
+gdata_batch_operation_add_insertion
+gdata_batch_operation_add_update
+gdata_batch_operation_add_deletion
+gdata_batch_operation_run
+gdata_batch_operation_run_async
+gdata_batch_operation_run_finish
+gdata_batch_operation_get_service
+gdata_batch_operation_get_feed_uri
+<SUBSECTION Standard>
+GDATA_BATCH_OPERATION
+GDATA_IS_BATCH_OPERATION
+GDATA_TYPE_BATCH_OPERATION
+gdata_batch_operation_get_type
+GDATA_BATCH_OPERATION_GET_CLASS
+GDATA_BATCH_OPERATION_CLASS
+GDATA_IS_BATCH_OPERATION_CLASS
+<SUBSECTION Private>
+GDataBatchOperationPrivate
+</SECTION>
+
+<SECTION>
+<FILE>gdata-batchable</FILE>
+<TITLE>GDataBatchable</TITLE>
+GDataBatchable
+GDataBatchableIface
+gdata_batchable_create_operation
+<SUBSECTION Standard>
+gdata_batchable_get_type
+GDATA_BATCHABLE
+GDATA_BATCHABLE_CLASS
+GDATA_BATCHABLE_GET_IFACE
+GDATA_IS_BATCHABLE
+GDATA_TYPE_BATCHABLE
+</SECTION>
diff --git a/gdata/atom/gdata-link.c b/gdata/atom/gdata-link.c
index 581ffe8..3eddd86 100644
--- a/gdata/atom/gdata-link.c
+++ b/gdata/atom/gdata-link.c
@@ -344,8 +344,14 @@ pre_get_xml (GDataParsable *parsable, GString *xml_string)
 
 	if (priv->title != NULL)
 		gdata_parser_string_append_escaped (xml_string, " title='", priv->title, "'");
-	if (priv->relation_type != NULL)
-		g_string_append_printf (xml_string, " rel='%s'", priv->relation_type);
+	if (priv->relation_type != NULL) {
+		/* TODO: This hack is necessary to get batch operations working.
+		 * See: http://code.google.com/p/gdata-issues/issues/detail?id=2129 */
+		if (strcmp (priv->relation_type, GDATA_LINK_EDIT) == 0)
+			g_string_append_printf (xml_string, " rel='%s'", "edit");
+		else
+			g_string_append_printf (xml_string, " rel='%s'", priv->relation_type);
+	}
 	if (priv->content_type != NULL)
 		g_string_append_printf (xml_string, " type='%s'", priv->content_type);
 	if (priv->language != NULL)
diff --git a/gdata/gdata-batch-feed.c b/gdata/gdata-batch-feed.c
new file mode 100644
index 0000000..c59043b
--- /dev/null
+++ b/gdata/gdata-batch-feed.c
@@ -0,0 +1,167 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Philip Withnall 2010 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * SECTION:gdata-batch-feed
+ * @short_description: GData batch feed helper object
+ * @stability: Unstable
+ * @include: gdata/gdata-batch-feed.h
+ *
+ * Helper class to parse the feed returned from a batch operation and instantiate different types of #GDataEntry according to the batch operation
+ * associated with each one. It's tightly coupled with #GDataBatchOperation, and isn't exposed publicly.
+ *
+ * For more information, see the <ulink type="http" url="http://code.google.com/apis/gdata/docs/batch.html";>online documentation</ulink>.
+ *
+ * Since: 0.7.0
+ */
+
+#include <glib.h>
+#include <libxml/parser.h>
+
+#include "gdata-batch-feed.h"
+#include "gdata-private.h"
+#include "gdata-batch-private.h"
+
+static gboolean parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *root_node, gpointer user_data, GError **error);
+
+G_DEFINE_TYPE (GDataBatchFeed, gdata_batch_feed, GDATA_TYPE_FEED)
+
+static void
+gdata_batch_feed_class_init (GDataBatchFeedClass *klass)
+{
+	GDataParsableClass *parsable_class = GDATA_PARSABLE_CLASS (klass);
+	parsable_class->parse_xml = parse_xml;
+}
+
+static void
+gdata_batch_feed_init (GDataBatchFeed *self)
+{
+	/* Nothing to see here */
+}
+
+static gboolean
+parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error)
+{
+	GDataBatchOperation *operation = GDATA_BATCH_OPERATION (user_data);
+
+	if (xmlStrcmp (node->name, (xmlChar*) "entry") == 0) {
+		GDataEntry *entry = NULL;
+		xmlBuffer *status_response = NULL;
+		gchar *status_reason = NULL;
+		guint id = 0, status_code = 0;
+		xmlNode *entry_node;
+		BatchOperation *op;
+
+		status_response = xmlBufferCreate ();
+
+		/* Parse the child nodes of the <entry> to get the batch namespace elements containing information about this operation */
+		for (entry_node = node->children; entry_node != NULL; entry_node = entry_node->next) {
+			/* We have to be careful about namespaces here, and we can skip text nodes (since none of the nodes we're looking for
+			 * are text nodes) */
+			if (entry_node->type == XML_TEXT_NODE ||
+			    gdata_parser_is_namespace (entry_node, "http://schemas.google.com/gdata/batch";) == FALSE)
+				continue;
+
+			if (xmlStrcmp (entry_node->name, (xmlChar*) "id") == 0) {
+				/* batch:id */
+				xmlChar *id_string = xmlNodeListGetString (doc, entry_node->children, TRUE);
+				id = strtoul ((char*) id_string, NULL, 10);
+				xmlFree (id_string);
+			} else if (xmlStrcmp (entry_node->name, (xmlChar*) "status") == 0) {
+				/* batch:status */
+				xmlChar *status_code_string;
+				xmlNode *child_node;
+
+				status_code_string = xmlGetProp (entry_node, (xmlChar*) "code");
+				status_code = strtoul ((char*) status_code_string, NULL, 10);
+				xmlFree (status_code_string);
+
+				status_reason = (gchar*) xmlGetProp (entry_node, (xmlChar*) "reason");
+
+				/* Dump the content of the status node, since it's service-specific, and could be anything from plain text to XML */
+				for (child_node = entry_node->children; child_node != NULL; child_node = child_node->next)
+					xmlNodeDump (status_response, doc, child_node, 0, 0);
+			}
+
+			if (id != 0 && status_code != 0)
+				break;
+		}
+
+		/* Check we've got all the required data */
+		if (id == 0) {
+			gdata_parser_error_required_element_missing ("batch:id", "entry", error);
+			goto error;
+		} else if (status_code == 0) {
+			gdata_parser_error_required_element_missing ("batch:status", "entry", error);
+			goto error;
+		}
+
+		op = _gdata_batch_operation_get_operation (operation, id);
+
+		/* Check for errors */
+		if (SOUP_STATUS_IS_SUCCESSFUL (status_code) == FALSE) {
+			/* Handle the error */
+			GDataService *service = gdata_batch_operation_get_service (operation);
+			GDataServiceClass *klass = GDATA_SERVICE_GET_CLASS (service);
+			GError *child_error = NULL;
+
+			/* Parse the error (it's returned in a service-specific format */
+			g_assert (klass->parse_error_response != NULL);
+			klass->parse_error_response (service, op->type, status_code, status_reason, (gchar*) xmlBufferContent (status_response),
+			                             xmlBufferLength (status_response), &child_error);
+
+			/* Run the operation's callback. This takes ownership of @child_error. */
+			_gdata_batch_operation_run_callback (operation, op, NULL, child_error);
+
+			g_free (status_reason);
+			xmlBufferFree (status_response);
+
+			/* We return TRUE because we parsed the XML successfully; despite it being an error that we parsed */
+			return TRUE;
+		}
+
+		/* If there wasn't an error, parse the resulting GDataEntry and run the operation's callback */
+		if (op->type == GDATA_BATCH_OPERATION_QUERY)
+			entry = GDATA_ENTRY (_gdata_parsable_new_from_xml_node (op->entry_type, doc, node, NULL, error));
+		else if (op->type != GDATA_BATCH_OPERATION_DELETION)
+			entry = GDATA_ENTRY (_gdata_parsable_new_from_xml_node (G_OBJECT_TYPE (op->entry), doc, node, NULL, error));
+
+		if (op->type != GDATA_BATCH_OPERATION_DELETION && entry == NULL)
+			goto error;
+
+		if (entry != NULL)
+			_gdata_batch_operation_run_callback (operation, op, entry, NULL);
+
+		g_free (status_reason);
+		xmlBufferFree (status_response);
+
+		return TRUE;
+
+error:
+		g_free (status_reason);
+		xmlBufferFree (status_response);
+
+		return FALSE;
+	} else if (GDATA_PARSABLE_CLASS (gdata_batch_feed_parent_class)->parse_xml (parsable, doc, node, user_data, error) == FALSE) {
+		/* Error! */
+		return FALSE;
+	}
+
+	return TRUE;
+}
diff --git a/gdata/gdata-batch-feed.h b/gdata/gdata-batch-feed.h
new file mode 100644
index 0000000..d69f560
--- /dev/null
+++ b/gdata/gdata-batch-feed.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Philip Withnall 2010 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GDATA_BATCH_FEED_H
+#define GDATA_BATCH_FEED_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <gdata/gdata-feed.h>
+
+G_BEGIN_DECLS
+
+#define GDATA_TYPE_BATCH_FEED		(gdata_batch_feed_get_type ())
+#define GDATA_BATCH_FEED(o)		(G_TYPE_CHECK_INSTANCE_CAST ((o), GDATA_TYPE_BATCH_FEED, GDataBatchFeed))
+#define GDATA_BATCH_FEED_CLASS(k)	(G_TYPE_CHECK_CLASS_CAST((k), GDATA_TYPE_BATCH_FEED, GDataBatchFeedClass))
+#define GDATA_IS_BATCH_FEED(o)		(G_TYPE_CHECK_INSTANCE_TYPE ((o), GDATA_TYPE_BATCH_FEED))
+#define GDATA_IS_BATCH_FEED_CLASS(k)	(G_TYPE_CHECK_CLASS_TYPE ((k), GDATA_TYPE_BATCH_FEED))
+#define GDATA_BATCH_FEED_GET_CLASS(o)	(G_TYPE_INSTANCE_GET_CLASS ((o), GDATA_TYPE_BATCH_FEED, GDataBatchFeedClass))
+
+typedef struct _GDataBatchFeedPrivate	GDataBatchFeedPrivate;
+
+/**
+ * GDataBatchFeed:
+ *
+ * All the fields in the #GDataBatchFeed structure are private and should never be accessed directly.
+ *
+ * Since: 0.7.0
+ **/
+typedef struct {
+	/*< private >*/
+	GDataFeed parent;
+	GDataBatchFeedPrivate *priv;
+} GDataBatchFeed;
+
+/**
+ * GDataBatchFeedClass:
+ *
+ * All the fields in the #GDataBatchFeedClass structure are private and should never be accessed directly.
+ *
+ * Since: 0.7.0
+ **/
+typedef struct {
+	/*< private >*/
+	GDataFeedClass parent;
+} GDataBatchFeedClass;
+
+GType gdata_batch_feed_get_type (void) G_GNUC_CONST;
+
+G_END_DECLS
+
+#endif /* !GDATA_BATCH_FEED_H */
diff --git a/gdata/gdata-batch-operation.c b/gdata/gdata-batch-operation.c
new file mode 100644
index 0000000..e0e23df
--- /dev/null
+++ b/gdata/gdata-batch-operation.c
@@ -0,0 +1,696 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Philip Withnall 2010 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION:gdata-batch-operation
+ * @short_description: GData batch operation object
+ * @stability: Unstable
+ * @include: gdata/gdata-batch-operation.h
+ *
+ * #GDataBatchOperation is a transient standalone class which represents and handles a single batch operation request to a service. To make a batch
+ * operation request: create a new #GDataBatchOperation; add the required queries, insertions, updates and deletions to the operation using
+ * gdata_batch_operation_add_query(), gdata_batch_operation_add_insertion(), gdata_batch_operation_add_update() and
+ * gdata_batch_operation_add_deletion(), respectively; run the request with gdata_batch_operation_run() or gdata_batch_operation_run_async(); and
+ * handle the results in the callback functions which are invoked by the operation as the results are received and parsed.
+ *
+ * <example>
+ * 	<title>Running a Synchronous Operation</title>
+ * 	<programlisting>
+ *	guint op_id, op_id2;
+ *	GDataBatchOperation *operation;
+ *	GDataContactsContact *contact;
+ *	GDataService *service;
+ *	GDataLink *self_link;
+ *
+ *	service = create_contacts_service ();
+ *	contact = create_new_contact ();
+ *	self_link = gdata_entry_look_up_link (other_contact, GDATA_LINK_SELF);
+ *	batch_link = gdata_feed_look_up_link (contacts_feed, GDATA_LINK_BATCH);
+ *
+ *	operation = gdata_batchable_create_operation (GDATA_BATCHABLE (service), gdata_link_get_uri (batch_link));
+ *
+ *	/<!-- -->* Add to the operation to insert a new contact and query for another one *<!-- -->/
+ *	op_id = gdata_batch_operation_add_insertion (operation, GDATA_ENTRY (contact), insertion_cb, user_data);
+ *	op_id2 = gdata_batch_operation_add_query (operation, gdata_link_get_uri (self_link), GDATA_TYPE_CONTACTS_CONTACT, query_cb, user_data);
+ *
+ *	g_object_unref (contact);
+ *	g_object_unref (service);
+ *
+ *	/<!-- -->* Run the operations in a blocking fashion. Ideally, check and free the error as appropriate after running the operation. *<!-- -->/
+ *	gdata_batch_operation_run (operation, NULL, &error);
+ *
+ *	g_object_unref (operation);
+ *
+ *	static void
+ *	insertion_cb (guint operation_id, GDataBatchOperationType operation_type, GDataEntry *entry, GError *error, gpointer user_data)
+ *	{
+ *		/<!-- -->* operation_id == op_id, operation_type == GDATA_BATCH_OPERATION_INSERTION *<!-- -->/
+ *
+ *		/<!-- -->* Process the new inserted entry, ideally after checking for errors. Note that the entry should be reffed if it needs to stay
+ *		 * alive after execution of the callback finishes. *<!-- -->/
+ *		process_inserted_entry (entry, user_data);
+ *	}
+ *
+ *	static void
+ *	query_cb (guint operation_id, GDataBatchOperationType operation_type, GDataEntry *entry, GError *error, gpointer user_data)
+ *	{
+ *		/<!-- -->* operation_id == op_id2, operation_type == GDATA_BATCH_OPERATION_QUERY *<!-- -->/
+ *
+ *		/<!-- -->* Process the results of the query, ideally after checking for errors. Note that the entry should be reffed if it needs to
+ *		 * stay alive after execution of the callback finishes. *<!-- -->/
+ *		process_queried_entry (entry, user_data);
+ *	}
+ * 	</programlisting>
+ * </example>
+ *
+ * Since: 0.7.0
+ **/
+
+#include <config.h>
+#include <glib.h>
+#include <string.h>
+
+#include "gdata-batch-operation.h"
+#include "gdata-batch-feed.h"
+#include "gdata-private.h"
+#include "gdata-batch-private.h"
+
+static void operation_free (BatchOperation *op);
+
+static void gdata_batch_operation_dispose (GObject *object);
+static void gdata_batch_operation_finalize (GObject *object);
+static void gdata_batch_operation_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec);
+static void gdata_batch_operation_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec);
+
+struct _GDataBatchOperationPrivate {
+	GDataService *service;
+	gchar *feed_uri;
+	GHashTable *operations;
+	guint next_id; /* next available operation ID */
+	gboolean has_run; /* TRUE if the operation has been run already (though it does not necessarily have to have finished running) */
+	gboolean is_async; /* TRUE if the operation was run with *_run_async(); FALSE if run with *_run() */
+};
+
+enum {
+	PROP_SERVICE = 1,
+	PROP_FEED_URI
+};
+
+G_DEFINE_TYPE (GDataBatchOperation, gdata_batch_operation, G_TYPE_OBJECT)
+#define GDATA_BATCH_OPERATION_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GDATA_TYPE_BATCH_OPERATION, GDataBatchOperationPrivate))
+
+static void
+gdata_batch_operation_class_init (GDataBatchOperationClass *klass)
+{
+	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+	g_type_class_add_private (klass, sizeof (GDataBatchOperationPrivate));
+
+	gobject_class->dispose = gdata_batch_operation_dispose;
+	gobject_class->finalize = gdata_batch_operation_finalize;
+	gobject_class->get_property = gdata_batch_operation_get_property;
+	gobject_class->set_property = gdata_batch_operation_set_property;
+
+	/**
+	 * GDataBatchOperation:service:
+	 *
+	 * The service this batch operation is attached to.
+	 *
+	 * Since: 0.7.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_SERVICE,
+	                                 g_param_spec_object ("service",
+	                                                      "Service", "The service this batch operation is attached to.",
+	                                                      GDATA_TYPE_SERVICE,
+	                                                      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataBatchOperation:feed-uri:
+	 *
+	 * The feed URI that this batch operation will be sent to.
+	 *
+	 * Since: 0.7.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_FEED_URI,
+	                                 g_param_spec_string ("feed-uri",
+	                                                      "Feed URI", "The feed URI that this batch operation will be sent to.",
+	                                                      NULL,
+	                                                      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+}
+
+static void
+gdata_batch_operation_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
+{
+	GDataBatchOperationPrivate *priv = GDATA_BATCH_OPERATION_GET_PRIVATE (object);
+
+	switch (property_id) {
+		case PROP_SERVICE:
+			g_value_set_object (value, priv->service);
+			break;
+		case PROP_FEED_URI:
+			g_value_set_string (value, priv->feed_uri);
+			break;
+		default:
+			/* We don't have any other property... */
+			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+			break;
+	}
+}
+
+static void
+gdata_batch_operation_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
+{
+	GDataBatchOperationPrivate *priv = GDATA_BATCH_OPERATION_GET_PRIVATE (object);
+
+	switch (property_id) {
+		case PROP_SERVICE:
+			priv->service = g_value_dup_object (value);
+			break;
+		case PROP_FEED_URI:
+			priv->feed_uri = g_value_dup_string (value);
+			break;
+		default:
+			/* We don't have any other property... */
+			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+			break;
+	}
+}
+
+static void
+gdata_batch_operation_init (GDataBatchOperation *self)
+{
+	self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GDATA_TYPE_BATCH_OPERATION, GDataBatchOperationPrivate);
+	self->priv->next_id = 1; /* reserve ID 0 for error conditions */
+	self->priv->operations = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) operation_free);
+}
+
+static void
+gdata_batch_operation_dispose (GObject *object)
+{
+	GDataBatchOperationPrivate *priv = GDATA_BATCH_OPERATION_GET_PRIVATE (object);
+
+	if (priv->service != NULL)
+		g_object_unref (priv->service);
+	priv->service = NULL;
+
+	/* Chain up to the parent class */
+	G_OBJECT_CLASS (gdata_batch_operation_parent_class)->dispose (object);
+}
+
+static void
+gdata_batch_operation_finalize (GObject *object)
+{
+	GDataBatchOperationPrivate *priv = GDATA_BATCH_OPERATION_GET_PRIVATE (object);
+
+	g_free (priv->feed_uri);
+	g_hash_table_destroy (priv->operations);
+
+	/* Chain up to the parent class */
+	G_OBJECT_CLASS (gdata_batch_operation_parent_class)->finalize (object);
+}
+
+/**
+ * gdata_batch_operation_get_service:
+ * @self: a #GDataBatchOperation
+ *
+ * Gets the #GDataBatchOperation:service property.
+ *
+ * Return value: the batch operation's attached service
+ *
+ * Since: 0.7.0
+ **/
+GDataService *
+gdata_batch_operation_get_service (GDataBatchOperation *self)
+{
+	g_return_val_if_fail (GDATA_IS_BATCH_OPERATION (self), NULL);
+	return self->priv->service;
+}
+
+/**
+ * gdata_batch_operation_get_feed_uri:
+ * @self: a #GDataBatchOperation
+ *
+ * Gets the #GDataBatchOperation:feed-uri property.
+ *
+ * Return value: the batch operation's feed URI
+ *
+ * Since: 0.7.0
+ **/
+const gchar *
+gdata_batch_operation_get_feed_uri (GDataBatchOperation *self)
+{
+	g_return_val_if_fail (GDATA_IS_BATCH_OPERATION (self), NULL);
+	return self->priv->feed_uri;
+}
+
+/* Add an operation to the list of operations to be executed when the #GDataBatchOperation is run, and return its operation ID */
+static guint
+add_operation (GDataBatchOperation *self, GDataBatchOperationType type, GDataEntry *entry, GDataBatchOperationCallback callback, gpointer user_data)
+{
+	BatchOperation *op;
+
+	/* Create the operation */
+	op = g_slice_new0 (BatchOperation);
+	op->id = (self->priv->next_id++);
+	op->type = type;
+	op->callback = callback;
+	op->user_data = user_data;
+	op->entry = g_object_ref (entry);
+
+	/* Add the operation to the table */
+	g_hash_table_insert (self->priv->operations, GUINT_TO_POINTER (op->id), op);
+
+	return op->id;
+}
+
+/**
+ * _gdata_batch_operation_get_operation:
+ * @self: a #GDataBatchOperation
+ * @id: the operation ID
+ *
+ * Return the #BatchOperation for the given operation ID.
+ *
+ * Return value: the relevant #BatchOperation, or %NULL
+ *
+ * Since: 0.7.0
+ **/
+BatchOperation *
+_gdata_batch_operation_get_operation (GDataBatchOperation *self, guint id)
+{
+	g_return_val_if_fail (GDATA_IS_BATCH_OPERATION (self), NULL);
+	g_return_val_if_fail (id > 0, NULL);
+
+	return g_hash_table_lookup (self->priv->operations, GUINT_TO_POINTER (id));
+}
+
+/* Run a user-supplied callback for a #BatchOperation whose return value we've just processed. This is designed to be used in an idle handler, so
+ * that the callback is run in the main thread. It should not be called if the user-supplied callback is %NULL. */
+static gboolean
+run_callback_cb (BatchOperation *op)
+{
+	/* We do allow op->callback to be unset, but in that case run_callback_cb() shouldn't be being called */
+	g_assert (op->callback != NULL);
+	op->callback (op->id, op->type, op->entry, op->error, op->user_data);
+
+	return FALSE;
+}
+
+/**
+ * _gdata_batch_operation_run_callback:
+ * @self: a #GDataBatchOperation
+ * @op: the #BatchOperation which has been finished
+ * @entry: the entry representing the operation's result, or %NULL
+ * @error: the error from the operation, or %NULL
+ *
+ * Run the callback for @op to notify the user code that the operation's result has been received and processed. Either @entry or @error should be
+ * set (and the other should be %NULL), signifying a successful operation or a failed operation, respectively.
+ *
+ * The function will call @op's user-supplied callback, if available, in either the current or the main thread, depending on whether the
+ * #GDataBatchOperation was run with gdata_batch_operation_run() or gdata_batch_operation_run_async().
+ *
+ * Since: 0.7.0
+ **/
+void
+_gdata_batch_operation_run_callback (GDataBatchOperation *self, BatchOperation *op, GDataEntry *entry, GError *error)
+{
+	g_return_if_fail (GDATA_IS_BATCH_OPERATION (self));
+	g_return_if_fail (op != NULL);
+	g_return_if_fail (entry == NULL || GDATA_IS_ENTRY (entry));
+	g_return_if_fail (entry == NULL || error == NULL);
+
+	/* We can free the request data, and replace it with the response data */
+	g_free (op->query_id);
+	op->query_id = NULL;
+	if (op->entry != NULL)
+		g_object_unref (op->entry);
+	if (entry != NULL)
+		g_object_ref (entry);
+	op->entry = entry;
+	op->error = error;
+
+	/* Don't bother scheduling run_callback_cb() if there is no callback to run */
+	if (op->callback == NULL)
+		return;
+
+	/* Only dispatch it in the main thread if the request was run with *_run_async(). This allows applications to run batch operations entirely in
+	 * application-owned threads if desired. */
+	if (self->priv->is_async == TRUE)
+		g_idle_add ((GSourceFunc) run_callback_cb, op);
+	else
+		run_callback_cb (op);
+}
+
+/* Free a #BatchOperation */
+static void
+operation_free (BatchOperation *op)
+{
+	g_free (op->query_id);
+	if (op->entry != NULL)
+		g_object_unref (op->entry);
+	if (op->error != NULL)
+		g_error_free (op->error);
+
+	g_slice_free (BatchOperation, op);
+}
+
+/**
+ * gdata_batch_operation_add_query:
+ * @self: a #GDataBatchOperation
+ * @id: the ID of the entry being queried for
+ * @entry_type: the type of the entry which will be returned
+ * @callback: a #GDataBatchOperationCallback to call when the query is finished, or %NULL
+ * @user_data: data to pass to the @callback function
+ *
+ * Add a query to the #GDataBatchOperation, to be executed when the operation is run. The query will return a #GDataEntry (of subclass type
+ * @entry_type) representing the given entry @id. The ID is of the same format as that returned by gdata_entry_get_id().
+ *
+ * Note that a single batch operation should not operate on a given #GDataEntry more than once, as there's no guarantee about the order in which the
+ * batch operation's operations will be performed.
+ *
+ * @callback will be called when the #GDataBatchOperation is run with gdata_batch_operation_run() (in which case it will be called in the thread which
+ * ran the batch operation), or with gdata_batch_operation_run_async() (in which case it will be called in an idle handler in the main thread). The
+ * @operation_id passed to the callback will match the return value of gdata_batch_operation_add_query(), and the @operation_type will be
+ * %GDATA_BATCH_OPERATION_QUERY. If the query was successful, the resulting entry will be passed to the callback function as @entry, and @error will
+ * be %NULL. If, however, the query was unsuccessful, @entry will be %NULL and @error will contain a #GError detailing what went wrong.
+ *
+ * Return value: operation ID for the added query, or <code class="literal">0</code>
+ *
+ * Since: 0.7.0
+ **/
+guint
+gdata_batch_operation_add_query (GDataBatchOperation *self, const gchar *id, GType entry_type,
+                                 GDataBatchOperationCallback callback, gpointer user_data)
+{
+	BatchOperation *op;
+
+	g_return_val_if_fail (GDATA_IS_BATCH_OPERATION (self), 0);
+	g_return_val_if_fail (id != NULL, 0);
+	g_return_val_if_fail (g_type_is_a (entry_type, GDATA_TYPE_ENTRY), 0);
+	g_return_val_if_fail (self->priv->has_run == FALSE, 0);
+
+	/* Create the operation manually, since it would be messy to special-case add_operation() to do this */
+	op = g_slice_new0 (BatchOperation);
+	op->id = (self->priv->next_id++);
+	op->type = GDATA_BATCH_OPERATION_QUERY;
+	op->callback = callback;
+	op->user_data = user_data;
+	op->query_id = g_strdup (id);
+	op->entry_type = entry_type;
+
+	/* Add the operation to the table */
+	g_hash_table_insert (self->priv->operations, GUINT_TO_POINTER (op->id), op);
+
+	return op->id;
+}
+
+/**
+ * gdata_batch_operation_add_insertion:
+ * @self: a #GDataBatchOperation
+ * @entry: the #GDataEntry to insert
+ * @callback: a #GDataBatchOperationCallback to call when the insertion is finished, or %NULL
+ * @user_data: data to pass to the @callback function
+ *
+ * Add an entry to the #GDataBatchOperation, to be inserted on the server when the operation is run. The insertion will return the inserted version
+ * of @entry. @entry is reffed by the function, so may be freed after it returns.
+ *
+ * @callback will be called as specified in the documentation for gdata_batch_operation_add_query(), with an @operation_type of
+ * %GDATA_BATCH_OPERATION_INSERTION.
+ *
+ * Return value: operation ID for the added insertion, or <code class="literal">0</code>
+ *
+ * Since: 0.7.0
+ **/
+guint
+gdata_batch_operation_add_insertion (GDataBatchOperation *self, GDataEntry *entry, GDataBatchOperationCallback callback, gpointer user_data)
+{
+	g_return_val_if_fail (GDATA_IS_BATCH_OPERATION (self), 0);
+	g_return_val_if_fail (GDATA_IS_ENTRY (entry), 0);
+	g_return_val_if_fail (self->priv->has_run == FALSE, 0);
+
+	return add_operation (self, GDATA_BATCH_OPERATION_INSERTION, entry, callback, user_data);
+}
+
+/**
+ * gdata_batch_operation_add_update:
+ * @self: a #GDataBatchOperation
+ * @entry: the #GDataEntry to update
+ * @callback: a #GDataBatchOperationCallback to call when the update is finished, or %NULL
+ * @user_data: data to pass to the @callback function
+ *
+ * Add an entry to the #GDataBatchOperation, to be updated on the server when the operation is run. The update will return the updated version of
+ * @entry. @entry is reffed by the function, so may be freed after it returns.
+ *
+ * Note that a single batch operation should not operate on a given #GDataEntry more than once, as there's no guarantee about the order in which the
+ * batch operation's operations will be performed.
+ *
+ * @callback will be called as specified in the documentation for gdata_batch_operation_add_query(), with an @operation_type of
+ * %GDATA_BATCH_OPERATION_UPDATE.
+ *
+ * Return value: operation ID for the added update, or <code class="literal">0</code>
+ *
+ * Since: 0.7.0
+ **/
+guint
+gdata_batch_operation_add_update (GDataBatchOperation *self, GDataEntry *entry, GDataBatchOperationCallback callback, gpointer user_data)
+{
+	g_return_val_if_fail (GDATA_IS_BATCH_OPERATION (self), 0);
+	g_return_val_if_fail (GDATA_IS_ENTRY (entry), 0);
+	g_return_val_if_fail (self->priv->has_run == FALSE, 0);
+
+	return add_operation (self, GDATA_BATCH_OPERATION_UPDATE, entry, callback, user_data);
+}
+
+/**
+ * gdata_batch_operation_add_deletion:
+ * @self: a #GDataBatchOperation
+ * @entry: the #GDataEntry to delete
+ * @callback: a #GDataBatchOperationCallback to call when the deletion is finished, or %NULL
+ * @user_data: data to pass to the @callback function
+ *
+ * Add an entry to the #GDataBatchOperation, to be deleted on the server when the operation is run. @entry is reffed by the function, so may be freed
+ * after it returns.
+ *
+ * Note that a single batch operation should not operate on a given #GDataEntry more than once, as there's no guarantee about the order in which the
+ * batch operation's operations will be performed.
+ *
+ * @callback will be called as specified in the documentation for gdata_batch_operation_add_query(), with an @operation_type of
+ * %GDATA_BATCH_OPERATION_DELETION.
+ *
+ * Return value: operation ID for the added deletion, or <code class="literal">0</code>
+ *
+ * Since: 0.7.0
+ **/
+guint
+gdata_batch_operation_add_deletion (GDataBatchOperation *self, GDataEntry *entry, GDataBatchOperationCallback callback, gpointer user_data)
+{
+	g_return_val_if_fail (GDATA_IS_BATCH_OPERATION (self), 0);
+	g_return_val_if_fail (GDATA_IS_ENTRY (entry), 0);
+	g_return_val_if_fail (self->priv->has_run == FALSE, 0);
+
+	return add_operation (self, GDATA_BATCH_OPERATION_DELETION, entry, callback, user_data);
+}
+
+/* Called for each BatchOperation in GDataBatchOperation->operations to add it to a request feed */
+static void
+run_cb (gpointer key, BatchOperation *op, GDataFeed *feed)
+{
+	if (op->type == GDATA_BATCH_OPERATION_QUERY) {
+		/* Queries are weird; build a new throwaway entry, and add it to the feed */
+		GDataEntry *entry;
+		GTimeVal updated;
+
+		g_get_current_time (&updated);
+
+		entry = gdata_entry_new (op->query_id);
+		gdata_entry_set_title (entry, "Batch operation query");
+		_gdata_entry_set_updated (entry, &updated);
+
+		_gdata_entry_set_batch_data (entry, op->id, op->type);
+		_gdata_feed_add_entry (feed, entry);
+
+		g_object_unref (entry);
+	} else {
+		/* Everything else just dumps the entry's XML in the request */
+		_gdata_entry_set_batch_data (op->entry, op->id, op->type);
+		_gdata_feed_add_entry (feed, op->entry);
+	}
+}
+
+/**
+ * gdata_batch_operation_run:
+ * @self: a #GDataBatchOperation
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Run the #GDataBatchOperation synchronously. This will send all the operations in the batch operation to the server, and call their respective
+ * callbacks synchronously (i.e. before gdata_batch_operation_run() returns, and in the same thread that called gdata_batch_operation_run()) as the
+ * server returns results for each operation.
+ *
+ * The return value of the function indicates whether the overall batch operation was successful, and doesn't indicate the status of any of the
+ * operations it comprises. gdata_batch_operation_run() could return %TRUE even if all of its operations failed.
+ *
+ * @cancellable can be used to cancel the entire batch operation any time before or during the network activity. If @cancellable is cancelled
+ * after network activity has finished, gdata_batch_operation_run() will continue and finish as normal.
+ *
+ * Return value: %TRUE on success, %FALSE otherwise
+ *
+ * Since: 0.7.0
+ **/
+gboolean
+gdata_batch_operation_run (GDataBatchOperation *self, GCancellable *cancellable, GError **error)
+{
+	GDataBatchOperationPrivate *priv = self->priv;
+	SoupMessage *message;
+	GDataFeed *feed;
+	GTimeVal updated;
+	gchar *upload_data;
+	guint status;
+
+	g_return_val_if_fail (GDATA_IS_BATCH_OPERATION (self), FALSE);
+	g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE);
+	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+	g_return_val_if_fail (priv->has_run == FALSE, FALSE);
+
+	message = _gdata_service_build_message (priv->service, SOUP_METHOD_POST, priv->feed_uri, NULL, TRUE);
+
+	/* Build the request */
+	g_get_current_time (&updated);
+	feed = _gdata_feed_new ("Batch operation feed", "batch1", &updated);
+	g_hash_table_foreach (priv->operations, (GHFunc) run_cb, feed);
+
+	upload_data = gdata_parsable_get_xml (GDATA_PARSABLE (feed));
+	soup_message_set_request (message, "application/atom+xml", SOUP_MEMORY_TAKE, upload_data, strlen (upload_data));
+
+	g_object_unref (feed);
+
+	/* Ensure that this GDataBatchOperation can't be run again */
+	priv->has_run = TRUE;
+
+	/* Send the message */
+	status = _gdata_service_send_message (priv->service, message, cancellable, error);
+
+	if (status == SOUP_STATUS_NONE || status == SOUP_STATUS_CANCELLED) {
+		/* Redirect error or cancelled */
+		g_object_unref (message);
+		return FALSE;
+	} else if (status != SOUP_STATUS_OK) {
+		/* Error */
+		GDataServiceClass *klass = GDATA_SERVICE_GET_CLASS (priv->service);
+		g_assert (klass->parse_error_response != NULL);
+		klass->parse_error_response (priv->service, GDATA_OPERATION_BATCH, status, message->reason_phrase, message->response_body->data,
+		                             message->response_body->length, error);
+		g_object_unref (message);
+		return FALSE;
+	}
+
+	/* Parse the XML; GDataBatchFeed will fire off the relevant callbacks */
+	g_assert (message->response_body->data != NULL);
+	feed = GDATA_FEED (_gdata_parsable_new_from_xml (GDATA_TYPE_BATCH_FEED, message->response_body->data, message->response_body->length,
+	                                                 self, error));
+	g_object_unref (message);
+
+	if (feed == NULL)
+		return FALSE;
+	g_object_unref (feed);
+
+	return TRUE;
+}
+
+static void
+run_thread (GSimpleAsyncResult *result, GDataBatchOperation *operation, GCancellable *cancellable)
+{
+	gboolean success;
+	GError *error = NULL;
+
+	/* Run the batch operation and return */
+	success = gdata_batch_operation_run (operation, cancellable, &error);
+	g_simple_async_result_set_op_res_gboolean (result, success);
+
+	/* Propagate any errors */
+	if (success == FALSE) {
+		g_simple_async_result_set_from_error (result, error);
+		g_error_free (error);
+	}
+}
+
+/**
+ * gdata_batch_operation_run_async:
+ * @self: a #GDataBatchOperation
+ * @cancellable: a #GCancellable, or %NULL
+ * @callback: a #GAsyncReadyCallback to call when the batch operation is finished, or %NULL
+ * @user_data: data to pass to the @callback function
+ *
+ * Run the #GDataBatchOperation asynchronously. This will send all the operations in the batch operation to the server, and call their respective
+ * callbacks asynchronously (i.e. in idle functions in the main thread, usually after gdata_batch_operation_run_async() has returned) as the
+ * server returns results for each operation. @self is reffed when this function is called, so can safely be unreffed after this function returns.
+ *
+ * For more details, see gdata_batch_operation_run(), which is the synchronous version of this function.
+ *
+ * When the entire batch operation is finished, @callback will be called. You can then call gdata_batch_operation_run_finish() to get the results of
+ * the batch operation.
+ *
+ * Since: 0.7.0
+ **/
+void
+gdata_batch_operation_run_async (GDataBatchOperation *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data)
+{
+	GSimpleAsyncResult *result;
+
+	g_return_if_fail (GDATA_IS_BATCH_OPERATION (self));
+	g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+	g_return_if_fail (self->priv->has_run == FALSE);
+
+	/* Mark the operation as async for the purposes of deciding where to call the callbacks */
+	self->priv->is_async = TRUE;
+
+	result = g_simple_async_result_new (G_OBJECT (self), callback, user_data, gdata_batch_operation_run_async);
+	g_simple_async_result_run_in_thread (result, (GSimpleAsyncThreadFunc) run_thread, G_PRIORITY_DEFAULT, cancellable);
+	g_object_unref (result);
+}
+
+/**
+ * gdata_batch_operation_run_finish:
+ * @self: a #GDataBatchOperation
+ * @async_result: a #GAsyncResult
+ * @error: a #GError, or %NULL
+ *
+ * Finishes an asynchronous batch operation run with gdata_batch_operation_run_async().
+ *
+ * Return values are as for gdata_batch_operation_run().
+ *
+ * Return value: %TRUE on success, %FALSE otherwise
+ *
+ * Since: 0.7.0
+ **/
+gboolean
+gdata_batch_operation_run_finish (GDataBatchOperation *self, GAsyncResult *async_result, GError **error)
+{
+	GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT (async_result);
+
+	g_return_val_if_fail (GDATA_IS_BATCH_OPERATION (self), FALSE);
+	g_return_val_if_fail (G_IS_ASYNC_RESULT (async_result), FALSE);
+	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+	g_warn_if_fail (g_simple_async_result_get_source_tag (result) == gdata_batch_operation_run_async);
+
+	if (g_simple_async_result_propagate_error (result, error) == TRUE)
+		return FALSE;
+
+	return g_simple_async_result_get_op_res_gboolean (result);
+}
diff --git a/gdata/gdata-batch-operation.h b/gdata/gdata-batch-operation.h
new file mode 100644
index 0000000..76a05e8
--- /dev/null
+++ b/gdata/gdata-batch-operation.h
@@ -0,0 +1,139 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Philip Withnall 2010 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GDATA_BATCH_OPERATION_H
+#define GDATA_BATCH_OPERATION_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <gdata/gdata-service.h>
+#include <gdata/gdata-entry.h>
+
+G_BEGIN_DECLS
+
+/**
+ * GDATA_LINK_BATCH:
+ *
+ * The relation type URI for the batch operation URI for a given #GDataFeed.
+ *
+ * For more information, see the <ulink type="http" url="http://code.google.com/apis/gdata/docs/batch.html#Submit_HTTP";>GData specification</ulink>.
+ *
+ * Since: 0.7.0
+ **/
+#define GDATA_LINK_BATCH "http://schemas.google.com/g/2005#batch";
+
+/**
+ * GDataBatchOperationType:
+ * @GDATA_BATCH_OPERATION_QUERY: a query operation
+ * @GDATA_BATCH_OPERATION_INSERTION: an insertion operation
+ * @GDATA_BATCH_OPERATION_UPDATE: an update operation
+ * @GDATA_BATCH_OPERATION_DELETION: a deletion operation
+ *
+ * Indicates which type of batch operation caused the current #GDataBatchOperationCallback to be called.
+ *
+ * Since: 0.7.0
+ **/
+typedef enum {
+	GDATA_BATCH_OPERATION_QUERY = 0,
+	GDATA_BATCH_OPERATION_INSERTION,
+	GDATA_BATCH_OPERATION_UPDATE,
+	GDATA_BATCH_OPERATION_DELETION
+} GDataBatchOperationType;
+
+/**
+ * GDataBatchOperationCallback:
+ * @operation_id: the operation ID returned from gdata_batch_operation_add_*()
+ * @operation_type: the type of operation which was requested
+ * @entry: the result of the operation, or %NULL
+ * @error: a #GError describing any error which occurred, or %NULL
+ * @user_data: user data passed to the callback
+ *
+ * Callback function called once for each operation in a batch operation run. The operation is identified by @operation_id and @operation_type (where
+ * @operation_id is the ID returned by the relevant call to gdata_batch_operation_add_query(), gdata_batch_operation_add_insertion(),
+ * gdata_batch_operation_add_update() or gdata_batch_operation_add_deletion(), and @operation_type shows which one of the above was called).
+ *
+ * If the operation was successful, the resulting #GDataEntry will be passed in as @entry, and @error will be %NULL. Otherwise, @entry will be %NULL
+ * and a descriptive error will be in @error. If @operation_type is %GDATA_BATCH_OPERATION_DELETION, @entry will always be %NULL, and @error will be
+ * %NULL or non-%NULL as appropriate.
+ *
+ * If the callback code needs to retain a copy of @entry, it must be referenced (with g_object_ref()). Similarly, @error is owned by the calling code,
+ * and must not be freed.
+ *
+ * The callback is called in the main thread, and there is no guarantee on the order in which the callbacks for the operations in a run are executed,
+ * or whether they will be called in a timely manner. It is, however, guaranteed that they will all be called before the #GAsyncReadyCallback which
+ * signals the completion of the run (if initiated with gdata_batch_operation_run_async()) is called; or gdata_batch_operation_run() returns (if
+ * initiated synchronously).
+ *
+ * Since: 0.7.0
+ **/
+typedef void (*GDataBatchOperationCallback) (guint operation_id, GDataBatchOperationType operation_type, GDataEntry *entry,
+                                             GError *error, gpointer user_data);
+
+#define GDATA_TYPE_BATCH_OPERATION		(gdata_batch_operation_get_type ())
+#define GDATA_BATCH_OPERATION(o)		(G_TYPE_CHECK_INSTANCE_CAST ((o), GDATA_TYPE_BATCH_OPERATION, GDataBatchOperation))
+#define GDATA_BATCH_OPERATION_CLASS(k)		(G_TYPE_CHECK_CLASS_CAST((k), GDATA_TYPE_BATCH_OPERATION, GDataBatchOperationClass))
+#define GDATA_IS_BATCH_OPERATION(o)		(G_TYPE_CHECK_INSTANCE_TYPE ((o), GDATA_TYPE_BATCH_OPERATION))
+#define GDATA_IS_BATCH_OPERATION_CLASS(k)	(G_TYPE_CHECK_CLASS_TYPE ((k), GDATA_TYPE_BATCH_OPERATION))
+#define GDATA_BATCH_OPERATION_GET_CLASS(o)	(G_TYPE_INSTANCE_GET_CLASS ((o), GDATA_TYPE_BATCH_OPERATION, GDataBatchOperationClass))
+
+typedef struct _GDataBatchOperationPrivate	GDataBatchOperationPrivate;
+
+/**
+ * GDataBatchOperation:
+ *
+ * All the fields in the #GDataBatchOperation structure are private and should never be accessed directly.
+ *
+ * Since: 0.7.0
+ **/
+typedef struct {
+	GObject parent;
+	GDataBatchOperationPrivate *priv;
+} GDataBatchOperation;
+
+/**
+ * GDataBatchOperationClass:
+ *
+ * All the fields in the #GDataBatchOperationClass structure are private and should never be accessed directly.
+ *
+ * Since: 0.7.0
+ **/
+typedef struct {
+	/*< private >*/
+	GObjectClass parent;
+} GDataBatchOperationClass;
+
+GType gdata_batch_operation_get_type (void) G_GNUC_CONST;
+
+GDataService *gdata_batch_operation_get_service (GDataBatchOperation *self) G_GNUC_PURE;
+const gchar *gdata_batch_operation_get_feed_uri (GDataBatchOperation *self) G_GNUC_PURE;
+
+guint gdata_batch_operation_add_query (GDataBatchOperation *self, const gchar *id, GType entry_type,
+                                       GDataBatchOperationCallback callback, gpointer user_data);
+guint gdata_batch_operation_add_insertion (GDataBatchOperation *self, GDataEntry *entry, GDataBatchOperationCallback callback, gpointer user_data);
+guint gdata_batch_operation_add_update (GDataBatchOperation *self, GDataEntry *entry, GDataBatchOperationCallback callback, gpointer user_data);
+guint gdata_batch_operation_add_deletion (GDataBatchOperation *self, GDataEntry *entry, GDataBatchOperationCallback callback, gpointer user_data);
+
+gboolean gdata_batch_operation_run (GDataBatchOperation *self, GCancellable *cancellable, GError **error);
+void gdata_batch_operation_run_async (GDataBatchOperation *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data);
+gboolean gdata_batch_operation_run_finish (GDataBatchOperation *self, GAsyncResult *async_result, GError **error);
+
+G_END_DECLS
+
+#endif /* !GDATA_BATCH_OPERATION_H */
diff --git a/gdata/gdata-batch-private.h b/gdata/gdata-batch-private.h
new file mode 100644
index 0000000..d0ccd3e
--- /dev/null
+++ b/gdata/gdata-batch-private.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Philip Withnall 2010 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GDATA_BATCH_PRIVATE_H
+#define GDATA_BATCH_PRIVATE_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef struct {
+	guint id;
+	GDataBatchOperationType type;
+	GDataBatchOperationCallback callback;
+	gpointer user_data;
+	gchar *query_id; /* only used for queries */
+	GType entry_type; /* only used for queries */
+	GError *error;
+	GDataEntry *entry; /* used for anything except queries, and to store the results of all operations */
+} BatchOperation;
+
+BatchOperation *_gdata_batch_operation_get_operation (GDataBatchOperation *self, guint id) G_GNUC_PURE;
+void _gdata_batch_operation_run_callback (GDataBatchOperation *self, BatchOperation *op, GDataEntry *entry, GError *error);
+
+G_END_DECLS
+
+#endif /* !GDATA_BATCH_PRIVATE_H */
diff --git a/gdata/gdata-batchable.c b/gdata/gdata-batchable.c
new file mode 100644
index 0000000..e124fa7
--- /dev/null
+++ b/gdata/gdata-batchable.c
@@ -0,0 +1,73 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Philip Withnall 2010 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION:gdata-batchable
+ * @short_description: GData batch service interface
+ * @stability: Unstable
+ * @include: gdata/gdata-batchable.h
+ *
+ * #GDataBatchable is an interface which can be implemented by #GDataService<!-- -->s which support batch operations on their entries. It allows the
+ * creation of a #GDataBatchOperation for the service, which allows a set of batch operations to be run.
+ *
+ * Since: 0.7.0
+ **/
+
+#include <config.h>
+#include <glib.h>
+
+#include "gdata-batchable.h"
+#include "gdata-service.h"
+#include "gdata-batch-operation.h"
+
+GType
+gdata_batchable_get_type (void)
+{
+	static GType batchable_type = 0;
+
+	if (!batchable_type) {
+		batchable_type = g_type_register_static_simple (G_TYPE_INTERFACE, "GDataBatchable",
+		                                                sizeof (GDataBatchableIface),
+		                                                NULL, 0, NULL, 0);
+		g_type_interface_add_prerequisite (batchable_type, GDATA_TYPE_SERVICE);
+	}
+
+	return batchable_type;
+}
+
+/**
+ * gdata_batchable_create_operation:
+ * @self: a #GDataBatchable
+ * @feed_uri: the URI to send the batch operation request to
+ *
+ * Creates a new #GDataBatchOperation for the given #GDataBatchable service, and with the given @feed_uri. @feed_uri is normally the %GDATA_LINK_BATCH
+ * link URI in the appropriate #GDataFeed from the service.
+ *
+ * Return value: a new #GDataBatchOperation; unref with g_object_unref()
+ *
+ * Since: 0.7.0
+ **/
+GDataBatchOperation *
+gdata_batchable_create_operation (GDataBatchable *self, const gchar *feed_uri)
+{
+	g_return_val_if_fail (GDATA_IS_BATCHABLE (self), NULL);
+	g_return_val_if_fail (feed_uri != NULL, NULL);
+
+	return g_object_new (GDATA_TYPE_BATCH_OPERATION, "service", self, "feed-uri", feed_uri, NULL);
+}
diff --git a/gdata/gdata-batchable.h b/gdata/gdata-batchable.h
new file mode 100644
index 0000000..e67969a
--- /dev/null
+++ b/gdata/gdata-batchable.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Philip Withnall 2010 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GDATA_BATCHABLE_H
+#define GDATA_BATCHABLE_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <gdata/gdata-service.h>
+#include <gdata/gdata-batch-operation.h>
+
+G_BEGIN_DECLS
+
+#define GDATA_TYPE_BATCHABLE		(gdata_batchable_get_type ())
+#define GDATA_BATCHABLE(o)		(G_TYPE_CHECK_INSTANCE_CAST ((o), GDATA_TYPE_BATCHABLE, GDataBatchable))
+#define GDATA_BATCHABLE_CLASS(k)	(G_TYPE_CHECK_CLASS_CAST((k), GDATA_TYPE_BATCHABLE, GDataBatchableIface))
+#define GDATA_IS_BATCHABLE(o)		(G_TYPE_CHECK_INSTANCE_TYPE ((o), GDATA_TYPE_BATCHABLE))
+#define GDATA_BATCHABLE_GET_IFACE(o)	(G_TYPE_INSTANCE_GET_INTERFACE ((o), GDATA_TYPE_BATCHABLE, GDataBatchableIface))
+
+/**
+ * GDataBatchable:
+ *
+ * All the fields in the #GDataBatchable structure are private and should never be accessed directly.
+ *
+ * Since: 0.7.0
+ **/
+typedef struct _GDataBatchable		GDataBatchable; /* dummy typedef */
+
+/**
+ * GDataBatchableIface:
+ *
+ * All the fields in the #GDataBatchableIface structure are private and should never be accessed directly.
+ *
+ * Since: 0.7.0
+ **/
+typedef struct {
+	/*< private >*/
+	GTypeInterface parent;
+} GDataBatchableIface;
+
+GType gdata_batchable_get_type (void) G_GNUC_CONST;
+
+GDataBatchOperation *gdata_batchable_create_operation (GDataBatchable *self, const gchar *feed_uri) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
+
+G_END_DECLS
+
+#endif /* !GDATA_BATCHABLE_H */
diff --git a/gdata/gdata-entry.c b/gdata/gdata-entry.c
index d2c1e6a..5b085e8 100644
--- a/gdata/gdata-entry.c
+++ b/gdata/gdata-entry.c
@@ -68,6 +68,10 @@ struct _GDataEntryPrivate {
 	GList *links; /* GDataLink */
 	GList *authors; /* GDataAuthor */
 	gchar *rights;
+
+	/* Batch processing data */
+	GDataBatchOperationType batch_operation_type;
+	guint batch_id;
 };
 
 enum {
@@ -419,6 +423,13 @@ parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_da
 
 			return TRUE;
 		}
+	} else if (gdata_parser_is_namespace (node, "http://schemas.google.com/gdata/batch";) == TRUE) {
+		if (xmlStrcmp (node->name, (xmlChar*) "id") == 0 ||
+		    xmlStrcmp (node->name, (xmlChar*) "status") == 0 ||
+		    xmlStrcmp (node->name, (xmlChar*) "operation") == 0) {
+			/* Ignore batch operation elements; they're handled in GDataBatchFeed */
+			return TRUE;
+		}
 	}
 
 	return GDATA_PARSABLE_CLASS (gdata_entry_parent_class)->parse_xml (parsable, doc, node, user_data, error);
@@ -496,12 +507,40 @@ get_xml (GDataParsable *parsable, GString *xml_string)
 
 	for (authors = priv->authors; authors != NULL; authors = authors->next)
 		_gdata_parsable_get_xml (GDATA_PARSABLE (authors->data), xml_string, FALSE);
+
+	/* Batch operation data */
+	if (priv->batch_id != 0) {
+		const gchar *batch_op;
+
+		switch (priv->batch_operation_type) {
+			case GDATA_BATCH_OPERATION_QUERY:
+				batch_op = "query";
+				break;
+			case GDATA_BATCH_OPERATION_INSERTION:
+				batch_op = "insert";
+				break;
+			case GDATA_BATCH_OPERATION_UPDATE:
+				batch_op = "update";
+				break;
+			case GDATA_BATCH_OPERATION_DELETION:
+				batch_op = "delete";
+				break;
+			default:
+				g_assert_not_reached ();
+				break;
+		}
+
+		g_string_append_printf (xml_string, "<batch:id>%u</batch:id><batch:operation type='%s'/>", priv->batch_id, batch_op);
+	}
 }
 
 static void
 get_namespaces (GDataParsable *parsable, GHashTable *namespaces)
 {
 	g_hash_table_insert (namespaces, (gchar*) "gd", (gchar*) "http://schemas.google.com/g/2005";);
+
+	if (GDATA_ENTRY (parsable)->priv->batch_id != 0)
+		g_hash_table_insert (namespaces, (gchar*) "batch", (gchar*) "http://schemas.google.com/gdata/batch";);
 }
 
 static gchar *
@@ -642,6 +681,24 @@ gdata_entry_get_updated (GDataEntry *self, GTimeVal *updated)
 	*updated = self->priv->updated;
 }
 
+/*
+ * _gdata_entry_set_updated:
+ * @self: a #GDataEntry
+ * @updated: the new updated value
+ *
+ * Sets the value of the #GDataEntry:updated property to @updated.
+ *
+ * Since: 0.6.0
+ */
+void
+_gdata_entry_set_updated (GDataEntry *self, GTimeVal *updated)
+{
+	g_return_if_fail (GDATA_IS_ENTRY (self));
+	g_return_if_fail (updated != NULL);
+
+	self->priv->updated = *updated;
+}
+
 /**
  * gdata_entry_get_published:
  * @self: a #GDataEntry
@@ -912,3 +969,22 @@ gdata_entry_set_rights (GDataEntry *self, const gchar *rights)
 	self->priv->rights = g_strdup (rights);
 	g_object_notify (G_OBJECT (self), "rights");
 }
+
+/*
+ * _gdata_entry_set_batch_data:
+ * @self: a #GDataEntry
+ * @id: the batch operation ID
+ * @type: the type of batch operation being performed on the #GDataEntry
+ *
+ * Sets the batch operation data needed when outputting the XML for a #GDataEntry to be put into a batch operation feed.
+ *
+ * Since: 0.6.0
+ */
+void
+_gdata_entry_set_batch_data (GDataEntry *self, guint id, GDataBatchOperationType type)
+{
+	g_return_if_fail (GDATA_IS_ENTRY (self));
+
+	self->priv->batch_id = id;
+	self->priv->batch_operation_type = type;
+}
diff --git a/gdata/gdata-feed.c b/gdata/gdata-feed.c
index 4dc98c1..0a19794 100644
--- a/gdata/gdata-feed.c
+++ b/gdata/gdata-feed.c
@@ -50,6 +50,8 @@ static void gdata_feed_get_property (GObject *object, guint property_id, GValue
 static gboolean pre_parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *root_node, gpointer user_data, GError **error);
 static gboolean parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error);
 static gboolean post_parse_xml (GDataParsable *parsable, gpointer user_data, GError **error);
+static void get_xml (GDataParsable *parsable, GString *xml_string);
+static void get_namespaces (GDataParsable *parsable, GHashTable *namespaces);
 
 static void _gdata_feed_add_category (GDataFeed *self, GDataCategory *category);
 static void _gdata_feed_add_link (GDataFeed *self, GDataLink *link);
@@ -106,6 +108,8 @@ gdata_feed_class_init (GDataFeedClass *klass)
 	parsable_class->pre_parse_xml = pre_parse_xml;
 	parsable_class->parse_xml = parse_xml;
 	parsable_class->post_parse_xml = post_parse_xml;
+	parsable_class->get_xml = get_xml;
+	parsable_class->get_namespaces = get_namespaces;
 	parsable_class->element_name = "feed";
 
 	/**
@@ -542,6 +546,66 @@ post_parse_xml (GDataParsable *parsable, gpointer user_data, GError **error)
 	return TRUE;
 }
 
+static void
+get_xml (GDataParsable *parsable, GString *xml_string)
+{
+	GDataFeedPrivate *priv = GDATA_FEED (parsable)->priv;
+	GList *entries;
+	gchar *updated;
+
+	/* NOTE: Only the required elements are implemented at the moment */
+	gdata_parser_string_append_escaped (xml_string, "<title type='text'>", priv->title, "</title>");
+	g_string_append_printf (xml_string, "<id>%s</id>", priv->id);
+
+	updated = g_time_val_to_iso8601 (&(priv->updated));
+	g_string_append_printf (xml_string, "<updated>%s</updated>", updated);
+	g_free (updated);
+
+	/* Entries */
+	for (entries = priv->entries; entries != NULL; entries = entries->next)
+		_gdata_parsable_get_xml (GDATA_PARSABLE (entries->data), xml_string, FALSE);
+}
+
+static void
+get_namespaces (GDataParsable *parsable, GHashTable *namespaces)
+{
+	GDataFeedPrivate *priv = GDATA_FEED (parsable)->priv;
+
+	/* Assume that all the entries in the feed have identical namespaces, so we just call get_namespaces() for the first one */
+	if (priv->entries != NULL)
+		GDATA_PARSABLE_GET_CLASS (priv->entries->data)->get_namespaces (GDATA_PARSABLE (priv->entries->data), namespaces);
+}
+
+/*
+ * _gdata_feed_new:
+ * @title: the feed's title
+ * @id: the feed's ID
+ * @updated: when the feed was last updated
+ *
+ * Creates a new #GDataFeed with the bare minimum of data to be valid.
+ *
+ * Return value: a new #GDataFeed
+ *
+ * Since: 0.6.0
+ */
+GDataFeed *
+_gdata_feed_new (const gchar *title, const gchar *id, GTimeVal *updated)
+{
+	GDataFeed *feed;
+
+	g_return_val_if_fail (title != NULL, NULL);
+	g_return_val_if_fail (id != NULL, NULL);
+	g_return_val_if_fail (updated != NULL, NULL);
+
+	feed = g_object_new (GDATA_TYPE_FEED, NULL);
+	feed->priv->title = g_strdup (title);
+	feed->priv->id = g_strdup (id);
+	feed->priv->updated.tv_sec = updated->tv_sec;
+	feed->priv->updated.tv_usec = updated->tv_usec;
+
+	return feed;
+}
+
 GDataFeed *
 _gdata_feed_new_from_xml (GType feed_type, const gchar *xml, gint length, GType entry_type,
                           GDataQueryProgressCallback progress_callback, gpointer progress_user_data, GError **error)
diff --git a/gdata/gdata-private.h b/gdata/gdata-private.h
index 2e6fb29..58d704b 100644
--- a/gdata/gdata-private.h
+++ b/gdata/gdata-private.h
@@ -67,6 +67,7 @@ void _gdata_parsable_get_xml (GDataParsable *self, GString *xml_string, gboolean
 void _gdata_parsable_string_append_escaped (GString *xml_string, const gchar *pre, const gchar *element_content, const gchar *post);
 
 #include "gdata-feed.h"
+GDataFeed *_gdata_feed_new (const gchar *title, const gchar *id, GTimeVal *updated) G_GNUC_WARN_UNUSED_RESULT;
 GDataFeed *_gdata_feed_new_from_xml (GType feed_type, const gchar *xml, gint length, GType entry_type,
                                      GDataQueryProgressCallback progress_callback, gpointer progress_user_data,
                                      GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
@@ -75,6 +76,11 @@ gpointer _gdata_feed_parse_data_new (GType entry_type, GDataQueryProgressCallbac
 void _gdata_feed_parse_data_free (gpointer data);
 void _gdata_feed_call_progress_callback (GDataFeed *self, gpointer user_data, GDataEntry *entry);
 
+#include "gdata-entry.h"
+#include "gdata-batch-operation.h"
+void _gdata_entry_set_updated (GDataEntry *self, GTimeVal *updated);
+void _gdata_entry_set_batch_data (GDataEntry *self, guint id, GDataBatchOperationType type);
+
 #include "gdata/services/documents/gdata-documents-entry.h"
 GFile *_gdata_documents_entry_download_document (GDataDocumentsEntry *self, GDataService *service, gchar **content_type, const gchar *download_uri,
                                                  GFile *destination_directory, const gchar *file_extension, gboolean replace_file_if_exists,
diff --git a/gdata/gdata-service.c b/gdata/gdata-service.c
index 70bb2f4..03372a1 100644
--- a/gdata/gdata-service.c
+++ b/gdata/gdata-service.c
@@ -510,6 +510,12 @@ real_parse_error_response (GDataService *self, GDataOperationType operation_type
 			              * and the second is an error message returned by the server. */
 			             _("Error code %u when uploading: %s"), status, response_body);
 			break;
+		case GDATA_OPERATION_BATCH:
+			g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_WITH_BATCH_OPERATION,
+			             /* Translators: the first parameter is a HTTP status,
+			              * and the second is an error message returned by the server. */
+			             _("Error code %u when running a batch operation: %s"), status, response_body);
+			break;
 		default:
 			/* We should not be called with anything other than the above operation types */
 			g_assert_not_reached ();
diff --git a/gdata/gdata-service.h b/gdata/gdata-service.h
index 5475f5a..15632b2 100644
--- a/gdata/gdata-service.h
+++ b/gdata/gdata-service.h
@@ -37,6 +37,7 @@ G_BEGIN_DECLS
  * @GDATA_OPERATION_DOWNLOAD: a download of a file
  * @GDATA_OPERATION_UPLOAD: an upload of a file
  * @GDATA_OPERATION_AUTHENTICATION: authentication with the service
+ * @GDATA_OPERATION_BATCH: a batch operation with #GDataBatchOperation
  *
  * Representations of the different operations performed by the library.
  *
@@ -49,7 +50,8 @@ typedef enum {
 	GDATA_OPERATION_DELETION,
 	GDATA_OPERATION_DOWNLOAD,
 	GDATA_OPERATION_UPLOAD,
-	GDATA_OPERATION_AUTHENTICATION
+	GDATA_OPERATION_AUTHENTICATION,
+	GDATA_OPERATION_BATCH
 } GDataOperationType;
 
 /**
@@ -65,6 +67,7 @@ typedef enum {
  * @GDATA_SERVICE_ERROR_BAD_QUERY_PARAMETER: A given query parameter was invalid for the query type
  * @GDATA_SERVICE_ERROR_NETWORK_ERROR: The service is unavailable due to local network errors (e.g. no Internet connection)
  * @GDATA_SERVICE_ERROR_PROXY_ERROR: The service is unavailable due to proxy network errors (e.g. proxy unreachable)
+ * @GDATA_SERVICE_ERROR_WITH_BATCH_OPERATION: Generic error when running a batch operation and the whole operation fails
  *
  * Error codes for #GDataService operations.
  **/
@@ -78,7 +81,8 @@ typedef enum {
 	GDATA_SERVICE_ERROR_FORBIDDEN,
 	GDATA_SERVICE_ERROR_BAD_QUERY_PARAMETER,
 	GDATA_SERVICE_ERROR_NETWORK_ERROR,
-	GDATA_SERVICE_ERROR_PROXY_ERROR
+	GDATA_SERVICE_ERROR_PROXY_ERROR,
+	GDATA_SERVICE_ERROR_WITH_BATCH_OPERATION
 } GDataServiceError;
 
 /**
diff --git a/gdata/gdata.h b/gdata/gdata.h
index 1c57d6d..c6d252d 100644
--- a/gdata/gdata.h
+++ b/gdata/gdata.h
@@ -33,6 +33,8 @@
 #include <gdata/gdata-download-stream.h>
 #include <gdata/gdata-upload-stream.h>
 #include <gdata/gdata-comparable.h>
+#include <gdata/gdata-batchable.h>
+#include <gdata/gdata-batch-operation.h>
 
 /* Namespaces */
 
diff --git a/gdata/gdata.symbols b/gdata/gdata.symbols
index 1535ed6..0ab8b87 100644
--- a/gdata/gdata.symbols
+++ b/gdata/gdata.symbols
@@ -829,3 +829,16 @@ gdata_app_categories_get_categories
 gdata_app_categories_is_fixed
 gdata_comparable_get_type
 gdata_comparable_compare
+gdata_batch_operation_get_type
+gdata_batch_operation_get_service
+gdata_batch_operation_get_feed_uri
+gdata_batch_operation_add_query
+gdata_batch_operation_add_insertion
+gdata_batch_operation_add_update
+gdata_batch_operation_add_deletion
+gdata_batch_operation_run
+gdata_batch_operation_run_async
+gdata_batch_operation_run_finish
+gdata_batch_operation_type_get_type
+gdata_batchable_get_type
+gdata_batchable_create_operation
diff --git a/gdata/tests/common.c b/gdata/tests/common.c
index 867a5b9..e1185f5 100644
--- a/gdata/tests/common.c
+++ b/gdata/tests/common.c
@@ -34,3 +34,220 @@ gdata_test_init (int *argc, char ***argv)
 	/* Enable full debugging */
 	g_setenv ("LIBGDATA_DEBUG", "3", FALSE);
 }
+
+typedef struct {
+	guint op_id;
+	GDataBatchOperationType operation_type;
+	GDataEntry *entry;
+	GDataEntry **returned_entry;
+	gchar *id;
+	GType entry_type;
+	GError **error;
+} BatchOperationData;
+
+static void
+batch_operation_data_free (BatchOperationData *data)
+{
+	if (data->entry != NULL)
+		g_object_unref (data->entry);
+	g_free (data->id);
+
+	/* We don't free data->error, as it's owned by the calling code */
+
+	g_slice_free (BatchOperationData, data);
+}
+
+static void
+test_batch_operation_query_cb (guint operation_id, GDataBatchOperationType operation_type, GDataEntry *entry, GError *error, gpointer user_data)
+{
+	BatchOperationData *data = user_data;
+
+	/* Check that the @operation_type and @operation_id matches those stored in @data */
+	g_assert_cmpuint (operation_id, ==, data->op_id);
+	g_assert_cmpuint (operation_type, ==, data->operation_type);
+
+	/* If data->error is set, we're expecting the operation to fail; otherwise, we're expecting it to succeed */
+	if (data->error != NULL) {
+		g_assert (error != NULL);
+		*(data->error) = g_error_copy (error);
+		g_assert (entry == NULL);
+
+		if (data->returned_entry != NULL)
+			*(data->returned_entry) = NULL;
+	} else {
+		g_assert_no_error (error);
+		g_assert (entry != NULL);
+		g_assert (entry != data->entry); /* check that the pointers aren't the same */
+		g_assert (gdata_entry_is_inserted (entry) == TRUE);
+
+		/* Check the ID and type of the returned entry */
+		/* TODO: We can't check this, because the Contacts service is stupid with IDs
+		 * g_assert_cmpstr (gdata_entry_get_id (entry), ==, data->id); */
+		g_assert (G_TYPE_CHECK_INSTANCE_TYPE (entry, data->entry_type));
+
+		/* Check the entries match */
+		if (data->entry != NULL) {
+			g_assert_cmpstr (gdata_entry_get_title (entry), ==, gdata_entry_get_title (data->entry));
+			g_assert_cmpstr (gdata_entry_get_summary (entry), ==, gdata_entry_get_summary (data->entry));
+			g_assert_cmpstr (gdata_entry_get_content (entry), ==, gdata_entry_get_content (data->entry));
+			g_assert_cmpstr (gdata_entry_get_rights (entry), ==, gdata_entry_get_rights (data->entry));
+		}
+
+		/* Copy the returned entry for the calling test code to prod later */
+		if (data->returned_entry != NULL)
+			*(data->returned_entry) = g_object_ref (entry);
+	}
+
+	/* Free the data */
+	batch_operation_data_free (data);
+}
+
+guint
+gdata_test_batch_operation_query (GDataBatchOperation *operation, const gchar *id, GType entry_type, GDataEntry *entry, GDataEntry **returned_entry,
+                                  GError **error)
+{
+	guint op_id;
+	BatchOperationData *data;
+
+	data = g_slice_new (BatchOperationData);
+	data->op_id = 0;
+	data->operation_type = GDATA_BATCH_OPERATION_QUERY;
+	data->entry = g_object_ref (entry);
+	data->returned_entry = returned_entry;
+	data->id = g_strdup (id);
+	data->entry_type = entry_type;
+	data->error = error;
+
+	op_id = gdata_batch_operation_add_query (operation, id, entry_type, test_batch_operation_query_cb, data);
+
+	data->op_id = op_id;
+
+	return op_id;
+}
+
+static void
+test_batch_operation_insertion_update_cb (guint operation_id, GDataBatchOperationType operation_type, GDataEntry *entry, GError *error,
+                                          gpointer user_data)
+{
+	BatchOperationData *data = user_data;
+
+	/* Check that the @operation_type and @operation_id matches those stored in @data */
+	g_assert_cmpuint (operation_id, ==, data->op_id);
+	g_assert_cmpuint (operation_type, ==, data->operation_type);
+
+	/* If data->error is set, we're expecting the operation to fail; otherwise, we're expecting it to succeed */
+	if (data->error != NULL) {
+		g_assert (error != NULL);
+		*(data->error) = g_error_copy (error);
+		g_assert (entry == NULL);
+
+		if (data->returned_entry != NULL)
+			*(data->returned_entry) = NULL;
+	} else {
+		g_assert_no_error (error);
+		g_assert (entry != NULL);
+		g_assert (entry != data->entry); /* check that the pointers aren't the same */
+		g_assert (gdata_entry_is_inserted (entry) == TRUE);
+
+		/* Check the entries match */
+		g_assert_cmpstr (gdata_entry_get_title (entry), ==, gdata_entry_get_title (data->entry));
+		g_assert_cmpstr (gdata_entry_get_summary (entry), ==, gdata_entry_get_summary (data->entry));
+		g_assert_cmpstr (gdata_entry_get_content (entry), ==, gdata_entry_get_content (data->entry));
+		g_assert_cmpstr (gdata_entry_get_rights (entry), ==, gdata_entry_get_rights (data->entry));
+
+		/* Copy the inserted entry for the calling test code to prod later */
+		if (data->returned_entry != NULL)
+			*(data->returned_entry) = g_object_ref (entry);
+	}
+
+	/* Free the data */
+	batch_operation_data_free (data);
+}
+
+guint
+gdata_test_batch_operation_insertion (GDataBatchOperation *operation, GDataEntry *entry, GDataEntry **inserted_entry, GError **error)
+{
+	guint op_id;
+	BatchOperationData *data;
+
+	data = g_slice_new (BatchOperationData);
+	data->op_id = 0;
+	data->operation_type = GDATA_BATCH_OPERATION_INSERTION;
+	data->entry = g_object_ref (entry);
+	data->returned_entry = inserted_entry;
+	data->id = NULL;
+	data->entry_type = G_TYPE_INVALID;
+	data->error = error;
+
+	op_id = gdata_batch_operation_add_insertion (operation, entry, test_batch_operation_insertion_update_cb, data);
+
+	data->op_id = op_id;
+
+	return op_id;
+}
+
+guint
+gdata_test_batch_operation_update (GDataBatchOperation *operation, GDataEntry *entry, GDataEntry **updated_entry, GError **error)
+{
+	guint op_id;
+	BatchOperationData *data;
+
+	data = g_slice_new (BatchOperationData);
+	data->op_id = 0;
+	data->operation_type = GDATA_BATCH_OPERATION_UPDATE;
+	data->entry = g_object_ref (entry);
+	data->returned_entry = updated_entry;
+	data->id = NULL;
+	data->entry_type = G_TYPE_INVALID;
+	data->error = error;
+
+	op_id = gdata_batch_operation_add_update (operation, entry, test_batch_operation_insertion_update_cb, data);
+
+	data->op_id = op_id;
+
+	return op_id;
+}
+
+static void
+test_batch_operation_deletion_cb (guint operation_id, GDataBatchOperationType operation_type, GDataEntry *entry, GError *error, gpointer user_data)
+{
+	BatchOperationData *data = user_data;
+
+	/* Check that the @operation_type and @operation_id matches those stored in @data */
+	g_assert_cmpuint (operation_id, ==, data->op_id);
+	g_assert_cmpuint (operation_type, ==, data->operation_type);
+	g_assert (entry == NULL);
+
+	/* If data->error is set, we're expecting the operation to fail; otherwise, we're expecting it to succeed */
+	if (data->error != NULL) {
+		g_assert (error != NULL);
+		*(data->error) = g_error_copy (error);
+	} else {
+		g_assert_no_error (error);
+	}
+
+	/* Free the data */
+	batch_operation_data_free (data);
+}
+
+guint
+gdata_test_batch_operation_deletion (GDataBatchOperation *operation, GDataEntry *entry, GError **error)
+{
+	guint op_id;
+	BatchOperationData *data;
+
+	data = g_slice_new (BatchOperationData);
+	data->op_id = 0;
+	data->operation_type = GDATA_BATCH_OPERATION_DELETION;
+	data->entry = g_object_ref (entry);
+	data->returned_entry = NULL;
+	data->id = NULL;
+	data->entry_type = G_TYPE_INVALID;
+	data->error = error;
+
+	op_id = gdata_batch_operation_add_deletion (operation, entry, test_batch_operation_deletion_cb, data);
+
+	data->op_id = op_id;
+
+	return op_id;
+}
diff --git a/gdata/tests/common.h b/gdata/tests/common.h
index 56ce80d..325d979 100644
--- a/gdata/tests/common.h
+++ b/gdata/tests/common.h
@@ -18,6 +18,7 @@
  */
 
 #include <glib.h>
+#include <gdata/gdata.h>
 
 #ifndef GDATA_TEST_COMMON_H
 #define GDATA_TEST_COMMON_H
@@ -31,6 +32,12 @@ G_BEGIN_DECLS
 
 void gdata_test_init (int *argc, char ***argv);
 
+guint gdata_test_batch_operation_query (GDataBatchOperation *operation, const gchar *id, GType entry_type,
+                                        GDataEntry *entry, GDataEntry **returned_entry, GError **error);
+guint gdata_test_batch_operation_insertion (GDataBatchOperation *operation, GDataEntry *entry, GDataEntry **inserted_entry, GError **error);
+guint gdata_test_batch_operation_update (GDataBatchOperation *operation, GDataEntry *entry, GDataEntry **updated_entry, GError **error);
+guint gdata_test_batch_operation_deletion (GDataBatchOperation *operation, GDataEntry *entry, GError **error);
+
 G_END_DECLS
 
 #endif /* !GDATA_TEST_COMMON_H */



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