From d32066665fb25833be117fdb55685ff3f7effbf1 Mon Sep 17 00:00:00 2001 From: Jake TM Pearce Date: Tue, 24 Jan 2012 21:19:27 +0000 Subject: [PATCH] Adding GDataXMLNode sources. --- .gitignore | 2 - epub/GDataXMLNode.h | 221 ++++++ epub/GDataXMLNode.m | 1830 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 2051 insertions(+), 2 deletions(-) create mode 100644 epub/GDataXMLNode.h create mode 100644 epub/GDataXMLNode.m diff --git a/.gitignore b/.gitignore index cc20e01..02b2ad9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,3 @@ *.pbxuser *.mode1v3 xcuserdata/ -GDataXMLNode.h -GDataXMLNode.m diff --git a/epub/GDataXMLNode.h b/epub/GDataXMLNode.h new file mode 100644 index 0000000..2cc8916 --- /dev/null +++ b/epub/GDataXMLNode.h @@ -0,0 +1,221 @@ +/* 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 + +// libxml includes require that the target Header Search Paths contain +// +// /usr/include/libxml2 +// +// and Other Linker Flags contain +// +// -lxml2 + +#import +#import +#import +#import +#import + + +#ifdef GDATA_TARGET_NAMESPACE + // 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 { +@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 diff --git a/epub/GDataXMLNode.m b/epub/GDataXMLNode.m new file mode 100644 index 0000000..b730ca6 --- /dev/null +++ b/epub/GDataXMLNode.m @@ -0,0 +1,1830 @@ +/* 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; +}