/*
 * Copyright (C) 2012 Intel Corporation
 *
 * Authors: Krzesimir Nowak <krnowak@openismus.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 */

#include <config.h>

#include <stdarg.h>
#include <libxml/parserInternals.h>

#include "fragment-util.h"
#include "xml-util.h"

typedef struct {
        gchar *node_name;
        gchar *attribute_name;
} NodeDiff;

static NodeDiff *
node_diff_new (const xmlChar *node_name,
               const xmlChar *attribute_name)
{
        NodeDiff *diff = g_slice_new (NodeDiff);

        diff->node_name = g_strdup ((gchar *) node_name);
        diff->attribute_name = g_strdup ((gchar *) attribute_name);

        return diff;
}

static void
node_diff_free (NodeDiff *diff)
{
        if (diff != NULL) {
                g_free (diff->node_name);
                g_free (diff->attribute_name);
                g_slice_free (NodeDiff, diff);
        }
}

static GList *
get_toplevel_changes (xmlNodePtr current_node,
                      xmlNodePtr new_node)
{
        xmlAttrPtr attribute;
        GHashTable *current_attributes = av_xml_util_get_attributes_map
                                        (current_node);
        GList *changes = NULL;
        const xmlChar *name = new_node->name;

        /* compare attributes */
        for (attribute = new_node->properties;
             attribute != NULL;
             attribute = attribute->next) {
                const xmlChar *value = NULL;
                const xmlChar *key = attribute->name;
                gboolean differs = FALSE;

                if (g_hash_table_lookup_extended (current_attributes,
                                                  key,
                                                  NULL,
                                                  (gpointer *) &value)) {
                        if (xmlStrcmp (value, attribute->children->content))
                                differs = TRUE;
                        g_hash_table_remove (current_attributes, key);
                } else
                        differs = TRUE;
                if (differs)
                        changes = g_list_prepend (changes,
                                                  node_diff_new (name,
                                                                 key));
        }

        if (g_hash_table_size (current_attributes) > 0) {
                GHashTableIter iter;
                xmlChar *key = NULL;

                g_hash_table_iter_init (&iter, current_attributes);
                while (g_hash_table_iter_next (&iter,
                                               (gpointer *) &key,
                                               NULL))
                        changes = g_list_prepend (changes, node_diff_new (name,
                                                                          key));
        }

        g_hash_table_unref (current_attributes);

        return changes;
}

static gboolean
is_read_only (const gchar *changed_element,
              const gchar *changed_attribute)
{
        static GHashTable *readonly_props = NULL;
        static gsize readonly_props_loaded = 0;

        if (g_once_init_enter (&readonly_props_loaded)) {
                readonly_props = g_hash_table_new (g_str_hash,
                                                   g_str_equal);

                g_hash_table_add (readonly_props, (gpointer)  "@id");
                g_hash_table_add (readonly_props, (gpointer)  "@parentID");
                g_hash_table_add (readonly_props, (gpointer)  "@refID");
                g_hash_table_add (readonly_props, (gpointer)  "@restricted");
                g_hash_table_add (readonly_props, (gpointer)  "@searchable");
                g_hash_table_add (readonly_props, (gpointer)  "@childCount");
                g_hash_table_add (readonly_props, (gpointer)  "searchClass");
                g_hash_table_add (readonly_props, (gpointer)  "searchClass@name");
                g_hash_table_add (readonly_props, (gpointer)  "searchClass@includeDerived");
                g_hash_table_add (readonly_props, (gpointer)  "createClass");
                g_hash_table_add (readonly_props, (gpointer)  "createClass@name");
                g_hash_table_add (readonly_props, (gpointer)  "createClass@includeDerived");
                g_hash_table_add (readonly_props, (gpointer)  "writeStatus");
                g_hash_table_add (readonly_props, (gpointer)  "res@importUri");
                g_hash_table_add (readonly_props, (gpointer)  "storageTotal");
                g_hash_table_add (readonly_props, (gpointer)  "storageUsed");
                g_hash_table_add (readonly_props, (gpointer)  "storageFree");
                g_hash_table_add (readonly_props, (gpointer)  "storageMaxPartition");
                g_hash_table_add (readonly_props, (gpointer)  "storageMedium");
                g_hash_table_add (readonly_props, (gpointer)  "playbackCount");
                g_hash_table_add (readonly_props, (gpointer)  "srsRecordScheduleID");
                g_hash_table_add (readonly_props, (gpointer)  "srsRecordTaskID");
                g_hash_table_add (readonly_props, (gpointer)  "price");
                g_hash_table_add (readonly_props, (gpointer)  "price@currency");
                g_hash_table_add (readonly_props, (gpointer)  "payPerView");
                g_hash_table_add (readonly_props, (gpointer)  "dateTimeRange");
                g_hash_table_add (readonly_props, (gpointer)
                                "dateTimeRange@daylightSaving");
                g_hash_table_add (readonly_props, (gpointer)  "signalStrength");
                g_hash_table_add (readonly_props, (gpointer)  "signalLocked");
                g_hash_table_add (readonly_props, (gpointer)  "tuned");
                g_hash_table_add (readonly_props, (gpointer)  "containerUpdateID");
                g_hash_table_add (readonly_props, (gpointer)  "objectUpdateID");
                g_hash_table_add (readonly_props, (gpointer)  "totalDeletedChildCount");
                g_hash_table_add (readonly_props, (gpointer)  "res@updateCount");
                g_once_init_leave (&readonly_props_loaded, 1);
        }
        if (changed_element != NULL) {
                if (changed_attribute != NULL) {
                        gchar *test_prop = g_strdup_printf ("%s@%s",
                                                            changed_element,
                                                            changed_attribute);
                        gboolean result = g_hash_table_contains (readonly_props,
                                                               test_prop);

                        g_free (test_prop);
                        if (result)
                                return TRUE;
                        test_prop = g_strdup_printf ("@%s", changed_attribute);
                        result = g_hash_table_contains (readonly_props,
                                                      test_prop);
                        g_free (test_prop);
                        if (result)
                                return TRUE;
                }

                return g_hash_table_contains (readonly_props, changed_element);
        }

        return FALSE;
}

