Skip to content

XML: Add namespace support for XML attributes #2313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 75 additions & 5 deletions CoreFoundation/Parsing.subproj/CFXMLInterface.c
Original file line number Diff line number Diff line change
Expand Up @@ -367,16 +367,38 @@ _CFXMLNodePtr _CFXMLNewComment(const unsigned char* value) {
return xmlNewComment(value);
}

_CFXMLNodePtr _CFXMLNewProperty(_CFXMLNodePtr node, const unsigned char* name, const unsigned char* value) {
return xmlNewProp(node, name, value);
_CFXMLNodePtr _CFXMLNewProperty(_CFXMLNodePtr node, const unsigned char* name, const unsigned char* uri, const unsigned char* value) {
xmlNodePtr nodePtr = (xmlNodePtr)node;
xmlChar *prefix = NULL;
xmlChar *localName = xmlSplitQName2(name, &prefix);

_CFXMLNodePtr result;
if (uri == NULL && localName == NULL) {
result = xmlNewProp(node, name, value);
} else {
xmlNsPtr ns = xmlNewNs(nodePtr, uri, localName ? prefix : NULL);
result = xmlNewNsProp(nodePtr, ns, localName ? localName : name, value);
}

if (localName) {
xmlFree(localName);
}
if (prefix) {
xmlFree(prefix);
}
return result;
}

CFStringRef _CFXMLNodeCopyURI(_CFXMLNodePtr node) {
xmlNodePtr nodePtr = (xmlNodePtr)node;
switch (nodePtr->type) {
case XML_ATTRIBUTE_NODE:
case XML_ELEMENT_NODE:
return CFStringCreateWithCString(NULL, (const char*)nodePtr->ns->href, kCFStringEncodingUTF8);
if (nodePtr->ns && nodePtr->ns->href) {
return CFStringCreateWithCString(NULL, (const char*)nodePtr->ns->href, kCFStringEncodingUTF8);
} else {
return NULL;
}

case XML_DOCUMENT_NODE:
{
Expand Down Expand Up @@ -914,8 +936,56 @@ CFStringRef _Nullable _CFXMLCopyPathForNode(_CFXMLNodePtr node) {
return result;
}

_CFXMLNodePtr _CFXMLNodeHasProp(_CFXMLNodePtr node, const char* propertyName) {
return xmlHasProp(node, (const xmlChar*)propertyName);
static inline xmlNsPtr _searchNamespace(xmlNodePtr nodePtr, const xmlChar* prefix) {
while (nodePtr != NULL) {
xmlNsPtr ns = nodePtr->ns;
while (ns != NULL) {
if (xmlStrcmp(prefix, ns->prefix) == 0) {
return ns;
}
ns = ns->next;
}
nodePtr = nodePtr->parent;
}
return NULL;
}

void _CFXMLCompletePropURI(_CFXMLNodePtr propertyNode, _CFXMLNodePtr node) {
xmlNodePtr propNodePtr = (xmlNodePtr)propertyNode;
xmlNodePtr nodePtr = (xmlNodePtr)node;
if (propNodePtr->type != XML_ATTRIBUTE_NODE || nodePtr->type != XML_ELEMENT_NODE) {
return;
}
if (propNodePtr->ns != NULL
&& propNodePtr->ns->href == NULL
&& propNodePtr->ns->prefix != NULL) {
xmlNsPtr ns = _searchNamespace(nodePtr, propNodePtr->ns->prefix);
if (ns != NULL && ns->href != NULL) {
propNodePtr->ns->href = xmlStrdup(ns->href);
}
}
}

_CFXMLNodePtr _CFXMLNodeHasProp(_CFXMLNodePtr node, const unsigned char* propertyName, const unsigned char* uri) {
xmlNodePtr nodePtr = (xmlNodePtr)node;
xmlChar* prefix = NULL;
xmlChar* localName = xmlSplitQName2(propertyName, &prefix);

if (!uri) {
xmlNsPtr ns = _searchNamespace(nodePtr, prefix);
uri = ns ? ns->href : NULL;
}
_CFXMLNodePtr result;
result = xmlHasNsProp(node, localName ? localName : propertyName, uri);

if (localName) {
xmlFree(localName);
}
if (prefix) {
xmlFree(prefix);
}

return result;
}

_CFXMLDocPtr _CFXMLDocPtrFromDataWithOptions(CFDataRef data, unsigned int options) {
Expand Down
5 changes: 3 additions & 2 deletions CoreFoundation/Parsing.subproj/CFXMLInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ _CFXMLDocPtr _CFXMLNewDoc(const unsigned char* version);
_CFXMLNodePtr _CFXMLNewProcessingInstruction(const unsigned char* name, const unsigned char* value);
_CFXMLNodePtr _CFXMLNewTextNode(const unsigned char* value);
_CFXMLNodePtr _CFXMLNewComment(const unsigned char* value);
_CFXMLNodePtr _CFXMLNewProperty(_CFXMLNodePtr _Nullable node, const unsigned char* name, const unsigned char* value);
_CFXMLNodePtr _CFXMLNewProperty(_CFXMLNodePtr _Nullable node, const unsigned char* name, const unsigned char* _Nullable uri, const unsigned char* value);

CFStringRef _Nullable _CFXMLNodeCopyURI(_CFXMLNodePtr node);
void _CFXMLNodeSetURI(_CFXMLNodePtr node, const unsigned char* _Nullable URI);
Expand Down Expand Up @@ -197,7 +197,8 @@ CFStringRef _CFXMLCopyStringWithOptions(_CFXMLNodePtr node, uint32_t options);
CF_RETURNS_RETAINED CFArrayRef _Nullable _CFXMLNodesForXPath(_CFXMLNodePtr node, const unsigned char* xpath);
CFStringRef _Nullable _CFXMLCopyPathForNode(_CFXMLNodePtr node);

_CFXMLNodePtr _Nullable _CFXMLNodeHasProp(_CFXMLNodePtr node, const char* propertyName);
void _CFXMLCompletePropURI(_CFXMLNodePtr propertyNode, _CFXMLNodePtr node);
_CFXMLNodePtr _Nullable _CFXMLNodeHasProp(_CFXMLNodePtr node, const unsigned char* propertyName, const unsigned char* _Nullable uri);

_CFXMLDocPtr _CFXMLDocPtrFromDataWithOptions(CFDataRef data, unsigned int options);

Expand Down
2 changes: 1 addition & 1 deletion Docs/Status.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ There is no _Complete_ status for test coverage because there are always additio
| `XMLDocument` | Mostly Complete | Substantial | `init()`, `replacementClass(for:)`, and `object(byApplyingXSLT...)` remain unimplemented |
| `XMLDTD` | Mostly Complete | Substantial | `init()` remains unimplemented |
| `XMLDTDNode` | Complete | Incomplete | |
| `XMLElement` | Incomplete | Incomplete | `init(xmlString:)`, `elements(forLocalName:uri:)`, `attribute(forLocalName:uri:)`, namespace support, and others remain unimplemented |
| `XMLElement` | Incomplete | Incomplete | `init(xmlString:)`, `elements(forLocalName:uri:)`, namespace support, and others remain unimplemented |
| `XMLNode` | Incomplete | Incomplete | `localName(forName:)`, `prefix(forName:)`, `predefinedNamespace(forPrefix:)`, and others remain unimplemented |
| `XMLParser` | Complete | Incomplete | |

Expand Down
8 changes: 5 additions & 3 deletions Foundation/XMLElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ open class XMLElement: XMLNode {
}

removeAttribute(forName: name)
_CFXMLCompletePropURI(attribute._xmlNode, _xmlNode);
addChild(attribute)
}

Expand All @@ -98,7 +99,7 @@ open class XMLElement: XMLNode {
@abstract Removes an attribute based on its name.
*/
open func removeAttribute(forName name: String) {
if let prop = _CFXMLNodeHasProp(_xmlNode, name) {
if let prop = _CFXMLNodeHasProp(_xmlNode, name, nil) {
let propNode = XMLNode._objectNodeForNode(_CFXMLNodePtr(prop))
_childNodes.remove(propNode)
// We can't use `xmlRemoveProp` because someone else may still have a reference to this attribute
Expand Down Expand Up @@ -170,7 +171,7 @@ open class XMLElement: XMLNode {
@abstract Returns an attribute matching this name.
*/
open func attribute(forName name: String) -> XMLNode? {
guard let attribute = _CFXMLNodeHasProp(_xmlNode, name) else { return nil }
guard let attribute = _CFXMLNodeHasProp(_xmlNode, name, nil) else { return nil }
return XMLNode._objectNodeForNode(attribute)
}

Expand All @@ -179,7 +180,8 @@ open class XMLElement: XMLNode {
@abstract Returns an attribute matching this localname URI pair.
*/
open func attribute(forLocalName localName: String, uri URI: String?) -> XMLNode? {
NSUnimplemented()
guard let attribute = _CFXMLNodeHasProp(_xmlNode, localName, URI) else { return nil }
return XMLNode._objectNodeForNode(attribute)
}

/*!
Expand Down
9 changes: 4 additions & 5 deletions Foundation/XMLNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ open class XMLNode: NSObject, NSCopying {
_xmlNode = _CFXMLNewNode(nil, "")

case .attribute:
_xmlNode = _CFXMLNodePtr(_CFXMLNewProperty(nil, "", ""))
_xmlNode = _CFXMLNodePtr(_CFXMLNewProperty(nil, "", nil, ""))

case .DTDKind:
_xmlNode = _CFXMLNewDTD(nil, "", "", "")
Expand Down Expand Up @@ -199,7 +199,7 @@ open class XMLNode: NSObject, NSCopying {
@abstract Returns an attribute <tt>name="stringValue"</tt>.
*/
open class func attribute(withName name: String, stringValue: String) -> Any {
let attribute = _CFXMLNewProperty(nil, name, stringValue)
let attribute = _CFXMLNewProperty(nil, name, nil, stringValue)

return XMLNode(ptr: attribute)
}
Expand All @@ -209,10 +209,9 @@ open class XMLNode: NSObject, NSCopying {
@abstract Returns an attribute whose full QName is specified.
*/
open class func attribute(withName name: String, uri: String, stringValue: String) -> Any {
let attribute = XMLNode.attribute(withName: name, stringValue: stringValue) as! XMLNode
// attribute.URI = URI
let attribute = _CFXMLNewProperty(nil, name, uri, stringValue)

return attribute
return XMLNode(ptr: attribute)
}

/*!
Expand Down
47 changes: 47 additions & 0 deletions TestFoundation/TestXMLDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class TestXMLDocument : LoopbackServerTest {
("test_stringValue", test_stringValue),
("test_objectValue", test_objectValue),
("test_attributes", test_attributes),
("test_attributesWithNamespace", test_attributesWithNamespace),
("test_comments", test_comments),
("test_processingInstruction", test_processingInstruction),
("test_parseXMLString", test_parseXMLString),
Expand Down Expand Up @@ -264,6 +265,52 @@ class TestXMLDocument : LoopbackServerTest {
XCTAssertEqual(element.attribute(forName:"hello")?.stringValue, "world", "\(element.attribute(forName:"hello")?.stringValue as Optional)")
XCTAssertEqual(element.attribute(forName:"foobar")?.stringValue, "buzbaz", "\(element.attributes ?? [])")
}

func test_attributesWithNamespace() {
let uriNs1 = "http://example.com/ns1"
let uriNs2 = "http://example.com/ns2"

let root = XMLNode.element(withName: "root") as! XMLElement
root.addNamespace(XMLNode.namespace(withName: "ns1", stringValue: uriNs1) as! XMLNode)

let element = XMLNode.element(withName: "element") as! XMLElement
element.addNamespace(XMLNode.namespace(withName: "ns2", stringValue: uriNs2) as! XMLNode)
root.addChild(element)

// Add attributes without URI
element.addAttribute(XMLNode.attribute(withName: "name", stringValue: "John") as! XMLNode)
element.addAttribute(XMLNode.attribute(withName: "ns1:name", stringValue: "Tom") as! XMLNode)

// Add attributes with URI
element.addAttribute(XMLNode.attribute(withName: "ns1:age", uri: uriNs1, stringValue: "44") as! XMLNode)
element.addAttribute(XMLNode.attribute(withName: "ns2:address", uri: uriNs2, stringValue: "Foobar City") as! XMLNode)

// Retrieve attributes without URI
XCTAssertEqual(element.attribute(forName: "name")?.stringValue, "John", "name==John")
XCTAssertEqual(element.attribute(forName: "ns1:name")?.stringValue, "Tom", "ns1:name==Tom")
XCTAssertEqual(element.attribute(forName: "ns1:age")?.stringValue, "44", "ns1:age==44")
XCTAssertEqual(element.attribute(forName: "ns2:address")?.stringValue, "Foobar City", "ns2:addresss==Foobar City")

// Retrieve attributes with URI
XCTAssertEqual(element.attribute(forLocalName: "name", uri: nil)?.stringValue, "John", "name==John")
XCTAssertEqual(element.attribute(forLocalName: "name", uri: uriNs1)?.stringValue, "Tom", "name==Tom")
XCTAssertEqual(element.attribute(forLocalName: "age", uri: uriNs1)?.stringValue, "44", "age==44")
XCTAssertNil(element.attribute(forLocalName: "address", uri: uriNs1), "address==nil")
XCTAssertEqual(element.attribute(forLocalName: "address", uri: uriNs2)?.stringValue, "Foobar City", "addresss==Foobar City")

// Overwrite attributes
element.addAttribute(XMLNode.attribute(withName: "ns1:age", stringValue: "33") as! XMLNode)
XCTAssertEqual(element.attribute(forName: "ns1:age")?.stringValue, "33", "ns1:age==33")
element.addAttribute(XMLNode.attribute(withName: "ns1:name", uri: uriNs1, stringValue: "Tommy") as! XMLNode)
XCTAssertEqual(element.attribute(forLocalName: "name", uri: uriNs1)?.stringValue, "Tommy", "ns1:name==Tommy")

// Remove attributes
element.removeAttribute(forName: "name")
XCTAssertNil(element.attribute(forLocalName: "name", uri: nil), "name removed")
XCTAssertNotNil(element.attribute(forLocalName: "name", uri: uriNs1), "ns1:name not removed")
element.removeAttribute(forName: "ns1:name")
XCTAssertNil(element.attribute(forLocalName: "name", uri: uriNs1), "ns1:name removed")
}

func test_comments() {
let element = XMLElement(name: "root")
Expand Down