xmpedit

GTK+ editor for XMP metadata embedded in images
git clone https://code.djc.id.au/git/xmpedit/
commit c67614722aeee3096b43eb0398900eeab2bc719e
parent 7e0718158dafde0e220d0a07e11a6b62941e72f6
Author: Dan Callaghan <djc@djc.id.au>
Date:   Mon,  9 Aug 2010 22:43:39 +1000

support some more RDF/XML features (enough to parse some rudimentary XMP packets, yay)

Diffstat:
Msrc/RDF.vala | 184++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
1 file changed, 165 insertions(+), 19 deletions(-)
diff --git a/src/RDF.vala b/src/RDF.vala
@@ -37,9 +37,10 @@ public class URIRef : SubjectNode {
 
 public class Blank : SubjectNode {
 
-    public string id { get; construct; }
+    /** This is for informational purposes only! Not a unique id for equality! */
+    public string? id { get; construct; }
     
-    public Blank(string id) {
+    public Blank(string? id) {
         Object(id: id);
     }
     
@@ -48,7 +49,9 @@ public class Blank : SubjectNode {
     }
     
     public override string to_string() {
-        return @"_:$id";
+        if (id != null)
+            return @"_:$id";
+        return "Blank@%p".printf(&this);
     }
 
 }
@@ -117,7 +120,7 @@ public class Graph : Object {
     }
 
     private Gee.List<Statement> statements =
-        new Gee.LinkedList<Statement>((EqualFunc) Statement.equal);
+        new Gee.ArrayList<Statement>((EqualFunc) Statement.equal);
     private string? base_uri;
     
     public Graph() {
@@ -132,9 +135,10 @@ public class Graph : Object {
             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) {
+            var document_element = find_rdf_document_element(root);
+            if (document_element == null)
+                throw new ParseError.DOCUMENT_ELEMENT_NOT_FOUND("no <rdf:RDF> element");
+            for (Xml.Node* child = document_element->children; child != null; child = child->next) {
                 if (child->type != Xml.ElementType.ELEMENT_NODE)
                     continue;
                 parse_node_element(child);
@@ -144,14 +148,37 @@ public class Graph : Object {
         }
     }
     
+    // XXX use explicit stack instead of recursion
+    private Xml.Node* find_rdf_document_element(Xml.Node* element) {
+        if (element->name == "RDF" || element->ns->href == RDF_NS)
+            return element;
+        for (Xml.Node* child = element->children; child != null; child = child->next) {
+            if (child->type != Xml.ElementType.ELEMENT_NODE)
+                continue;
+            var found = find_rdf_document_element(child);
+            if (found != null)
+                return found;
+        }
+        return null;
+    }
+    
     // XXX intern URIs and lang tags
     
-    private void parse_node_element(Xml.Node* element) throws ParseError {
+    private SubjectNode parse_node_element(Xml.Node* element) throws ParseError {
         // determine resource URI
+        SubjectNode subject;
         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));
+        if (subject_uri != null)
+            subject = new URIRef(resolve_uri(subject_uri, base_uri));
+        else
+            subject = new Blank(null);
+        
+        // is it a typed element?
+        if (!(element->name == "Description" && element->ns->href == RDF_NS)) {
+            statements.add(new Statement(subject,
+                    new URIRef(RDF_NS + "type"),
+                    new URIRef(element->ns->href + element->name)));
+        }
     
         // handle attributes
         // skip rdf:about, xml:lang, rdf:parseType
@@ -170,10 +197,12 @@ public class Graph : Object {
                 continue;
             parse_property_element(subject, child);
         }
+        
+        return subject;
     }
     
-    private void parse_property_attribute(URIRef subject, Xml.Attr* attr) {
-        var property = new URIRef(attr->ns->href + attr->name);
+    private void parse_property_attribute(SubjectNode subject, Xml.Attr* attr) {
+        var predicate = new URIRef(attr->ns->href + attr->name);
         Node object;
         if (attr->name == "type" && attr->ns->href == RDF_NS) {
             object = new URIRef(attr->children->content);
@@ -184,17 +213,17 @@ public class Graph : Object {
             else
                 object = new PlainLiteral(attr->children->content);
         }
-        statements.add(new Statement(subject, property, object));
+        statements.add(new Statement(subject, predicate, object));
     }
     
-    private void parse_property_element(URIRef subject, Xml.Node* element) {
-        var property = new URIRef(element->ns->href + element->name);
+    private void parse_property_element(SubjectNode subject, Xml.Node* element) throws ParseError {
+        var predicate = 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));
+            statements.add(new Statement(subject, predicate, object));
             return;
         }
         
@@ -206,18 +235,37 @@ public class Graph : Object {
                 object = new PlainLiteral.with_lang(element->get_content(), lang);
             else
                 object = new PlainLiteral(element->get_content());
-            statements.add(new Statement(subject, property, object));
+            statements.add(new Statement(subject, predicate, object));
             return;
         }
         
         // need to recurse
-        // XXX
+        for (Xml.Node* child = element->children; child != null; child = child->next) {
+            if (child->type != Xml.ElementType.ELEMENT_NODE)
+                continue;
+            var object = parse_node_element(child);
+            statements.add(new Statement(subject, predicate, object));            
+            break; // ignore any other child elements, not legal anyway
+        }
     }
     
     public Gee.Collection<Statement> get_statements() {
         return statements;
     }
     
+    public Gee.Collection<Statement> find_matching_statements(
+            SubjectNode? subject, URIRef? predicate, Node? object) {
+        // XXX naive
+        var result = new Gee.ArrayList<Statement>((EqualFunc) Statement.equal);
+        foreach (var s in statements) {
+            if ((subject == null || s.subject.equals(subject)) &&
+                    (predicate == null || s.predicate.equals(predicate)) &&
+                    (object == null || s.object.equals(object)))
+                result.add(s);
+        }
+        return result;
+    }
+    
 }
 
 private const string RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
@@ -321,12 +369,110 @@ public void test_unicode() {
             new PlainLiteral.with_lang("\xd0\xb4\xd0\xb5\xd0\xbd\xd1\x8c", "ru"))));
 }
 