static gboolean
is_any_change_read_only (xmlNodePtr current_node,
                         xmlNodePtr new_node)
{
        GList *changes = get_toplevel_changes (current_node, new_node);
        GList *iter;
        gboolean read_only = FALSE;

        for (iter = changes; iter != NULL; iter = iter->next) {
                NodeDiff *diff = (NodeDiff *) iter->data;

                if (is_read_only (diff->node_name,
                                  diff->attribute_name)) {
                        read_only = TRUE;

                        break;
                }
        }

        if (changes != NULL)
                g_list_free_full (changes, (GDestroyNotify) node_diff_free);
        return read_only;
}

static GUPnPDIDLLiteFragmentResult
apply_temporary_modification (DocNode    *modified,
                              xmlNodePtr  current_node,
                              xmlNodePtr  new_node,
                              XSDData    *xsd_data)
{
        xmlNodePtr mod_cur_node = av_xml_util_find_node (modified->node,
                                                         current_node);
        xmlNodePtr new_node_copy = av_xml_util_copy_node (new_node);

        if (mod_cur_node == NULL) {
                return GUPNP_DIDL_LITE_FRAGMENT_RESULT_UNKNOWN_ERROR;
        }

        xmlUnlinkNode (new_node_copy);
        mod_cur_node = xmlReplaceNode (mod_cur_node, new_node_copy);
        xmlUnlinkNode (mod_cur_node);
        xmlFreeNode (mod_cur_node);

        if (!xsd_data_validate_doc (xsd_data, modified->doc))
                return GUPNP_DIDL_LITE_FRAGMENT_RESULT_NEW_INVALID;

        return GUPNP_DIDL_LITE_FRAGMENT_RESULT_OK;
}

static GUPnPDIDLLiteFragmentResult
apply_temporary_addition (DocNode    *modified,
                          xmlNodePtr  sibling,
                          xmlNodePtr  new_node,
                          XSDData    *xsd_data)
{
        xmlNodePtr mod_sibling;
        xmlNodePtr new_node_copy = av_xml_util_copy_node (new_node);

        if (sibling->doc == modified->doc)
                mod_sibling = sibling;
        else
                mod_sibling = av_xml_util_find_node (modified->node, sibling);

        if (mod_sibling == NULL)
                return GUPNP_DIDL_LITE_FRAGMENT_RESULT_UNKNOWN_ERROR;

        xmlUnlinkNode (new_node_copy);

        if (xmlAddNextSibling (mod_sibling, new_node_copy) == NULL) {
                xmlFreeNode (new_node_copy);

                return GUPNP_DIDL_LITE_FRAGMENT_RESULT_UNKNOWN_ERROR;
        }

        if (!xsd_data_validate_doc (xsd_data, modified->doc))
                return GUPNP_DIDL_LITE_FRAGMENT_RESULT_NEW_INVALID;

        return GUPNP_DIDL_LITE_FRAGMENT_RESULT_OK;
}

