[libgdata] [contacts] Add contact photo support



commit 59fca6a0a7fc0f68f0767c4260d3c0dd9625b635
Author: Philip Withnall <philip tecnocode co uk>
Date:   Tue Jun 2 00:15:49 2009 +0100

    [contacts] Add contact photo support
    
    Add support for getting, setting and removing contact photos. Full test cases
    and documentation are included.
---
 docs/reference/gdata-sections.txt                |    3 +
 gdata/gdata.symbols                              |    3 +
 gdata/services/contacts/gdata-contacts-contact.c |  245 +++++++++++++++++++++-
 gdata/services/contacts/gdata-contacts-contact.h |    8 +
 gdata/services/contacts/gdata-contacts-service.h |    3 +-
 gdata/tests/contacts.c                           |  156 ++++++++++++++
 gdata/tests/photo.jpg                            |  Bin 0 -> 17659 bytes
 7 files changed, 413 insertions(+), 5 deletions(-)

diff --git a/docs/reference/gdata-sections.txt b/docs/reference/gdata-sections.txt
index 79fac71..a26c4c1 100644
--- a/docs/reference/gdata-sections.txt
+++ b/docs/reference/gdata-sections.txt
@@ -429,6 +429,9 @@ gdata_contacts_contact_get_extended_properties
 gdata_contacts_contact_set_extended_property
 gdata_contacts_contact_get_edited
 gdata_contacts_contact_is_deleted
+gdata_contacts_contact_has_photo
+gdata_contacts_contact_get_photo
+gdata_contacts_contact_set_photo
 <SUBSECTION Standard>
 gdata_contacts_contact_get_type
 GDATA_CONTACTS_CONTACT
diff --git a/gdata/gdata.symbols b/gdata/gdata.symbols
index 404af68..349e904 100644
--- a/gdata/gdata.symbols
+++ b/gdata/gdata.symbols
@@ -351,6 +351,9 @@ gdata_contacts_contact_remove_group
 gdata_contacts_contact_is_group_deleted
 gdata_contacts_contact_get_groups
 gdata_contacts_contact_is_deleted
+gdata_contacts_contact_has_photo
+gdata_contacts_contact_get_photo
+gdata_contacts_contact_set_photo
 gdata_access_handler_get_type
 gdata_access_handler_get_rules
 gdata_access_handler_insert_rule
diff --git a/gdata/services/contacts/gdata-contacts-contact.c b/gdata/services/contacts/gdata-contacts-contact.c
index 8ac6762..c87b5d1 100644
--- a/gdata/services/contacts/gdata-contacts-contact.c
+++ b/gdata/services/contacts/gdata-contacts-contact.c
@@ -63,11 +63,13 @@ struct _GDataContactsContactPrivate {
 	GHashTable *extended_properties;
 	GHashTable *groups;
 	gboolean deleted;
+	gchar *photo_etag;
 };
 
 enum {
 	PROP_EDITED = 1,
-	PROP_DELETED
+	PROP_DELETED,
+	PROP_HAS_PHOTO
 };
 
 G_DEFINE_TYPE (GDataContactsContact, gdata_contacts_contact, GDATA_TYPE_ENTRY)
@@ -118,6 +120,19 @@ gdata_contacts_contact_class_init (GDataContactsContactClass *klass)
 					"Deleted", "Whether the entry has been deleted.",
 					FALSE,
 					G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataContactsContact:has-photo:
+	 *
+	 * Whether the contact has a photo.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_HAS_PHOTO,
+				g_param_spec_boolean ("has-photo",
+					"Has photo?", "Whether the contact has a photo.",
+					FALSE,
+					G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 }
 
 static void
@@ -145,6 +160,7 @@ gdata_contacts_contact_finalize (GObject *object)
 	g_list_free (priv->organizations);
 	g_hash_table_destroy (priv->extended_properties);
 	g_hash_table_destroy (priv->groups);
+	g_free (priv->photo_etag);
 
 	/* Chain up to the parent class */
 	G_OBJECT_CLASS (gdata_contacts_contact_parent_class)->finalize (object);
