xeme/xeme

Add a simple unit test for the input stream and fix a few issues
/*
* Copyright (C) 2023 Dodo Developers
*
* This library 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.
*
* This library 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 this library; if not, see <https://www.gnu.org/licenses/>.
*/
#include "dodomessage.h"
#include "dodostring.h"
enum {
PROP_0,
PROP_THREAD,
PROP_THREAD_PARENT,
N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };
struct _DodoMessage {
DodoStanza parent;
GHashTable *bodies;
GPtrArray *bodies_order;
GHashTable *subjects;
GPtrArray *subjects_order;
char *thread;
char *thread_parent;
};
/******************************************************************************
* Helpers
*****************************************************************************/
static void
dodo_message_add_element_with_language(DodoMessage *message,
GHashTable *collection,
GPtrArray *order,
const char *language,
const char *content)
{
char *lang = NULL;
gboolean found = FALSE;
gpointer key = NULL;
g_return_if_fail(DODO_IS_MESSAGE(message));
g_return_if_fail(content != NULL);
/* If the caller passes in NULL, we just convert it to empty string. */
if(!dodo_str_is_empty(language)) {
lang = g_strdup(language);
} else {
lang = g_strdup("");
}
/* Look if the language already exists, and if so, remove it from our
* ordered list of languages.
*/
found = g_hash_table_lookup_extended(collection, lang, &key, NULL);
if(found) {
g_ptr_array_remove(order, key);
}
/* Add the language to our list of ordered languages. This is used to look
* up keys in the bodies hash table, so we just keep a pointer to that
* value rather than creating a new copy and requiring a strcmp to find it
* again.
*/
g_ptr_array_add(order, lang);
/* We use g_hash_table_replace because we want to free the old key and
* have it use the new one we pass in, which is the opposite of what
* g_hash_table_insert does.
*/
g_hash_table_replace(collection, lang, g_strdup(content));
}
static void
dodo_message_marshal_language_collection(GHashTable *collection,
GPtrArray *order,
GString *str,
const char *element)
{
for(guint i = 0; i < order->len; i++) {
const char *lang = NULL;
const char *content = NULL;
lang = g_ptr_array_index(order, i);
content = g_hash_table_lookup(collection, lang);
g_string_append_printf(str, "<%s", element);
if(dodo_str_is_empty(lang)) {
g_string_append_printf(str, ">%s", content);
} else {
g_string_append_printf(str, " xml:lang=\"%s\">%s", lang, content);
}
g_string_append_printf(str, "</%s>", element);
}
}
/******************************************************************************
* DodoStanza Implementation
*****************************************************************************/
static char *
dodo_message_marshal(DodoStanza *stanza, G_GNUC_UNUSED GError **error) {
DodoMessage *message = DODO_MESSAGE(stanza);
GString *str = NULL;
char *attrs = NULL;
str = g_string_new("<message");
attrs = dodo_stanza_marshal_attributes(stanza);
if(!dodo_str_is_empty(attrs)) {
g_string_append_printf(str, " %s>", attrs);
} else {
g_string_append(str, ">");
}
g_clear_pointer(&attrs, g_free);
/* Now run through the collections. */
dodo_message_marshal_language_collection(message->subjects,
message->subjects_order, str,
"subject");
dodo_message_marshal_language_collection(message->bodies,
message->bodies_order, str,
"body");
if(!dodo_str_is_empty(message->thread)) {
if(!dodo_str_is_empty(message->thread_parent)) {
g_string_append_printf(str, "<thread parent=\"%s\">%s</thread>",
message->thread_parent, message->thread);
} else {
g_string_append_printf(str, "<thread>%s</thread>",
message->thread);
}
}
/* Finish up */
g_string_append(str, "</message>");
return g_string_free(str, FALSE);
}
/******************************************************************************
* GObject Implementation
*****************************************************************************/
G_DEFINE_TYPE(DodoMessage, dodo_message, DODO_TYPE_STANZA)
static void
dodo_message_finalize(GObject *obj) {
DodoMessage *message = DODO_MESSAGE(obj);
g_ptr_array_free(message->subjects_order, TRUE);
message->subjects_order = NULL;
g_clear_pointer(&message->subjects, g_hash_table_destroy);
g_ptr_array_free(message->bodies_order, TRUE);
message->bodies_order = NULL;
g_clear_pointer(&message->bodies, g_hash_table_destroy);
g_clear_pointer(&message->thread, g_free);
g_clear_pointer(&message->thread_parent, g_free);
G_OBJECT_CLASS(dodo_message_parent_class)->finalize(obj);
}
static void
dodo_message_get_property(GObject *obj, guint param_id, GValue *value,
GParamSpec *pspec)
{
DodoMessage *message = DODO_MESSAGE(obj);
switch(param_id) {
case PROP_THREAD:
g_value_set_string(value, dodo_message_get_thread(message));
break;
case PROP_THREAD_PARENT:
g_value_set_string(value, dodo_message_get_thread_parent(message));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
break;
}
}
static void
dodo_message_set_property(GObject *obj, guint param_id, const GValue *value,
GParamSpec *pspec)
{
DodoMessage *message = DODO_MESSAGE(obj);
switch(param_id) {
case PROP_THREAD:
dodo_message_set_thread(message, g_value_get_string(value));
break;
case PROP_THREAD_PARENT:
dodo_message_set_thread_parent(message, g_value_get_string(value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
break;
}
}
static void
dodo_message_init(DodoMessage *message) {
message->subjects = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
g_free);
message->subjects_order = g_ptr_array_new();
message->bodies = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
g_free);
message->bodies_order = g_ptr_array_new();
}
static void
dodo_message_class_init(DodoMessageClass *klass) {
GObjectClass *obj_class = G_OBJECT_CLASS(klass);
DodoStanzaClass *stanza_class = DODO_STANZA_CLASS(klass);
obj_class->finalize = dodo_message_finalize;
obj_class->get_property = dodo_message_get_property;
obj_class->set_property = dodo_message_set_property;
stanza_class->marshal = dodo_message_marshal;
/**
* DodoMessage:thread:
*
* The thread identifier for this message.
*
* Since: 0.1.0
*/
properties[PROP_THREAD] = g_param_spec_string(
"thread", "thread",
"The thread's id for this message.",
NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
/**
* DodoMessage:thread-parent:
*
* The thread parent identifier for this message.
*
* Since: 0.1.0
*/
properties[PROP_THREAD_PARENT] = g_param_spec_string(
"thread-parent", "thread-parent",
"The thread parent's id for this message.",
NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}
/******************************************************************************
* Public API
*****************************************************************************/
DodoStanza *
dodo_message_new(void) {
return g_object_new(DODO_TYPE_MESSAGE, NULL);
}
const char *
dodo_message_get_thread(DodoMessage *message) {
g_return_val_if_fail(DODO_IS_MESSAGE(message), NULL);
return message->thread;
}
void
dodo_message_set_thread(DodoMessage *message, const char *thread) {
g_return_if_fail(DODO_IS_MESSAGE(message));
if(g_strcmp0(message->thread, thread) != 0) {
g_free(message->thread);
message->thread = g_strdup(thread);
g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_THREAD]);
}
}
const char *
dodo_message_get_thread_parent(DodoMessage *message) {
g_return_val_if_fail(DODO_IS_MESSAGE(message), NULL);
return message->thread_parent;
}
void
dodo_message_set_thread_parent(DodoMessage *message,
const char *thread_parent)
{
g_return_if_fail(DODO_IS_MESSAGE(message));
if(g_strcmp0(message->thread_parent, thread_parent) != 0) {
g_free(message->thread_parent);
message->thread_parent = g_strdup(thread_parent);
g_object_notify_by_pspec(G_OBJECT(message),
properties[PROP_THREAD_PARENT]);
}
}
void
dodo_message_add_body(DodoMessage *message, const char *language,
const char *content)
{
g_return_if_fail(DODO_IS_MESSAGE(message));
g_return_if_fail(content != NULL);
dodo_message_add_element_with_language(message, message->bodies,
message->bodies_order, language,
content);
}
void
dodo_message_add_subject(DodoMessage *message, const char *language,
const char *content)
{
g_return_if_fail(DODO_IS_MESSAGE(message));
g_return_if_fail(content != NULL);
dodo_message_add_element_with_language(message, message->subjects,
message->subjects_order, language,
content);
}