static GUPnPDIDLLiteFragmentResult
apply_temporary_removal (DocNode    *modified,
                         xmlNodePtr  current_node,
                         XSDData    *xsd_data)
{
        xmlNodePtr mod_cur_node = av_xml_util_find_node (modified->node,
                                                         current_node);

        if (mod_cur_node == NULL)
                return GUPNP_DIDL_LITE_FRAGMENT_RESULT_UNKNOWN_ERROR;

        xmlUnlinkNode (mod_cur_node);
        xmlFreeNode (mod_cur_node);
        if (!xsd_data_validate_doc (xsd_data, modified->doc))
                /* not sure if this is correct */
                return GUPNP_DIDL_LITE_FRAGMENT_RESULT_REQUIRED_TAG;

        return GUPNP_DIDL_LITE_FRAGMENT_RESULT_OK;
}

typedef struct {
        gboolean required;
        GHashTable* required_dep_props; /* string set */
        GHashTable* required_indep_props; /* string to indep prop */
} IndependentProperty;

static void
independent_property_free (IndependentProperty *indep)
{
        if (indep != NULL) {
                g_hash_table_unref (indep->required_dep_props);
                g_hash_table_unref (indep->required_indep_props);
                g_slice_free (IndependentProperty, indep);
        }
}

static IndependentProperty *
independent_property_new (gboolean required)
{
        IndependentProperty *indep = g_slice_new (IndependentProperty);

        indep->required = required;
        indep->required_dep_props = g_hash_table_new_full (g_str_hash,
                                                           g_str_equal,
                                                           g_free,
                                                           NULL);
        indep->required_indep_props = g_hash_table_new_full
                                   (g_str_hash,
                                    g_str_equal,
                                    g_free,
                                    (GDestroyNotify) independent_property_free);

        return indep;
}

static void
insert_indep_prop (GHashTable          *props,
                   const gchar         *name,
                   IndependentProperty *prop)
{
        g_hash_table_insert (props, g_strdup (name), prop);
}

static void
insert_indep_prop_to_indep (IndependentProperty *prop,
                            const gchar         *name,
                            IndependentProperty *req_prop)
{
        insert_indep_prop (prop->required_indep_props, name, req_prop);
}

static void
add_dep_prop (IndependentProperty *indep,
              const gchar         *name)
{
        g_hash_table_add (indep->required_dep_props, g_strdup (name));
}

static IndependentProperty *
create_prop_with_required_dep_props (gboolean     required,
                                     const gchar *dep_prop,
                                     ...)
{
        IndependentProperty *indep = independent_property_new (required);

        if (dep_prop != NULL) {
                va_list var_args;
                const gchar *name = dep_prop;

                va_start (var_args, dep_prop);
                do {
                        add_dep_prop (indep, name);
                        name = va_arg (var_args, gchar *);
                } while (name != NULL);
                va_end (var_args);
        }

        return indep;
}

static IndependentProperty *
create_foreign_metadata_props (void)
{
        IndependentProperty *fm = independent_property_new (FALSE);
        IndependentProperty *other;

        add_dep_prop (fm, "type");

        other = independent_property_new (TRUE);
        insert_indep_prop_to_indep (fm, "fmId", other);

        other = independent_property_new (TRUE);
        insert_indep_prop_to_indep (fm, "fmClass", other);

        other = independent_property_new (TRUE);
        insert_indep_prop_to_indep (fm, "fmProvider", other);

        other = independent_property_new (TRUE);
        add_dep_prop (other, "xmlFlag");
        insert_indep_prop_to_indep (fm, "fmBody", other);

        return fm;
}

