using System; using System.Collections; using System.IO; using System.Text; using System.Xml; using SemWeb.Util; namespace SemWeb { public class RdfXmlReader : RdfReader { // TODO: Make some of the errors warnings. XmlReader xml; Hashtable blankNodes = new Hashtable(); UriMap namedNodes = new UriMap(); Hashtable seenIDs = new Hashtable(); StatementSink storage; static readonly Entity rdfType = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", rdfFirst = "http://www.w3.org/1999/02/22-rdf-syntax-ns#first", rdfRest = "http://www.w3.org/1999/02/22-rdf-syntax-ns#rest", rdfNil = "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil", rdfSubject = "http://www.w3.org/1999/02/22-rdf-syntax-ns#subject", rdfPredicate = "http://www.w3.org/1999/02/22-rdf-syntax-ns#predicate", rdfObject = "http://www.w3.org/1999/02/22-rdf-syntax-ns#object", rdfStatement = "http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement"; public RdfXmlReader(XmlDocument document) { xml = new XmlBaseAwareReader(new XmlNodeReader(document)); } public RdfXmlReader(XmlReader document) { XmlValidatingReader reader = new XmlValidatingReader(document); reader.ValidationType = ValidationType.None; xml = new XmlBaseAwareReader(reader); } public RdfXmlReader(TextReader document) : this(new XmlTextReader(document)) { } public RdfXmlReader(Stream document) : this(new XmlTextReader(document)) { } public RdfXmlReader(string file) : this(GetReader(file)) { } public override void Select(StatementSink storage) { // Read past the processing instructions to // the document element. If it is rdf:RDF, // then process the description nodes within it. // Otherwise, the document element is itself a // description. this.storage = storage; while (xml.Read()) { if (xml.NamespaceURI == NS.RDF && xml.LocalName == "RDF" ) { // If there is an xml:base here, set BaseUri so // the application can recover it. It doesn't // affect parsing since the xml:base attribute // will override BaseUri. string xmlbase = xml.GetAttribute("xml:base"); if (xmlbase != null) BaseUri = xmlbase; while (xml.Read()) { if (xml.NodeType == XmlNodeType.Element) ParseDescription(); } break; } } xml.Close(); } private string CurNode() { if (xml.NamespaceURI == "") OnWarning("Element node must be qualified (" + xml.Name + ")"); if (xml.Prefix != "") Namespaces.AddNamespace(xml.NamespaceURI, xml.Prefix); // This probably isn't quite right, but compensates // for a corresponding hash issue in XmlWriter. if (xml.NamespaceURI == "" && BaseUri == null) return "#" + xml.LocalName; return xml.NamespaceURI + xml.LocalName; } private int isset(string attribute) { return attribute != null ? 1 : 0; } private string Unrelativize(string uri) { return GetAbsoluteUri(xml.BaseURI != "" ? xml.BaseURI : BaseUri, uri); } private Entity GetBlankNode(string nodeID) { if (blankNodes.ContainsKey(nodeID)) return (Entity)blankNodes[nodeID]; Entity entity = new BNode(nodeID); blankNodes[nodeID] = entity; return entity; } private Entity GetNamedNode(string uri) { if (!ReuseEntities) return new Entity(uri); Entity ret = (Entity)namedNodes[uri]; if (ret != null) return ret; ret = new Entity(uri); namedNodes[uri] = ret; return ret; } private Entity ParseDescription() { // The XmlReader is positioned on an element node // that is a description of an entity. // On returning, the reader is positioned after the // end element of the description node. string nodeID = xml.GetAttribute("nodeID", NS.RDF); string about = xml.GetAttribute("about", NS.RDF); //if (about == null) // about = xml.GetAttribute("about"); string ID = xml.GetAttribute("ID", NS.RDF); if (isset(nodeID) + isset(about) + isset(ID) > 1) OnError("An entity description cannot specify more than one of rdf:nodeID, rdf:about, and rdf:ID"); if (nodeID != null && !IsValidXmlName(nodeID)) OnWarning("'" + nodeID + "' is not a valid XML Name"); if (ID != null && !IsValidXmlName(ID)) OnWarning("'" + ID + "' is not a valid XML Name"); Entity entity; if (about != null) entity = GetNamedNode(Unrelativize(about)); else if (ID != null) { entity = GetNamedNode(Unrelativize("#" + ID)); if (seenIDs.ContainsKey(entity.Uri)) OnWarning("Two descriptions should not use the same rdf:ID: <" + entity.Uri + ">"); seenIDs[entity.Uri] = seenIDs; } else if (nodeID != null) entity = GetBlankNode(nodeID); else entity = new BNode(); // If the name of the element is not rdf:Description, // then the name gives its type. if (CurNode() != NS.RDF + "Description") { if (IsRestrictedName(CurNode()) || IsDeprecatedName(CurNode())) OnError(xml.Name + " cannot be the type of a resource."); if (CurNode() == NS.RDF + "li") OnError("rdf:li cannot be the type of a resource"); storage.Add(new Statement(entity, rdfType, (Entity)CurNode(), Meta)); } ParsePropertyAttributes(entity); ParsePropertyNodes(entity); return entity; } private bool ParsePropertyAttributes(Entity entity) { bool foundAttrs = false; if (!xml.MoveToFirstAttribute()) return false; do { // Propery attributes in the default namespace // should be ignored. if (xml.NamespaceURI == "") continue; string curnode = CurNode(); // rdf:type is interpreted with an entity object, // not a literal object. if (curnode == NS.RDF + "type") { storage.Add(new Statement(entity, rdfType, (Entity)xml.Value, Meta)); foundAttrs = true; continue; } // Properties which are not recognized as property // attributes and should be ignored. if (IsRestrictedName(curnode)) continue; if (IsDeprecatedName(curnode)) OnError(xml.Name + " is deprecated."); // Properties which are invalid as attributes. if (curnode == NS.RDF + "li") OnError("rdf:li is not a valid attribute"); if (curnode == NS.RDF + "aboutEach" || curnode == NS.RDF + "aboutEachPrefix") OnError("rdf:aboutEach has been removed from the RDF spec"); // Unrecognized attributes in the xml namespace should be ignored. if (xml.Prefix == "xml") continue; if (xml.Prefix == "xmlns") continue; if (curnode == "http://www.w3.org/2000/xmlns/xmlns") continue; // This is a literal property attribute. string lang = xml.XmlLang != "" ? xml.XmlLang : null; storage.Add(new Statement(entity, curnode, new Literal(xml.Value, lang, null), Meta)); Namespaces.AddNamespace(xml.NamespaceURI, xml.Prefix); foundAttrs = true; } while (xml.MoveToNextAttribute()); xml.MoveToElement(); return foundAttrs; } private void ParsePropertyNodes(Entity subject) { // The reader is positioned within a description node. // On returning, the reader is positioned after the // end element of the description node. if (xml.IsEmptyElement) return; int liIndex = 1; while (xml.Read()) { if (xml.NodeType == XmlNodeType.EndElement) break; if (xml.NodeType == XmlNodeType.Text) { OnWarning("Text \"" + xml.Value + "\" ignored in description node"); continue; } if (xml.NodeType != XmlNodeType.Element) continue; ParseProperty(subject, ref liIndex); } } private void ParseProperty(Entity subject, ref int liIndex) { // The reader is positioned on a propert node, // and on returning the reader is positioned past // that node. // Get all of the attributes before we move the reader forward. string nodeID = xml.GetAttribute("nodeID", NS.RDF); string resource = xml.GetAttribute("resource", NS.RDF); string parseType = xml.GetAttribute("parseType", NS.RDF); string datatype = xml.GetAttribute("datatype", NS.RDF); string lang = xml.XmlLang != "" ? xml.XmlLang : null; string predicate = CurNode(); if (predicate == NS.RDF + "li") predicate = NS.RDF + "_" + (liIndex++); if (IsRestrictedName(predicate)) OnError(xml.Name + " cannot be used as a property name."); if (IsDeprecatedName(predicate)) OnError(xml.Name + " has been deprecated and cannot be used as a property name."); string ID = xml.GetAttribute("ID", NS.RDF); if (nodeID != null && !IsValidXmlName(nodeID)) OnWarning("'" + nodeID + "' is not a valid XML Name"); if (ID != null && !IsValidXmlName(ID)) OnWarning("'" + ID + "' is not a valid XML Name"); Resource objct = null; if (nodeID != null || resource != null) { if (isset(nodeID) + isset(resource) > 1) OnError("A predicate node cannot specify more than one of rdf:nodeID and rdf:resource"); if (parseType != null || datatype != null) OnError("The attributes rdf:parseType and rdf:datatype are not valid on a predicate with a rdf:nodeID or rdf:resource attribute"); // Object is an entity given by nodeID or resource. if (nodeID != null) objct = GetBlankNode(nodeID); else if (resource != null) objct = GetNamedNode(Unrelativize(resource)); ParsePropertyAttributes((Entity)objct); // No children are allowed in this element. if (!xml.IsEmptyElement) while (xml.Read()) { if (xml.NodeType == XmlNodeType.EndElement) break; if (xml.NodeType == XmlNodeType.Whitespace) continue; if (xml.NodeType == XmlNodeType.Comment) continue; if (xml.NodeType == XmlNodeType.ProcessingInstruction) continue; OnError("Content is not allowed within a property with a rdf:nodeID or rdf:resource attribute"); } } else if (parseType != null && parseType == "Literal") { if (datatype == null) datatype = "http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral"; if (ParsePropertyAttributes(new BNode())) OnError("Property attributes are not valid when parseType is Literal"); // TODO: Do we canonicalize according to: // http://www.w3.org/TR/2002/REC-xml-exc-c14n-20020718/ ? objct = new Literal(xml.ReadInnerXml(), null, datatype); } else if (parseType != null && parseType == "Resource") { objct = new BNode(); ParsePropertyAttributes((Entity)objct); if (!xml.IsEmptyElement) ParsePropertyNodes((Entity)objct); } else if (parseType != null && parseType == "Collection") { Entity collection = new BNode(); Entity lastnode = collection; bool empty = true; ParsePropertyAttributes(collection); if (!xml.IsEmptyElement) while (xml.Read()) { if (xml.NodeType == XmlNodeType.EndElement) break; if (xml.NodeType != XmlNodeType.Element) continue; if (!empty) { Entity next = new BNode(); storage.Add(new Statement(lastnode, rdfRest, next, Meta)); lastnode = next; } Entity item = ParseDescription(); storage.Add(new Statement(lastnode, rdfFirst, item, Meta)); empty = false; } storage.Add(new Statement(lastnode, rdfRest, rdfNil, Meta)); if (empty) objct = rdfNil; else objct = collection; } else if (parseType != null) { OnError("Invalid value for parseType: '" + parseType + "'"); } else if (datatype != null) { // Note that any xml:lang is discarded. if (ParsePropertyAttributes(new BNode())) OnError("Property attributes are not valid when a datatype is given"); if (xml.IsEmptyElement) { objct = new Literal("", null, datatype); } else { objct = new Literal(xml.ReadString(), null, datatype); if (xml.NodeType != XmlNodeType.EndElement) OnError("XML markup may not appear in a datatyped literal property."); } } else { // We don't know whether the contents of this element // refer to a literal or an entity. If an element is // a child of this node, then it must be an entity. // If the property has predicate attributes, then it // is an anonymous entity. Otherwise the text content // is the literal value. objct = new BNode(); if (ParsePropertyAttributes((Entity)objct)) { // Found property attributes. There should be no other internal content? if (!xml.IsEmptyElement) while (xml.Read()) { if (xml.NodeType == XmlNodeType.EndElement) break; if (xml.NodeType == XmlNodeType.Whitespace) continue; if (xml.NodeType == XmlNodeType.Comment) continue; if (xml.NodeType == XmlNodeType.ProcessingInstruction) continue; OnError(xml.NodeType + " is not allowed within a property with property attributes"); } } else { StringBuilder textcontent = new StringBuilder(); bool hadText = false; bool hadElement = false; if (!xml.IsEmptyElement) while (xml.Read()) { if (xml.NodeType == XmlNodeType.EndElement) break; if (xml.NodeType == XmlNodeType.Element) { if (hadText) OnError("Both text and elements are present as a property value"); if (hadElement) OnError("A property node cannot contain more than one entity description. " + objct + " already found."); hadElement = true; objct = ParseDescription(); } else if (xml.NodeType == XmlNodeType.Text || xml.NodeType == XmlNodeType.SignificantWhitespace) { if (hadElement) OnError("Both text and elements are present as a property value"); textcontent.Append(xml.Value); hadText = true; } else { textcontent.Append(xml.Value); } } if (!hadElement) objct = new Literal(textcontent.ToString(), lang, null); } } storage.Add(new Statement(subject, predicate, objct, Meta)); if (ID != null) { // In addition to adding the statement as normal, also // add a reified statement. Entity statement = GetNamedNode(Unrelativize("#" + ID));; storage.Add(new Statement(statement, rdfType, rdfStatement, Meta)); storage.Add(new Statement(statement, rdfSubject, subject, Meta)); storage.Add(new Statement(statement, rdfPredicate, (Entity)predicate, Meta)); storage.Add(new Statement(statement, rdfObject, objct, Meta)); } } private bool IsRestrictedName(string name) { if (name == NS.RDF + "RDF") return true; if (name == NS.RDF + "Description") return true; if (name == NS.RDF + "ID") return true; if (name == NS.RDF + "about") return true; if (name == NS.RDF + "parseType") return true; if (name == NS.RDF + "resource") return true; if (name == NS.RDF + "nodeID") return true; if (name == NS.RDF + "datatype") return true; return false; } private bool IsDeprecatedName(string name) { if (name == NS.RDF + "bagID") return true; if (name == NS.RDF + "aboutEach") return true; if (name == NS.RDF + "aboutEachPrefix") return true; return false; } private bool IsValidXmlName(string name) { // I'm not sure what's supposed to be valid, but this is just for warnings anyway. // CombiningChar and Extender characters are omitted. if (name.Length == 0) return false; char f = name[0]; if (!char.IsLetter(f) && f != '_') return false; for (int i = 1; i < name.Length; i++) { f = name[i]; if (!char.IsLetter(f) && !char.IsDigit(f) && f != '.' && f != '-' && f != '_') return false; } return true; } private void OnError(string message) { if (xml is IXmlLineInfo && ((IXmlLineInfo)xml).HasLineInfo()) { IXmlLineInfo line = (IXmlLineInfo)xml; message += ", line " + line.LineNumber + " col " + line.LinePosition; } throw new ParserException(message); } private new void OnWarning(string message) { if (xml is IXmlLineInfo && ((IXmlLineInfo)xml).HasLineInfo()) { IXmlLineInfo line = (IXmlLineInfo)xml; message += ", line " + line.LineNumber + " col " + line.LinePosition; } base.OnWarning(message); } private class XmlBaseAwareReader : XmlReader { XmlReader _reader; Stack _bases = new Stack(); Uri _baseUri; bool temp = false; XmlResolver resolver = new XmlUrlResolver(); public XmlBaseAwareReader(XmlReader reader) { _reader = reader; if (_reader.BaseURI != "") _baseUri = new Uri(_reader.BaseURI); } public override string BaseURI { get { return _baseUri == null ? "" : _baseUri.AbsoluteUri; } } public override bool Read() { if (!_reader.Read()) return false; if (temp) { // last element was an empty element _baseUri = (Uri)_bases.Pop(); temp = false; } if (_reader.NodeType == XmlNodeType.EndElement) _baseUri = (Uri)_bases.Pop(); if (_reader.NodeType == XmlNodeType.Element) { string baseAttr = _reader.GetAttribute("xml:base"); if (_reader.IsEmptyElement && baseAttr == null) { // do nothing : there is no EndElement, so no pop } else { _bases.Push(_baseUri); // even if no change, there will be a pop if (baseAttr != null) _baseUri = resolver.ResolveUri(_baseUri, baseAttr); // if this is an empty element, there is no EndElement, // so we must do a pop before processing the next node. temp = _reader.IsEmptyElement; } } return true; } public override void Close () { _reader.Close(); } public override string GetAttribute (int i) { return _reader.GetAttribute(i); } public override string GetAttribute (string name) { return _reader.GetAttribute(name); } public override string GetAttribute (string localName, string namespaceName) { return _reader.GetAttribute(localName, namespaceName); } public override bool IsStartElement () { return _reader.IsStartElement(); } public override bool IsStartElement (string name) { return _reader.IsStartElement(name); } public override bool IsStartElement (string localName, string namespaceName) { return _reader.IsStartElement(localName, namespaceName); } public override string LookupNamespace (string prefix) { return _reader.LookupNamespace(prefix); } public override void MoveToAttribute (int i) { _reader.MoveToAttribute(i); } public override bool MoveToAttribute (string name) { return _reader.MoveToAttribute(name); } public override bool MoveToAttribute (string localName, string namespaceName) { return _reader.MoveToAttribute(localName, namespaceName); } public override XmlNodeType MoveToContent () { return _reader.MoveToContent(); } public override bool MoveToElement () { return _reader.MoveToElement(); } public override bool MoveToFirstAttribute () { return _reader.MoveToFirstAttribute(); } public override bool MoveToNextAttribute () { return _reader.MoveToNextAttribute(); } public override bool ReadAttributeValue () { return _reader.ReadAttributeValue(); } public override string ReadElementString () { return _reader.ReadElementString(); } public override string ReadElementString (string name) { return _reader.ReadElementString(name); } public override string ReadElementString (string localName, string namespaceName) { return _reader.ReadElementString(localName, namespaceName); } public override void ReadEndElement () { _reader.ReadEndElement(); } public override string ReadInnerXml () { return _reader.ReadInnerXml(); } public override string ReadOuterXml () { return _reader.ReadOuterXml(); } public override void ReadStartElement () { _reader.ReadStartElement(); } public override void ReadStartElement (string name) { _reader.ReadStartElement(name); } public override void ReadStartElement (string localName, string namespaceName) { _reader.ReadStartElement(localName, namespaceName); } public override string ReadString () { return _reader.ReadString(); } public override void ResolveEntity () { _reader.ResolveEntity(); } public override void Skip () { _reader.Skip(); } public override int AttributeCount { get { return _reader.AttributeCount; } } public override bool CanResolveEntity { get { return _reader.CanResolveEntity; } } public override int Depth { get { return _reader.Depth; } } public override bool EOF { get { return _reader.EOF; } } public override bool HasAttributes { get { return _reader.HasAttributes; } } public override bool HasValue { get { return _reader.HasValue; } } public override bool IsDefault { get { return _reader.IsDefault; } } public override bool IsEmptyElement { get { return _reader.IsEmptyElement; } } public override string this [int i] { get { return _reader[i]; } } public override string this [string name] { get { return _reader[name]; } } public override string this [string localName, string namespaceName] { get { return _reader[localName, namespaceName]; } } public override string LocalName { get { return _reader.LocalName; } } public override string Name { get { return _reader.Name; } } public override string NamespaceURI { get { return _reader.NamespaceURI; } } public override XmlNameTable NameTable { get { return _reader.NameTable; } } public override XmlNodeType NodeType { get { return _reader.NodeType; } } public override string Prefix { get { return _reader.Prefix; } } public override char QuoteChar { get { return _reader.QuoteChar; } } public override ReadState ReadState { get { return _reader.ReadState; } } public override string Value { get { return _reader.Value; } } public override string XmlLang { get { return _reader.XmlLang; } } public override XmlSpace XmlSpace { get { return _reader.XmlSpace; } } } } }