mirror of
https://bitbucket.org/vendoo/vendoo_v1.0.git
synced 2025-12-25 03:37:39 +00:00
need to rework on categories since subcategory workflow added some complications, but that will be taken care of in a bug fix task
2013 lines
64 KiB
Objective-C
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;
|
|
}
|
|
|