@@ -162,6 +178,9 @@ gdata_contacts_contact_get_property (GObject *object, guint property_id, GValue
 		case PROP_DELETED:
 			g_value_set_boolean (value, priv->deleted);
 			break;
+		case PROP_HAS_PHOTO:
+			g_value_set_boolean (value, (priv->photo_etag != NULL) ? TRUE : FALSE);
+			break;
 		default:
 			/* We don't have any other property... */
 			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -488,9 +507,26 @@ parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_da
 	} else if (xmlStrcmp (node->name, (xmlChar*) "deleted") == 0) {
 		/* gd:deleted */
 		self->priv->deleted = TRUE;
-	} else if (GDATA_PARSABLE_CLASS (gdata_contacts_contact_parent_class)->parse_xml (parsable, doc, node, user_data, error) == FALSE) {
-		/* Error! */
-		return FALSE;
+	} else {
+		/* If we haven't yet found a photo, check to see if it's a photo <link> element */
+		if (self->priv->photo_etag == NULL && xmlStrcmp (node->name, (xmlChar*) "link") == 0) {
+			xmlChar *rel = xmlGetProp (node, (xmlChar*) "rel");
+			if (xmlStrcmp (rel, (xmlChar*) "http://schemas.google.com/contacts/2008/rel#photo";) == 0) {
+				xmlChar *etag;
+
+				/* It's the photo link (http://code.google.com/apis/contacts/docs/2.0/reference.html#Photos), whose ETag we should
+				 * note down, then pass onto the parent class to parse properly */
+				etag = xmlGetProp (node, (xmlChar*) "etag");
+				self->priv->photo_etag = g_strdup ((gchar*) etag);
+				xmlFree (etag);
+			}
+			xmlFree (rel);
+		}
+
+		if (GDATA_PARSABLE_CLASS (gdata_contacts_contact_parent_class)->parse_xml (parsable, doc, node, user_data, error) == FALSE) {
+			/* Error! */
+			return FALSE;
+		}
 	}
 
 	return TRUE;
@@ -1209,3 +1245,204 @@ gdata_contacts_contact_is_deleted (GDataContactsContact *self)
 	g_return_val_if_fail (GDATA_IS_CONTACTS_CONTACT (self), FALSE);
 	return self->priv->deleted;
 }
