/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ /* * GData Client * Copyright (C) Philip Withnall 2009ā€“2010 * * 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 . */ /** * SECTION:gdata-parsable * @short_description: GData parsable object * @stability: Stable * @include: gdata/gdata-parsable.h * * #GDataParsable is an abstract class allowing easy implementation of an extensible parser. It is primarily extended by #GDataFeed and #GDataEntry, * both of which require XML parsing which can be extended by subclassing. * * It allows methods to be defined for handling the root XML node, each of its child nodes, and a method to be called after parsing is complete. * * Since: 0.3.0 **/ #include #include #include #include #include #include #include "gdata-parsable.h" #include "gdata-private.h" #include "gdata-parser.h" GQuark gdata_parser_error_quark (void) { return g_quark_from_static_string ("gdata-parser-error-quark"); } static void gdata_parsable_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec); static void gdata_parsable_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); static void gdata_parsable_finalize (GObject *object); static gboolean real_parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error); static gboolean real_parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError **error); static const gchar *get_content_type (void); struct _GDataParsablePrivate { /* XML stuff. */ GString *extra_xml; GHashTable *extra_namespaces; /* JSON stuff. */ GHashTable/**/ *extra_json; gboolean constructed_from_xml; }; enum { PROP_CONSTRUCTED_FROM_XML = 1, }; G_DEFINE_ABSTRACT_TYPE (GDataParsable, gdata_parsable, G_TYPE_OBJECT) static void gdata_parsable_class_init (GDataParsableClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); g_type_class_add_private (klass, sizeof (GDataParsablePrivate)); gobject_class->get_property = gdata_parsable_get_property; gobject_class->set_property = gdata_parsable_set_property; gobject_class->finalize = gdata_parsable_finalize; klass->parse_xml = real_parse_xml; klass->parse_json = real_parse_json; klass->get_content_type = get_content_type; /** * GDataParsable:constructed-from-xml: * * Specifies whether the object was constructed by parsing XML or manually. * * Since: 0.7.0 **/ g_object_class_install_property (gobject_class, PROP_CONSTRUCTED_FROM_XML, g_param_spec_boolean ("constructed-from-xml", "Constructed from XML?", "Specifies whether the object was constructed by parsing XML or manually.", FALSE, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); } static void gdata_parsable_init (GDataParsable *self) { self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GDATA_TYPE_PARSABLE, GDataParsablePrivate); self->priv->extra_xml = g_string_new (""); self->priv->extra_namespaces = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); self->priv->extra_json = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) json_node_free); self->priv->constructed_from_xml = FALSE; } static void gdata_parsable_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GDataParsablePrivate *priv = GDATA_PARSABLE (object)->priv; switch (property_id) { case PROP_CONSTRUCTED_FROM_XML: g_value_set_boolean (value, priv->constructed_from_xml); break; default: /* We don't have any other property... */ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void gdata_parsable_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GDataParsablePrivate *priv = GDATA_PARSABLE (object)->priv; switch (property_id) { case PROP_CONSTRUCTED_FROM_XML: priv->constructed_from_xml = g_value_get_boolean (value); break; default: /* We don't have any other property... */ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void gdata_parsable_finalize (GObject *object) { GDataParsablePrivate *priv = GDATA_PARSABLE (object)->priv; g_string_free (priv->extra_xml, TRUE); g_hash_table_destroy (priv->extra_namespaces); g_hash_table_destroy (priv->extra_json); /* Chain up to the parent class */ G_OBJECT_CLASS (gdata_parsable_parent_class)->finalize (object); } static gboolean real_parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error) { xmlBuffer *buffer; xmlNs **namespaces, **namespace; /* Unhandled XML */ buffer = xmlBufferCreate (); xmlNodeDump (buffer, doc, node, 0, 0); g_string_append (parsable->priv->extra_xml, (gchar*) xmlBufferContent (buffer)); g_debug ("Unhandled XML in %s: %s", G_OBJECT_TYPE_NAME (parsable), (gchar*) xmlBufferContent (buffer)); xmlBufferFree (buffer); /* Get the namespaces */ namespaces = xmlGetNsList (doc, node); if (namespaces == NULL) return TRUE; for (namespace = namespaces; *namespace != NULL; namespace++) { if ((*namespace)->prefix != NULL) { /* NOTE: These two g_strdup()s leak, but it's probably acceptable, given that it saves us * g_strdup()ing every other namespace we put in @extra_namespaces. */ g_hash_table_insert (parsable->priv->extra_namespaces, g_strdup ((gchar*) ((*namespace)->prefix)), g_strdup ((gchar*) ((*namespace)->href))); } } xmlFree (namespaces); return TRUE; } /* Extract the member node. This would be a lot easier if JsonReader had an API to return * the current node (regardless of whether it's a value, object or array). FIXME: bgo#707100. */ static JsonNode * /* transfer full */ _json_reader_dup_current_node (JsonReader *reader) { JsonNode *value; if (json_reader_is_value (reader) == TRUE) { /* Value nodes are easy. Well, ignoring the complication of nulls. */ if (json_reader_get_null_value (reader) == TRUE) { value = json_node_new (JSON_NODE_NULL); } else { value = json_node_copy (json_reader_get_value (reader)); } } else if (json_reader_is_object (reader) == TRUE) { /* Object nodes require deep copies. */ gint i; gchar **members; JsonObject *obj; obj = json_object_new (); for (i = 0, members = json_reader_list_members (reader); members[i] != NULL; i++) { json_reader_read_member (reader, members[i]); json_object_set_member (obj, members[i], _json_reader_dup_current_node (reader)); json_reader_end_member (reader); } g_strfreev (members); value = json_node_new (JSON_NODE_OBJECT); json_node_take_object (value, obj); } else if (json_reader_is_array (reader) == TRUE) { /* Array nodes require deep copies. */ gint i, elements; JsonArray *arr; arr = json_array_new (); for (i = 0, elements = json_reader_count_elements (reader); i < elements; i++) { json_reader_read_element (reader, i); json_array_add_element (arr, _json_reader_dup_current_node (reader)); json_reader_end_element (reader); } value = json_node_new (JSON_NODE_ARRAY); json_node_take_array (value, arr); } else { /* Uh-oh. */ g_assert_not_reached (); } return value; } static gboolean real_parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError **error) { gchar *json, *member_name; JsonGenerator *generator; JsonNode *value; /* Unhandled JSON member. Save it and its value to ->extra_xml so that it's not lost if we * re-upload this Parsable to the server. */ member_name = g_strdup (json_reader_get_member_name (reader)); g_assert (member_name != NULL); /* Extract a copy of the current node. */ value = _json_reader_dup_current_node (reader); g_assert (value != NULL); /* Serialise the value for debugging. */ generator = json_generator_new (); json_generator_set_root (generator, value); json = json_generator_to_data (generator, NULL); g_debug ("Unhandled JSON member ā€˜%sā€™ in %s: %s", member_name, G_OBJECT_TYPE_NAME (parsable), json); g_free (json); g_object_unref (generator); /* Save the value. Transfer ownership of the member_name and value. */ g_hash_table_replace (parsable->priv->extra_json, (gpointer) member_name, (gpointer) value); return TRUE; } static const gchar * get_content_type (void) { return "application/atom+xml"; } /** * gdata_parsable_new_from_xml: * @parsable_type: the type of the class represented by the XML * @xml: the XML for just the parsable object, with full namespace declarations * @length: the length of @xml, or -1 * @error: a #GError, or %NULL * * Creates a new #GDataParsable subclass (of the given @parsable_type) from the given @xml. * * An object of the given @parsable_type is created, and its pre_parse_xml, parse_xml and * post_parse_xml class functions called on the XML tree obtained from @xml. pre_parse_xml and * post_parse_xml are called once each on the root node of the tree, while parse_xml is called for * each of the child nodes of the root node. * * If @length is -1, @xml will be assumed to be null-terminated. * * If an error occurs during parsing, a suitable error from #GDataParserError will be returned. * * Return value: a new #GDataParsable, or %NULL; unref with g_object_unref() * * Since: 0.4.0 **/ GDataParsable * gdata_parsable_new_from_xml (GType parsable_type, const gchar *xml, gint length, GError **error) { g_return_val_if_fail (g_type_is_a (parsable_type, GDATA_TYPE_PARSABLE), NULL); g_return_val_if_fail (xml != NULL && *xml != '\0', NULL); g_return_val_if_fail (length >= -1, NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); return _gdata_parsable_new_from_xml (parsable_type, xml, length, NULL, error); } GDataParsable * _gdata_parsable_new_from_xml (GType parsable_type, const gchar *xml, gint length, gpointer user_data, GError **error) { xmlDoc *doc; xmlNode *node; GDataParsable *parsable; static gboolean libxml_initialised = FALSE; g_return_val_if_fail (g_type_is_a (parsable_type, GDATA_TYPE_PARSABLE), NULL); g_return_val_if_fail (xml != NULL && *xml != '\0', NULL); g_return_val_if_fail (length >= -1, NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); /* Set up libxml. We do this here to avoid introducing a libgdata setup function, which would be unnecessary hassle. This is the only place * that libxml can be initialised in the library. */ if (libxml_initialised == FALSE) { /* Change the libxml memory allocation functions to be GLib's. This means we don't have to re-allocate all the strings we get from * libxml, which cuts down on strdup() calls dramatically. */ xmlMemSetup ((xmlFreeFunc) g_free, (xmlMallocFunc) g_malloc, (xmlReallocFunc) g_realloc, (xmlStrdupFunc) g_strdup); libxml_initialised = TRUE; } if (length == -1) length = strlen (xml); /* Parse the XML */ doc = xmlReadMemory (xml, length, "/dev/null", NULL, 0); if (doc == NULL) { xmlError *xml_error = xmlGetLastError (); g_set_error (error, GDATA_PARSER_ERROR, GDATA_PARSER_ERROR_PARSING_STRING, /* Translators: the parameter is an error message */ _("Error parsing XML: %s"), (xml_error != NULL) ? xml_error->message : NULL); return NULL; } /* Get the root element */ node = xmlDocGetRootElement (doc); if (node == NULL) { /* XML document's empty */ xmlFreeDoc (doc); g_set_error (error, GDATA_PARSER_ERROR, GDATA_PARSER_ERROR_EMPTY_DOCUMENT, _("Error parsing XML: %s"), /* Translators: this is a dummy error message to be substituted into "Error parsing XML: %s". */ _("Empty document.")); return NULL; } parsable = _gdata_parsable_new_from_xml_node (parsable_type, doc, node, user_data, error); xmlFreeDoc (doc); return parsable; } GDataParsable * _gdata_parsable_new_from_xml_node (GType parsable_type, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error) { GDataParsable *parsable; GDataParsableClass *klass; g_return_val_if_fail (g_type_is_a (parsable_type, GDATA_TYPE_PARSABLE), NULL); g_return_val_if_fail (doc != NULL, NULL); g_return_val_if_fail (node != NULL, NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); parsable = g_object_new (parsable_type, "constructed-from-xml", TRUE, NULL); klass = GDATA_PARSABLE_GET_CLASS (parsable); if (klass->parse_xml == NULL) { g_object_unref (parsable); return NULL; } g_assert (klass->element_name != NULL); /* TODO: See gdata-documents-entry.c:260 for an example of where the code below doesn't work */ /*if (xmlStrcmp (node->name, (xmlChar*) klass->element_name) != 0 || (node->ns != NULL && xmlStrcmp (node->ns->prefix, (xmlChar*) klass->element_namespace) != 0)) { * No element (required) * gdata_parser_error_required_element_missing (klass->element_name, "root", error); return NULL; }*/ /* Call the pre-parse function first */ if (klass->pre_parse_xml != NULL && klass->pre_parse_xml (parsable, doc, node, user_data, error) == FALSE) { g_object_unref (parsable); return NULL; } /* Parse each child element */ node = node->children; while (node != NULL) { if (klass->parse_xml (parsable, doc, node, user_data, error) == FALSE) { g_object_unref (parsable); return NULL; } node = node->next; } /* Call the post-parse function */ if (klass->post_parse_xml != NULL && klass->post_parse_xml (parsable, user_data, error) == FALSE) { g_object_unref (parsable); return NULL; } return parsable; } /** * gdata_parsable_new_from_json: * @parsable_type: the type of the class represented by the JSON * @json: the JSON for just the parsable object * @length: the length of @json, or -1 * @error: a #GError, or %NULL * * Creates a new #GDataParsable subclass (of the given @parsable_type) from the given @json. * * An object of the given @parsable_type is created, and its parse_json and * post_parse_json class functions called on the JSON node obtained from @json. * post_parse_json is called once on the root node, while parse_json is called for * each of the node's members. * * If @length is -1, @json will be assumed to be nul-terminated. * * If an error occurs during parsing, a suitable error from #GDataParserError will be returned. * * Return value: a new #GDataParsable, or %NULL; unref with g_object_unref() * * Since: 0.15.0 */ GDataParsable * gdata_parsable_new_from_json (GType parsable_type, const gchar *json, gint length, GError **error) { g_return_val_if_fail (g_type_is_a (parsable_type, GDATA_TYPE_PARSABLE), NULL); g_return_val_if_fail (json != NULL && *json != '\0', NULL); g_return_val_if_fail (length >= -1, NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); return _gdata_parsable_new_from_json (parsable_type, json, length, NULL, error); } GDataParsable * _gdata_parsable_new_from_json (GType parsable_type, const gchar *json, gint length, gpointer user_data, GError **error) { JsonParser *parser; JsonReader *reader; GDataParsable *parsable; GError *child_error = NULL; g_return_val_if_fail (g_type_is_a (parsable_type, GDATA_TYPE_PARSABLE), NULL); g_return_val_if_fail (json != NULL && *json != '\0', NULL); g_return_val_if_fail (length >= -1, NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); if (length == -1) length = strlen (json); parser = json_parser_new (); if (!json_parser_load_from_data (parser, json, length, &child_error)) { g_set_error (error, GDATA_PARSER_ERROR, GDATA_PARSER_ERROR_PARSING_STRING, /* Translators: the parameter is an error message */ _("Error parsing JSON: %s"), child_error->message); g_error_free (child_error); g_object_unref (parser); return NULL; } reader = json_reader_new (json_parser_get_root (parser)); parsable = _gdata_parsable_new_from_json_node (parsable_type, reader, user_data, error); g_object_unref (reader); g_object_unref (parser); return parsable; } GDataParsable * _gdata_parsable_new_from_json_node (GType parsable_type, JsonReader *reader, gpointer user_data, GError **error) { GDataParsable *parsable; GDataParsableClass *klass; gint i; g_return_val_if_fail (g_type_is_a (parsable_type, GDATA_TYPE_PARSABLE), NULL); g_return_val_if_fail (reader != NULL, NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); /* Indicator property which allows distinguishing between locally created and server based objects * as it is used for non-XML tasks, and adding another one for JSON would be a bit pointless. */ parsable = g_object_new (parsable_type, "constructed-from-xml", TRUE, NULL); klass = GDATA_PARSABLE_GET_CLASS (parsable); g_assert (klass->parse_json != NULL); /* Check that the outermost node is an object. */ if (json_reader_is_object (reader) == FALSE) { g_set_error (error, GDATA_PARSER_ERROR, GDATA_PARSER_ERROR_PARSING_STRING, /* Translators: the parameter is an error message */ _("Error parsing JSON: %s"), _("Outermost JSON node is not an object.")); g_object_unref (parsable); return NULL; } /* Parse each child member. This assumes the outermost node is an object. */ for (i = 0; i < json_reader_count_members (reader); i++) { g_return_val_if_fail (json_reader_read_element (reader, i), NULL); if (klass->parse_json (parsable, reader, user_data, error) == FALSE) { json_reader_end_element (reader); g_object_unref (parsable); return NULL; } json_reader_end_element (reader); } /* Call the post-parse function */ if (klass->post_parse_json != NULL && klass->post_parse_json (parsable, user_data, error) == FALSE) { g_object_unref (parsable); return NULL; } return parsable; } static void build_namespaces_cb (gchar *prefix, gchar *href, GString *output) { g_string_append_printf (output, " xmlns:%s='%s'", prefix, href); } static gboolean filter_namespaces_cb (gchar *prefix, gchar *href, GHashTable *canonical_namespaces) { if (g_hash_table_lookup (canonical_namespaces, prefix) != NULL) return TRUE; return FALSE; } /** * gdata_parsable_get_xml: * @self: a #GDataParsable * * Builds an XML representation of the #GDataParsable in its current state, such that it could be inserted on the server. The XML is guaranteed * to have all its namespaces declared properly in a self-contained fashion, and is valid for stand-alone use. * * Return value: the object's XML; free with g_free() * * Since: 0.4.0 **/ gchar * gdata_parsable_get_xml (GDataParsable *self) { GString *xml_string; g_return_val_if_fail (GDATA_IS_PARSABLE (self), NULL); xml_string = g_string_sized_new (1000); g_string_append (xml_string, ""); _gdata_parsable_get_xml (self, xml_string, TRUE); return g_string_free (xml_string, FALSE); } /* * _gdata_parsable_get_xml: * @self: a #GDataParsable * @xml_string: a #GString to build the XML in * @declare_namespaces: %TRUE if all the namespaces used in the outputted XML should be declared in the opening tag of the root element, * %FALSE otherwise * * Builds an XML representation of the #GDataParsable in its current state, such that it could be inserted on the server. If @declare_namespaces is * %TRUE, the XML is guaranteed to have all its namespaces declared properly in a self-contained fashion, and is valid for stand-alone use. If * @declare_namespaces is %FALSE, none of the used namespaces are declared, and the XML is suitable for insertion into a larger XML tree. * * Return value: the object's XML; free with g_free() * * Since: 0.4.0 */ void _gdata_parsable_get_xml (GDataParsable *self, GString *xml_string, gboolean declare_namespaces) { GDataParsableClass *klass; guint length; GHashTable *namespaces = NULL; /* shut up, gcc */ g_return_if_fail (GDATA_IS_PARSABLE (self)); g_return_if_fail (xml_string != NULL); klass = GDATA_PARSABLE_GET_CLASS (self); g_assert (klass->element_name != NULL); /* Get the namespaces the class uses */ if (declare_namespaces == TRUE && klass->get_namespaces != NULL) { namespaces = g_hash_table_new (g_str_hash, g_str_equal); klass->get_namespaces (self, namespaces); /* Remove any duplicate extra namespaces */ g_hash_table_foreach_remove (self->priv->extra_namespaces, (GHRFunc) filter_namespaces_cb, namespaces); } /* Build up the namespace list */ if (klass->element_namespace != NULL) g_string_append_printf (xml_string, "<%s:%s", klass->element_namespace, klass->element_name); else g_string_append_printf (xml_string, "<%s", klass->element_name); /* We only include the normal namespaces if we're not at the top level of XML building */ if (declare_namespaces == TRUE) { g_string_append (xml_string, " xmlns='http://www.w3.org/2005/Atom'"); if (namespaces != NULL) { g_hash_table_foreach (namespaces, (GHFunc) build_namespaces_cb, xml_string); g_hash_table_destroy (namespaces); } } g_hash_table_foreach (self->priv->extra_namespaces, (GHFunc) build_namespaces_cb, xml_string); /* Add anything the class thinks is suitable */ if (klass->pre_get_xml != NULL) klass->pre_get_xml (self, xml_string); g_string_append_c (xml_string, '>'); /* Store the length before we close the opening tag, so we can determine whether to self-close later on */ length = xml_string->len; /* Add the rest of the XML */ if (klass->get_xml != NULL) klass->get_xml (self, xml_string); /* Any extra XML? */ if (self->priv->extra_xml != NULL && self->priv->extra_xml->str != NULL) g_string_append (xml_string, self->priv->extra_xml->str); /* Close the element; either by self-closing the opening tag, or by writing out a closing tag */ if (xml_string->len == length) g_string_overwrite (xml_string, length - 1, "/>"); else if (klass->element_namespace != NULL) g_string_append_printf (xml_string, "", klass->element_namespace, klass->element_name); else g_string_append_printf (xml_string, "", klass->element_name); } /** * gdata_parsable_get_json: * @self: a #GDataParsable * * Builds a JSON representation of the #GDataParsable in its current state, such that it could be inserted on the server. The JSON * is valid for stand-alone use. * * Return value: the object's JSON; free with g_free() * * Since: 0.15.0 */ gchar * gdata_parsable_get_json (GDataParsable *self) { JsonGenerator *generator; JsonBuilder *builder; JsonNode *root; gchar *output; g_return_val_if_fail (GDATA_IS_PARSABLE (self), NULL); /* Build the JSON tree. */ builder = json_builder_new (); _gdata_parsable_get_json (self, builder); root = json_builder_get_root (builder); g_object_unref (builder); /* Serialise it to a string. */ generator = json_generator_new (); json_generator_set_root (generator, root); output = json_generator_to_data (generator, NULL); g_object_unref (generator); json_node_free (root); return output; } /* * _gdata_parsable_get_json: * @self: a #GDataParsable * @builder: a #JsonBuilder to build the JSON in * * Builds a JSON representation of the #GDataParsable in its current state, such that it could be inserted on the server. * * Since: 0.15.0 */ void _gdata_parsable_get_json (GDataParsable *self, JsonBuilder *builder) { GDataParsableClass *klass; GHashTableIter iter; gchar *member_name; JsonNode *value; g_return_if_fail (GDATA_IS_PARSABLE (self)); g_return_if_fail (JSON_IS_BUILDER (builder)); klass = GDATA_PARSABLE_GET_CLASS (self); json_builder_begin_object (builder); /* Add the JSON. */ if (klass->get_json != NULL) klass->get_json (self, builder); /* Any extra JSON which we couldn't parse before? */ g_hash_table_iter_init (&iter, self->priv->extra_json); while (g_hash_table_iter_next (&iter, (gpointer *) &member_name, (gpointer *) &value) == TRUE) { json_builder_set_member_name (builder, member_name); json_builder_add_value (builder, json_node_copy (value)); /* transfers ownership */ } json_builder_end_object (builder); } /* * _gdata_parsable_is_constructed_from_xml: * @self: a #GDataParsable * * Returns the value of #GDataParsable:constructed-from-xml. * * Return value: %TRUE if the #GDataParsable was constructed from XML, %FALSE otherwise * * Since: 0.7.0 */ gboolean _gdata_parsable_is_constructed_from_xml (GDataParsable *self) { g_return_val_if_fail (GDATA_IS_PARSABLE (self), FALSE); return self->priv->constructed_from_xml; }