xmpedit

GTK+ editor for XMP metadata embedded in images
git clone https://code.djc.id.au/git/xmpedit/
commit e2aec90e2a5ae73a54d731c1a8625c4162e2f843
parent f889b953788f8300717cb208392511e35cb84335
Author: Dan Callaghan <djc@djc.id.au>
Date:   Sun,  8 Aug 2010 18:24:44 +1000

the beginnings of an RDF/XML parsing library

Diffstat:
Asrc/RDF.vala | 312+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/xmpedit.vala | 15++++++++++++++-
2 files changed, 326 insertions(+), 1 deletion(-)
diff --git a/src/RDF.vala b/src/RDF.vala
@@ -0,0 +1,312 @@
+
+namespace RDF {
+
+public abstract class Node : Object {
+
+    public abstract bool equals(Node other);
+    
+    public abstract string to_string();
+
+}
+
+public abstract class SubjectNode : Node {
+
+}
+
+public class URIRef : SubjectNode {
+
+    public string uri { get; construct; }
+    
+    public URIRef(string uri) {
+        Object(uri: uri);
+    }
+    
+    public override bool equals(Node _other) {
+        var other = _other as URIRef;
+        if (other != null) {
+            return this.uri == other.uri;
+        }
+        return false;
+    }
+    
+    public override string to_string() {
+        return @"<$uri>";
+    }
+    
+}
+
+public class Blank : SubjectNode {
+
+    public string id { get; construct; }
+    
+    public Blank(string id) {
+        Object(id: id);
+    }
+    
+    public override bool equals(Node _other) {
+        return this == _other;
+    }
+    
+    public override string to_string() {
+        return @"_:$id";
+    }
+
+}
+
+public abstract class Literal : Node {
+
+}
+
+public class PlainLiteral : Literal {
+
+    public string lexical_value { get; construct; }
+    public string? lang { get; construct; }
+    
+    public PlainLiteral(string lexical_value) {
+        Object(lexical_value: lexical_value);
+    }
+    
+    public PlainLiteral.with_lang(string lexical_value, string lang) {
+        Object(lexical_value: lexical_value, lang: lang);
+    }
+    
+    public override bool equals(Node _other) {
+        var other = _other as PlainLiteral;
+        if (other != null) {
+            return this.lexical_value == other.lexical_value &&
+                    this.lang == other.lang;
+        }
+        return false;
+    }
+    
+    public override string to_string() {
+        if (lang != null)
+            return @"\"$lexical_value\"@$lang";
+        return @"\"$lexical_value\"";
+    }
+    
+}
+
+public class Statement : Object {
+
+    public SubjectNode subject { get; construct; }
+    public URIRef predicate { get; construct; }
+    public Node object { get; construct; }
+    
+    public Statement(SubjectNode subject, URIRef predicate, Node object) {
+        Object(subject: subject, predicate: predicate, object: object);
+    }
+    
+    public string to_string() {
+        return @"$subject $predicate $object .";
+    }
+    
+    public static bool equal(Statement left, Statement right) {
+        return left.subject.equals(right.subject) &&
+                left.predicate.equals(right.predicate) &&
+                left.object.equals(right.object);
+    }
+    
+}
+
+// XXX naive
+public class Graph : Object {
+
+    class construct {
+        Xml.Parser.init();
+    }
+
+    private Gee.List<Statement> statements =
+        new Gee.LinkedList<Statement>((EqualFunc) Statement.equal);
+    private string? base_uri;
+    
+    public Graph() {
+    }
+    
+    public Graph.from_xml(string xml, string base_uri) throws ParseError {
+        this.base_uri = base_uri;
+        Xml.Doc* doc = Xml.Parser.parse_memory(xml, (int) xml.length);
+        if (doc == null)
+            throw new ParseError.UNPARSEABLE_XML("doc == null");
+        try {
+            Xml.Node* root = doc->get_root_element();
+            if (root == null)
+                throw new ParseError.EMPTY_XML("root == null");
+            if (root->name != "RDF" || root->ns->href != RDF_NS)
+                throw new ParseError.DOCUMENT_ELEMENT_NOT_FOUND("root was not <rdf:RDF>");
+            for (Xml.Node* child = root->children; child != null; child = child->next) {
+                if (child->type != Xml.ElementType.ELEMENT_NODE)
+                    continue;
+                parse_node_element(child);
+            }
+        } finally {
+            delete doc;
+        }
+    }
+    
+    // XXX intern URIs and lang tags
+    
+    private void parse_node_element(Xml.Node* element) throws ParseError {
+        // determine resource URI
+        var subject_uri = element->get_ns_prop("about", RDF_NS);
+        if (subject_uri == null)
+            throw new ParseError.ILLEGAL_RDFXML("missing rdf:about attribute");
+        var subject = new URIRef(resolve_uri(subject_uri, base_uri));
+    
+        // handle attributes
+        // skip rdf:about, xml:lang, rdf:parseType
+        for (Xml.Attr* attr = element->properties; attr != null; attr = attr->next) {
+            if (attr->atype != 0 ||
+                    (attr->name == "about" && attr->ns->href == RDF_NS) ||
+                    (attr->name == "lang" && attr->ns->href == XML_NS) ||
+                    (attr->name == "parseType" && attr->ns->href == RDF_NS))
+                continue;
+            parse_property_attribute(subject, attr);
+        }
+        
+        // handle child elements
+        for (Xml.Node* child = element->children; child != null; child = child->next) {
+            if (child->type != Xml.ElementType.ELEMENT_NODE)
+                continue;
+            parse_property_element(subject, child);
+        }
+    }
+    
+    private void parse_property_attribute(URIRef subject, Xml.Attr* attr) {
+        var property = new URIRef(attr->ns->href + attr->name);
+        Node object;
+        if (attr->name == "type" && attr->ns->href == RDF_NS) {
+            object = new URIRef(attr->children->content);
+        } else {
+            var lang = attr->parent->get_lang();
+            if (lang != null)
+                object = new PlainLiteral.with_lang(attr->children->content, lang);
+            else
+                object = new PlainLiteral(attr->children->content);
+        }
+        statements.add(new Statement(subject, property, object));
+    }
+    
+    private void parse_property_element(URIRef subject, Xml.Node* element) {
+        var property = new URIRef(element->ns->href + element->name);
+        
+        // is the object a URI ref? (rdf:resource)
+        var object_uri = element->get_ns_prop("resource", RDF_NS);
+        if (object_uri != null) {
+            var object = new URIRef(object_uri);
+            statements.add(new Statement(subject, property, object));
+            return;
+        }
+        
+        // is it a literal? (no children)
+        if (element->child_element_count() == 0) {
+            PlainLiteral object;
+            var lang = element->get_lang();
+            if (lang != null)
+                object = new PlainLiteral.with_lang(element->get_content(), lang);
+            else
+                object = new PlainLiteral(element->get_content());
+            statements.add(new Statement(subject, property, object));
+            return;
+        }
+        
+        // need to recurse
+        // XXX
+    }
+    
+    public Gee.Collection<Statement> get_statements() {
+        return statements;
+    }
+    
+}
+
+private const string RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+private const string XML_NS = "http://www.w3.org/XML/1998/namespace";
+
+private string resolve_uri(string uri, string base_uri) {
+    return new Soup.URI.with_base(new Soup.URI(base_uri), uri).to_string(false);
+}
+
+errordomain ParseError {
+    UNPARSEABLE_XML,
+    EMPTY_XML,
+    DOCUMENT_ELEMENT_NOT_FOUND,
+    ILLEGAL_RDFXML
+}
+
+#if TEST
+
+public void test_property_attributes() {
+    var g = new Graph.from_xml("""
+            <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+                <rdf:Description rdf:about=""
+                    xml:lang="en"
+                    xmlns:Iptc4xmpCore="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"
+                    Iptc4xmpCore:Location="UQ St Lucia">
+                </rdf:Description>
+            </rdf:RDF>
+            """, "http://example.com/");
+    assert(g.get_statements().size == 1);
+    assert(g.get_statements().contains(new Statement(
+            new URIRef("http://example.com/"),
+            new URIRef("http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/Location"),
+            new PlainLiteral.with_lang("UQ St Lucia", "en"))));
+}
+
+public void test_property_attributes_rdf_type() {
+    var g = new Graph.from_xml("""
+            <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+                <rdf:Description rdf:about=""
+                    rdf:type="http://example.com/Class">
+                </rdf:Description>
+            </rdf:RDF>
+            """, "http://example.com/");
+    assert(g.get_statements().size == 1);
+    assert(g.get_statements().contains(new Statement(
+            new URIRef("http://example.com/"),
+            new URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
+            new URIRef("http://example.com/Class"))));
+}
+
+public void test_property_elements() {
+    var g = new Graph.from_xml("""
+            <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+                <rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">
+                    <dc:description xml:lang="en">Some stuff.</dc:description>
+                </rdf:Description>
+            </rdf:RDF>
+            """, "http://example.com/");
+    assert(g.get_statements().size == 1);
+    assert(g.get_statements().contains(new Statement(
+            new URIRef("http://example.com/"),
+            new URIRef("http://purl.org/dc/elements/1.1/description"),
+            new PlainLiteral.with_lang("Some stuff.", "en"))));
+}
+
+public void test_property_elements_inherit_lang() {
+    var g = new Graph.from_xml("""
+            <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+                <rdf:Description rdf:about=""
+                    xmlns:dc="http://purl.org/dc/elements/1.1/"
+                    xml:lang="en">
+                    <dc:description>Some stuff.</dc:description>
+                </rdf:Description>
+            </rdf:RDF>
+            """, "http://example.com/");
+    assert(g.get_statements().size == 1);
+    assert(g.get_statements().contains(new Statement(
+            new URIRef("http://example.com/"),
+            new URIRef("http://purl.org/dc/elements/1.1/description"),
+            new PlainLiteral.with_lang("Some stuff.", "en"))));
+}
+
+public void register_tests() {
+    Test.add_func("/xmpedit/rdf/test_property_attributes", test_property_attributes);
+    Test.add_func("/xmpedit/rdf/test_property_attributes_rdf_type", test_property_attributes_rdf_type);
+    Test.add_func("/xmpedit/rdf/test_property_elements", test_property_elements);
+    Test.add_func("/xmpedit/rdf/test_property_elements_inherit_lang", test_property_elements_inherit_lang);
+}
+
+#endif
+
+}
diff --git a/src/xmpedit.vala b/src/xmpedit.vala
@@ -1,7 +1,18 @@
 
 namespace XmpEdit {
 
-public static int main (string[] args) {
+#if TEST
+
+public int main(string[] args) {
+    Test.init(ref args);
+    RDF.register_tests();
+    Test.run();
+    return 0;
+}
+
+#else
+
+public int main (string[] args) {
     Gtk.init_with_args(ref args, "PHOTO_FILENAME", { }, /* translation_domain */ null);
     if (args.length < 2) {
         stderr.puts("xmpedit: no photo filename supplied\n");
@@ -15,4 +26,6 @@ public static int main (string[] args) {
     return 0;
 }
 
+#endif
+
 }