+
+/**
+ * gdata_contacts_contact_has_photo:
+ * @self: a #GDataContactsContact
+ *
+ * Returns whether the contact has a photo attached to their contact entry. If the contact
+ * does have a photo, it can be returned using gdata_contacts_contact_get_photo().
+ *
+ * Return value: %TRUE if the contact has a photo, %FALSE otherwise
+ *
+ * Since: 0.4.0
+ **/
+gboolean
+gdata_contacts_contact_has_photo (GDataContactsContact *self)
+{
+	g_return_val_if_fail (GDATA_IS_CONTACTS_CONTACT (self), FALSE);
+	return (self->priv->photo_etag != NULL) ? TRUE : FALSE;
+}
+
+/**
+ * gdata_contacts_contact_get_photo:
+ * @self: a #GDataContactsContact
+ * @service: a #GDataContactsService
+ * @length: return location for the image length, in bytes
+ * @content_type: return location for the image's content type, or %NULL; free with g_free()
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Downloads and returns the contact's photo, if they have one. If the contact doesn't
+ * have a photo (i.e. gdata_contacts_contact_has_photo() returns %FALSE), %NULL is returned, but
+ * no error is set in @error.
+ *
+ * If @cancellable is not %NULL, then the operation can be cancelled by triggering the @cancellable object from another thread.
+ * If the operation was cancelled, the error %G_IO_ERROR_CANCELLED will be returned.
+ *
+ * If there is an error getting the photo, a %GDATA_SERVICE_ERROR_WITH_QUERY error will be returned.
+ *
+ * Return value: the image data, or %NULL; free with g_free()
+ *
+ * Since: 0.4.0
+ **/
+gchar *
+gdata_contacts_contact_get_photo (GDataContactsContact *self, GDataContactsService *service, gsize *length, gchar **content_type,
+				  GCancellable *cancellable, GError **error)
+{
+	GDataServiceClass *klass;
+	GDataLink *link;
+	SoupMessage *message;
+	guint status;
+	gchar *data;
+
+	/* TODO: async version */
+	g_return_val_if_fail (GDATA_IS_CONTACTS_CONTACT (self), NULL);
+	g_return_val_if_fail (GDATA_IS_CONTACTS_SERVICE (service), NULL);
+	g_return_val_if_fail (length != NULL, NULL);
+
+	/* Return if there is no photo */
+	if (gdata_contacts_contact_has_photo (self) == FALSE)
+		return NULL;
+
+	/* Get the photo URI */
+	link = gdata_entry_look_up_link (GDATA_ENTRY (self), "http://schemas.google.com/contacts/2008/rel#photo";);
+	g_assert (link != NULL);
+	message = soup_message_new (SOUP_METHOD_GET, link->href);
+
+	/* Make sure the headers are set */
+	klass = GDATA_SERVICE_GET_CLASS (service);
+	if (klass->append_query_headers != NULL)
+		klass->append_query_headers (GDATA_SERVICE (service), message);
+
+	/* Send the message */
+	status = _gdata_service_send_message (GDATA_SERVICE (service), message, error);
+	if (status == SOUP_STATUS_NONE) {
+		g_object_unref (message);
+		return NULL;
+	}
+
+	/* Check for cancellation */
+	if (g_cancellable_set_error_if_cancelled (cancellable, error) == TRUE) {
+		g_object_unref (message);
+		return NULL;
+	}
+
+	if (status != 200) {
+		/* Error */
+		g_assert (klass->parse_error_response != NULL);
+		klass->parse_error_response (GDATA_SERVICE (service), GDATA_SERVICE_ERROR_WITH_QUERY, status, message->reason_phrase,
+					     message->response_body->data, message->response_body->length, error);
+		g_object_unref (message);
+		return NULL;
+	}
+
+	g_assert (message->response_body->data != NULL);
+
+	/* Sort out the return values */
+	if (content_type != NULL)
+		*content_type = g_strdup (soup_message_headers_get_content_type (message->response_headers, NULL));
+	*length = message->response_body->length;
+	data = g_memdup (message->response_body->data, message->response_body->length);
+
+	/* Update the stored photo ETag */
+	g_free (self->priv->photo_etag);
+	self->priv->photo_etag = g_strdup (soup_message_headers_get_one (message->response_headers, "ETag"));
+	g_object_unref (message);
+
+	return data;
+}
+
+/**
+ * gdata_contacts_contact_set_photo:
+ * @self: a #GDataContactsContact
+ * @service: a #GDataService
+ * @data: the image data, or %NULL
+ * @length: the image length, in bytes, or %0
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Sets the contact's photo to @data or, if @data is %NULL, deletes the contact's photo.
+ *
+ * If @cancellable is not %NULL, then the operation can be cancelled by triggering the @cancellable object from another thread.
+ * If the operation was cancelled, the error %G_IO_ERROR_CANCELLED will be returned.
+ *
+ * If there is an error setting the photo, a %GDATA_SERVICE_ERROR_WITH_UPDATE error will be returned.
+ *
+ * Return value: %TRUE on success, %FALSE otherwise
+ *
+ * Since: 0.4.0
+ **/
+gboolean
+gdata_contacts_contact_set_photo (GDataContactsContact *self, GDataService *service, gchar *data, gsize length,
+				  GCancellable *cancellable, GError **error)
+{
+	GDataServiceClass *klass;
+	GDataLink *link;
+	SoupMessage *message;
+	guint status;
+	gboolean adding_photo = FALSE, deleting_photo = FALSE;
+
+	/* TODO: async version */
+	g_return_val_if_fail (GDATA_IS_CONTACTS_CONTACT (self), FALSE);
+	g_return_val_if_fail (GDATA_IS_SERVICE (service), FALSE);
+
+	if (self->priv->photo_etag == NULL && data != NULL)
+		adding_photo = TRUE;
+	else if (self->priv->photo_etag != NULL && data == NULL)
+		deleting_photo = TRUE;
+
+	/* Get the photo URI */
+	link = gdata_entry_look_up_link (GDATA_ENTRY (self), "http://schemas.google.com/contacts/2008/rel#photo";);
+	g_assert (link != NULL);
+	if (deleting_photo == TRUE)
+		message = soup_message_new (SOUP_METHOD_DELETE, link->href);
+	else
+		message = soup_message_new (SOUP_METHOD_PUT, link->href);
+
+	/* Make sure the headers are set */
+	klass = GDATA_SERVICE_GET_CLASS (service);
+	if (klass->append_query_headers != NULL)
+		klass->append_query_headers (service, message);
+
+	/* Append the ETag header if possible */
+	if (self->priv->photo_etag != NULL)
+		soup_message_headers_append (message->request_headers, "If-Match", self->priv->photo_etag);
+
+	if (deleting_photo == FALSE) {
+		/* Append the data */
+		soup_message_set_request (message, "image/*", SOUP_MEMORY_STATIC, (gchar*) data, length);
+	}
+
+	/* Send the message */
+	status = _gdata_service_send_message (service, message, error);
+	if (status == SOUP_STATUS_NONE) {
+		g_object_unref (message);
+		return FALSE;
+	}
+
+	/* Check for cancellation */
+	if (g_cancellable_set_error_if_cancelled (cancellable, error) == TRUE) {
+		g_object_unref (message);
+		return FALSE;
+	}
+
+	if (status != 200) {
+		/* Error */
+		g_assert (klass->parse_error_response != NULL);
+		klass->parse_error_response (service, GDATA_SERVICE_ERROR_WITH_UPDATE, status, message->reason_phrase, message->response_body->data,
+					     message->response_body->length, error);
+		g_object_unref (message);
+		return FALSE;
+	}
+
+	/* Update the stored photo ETag */
+	g_free (self->priv->photo_etag);
+	self->priv->photo_etag = g_strdup (soup_message_headers_get_one (message->response_headers, "ETag"));
+	g_object_unref (message);
+
+	if (adding_photo == TRUE || deleting_photo == TRUE)
+		g_object_notify (G_OBJECT (self), "has-photo");
+
+	return TRUE;
+}
diff --git a/gdata/services/contacts/gdata-contacts-contact.h b/gdata/services/contacts/gdata-contacts-contact.h
index ecf6b75..f3e9b5c 100644
--- a/gdata/services/contacts/gdata-contacts-contact.h
+++ b/gdata/services/contacts/gdata-contacts-contact.h
@@ -94,6 +94,14 @@ void gdata_contacts_contact_remove_group (GDataContactsContact *self, const gcha
 gboolean gdata_contacts_contact_is_group_deleted (GDataContactsContact *self, const gchar *href);
 GList *gdata_contacts_contact_get_groups (GDataContactsContact *self) G_GNUC_WARN_UNUSED_RESULT;
 
+#include <gdata/services/contacts/gdata-contacts-service.h>
+
+gboolean gdata_contacts_contact_has_photo (GDataContactsContact *self);
+gchar *gdata_contacts_contact_get_photo (GDataContactsContact *self, GDataContactsService *service, gsize *length, gchar **content_type,
+					  GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT;
+gboolean gdata_contacts_contact_set_photo (GDataContactsContact *self, GDataService *service, gchar *data, gsize length,
+					   GCancellable *cancellable, GError **error);
+
 G_END_DECLS
 
 #endif /* !GDATA_CONTACTS_CONTACT_H */
diff --git a/gdata/services/contacts/gdata-contacts-service.h b/gdata/services/contacts/gdata-contacts-service.h
index 7a64c18..eea077e 100644
--- a/gdata/services/contacts/gdata-contacts-service.h
+++ b/gdata/services/contacts/gdata-contacts-service.h
@@ -25,7 +25,6 @@
 
 #include <gdata/gdata-service.h>
 #include <gdata/gdata-query.h>
-#include <gdata/services/contacts/gdata-contacts-contact.h>
 
 G_BEGIN_DECLS
 
@@ -68,6 +67,8 @@ void gdata_contacts_service_query_contacts_async (GDataContactsService *self, GD
 						  GDataQueryProgressCallback progress_callback, gpointer progress_user_data,
 						  GAsyncReadyCallback callback, gpointer user_data);
 
+#include <gdata/services/contacts/gdata-contacts-contact.h>
+
 GDataContactsContact *gdata_contacts_service_insert_contact (GDataContactsService *self, GDataContactsContact *contact,
 							     GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT;
 
diff --git a/gdata/tests/contacts.c b/gdata/tests/contacts.c
index fb4104e..cf41c1a 100644
--- a/gdata/tests/contacts.c
+++ b/gdata/tests/contacts.c
@@ -27,6 +27,32 @@
 static GDataService *service = NULL;
 static GMainLoop *main_loop = NULL;
 
+static GDataContactsContact *
+get_contact (void)
+{
+	GDataFeed *feed;
+	GDataEntry *entry;
+	GList *entries;
+	GError *error = NULL;
+
+	g_assert (service != NULL);
+
+	feed = gdata_contacts_service_query_contacts (GDATA_CONTACTS_SERVICE (service), NULL, NULL, NULL, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_FEED (feed));
+	g_clear_error (&error);
+
+	entries = gdata_feed_get_entries (feed);
+	g_assert (entries != NULL);
+	entry = entries->data;
+	g_assert (GDATA_IS_CONTACTS_CONTACT (entry));
+
+	g_object_ref (entry);
+	g_object_unref (feed);
+
+	return GDATA_CONTACTS_CONTACT (entry);
+}
+
 static void
 test_authentication (void)
 {
@@ -239,6 +265,130 @@ test_parser_minimal (void)
 	g_assert (*gdata_entry_get_title (GDATA_ENTRY (contact)) == '\0');
 
 	/* TODO: Check the other properties */
+
+	g_object_unref (contact);
+}
+
+static void
+test_photo_has_photo (void)
+{
+	GDataContactsContact *contact;
+	gsize length = 0;
+	gchar *content_type = NULL;
+	GError *error = NULL;
+
+	contact = gdata_contacts_contact_new_from_xml (
+		"<entry xmlns='http://www.w3.org/2005/Atom' "
+			"xmlns:gd='http://schemas.google.com/g/2005'>"
+			"<id>http://www.google.com/m8/feeds/contacts/libgdata test googlemail com/base/1b46cdd20bfbee3b</id>"
+			"<updated>2009-04-25T15:21:53.688Z</updated>"
+			"<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/contact/2008#contact'/>"
+			"<title></title>" /* Here's where it all went wrong */
+			"<link rel='http://schemas.google.com/contacts/2008/rel#photo' type='image/*' "
+				"href='http://www.google.com/m8/feeds/photos/media/libgdata test googlemail com/1b46cdd20bfbee3b'/>"
+		"</entry>", -1, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_ENTRY (contact));
+	g_clear_error (&error);
+
+	/* Check for no photo */
+	g_assert (gdata_contacts_contact_has_photo (contact) == FALSE);
+	g_assert (gdata_contacts_contact_get_photo (contact, GDATA_CONTACTS_SERVICE (service), &length, &content_type, NULL, &error) == NULL);
+	g_assert_cmpint (length, ==, 0);
+	g_assert (content_type == NULL);
+	g_assert_no_error (error);
+
+	g_clear_error (&error);
+	g_free (content_type);
+	g_object_unref (contact);
+
+	/* Try again with a photo */
+	contact = gdata_contacts_contact_new_from_xml (
+		"<entry xmlns='http://www.w3.org/2005/Atom' "
+			"xmlns:gd='http://schemas.google.com/g/2005'>"
+			"<id>http://www.google.com/m8/feeds/contacts/libgdata test googlemail com/base/1b46cdd20bfbee3b</id>"
+			"<updated>2009-04-25T15:21:53.688Z</updated>"
+			"<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/contact/2008#contact'/>"
+			"<title></title>" /* Here's where it all went wrong */
+			"<link rel='http://schemas.google.com/contacts/2008/rel#photo' type='image/*' "
+				"href='http://www.google.com/m8/feeds/photos/media/libgdata test googlemail com/1b46cdd20bfbee3b' "
+				"gd:etag='&quot;QngzcDVSLyp7ImA9WxJTFkoITgU.&quot;'/>"
+		"</entry>", -1, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_ENTRY (contact));
+	g_clear_error (&error);
+
+	g_assert (gdata_contacts_contact_has_photo (contact) == TRUE);
+	g_object_unref (contact);
+}
+
+static void
+test_photo_add (void)
+{
+	GDataContactsContact *contact;
+	gchar *data;
+	gsize length;
+	gboolean retval;
+	GError *error = NULL;
+
+	/* Get the photo */
+	/* TODO: Fix the path */
+	g_assert (g_file_get_contents ("/home/philip/Development/libgdata/gdata/tests/photo.jpg", &data, &length, NULL) == TRUE);
+
+	/* Add it to the contact */
+	contact = get_contact ();
+	retval = gdata_contacts_contact_set_photo (contact, service, data, length, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (retval == TRUE);
+
+	g_clear_error (&error);
+	g_object_unref (contact);
+	g_free (data);
+}
+
+static void
+test_photo_get (void)
+{
+	GDataContactsContact *contact;
+	gchar *data, *content_type = NULL;
+	gsize length = 0;
+	GError *error = NULL;
+
+	contact = get_contact ();
+	g_assert (gdata_contacts_contact_has_photo (contact) == TRUE);
+
+	/* Get the photo from the network */
+	data = gdata_contacts_contact_get_photo (contact, GDATA_CONTACTS_SERVICE (service), &length, &content_type, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (data != NULL);
+	g_assert (length != 0);
+	g_assert_cmpstr (content_type, ==, "image/jpg");
+
+	g_assert (gdata_contacts_contact_has_photo (contact) == TRUE);
+
+	g_free (content_type);
+	g_free (data);
+	g_object_unref (contact);
+	g_clear_error (&error);
+}
+
+static void
+test_photo_delete (void)
+{
+	GDataContactsContact *contact;
+	GError *error = NULL;
+
+	contact = get_contact ();
+	g_assert (gdata_contacts_contact_has_photo (contact) == TRUE);
+
+	/* Remove the contact's photo */
+	g_assert (gdata_contacts_contact_set_photo (contact, service, NULL, 0, NULL, &error) == TRUE);
+	g_assert_no_error (error);
+
+	g_assert (gdata_contacts_contact_has_photo (contact) == FALSE);
+
+	g_clear_error (&error);
+	g_object_unref (contact);
 }
 
 int
@@ -259,6 +409,12 @@ main (int argc, char *argv[])
 		g_test_add_func ("/contacts/insert/simple", test_insert_simple);
 	g_test_add_func ("/contacts/query/uri", test_query_uri);
 	g_test_add_func ("/contacts/parser/minimal", test_parser_minimal);
+	g_test_add_func ("/contacts/photo/has_photo", test_photo_has_photo);
+	if (g_test_slow () == TRUE) {
+		g_test_add_func ("/contacts/photo/add", test_photo_add);
+		g_test_add_func ("/contacts/photo/get", test_photo_get);
+		g_test_add_func ("/contacts/photo/delete", test_photo_delete);
+	}
 
 	retval = g_test_run ();
 	if (service != NULL)
diff --git a/gdata/tests/photo.jpg b/gdata/tests/photo.jpg
new file mode 100644
index 0000000..281171d
Binary files /dev/null and b/gdata/tests/photo.jpg differ



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