static GHashTable *
get_required_properties (void)
{
        static GHashTable *required_props = NULL;
        static gsize required_props_loaded = 0;

        if (g_once_init_enter (&required_props_loaded)) {
                required_props = g_hash_table_new_full
                                   (g_str_hash,
                                    g_str_equal,
                                    g_free,
                                    (GDestroyNotify) independent_property_free);

                insert_indep_prop (required_props,
                                   "",
                                   create_prop_with_required_dep_props
                                        (FALSE,
                                         "id",
                                         "parentID",
                                         "restricted",
                                         NULL));

                insert_indep_prop (required_props,
                                   "title",
                                   independent_property_new (TRUE));
                insert_indep_prop (required_props,
                                   "class",
                                   independent_property_new (TRUE));

                insert_indep_prop (required_props,
                                   "res",
                                   create_prop_with_required_dep_props
                                        (FALSE,
                                         "protocolInfo",
                                         NULL));
                insert_indep_prop (required_props,
                                   "programID",
                                   create_prop_with_required_dep_props
                                        (FALSE,
                                         "type",
                                         NULL));
                insert_indep_prop (required_props,
                                   "seriesID",
                                   create_prop_with_required_dep_props
                                        (FALSE,
                                         "type",
                                         NULL));
                insert_indep_prop (required_props,
                                   "channelID",
                                   create_prop_with_required_dep_props
                                        (FALSE,
                                         "type",
                                         NULL));
                insert_indep_prop (required_props,
                                   "programCode",
                                   create_prop_with_required_dep_props
                                        (FALSE,
                                         "type",
                                         NULL));
                insert_indep_prop (required_props,
                                   "channelGroupName",
                                   create_prop_with_required_dep_props
                                        (FALSE,
                                         "id",
                                         NULL));
                insert_indep_prop (required_props,
                                   "price",
                                   create_prop_with_required_dep_props
                                        (FALSE,
                                         "currency",
                                         NULL));
                insert_indep_prop (required_props,
                                   "desc",
                                   create_prop_with_required_dep_props
                                        (FALSE,
                                         "nameSpace",
                                         NULL));
                insert_indep_prop (required_props,
                                   "deviceUDN",
                                   create_prop_with_required_dep_props
                                        (FALSE,
                                         "serviceType",
                                         "serviceId",
                                         NULL));
                insert_indep_prop (required_props,
                                   "stateVariableCollection",
                                   create_prop_with_required_dep_props
                                        (FALSE,
                                         "serviceName",
                                         "rcsInstanceType",
                                         NULL));
                insert_indep_prop (required_props,
                                   "foreignMetadata",
                                   create_foreign_metadata_props ());
                g_once_init_leave (&required_props_loaded, 1);
        }

        return required_props;
}

static gboolean
is_required (const xmlChar *changed_element,
             const xmlChar *changed_attribute)
{
        GHashTable *required_props = get_required_properties ();

        if (changed_element != NULL) {
                IndependentProperty *toplevel_prop = g_hash_table_lookup
                                        (required_props,
                                         "");
                IndependentProperty *this_prop = g_hash_table_lookup
                                        (required_props,
                                         (gpointer) changed_element);

                if (changed_attribute != NULL) {
                        if (g_hash_table_contains
                                        (toplevel_prop->required_dep_props,
                                         changed_attribute))
                                return TRUE;
                        if (g_hash_table_contains (this_prop->required_dep_props,
                                                 changed_attribute))
                                return TRUE;
                }
                if (g_hash_table_contains (toplevel_prop->required_indep_props,
                                         changed_element))
                                return TRUE;
                /* TODO: check if changed element is not a required
                 * property of its parent element. That needs some
                 * additions in IndepependentProperty.
                 */
        }

        return FALSE;
}