+public void test_find_rdf_root() {
+    var g = new Graph.from_xml("""
+            <ex:other xmlns:ex="http://some.other.crap/">
+                <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>
+            </ex:other>
+            """, "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_nested_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:foaf="http://xmlns.com/foaf/0.1/">
+                    <foaf:knows>
+                        <rdf:Description rdf:about="http://example.com/buddy">
+                            <foaf:name>My Buddy</foaf:name>
+                        </rdf:Description>
+                    </foaf:knows>
+                </rdf:Description>
+            </rdf:RDF>
+            """, "http://example.com/");
+    assert(g.get_statements().size == 2);
+    assert(g.get_statements().contains(new Statement(
+            new URIRef("http://example.com/"),
+            new URIRef("http://xmlns.com/foaf/0.1/knows"),
+            new URIRef("http://example.com/buddy"))));
+    assert(g.get_statements().contains(new Statement(
+            new URIRef("http://example.com/buddy"),
+            new URIRef("http://xmlns.com/foaf/0.1/name"),
+            new PlainLiteral("My Buddy"))));
+}
+
+public void test_blank() {
+    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:foaf="http://xmlns.com/foaf/0.1/">
+                    <foaf:knows>
+                        <rdf:Description>
+                            <foaf:name>My Buddy</foaf:name>
+                        </rdf:Description>
+                    </foaf:knows>
+                </rdf:Description>
+            </rdf:RDF>
+            """, "http://example.com/");
+    assert(g.get_statements().size == 2);
+    var statements = g.find_matching_statements(
+            new URIRef("http://example.com/"),
+            new URIRef("http://xmlns.com/foaf/0.1/knows"),
+            null);
+    assert(statements.size == 1);
+    Blank blank;
+    {
+        Gee.Iterator<Statement> it = statements.iterator();
+        it.next();
+        blank = (Blank) it.get().object;
+    }
+    assert(g.find_matching_statements(
+            blank,
+            new URIRef("http://xmlns.com/foaf/0.1/name"),
+            new PlainLiteral("My Buddy")).size == 1);
+}
+
+public void test_typed_node_element() {
+    var g = new Graph.from_xml("""
+            <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+                <foaf:Person rdf:about=""
+                    xmlns:foaf="http://xmlns.com/foaf/0.1/">
+                    <foaf:name>Person</foaf:name>
+                </foaf:Person>
+            </rdf:RDF>
+            """, "http://example.com/");
+    assert(g.get_statements().size == 2);
+    assert(g.find_matching_statements(
+            new URIRef("http://example.com/"),
+            new URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
+            new URIRef("http://xmlns.com/foaf/0.1/Person")).size == 1);
+    assert(g.find_matching_statements(
+            new URIRef("http://example.com/"),
+            new URIRef("http://xmlns.com/foaf/0.1/name"),
+            new PlainLiteral("Person")).size == 1);
+}
+
 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);
     Test.add_func("/xmpedit/rdf/test_unicode", test_unicode);
+    Test.add_func("/xmpedit/rdf/test_find_rdf_root", test_find_rdf_root);
+    Test.add_func("/xmpedit/rdf/test_nested_property_elements", test_nested_property_elements);
+    Test.add_func("/xmpedit/rdf/test_blank", test_blank);
+    Test.add_func("/xmpedit/rdf/test_typed_node_element", test_typed_node_element);
 }
 
 #endif