Blob Blame History Raw
/* -*- 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: Stable
 * @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.
 *
 * If authorization is required for any of the requests in the batch operation, the #GDataService set in #GDataBatchOperation:service must have
 * a #GDataAuthorizer set as its #GDataService:authorizer property, and that authorizer must be authorized for the #GDataAuthorizationDomain set
 * in #GDataBatchOperation:authorization-domain. It's not possible for requests in a single batch operation to be authorized under multiple domains;
 * in that case, the requests must be split up across several batch operations using different authorization domains.
 *
 * If all of the requests in the batch operation don't require authorization (i.e. they all operate on public data; see the documentation for the
 * #GDataService subclass in question's operations for details of which require authorization), #GDataBatchOperation:authorization-domain can be set
 * to %NULL to save the overhead of sending authorization data to the online service.
 *
 * <example>
 * 	<title>Running a Synchronous Operation</title>
 * 	<programlisting>
 *	guint op_id, op_id2;
 *	GDataBatchOperation *operation;
 *	GDataContactsContact *contact;
 *	GDataService *service;
 *	GDataAuthorizationDomain *domain;
 *
 *	service = create_contacts_service ();
 *	domain = get_authorization_domain_from_service (service);
 *	contact = create_new_contact ();
 *	batch_link = gdata_feed_look_up_link (contacts_feed, GDATA_LINK_BATCH);
 *
 *	operation = gdata_batchable_create_operation (GDATA_BATCHABLE (service), domain, 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_entry_get_id (other_contact), GDATA_TYPE_CONTACTS_CONTACT, query_cb, user_data);
 *
 *	g_object_unref (contact);
 *	g_object_unref (domain);
 *	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 <glib/gi18n-lib.h>
#include <string.h>

#include "gdata-batch-operation.h"
#include "gdata-batch-feed.h"
#include "gdata-batchable.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;
	GDataAuthorizationDomain *authorization_domain;
	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,
	PROP_AUTHORIZATION_DOMAIN,
};

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:authorization-domain:
	 *
	 * The authorization domain for the batch operation, against which the #GDataService:authorizer for the #GDataBatchOperation:service should be
	 * authorized. This may be %NULL if authorization is not needed for any of the requests in the batch operation.
	 *
	 * All requests in the batch operation must be authorizable under this single authorization domain. If requests need different authorization
	 * domains, they must be performed in different batch operations.
	 *
	 * Since: 0.9.0
	 */
	g_object_class_install_property (gobject_class, PROP_AUTHORIZATION_DOMAIN,
	                                 g_param_spec_object ("authorization-domain",
	                                                      "Authorization domain", "The authorization domain for the batch operation.",
	                                                      GDATA_TYPE_AUTHORIZATION_DOMAIN,
	                                                      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_AUTHORIZATION_DOMAIN:
			g_value_set_object (value, priv->authorization_domain);
			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;
		/* Construct only */
		case PROP_AUTHORIZATION_DOMAIN:
			priv->authorization_domain = 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->authorization_domain != NULL)
		g_object_unref (priv->authorization_domain);
	priv->authorization_domain = NULL;

	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: (transfer none): 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_authorization_domain:
 * @self: a #GDataBatchOperation
 *
 * Gets the #GDataBatchOperation:authorization-domain property.
 *
 * Return value: (transfer none) (allow-none): the #GDataAuthorizationDomain used to authorize the batch operation, or %NULL
 *
 * Since: 0.9.0
 */
GDataAuthorizationDomain *
gdata_batch_operation_get_authorization_domain (GDataBatchOperation *self)
{
	g_return_val_if_fail (GDATA_IS_BATCH_OPERATION (self), NULL);

	return self->priv->authorization_domain;
}

/**
 * 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 can be called if the user-supplied callback is %NULL (e.g. in the case that the callback's been
 * called before). */
static gboolean
run_callback_cb (BatchOperation *op)
{
	if (op->callback != NULL)
		op->callback (op->id, op->type, op->entry, op->error, op->user_data);

	/* Unset the callback so that it can't be called again */
	op->callback = NULL;

	return FALSE;
}

/**
 * _gdata_batch_operation_run_callback:
 * @self: a #GDataBatchOperation
 * @op: the #BatchOperation which has been finished
 * @entry: (allow-none): 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) {
		/* Send the callback; use G_PRIORITY_DEFAULT rather than G_PRIORITY_DEFAULT_IDLE
		 * to contend with the priorities used by the callback functions in GAsyncResult */
		g_idle_add_full (G_PRIORITY_DEFAULT, (GSourceFunc) run_callback_cb, op, NULL);
	} 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: (scope async): a #GDataBatchOperationCallback to call when the query is finished, or %NULL
 * @user_data: (closure): 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: (scope async): a #GDataBatchOperationCallback to call when the insertion is finished, or %NULL
 * @user_data: (closure): 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: (scope async): a #GDataBatchOperationCallback to call when the update is finished, or %NULL
 * @user_data: (closure): 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: (scope async): a #GDataBatchOperationCallback to call when the deletion is finished, or %NULL
 * @user_data: (closure): 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);
}