static GUPnPDIDLLiteFragmentResult
new_doc_is_valid_modification (DocNode   *modified,
                               xmlDocPtr  current_doc,
                               xmlDocPtr  new_doc,
                               XSDData   *xsd_data)
{
        xmlNodePtr current_node = current_doc->children->children;
        xmlNodePtr new_node = new_doc->children->children;
        xmlNodePtr last_sibling = NULL;

        while (current_node != NULL && new_node != NULL) {
                GUPnPDIDLLiteFragmentResult result;
                xmlNodePtr temp_current_node = current_node;
                xmlNodePtr temp_new_node = new_node;

                last_sibling = new_node;
                /* We can't put this line into for instruction,
                 * because new_node could be unlinked from its
                 * document and put into another one in
                 * apply_temporary_modification. We have to get its
                 * sibling before that could happen.
                 */
                new_node = new_node->next;
                current_node = current_node->next;
                if (av_xml_util_node_deep_equal (temp_current_node,
                                                 temp_new_node))
                        /* This is just a context, skip the checks. */
                        continue;
                if (xmlStrcmp (temp_current_node->name, temp_new_node->name))
                        return GUPNP_DIDL_LITE_FRAGMENT_RESULT_NEW_INVALID;
                if (is_any_change_read_only (temp_current_node, temp_new_node))
                        return GUPNP_DIDL_LITE_FRAGMENT_RESULT_READONLY_TAG;
                result = apply_temporary_modification (modified,
                                                       temp_current_node,
                                                       temp_new_node,
                                                       xsd_data);
                if (result != GUPNP_DIDL_LITE_FRAGMENT_RESULT_OK)
                        return result;
        }
        if (last_sibling == NULL) {
                if (modified->node->children != NULL)
                        last_sibling = modified->node->last;
                else
                        /* We expect that modified object has some
                         * required tags like <upnp:class> or
                         * <dc:title>.
                         */
                        return GUPNP_DIDL_LITE_FRAGMENT_RESULT_UNKNOWN_ERROR;
        }
        /* If there are some more nodes in current fragment then it
         * means they are going to be removed. Check against required
         * or read-only tag removal.
         */
        while (current_node != NULL) {
                GUPnPDIDLLiteFragmentResult result;
                xmlNodePtr temp_node = current_node;

                current_node = current_node->next;
                /* TODO: should we check if there are some readonly
                 * attributes when we remove whole element?
                 */
                if (is_read_only ((gchar *) temp_node->name, NULL))
                        return GUPNP_DIDL_LITE_FRAGMENT_RESULT_READONLY_TAG;
                /* We don't check for required attributes or
                 * subelements, because most of them are required only
                 * when the element exists. And we are removing this
                 * one.
                 */
                if (is_required (temp_node->name, NULL))
                        return GUPNP_DIDL_LITE_FRAGMENT_RESULT_REQUIRED_TAG;
                result = apply_temporary_removal (modified,
                                                  temp_node,
                                                  xsd_data);

                if (result != GUPNP_DIDL_LITE_FRAGMENT_RESULT_OK)
                        return result;
        }
        /* If there are some more nodes in new fragment then it means
         * they are going to be added. Check against read-only tags
         * addition and general sanity check.
         */
        while (new_node != NULL) {
                GUPnPDIDLLiteFragmentResult result;
                xmlNodePtr temp_node;

                if (is_read_only ((gchar *) new_node->name, NULL))
                        return GUPNP_DIDL_LITE_FRAGMENT_RESULT_READONLY_TAG;
                /* TODO: We probably should check if newly added node
                 * has all required properties. Maybe XSD check could
                 * do that for us.
                 */
                temp_node = new_node;
                new_node = new_node->next;
                result = apply_temporary_addition (modified,
                                                   last_sibling,
                                                   temp_node,
                                                   xsd_data);
                if (result != GUPNP_DIDL_LITE_FRAGMENT_RESULT_OK)
                        return result;
        }

        return GUPNP_DIDL_LITE_FRAGMENT_RESULT_OK;
}

static gchar *
fix_fragment (const gchar *fragment)
{
        return g_strdup_printf
                    ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
                     "<DIDLLiteFragment\n"
                     "xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n"
                     "xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\"\n"
                     "xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\"\n"
                     "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
                     ">%s</DIDLLiteFragment>\n",
                     fragment);
}

static gboolean
is_current_doc_part_of_original_doc (DocNode   *original,
                                     xmlDocPtr  current_doc)
{
        xmlNodePtr current_node = current_doc->children->children;
        xmlNodePtr this_node;

        /* No current node means that we want to add new elements to
           the document. */
        if (current_node == NULL)
                return TRUE;

        this_node = av_xml_util_find_node (original->node, current_node);

        if (this_node == NULL)
                return FALSE;

        for (current_node = current_node->next, this_node = this_node->next;
             current_node != NULL && this_node != NULL;
             current_node = current_node->next, this_node = this_node->next)
                if (!av_xml_util_node_deep_equal (current_node, this_node))
                        return FALSE;

        return TRUE;
}

