/** * @file * Auxiliary class to handle "session" XML files (augmented Folker). * It is a GObject that must be freed using \e g_object_unref. */ /* * Copyright (C) 2012-2013 Otto-von-Guericke-Universität Magdeburg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #include #include #include "cclosure-marshallers.h" #include "experiment-reader.h" static void experiment_reader_class_init(ExperimentReaderClass *klass); static void experiment_reader_init(ExperimentReader *klass); static void experiment_reader_finalize(GObject *gobject); static gint64 get_timepoint_by_ref(xmlDoc *doc, xmlChar *ref); static xmlNode *get_first_element(xmlNode *children, const gchar *name); static xmlNode *get_last_element(xmlNode *children, const gchar *name); static GClosure *experiment_reader_topic_callback_new(ExperimentReaderTopicCallback, gpointer); static void experiment_reader_topic_callback_invoke(ExperimentReader *reader, GClosure *closure, const gchar *topic_id, gint64 start_time, gint64 end_time); static gboolean generic_foreach_topic(ExperimentReader *reader, xmlNodeSet *nodes, GClosure *closure); static gint experiment_reader_contrib_cmp(const ExperimentReaderContrib *a, const ExperimentReaderContrib *b); static void insert_contribution(gint64 start_time, gchar *text, GList **list); static inline void process_contribution(xmlDoc *doc, xmlNode *contrib, GList **list); /** @private */ #define XML_CHAR(STR) \ ((const xmlChar *)(STR)) /** @private */ #define EXPERIMENT_READER_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE((obj), EXPERIMENT_TYPE_READER, ExperimentReaderPrivate)) /** @private */ struct _ExperimentReaderPrivate { xmlDoc *doc; }; /** * @private * Will create \e experiment_reader_get_type and set * \e experiment_reader_parent_class */ G_DEFINE_TYPE(ExperimentReader, experiment_reader, G_TYPE_OBJECT); static void experiment_reader_class_init(ExperimentReaderClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS(klass); /* gobject_class->dispose = experiment_reader_dispose; */ gobject_class->finalize = experiment_reader_finalize; g_type_class_add_private(klass, sizeof(ExperimentReaderPrivate)); } static void experiment_reader_init(ExperimentReader *klass) { klass->priv = EXPERIMENT_READER_GET_PRIVATE(klass); klass->priv->doc = NULL; } static void experiment_reader_finalize(GObject *gobject) { ExperimentReader *reader = EXPERIMENT_READER(gobject); if (reader->priv->doc != NULL) xmlFreeDoc(reader->priv->doc); /* Chain up to the parent class */ G_OBJECT_CLASS(experiment_reader_parent_class)->finalize(gobject); } static gint64 get_timepoint_by_ref(xmlDoc *doc, xmlChar *ref) { xmlChar expr[255]; xmlXPathContext *xpathCtx; xmlXPathObject *xpathObj; double value; xpathCtx = xmlXPathNewContext(doc); assert(xpathCtx != NULL); /** @todo precompile XPath expression */ xmlStrPrintf(expr, sizeof(expr), XML_CHAR("/session/timeline/" "timepoint[@timepoint-id = '%s']/" "@absolute-time"), ref); xpathObj = xmlXPathEvalExpression(expr, xpathCtx); assert(xpathObj != NULL); value = xmlXPathCastToNumber(xpathObj); xmlXPathFreeObject(xpathObj); xmlXPathFreeContext(xpathCtx); return (gint64)(value*1000.); } static xmlNode * get_first_element(xmlNode *children, const gchar *name) { for (xmlNode *cur = children; cur != NULL; cur = cur->next) if (cur->type == XML_ELEMENT_NODE && !g_strcmp0((const gchar *)cur->name, name)) return cur; return NULL; } static xmlNode * get_last_element(xmlNode *children, const gchar *name) { xmlNode *ret = NULL; for (xmlNode *cur = children; cur != NULL; cur = cur->next) if (cur->type == XML_ELEMENT_NODE && !g_strcmp0((const gchar *)cur->name, name)) ret = cur; return ret; } static GClosure * experiment_reader_topic_callback_new(ExperimentReaderTopicCallback callback, gpointer data) { GClosure *closure = g_cclosure_new(G_CALLBACK(callback), data, NULL); g_closure_set_marshal(closure, experiment_reader_marshal_VOID__STRING_INT64_INT64); g_closure_ref(closure); g_closure_sink(closure); return closure; } static void experiment_reader_topic_callback_invoke(ExperimentReader *reader, GClosure *closure, const gchar *topic_id, gint64 start_time, gint64 end_time) { GValue params[4]; memset(params, 0, sizeof(params)); g_value_init(params + 0, G_TYPE_OBJECT); g_value_set_object(params + 0, reader); g_value_init(params + 1, G_TYPE_STRING); g_value_set_string(params + 1, topic_id); g_value_init(params + 2, G_TYPE_INT64); g_value_set_int64(params + 2, start_time); g_value_init(params + 3, G_TYPE_INT64); g_value_set_int64(params + 3, end_time); g_closure_invoke(closure, NULL, G_N_ELEMENTS(params), params, NULL); for (gint i = 0; i < G_N_ELEMENTS(params); i++) g_value_unset(params + i); } static gboolean generic_foreach_topic(ExperimentReader *reader, xmlNodeSet *nodes, GClosure *closure) { if (nodes == NULL) return TRUE; for (int i = 0; i < nodes->nodeNr; i++) { xmlNode *cur = nodes->nodeTab[i]; assert(cur != NULL && cur->type == XML_ELEMENT_NODE); xmlNode *first_contrib = get_first_element(cur->children, "contribution"); xmlNode *last_contrib = get_last_element(cur->children, "contribution"); xmlChar *topic_id = xmlGetProp(cur, XML_CHAR("id")); gint64 start_time = -1; gint64 end_time = -1; if (first_contrib != NULL) { xmlChar *contrib_start_ref; contrib_start_ref = xmlGetProp(first_contrib, XML_CHAR("start-reference")); start_time = get_timepoint_by_ref(reader->priv->doc, contrib_start_ref); xmlFree(contrib_start_ref); } if (last_contrib != NULL) { xmlChar *contrib_end_ref; contrib_end_ref = xmlGetProp(last_contrib, XML_CHAR("end-reference")); end_time = get_timepoint_by_ref(reader->priv->doc, contrib_end_ref); xmlFree(contrib_end_ref); } experiment_reader_topic_callback_invoke(reader, closure, (const gchar *)topic_id, start_time, end_time); xmlFree(topic_id); } return FALSE; } static gint experiment_reader_contrib_cmp(const ExperimentReaderContrib *a, const ExperimentReaderContrib *b) { if (a->start_time < b->start_time) return -1; if (a->start_time > b->start_time) return 1; return 0; } static void insert_contribution(gint64 start_time, gchar *text, GList **list) { ExperimentReaderContrib *contrib; if (text == NULL) return; contrib = g_malloc(sizeof(ExperimentReaderContrib) + strlen(text) + 1); contrib->start_time = start_time; g_stpcpy(contrib->text, g_strchomp(text)); *list = g_list_insert_sorted(*list, contrib, (GCompareFunc)experiment_reader_contrib_cmp); } static inline void process_contribution(xmlDoc *doc, xmlNode *contrib, GList **list) { xmlChar *ref; gint64 start_time; gchar *text = NULL; ref = xmlGetProp(contrib, XML_CHAR("start-reference")); start_time = get_timepoint_by_ref(doc, ref); xmlFree(ref); for (xmlNode *cur = contrib->children; cur != NULL; cur = cur->next) { xmlChar *content; gchar *new; switch (cur->type) { case XML_TEXT_NODE: content = xmlNodeGetContent(cur); new = g_strconcat(text != NULL ? text : "", g_strstrip((gchar *)content), " ", NULL); g_free(text); text = new; xmlFree(content); break; case XML_ELEMENT_NODE: if (!xmlStrcmp(cur->name, XML_CHAR("pause"))) { xmlChar *duration; duration = xmlGetProp(cur, XML_CHAR("duration")); if (duration == NULL) break; if (!xmlStrcmp(duration, XML_CHAR("micro")) || !xmlStrcmp(duration, XML_CHAR("short"))) new = g_strconcat(text != NULL ? text : "", "... ", NULL); else if (text == NULL) new = g_strdup("...\n"); else new = g_strconcat(g_strchomp(text), "\n", NULL); g_free(text); text = new; xmlFree(duration); } else if (!xmlStrcmp(cur->name, XML_CHAR("time"))) { insert_contribution(start_time, text, list); g_free(text); text = NULL; ref = xmlGetProp(cur, XML_CHAR("timepoint-reference")); start_time = get_timepoint_by_ref(doc, ref); xmlFree(ref); } break; default: break; } } insert_contribution(start_time, text, list); g_free(text); } /* * API */ /** * @brief Constructs a new ExperimentReader object * * @param filename Filename of XML file to open * @return A new \e ExperimentReader object. Free with \e g_object_unref. */ ExperimentReader * experiment_reader_new(const gchar *filename) { ExperimentReader *reader; reader = EXPERIMENT_READER(g_object_new(EXPERIMENT_TYPE_READER, NULL)); reader->priv->doc = xmlParseFile(filename); if (reader->priv->doc == NULL) { g_object_unref(G_OBJECT(reader)); return NULL; } /** @todo validate against session.dtd */ return reader; } /** * @brief Retrieve list of contributions by speaker * * Returns a newly-allocated doubly-linked list of * \ref ExperimentReaderContrib structures representing all contributions * by a given speaker. Every text fragment with a \e timepoint reference is * considered a contribution. * The list is sorted by the contributions' start times, in ascending order. * * @sa ExperimentReaderContrib * @sa experiment_reader_get_contribution_by_time * @sa experiment_reader_free_contributions * * @param reader \e ExperimentReader instance * @param speaker Full name of the speaker (e.g. "Wizard") * @return Newly allocated list of contributions (must be freed with * \ref experiment_reader_free_contributions) */ GList * experiment_reader_get_contributions_by_speaker(ExperimentReader *reader, const gchar *speaker) { GList *list = NULL; xmlXPathContext *xpathCtx; xmlXPathObject *xpathObj; xmlChar expr[255]; xpathCtx = xmlXPathNewContext(reader->priv->doc); /* Evaluate xpath expression */ xmlStrPrintf(expr, sizeof(expr), XML_CHAR("//contribution[@speaker-reference = " "/session/speakers/speaker[name = '%s']/@speaker-id]"), speaker); xpathObj = xmlXPathEvalExpression(expr, xpathCtx); for (int i = 0; i < xpathObj->nodesetval->nodeNr; i++) { xmlNode *contrib = xpathObj->nodesetval->nodeTab[i]; process_contribution(reader->priv->doc, contrib, &list); } xmlXPathFreeObject(xpathObj); xmlXPathFreeContext(xpathCtx); return list; } /** * @brief Get a contribution by time * * Gets the closest contribution after the specified time or the last one * if there is no contribution after the specified time. * The contribution is returned as a pointer into the contribution list * so that the list may be traversed by the caller. * * @param contribs List of \ref ExperimentReaderContrib structures as returned * by \ref experiment_reader_get_contributions_by_speaker * @param timept Time in milliseconds * @return List of contributions beginning with the desired contribution. * It is a pointer into contribs and must not be freed directly. */ GList * experiment_reader_get_contribution_by_time(GList *contribs, gint64 timept) { for (GList *cur = contribs; cur != NULL; cur = cur->next) { ExperimentReaderContrib *contrib = (ExperimentReaderContrib *)cur->data; if (contrib->start_time > timept || cur->next == NULL) return cur; } return NULL; } /** * @brief Free list of contributions and associated data * * @sa experiment_reader_get_contributions_by_speaker * * @param contribs List of \ref ExperimentReaderContrib structures to free */ void experiment_reader_free_contributions(GList *contribs) { for (GList *cur = contribs; cur != NULL; cur = cur->next) g_free(cur->data); g_list_free(contribs); } /** * Calls \e callback with \e userdata for each \b topic in the \b greeting * section of the experiment. * * @param reader \e ExperimentReader instance * @param callback Function to invoke * @param userdata User data to pass to \e callback */ void experiment_reader_foreach_greeting_topic(ExperimentReader *reader, ExperimentReaderTopicCallback callback, gpointer userdata) { xmlXPathContext *xpathCtx; xmlXPathObject *xpathObj; GClosure *closure; xpathCtx = xmlXPathNewContext(reader->priv->doc); xpathObj = xmlXPathEvalExpression(XML_CHAR("/session/greeting/topic"), xpathCtx); closure = experiment_reader_topic_callback_new(callback, userdata); generic_foreach_topic(reader, xpathObj->nodesetval, closure); g_closure_unref(closure); xmlXPathFreeObject(xpathObj); xmlXPathFreeContext(xpathCtx); } /** * Calls \e callback with \e userdata for each \b topic in the * \b initial-narrative subsection of the \b experiment section of * the experiment. * * @param reader \e ExperimentReader instance * @param callback Function to invoke * @param userdata User data to pass to \e callback */ void experiment_reader_foreach_exp_initial_narrative_topic(reader, callback, userdata) ExperimentReader *reader; ExperimentReaderTopicCallback callback; gpointer userdata; { xmlXPathContext *xpathCtx; xmlXPathObject *xpathObj; GClosure *closure; xpathCtx = xmlXPathNewContext(reader->priv->doc); xpathObj = xmlXPathEvalExpression(XML_CHAR("/session/experiment/" "initial-narrative/topic"), xpathCtx); closure = experiment_reader_topic_callback_new(callback, userdata); generic_foreach_topic(reader, xpathObj->nodesetval, closure); g_closure_unref(closure); xmlXPathFreeObject(xpathObj); xmlXPathFreeContext(xpathCtx); } /** * Calls \e callback with \e userdata for each \b topic in a \b phase of * the \b last-minute subsection of the \b experiment section of * the experiment. * * @param reader \e ExperimentReader instance * @param phase \b Phase section (integer from 1 to 6) * @param callback Function to invoke * @param userdata User data to pass to \e callback */ void experiment_reader_foreach_exp_last_minute_phase_topic(reader, phase, callback, userdata) ExperimentReader *reader; gint phase; ExperimentReaderTopicCallback callback; gpointer userdata; { xmlXPathContext *xpathCtx; xmlXPathObject *xpathObj; GClosure *closure; xmlChar expr[255]; xpathCtx = xmlXPathNewContext(reader->priv->doc); /* Evaluate xpath expression */ xmlStrPrintf(expr, sizeof(expr), XML_CHAR("/session/experiment/last-minute/" "phase[@id = '%d']/topic"), phase); xpathObj = xmlXPathEvalExpression(expr, xpathCtx); closure = experiment_reader_topic_callback_new(callback, userdata); generic_foreach_topic(reader, xpathObj->nodesetval, closure); g_closure_unref(closure); xmlXPathFreeObject(xpathObj); xmlXPathFreeContext(xpathCtx); } /** * Calls \e callback with \e userdata for each \b topic in the \b farewell * section of the experiment. * * @param reader \e ExperimentReader instance * @param callback Function to invoke * @param userdata User data to pass to \e callback */ void experiment_reader_foreach_farewell_topic(ExperimentReader *reader, ExperimentReaderTopicCallback callback, gpointer userdata) { xmlXPathContext *xpathCtx; xmlXPathObject *xpathObj; GClosure *closure; xpathCtx = xmlXPathNewContext(reader->priv->doc); xpathObj = xmlXPathEvalExpression(XML_CHAR("/session/farewell/topic"), xpathCtx); closure = experiment_reader_topic_callback_new(callback, userdata); generic_foreach_topic(reader, xpathObj->nodesetval, closure); g_closure_unref(closure); xmlXPathFreeObject(xpathObj); xmlXPathFreeContext(xpathCtx); }