xmpedit

GTK+ editor for XMP metadata embedded in images
git clone https://code.djc.id.au/git/xmpedit/
commit 0f970b261f52d6c4096fc7351e3af3e86ea90b50
parent 7afeb3d2b9359d5b0f7c3cf5001b3e3d3c24c420
Author: Dan Callaghan <djc@djc.id.au>
Date:   Wed,  1 Sep 2010 21:58:01 +1000

first steps towards RDF writer (full of sucky error checking)

Diffstat:
Mbuild | 16++++++++++------
Msrc/RDF.vala | 11+++++++++++
Asrc/RDF_Writer.vala | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/genx-glib.c | 29+++++++++++++++++++++++++++++
Avapi/genx.vapi | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 320 insertions(+), 6 deletions(-)
diff --git a/build b/build
@@ -19,18 +19,22 @@ def files_under(path):
         result.extend(os.path.join(dirpath, filename) for filename in filenames)
     return result
 
-def compile_vala(sources, target, pkgs=[], defs=[]):
-    invoke(['valac', '-g', '--save-temps', '-d', 'target', '-o', target] +
+def compile_vala(sources, target, pkgs=[], defs=[], vapidirs=[]):
+    invoke(['valac', '--save-temps', '-d', 'target', '-o', target] +
+           ['--Xcc=-Ilib/genx'] + # XXX generalise
            ['--pkg=%s' % pkg for pkg in pkgs] +
            ['--define=%s' % define for define in defs] +
+           ['--vapidir=%s' % vapidir for vapidir in vapidirs] +
            sources)
 
 def main():
-    pkgs = ['gtk+-2.0', 'gee-1.0', 'gexiv2', 'libxml-2.0', 'libsoup-2.4']
-    main_sources = [f for f in files_under('src') if f.endswith('.vala')]
-    compile_vala(sources=main_sources, target='xmpedit', pkgs=pkgs)
+    pkgs = ['gtk+-2.0', 'gee-1.0', 'gexiv2', 'libxml-2.0', 'libsoup-2.4', 'genx']
+    vapidirs = ['vapi']
+    main_sources = [f for f in files_under('src') if f.endswith('.vala') or f.endswith('.c')]
+    lib_sources = [f for f in files_under('lib') if f.endswith('.c')]
+    compile_vala(sources=main_sources + lib_sources, target='xmpedit', pkgs=pkgs, vapidirs=vapidirs, defs=['DEBUG'])
 
-    compile_vala(sources=main_sources, target='xmpedit_test', pkgs=pkgs, defs=['TEST'])
+    compile_vala(sources=main_sources + lib_sources, target='xmpedit_test', pkgs=pkgs, vapidirs=vapidirs, defs=['DEBUG', 'TEST'])
     invoke(['gtester', '--verbose', 'target/xmpedit_test'])
 
 if __name__ == '__main__':
diff --git a/src/RDF.vala b/src/RDF.vala
@@ -131,6 +131,16 @@ public class Graph : Object {
         Parser(this, base_uri).parse(xml);
     }
     
+    /**
+     * N.B. not a general RDF writer,
+     * only includes statements reachable from start_node!
+     */
+    public string to_xml(URIRef start_node) {
+        var writer = new Writer(this);
+        writer.write(start_node);
+        return writer.get_xml();
+    }
+    
     public void insert(Statement statement) {
         if (!has_matching_statement(statement.subject, statement.predicate, statement.object))
             statements.add(statement);
@@ -207,6 +217,7 @@ private const string XML_NS = "http://www.w3.org/XML/1998/namespace";
 
 public void register_tests() {
     register_parser_tests();
+    register_writer_tests();
 }
 
 #endif
diff --git a/src/RDF_Writer.vala b/src/RDF_Writer.vala
@@ -0,0 +1,151 @@
+
+namespace RDF {
+
+private class Writer {
+
+    private Graph graph;
+    private Genx.Writer genx_writer = new Genx.Writer();
+    private StringBuilder output = new StringBuilder();
+    
+    public Writer(Graph graph) {
+        this.graph = graph;
+    }
+    
+    public string get_xml() {
+        return output.str;
+    }
+    
+    /**
+     * N.B. not a general RDF writer,
+     * only includes statements reachable from start_node!
+     */
+    public void write(URIRef start_node) {
+        Genx.Status status;
+        status = genx_writer.start_doc(output);
+        assert(status == Genx.Status.SUCCESS);
+        unowned Genx.Namespace rdf_ns = genx_writer.declare_namespace(RDF_NS, "rdf", out status);
+        assert(status == Genx.Status.SUCCESS);
+        unowned Genx.Element rdf_el = genx_writer.declare_element(rdf_ns, "RDF", out status);
+        assert(status == Genx.Status.SUCCESS);
+        
+        rdf_description_el = genx_writer.declare_element(rdf_ns, "Description", out status);
+        assert(status == Genx.Status.SUCCESS);
+        rdf_about_attr = genx_writer.declare_attribute(rdf_ns, "about", out status);
+        assert(status == Genx.Status.SUCCESS);
+        unowned Genx.Namespace xml_ns = genx_writer.declare_namespace(XML_NS, "xml", out status);
+        assert(status == Genx.Status.SUCCESS);
+        xml_lang_attr = genx_writer.declare_attribute(xml_ns, "lang", out status);
+        assert(status == Genx.Status.SUCCESS);
+        
+        status = rdf_el.start();
+        assert(status == Genx.Status.SUCCESS);
+        write_resource(start_node);
+        status = genx_writer.end_element();
+        assert(status == Genx.Status.SUCCESS);
+        status = genx_writer.end_doc();
+        assert(status == Genx.Status.SUCCESS);
+    }
+    
+    /* for efficiency */
+    private unowned Genx.Element rdf_description_el;
+    private unowned Genx.Attribute rdf_about_attr;
+    private unowned Genx.Attribute xml_lang_attr;
+    
+    private void write_resource(URIRef node) {
+        Genx.Status status;
+        status = rdf_description_el.start();
+        assert(status == Genx.Status.SUCCESS);
+        status = rdf_about_attr.add(node.uri);
+        assert(status == Genx.Status.SUCCESS);
+        foreach (var statement in graph.find_matching_statements(node, null, null)) {
+            write_property(statement.predicate, statement.object);
+        }
+        status = genx_writer.end_element();
+        assert(status == Genx.Status.SUCCESS);
+    }
+    
+    private void write_property(URIRef predicate, Node object) {
+        unowned Genx.Namespace ns;
+        string local_name;
+        split_uri(predicate.uri, out ns, out local_name);
+        Genx.Status status;
+        unowned Genx.Element property_el; // XXX reuse
+        property_el = genx_writer.declare_element(ns, local_name, out status);
+        assert(status == Genx.Status.SUCCESS);
+        status = property_el.start();
+        assert(status == Genx.Status.SUCCESS);
+        if (object is PlainLiteral) {
+            PlainLiteral literal = (PlainLiteral) object;
+            if (literal.lang != null) {
+                status = xml_lang_attr.add(literal.lang);
+                assert(status == Genx.Status.SUCCESS);
+            }
+            status = genx_writer.add_text(literal.lexical_value);
+            assert(status == Genx.Status.SUCCESS);
+        } else {
+            assert_not_reached();
+        }
+        status = genx_writer.end_element();
+        assert(status == Genx.Status.SUCCESS);
+    }
+    
+    private void split_uri(string uri,
+            out unowned Genx.Namespace ns, out string local_name) {
+        var last_slash = uri.pointer_to_offset(uri.rchr(-1, '/')); // XXX crude
+        var ns_uri = uri.substring(0, last_slash + 1);
+        var ns_prefix = is_wellknown_ns(ns_uri);
+        Genx.Status status;
+        ns = genx_writer.declare_namespace(ns_uri, ns_prefix, out status);
+        assert(status == Genx.Status.SUCCESS);
+        local_name = uri.substring(last_slash + 1);
+    }
+    
+    private string? is_wellknown_ns(string ns) {
+        for (int i = 0; i < WELL_KNOWN_NAMESPACES.length[0]; i ++)
+            if (WELL_KNOWN_NAMESPACES[i,1] == ns)
+                return WELL_KNOWN_NAMESPACES[i,0];
+        return null;
+    }
+    
+    private static string[,] WELL_KNOWN_NAMESPACES = {
+        { "foaf", "http://xmlns.com/foaf/0.1/" }
+    };
+
+}
+
+#if TEST
+
+namespace WriterTests {
+
+public void test_literal_property() {
+    var person = new URIRef("http://example.com/");
+    var foaf_name = new URIRef("http://xmlns.com/foaf/0.1/name");
+    var person_name = new PlainLiteral.with_lang("Person", "en");
+    
+    var g = new Graph();
+    g.insert(new Statement(person, foaf_name, person_name));
+    assert_equal(
+        """<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">""" +
+            """<rdf:Description rdf:about="http://example.com/">""" +
+                """<foaf:name xmlns:foaf="http://xmlns.com/foaf/0.1/" xml:lang="en">Person</foaf:name>""" +
+            """</rdf:Description>""" +
+        """</rdf:RDF>""",
+        g.to_xml(person));
+}
+
+private void assert_equal(string expected, string actual) {
+    if (actual != expected) {
+        stderr.puts(@"\nActual: [$(actual)]\nExpected: [$(expected)]\n");
+        assert(actual == expected);
+    }
+}
+
+}
+
+public void register_writer_tests() {
+    Test.add_func("/rdf/writer/test_literal_property", WriterTests.test_literal_property);
+}
+
+#endif
+
+}
diff --git a/src/genx-glib.c b/src/genx-glib.c
@@ -0,0 +1,29 @@
+
+#include <glib.h>
+#include <genx.h>
+
+static genxStatus _genx_send_gstring(void *userData, constUtf8 s) {
+    g_string_append((GString *)userData, (const gchar *)s);
+    return GENX_SUCCESS;
+}
+
+static genxStatus _genx_sendBounded_gstring(void *userData, constUtf8 start, constUtf8 end) {
+    g_string_append_len((GString *)userData, (const gchar *)start, (gssize) (end - start));
+    return GENX_SUCCESS;
+}
+
+static genxStatus _genx_flush_gstring(void *userData) {
+    return GENX_SUCCESS;
+}
+
+static genxSender _genx_sender_gstring = {
+    _genx_send_gstring,
+    _genx_sendBounded_gstring,
+    _genx_flush_gstring
+};
+
+genxStatus _genx_start_doc_gstring(genxWriter w, GString *gs) {
+    // XXX bad, we should hold a proper ref to gs here
+    genxSetUserData(w, (void *)gs);
+    genxStartDocSender(w, &_genx_sender_gstring);
+}
diff --git a/vapi/genx.vapi b/vapi/genx.vapi
@@ -0,0 +1,119 @@
+[CCode (cheader_filename = "genx.h")]
+namespace Genx {
+
+    [CCode (cname = "genxStatus", cprefix = "GENX_")]
+    public enum Status {
+        SUCCESS,
+        BAD_UTF8,
+        NON_XML_CHARACTER,
+        BAD_NAME,
+        ALLOC_FAILED,
+        BAD_NAMESPACE_NAME,
+        INTERNAL_ERROR,
+        DUPLICATE_PREFIX,
+        SEQUENCE_ERROR,
+        NO_START_TAG,
+        IO_ERROR,
+        MISSING_VALUE,
+        MALFORMED_COMMENT,
+        XML_PI_TARGET,
+        MALFORMED_PI,
+        DUPLICATE_ATTRIBUTE,
+        ATTRIBUTE_IN_DEFAULT_NAMESPACE,
+        DUPLICATE_NAMESPACE,
+        BAD_DEFAULT_DECLARATION
+    }
+    
+    [Compact]
+    [CCode (cname = "struct genxWriter_rec", free_function = "genxDispose")]
+    public class Writer {
+    
+        [CCode (has_target = 0)]
+        public delegate void* AllocCallback(void* user_data, int bytes);
+        [CCode (has_target = 0)]
+        public delegate void DeallocCallback(void* user_data, void* data);
+        
+        [CCode (cname = "_genx_default_alloc")]
+        private static void* default_alloc(void* user_data, int bytes) {
+            return GLib.malloc(bytes);
+        }
+        [CCode (cname = "_genx_default_dealloc")]
+        private static void default_dealloc(void* user_data, void* data) {
+            GLib.free(data);
+        }
+        
+        [CCode (cname = "genxNew")]
+        public Writer(
+                AllocCallback alloc = default_alloc,
+                DeallocCallback dealloc = default_dealloc,
+                void* user_data = null);
+        
+        [CCode (cname = "genxDeclareNamespace")]
+        public unowned Namespace declare_namespace(string uri, string prefix, out Status status);
+        
+        [CCode (cname = "genxDeclareElement")]
+        public unowned Element declare_element(Namespace ns, string type, out Status status);
+        
+        [CCode (cname = "genxDeclareAttribute")]
+        public unowned Attribute declare_attribute(Namespace ns, string name, out Status status);
+        
+        [CCode (cname = "_genx_start_doc_gstring")]
+        public Status start_doc(GLib.StringBuilder output);
+        
+        [CCode (cname = "genxEndDocument")]
+        public Status end_doc();
+        
+        [CCode (cname = "genxComment")]
+        public Status comment(string text);
+        
+        [CCode (cname = "genxPI")]
+        public Status pi(string target, string text);
+        
+        [CCode (cname = "genxStartElementLiteral")]
+        public Status start_element_literal(string xmlns, string type);
+        
+        [CCode (cname = "genxAddAttributeLiteral")]
+        public Status add_attribute_literal(string xmlns, string name, string value);
+        
+        [CCode (cname = "genxUnsetDefaultNamespace")]
+        public Status unset_default_namespace();
+        
+        [CCode (cname = "genxEndElement")]
+        public Status end_element();
+        
+        [CCode (cname = "genxAddText")]
+        public Status add_text(string text);
+        
+        [CCode (cname = "genxAddCharacter")]
+        public Status add_character(int character);
+    
+    }
+    
+    [Compact]
+    [CCode (cname = "struct genxNamespace_rec", free_function = "NAMESPACE_HAS_NO_PUBLIC_FREE")]
+    public class Namespace {
+    
+        [CCode (cname = "genxAddNamespace")]
+        public Status add(string? prefix = null);
+    
+    }
+    
+    [Compact]
+    [CCode (cname = "struct genxElement_rec", free_function = "ELEMENT_HAS_NO_PUBLIC_FREE")]
+    public class Element {
+    
+        [CCode (cname = "genxStartElement")]
+        public Status start();
+    
+    }
+        
+    [Compact]
+    [CCode (cname = "struct genxAttribute_rec", free_function = "ATTRIBUTE_HAS_NO_PUBLIC_FREE")]
+    public class Attribute {
+    
+        [CCode (cname = "genxAddAttribute")]
+        public Status add(string value);
+    
+    }
+
+}