GUPnPDIDLLiteFragmentResult
fragment_util_check_fragments (DocNode     *original,
                               DocNode     *modified,
                               const gchar *current_fragment,
                               const gchar *new_fragment,
                               XSDData     *xsd_data)
{
        gchar *fixed_current_fragment = fix_fragment (current_fragment);
        gchar *fixed_new_fragment = fix_fragment (new_fragment);
        xmlDocPtr current_doc = xmlReadDoc (BAD_CAST (fixed_current_fragment),
                                            NULL,
                                            NULL,
                                            XML_PARSE_NONET);
        xmlDocPtr new_doc = xmlReadDoc (BAD_CAST (fixed_new_fragment),
                                        NULL,
                                        NULL,
                                        XML_PARSE_NONET);
        GUPnPDIDLLiteFragmentResult result;

        if (current_doc == NULL || current_doc->children == NULL) {
                result = GUPNP_DIDL_LITE_FRAGMENT_RESULT_CURRENT_BAD_XML;

                goto out;
        }

        if (new_doc == NULL || new_doc->children == NULL) {
                result = GUPNP_DIDL_LITE_FRAGMENT_RESULT_NEW_BAD_XML;

                goto out;
        }

        /* Assuming current_doc->children is non-NULL. */
        if (current_doc->children->children != NULL) {
            /* If the child element is title or class,
             * it must not be set to empty or removed.
             */
            if (g_strrstr ((const char *) current_doc->children->children->name,
                           "title") != NULL ||
                g_strrstr ((const char *) current_doc->children->children->name,
                           "class") != NULL) {
                /* If the new tag has no corresponding title or class element */
                if (new_doc->children->children == NULL) {
                    result = GUPNP_DIDL_LITE_FRAGMENT_RESULT_REQUIRED_TAG;

                    goto out;
                }
                /* If the new tag has an empty value for title or class */
                if (new_doc->children->children->children == NULL) {
                    result = GUPNP_DIDL_LITE_FRAGMENT_RESULT_REQUIRED_TAG;

                    goto out;
                }
            }
        }

        if (!is_current_doc_part_of_original_doc (original, current_doc)) {
                result = GUPNP_DIDL_LITE_FRAGMENT_RESULT_CURRENT_INVALID;

                goto out;
        }

        result = new_doc_is_valid_modification (modified,
                                                current_doc,
                                                new_doc,
                                                xsd_data);

 out:
        if (new_doc != NULL)
                xmlFreeDoc (new_doc);
        if (current_doc != NULL)
                xmlFreeDoc (current_doc);
        g_free (fixed_new_fragment);
        g_free (fixed_current_fragment);

        return result;
}

static const gchar *
get_data_dir (void)
{
        const gchar *datadir = g_getenv ("GUPNP_AV_DATADIR");

        if (datadir == NULL)
                /* that's a macro defined by -DDATADIR=foo */
                datadir = DATADIR;

        return datadir;
}

static gchar *
get_xsd_path (const gchar *path)
{
        return g_strdup_printf ("%s" G_DIR_SEPARATOR_S "%s",
                                get_data_dir (),
                                path);
}

static xmlParserInputPtr
our_own_loader (const char       *url,
                const char       *id,
                xmlParserCtxtPtr  context)
{
        gchar *basename;
        gchar *path;
        xmlParserInputPtr input;

        g_debug ("URL: %s, ID: %s.", url, id);

        basename = g_path_get_basename (url);
        path = get_xsd_path (basename);
        g_debug ("BASENAME: %s, PATH: %s", basename, path);
        input = xmlNewInputFromFile (context, path);

        g_free (basename);
        g_free (path);

        return input;
}

XSDData *
fragment_util_get_didl_lite_xsd_data (void)
{
        gchar *path = get_xsd_path ("didl-lite-v2.xsd");
        xmlExternalEntityLoader original_loader = xmlGetExternalEntityLoader ();
        XSDData *xsd_data;

        xmlSetExternalEntityLoader (our_own_loader);
        xsd_data = xsd_data_new (path);
        xmlSetExternalEntityLoader (original_loader);

        g_free (path);

        return xsd_data;
}

gboolean
fragment_util_apply_modification (xmlNodePtr *node_ptr,
                                  DocNode    *modified)
{
        xmlNodePtr node_copy;
        xmlNodePtr old;

        if (node_ptr == NULL || *node_ptr == NULL)
                return FALSE;

        node_copy = av_xml_util_copy_node (modified->node);

        if (node_copy == NULL)
                return FALSE;

        old = xmlReplaceNode (*node_ptr, node_copy);

        if (old == NULL)
                return FALSE;

        *node_ptr = node_copy;
        xmlFreeNode (old);

        return TRUE;
}
