GDataXMLNode 。好东西,处理xml 在iOS 中使用。可以编辑和读取Xml文档。支持Xpath.这个很好。
GDataXMLNode.h
GDataXMLNode.m 文件很不好找啊。
/* Copyright (c) 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// These node, element, and document classes implement a subset of the methods
// provided by NSXML. While NSXML behavior is mimicked as much as possible,
// there are important differences.
//
// The biggest difference is that, since this is based on libxml2, there
// is no retain model for the underlying node data. Rather than copy every
// node obtained from a parse tree (which would have a substantial memory
// impact), we rely on weak references, and it is up to the code that
// created a document to retain it for as long as any
// references rely on nodes inside that document tree.
#import <Foundation/Foundation.h>
// libxml includes require that the target Header Search Paths contain
//
// /usr/include/libxml2
//
// and Other Linker Flags contain
//
// -lxml2
#import <libxml/tree.h>
#import <libxml/parser.h>
#import <libxml/xmlstring.h>
#import <libxml/xpath.h>
#import <libxml/xpathInternals.h>
#if (MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4) || defined(GDATA_TARGET_NAMESPACE)
// we need NSInteger for the 10.4 SDK, or we're using target namespace macros
#import "GDataDefines.h"
#endif
#undef _EXTERN
#undef _INITIALIZE_AS
#ifdef GDATAXMLNODE_DEFINE_GLOBALS
#define _EXTERN
#define _INITIALIZE_AS(x) =x
#else
#if defined(__cplusplus)
#define _EXTERN extern "C"
#else
#define _EXTERN extern
#endif
#define _INITIALIZE_AS(x)
#endif
// when no namespace dictionary is supplied for XPath, the default namespace
// for the evaluated tree is registered with the prefix _def_ns
_EXTERN const char* kGDataXMLXPathDefaultNamespacePrefix _INITIALIZE_AS("_def_ns");
// Nomenclature for method names:
//
// Node = GData node
// XMLNode = xmlNodePtr
//
// So, for example:
// + (id)nodeConsumingXMLNode:(xmlNodePtr)theXMLNode;
@class NSArray, NSDictionary, NSError, NSString, NSURL;
@class GDataXMLElement, GDataXMLDocument;
enum {
GDataXMLInvalidKind = 0,
GDataXMLDocumentKind,
GDataXMLElementKind,
GDataXMLAttributeKind,
GDataXMLNamespaceKind,
GDataXMLProcessingInstructionKind,
GDataXMLCommentKind,
GDataXMLTextKind,
GDataXMLDTDKind,
GDataXMLEntityDeclarationKind,
GDataXMLAttributeDeclarationKind,
GDataXMLElementDeclarationKind,
GDataXMLNotationDeclarationKind
};
typedef NSUInteger GDataXMLNodeKind;
@interface GDataXMLNode : NSObject <NSCopying> {
@protected
// NSXMLNodes can have a namespace URI or prefix even if not part
// of a tree; xmlNodes cannot. When we create nodes apart from
// a tree, we'll store the dangling prefix or URI in the xmlNode's name,
// like
// "prefix:name"
// or
// "{http://uri}:name"
//
// We will fix up the node's namespace and name (and those of any children)
// later when adding the node to a tree with addChild: or addAttribute:.
// See fixUpNamespacesForNode:.
xmlNodePtr xmlNode_; // may also be an xmlAttrPtr or xmlNsPtr
BOOL shouldFreeXMLNode_; // if yes, xmlNode_ will be free'd in dealloc
// cached values
NSString *cachedName_;
NSArray *cachedChildren_;
NSArray *cachedAttributes_;
}
+ (GDataXMLElement *)elementWithName:(NSString *)name;
+ (GDataXMLElement *)elementWithName:(NSString *)name stringValue:(NSString *)value;
+ (GDataXMLElement *)elementWithName:(NSString *)name URI:(NSString *)value;
+ (id)attributeWithName:(NSString *)name stringValue:(NSString *)value;
+ (id)attributeWithName:(NSString *)name URI:(NSString *)attributeURI stringValue:(NSString *)value;
+ (id)namespaceWithName:(NSString *)name stringValue:(NSString *)value;
+ (id)textWithStringValue:(NSString *)value;
- (NSString *)stringValue;
- (void)setStringValue:(NSString *)str;
- (NSUInteger)childCount;
- (NSArray *)children;
- (GDataXMLNode *)childAtIndex:(unsigned)index;
- (NSString *)localName;
- (NSString *)name;
- (NSString *)prefix;
- (NSString *)URI;
- (GDataXMLNodeKind)kind;
- (NSString *)XMLString;
+ (NSString *)localNameForName:(NSString *)name;
+ (NSString *)prefixForName:(NSString *)name;
// This is the preferred entry point for nodesForXPath. This takes an explicit
// namespace dictionary (keys are prefixes, values are URIs).
- (NSArray *)nodesForXPath:(NSString *)xpath namespaces:(NSDictionary *)namespaces error:(NSError **)error;
// This implementation of nodesForXPath registers namespaces only from the
// document's root node. _def_ns may be used as a prefix for the default
// namespace, though there's no guarantee that the default namespace will
// be consistenly the same namespace in server responses.
- (NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error;
// access to the underlying libxml node; be sure to release the cached values
// if you change the underlying tree at all
- (xmlNodePtr)XMLNode;
- (void)releaseCachedValues;
@end
@interface GDataXMLElement : GDataXMLNode
- (id)initWithXMLString:(NSString *)str error:(NSError **)error;
- (NSArray *)namespaces;
- (void)setNamespaces:(NSArray *)namespaces;
- (void)addNamespace:(GDataXMLNode *)aNamespace;
// addChild adds a copy of the child node to the element
- (void)addChild:(GDataXMLNode *)child;
- (void)removeChild:(GDataXMLNode *)child;
- (NSArray *)elementsForName:(NSString *)name;
- (NSArray *)elementsForLocalName:(NSString *)localName URI:(NSString *)URI;
- (NSArray *)attributes;
- (GDataXMLNode *)attributeForName:(NSString *)name;
- (GDataXMLNode *)attributeForLocalName:(NSString *)name URI:(NSString *)attributeURI;
- (void)addAttribute:(GDataXMLNode *)attribute;
- (NSString *)resolvePrefixForNamespaceURI:(NSString *)namespaceURI;
@end
@interface GDataXMLDocument : NSObject {
@protected
xmlDoc* xmlDoc_; // strong; always free'd in dealloc
}
- (id)initWithXMLString:(NSString *)str options:(unsigned int)mask error:(NSError **)error;
- (id)initWithData:(NSData *)data options:(unsigned int)mask error:(NSError **)error;
// initWithRootElement uses a copy of the argument as the new document's root
- (id)initWithRootElement:(GDataXMLElement *)element;
- (GDataXMLElement *)rootElement;
- (NSData *)XMLData;
- (void)setVersion:(NSString *)version;
- (void)setCharacterEncoding:(NSString *)encoding;
// This is the preferred entry point for nodesForXPath. This takes an explicit
// namespace dictionary (keys are prefixes, values are URIs).
- (NSArray *)nodesForXPath:(NSString *)xpath namespaces:(NSDictionary *)namespaces error:(NSError **)error;
// This implementation of nodesForXPath registers namespaces only from the
// document's root node. _def_ns may be used as a prefix for the default
// namespace, though there's no guarantee that the default namespace will
// be consistenly the same namespace in server responses.
- (NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error;
- (NSString *)description;
@end
/* Copyright (c) 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define GDATAXMLNODE_DEFINE_GLOBALS 1
#import "GDataXMLNode.h"
@class NSArray, NSDictionary, NSError, NSString, NSURL;
@class GDataXMLElement, GDataXMLDocument;
static const int kGDataXMLParseOptions = (XML_PARSE_NOCDATA | XML_PARSE_NOBLANKS);
// dictionary key callbacks for string cache
static const void *StringCacheKeyRetainCallBack(CFAllocatorRef allocator, const void *str);
static void StringCacheKeyReleaseCallBack(CFAllocatorRef allocator, const void *str);
static CFStringRef StringCacheKeyCopyDescriptionCallBack(const void *str);
static Boolean StringCacheKeyEqualCallBack(const void *str1, const void *str2);
static CFHashCode StringCacheKeyHashCallBack(const void *str);
// isEqual: has the fatal flaw that it doesn't deal well with the received
// being nil. We'll use this utility instead.
// Static copy of AreEqualOrBothNil from GDataObject.m, so that using
// GDataXMLNode does not require pulling in all of GData.
static BOOL AreEqualOrBothNilPrivate(id obj1, id obj2) {
if (obj1 == obj2) {
return YES;
}
if (obj1 && obj2) {
return [obj1 isEqual:obj2];
}
return NO;
}
// convert NSString* to xmlChar*
//
// the "Get" part implies that ownership remains with str
static xmlChar* GDataGetXMLString(NSString *str) {
xmlChar* result = (xmlChar *)[str UTF8String];
return result;
}
// Make a fake qualified name we use as local name internally in libxml
// data structures when there's no actual namespace node available to point to
// from an element or attribute node
//
// Returns an autoreleased NSString*
static NSString *GDataFakeQNameForURIAndName(NSString *theURI, NSString *name) {
NSString *localName = [GDataXMLNode localNameForName:name];
NSString *fakeQName = [NSString stringWithFormat:@"{%@}:%@",
theURI, localName];
return fakeQName;
}
// libxml2 offers xmlSplitQName2, but that searches forwards. Since we may
// be searching for a whole URI shoved in as a prefix, like
// {http://foo}:name
// we'll search for the prefix in backwards from the end of the qualified name
//
// returns a copy of qname as the local name if there's no prefix
static xmlChar *SplitQNameReverse(const xmlChar *qname, xmlChar **prefix) {
// search backwards for a colon
int qnameLen = xmlStrlen(qname);
for (int idx = qnameLen - 1; idx >= 0; idx--) {
if (qname[idx] == ':') {
// found the prefix; copy the prefix, if requested
if (prefix != NULL) {
if (idx > 0) {
*prefix = xmlStrsub(qname, 0, idx);
} else {
*prefix = NULL;
}
}
if (idx < qnameLen - 1) {
// return a copy of the local name
xmlChar *localName = xmlStrsub(qname, idx + 1, qnameLen - idx - 1);
return localName;
} else {
return NULL;
}
}
}
// no colon found, so the qualified name is the local name
xmlChar *qnameCopy = xmlStrdup(qname);
return qnameCopy;
}
@interface GDataXMLNode (PrivateMethods)
// consuming a node implies it will later be freed when the instance is
// dealloc'd; borrowing it implies that ownership and disposal remain the
// job of the supplier of the node
+ (id)nodeConsumingXMLNode:(xmlNodePtr)theXMLNode;
- (id)initConsumingXMLNode:(xmlNodePtr)theXMLNode;
+ (id)nodeBorrowingXMLNode:(xmlNodePtr)theXMLNode;
- (id)initBorrowingXMLNode:(xmlNodePtr)theXMLNode;
// getters of the underlying node
- (xmlNodePtr)XMLNode;
- (xmlNodePtr)XMLNodeCopy;
// search for an underlying attribute
- (GDataXMLNode *)attributeForXMLNode:(xmlAttrPtr)theXMLNode;
// return an NSString for an xmlChar*, using our strings cache in the
// document
- (NSString *)stringFromXMLString:(const xmlChar *)chars;
// setter/getter of the dealloc flag for the underlying node
- (BOOL)shouldFreeXMLNode;
- (void)setShouldFreeXMLNode:(BOOL)flag;
@end
@interface GDataXMLElement (PrivateMethods)
+ (void)fixUpNamespacesForNode:(xmlNodePtr)nodeToFix
graftingToTreeNode:(xmlNodePtr)graftPointNode;
@end
@implementation GDataXMLNode
+ (void)load {
xmlInitParser();
}
// Note on convenience methods for making stand-alone element and
// attribute nodes:
//
// Since we're making a node from scratch, we don't
// have any namespace info. So the namespace prefix, if
// any, will just be slammed into the node name.
// We'll rely on the -addChild method below to remove
// the namespace prefix and replace it with a proper ns
// pointer.
+ (GDataXMLElement *)elementWithName:(NSString *)name {
xmlNodePtr theNewNode = xmlNewNode(NULL, // namespace
GDataGetXMLString(name));
if (theNewNode) {
// succeeded
return [self nodeConsumingXMLNode:theNewNode];
}
return nil;
}
+ (GDataXMLElement *)elementWithName:(NSString *)name stringValue:(NSString *)value {
xmlNodePtr theNewNode = xmlNewNode(NULL, // namespace
GDataGetXMLString(name));
if (theNewNode) {
xmlNodePtr textNode = xmlNewText(GDataGetXMLString(value));
if (textNode) {
xmlNodePtr temp = xmlAddChild(theNewNode, textNode);
if (temp) {
// succeeded
return [self nodeConsumingXMLNode:theNewNode];
}
}
// failed; free the node and any children
xmlFreeNode(theNewNode);
}
return nil;
}
+ (GDataXMLElement *)elementWithName:(NSString *)name URI:(NSString *)theURI {
// since we don't know a prefix yet, shove in the whole URI; we'll look for
// a proper namespace ptr later when addChild calls fixUpNamespacesForNode
NSString *fakeQName = GDataFakeQNameForURIAndName(theURI, name);
xmlNodePtr theNewNode = xmlNewNode(NULL, // namespace
GDataGetXMLString(fakeQName));
if (theNewNode) {
return [self nodeConsumingXMLNode:theNewNode];
}
return nil;
}
+ (id)attributeWithName:(NSString *)name stringValue:(NSString *)value {
xmlChar *xmlName = GDataGetXMLString(name);
xmlChar *xmlValue = GDataGetXMLString(value);
xmlAttrPtr theNewAttr = xmlNewProp(NULL, // parent node for the attr
xmlName, xmlValue);
if (theNewAttr) {
return [self nodeConsumingXMLNode:(xmlNodePtr) theNewAttr];
}
return nil;
}
+ (id)attributeWithName:(NSString *)name URI:(NSString *)attributeURI stringValue:(NSString *)value {
// since we don't know a prefix yet, shove in the whole URI; we'll look for
// a proper namespace ptr later when addChild calls fixUpNamespacesForNode
NSString *fakeQName = GDataFakeQNameForURIAndName(attributeURI, name);
xmlChar *xmlName = GDataGetXMLString(fakeQName);
xmlChar *xmlValue = GDataGetXMLString(value);
xmlAttrPtr theNewAttr = xmlNewProp(NULL, // parent node for the attr
xmlName, xmlValue);
if (theNewAttr) {
return [self nodeConsumingXMLNode:(xmlNodePtr) theNewAttr];
}
return nil;
}
+ (id)textWithStringValue:(NSString *)value {
xmlNodePtr theNewText = xmlNewText(GDataGetXMLString(value));
if (theNewText) {
return [self nodeConsumingXMLNode:theNewText];
}
return nil;
}
+ (id)namespaceWithName:(NSString *)name stringValue:(NSString *)value {
xmlChar *href = GDataGetXMLString(value);
xmlChar *prefix;
if ([name length] > 0) {
prefix = GDataGetXMLString(name);
} else {
// default namespace is represented by a nil prefix
prefix = nil;
}
xmlNsPtr theNewNs = xmlNewNs(NULL, // parent node
href, prefix);
if (theNewNs) {
return [self nodeConsumingXMLNode:(xmlNodePtr) theNewNs];
}
return nil;
}
+ (id)nodeConsumingXMLNode:(xmlNodePtr)theXMLNode {
Class theClass;
if (theXMLNode->type == XML_ELEMENT_NODE) {
theClass = [GDataXMLElement class];
} else {
theClass = [GDataXMLNode class];
}
return [[[theClass alloc] initConsumingXMLNode:theXMLNode] autorelease];
}
- (id)initConsumingXMLNode:(xmlNodePtr)theXMLNode {
self = [super init];
if (self) {
xmlNode_ = theXMLNode;
shouldFreeXMLNode_ = YES;
}
return self;
}
+ (id)nodeBorrowingXMLNode:(xmlNodePtr)theXMLNode {
Class theClass;
if (theXMLNode->type == XML_ELEMENT_NODE) {
theClass = [GDataXMLElement class];
} else {
theClass = [GDataXMLNode class];
}
return [[[theClass alloc] initBorrowingXMLNode:theXMLNode] autorelease];
}
- (id)initBorrowingXMLNode:(xmlNodePtr)theXMLNode {
self = [super init];
if (self) {
xmlNode_ = theXMLNode;
shouldFreeXMLNode_ = NO;
}
return self;
}
- (void)releaseCachedValues {
[cachedName_ release];
cachedName_ = nil;
[cachedChildren_ release];
cachedChildren_ = nil;
[cachedAttributes_ release];
cachedAttributes_ = nil;
}
// convert xmlChar* to NSString*
//
// returns an autoreleased NSString*, from the current node's document strings
// cache if possible
- (NSString *)stringFromXMLString:(const xmlChar *)chars {
#if DEBUG
NSCAssert(chars != NULL, @"GDataXMLNode sees an unexpected empty string");
#endif
if (chars == NULL) return nil;
CFMutableDictionaryRef cacheDict = NULL;
NSString *result = nil;
if (xmlNode_ != NULL
&& (xmlNode_->type == XML_ELEMENT_NODE
|| xmlNode_->type == XML_ATTRIBUTE_NODE
|| xmlNode_->type == XML_TEXT_NODE)) {
// there is no xmlDocPtr in XML_NAMESPACE_DECL nodes,
// so we can't cache the text of those
// look for a strings cache in the document
//
// the cache is in the document's user-defined _private field
if (xmlNode_->doc != NULL) {
cacheDict = xmlNode_->doc->_private;
if (cacheDict) {
// this document has a strings cache
result = (NSString *) CFDictionaryGetValue(cacheDict, chars);
if (result) {
// we found the xmlChar string in the cache; return the previously
// allocated NSString, rather than allocate a new one
return result;
}
}
}
}
// allocate a new NSString for this xmlChar*
result = [NSString stringWithUTF8String:(const char *) chars];
if (cacheDict) {
// save the string in the document's string cache
CFDictionarySetValue(cacheDict, chars, result);
}
return result;
}
- (void)dealloc {
if (xmlNode_ && shouldFreeXMLNode_) {
xmlFreeNode(xmlNode_);
xmlNode_ = NULL;
}
[self releaseCachedValues];
[super dealloc];
}
#pragma mark -
- (void)setStringValue:(NSString *)str {
if (xmlNode_ != NULL && str != nil) {
if (xmlNode_->type == XML_NAMESPACE_DECL) {
// for a namespace node, the value is the namespace URI
xmlNsPtr nsNode = (xmlNsPtr)xmlNode_;
if (nsNode->href != NULL) xmlFree((char *)nsNode->href);
nsNode->href = xmlStrdup(GDataGetXMLString(str));
} else {
// attribute or element node
// do we need to call xmlEncodeSpecialChars?
xmlNodeSetContent(xmlNode_, GDataGetXMLString(str));
}
}
}
- (NSString *)stringValue {
NSString *str = nil;
if (xmlNode_ != NULL) {
if (xmlNode_->type == XML_NAMESPACE_DECL) {
// for a namespace node, the value is the namespace URI
xmlNsPtr nsNode = (xmlNsPtr)xmlNode_;
str = [self stringFromXMLString:(nsNode->href)];
} else {
// attribute or element node
xmlChar* chars = xmlNodeGetContent(xmlNode_);
if (chars) {
str = [self stringFromXMLString:chars];
xmlFree(chars);
}
}
}
return str;
}
- (NSString *)XMLString {
NSString *str = nil;
if (xmlNode_ != NULL) {
xmlBufferPtr buff = xmlBufferCreate();
if (buff) {
xmlDocPtr doc = NULL;
int level = 0;
int format = 0;
int result = xmlNodeDump(buff, doc, xmlNode_, level, format);
if (result > -1) {
str = [[[NSString alloc] initWithBytes:(xmlBufferContent(buff))
length:(xmlBufferLength(buff))
encoding:NSUTF8StringEncoding] autorelease];
}
xmlBufferFree(buff);
}
}
// remove leading and trailing whitespace
NSCharacterSet *ws = [NSCharacterSet whitespaceAndNewlineCharacterSet];
NSString *trimmed = [str stringByTrimmingCharactersInSet:ws];
return trimmed;
}
- (NSString *)localName {
NSString *str = nil;
if (xmlNode_ != NULL) {
str = [self stringFromXMLString:(xmlNode_->name)];
// if this is part of a detached subtree, str may have a prefix in it
str = [[self class] localNameForName:str];
}
return str;
}
- (NSString *)prefix {
NSString *str = nil;
if (xmlNode_ != NULL) {
// the default namespace's prefix is an empty string, though libxml
// represents it as NULL for ns->prefix
str = @"";
if (xmlNode_->ns != NULL && xmlNode_->ns->prefix != NULL) {
str = [self stringFromXMLString:(xmlNode_->ns->prefix)];
}
}
return str;
}
- (NSString *)URI {
NSString *str = nil;
if (xmlNode_ != NULL) {
if (xmlNode_->ns != NULL && xmlNode_->ns->href != NULL) {
str = [self stringFromXMLString:(xmlNode_->ns->href)];
}
}
return str;
}
- (NSString *)qualifiedName {
// internal utility
NSString *str = nil;
if (xmlNode_ != NULL) {
if (xmlNode_->type == XML_NAMESPACE_DECL) {
// name of a namespace node
xmlNsPtr nsNode = (xmlNsPtr)xmlNode_;
// null is the default namespace; one is the loneliest number
if (nsNode->prefix == NULL) {
str = @"";
}
else {
str = [self stringFromXMLString:(nsNode->prefix)];
}
} else if (xmlNode_->ns != NULL && xmlNode_->ns->prefix != NULL) {
// name of a non-namespace node
// has a prefix
char *qname;
if (asprintf(&qname, "%s:%s", (const char *)xmlNode_->ns->prefix,
xmlNode_->name) != -1) {
str = [self stringFromXMLString:(const xmlChar *)qname];
free(qname);
}
} else {
// lacks a prefix
str = [self stringFromXMLString:(xmlNode_->name)];
}
}
return str;
}
- (NSString *)name {
if (cachedName_ != nil) {
return cachedName_;
}
NSString *str = [self qualifiedName];
cachedName_ = [str retain];
return str;
}
+ (NSString *)localNameForName:(NSString *)name {
if (name != nil) {
NSRange range = [name rangeOfString:@":"];
if (range.location != NSNotFound) {
// found a colon
if (range.location + 1 < [name length]) {
NSString *localName = [name substringFromIndex:(range.location + 1)];
return localName;
}
}
}
return name;
}
+ (NSString *)prefixForName:(NSString *)name {
if (name != nil) {
NSRange range = [name rangeOfString:@":"];
if (range.location != NSNotFound) {
NSString *prefix = [name substringToIndex:(range.location)];
return prefix;
}
}
return nil;
}
- (NSUInteger)childCount {
if (cachedChildren_ != nil) {
return [cachedChildren_ count];
}
if (xmlNode_ != NULL) {
unsigned int count = 0;
xmlNodePtr currChild = xmlNode_->children;
while (currChild != NULL) {
++count;
currChild = currChild->next;
}
return count;
}
return 0;
}
- (NSArray *)children {
if (cachedChildren_ != nil) {
return cachedChildren_;
}
NSMutableArray *array = nil;
if (xmlNode_ != NULL) {
xmlNodePtr currChild = xmlNode_->children;
while (currChild != NULL) {
GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:currChild];
if (array == nil) {
array = [NSMutableArray arrayWithObject:node];
} else {
[array addObject:node];
}
currChild = currChild->next;
}
cachedChildren_ = [array retain];
}
return array;
}
- (GDataXMLNode *)childAtIndex:(unsigned)index {
NSArray *children = [self children];
if ([children count] > index) {
return [children objectAtIndex:index];
}
return nil;
}
- (GDataXMLNodeKind)kind {
if (xmlNode_ != NULL) {
xmlElementType nodeType = xmlNode_->type;
switch (nodeType) {
case XML_ELEMENT_NODE: return GDataXMLElementKind;
case XML_ATTRIBUTE_NODE: return GDataXMLAttributeKind;
case XML_TEXT_NODE: return GDataXMLTextKind;
case XML_CDATA_SECTION_NODE: return GDataXMLTextKind;
case XML_ENTITY_REF_NODE: return GDataXMLEntityDeclarationKind;
case XML_ENTITY_NODE: return GDataXMLEntityDeclarationKind;
case XML_PI_NODE: return GDataXMLProcessingInstructionKind;
case XML_COMMENT_NODE: return GDataXMLCommentKind;
case XML_DOCUMENT_NODE: return GDataXMLDocumentKind;
case XML_DOCUMENT_TYPE_NODE: return GDataXMLDocumentKind;
case XML_DOCUMENT_FRAG_NODE: return GDataXMLDocumentKind;
case XML_NOTATION_NODE: return GDataXMLNotationDeclarationKind;
case XML_HTML_DOCUMENT_NODE: return GDataXMLDocumentKind;
case XML_DTD_NODE: return GDataXMLDTDKind;
case XML_ELEMENT_DECL: return GDataXMLElementDeclarationKind;
case XML_ATTRIBUTE_DECL: return GDataXMLAttributeDeclarationKind;
case XML_ENTITY_DECL: return GDataXMLEntityDeclarationKind;
case XML_NAMESPACE_DECL: return GDataXMLNamespaceKind;
case XML_XINCLUDE_START: return GDataXMLProcessingInstructionKind;
case XML_XINCLUDE_END: return GDataXMLProcessingInstructionKind;
case XML_DOCB_DOCUMENT_NODE: return GDataXMLDocumentKind;
}
}
return GDataXMLInvalidKind;
}
- (NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error {
// call through with no explicit namespace dictionary; that will register the
// root node's namespaces
return [self nodesForXPath:xpath namespaces:nil error:error];
}
- (NSArray *)nodesForXPath:(NSString *)xpath
namespaces:(NSDictionary *)namespaces
error:(NSError **)error {
NSMutableArray *array = nil;
NSInteger errorCode = -1;
NSDictionary *errorInfo = nil;
// xmlXPathNewContext requires a doc for its context, but if our elements
// are created from GDataXMLElement's initWithXMLString there may not be
// a document. (We may later decide that we want to stuff the doc used
// there into a GDataXMLDocument and retain it, but we don't do that now.)
//
// We'll temporarily make a document to use for the xpath context.
xmlDocPtr tempDoc = NULL;
xmlNodePtr topParent = NULL;
if (xmlNode_->doc == NULL) {
tempDoc = xmlNewDoc(NULL);
if (tempDoc) {
// find the topmost node of the current tree to make the root of
// our temporary document
topParent = xmlNode_;
while (topParent->parent != NULL) {
topParent = topParent->parent;
}
xmlDocSetRootElement(tempDoc, topParent);
}
}
if (xmlNode_ != NULL && xmlNode_->doc != NULL) {
xmlXPathContextPtr xpathCtx = xmlXPathNewContext(xmlNode_->doc);
if (xpathCtx) {
// anchor at our current node
xpathCtx->node = xmlNode_;
// if a namespace dictionary was provided, register its contents
if (namespaces) {
// the dictionary keys are prefixes; the values are URIs
for (NSString *prefix in namespaces) {
NSString *uri = [namespaces objectForKey:prefix];
xmlChar *prefixChars = (xmlChar *) [prefix UTF8String];
xmlChar *uriChars = (xmlChar *) [uri UTF8String];
int result = xmlXPathRegisterNs(xpathCtx, prefixChars, uriChars);
if (result != 0) {
#if DEBUG
NSCAssert1(result == 0, @"GDataXMLNode XPath namespace %@ issue",
prefix);
#endif
}
}
} else {
// no namespace dictionary was provided
//
// register the namespaces of this node, if it's an element, or of
// this node's root element, if it's a document
xmlNodePtr nsNodePtr = xmlNode_;
if (xmlNode_->type == XML_DOCUMENT_NODE) {
nsNodePtr = xmlDocGetRootElement((xmlDocPtr) xmlNode_);
}
// step through the namespaces, if any, and register each with the
// xpath context
if (nsNodePtr != NULL) {
for (xmlNsPtr nsPtr = nsNodePtr->ns; nsPtr != NULL; nsPtr = nsPtr->next) {
// default namespace is nil in the tree, but there's no way to
// register a default namespace, so we'll register a fake one,
// _def_ns
const xmlChar* prefix = nsPtr->prefix;
if (prefix == NULL) {
prefix = (xmlChar*) kGDataXMLXPathDefaultNamespacePrefix;
}
int result = xmlXPathRegisterNs(xpathCtx, prefix, nsPtr->href);
if (result != 0) {
#if DEBUG
NSCAssert1(result == 0, @"GDataXMLNode XPath namespace %@ issue",
prefix);
#endif
}
}
}
}
// now evaluate the path
xmlXPathObjectPtr xpathObj;
xpathObj = xmlXPathEval(GDataGetXMLString(xpath), xpathCtx);
if (xpathObj) {
// we have some result from the search
array = [NSMutableArray array];
xmlNodeSetPtr nodeSet = xpathObj->nodesetval;
if (nodeSet) {
// add each node in the result set to our array
for (int index = 0; index < nodeSet->nodeNr; index++) {
xmlNodePtr currNode = nodeSet->nodeTab[index];
GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:currNode];
if (node) {
[array addObject:node];
}
}
}
xmlXPathFreeObject(xpathObj);
} else {
// provide an error for failed evaluation
const char *msg = xpathCtx->lastError.str1;
errorCode = xpathCtx->lastError.code;
if (msg) {
NSString *errStr = [NSString stringWithUTF8String:msg];
errorInfo = [NSDictionary dictionaryWithObject:errStr
forKey:@"error"];
}
}
xmlXPathFreeContext(xpathCtx);
}
} else {
// not a valid node for using XPath
errorInfo = [NSDictionary dictionaryWithObject:@"invalid node"
forKey:@"error"];
}
if (array == nil && error != nil) {
*error = [NSError errorWithDomain:@"com.google.GDataXML"
code:errorCode
userInfo:errorInfo];
}
if (tempDoc != NULL) {
xmlUnlinkNode(topParent);
xmlSetTreeDoc(topParent, NULL);
xmlFreeDoc(tempDoc);
}
return array;
}
- (NSString *)description {
int nodeType = (xmlNode_ ? (int)xmlNode_->type : -1);
return [NSString stringWithFormat:@"%@ %p: {type:%d name:%@ xml:"%@"}",
[self class], self, nodeType, [self name], [self XMLString]];
}
- (id)copyWithZone:(NSZone *)zone {
xmlNodePtr nodeCopy = [self XMLNodeCopy];
if (nodeCopy != NULL) {
return [[[self class] alloc] initConsumingXMLNode:nodeCopy];
}
return nil;
}
- (BOOL)isEqual:(GDataXMLNode *)other {
if (self == other) return YES;
if (![other isKindOfClass:[GDataXMLNode class]]) return NO;
return [self XMLNode] == [other XMLNode]
|| ([self kind] == [other kind]
&& AreEqualOrBothNilPrivate([self name], [other name])
&& [[self children] count] == [[other children] count]);
}
- (NSUInteger)hash {
return (NSUInteger) (void *) [GDataXMLNode class];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [super methodSignatureForSelector:selector];
}
#pragma mark -
- (xmlNodePtr)XMLNodeCopy {
if (xmlNode_ != NULL) {
// Note: libxml will create a new copy of namespace nodes (xmlNs records)
// and attach them to this copy in order to keep namespaces within this
// node subtree copy value.
xmlNodePtr nodeCopy = xmlCopyNode(xmlNode_, 1); // 1 = recursive
return nodeCopy;
}
return NULL;
}
- (xmlNodePtr)XMLNode {
return xmlNode_;
}
- (BOOL)shouldFreeXMLNode {
return shouldFreeXMLNode_;
}
- (void)setShouldFreeXMLNode:(BOOL)flag {
shouldFreeXMLNode_ = flag;
}
@end
@implementation GDataXMLElement
- (id)initWithXMLString:(NSString *)str error:(NSError **)error {
self = [super init];
if (self) {
const char *utf8Str = [str UTF8String];
// NOTE: We are assuming a string length that fits into an int
xmlDocPtr doc = xmlReadMemory(utf8Str, (int)strlen(utf8Str), NULL, // URL
NULL, // encoding
kGDataXMLParseOptions);
if (doc == NULL) {
if (error) {
// TODO(grobbins) use xmlSetGenericErrorFunc to capture error
}
} else {
// copy the root node from the doc
xmlNodePtr root = xmlDocGetRootElement(doc);
if (root) {
xmlNode_ = xmlCopyNode(root, 1); // 1: recursive
shouldFreeXMLNode_ = YES;
}
xmlFreeDoc(doc);
}
if (xmlNode_ == NULL) {
// failure
if (error) {
*error = [NSError errorWithDomain:@"com.google.GDataXML"
code:-1
userInfo:nil];
}
[self release];
return nil;
}
}
return self;
}
- (NSArray *)namespaces {
NSMutableArray *array = nil;
if (xmlNode_ != NULL && xmlNode_->nsDef != NULL) {
xmlNsPtr currNS = xmlNode_->nsDef;
while (currNS != NULL) {
// add this prefix/URI to the list, unless it's the implicit xml prefix
if (!xmlStrEqual(currNS->prefix, (const xmlChar *) "xml")) {
GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:(xmlNodePtr) currNS];
if (array == nil) {
array = [NSMutableArray arrayWithObject:node];
} else {
[array addObject:node];
}
}
currNS = currNS->next;
}
}
return array;
}
- (void)setNamespaces:(NSArray *)namespaces {
if (xmlNode_ != NULL) {
[self releaseCachedValues];
// remove previous namespaces
if (xmlNode_->nsDef) {
xmlFreeNsList(xmlNode_->nsDef);
xmlNode_->nsDef = NULL;
}
// add a namespace for each object in the array
NSEnumerator *enumerator = [namespaces objectEnumerator];
GDataXMLNode *namespaceNode;
while ((namespaceNode = [enumerator nextObject]) != nil) {
xmlNsPtr ns = (xmlNsPtr) [namespaceNode XMLNode];
if (ns) {
(void)xmlNewNs(xmlNode_, ns->href, ns->prefix);
}
}
// we may need to fix this node's own name; the graft point is where
// the namespace search starts, so that points to this node too
[[self class] fixUpNamespacesForNode:xmlNode_
graftingToTreeNode:xmlNode_];
}
}
- (void)addNamespace:(GDataXMLNode *)aNamespace {
if (xmlNode_ != NULL) {
[self releaseCachedValues];
xmlNsPtr ns = (xmlNsPtr) [aNamespace XMLNode];
if (ns) {
(void)xmlNewNs(xmlNode_, ns->href, ns->prefix);
// we may need to fix this node's own name; the graft point is where
// the namespace search starts, so that points to this node too
[[self class] fixUpNamespacesForNode:xmlNode_
graftingToTreeNode:xmlNode_];
}
}
}
- (void)addChild:(GDataXMLNode *)child {
if ([child kind] == GDataXMLAttributeKind) {
[self addAttribute:child];
return;
}
if (xmlNode_ != NULL) {
[self releaseCachedValues];
xmlNodePtr childNodeCopy = [child XMLNodeCopy];
if (childNodeCopy) {
xmlNodePtr resultNode = xmlAddChild(xmlNode_, childNodeCopy);
if (resultNode == NULL) {
// failed to add
xmlFreeNode(childNodeCopy);
} else {
// added this child subtree successfully; see if it has
// previously-unresolved namespace prefixes that can now be fixed up
[[self class] fixUpNamespacesForNode:childNodeCopy
graftingToTreeNode:xmlNode_];
}
}
}
}
- (void)removeChild:(GDataXMLNode *)child {
// this is safe for attributes too
if (xmlNode_ != NULL) {
[self releaseCachedValues];
xmlNodePtr node = [child XMLNode];
xmlUnlinkNode(node);
// if the child node was borrowing its xmlNodePtr, then we need to
// explicitly free it, since there is probably no owning object that will
// free it on dealloc
if (![child shouldFreeXMLNode]) {
xmlFreeNode(node);
}
}
}
- (NSArray *)elementsForName:(NSString *)name {
NSString *desiredName = name;
if (xmlNode_ != NULL) {
NSString *prefix = [[self class] prefixForName:desiredName];
if (prefix) {
xmlChar* desiredPrefix = GDataGetXMLString(prefix);
xmlNsPtr foundNS = xmlSearchNs(xmlNode_->doc, xmlNode_, desiredPrefix);
if (foundNS) {
// we found a namespace; fall back on elementsForLocalName:URI:
// to get the elements
NSString *desiredURI = [self stringFromXMLString:(foundNS->href)];
NSString *localName = [[self class] localNameForName:desiredName];
NSArray *nsArray = [self elementsForLocalName:localName URI:desiredURI];
return nsArray;
}
}
// no namespace found for the node's prefix; try an exact match
// for the name argument, including any prefix
NSMutableArray *array = nil;
// walk our list of cached child nodes
NSArray *children = [self children];
for (GDataXMLNode *child in children) {
xmlNodePtr currNode = [child XMLNode];
// find all children which are elements with the desired name
if (currNode->type == XML_ELEMENT_NODE) {
NSString *qName = [child name];
if ([qName isEqual:name]) {
if (array == nil) {
array = [NSMutableArray arrayWithObject:child];
} else {
[array addObject:child];
}
}
}
}
return array;
}
return nil;
}
- (NSArray *)elementsForLocalName:(NSString *)localName URI:(NSString *)URI {
NSMutableArray *array = nil;
if (xmlNode_ != NULL && xmlNode_->children != NULL) {
xmlChar* desiredNSHref = GDataGetXMLString(URI);
xmlChar* requestedLocalName = GDataGetXMLString(localName);
xmlChar* expectedLocalName = requestedLocalName;
// resolve the URI at the parent level, since usually children won't
// have their own namespace definitions, and we don't want to try to
// resolve it once for every child
xmlNsPtr foundParentNS = xmlSearchNsByHref(xmlNode_->doc, xmlNode_, desiredNSHref);
if (foundParentNS == NULL) {
NSString *fakeQName = GDataFakeQNameForURIAndName(URI, localName);
expectedLocalName = GDataGetXMLString(fakeQName);
}
NSArray *children = [self children];
for (GDataXMLNode *child in children) {
xmlNodePtr currChildPtr = [child XMLNode];
// find all children which are elements with the desired name and
// namespace, or with the prefixed name and a null namespace
if (currChildPtr->type == XML_ELEMENT_NODE) {
// normally, we can assume the resolution done for the parent will apply
// to the child, as most children do not define their own namespaces
xmlNsPtr childLocalNS = foundParentNS;
xmlChar* childDesiredLocalName = expectedLocalName;
if (currChildPtr->nsDef != NULL) {
// this child has its own namespace definitons; do a fresh resolve
// of the namespace starting from the child, and see if it differs
// from the resolve done starting from the parent. If the resolve
// finds a different namespace, then override the desired local
// name just for this child.
childLocalNS = xmlSearchNsByHref(xmlNode_->doc, currChildPtr, desiredNSHref);
if (childLocalNS != foundParentNS) {
// this child does indeed have a different namespace resolution
// result than was found for its parent
if (childLocalNS == NULL) {
// no namespace found
NSString *fakeQName = GDataFakeQNameForURIAndName(URI, localName);
childDesiredLocalName = GDataGetXMLString(fakeQName);
} else {
// a namespace was found; use the original local name requested,
// not a faked one expected from resolving the parent
childDesiredLocalName = requestedLocalName;
}
}
}
// check if this child's namespace and local name are what we're
// seeking
if (currChildPtr->ns == childLocalNS
&& currChildPtr->name != NULL
&& xmlStrEqual(currChildPtr->name, childDesiredLocalName)) {
if (array == nil) {
array = [NSMutableArray arrayWithObject:child];
} else {
[array addObject:child];
}
}
}
}
// we return nil, not an empty array, according to docs
}
return array;
}
- (NSArray *)attributes {
if (cachedAttributes_ != nil) {
return cachedAttributes_;
}
NSMutableArray *array = nil;
if (xmlNode_ != NULL && xmlNode_->properties != NULL) {
xmlAttrPtr prop = xmlNode_->properties;
while (prop != NULL) {
GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:(xmlNodePtr) prop];
if (array == nil) {
array = [NSMutableArray arrayWithObject:node];
} else {
[array addObject:node];
}
prop = prop->next;
}
cachedAttributes_ = [array retain];
}
return array;
}
- (void)addAttribute:(GDataXMLNode *)attribute {
if (xmlNode_ != NULL) {
[self releaseCachedValues];
xmlAttrPtr attrPtr = (xmlAttrPtr) [attribute XMLNode];
if (attrPtr) {
// ignore this if an attribute with the name is already present,
// similar to NSXMLNode's addAttribute
xmlAttrPtr oldAttr;
if (attrPtr->ns == NULL) {
oldAttr = xmlHasProp(xmlNode_, attrPtr->name);
} else {
oldAttr = xmlHasNsProp(xmlNode_, attrPtr->name, attrPtr->ns->href);
}
if (oldAttr == NULL) {
xmlNsPtr newPropNS = NULL;
// if this attribute has a namespace, search for a matching namespace
// on the node we're adding to
if (attrPtr->ns != NULL) {
newPropNS = xmlSearchNsByHref(xmlNode_->doc, xmlNode_, attrPtr->ns->href);
if (newPropNS == NULL) {
// make a new namespace on the parent node, and use that for the
// new attribute
newPropNS = xmlNewNs(xmlNode_, attrPtr->ns->href, attrPtr->ns->prefix);
}
}
// copy the attribute onto this node
xmlChar *value = xmlNodeGetContent((xmlNodePtr) attrPtr);
xmlAttrPtr newProp = xmlNewNsProp(xmlNode_, newPropNS, attrPtr->name, value);
if (newProp != NULL) {
// we made the property, so clean up the property's namespace
[[self class] fixUpNamespacesForNode:(xmlNodePtr)newProp
graftingToTreeNode:xmlNode_];
}
if (value != NULL) {
xmlFree(value);
}
}
}
}
}
- (GDataXMLNode *)attributeForXMLNode:(xmlAttrPtr)theXMLNode {
// search the cached attributes list for the GDataXMLNode with
// the underlying xmlAttrPtr
NSArray *attributes = [self attributes];
for (GDataXMLNode *attr in attributes) {
if (theXMLNode == (xmlAttrPtr) [attr XMLNode]) {
return attr;
}
}
return nil;
}
- (GDataXMLNode *)attributeForName:(NSString *)name {
if (xmlNode_ != NULL) {
xmlAttrPtr attrPtr = xmlHasProp(xmlNode_, GDataGetXMLString(name));
if (attrPtr == NULL) {
// can we guarantee that xmlAttrPtrs always have the ns ptr and never
// a namespace as part of the actual attribute name?
// split the name and its prefix, if any
xmlNsPtr ns = NULL;
NSString *prefix = [[self class] prefixForName:name];
if (prefix) {
// find the namespace for this prefix, and search on its URI to find
// the xmlNsPtr
name = [[self class] localNameForName:name];
ns = xmlSearchNs(xmlNode_->doc, xmlNode_, GDataGetXMLString(prefix));
}
const xmlChar* nsURI = ((ns != NULL) ? ns->href : NULL);
attrPtr = xmlHasNsProp(xmlNode_, GDataGetXMLString(name), nsURI);
}
if (attrPtr) {
GDataXMLNode *attr = [self attributeForXMLNode:attrPtr];
return attr;
}
}
return nil;
}
- (GDataXMLNode *)attributeForLocalName:(NSString *)localName
URI:(NSString *)attributeURI {
if (xmlNode_ != NULL) {
const xmlChar* name = GDataGetXMLString(localName);
const xmlChar* nsURI = GDataGetXMLString(attributeURI);
xmlAttrPtr attrPtr = xmlHasNsProp(xmlNode_, name, nsURI);
if (attrPtr == NULL) {
// if the attribute is in a tree lacking the proper namespace,
// the local name may include the full URI as a prefix
NSString *fakeQName = GDataFakeQNameForURIAndName(attributeURI, localName);
const xmlChar* xmlFakeQName = GDataGetXMLString(fakeQName);
attrPtr = xmlHasProp(xmlNode_, xmlFakeQName);
}
if (attrPtr) {
GDataXMLNode *attr = [self attributeForXMLNode:attrPtr];
return attr;
}
}
return nil;
}
- (NSString *)resolvePrefixForNamespaceURI:(NSString *)namespaceURI {
if (xmlNode_ != NULL) {
xmlChar* desiredNSHref = GDataGetXMLString(namespaceURI);
xmlNsPtr foundNS = xmlSearchNsByHref(xmlNode_->doc, xmlNode_, desiredNSHref);
if (foundNS) {
// we found the namespace
if (foundNS->prefix != NULL) {
NSString *prefix = [self stringFromXMLString:(foundNS->prefix)];
return prefix;
} else {
// empty prefix is default namespace
return @"";
}
}
}
return nil;
}
#pragma mark Namespace fixup routines
+ (void)deleteNamespacePtr:(xmlNsPtr)namespaceToDelete
fromXMLNode:(xmlNodePtr)node {
// utilty routine to remove a namespace pointer from an element's
// namespace definition list. This is just removing the nsPtr
// from the singly-linked list, the node's namespace definitions.
xmlNsPtr currNS = node->nsDef;
xmlNsPtr prevNS = NULL;
while (currNS != NULL) {
xmlNsPtr nextNS = currNS->next;
if (namespaceToDelete == currNS) {
// found it; delete it from the head of the node's ns definition list
// or from the next field of the previous namespace
if (prevNS != NULL) prevNS->next = nextNS;
else node->nsDef = nextNS;
xmlFreeNs(currNS);
return;
}
prevNS = currNS;
currNS = nextNS;
}
}
+ (void)fixQualifiedNamesForNode:(xmlNodePtr)nodeToFix
graftingToTreeNode:(xmlNodePtr)graftPointNode {
// Replace prefix-in-name with proper namespace pointers
//
// This is an inner routine for fixUpNamespacesForNode:
//
// see if this node's name lacks a namespace and is qualified, and if so,
// see if we can resolve the prefix against the parent
//
// The prefix may either be normal, "gd:foo", or a URI
// "{http://blah.com/}:foo"
if (nodeToFix->ns == NULL) {
xmlNsPtr foundNS = NULL;
xmlChar* prefix = NULL;
xmlChar* localName = SplitQNameReverse(nodeToFix->name, &prefix);
if (localName != NULL) {
if (prefix != NULL) {
// if the prefix is wrapped by { and } then it's a URI
int prefixLen = xmlStrlen(prefix);
if (prefixLen > 2
&& prefix[0] == '{'
&& prefix[prefixLen - 1] == '}') {
// search for the namespace by URI
xmlChar* uri = xmlStrsub(prefix, 1, prefixLen - 2);
if (uri != NULL) {
foundNS = xmlSearchNsByHref(graftPointNode->doc, graftPointNode, uri);
xmlFree(uri);
}
}
}
if (foundNS == NULL) {
// search for the namespace by prefix, even if the prefix is nil
// (nil prefix means to search for the default namespace)
foundNS = xmlSearchNs(graftPointNode->doc, graftPointNode, prefix);
}
if (foundNS != NULL) {
// we found a namespace, so fix the ns pointer and the local name
xmlSetNs(nodeToFix, foundNS);
xmlNodeSetName(nodeToFix, localName);
}
if (prefix != NULL) {
xmlFree(prefix);
prefix = NULL;
}
xmlFree(localName);
}
}
}
+ (void)fixDuplicateNamespacesForNode:(xmlNodePtr)nodeToFix
graftingToTreeNode:(xmlNodePtr)graftPointNode
namespaceSubstitutionMap:(NSMutableDictionary *)nsMap {
// Duplicate namespace removal
//
// This is an inner routine for fixUpNamespacesForNode:
//
// If any of this node's namespaces are already defined at the graft point
// level, add that namespace to the map of namespace substitutions
// so it will be replaced in the children below the nodeToFix, and
// delete the namespace record
if (nodeToFix->type == XML_ELEMENT_NODE) {
// step through the namespaces defined on this node
xmlNsPtr definedNS = nodeToFix->nsDef;
while (definedNS != NULL) {
// see if this namespace is already defined higher in the tree,
// with both the same URI and the same prefix; if so, add a mapping for
// it
xmlNsPtr foundNS = xmlSearchNsByHref(graftPointNode->doc, graftPointNode,
definedNS->href);
if (foundNS != NULL
&& foundNS != definedNS
&& xmlStrEqual(definedNS->prefix, foundNS->prefix)) {
// store a mapping from this defined nsPtr to the one found higher
// in the tree
[nsMap setObject:[NSValue valueWithPointer:foundNS]
forKey:[NSValue valueWithPointer:definedNS]];
// remove this namespace from the ns definition list of this node;
// all child elements and attributes referencing this namespace
// now have a dangling pointer and must be updated (that is done later
// in this method)
//
// before we delete this namespace, move our pointer to the
// next one
xmlNsPtr nsToDelete = definedNS;
definedNS = definedNS->next;
[self deleteNamespacePtr:nsToDelete fromXMLNode:nodeToFix];
} else {
// this namespace wasn't a duplicate; move to the next
definedNS = definedNS->next;
}
}
}
// if this node's namespace is one we deleted, update it to point
// to someplace better
if (nodeToFix->ns != NULL) {
NSValue *currNS = [NSValue valueWithPointer:nodeToFix->ns];
NSValue *replacementNS = [nsMap objectForKey:currNS];
if (replacementNS != nil) {
xmlNsPtr replaceNSPtr = (xmlNsPtr)[replacementNS pointerValue];
xmlSetNs(nodeToFix, replaceNSPtr);
}
}
}
+ (void)fixUpNamespacesForNode:(xmlNodePtr)nodeToFix
graftingToTreeNode:(xmlNodePtr)graftPointNode
namespaceSubstitutionMap:(NSMutableDictionary *)nsMap {
// This is the inner routine for fixUpNamespacesForNode:graftingToTreeNode:
//
// This routine fixes two issues:
//
// Because we can create nodes with qualified names before adding
// them to the tree that declares the namespace for the prefix,
// we need to set the node namespaces after adding them to the tree.
//
// Because libxml adds namespaces to nodes when it copies them,
// we want to remove redundant namespaces after adding them to
// a tree.
//
// If only the Mac's libxml had xmlDOMWrapReconcileNamespaces, it could do
// namespace cleanup for us
// We only care about fixing names of elements and attributes
if (nodeToFix->type != XML_ELEMENT_NODE
&& nodeToFix->type != XML_ATTRIBUTE_NODE) return;
// Do the fixes
[self fixQualifiedNamesForNode:nodeToFix
graftingToTreeNode:graftPointNode];
[self fixDuplicateNamespacesForNode:nodeToFix
graftingToTreeNode:graftPointNode
namespaceSubstitutionMap:nsMap];
if (nodeToFix->type == XML_ELEMENT_NODE) {
// when fixing element nodes, recurse for each child element and
// for each attribute
xmlNodePtr currChild = nodeToFix->children;
while (currChild != NULL) {
[self fixUpNamespacesForNode:currChild
graftingToTreeNode:graftPointNode
namespaceSubstitutionMap:nsMap];
currChild = currChild->next;
}
xmlAttrPtr currProp = nodeToFix->properties;
while (currProp != NULL) {
[self fixUpNamespacesForNode:(xmlNodePtr)currProp
graftingToTreeNode:graftPointNode
namespaceSubstitutionMap:nsMap];
currProp = currProp->next;
}
}
}
+ (void)fixUpNamespacesForNode:(xmlNodePtr)nodeToFix
graftingToTreeNode:(xmlNodePtr)graftPointNode {
// allocate the namespace map that will be passed
// down on recursive calls
NSMutableDictionary *nsMap = [NSMutableDictionary dictionary];
[self fixUpNamespacesForNode:nodeToFix
graftingToTreeNode:graftPointNode
namespaceSubstitutionMap:nsMap];
}
@end
@interface GDataXMLDocument (PrivateMethods)
- (void)addStringsCacheToDoc;
@end
@implementation GDataXMLDocument
- (id)initWithXMLString:(NSString *)str options:(unsigned int)mask error:(NSError **)error {
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
GDataXMLDocument *doc = [self initWithData:data options:mask error:error];
return doc;
}
- (id)initWithData:(NSData *)data options:(unsigned int)mask error:(NSError **)error {
self = [super init];
if (self) {
const char *baseURL = NULL;
const char *encoding = NULL;
// NOTE: We are assuming [data length] fits into an int.
xmlDoc_ = xmlReadMemory((const char*)[data bytes], (int)[data length], baseURL, encoding,
kGDataXMLParseOptions); // TODO(grobbins) map option values
if (xmlDoc_ == NULL) {
if (error) {
*error = [NSError errorWithDomain:@"com.google.GDataXML"
code:-1
userInfo:nil];
// TODO(grobbins) use xmlSetGenericErrorFunc to capture error
}
[self release];
return nil;
} else {
if (error) *error = NULL;
[self addStringsCacheToDoc];
}
}
return self;
}
- (id)initWithRootElement:(GDataXMLElement *)element {
self = [super init];
if (self) {
xmlDoc_ = xmlNewDoc(NULL);
(void) xmlDocSetRootElement(xmlDoc_, [element XMLNodeCopy]);
[self addStringsCacheToDoc];
}
return self;
}
- (void)addStringsCacheToDoc {
// utility routine for init methods
#if DEBUG
NSCAssert(xmlDoc_ != NULL && xmlDoc_->_private == NULL,
@"GDataXMLDocument cache creation problem");
#endif
// add a strings cache as private data for the document
//
// we'll use plain C pointers (xmlChar*) as the keys, and NSStrings
// as the values
CFIndex capacity = 0; // no limit
CFDictionaryKeyCallBacks keyCallBacks = {
0, // version
StringCacheKeyRetainCallBack,
StringCacheKeyReleaseCallBack,
StringCacheKeyCopyDescriptionCallBack,
StringCacheKeyEqualCallBack,
StringCacheKeyHashCallBack
};
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(
kCFAllocatorDefault, capacity,
&keyCallBacks, &kCFTypeDictionaryValueCallBacks);
// we'll use the user-defined _private field for our cache
xmlDoc_->_private = dict;
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@ %p", [self class], self];
}
- (void)dealloc {
if (xmlDoc_ != NULL) {
// release the strings cache
//
// since it's a CF object, were anyone to use this in a GC environment,
// this would need to be released in a finalize method, too
if (xmlDoc_->_private != NULL) {
CFRelease(xmlDoc_->_private);
}
xmlFreeDoc(xmlDoc_);
}
[super dealloc];
}
#pragma mark -
- (GDataXMLElement *)rootElement {
GDataXMLElement *element = nil;
if (xmlDoc_ != NULL) {
xmlNodePtr rootNode = xmlDocGetRootElement(xmlDoc_);
if (rootNode) {
element = [GDataXMLElement nodeBorrowingXMLNode:rootNode];
}
}
return element;
}
- (NSData *)XMLData {
if (xmlDoc_ != NULL) {
xmlChar *buffer = NULL;
int bufferSize = 0;
xmlDocDumpMemory(xmlDoc_, &buffer, &bufferSize);
if (buffer) {
NSData *data = [NSData dataWithBytes:buffer
length:bufferSize];
xmlFree(buffer);
return data;
}
}
return nil;
}
- (void)setVersion:(NSString *)version {
if (xmlDoc_ != NULL) {
if (xmlDoc_->version != NULL) {
// version is a const char* so we must cast
xmlFree((char *) xmlDoc_->version);
xmlDoc_->version = NULL;
}
if (version != nil) {
xmlDoc_->version = xmlStrdup(GDataGetXMLString(version));
}
}
}
- (void)setCharacterEncoding:(NSString *)encoding {
if (xmlDoc_ != NULL) {
if (xmlDoc_->encoding != NULL) {
// version is a const char* so we must cast
xmlFree((char *) xmlDoc_->encoding);
xmlDoc_->encoding = NULL;
}
if (encoding != nil) {
xmlDoc_->encoding = xmlStrdup(GDataGetXMLString(encoding));
}
}
}
- (NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error {
return [self nodesForXPath:xpath namespaces:nil error:error];
}
- (NSArray *)nodesForXPath:(NSString *)xpath
namespaces:(NSDictionary *)namespaces
error:(NSError **)error {
if (xmlDoc_ != NULL) {
GDataXMLNode *docNode = [GDataXMLElement nodeBorrowingXMLNode:(xmlNodePtr)xmlDoc_];
NSArray *array = [docNode nodesForXPath:xpath
namespaces:namespaces
error:error];
return array;
}
return nil;
}
@end
//
// Dictionary key callbacks for our C-string to NSString cache dictionary
//
static const void *StringCacheKeyRetainCallBack(CFAllocatorRef allocator, const void *str) {
// copy the key
xmlChar* key = xmlStrdup(str);
return key;
}
static void StringCacheKeyReleaseCallBack(CFAllocatorRef allocator, const void *str) {
// free the key
char *chars = (char *)str;
xmlFree((char *) chars);
}
static CFStringRef StringCacheKeyCopyDescriptionCallBack(const void *str) {
// make a CFString from the key
CFStringRef cfStr = CFStringCreateWithCString(kCFAllocatorDefault,
(const char *)str,
kCFStringEncodingUTF8);
return cfStr;
}
static Boolean StringCacheKeyEqualCallBack(const void *str1, const void *str2) {
// compare the key strings
if (str1 == str2) return true;
int result = xmlStrcmp(str1, str2);
return (result == 0);
}
static CFHashCode StringCacheKeyHashCallBack(const void *str) {
// dhb hash, per http://www.cse.yorku.ca/~oz/hash.html
CFHashCode hash = 5381;
int c;
const char *chars = (const char *)str;
while ((c = *chars++) != 0) {
hash = ((hash << 5) + hash) + c;
}
return hash;
}
/* Copyright (c) 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define GDATAXMLNODE_DEFINE_GLOBALS 1
#import "GDataXMLNode.h"
@class NSArray, NSDictionary, NSError, NSString, NSURL;
@class GDataXMLElement, GDataXMLDocument;
static const int kGDataXMLParseOptions = (XML_PARSE_NOCDATA | XML_PARSE_NOBLANKS);
// dictionary key callbacks for string cache
static const void *StringCacheKeyRetainCallBack(CFAllocatorRef allocator, const void *str);
static void StringCacheKeyReleaseCallBack(CFAllocatorRef allocator, const void *str);
static CFStringRef StringCacheKeyCopyDescriptionCallBack(const void *str);
static Boolean StringCacheKeyEqualCallBack(const void *str1, const void *str2);
static CFHashCode StringCacheKeyHashCallBack(const void *str);
// isEqual: has the fatal flaw that it doesn't deal well with the received
// being nil. We'll use this utility instead.
// Static copy of AreEqualOrBothNil from GDataObject.m, so that using
// GDataXMLNode does not require pulling in all of GData.
static BOOL AreEqualOrBothNilPrivate(id obj1, id obj2) {
if (obj1 == obj2) {
return YES;
}
if (obj1 && obj2) {
return [obj1 isEqual:obj2];
}
return NO;
}
// convert NSString* to xmlChar*
//
// the "Get" part implies that ownership remains with str
static xmlChar* GDataGetXMLString(NSString *str) {
xmlChar* result = (xmlChar *)[str UTF8String];
return result;
}
// Make a fake qualified name we use as local name internally in libxml
// data structures when there's no actual namespace node available to point to
// from an element or attribute node
//
// Returns an autoreleased NSString*
static NSString *GDataFakeQNameForURIAndName(NSString *theURI, NSString *name) {
NSString *localName = [GDataXMLNode localNameForName:name];
NSString *fakeQName = [NSString stringWithFormat:@"{%@}:%@",
theURI, localName];
return fakeQName;
}
// libxml2 offers xmlSplitQName2, but that searches forwards. Since we may
// be searching for a whole URI shoved in as a prefix, like
// {http://foo}:name
// we'll search for the prefix in backwards from the end of the qualified name
//
// returns a copy of qname as the local name if there's no prefix
static xmlChar *SplitQNameReverse(const xmlChar *qname, xmlChar **prefix) {
// search backwards for a colon
int qnameLen = xmlStrlen(qname);
for (int idx = qnameLen - 1; idx >= 0; idx--) {
if (qname[idx] == ':') {
// found the prefix; copy the prefix, if requested
if (prefix != NULL) {
if (idx > 0) {
*prefix = xmlStrsub(qname, 0, idx);
} else {
*prefix = NULL;
}
}
if (idx < qnameLen - 1) {
// return a copy of the local name
xmlChar *localName = xmlStrsub(qname, idx + 1, qnameLen - idx - 1);
return localName;
} else {
return NULL;
}
}
}
// no colon found, so the qualified name is the local name
xmlChar *qnameCopy = xmlStrdup(qname);
return qnameCopy;
}
@interface GDataXMLNode (PrivateMethods)
// consuming a node implies it will later be freed when the instance is
// dealloc'd; borrowing it implies that ownership and disposal remain the
// job of the supplier of the node
+ (id)nodeConsumingXMLNode:(xmlNodePtr)theXMLNode;
- (id)initConsumingXMLNode:(xmlNodePtr)theXMLNode;
+ (id)nodeBorrowingXMLNode:(xmlNodePtr)theXMLNode;
- (id)initBorrowingXMLNode:(xmlNodePtr)theXMLNode;
// getters of the underlying node
- (xmlNodePtr)XMLNode;
- (xmlNodePtr)XMLNodeCopy;
// search for an underlying attribute
- (GDataXMLNode *)attributeForXMLNode:(xmlAttrPtr)theXMLNode;
// return an NSString for an xmlChar*, using our strings cache in the
// document
- (NSString *)stringFromXMLString:(const xmlChar *)chars;
// setter/getter of the dealloc flag for the underlying node
- (BOOL)shouldFreeXMLNode;
- (void)setShouldFreeXMLNode:(BOOL)flag;
@end
@interface GDataXMLElement (PrivateMethods)
+ (void)fixUpNamespacesForNode:(xmlNodePtr)nodeToFix
graftingToTreeNode:(xmlNodePtr)graftPointNode;
@end
@implementation GDataXMLNode
+ (void)load {
xmlInitParser();
}
// Note on convenience methods for making stand-alone element and
// attribute nodes:
//
// Since we're making a node from scratch, we don't
// have any namespace info. So the namespace prefix, if
// any, will just be slammed into the node name.
// We'll rely on the -addChild method below to remove
// the namespace prefix and replace it with a proper ns
// pointer.
+ (GDataXMLElement *)elementWithName:(NSString *)name {
xmlNodePtr theNewNode = xmlNewNode(NULL, // namespace
GDataGetXMLString(name));
if (theNewNode) {
// succeeded
return [self nodeConsumingXMLNode:theNewNode];
}
return nil;
}
+ (GDataXMLElement *)elementWithName:(NSString *)name stringValue:(NSString *)value {
xmlNodePtr theNewNode = xmlNewNode(NULL, // namespace
GDataGetXMLString(name));
if (theNewNode) {
xmlNodePtr textNode = xmlNewText(GDataGetXMLString(value));
if (textNode) {
xmlNodePtr temp = xmlAddChild(theNewNode, textNode);
if (temp) {
// succeeded
return [self nodeConsumingXMLNode:theNewNode];
}
}
// failed; free the node and any children
xmlFreeNode(theNewNode);
}
return nil;
}
+ (GDataXMLElement *)elementWithName:(NSString *)name URI:(NSString *)theURI {
// since we don't know a prefix yet, shove in the whole URI; we'll look for
// a proper namespace ptr later when addChild calls fixUpNamespacesForNode
NSString *fakeQName = GDataFakeQNameForURIAndName(theURI, name);
xmlNodePtr theNewNode = xmlNewNode(NULL, // namespace
GDataGetXMLString(fakeQName));
if (theNewNode) {
return [self nodeConsumingXMLNode:theNewNode];
}
return nil;
}
+ (id)attributeWithName:(NSString *)name stringValue:(NSString *)value {
xmlChar *xmlName = GDataGetXMLString(name);
xmlChar *xmlValue = GDataGetXMLString(value);
xmlAttrPtr theNewAttr = xmlNewProp(NULL, // parent node for the attr
xmlName, xmlValue);
if (theNewAttr) {
return [self nodeConsumingXMLNode:(xmlNodePtr) theNewAttr];
}
return nil;
}
+ (id)attributeWithName:(NSString *)name URI:(NSString *)attributeURI stringValue:(NSString *)value {
// since we don't know a prefix yet, shove in the whole URI; we'll look for
// a proper namespace ptr later when addChild calls fixUpNamespacesForNode
NSString *fakeQName = GDataFakeQNameForURIAndName(attributeURI, name);
xmlChar *xmlName = GDataGetXMLString(fakeQName);
xmlChar *xmlValue = GDataGetXMLString(value);
xmlAttrPtr theNewAttr = xmlNewProp(NULL, // parent node for the attr
xmlName, xmlValue);
if (theNewAttr) {
return [self nodeConsumingXMLNode:(xmlNodePtr) theNewAttr];
}
return nil;
}
+ (id)textWithStringValue:(NSString *)value {
xmlNodePtr theNewText = xmlNewText(GDataGetXMLString(value));
if (theNewText) {
return [self nodeConsumingXMLNode:theNewText];
}
return nil;
}
+ (id)namespaceWithName:(NSString *)name stringValue:(NSString *)value {
xmlChar *href = GDataGetXMLString(value);
xmlChar *prefix;
if ([name length] > 0) {
prefix = GDataGetXMLString(name);
} else {
// default namespace is represented by a nil prefix
prefix = nil;
}
xmlNsPtr theNewNs = xmlNewNs(NULL, // parent node
href, prefix);
if (theNewNs) {
return [self nodeConsumingXMLNode:(xmlNodePtr) theNewNs];
}
return nil;
}
+ (id)nodeConsumingXMLNode:(xmlNodePtr)theXMLNode {
Class theClass;
if (theXMLNode->type == XML_ELEMENT_NODE) {
theClass = [GDataXMLElement class];
} else {
theClass = [GDataXMLNode class];
}
return [[[theClass alloc] initConsumingXMLNode:theXMLNode] autorelease];
}
- (id)initConsumingXMLNode:(xmlNodePtr)theXMLNode {
self = [super init];
if (self) {
xmlNode_ = theXMLNode;
shouldFreeXMLNode_ = YES;
}
return self;
}
+ (id)nodeBorrowingXMLNode:(xmlNodePtr)theXMLNode {
Class theClass;
if (theXMLNode->type == XML_ELEMENT_NODE) {
theClass = [GDataXMLElement class];
} else {
theClass = [GDataXMLNode class];
}
return [[[theClass alloc] initBorrowingXMLNode:theXMLNode] autorelease];
}
- (id)initBorrowingXMLNode:(xmlNodePtr)theXMLNode {
self = [super init];
if (self) {
xmlNode_ = theXMLNode;
shouldFreeXMLNode_ = NO;
}
return self;
}
- (void)releaseCachedValues {
[cachedName_ release];
cachedName_ = nil;
[cachedChildren_ release];
cachedChildren_ = nil;
[cachedAttributes_ release];
cachedAttributes_ = nil;
}
// convert xmlChar* to NSString*
//
// returns an autoreleased NSString*, from the current node's document strings
// cache if possible
- (NSString *)stringFromXMLString:(const xmlChar *)chars {
#if DEBUG
NSCAssert(chars != NULL, @"GDataXMLNode sees an unexpected empty string");
#endif
if (chars == NULL) return nil;
CFMutableDictionaryRef cacheDict = NULL;
NSString *result = nil;
if (xmlNode_ != NULL
&& (xmlNode_->type == XML_ELEMENT_NODE
|| xmlNode_->type == XML_ATTRIBUTE_NODE
|| xmlNode_->type == XML_TEXT_NODE)) {
// there is no xmlDocPtr in XML_NAMESPACE_DECL nodes,
// so we can't cache the text of those
// look for a strings cache in the document
//
// the cache is in the document's user-defined _private field
if (xmlNode_->doc != NULL) {
cacheDict = xmlNode_->doc->_private;
if (cacheDict) {
// this document has a strings cache
result = (NSString *) CFDictionaryGetValue(cacheDict, chars);
if (result) {
// we found the xmlChar string in the cache; return the previously
// allocated NSString, rather than allocate a new one
return result;
}
}
}
}
// allocate a new NSString for this xmlChar*
result = [NSString stringWithUTF8String:(const char *) chars];
if (cacheDict) {
// save the string in the document's string cache
CFDictionarySetValue(cacheDict, chars, result);
}
return result;
}
- (void)dealloc {
if (xmlNode_ && shouldFreeXMLNode_) {
xmlFreeNode(xmlNode_);
xmlNode_ = NULL;
}
[self releaseCachedValues];
[super dealloc];
}
#pragma mark -
- (void)setStringValue:(NSString *)str {
if (xmlNode_ != NULL && str != nil) {
if (xmlNode_->type == XML_NAMESPACE_DECL) {
// for a namespace node, the value is the namespace URI
xmlNsPtr nsNode = (xmlNsPtr)xmlNode_;
if (nsNode->href != NULL) xmlFree((char *)nsNode->href);
nsNode->href = xmlStrdup(GDataGetXMLString(str));
} else {
// attribute or element node
// do we need to call xmlEncodeSpecialChars?
xmlNodeSetContent(xmlNode_, GDataGetXMLString(str));
}
}
}
- (NSString *)stringValue {
NSString *str = nil;
if (xmlNode_ != NULL) {
if (xmlNode_->type == XML_NAMESPACE_DECL) {
// for a namespace node, the value is the namespace URI
xmlNsPtr nsNode = (xmlNsPtr)xmlNode_;
str = [self stringFromXMLString:(nsNode->href)];
} else {
// attribute or element node
xmlChar* chars = xmlNodeGetContent(xmlNode_);
if (chars) {
str = [self stringFromXMLString:chars];
xmlFree(chars);
}
}
}
return str;
}
- (NSString *)XMLString {
NSString *str = nil;
if (xmlNode_ != NULL) {
xmlBufferPtr buff = xmlBufferCreate();
if (buff) {
xmlDocPtr doc = NULL;
int level = 0;
int format = 0;
int result = xmlNodeDump(buff, doc, xmlNode_, level, format);
if (result > -1) {
str = [[[NSString alloc] initWithBytes:(xmlBufferContent(buff))
length:(xmlBufferLength(buff))
encoding:NSUTF8StringEncoding] autorelease];
}
xmlBufferFree(buff);
}
}
// remove leading and trailing whitespace
NSCharacterSet *ws = [NSCharacterSet whitespaceAndNewlineCharacterSet];
NSString *trimmed = [str stringByTrimmingCharactersInSet:ws];
return trimmed;
}
- (NSString *)localName {
NSString *str = nil;
if (xmlNode_ != NULL) {
str = [self stringFromXMLString:(xmlNode_->name)];
// if this is part of a detached subtree, str may have a prefix in it
str = [[self class] localNameForName:str];
}
return str;
}
- (NSString *)prefix {
NSString *str = nil;
if (xmlNode_ != NULL) {
// the default namespace's prefix is an empty string, though libxml
// represents it as NULL for ns->prefix
str = @"";
if (xmlNode_->ns != NULL && xmlNode_->ns->prefix != NULL) {
str = [self stringFromXMLString:(xmlNode_->ns->prefix)];
}
}
return str;
}
- (NSString *)URI {
NSString *str = nil;
if (xmlNode_ != NULL) {
if (xmlNode_->ns != NULL && xmlNode_->ns->href != NULL) {
str = [self stringFromXMLString:(xmlNode_->ns->href)];
}
}
return str;
}
- (NSString *)qualifiedName {
// internal utility
NSString *str = nil;
if (xmlNode_ != NULL) {
if (xmlNode_->type == XML_NAMESPACE_DECL) {
// name of a namespace node
xmlNsPtr nsNode = (xmlNsPtr)xmlNode_;
// null is the default namespace; one is the loneliest number
if (nsNode->prefix == NULL) {
str = @"";
}
else {
str = [self stringFromXMLString:(nsNode->prefix)];
}
} else if (xmlNode_->ns != NULL && xmlNode_->ns->prefix != NULL) {
// name of a non-namespace node
// has a prefix
char *qname;
if (asprintf(&qname, "%s:%s", (const char *)xmlNode_->ns->prefix,
xmlNode_->name) != -1) {
str = [self stringFromXMLString:(const xmlChar *)qname];
free(qname);
}
} else {
// lacks a prefix
str = [self stringFromXMLString:(xmlNode_->name)];
}
}
return str;
}
- (NSString *)name {
if (cachedName_ != nil) {
return cachedName_;
}
NSString *str = [self qualifiedName];
cachedName_ = [str retain];
return str;
}
+ (NSString *)localNameForName:(NSString *)name {
if (name != nil) {
NSRange range = [name rangeOfString:@":"];
if (range.location != NSNotFound) {
// found a colon
if (range.location + 1 < [name length]) {
NSString *localName = [name substringFromIndex:(range.location + 1)];
return localName;
}
}
}
return name;
}
+ (NSString *)prefixForName:(NSString *)name {
if (name != nil) {
NSRange range = [name rangeOfString:@":"];
if (range.location != NSNotFound) {
NSString *prefix = [name substringToIndex:(range.location)];
return prefix;
}
}
return nil;
}
- (NSUInteger)childCount {
if (cachedChildren_ != nil) {
return [cachedChildren_ count];
}
if (xmlNode_ != NULL) {
unsigned int count = 0;
xmlNodePtr currChild = xmlNode_->children;
while (currChild != NULL) {
++count;
currChild = currChild->next;
}
return count;
}
return 0;
}
- (NSArray *)children {
if (cachedChildren_ != nil) {
return cachedChildren_;
}
NSMutableArray *array = nil;
if (xmlNode_ != NULL) {
xmlNodePtr currChild = xmlNode_->children;
while (currChild != NULL) {
GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:currChild];
if (array == nil) {
array = [NSMutableArray arrayWithObject:node];
} else {
[array addObject:node];
}
currChild = currChild->next;
}
cachedChildren_ = [array retain];
}
return array;
}
- (GDataXMLNode *)childAtIndex:(unsigned)index {
NSArray *children = [self children];
if ([children count] > index) {
return [children objectAtIndex:index];
}
return nil;
}
- (GDataXMLNodeKind)kind {
if (xmlNode_ != NULL) {
xmlElementType nodeType = xmlNode_->type;
switch (nodeType) {
case XML_ELEMENT_NODE: return GDataXMLElementKind;
case XML_ATTRIBUTE_NODE: return GDataXMLAttributeKind;
case XML_TEXT_NODE: return GDataXMLTextKind;
case XML_CDATA_SECTION_NODE: return GDataXMLTextKind;
case XML_ENTITY_REF_NODE: return GDataXMLEntityDeclarationKind;
case XML_ENTITY_NODE: return GDataXMLEntityDeclarationKind;
case XML_PI_NODE: return GDataXMLProcessingInstructionKind;
case XML_COMMENT_NODE: return GDataXMLCommentKind;
case XML_DOCUMENT_NODE: return GDataXMLDocumentKind;
case XML_DOCUMENT_TYPE_NODE: return GDataXMLDocumentKind;
case XML_DOCUMENT_FRAG_NODE: return GDataXMLDocumentKind;
case XML_NOTATION_NODE: return GDataXMLNotationDeclarationKind;
case XML_HTML_DOCUMENT_NODE: return GDataXMLDocumentKind;
case XML_DTD_NODE: return GDataXMLDTDKind;
case XML_ELEMENT_DECL: return GDataXMLElementDeclarationKind;
case XML_ATTRIBUTE_DECL: return GDataXMLAttributeDeclarationKind;
case XML_ENTITY_DECL: return GDataXMLEntityDeclarationKind;
case XML_NAMESPACE_DECL: return GDataXMLNamespaceKind;
case XML_XINCLUDE_START: return GDataXMLProcessingInstructionKind;
case XML_XINCLUDE_END: return GDataXMLProcessingInstructionKind;
case XML_DOCB_DOCUMENT_NODE: return GDataXMLDocumentKind;
}
}
return GDataXMLInvalidKind;
}
- (NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error {
// call through with no explicit namespace dictionary; that will register the
// root node's namespaces
return [self nodesForXPath:xpath namespaces:nil error:error];
}
- (NSArray *)nodesForXPath:(NSString *)xpath
namespaces:(NSDictionary *)namespaces
error:(NSError **)error {
NSMutableArray *array = nil;
NSInteger errorCode = -1;
NSDictionary *errorInfo = nil;
// xmlXPathNewContext requires a doc for its context, but if our elements
// are created from GDataXMLElement's initWithXMLString there may not be
// a document. (We may later decide that we want to stuff the doc used
// there into a GDataXMLDocument and retain it, but we don't do that now.)
//
// We'll temporarily make a document to use for the xpath context.
xmlDocPtr tempDoc = NULL;
xmlNodePtr topParent = NULL;
if (xmlNode_->doc == NULL) {
tempDoc = xmlNewDoc(NULL);
if (tempDoc) {
// find the topmost node of the current tree to make the root of
// our temporary document
topParent = xmlNode_;
while (topParent->parent != NULL) {
topParent = topParent->parent;
}
xmlDocSetRootElement(tempDoc, topParent);
}
}
if (xmlNode_ != NULL && xmlNode_->doc != NULL) {
xmlXPathContextPtr xpathCtx = xmlXPathNewContext(xmlNode_->doc);
if (xpathCtx) {
// anchor at our current node
xpathCtx->node = xmlNode_;
// if a namespace dictionary was provided, register its contents
if (namespaces) {
// the dictionary keys are prefixes; the values are URIs
for (NSString *prefix in namespaces) {
NSString *uri = [namespaces objectForKey:prefix];
xmlChar *prefixChars = (xmlChar *) [prefix UTF8String];
xmlChar *uriChars = (xmlChar *) [uri UTF8String];
int result = xmlXPathRegisterNs(xpathCtx, prefixChars, uriChars);
if (result != 0) {
#if DEBUG
NSCAssert1(result == 0, @"GDataXMLNode XPath namespace %@ issue",
prefix);
#endif
}
}
} else {
// no namespace dictionary was provided
//
// register the namespaces of this node, if it's an element, or of
// this node's root element, if it's a document
xmlNodePtr nsNodePtr = xmlNode_;
if (xmlNode_->type == XML_DOCUMENT_NODE) {
nsNodePtr = xmlDocGetRootElement((xmlDocPtr) xmlNode_);
}
// step through the namespaces, if any, and register each with the
// xpath context
if (nsNodePtr != NULL) {
for (xmlNsPtr nsPtr = nsNodePtr->ns; nsPtr != NULL; nsPtr = nsPtr->next) {
// default namespace is nil in the tree, but there's no way to
// register a default namespace, so we'll register a fake one,
// _def_ns
const xmlChar* prefix = nsPtr->prefix;
if (prefix == NULL) {
prefix = (xmlChar*) kGDataXMLXPathDefaultNamespacePrefix;
}
int result = xmlXPathRegisterNs(xpathCtx, prefix, nsPtr->href);
if (result != 0) {
#if DEBUG
NSCAssert1(result == 0, @"GDataXMLNode XPath namespace %@ issue",
prefix);
#endif
}
}
}
}
// now evaluate the path
xmlXPathObjectPtr xpathObj;
xpathObj = xmlXPathEval(GDataGetXMLString(xpath), xpathCtx);
if (xpathObj) {
// we have some result from the search
array = [NSMutableArray array];
xmlNodeSetPtr nodeSet = xpathObj->nodesetval;
if (nodeSet) {
// add each node in the result set to our array
for (int index = 0; index < nodeSet->nodeNr; index++) {
xmlNodePtr currNode = nodeSet->nodeTab[index];
GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:currNode];
if (node) {
[array addObject:node];
}
}
}
xmlXPathFreeObject(xpathObj);
} else {
// provide an error for failed evaluation
const char *msg = xpathCtx->lastError.str1;
errorCode = xpathCtx->lastError.code;
if (msg) {
NSString *errStr = [NSString stringWithUTF8String:msg];
errorInfo = [NSDictionary dictionaryWithObject:errStr
forKey:@"error"];
}
}
xmlXPathFreeContext(xpathCtx);
}
} else {
// not a valid node for using XPath
errorInfo = [NSDictionary dictionaryWithObject:@"invalid node"
forKey:@"error"];
}
if (array == nil && error != nil) {
*error = [NSError errorWithDomain:@"com.google.GDataXML"
code:errorCode
userInfo:errorInfo];
}
if (tempDoc != NULL) {
xmlUnlinkNode(topParent);
xmlSetTreeDoc(topParent, NULL);
xmlFreeDoc(tempDoc);
}
return array;
}
- (NSString *)description {
int nodeType = (xmlNode_ ? (int)xmlNode_->type : -1);
return [NSString stringWithFormat:@"%@ %p: {type:%d name:%@ xml:"%@"}",
[self class], self, nodeType, [self name], [self XMLString]];
}
- (id)copyWithZone:(NSZone *)zone {
xmlNodePtr nodeCopy = [self XMLNodeCopy];
if (nodeCopy != NULL) {
return [[[self class] alloc] initConsumingXMLNode:nodeCopy];
}
return nil;
}
- (BOOL)isEqual:(GDataXMLNode *)other {
if (self == other) return YES;
if (![other isKindOfClass:[GDataXMLNode class]]) return NO;
return [self XMLNode] == [other XMLNode]
|| ([self kind] == [other kind]
&& AreEqualOrBothNilPrivate([self name], [other name])
&& [[self children] count] == [[other children] count]);
}
- (NSUInteger)hash {
return (NSUInteger) (void *) [GDataXMLNode class];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [super methodSignatureForSelector:selector];
}
#pragma mark -
- (xmlNodePtr)XMLNodeCopy {
if (xmlNode_ != NULL) {
// Note: libxml will create a new copy of namespace nodes (xmlNs records)
// and attach them to this copy in order to keep namespaces within this
// node subtree copy value.
xmlNodePtr nodeCopy = xmlCopyNode(xmlNode_, 1); // 1 = recursive
return nodeCopy;
}
return NULL;
}
- (xmlNodePtr)XMLNode {
return xmlNode_;
}
- (BOOL)shouldFreeXMLNode {
return shouldFreeXMLNode_;
}
- (void)setShouldFreeXMLNode:(BOOL)flag {
shouldFreeXMLNode_ = flag;
}
@end
@implementation GDataXMLElement
- (id)initWithXMLString:(NSString *)str error:(NSError **)error {
self = [super init];
if (self) {
const char *utf8Str = [str UTF8String];
// NOTE: We are assuming a string length that fits into an int
xmlDocPtr doc = xmlReadMemory(utf8Str, (int)strlen(utf8Str), NULL, // URL
NULL, // encoding
kGDataXMLParseOptions);
if (doc == NULL) {
if (error) {
// TODO(grobbins) use xmlSetGenericErrorFunc to capture error
}
} else {
// copy the root node from the doc
xmlNodePtr root = xmlDocGetRootElement(doc);
if (root) {
xmlNode_ = xmlCopyNode(root, 1); // 1: recursive
shouldFreeXMLNode_ = YES;
}
xmlFreeDoc(doc);
}
if (xmlNode_ == NULL) {
// failure
if (error) {
*error = [NSError errorWithDomain:@"com.google.GDataXML"
code:-1
userInfo:nil];
}
[self release];
return nil;
}
}
return self;
}
- (NSArray *)namespaces {
NSMutableArray *array = nil;
if (xmlNode_ != NULL && xmlNode_->nsDef != NULL) {
xmlNsPtr currNS = xmlNode_->nsDef;
while (currNS != NULL) {
// add this prefix/URI to the list, unless it's the implicit xml prefix
if (!xmlStrEqual(currNS->prefix, (const xmlChar *) "xml")) {
GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:(xmlNodePtr) currNS];
if (array == nil) {
array = [NSMutableArray arrayWithObject:node];
} else {
[array addObject:node];
}
}
currNS = currNS->next;
}
}
return array;
}
- (void)setNamespaces:(NSArray *)namespaces {
if (xmlNode_ != NULL) {
[self releaseCachedValues];
// remove previous namespaces
if (xmlNode_->nsDef) {
xmlFreeNsList(xmlNode_->nsDef);
xmlNode_->nsDef = NULL;
}
// add a namespace for each object in the array
NSEnumerator *enumerator = [namespaces objectEnumerator];
GDataXMLNode *namespaceNode;
while ((namespaceNode = [enumerator nextObject]) != nil) {
xmlNsPtr ns = (xmlNsPtr) [namespaceNode XMLNode];
if (ns) {
(void)xmlNewNs(xmlNode_, ns->href, ns->prefix);
}
}
// we may need to fix this node's own name; the graft point is where
// the namespace search starts, so that points to this node too
[[self class] fixUpNamespacesForNode:xmlNode_
graftingToTreeNode:xmlNode_];
}
}
- (void)addNamespace:(GDataXMLNode *)aNamespace {
if (xmlNode_ != NULL) {
[self releaseCachedValues];
xmlNsPtr ns = (xmlNsPtr) [aNamespace XMLNode];
if (ns) {
(void)xmlNewNs(xmlNode_, ns->href, ns->prefix);
// we may need to fix this node's own name; the graft point is where
// the namespace search starts, so that points to this node too
[[self class] fixUpNamespacesForNode:xmlNode_
graftingToTreeNode:xmlNode_];
}
}
}
- (void)addChild:(GDataXMLNode *)child {
if ([child kind] == GDataXMLAttributeKind) {
[self addAttribute:child];
return;
}
if (xmlNode_ != NULL) {
[self releaseCachedValues];
xmlNodePtr childNodeCopy = [child XMLNodeCopy];
if (childNodeCopy) {
xmlNodePtr resultNode = xmlAddChild(xmlNode_, childNodeCopy);
if (resultNode == NULL) {
// failed to add
xmlFreeNode(childNodeCopy);
} else {
// added this child subtree successfully; see if it has
// previously-unresolved namespace prefixes that can now be fixed up
[[self class] fixUpNamespacesForNode:childNodeCopy
graftingToTreeNode:xmlNode_];
}
}
}
}
- (void)removeChild:(GDataXMLNode *)child {
// this is safe for attributes too
if (xmlNode_ != NULL) {
[self releaseCachedValues];
xmlNodePtr node = [child XMLNode];
xmlUnlinkNode(node);
// if the child node was borrowing its xmlNodePtr, then we need to
// explicitly free it, since there is probably no owning object that will
// free it on dealloc
if (![child shouldFreeXMLNode]) {
xmlFreeNode(node);
}
}
}
- (NSArray *)elementsForName:(NSString *)name {
NSString *desiredName = name;
if (xmlNode_ != NULL) {
NSString *prefix = [[self class] prefixForName:desiredName];
if (prefix) {
xmlChar* desiredPrefix = GDataGetXMLString(prefix);
xmlNsPtr foundNS = xmlSearchNs(xmlNode_->doc, xmlNode_, desiredPrefix);
if (foundNS) {
// we found a namespace; fall back on elementsForLocalName:URI:
// to get the elements
NSString *desiredURI = [self stringFromXMLString:(foundNS->href)];
NSString *localName = [[self class] localNameForName:desiredName];
NSArray *nsArray = [self elementsForLocalName:localName URI:desiredURI];
return nsArray;
}
}
// no namespace found for the node's prefix; try an exact match
// for the name argument, including any prefix
NSMutableArray *array = nil;
// walk our list of cached child nodes
NSArray *children = [self children];
for (GDataXMLNode *child in children) {
xmlNodePtr currNode = [child XMLNode];
// find all children which are elements with the desired name
if (currNode->type == XML_ELEMENT_NODE) {
NSString *qName = [child name];
if ([qName isEqual:name]) {
if (array == nil) {
array = [NSMutableArray arrayWithObject:child];
} else {
[array addObject:child];
}
}
}
}
return array;
}
return nil;
}
- (NSArray *)elementsForLocalName:(NSString *)localName URI:(NSString *)URI {
NSMutableArray *array = nil;
if (xmlNode_ != NULL && xmlNode_->children != NULL) {
xmlChar* desiredNSHref = GDataGetXMLString(URI);
xmlChar* requestedLocalName = GDataGetXMLString(localName);
xmlChar* expectedLocalName = requestedLocalName;
// resolve the URI at the parent level, since usually children won't
// have their own namespace definitions, and we don't want to try to
// resolve it once for every child
xmlNsPtr foundParentNS = xmlSearchNsByHref(xmlNode_->doc, xmlNode_, desiredNSHref);
if (foundParentNS == NULL) {
NSString *fakeQName = GDataFakeQNameForURIAndName(URI, localName);
expectedLocalName = GDataGetXMLString(fakeQName);
}
NSArray *children = [self children];
for (GDataXMLNode *child in children) {
xmlNodePtr currChildPtr = [child XMLNode];
// find all children which are elements with the desired name and
// namespace, or with the prefixed name and a null namespace
if (currChildPtr->type == XML_ELEMENT_NODE) {
// normally, we can assume the resolution done for the parent will apply
// to the child, as most children do not define their own namespaces
xmlNsPtr childLocalNS = foundParentNS;
xmlChar* childDesiredLocalName = expectedLocalName;
if (currChildPtr->nsDef != NULL) {
// this child has its own namespace definitons; do a fresh resolve
// of the namespace starting from the child, and see if it differs
// from the resolve done starting from the parent. If the resolve
// finds a different namespace, then override the desired local
// name just for this child.
childLocalNS = xmlSearchNsByHref(xmlNode_->doc, currChildPtr, desiredNSHref);
if (childLocalNS != foundParentNS) {
// this child does indeed have a different namespace resolution
// result than was found for its parent
if (childLocalNS == NULL) {
// no namespace found
NSString *fakeQName = GDataFakeQNameForURIAndName(URI, localName);
childDesiredLocalName = GDataGetXMLString(fakeQName);
} else {
// a namespace was found; use the original local name requested,
// not a faked one expected from resolving the parent
childDesiredLocalName = requestedLocalName;
}
}
}
// check if this child's namespace and local name are what we're
// seeking
if (currChildPtr->ns == childLocalNS
&& currChildPtr->name != NULL
&& xmlStrEqual(currChildPtr->name, childDesiredLocalName)) {
if (array == nil) {
array = [NSMutableArray arrayWithObject:child];
} else {
[array addObject:child];
}
}
}
}
// we return nil, not an empty array, according to docs
}
return array;
}
- (NSArray *)attributes {
if (cachedAttributes_ != nil) {
return cachedAttributes_;
}
NSMutableArray *array = nil;
if (xmlNode_ != NULL && xmlNode_->properties != NULL) {
xmlAttrPtr prop = xmlNode_->properties;
while (prop != NULL) {
GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:(xmlNodePtr) prop];
if (array == nil) {
array = [NSMutableArray arrayWithObject:node];
} else {
[array addObject:node];
}
prop = prop->next;
}
cachedAttributes_ = [array retain];
}
return array;
}
- (void)addAttribute:(GDataXMLNode *)attribute {
if (xmlNode_ != NULL) {
[self releaseCachedValues];
xmlAttrPtr attrPtr = (xmlAttrPtr) [attribute XMLNode];
if (attrPtr) {
// ignore this if an attribute with the name is already present,
// similar to NSXMLNode's addAttribute
xmlAttrPtr oldAttr;
if (attrPtr->ns == NULL) {
oldAttr = xmlHasProp(xmlNode_, attrPtr->name);
} else {
oldAttr = xmlHasNsProp(xmlNode_, attrPtr->name, attrPtr->ns->href);
}
if (oldAttr == NULL) {
xmlNsPtr newPropNS = NULL;
// if this attribute has a namespace, search for a matching namespace
// on the node we're adding to
if (attrPtr->ns != NULL) {
newPropNS = xmlSearchNsByHref(xmlNode_->doc, xmlNode_, attrPtr->ns->href);
if (newPropNS == NULL) {
// make a new namespace on the parent node, and use that for the
// new attribute
newPropNS = xmlNewNs(xmlNode_, attrPtr->ns->href, attrPtr->ns->prefix);
}
}
// copy the attribute onto this node
xmlChar *value = xmlNodeGetContent((xmlNodePtr) attrPtr);
xmlAttrPtr newProp = xmlNewNsProp(xmlNode_, newPropNS, attrPtr->name, value);
if (newProp != NULL) {
// we made the property, so clean up the property's namespace
[[self class] fixUpNamespacesForNode:(xmlNodePtr)newProp
graftingToTreeNode:xmlNode_];
}
if (value != NULL) {
xmlFree(value);
}
}
}
}
}
- (GDataXMLNode *)attributeForXMLNode:(xmlAttrPtr)theXMLNode {
// search the cached attributes list for the GDataXMLNode with
// the underlying xmlAttrPtr
NSArray *attributes = [self attributes];
for (GDataXMLNode *attr in attributes) {
if (theXMLNode == (xmlAttrPtr) [attr XMLNode]) {
return attr;
}
}
return nil;
}
- (GDataXMLNode *)attributeForName:(NSString *)name {
if (xmlNode_ != NULL) {
xmlAttrPtr attrPtr = xmlHasProp(xmlNode_, GDataGetXMLString(name));
if (attrPtr == NULL) {
// can we guarantee that xmlAttrPtrs always have the ns ptr and never
// a namespace as part of the actual attribute name?
// split the name and its prefix, if any
xmlNsPtr ns = NULL;
NSString *prefix = [[self class] prefixForName:name];
if (prefix) {
// find the namespace for this prefix, and search on its URI to find
// the xmlNsPtr
name = [[self class] localNameForName:name];
ns = xmlSearchNs(xmlNode_->doc, xmlNode_, GDataGetXMLString(prefix));
}
const xmlChar* nsURI = ((ns != NULL) ? ns->href : NULL);
attrPtr = xmlHasNsProp(xmlNode_, GDataGetXMLString(name), nsURI);
}
if (attrPtr) {
GDataXMLNode *attr = [self attributeForXMLNode:attrPtr];
return attr;
}
}
return nil;
}
- (GDataXMLNode *)attributeForLocalName:(NSString *)localName
URI:(NSString *)attributeURI {
if (xmlNode_ != NULL) {
const xmlChar* name = GDataGetXMLString(localName);
const xmlChar* nsURI = GDataGetXMLString(attributeURI);
xmlAttrPtr attrPtr = xmlHasNsProp(xmlNode_, name, nsURI);
if (attrPtr == NULL) {
// if the attribute is in a tree lacking the proper namespace,
// the local name may include the full URI as a prefix
NSString *fakeQName = GDataFakeQNameForURIAndName(attributeURI, localName);
const xmlChar* xmlFakeQName = GDataGetXMLString(fakeQName);
attrPtr = xmlHasProp(xmlNode_, xmlFakeQName);
}
if (attrPtr) {
GDataXMLNode *attr = [self attributeForXMLNode:attrPtr];
return attr;
}
}
return nil;
}
- (NSString *)resolvePrefixForNamespaceURI:(NSString *)namespaceURI {
if (xmlNode_ != NULL) {
xmlChar* desiredNSHref = GDataGetXMLString(namespaceURI);
xmlNsPtr foundNS = xmlSearchNsByHref(xmlNode_->doc, xmlNode_, desiredNSHref);
if (foundNS) {
// we found the namespace
if (foundNS->prefix != NULL) {
NSString *prefix = [self stringFromXMLString:(foundNS->prefix)];
return prefix;
} else {
// empty prefix is default namespace
return @"";
}
}
}
return nil;
}
#pragma mark Namespace fixup routines
+ (void)deleteNamespacePtr:(xmlNsPtr)namespaceToDelete
fromXMLNode:(xmlNodePtr)node {
// utilty routine to remove a namespace pointer from an element's
// namespace definition list. This is just removing the nsPtr
// from the singly-linked list, the node's namespace definitions.
xmlNsPtr currNS = node->nsDef;
xmlNsPtr prevNS = NULL;
while (currNS != NULL) {
xmlNsPtr nextNS = currNS->next;
if (namespaceToDelete == currNS) {
// found it; delete it from the head of the node's ns definition list
// or from the next field of the previous namespace
if (prevNS != NULL) prevNS->next = nextNS;
else node->nsDef = nextNS;
xmlFreeNs(currNS);
return;
}
prevNS = currNS;
currNS = nextNS;
}
}
+ (void)fixQualifiedNamesForNode:(xmlNodePtr)nodeToFix
graftingToTreeNode:(xmlNodePtr)graftPointNode {
// Replace prefix-in-name with proper namespace pointers
//
// This is an inner routine for fixUpNamespacesForNode:
//
// see if this node's name lacks a namespace and is qualified, and if so,
// see if we can resolve the prefix against the parent
//
// The prefix may either be normal, "gd:foo", or a URI
// "{http://blah.com/}:foo"
if (nodeToFix->ns == NULL) {
xmlNsPtr foundNS = NULL;
xmlChar* prefix = NULL;
xmlChar* localName = SplitQNameReverse(nodeToFix->name, &prefix);
if (localName != NULL) {
if (prefix != NULL) {
// if the prefix is wrapped by { and } then it's a URI
int prefixLen = xmlStrlen(prefix);
if (prefixLen > 2
&& prefix[0] == '{'
&& prefix[prefixLen - 1] == '}') {
// search for the namespace by URI
xmlChar* uri = xmlStrsub(prefix, 1, prefixLen - 2);
if (uri != NULL) {
foundNS = xmlSearchNsByHref(graftPointNode->doc, graftPointNode, uri);
xmlFree(uri);
}
}
}
if (foundNS == NULL) {
// search for the namespace by prefix, even if the prefix is nil
// (nil prefix means to search for the default namespace)
foundNS = xmlSearchNs(graftPointNode->doc, graftPointNode, prefix);
}
if (foundNS != NULL) {
// we found a namespace, so fix the ns pointer and the local name
xmlSetNs(nodeToFix, foundNS);
xmlNodeSetName(nodeToFix, localName);
}
if (prefix != NULL) {
xmlFree(prefix);
prefix = NULL;
}
xmlFree(localName);
}
}
}
+ (void)fixDuplicateNamespacesForNode:(xmlNodePtr)nodeToFix
graftingToTreeNode:(xmlNodePtr)graftPointNode
namespaceSubstitutionMap:(NSMutableDictionary *)nsMap {
// Duplicate namespace removal
//
// This is an inner routine for fixUpNamespacesForNode:
//
// If any of this node's namespaces are already defined at the graft point
// level, add that namespace to the map of namespace substitutions
// so it will be replaced in the children below the nodeToFix, and
// delete the namespace record
if (nodeToFix->type == XML_ELEMENT_NODE) {
// step through the namespaces defined on this node
xmlNsPtr definedNS = nodeToFix->nsDef;
while (definedNS != NULL) {
// see if this namespace is already defined higher in the tree,
// with both the same URI and the same prefix; if so, add a mapping for
// it
xmlNsPtr foundNS = xmlSearchNsByHref(graftPointNode->doc, graftPointNode,
definedNS->href);
if (foundNS != NULL
&& foundNS != definedNS
&& xmlStrEqual(definedNS->prefix, foundNS->prefix)) {
// store a mapping from this defined nsPtr to the one found higher
// in the tree
[nsMap setObject:[NSValue valueWithPointer:foundNS]
forKey:[NSValue valueWithPointer:definedNS]];
// remove this namespace from the ns definition list of this node;
// all child elements and attributes referencing this namespace
// now have a dangling pointer and must be updated (that is done later
// in this method)
//
// before we delete this namespace, move our pointer to the
// next one
xmlNsPtr nsToDelete = definedNS;
definedNS = definedNS->next;
[self deleteNamespacePtr:nsToDelete fromXMLNode:nodeToFix];
} else {
// this namespace wasn't a duplicate; move to the next
definedNS = definedNS->next;
}
}
}
// if this node's namespace is one we deleted, update it to point
// to someplace better
if (nodeToFix->ns != NULL) {
NSValue *currNS = [NSValue valueWithPointer:nodeToFix->ns];
NSValue *replacementNS = [nsMap objectForKey:currNS];
if (replacementNS != nil) {
xmlNsPtr replaceNSPtr = (xmlNsPtr)[replacementNS pointerValue];
xmlSetNs(nodeToFix, replaceNSPtr);
}
}
}
+ (void)fixUpNamespacesForNode:(xmlNodePtr)nodeToFix
graftingToTreeNode:(xmlNodePtr)graftPointNode
namespaceSubstitutionMap:(NSMutableDictionary *)nsMap {
// This is the inner routine for fixUpNamespacesForNode:graftingToTreeNode:
//
// This routine fixes two issues:
//
// Because we can create nodes with qualified names before adding
// them to the tree that declares the namespace for the prefix,
// we need to set the node namespaces after adding them to the tree.
//
// Because libxml adds namespaces to nodes when it copies them,
// we want to remove redundant namespaces after adding them to
// a tree.
//
// If only the Mac's libxml had xmlDOMWrapReconcileNamespaces, it could do
// namespace cleanup for us
// We only care about fixing names of elements and attributes
if (nodeToFix->type != XML_ELEMENT_NODE
&& nodeToFix->type != XML_ATTRIBUTE_NODE) return;
// Do the fixes
[self fixQualifiedNamesForNode:nodeToFix
graftingToTreeNode:graftPointNode];
[self fixDuplicateNamespacesForNode:nodeToFix
graftingToTreeNode:graftPointNode
namespaceSubstitutionMap:nsMap];
if (nodeToFix->type == XML_ELEMENT_NODE) {
// when fixing element nodes, recurse for each child element and
// for each attribute
xmlNodePtr currChild = nodeToFix->children;
while (currChild != NULL) {
[self fixUpNamespacesForNode:currChild
graftingToTreeNode:graftPointNode
namespaceSubstitutionMap:nsMap];
currChild = currChild->next;
}
xmlAttrPtr currProp = nodeToFix->properties;
while (currProp != NULL) {
[self fixUpNamespacesForNode:(xmlNodePtr)currProp
graftingToTreeNode:graftPointNode
namespaceSubstitutionMap:nsMap];
currProp = currProp->next;
}
}
}
+ (void)fixUpNamespacesForNode:(xmlNodePtr)nodeToFix
graftingToTreeNode:(xmlNodePtr)graftPointNode {
// allocate the namespace map that will be passed
// down on recursive calls
NSMutableDictionary *nsMap = [NSMutableDictionary dictionary];
[self fixUpNamespacesForNode:nodeToFix
graftingToTreeNode:graftPointNode
namespaceSubstitutionMap:nsMap];
}
@end
@interface GDataXMLDocument (PrivateMethods)
- (void)addStringsCacheToDoc;
@end
@implementation GDataXMLDocument
- (id)initWithXMLString:(NSString *)str options:(unsigned int)mask error:(NSError **)error {
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
GDataXMLDocument *doc = [self initWithData:data options:mask error:error];
return doc;
}
- (id)initWithData:(NSData *)data options:(unsigned int)mask error:(NSError **)error {
self = [super init];
if (self) {
const char *baseURL = NULL;
const char *encoding = NULL;
// NOTE: We are assuming [data length] fits into an int.
xmlDoc_ = xmlReadMemory((const char*)[data bytes], (int)[data length], baseURL, encoding,
kGDataXMLParseOptions); // TODO(grobbins) map option values
if (xmlDoc_ == NULL) {
if (error) {
*error = [NSError errorWithDomain:@"com.google.GDataXML"
code:-1
userInfo:nil];
// TODO(grobbins) use xmlSetGenericErrorFunc to capture error
}
[self release];
return nil;
} else {
if (error) *error = NULL;
[self addStringsCacheToDoc];
}
}
return self;
}
- (id)initWithRootElement:(GDataXMLElement *)element {
self = [super init];
if (self) {
xmlDoc_ = xmlNewDoc(NULL);
(void) xmlDocSetRootElement(xmlDoc_, [element XMLNodeCopy]);
[self addStringsCacheToDoc];
}
return self;
}
- (void)addStringsCacheToDoc {
// utility routine for init methods
#if DEBUG
NSCAssert(xmlDoc_ != NULL && xmlDoc_->_private == NULL,
@"GDataXMLDocument cache creation problem");
#endif
// add a strings cache as private data for the document
//
// we'll use plain C pointers (xmlChar*) as the keys, and NSStrings
// as the values
CFIndex capacity = 0; // no limit
CFDictionaryKeyCallBacks keyCallBacks = {
0, // version
StringCacheKeyRetainCallBack,
StringCacheKeyReleaseCallBack,
StringCacheKeyCopyDescriptionCallBack,
StringCacheKeyEqualCallBack,
StringCacheKeyHashCallBack
};
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(
kCFAllocatorDefault, capacity,
&keyCallBacks, &kCFTypeDictionaryValueCallBacks);
// we'll use the user-defined _private field for our cache
xmlDoc_->_private = dict;
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@ %p", [self class], self];
}
- (void)dealloc {
if (xmlDoc_ != NULL) {
// release the strings cache
//
// since it's a CF object, were anyone to use this in a GC environment,
// this would need to be released in a finalize method, too
if (xmlDoc_->_private != NULL) {
CFRelease(xmlDoc_->_private);
}
xmlFreeDoc(xmlDoc_);
}
[super dealloc];
}
#pragma mark -
- (GDataXMLElement *)rootElement {
GDataXMLElement *element = nil;
if (xmlDoc_ != NULL) {
xmlNodePtr rootNode = xmlDocGetRootElement(xmlDoc_);
if (rootNode) {
element = [GDataXMLElement nodeBorrowingXMLNode:rootNode];
}
}
return element;
}
- (NSData *)XMLData {
if (xmlDoc_ != NULL) {
xmlChar *buffer = NULL;
int bufferSize = 0;
xmlDocDumpMemory(xmlDoc_, &buffer, &bufferSize);
if (buffer) {
NSData *data = [NSData dataWithBytes:buffer
length:bufferSize];
xmlFree(buffer);
return data;
}
}
return nil;
}
- (void)setVersion:(NSString *)version {
if (xmlDoc_ != NULL) {
if (xmlDoc_->version != NULL) {
// version is a const char* so we must cast
xmlFree((char *) xmlDoc_->version);
xmlDoc_->version = NULL;
}
if (version != nil) {
xmlDoc_->version = xmlStrdup(GDataGetXMLString(version));
}
}
}
- (void)setCharacterEncoding:(NSString *)encoding {
if (xmlDoc_ != NULL) {
if (xmlDoc_->encoding != NULL) {
// version is a const char* so we must cast
xmlFree((char *) xmlDoc_->encoding);
xmlDoc_->encoding = NULL;
}
if (encoding != nil) {
xmlDoc_->encoding = xmlStrdup(GDataGetXMLString(encoding));
}
}
}
- (NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error {
return [self nodesForXPath:xpath namespaces:nil error:error];
}
- (NSArray *)nodesForXPath:(NSString *)xpath
namespaces:(NSDictionary *)namespaces
error:(NSError **)error {
if (xmlDoc_ != NULL) {
GDataXMLNode *docNode = [GDataXMLElement nodeBorrowingXMLNode:(xmlNodePtr)xmlDoc_];
NSArray *array = [docNode nodesForXPath:xpath
namespaces:namespaces
error:error];
return array;
}
return nil;
}
@end
//
// Dictionary key callbacks for our C-string to NSString cache dictionary
//
static const void *StringCacheKeyRetainCallBack(CFAllocatorRef allocator, const void *str) {
// copy the key
xmlChar* key = xmlStrdup(str);
return key;
}
static void StringCacheKeyReleaseCallBack(CFAllocatorRef allocator, const void *str) {
// free the key
char *chars = (char *)str;
xmlFree((char *) chars);
}
static CFStringRef StringCacheKeyCopyDescriptionCallBack(const void *str) {
// make a CFString from the key
CFStringRef cfStr = CFStringCreateWithCString(kCFAllocatorDefault,
(const char *)str,
kCFStringEncodingUTF8);
return cfStr;
}
static Boolean StringCacheKeyEqualCallBack(const void *str1, const void *str2) {
// compare the key strings
if (str1 == str2) return true;
int result = xmlStrcmp(str1, str2);
return (result == 0);
}
static CFHashCode StringCacheKeyHashCallBack(const void *str) {
// dhb hash, per http://www.cse.yorku.ca/~oz/hash.html
CFHashCode hash = 5381;
int c;
const char *chars = (const char *)str;
while ((c = *chars++) != 0) {
hash = ((hash << 5) + hash) + c;
}
return hash;
}