/**
 * gdata_batch_operation_run:
 * @self: a #GDataBatchOperation
 * @cancellable: (allow-none): 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 callbacks for all of the operations in the batch operation are always guaranteed to be called, even if the batch operation as a whole fails.
 * Each callback will be called exactly once for each time gdata_batch_operation_run() is called.
 *
 * 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;
	GHashTableIter iter;
	gpointer op_id;
	BatchOperation *op;
	GError *child_error = NULL;

	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);

	/* Check for early cancellation. */
	if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
		return FALSE;
	}

	/* Check whether the service actually supports these kinds of
	 * operations. */
	g_hash_table_iter_init (&iter, priv->operations);
	while (g_hash_table_iter_next (&iter, &op_id, (gpointer*) &op) == TRUE) {
		GDataBatchable *batchable = GDATA_BATCHABLE (priv->service);
		GDataBatchableIface *batchable_iface;

		batchable_iface = GDATA_BATCHABLE_GET_IFACE (batchable);

		if (batchable_iface->is_supported != NULL &&
		    !batchable_iface->is_supported (op->type)) {
			g_set_error (error, GDATA_SERVICE_ERROR,
			             GDATA_SERVICE_ERROR_WITH_BATCH_OPERATION,
			             _("Batch operations are unsupported by "
			               "this service."));
			return FALSE;
		}
	}

	message = _gdata_service_build_message (priv->service, priv->authorization_domain, SOUP_METHOD_POST, priv->feed_uri, NULL, TRUE);

	/* Build the request */
	g_get_current_time (&updated);
	feed = _gdata_feed_new (GDATA_TYPE_FEED, "Batch operation feed",
	                        "batch1", updated.tv_sec);

	g_hash_table_iter_init (&iter, priv->operations);
	while (g_hash_table_iter_next (&iter, &op_id, (gpointer*) &op) == TRUE) {
		if (op->type == GDATA_BATCH_OPERATION_QUERY) {
			/* Queries are weird; build a new throwaway entry, and add it to the feed */
			GDataEntry *entry;
			GDataEntryClass *klass;
			gchar *entry_uri;

			klass = g_type_class_ref (op->entry_type);
			g_assert (klass->get_entry_uri != NULL);

			entry_uri = klass->get_entry_uri (op->query_id);
			entry = gdata_entry_new (entry_uri);
			g_free (entry_uri);

			gdata_entry_set_title (entry, "Batch operation query");
			_gdata_entry_set_updated (entry, updated.tv_sec);

			_gdata_entry_set_batch_data (entry, op->id, op->type);
			_gdata_feed_add_entry (feed, entry);

			g_type_class_unref (klass);
			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);
		}
	}

	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, &child_error);

	if (status != SOUP_STATUS_OK) {
		/* Iff status is SOUP_STATUS_NONE or SOUP_STATUS_CANCELLED, child_error has already been set */
		if (status != SOUP_STATUS_NONE && status != SOUP_STATUS_CANCELLED) {
			/* 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, &child_error);
		}
		g_object_unref (message);

		goto error;
	}

	/* 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, &child_error));
	g_object_unref (message);

	if (feed == NULL)
		goto error;
	g_object_unref (feed);

	return TRUE;

error:
	/* Call the callbacks for each of our operations to notify them of the error */
	g_hash_table_iter_init (&iter, priv->operations);
	while (g_hash_table_iter_next (&iter, &op_id, (gpointer*) &op) == TRUE)
		_gdata_batch_operation_run_callback (self, op, NULL, g_error_copy (child_error));

	g_propagate_error (error, child_error);

	return FALSE;
}

static void
run_thread (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable)
{
	GDataBatchOperation *operation = GDATA_BATCH_OPERATION (source_object);
	g_autoptr(GError) error = NULL;

	/* Run the batch operation and return */
	if (!gdata_batch_operation_run (operation, cancellable, &error))
		g_task_return_error (task, g_steal_pointer (&error));
	else
		g_task_return_boolean (task, TRUE);
}

/**
 * gdata_batch_operation_run_async:
 * @self: a #GDataBatchOperation
 * @cancellable: (allow-none): a #GCancellable, or %NULL
 * @callback: a #GAsyncReadyCallback to call when the batch operation is finished, or %NULL
 * @user_data: (closure): 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)
{
	g_autoptr(GTask) task = NULL;

	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;

	task = g_task_new (self, cancellable, callback, user_data);
	g_task_set_source_tag (task, gdata_batch_operation_run_async);
	g_task_run_in_thread (task, run_thread);
}

/**
 * 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)
{
	GDataBatchOperationPrivate *priv = self->priv;
	g_autoptr(GError) child_error = NULL;

	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_return_val_if_fail (g_task_is_valid (async_result, self), FALSE);
	g_return_val_if_fail (g_async_result_is_tagged (async_result, gdata_batch_operation_run_async), FALSE);

	if (!g_task_propagate_boolean (G_TASK (async_result), &child_error)) {
		if (priv->has_run == FALSE) {
			GHashTableIter iter;
			gpointer op_id;
			BatchOperation *op;

			/* Temporarily mark the operation as synchronous so that the callbacks get dispatched in this thread */
			priv->is_async = FALSE;

			/* If has_run hasn't been set, the call to gdata_batch_operation_run() was never made in the thread, and so none of the
			 * operations' callbacks have been called. Call the callbacks for each of our operations to notify them of the error.
			 * If has_run has been set, gdata_batch_operation_run() has already done this for us. */
			g_hash_table_iter_init (&iter, priv->operations);
			while (g_hash_table_iter_next (&iter, &op_id, (gpointer*) &op) == TRUE)
				_gdata_batch_operation_run_callback (self, op, NULL, g_error_copy (child_error));

			priv->is_async = TRUE;
		}

		g_propagate_error (error, g_steal_pointer (&child_error));

		return FALSE;
	}

	return TRUE;
}