Okechi Onyeje 031a354996 Ebay Listing Now Posts both information and single image
need to rework on categories since subcategory workflow added some complications, but that will be taken care of in a bug fix task
2016-08-23 13:48:34 -04:00

2013 lines
64 KiB
Objective-C

/* Modifications for HTML parser support:
* Copyright (c) 2011 Simon Grätzer simon@graetzer.org
*
* 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);
static const int kGDataHTMLParseOptions = (HTML_PARSE_NOWARNING | HTML_PARSE_NOERROR);
// 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;
}
static void RegisterNamespaces(NSDictionary *namespaces, xmlXPathContextPtr xpathCtx, const xmlNodePtr nsNodePtr) {
// 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 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 %s issue",
prefix);
#endif
}
}
}
}
}
@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];
}
- (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];
}
- (id)initBorrowingXMLNode:(xmlNodePtr)theXMLNode {
self = [super init];
if (self) {
xmlNode_ = theXMLNode;
shouldFreeXMLNode_ = NO;
}
return self;
}
- (void)releaseCachedValues {
cachedName_ = nil;
cachedChildren_ = nil;
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 = (__bridge 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, (__bridge const void *)(result));
}
return result;
}
- (void)dealloc {
if (xmlNode_ && shouldFreeXMLNode_) {
xmlFreeNode(xmlNode_);
xmlNode_ = NULL;
}
[self releaseCachedValues];
}
#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];
}
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;
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;
}
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;
}
- (GDataXMLNode *)firstNodeForXPath:(NSString *)xpath error:(NSError **)error
{
NSArray *nodes = [self nodesForXPath:xpath error:error];
if (!nodes.count) {
return nil;
}
return [nodes objectAtIndex:0];
}
- (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];
}
- (GDataXMLNode *)firstNodeForXPath:(NSString *)xpath namespaces:(NSDictionary *)namespaces error:(NSError **)error
{
NSArray *nodes = [self nodesForXPath:xpath namespaces:namespaces error:error];
if (!nodes.count) {
return nil;
}
return [nodes objectAtIndex:0];
}
- (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_;
RegisterNamespaces(namespaces, xpathCtx, xmlNode_);
// 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) (__bridge 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 {
if (self = [super init]) {
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];
}
return nil;
}
}
return self;
}
- (id)initWithHTMLString:(NSString *)str error:(NSError **)error {
if (self = [super init]) {
const char *utf8Str = [str UTF8String];
// NOTE: We are assuming a string length that fits into an int
xmlDocPtr doc = htmlReadMemory(utf8Str, (int)strlen(utf8Str), NULL, // URL
NULL, // encoding
kGDataHTMLParseOptions);
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];
}
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;
}
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;
const char *IANAEncodingCStringFromNSStringEncoding(NSStringEncoding encoding);
@end
@implementation GDataXMLDocument
const char *IANAEncodingCStringFromNSStringEncoding(NSStringEncoding encoding)
{
CFStringEncoding cfEncoding = CFStringConvertNSStringEncodingToEncoding(encoding);
CFStringRef ianaCharacterSetName = CFStringConvertEncodingToIANACharSetName(cfEncoding);
// To avoid brainfuck with encoding of the encoding string, let's just use NSString convenience method
return [(__bridge NSString*)ianaCharacterSetName UTF8String];
// const char *cIanaCharacterSetName = NULL;
//
// cIanaCharacterSetName = CFStringGetCStringPtr(ianaCharacterSetName, kCFStringEncodingMacRoman);
//
// if (cIanaCharacterSetName == NULL) {
// CFStringGetCString(ianaCharacterSetName, cIanaCharacterSetName, CFStringGetLength(ianaCharacterSetName), kCFStringEncodingMacRoman);
// }
//
// return cIanaCharacterSetName;
}
- (id)initWithXMLString:(NSString *)str error:(NSError **)error
{
return [self initWithXMLString:str encoding:NSUTF8StringEncoding error:error];
}
- (id)initWithData:(NSData *)data error:(NSError **)error
{
return [self initWithData:data encoding:NSUTF8StringEncoding error:error];
}
- (id)initWithHTMLString:(NSString *)str error:(NSError **)error
{
return [self initWithHTMLString:str encoding:NSUTF8StringEncoding error:error];
}
- (id)initWithHTMLData:(NSData *)data error:(NSError **)error
{
return [self initWithHTMLData:data encoding:NSUTF8StringEncoding error:error];
}
- (id)initWithXMLString:(NSString *)str encoding:(NSStringEncoding)encoding error:(NSError **)error {
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
GDataXMLDocument *doc = [self initWithData:data encoding:encoding error:error];
return doc;
}
- (id)initWithHTMLString:(NSString *)str encoding:(NSStringEncoding)encoding error:(NSError **)error {
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
GDataXMLDocument *doc = [self initWithHTMLData:data encoding:encoding error:error];
return doc;
}
- (id)initWithData:(NSData *)data encoding:(NSStringEncoding)encoding error:(NSError **)error {
if (self = [super init]) {
_encoding = encoding;
const char *baseURL = NULL;
const char *xmlEncoding = IANAEncodingCStringFromNSStringEncoding(encoding);
// NOTE: We are assuming [data length] fits into an int.
xmlDoc_ = xmlReadMemory((const char*)[data bytes], (int)[data length], baseURL, xmlEncoding,
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
}
return nil;
} else {
if (error) *error = NULL;
[self addStringsCacheToDoc];
}
}
return self;
}
- (id)initWithHTMLData:(NSData *)data encoding:(NSStringEncoding)encoding error:(NSError **)error {
if (self = [super init]) {
const char *baseURL = NULL;
_encoding = encoding;
const char *xmlEncoding = IANAEncodingCStringFromNSStringEncoding(encoding);
xmlDoc_ = htmlReadMemory((const char*)[data bytes], (int)[data length], baseURL, xmlEncoding, kGDataHTMLParseOptions);
if (xmlDoc_ == NULL) {
if (error) {
*error = [NSError errorWithDomain:@"com.google.GDataXML"
code:-1
userInfo:nil];
// TODO(grobbins) use xmlSetGenericErrorFunc to capture error
}
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_);
}
}
#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];
}
- (GDataXMLNode *)firstNodeForXPath:(NSString *)xpath error:(NSError **)error {
NSArray *nodes = [self nodesForXPath:xpath error:error];
if (!nodes.count) {
return nil;
}
return [nodes objectAtIndex:0];
}
- (NSArray *)nodesForXPath:(NSString *)xpath
namespaces:(NSDictionary *)namespaces
error:(NSError **)error {
NSMutableArray *array = nil;
NSInteger errorCode = -1;
NSDictionary *errorInfo = nil;
if (xmlDoc_ != NULL) {
xmlXPathContextPtr xpathCtx = xmlXPathNewContext(xmlDoc_);
if (xpathCtx) {
xpathCtx->node = (xmlNodePtr)xmlDoc_;
RegisterNamespaces(namespaces, xpathCtx, xmlDocGetRootElement(xmlDoc_));
// now evaluate the path
xmlXPathObjectPtr xpathObj = xmlXPathEval(GDataGetXMLString(xpath), xpathCtx);
if (xpathObj) {
// we have some result from the search
array = [NSMutableArray array];
xmlNodeSetPtr nodeSet = xpathObj->nodesetval;
if (!xmlXPathNodeSetIsEmpty(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];
}
return array;
}
- (GDataXMLNode *)firstNodeForXPath:(NSString *)xpath namespaces:(NSDictionary *)namespaces error:(NSError *__autoreleasing *)error
{
NSArray *nodes = [self nodesForXPath:xpath namespaces:namespaces error:error];
if (!nodes.count) {
return nil;
}
return [nodes objectAtIndex:0];
}
@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;
}