849 lines
40 KiB
Mathematica
849 lines
40 KiB
Mathematica
|
//
|
||
|
// XMLNode.m
|
||
|
// Monal
|
||
|
//
|
||
|
// Created by Anurodh Pokharel on 6/29/13.
|
||
|
//
|
||
|
//
|
||
|
|
||
|
#include <stdarg.h>
|
||
|
|
||
|
#import "MLXMLNode.h"
|
||
|
|
||
|
#import "HelperTools.h"
|
||
|
#import "XMPPIQ.h"
|
||
|
#import "XMPPMessage.h"
|
||
|
#import "XMPPPresence.h"
|
||
|
#import "XMPPDataForm.h"
|
||
|
|
||
|
@import UIKit.UIApplication;
|
||
|
|
||
|
//#define DEBUG_XMLQueryLanguage 1
|
||
|
|
||
|
//this is the required prototype from Holger's snprintf.c
|
||
|
int rpl_vasprintf(char **, const char *, va_list *);
|
||
|
|
||
|
@interface MLXMLNode()
|
||
|
{
|
||
|
NSMutableArray* _children;
|
||
|
}
|
||
|
@property (nonatomic, strong) NSCache* cache;
|
||
|
@property (nonatomic, strong) NSCache* queryEntryCache;
|
||
|
|
||
|
@property (atomic, strong, readwrite) NSString* element;
|
||
|
@property (atomic, readwrite) NSMutableDictionary* attributes;
|
||
|
@property (atomic, weak, readwrite) MLXMLNode* parent;
|
||
|
@end
|
||
|
|
||
|
@implementation MLXMLNode
|
||
|
|
||
|
static NSRegularExpression* pathSplitterRegex;
|
||
|
static NSRegularExpression* componentParserRegex;
|
||
|
static NSRegularExpression* attributeFilterRegex;
|
||
|
|
||
|
#ifdef QueryStatistics
|
||
|
static NSMutableDictionary* statistics;
|
||
|
#endif
|
||
|
|
||
|
+(void) initialize
|
||
|
{
|
||
|
#ifdef QueryStatistics
|
||
|
statistics = [NSMutableDictionary new];
|
||
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nowIdle:) name:kMonalIdle object:nil];
|
||
|
#endif
|
||
|
|
||
|
//compile regexes only once (see https://unicode-org.github.io/icu/userguide/strings/regexp.html for syntax)
|
||
|
pathSplitterRegex = [NSRegularExpression regularExpressionWithPattern:@"^(/?(\\{(\\*|[^}]+)\\})?([!a-zA-Z0-9_:-]+|\\*|\\.\\.)?((\\<[^=~]+[=~][^>]+\\>)*))((/((\\{(\\*|[^}]+)\\})?([!a-zA-Z0-9_:-]+|\\*|\\.\\.)?((\\<[^=~]+[=~][^>]+\\>)*)))*)((@[a-zA-Z0-9_:-]+|@@|#|\\$|\\\\[^\\\\]+\\\\)(\\|(bool|int|uint|double|datetime|base64|uuid|uuidcast))?)?$" options:NSRegularExpressionCaseInsensitive error:nil];
|
||
|
componentParserRegex = [NSRegularExpression regularExpressionWithPattern:@"^(\\{(\\*|[^}]+)\\})?([!a-zA-Z0-9_:-]+|\\*|\\.\\.)?((\\<[^=~]+[=~][^>]+\\>)*)((@[a-zA-Z0-9_:-]+|@@|#|\\$|\\\\[^\\\\]+\\\\)(\\|(bool|int|uint|double|datetime|base64|uuid|uuidcast))?)?$" options:NSRegularExpressionCaseInsensitive error:nil];
|
||
|
attributeFilterRegex = [NSRegularExpression regularExpressionWithPattern:@"\\<([^=~]+)([=~])([^>]+)\\>" options:NSRegularExpressionCaseInsensitive error:nil];
|
||
|
|
||
|
// testcases for stanza
|
||
|
// <stream:features><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>SCRAM-SHA-1</mechanism><mechanism>PLAIN</mechanism><mechanism>SCRAM-SHA-1-PLUS</mechanism></mechanisms></stream:features>
|
||
|
// [self print_debug:@"/*" inTree:parsedStanza];
|
||
|
// [self print_debug:@"{*}*" inTree:parsedStanza];
|
||
|
// [self print_debug:@"{*}*/*@xmlns" inTree:parsedStanza];
|
||
|
// [self print_debug:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms" inTree:parsedStanza];
|
||
|
// [self print_debug:@"{*}*@xmlns" inTree:parsedStanza];
|
||
|
// [self print_debug:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/mechanism" inTree:parsedStanza];
|
||
|
// [self print_debug:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/mechanism#" inTree:parsedStanza];
|
||
|
// [self print_debug:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/*#" inTree:parsedStanza];
|
||
|
// [self print_debug:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/*@xmlns" inTree:parsedStanza];
|
||
|
// [self print_debug:@"/.." inTree:parsedStanza];
|
||
|
// [self print_debug:@"/../*" inTree:parsedStanza];
|
||
|
// [self print_debug:@"mechanisms/mechanism#" inTree:parsedStanza];
|
||
|
// [self print_debug:@"{jabber:client}iq@@" inTree:parsedStanza];
|
||
|
}
|
||
|
|
||
|
+(void) nowIdle:(NSNotification*) notification
|
||
|
{
|
||
|
#ifdef QueryStatistics
|
||
|
NSMutableDictionary* sortedStatistics = [NSMutableDictionary new];
|
||
|
@synchronized(statistics) {
|
||
|
NSArray* sortedKeys = [statistics keysSortedByValueUsingComparator: ^(id obj1, id obj2) {
|
||
|
if([obj1 integerValue] > [obj2 integerValue])
|
||
|
return (NSComparisonResult)NSOrderedDescending;
|
||
|
if([obj1 integerValue] < [obj2 integerValue])
|
||
|
return (NSComparisonResult)NSOrderedAscending;
|
||
|
return (NSComparisonResult)NSOrderedSame;
|
||
|
}];
|
||
|
for(NSString* key in sortedKeys)
|
||
|
DDLogDebug(@"STATISTICS: %@ = %@", key, statistics[key]);
|
||
|
//sortedStatistics[key] = statistics[key];
|
||
|
}
|
||
|
//DDLogDebug(@"XML QUERY STATISTICS: %@", sortedStatistics);
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
-(void) internalInit
|
||
|
{
|
||
|
_attributes = [NSMutableDictionary new];
|
||
|
_children = [NSMutableArray new];
|
||
|
_parent = nil;
|
||
|
_data = nil;
|
||
|
_element = @"";
|
||
|
self.cache = [NSCache new];
|
||
|
self.queryEntryCache = [NSCache new];
|
||
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMemoryPressureNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
|
||
|
}
|
||
|
|
||
|
-(id) init
|
||
|
{
|
||
|
self = [super init];
|
||
|
[self internalInit];
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
-(id) initWithElement:(NSString*) element
|
||
|
{
|
||
|
self = [super init];
|
||
|
[self internalInit];
|
||
|
_element = [element copy];
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
-(id) initWithElement:(NSString*) element andNamespace:(NSString*) xmlns
|
||
|
{
|
||
|
self = [self initWithElement:element];
|
||
|
[self setXMLNS:xmlns];
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
-(id) initWithElement:(NSString*) element andNamespace:(NSString*) xmlns withAttributes:(NSDictionary*) attributes andChildren:(NSArray*) children andData:(NSString*) data
|
||
|
{
|
||
|
self = [self initWithElement:element withAttributes:attributes andChildren:children andData:data];
|
||
|
[self setXMLNS:xmlns];
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
-(id) initWithElement:(NSString*) element withAttributes:(NSDictionary*) attributes andChildren:(NSArray*) children andData:(NSString*) data
|
||
|
{
|
||
|
self = [self initWithElement:element];
|
||
|
[_attributes addEntriesFromDictionary:[[NSDictionary alloc] initWithDictionary:attributes copyItems:YES]];
|
||
|
for(MLXMLNode* child in children)
|
||
|
[self addChildNode:child];
|
||
|
_data = nil;
|
||
|
if(data)
|
||
|
_data = [data copy];
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
-(id) initWithElement:(NSString*) element andData:(NSString* _Nullable) data
|
||
|
{
|
||
|
self = [self initWithElement:element withAttributes:@{} andChildren:@[] andData:data];
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
-(id) initWithElement:(NSString*) element andNamespace:(NSString*) xmlns andData:(NSString* _Nullable) data
|
||
|
{
|
||
|
self = [self initWithElement:element withAttributes:@{} andChildren:@[] andData:data];
|
||
|
[self setXMLNS:xmlns];
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
-(void) dealloc
|
||
|
{
|
||
|
/*
|
||
|
#ifdef IS_ALPHA
|
||
|
DDLogVerbose(@"Dealloc of MLXMLNode: %@", self);
|
||
|
#endif
|
||
|
*/
|
||
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||
|
[self.cache removeAllObjects];
|
||
|
[self.queryEntryCache removeAllObjects];
|
||
|
}
|
||
|
|
||
|
-(id) initWithCoder:(NSCoder*) decoder
|
||
|
{
|
||
|
self = [super init];
|
||
|
if(!self)
|
||
|
return nil;
|
||
|
[self internalInit];
|
||
|
|
||
|
_element = [decoder decodeObjectOfClass:[NSString class] forKey:@"element"];
|
||
|
_attributes = [decoder decodeObjectOfClasses:[[NSSet alloc] initWithArray:@[[NSMutableDictionary class], [NSDictionary class], [NSMutableString class], [NSString class]]] forKey:@"attributes"];
|
||
|
NSArray* decodedChildren = [decoder decodeObjectOfClasses:[[NSSet alloc] initWithArray:@[[NSMutableArray class], [NSArray class], [MLXMLNode class], [XMPPIQ class], [XMPPMessage class], [XMPPPresence class], [XMPPDataForm class]]] forKey:@"children"];
|
||
|
for(MLXMLNode* child in decodedChildren)
|
||
|
[self addChildNodeWithoutCopy:child];
|
||
|
_data = [decoder decodeObjectOfClass:[NSString class] forKey:@"data"];
|
||
|
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
-(void) encodeWithCoder:(NSCoder*) encoder
|
||
|
{
|
||
|
[encoder encodeObject:_element forKey:@"element"];
|
||
|
[encoder encodeObject:_attributes forKey:@"attributes"];
|
||
|
[encoder encodeObject:_children forKey:@"children"];
|
||
|
[encoder encodeObject:_data forKey:@"data"];
|
||
|
}
|
||
|
|
||
|
+(BOOL) supportsSecureCoding
|
||
|
{
|
||
|
return YES;
|
||
|
}
|
||
|
|
||
|
-(id) copyWithZone:(NSZone*) zone
|
||
|
{
|
||
|
MLXMLNode* copy = [[[self class] alloc] initWithElement:[_element copy]];
|
||
|
copy.attributes = [[NSMutableDictionary alloc] initWithDictionary:_attributes copyItems:YES];
|
||
|
for(MLXMLNode* child in _children)
|
||
|
[copy addChildNode:child];
|
||
|
copy.data = _data ? [_data copy] : nil;
|
||
|
return copy;
|
||
|
}
|
||
|
|
||
|
-(id) shallowCopy
|
||
|
{
|
||
|
return [self shallowCopyWithData:NO];
|
||
|
}
|
||
|
|
||
|
-(id) shallowCopyWithData:(BOOL) copyData
|
||
|
{
|
||
|
MLXMLNode* copy = [[[self class] alloc] initWithElement:[_element copy]];
|
||
|
copy.attributes = [[NSMutableDictionary alloc] initWithDictionary:_attributes copyItems:YES];
|
||
|
if(copyData)
|
||
|
copy.data = _data ? [_data copy] : nil;
|
||
|
return copy;
|
||
|
}
|
||
|
|
||
|
-(void) handleMemoryPressureNotification
|
||
|
{
|
||
|
[self.cache removeAllObjects];
|
||
|
[self.queryEntryCache removeAllObjects];
|
||
|
DDLogVerbose(@"Removed all cached objects in this MLXMLNode due to memory pressure");
|
||
|
DDLogVerbose(@"Node: %@", self);
|
||
|
}
|
||
|
|
||
|
-(void) setXMLNS:(NSString*) xmlns
|
||
|
{
|
||
|
[_attributes setObject:[xmlns copy] forKey:@"xmlns"];
|
||
|
}
|
||
|
|
||
|
-(MLXMLNode*) addChildNode:(MLXMLNode*) child
|
||
|
{
|
||
|
if(nilExtractor(child) == nil)
|
||
|
return nil;
|
||
|
return [self addChildNodeWithoutCopy:[child copy]];
|
||
|
}
|
||
|
|
||
|
//only used by MLBaseParser to add new childs without deep-copying the object
|
||
|
-(MLXMLNode*) addChildNodeWithoutCopy:(MLXMLNode*) child
|
||
|
{
|
||
|
if(!child)
|
||
|
return nil;
|
||
|
MLXMLNode* insertedChild = child;
|
||
|
insertedChild.parent = self;
|
||
|
//namespace inheritance (will be stripped by XMLString later on)
|
||
|
//we do this here to make sure manual created nodes always have a namespace like the nodes created by the xml parser do
|
||
|
if(!insertedChild.attributes[@"xmlns"])
|
||
|
insertedChild.attributes[@"xmlns"] = [_attributes[@"xmlns"] copy];
|
||
|
[_children addObject:insertedChild];
|
||
|
[self invalidateUpstreamCache];
|
||
|
//this one can be removed if the query path component ".." is removed from our language
|
||
|
[insertedChild invalidateDownstreamCache];
|
||
|
return insertedChild;
|
||
|
}
|
||
|
|
||
|
-(MLXMLNode*) removeChildNode:(MLXMLNode*) child
|
||
|
{
|
||
|
MLXMLNode* foundChild = nil;
|
||
|
if(!child)
|
||
|
return foundChild;
|
||
|
NSInteger index = [_children indexOfObject:child];
|
||
|
if(index != NSNotFound)
|
||
|
{
|
||
|
foundChild = [_children objectAtIndex:index];
|
||
|
foundChild.parent = nil;
|
||
|
[_children removeObjectAtIndex:index];
|
||
|
[self invalidateUpstreamCache];
|
||
|
}
|
||
|
return foundChild;
|
||
|
}
|
||
|
|
||
|
-(NSArray*) children
|
||
|
{
|
||
|
return [NSArray arrayWithArray:_children];
|
||
|
}
|
||
|
|
||
|
-(void) invalidateUpstreamCache
|
||
|
{
|
||
|
//invalidate caches of all nodes upstream in our tree
|
||
|
for(MLXMLNode* node = self; node; node = node.parent)
|
||
|
[node.cache removeAllObjects];
|
||
|
}
|
||
|
|
||
|
-(void) invalidateDownstreamCache
|
||
|
{
|
||
|
[self.cache removeAllObjects];
|
||
|
for(MLXMLNode* node in _children)
|
||
|
[node invalidateDownstreamCache];
|
||
|
}
|
||
|
|
||
|
//query language similar to the one prosody uses (which in turn is loosely based on xpath)
|
||
|
//this implements a strict superset of prosody's language which makes it possible to use queries from prosody directly
|
||
|
//unlinke the language used in prosody, this returns *all* nodes mathching the query (use findFirst to get only the first match like prosody does)
|
||
|
//see https://prosody.im/doc/developers/util/stanza (function stanza:find(path)) for examples and description
|
||
|
//extensions to prosody's language:
|
||
|
//we extended this language to automatically infer the namespace from the parent element, if no namespace was given explicitly in the query
|
||
|
//we also added support for "*" as element name or namespace meaning "any nodename" / "any namespace"
|
||
|
//the additional ".." element name can be used to ascend to the parent node and do a find() on this node using the rest of the query path
|
||
|
//if you begin a path with "/" that means "begin with checking the current element", if your path does not begin with a "/"
|
||
|
//this means "begin witch checking the children of this node" (normal prosody behaviour)
|
||
|
//we also added additional extraction commands ("@attrName" and "#" are extraction commands defined within prosody):
|
||
|
//extraction command "$" returns the name of the XML element (just like "#" returns its text content)
|
||
|
//the argument "@" for extraction command "@" returns the full attribute dictionary of the XML element (full command: "@@")
|
||
|
//we also added conversion commands that can be appended to a query string:
|
||
|
//"|bool" --> convert xml string to NSNumber containing bool (XMPP defines "1"/"true" to be true and "0"/"false" to be false)
|
||
|
//"|int" --> convert xml string to NSNumber containing NSInteger
|
||
|
//"|uint" --> convert xml string to NSNumber containing NSUInteger
|
||
|
//"|double" --> convert xml string to NSNumber containing double
|
||
|
//"|datetime" --> convert xml datetime string to NSDate
|
||
|
//"|base64" --> convert base64 encoded xml string to NSData
|
||
|
//"|uuid" --> interprete xml string as NSUUID
|
||
|
//"|uuidcast" --> try to interprete xml string as UUID and convert xml string to NSUUID via sha256 transformation, if not
|
||
|
-(NSArray*) find:(NSString* _Nonnull) queryString, ... NS_FORMAT_FUNCTION(1, 2)
|
||
|
{
|
||
|
va_list args;
|
||
|
va_start(args, queryString);
|
||
|
NSArray* retval = [self find:queryString arguments:&args];
|
||
|
va_end(args);
|
||
|
return retval;
|
||
|
}
|
||
|
|
||
|
//like find: above, but only return the first match
|
||
|
-(id) findFirst:(NSString* _Nonnull) queryString, ... NS_FORMAT_FUNCTION(1, 2)
|
||
|
{
|
||
|
va_list args;
|
||
|
va_start(args, queryString);
|
||
|
id retval = [self find:queryString arguments:&args].firstObject;
|
||
|
va_end(args);
|
||
|
return retval;
|
||
|
}
|
||
|
|
||
|
//like findFirst, but only check if it would return something
|
||
|
-(BOOL) check:(NSString* _Nonnull) queryString, ... NS_FORMAT_FUNCTION(1, 2)
|
||
|
{
|
||
|
va_list args;
|
||
|
va_start(args, queryString);
|
||
|
BOOL retval = [self find:queryString arguments:&args].firstObject != nil ? YES : NO;
|
||
|
va_end(args);
|
||
|
return retval;
|
||
|
}
|
||
|
|
||
|
-(NSArray*) find:(NSString* _Nonnull) queryString arguments:(va_list*) args
|
||
|
{
|
||
|
//return our own node if the query string is empty (this makes queries like "/.." possible which will return the parent node
|
||
|
if(!queryString || [queryString isEqualToString:@""])
|
||
|
return @[self];
|
||
|
|
||
|
va_list cacheKeyArgs;
|
||
|
va_copy(cacheKeyArgs, *args);
|
||
|
NSString* cacheKey = [NSString stringWithFormat:@"%@§§%@", queryString, [[NSString alloc] initWithFormat:queryString arguments:cacheKeyArgs]];
|
||
|
va_end(cacheKeyArgs);
|
||
|
#ifdef DEBUG_XMLQueryLanguage
|
||
|
DDLogVerbose(@"Cache key: %@", cacheKey);
|
||
|
#endif
|
||
|
|
||
|
//return results from cache if possible
|
||
|
NSArray* cacheObj = nil;
|
||
|
WeakContainer* cacheEntryContainer = [self.cache objectForKey:cacheKey];
|
||
|
if(cacheEntryContainer != nil)
|
||
|
cacheObj = cacheEntryContainer.obj;
|
||
|
if(cacheObj != nil)
|
||
|
{
|
||
|
#ifdef DEBUG_XMLQueryLanguage
|
||
|
DDLogVerbose(@"Returning cached result: %@", cacheObj);
|
||
|
#endif
|
||
|
return cacheObj;
|
||
|
}
|
||
|
|
||
|
#ifdef QueryStatistics
|
||
|
@synchronized(statistics) {
|
||
|
if(!statistics[queryString])
|
||
|
statistics[queryString] = @0;
|
||
|
statistics[queryString] = [NSNumber numberWithInteger:[statistics[queryString] integerValue] + 1];
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
//shortcut syntax for queries operating directly on this node
|
||
|
//this translates "/@attr", "/#" or "/$" into their correct form "/{*}*@attr", "/{*}*#" or "/{*}*$"
|
||
|
if(
|
||
|
[queryString characterAtIndex:0] == '/' &&
|
||
|
[queryString length] >=2 &&
|
||
|
[[NSCharacterSet characterSetWithCharactersInString:@"@#$<"] characterIsMember:[queryString characterAtIndex:1]]
|
||
|
)
|
||
|
queryString = [NSString stringWithFormat:@"/{*}*%@", [queryString substringFromIndex:1]];
|
||
|
|
||
|
NSArray* results;
|
||
|
//check if the current element our our children should be queried "/" makes the path "absolute" instead of "relative"
|
||
|
if([[queryString substringToIndex:1] isEqualToString:@"/"])
|
||
|
results = [self find:[queryString substringFromIndex:1] inNodeList:@[self] arguments:args]; //absolute path (check self first)
|
||
|
else
|
||
|
results = [self find:queryString inNodeList:_children arguments:args]; //relative path (check childs first)
|
||
|
|
||
|
//update cache and return results
|
||
|
[self.cache setObject:[[WeakContainer alloc] initWithObj:results] forKey:cacheKey]; //use weak container to break retain circle
|
||
|
return results;
|
||
|
}
|
||
|
|
||
|
-(NSArray*) find:(NSString* _Nonnull) queryString inNodeList:(NSArray* _Nonnull) nodesToCheck arguments:(va_list*) args
|
||
|
{
|
||
|
//shortcut for empty nodesToCheck
|
||
|
if(![nodesToCheck count])
|
||
|
return @[];
|
||
|
NSMutableArray* results = [NSMutableArray new];
|
||
|
//split our path into first component and rest
|
||
|
NSArray* matches = [pathSplitterRegex matchesInString:queryString options:0 range:NSMakeRange(0, [queryString length])];
|
||
|
if(![matches count])
|
||
|
@throw [NSException exceptionWithName:@"RuntimeException" reason:@"XML query has syntax errors (no matches for path splitter regex)!" userInfo:@{
|
||
|
@"self": self,
|
||
|
@"queryString": queryString,
|
||
|
}];
|
||
|
NSTextCheckingResult* match = matches.firstObject;
|
||
|
NSRange pathComponent1Range = [match rangeAtIndex:1];
|
||
|
NSRange pathComponent2Range = [match rangeAtIndex:7];
|
||
|
NSRange pathComponent3Range = [match rangeAtIndex:15];
|
||
|
NSString* pathComponent1 = @"";
|
||
|
NSString* pathComponent2 = @"";
|
||
|
NSString* pathComponent3 = @"";
|
||
|
if(pathComponent1Range.location != NSNotFound && pathComponent1Range.length > 0)
|
||
|
pathComponent1 = [queryString substringWithRange:pathComponent1Range];
|
||
|
if(pathComponent2Range.location != NSNotFound && pathComponent2Range.length > 0)
|
||
|
pathComponent2 = [queryString substringWithRange:pathComponent2Range];
|
||
|
if(pathComponent3Range.location != NSNotFound && pathComponent3Range.length > 0)
|
||
|
pathComponent3 = [queryString substringWithRange:pathComponent3Range];
|
||
|
|
||
|
NSString* pathComponent = pathComponent1;
|
||
|
NSString* rest = @"";
|
||
|
if(![pathComponent2 length])
|
||
|
pathComponent = [NSString stringWithFormat:@"%@%@", pathComponent1, pathComponent3];
|
||
|
else
|
||
|
rest = [NSString stringWithFormat:@"%@%@", [pathComponent2 substringFromIndex:1], pathComponent3];
|
||
|
NSMutableDictionary* parsedEntry = [self parseQueryEntry:pathComponent arguments:args];
|
||
|
|
||
|
//check if the parent element was selected and ask our parent to check the rest of our query path if needed
|
||
|
if([pathComponent isEqualToString:@".."])
|
||
|
{
|
||
|
MLXMLNode* parent = self.parent;
|
||
|
if(!parent)
|
||
|
@throw [NSException exceptionWithName:@"RuntimeException" reason:@"XML query tries to ascend to non-existent parent element!" userInfo:@{
|
||
|
@"self": self,
|
||
|
@"queryString": queryString,
|
||
|
@"pathComponent": pathComponent,
|
||
|
@"parsedEntry": parsedEntry
|
||
|
}];
|
||
|
return [parent find:rest arguments:args];
|
||
|
}
|
||
|
|
||
|
//shortcut for dataform subqueries: allow empty element names and namespaces, they get autofilled with {jabber:x:data}x
|
||
|
if(!parsedEntry[@"elementName"] && !parsedEntry[@"namespace"] && [parsedEntry[@"extractionCommand"] isEqualToString:@"\\"])
|
||
|
{
|
||
|
parsedEntry[@"elementName"] = @"x";
|
||
|
parsedEntry[@"namespace"] = @"jabber:x:data";
|
||
|
}
|
||
|
|
||
|
if(!parsedEntry[@"elementName"] && !parsedEntry[@"namespace"])
|
||
|
@throw [NSException exceptionWithName:@"RuntimeException" reason:@"XML queries must not contain a path component having neither element name nor namespace!" userInfo:@{
|
||
|
@"self": self,
|
||
|
@"queryString": queryString,
|
||
|
@"pathComponent": pathComponent,
|
||
|
@"parsedEntry": parsedEntry
|
||
|
}];
|
||
|
|
||
|
//"*" is just syntactic sugar for an empty element name
|
||
|
//(but empty element names are not allowed if no namespace was given, which makes this sugar mandatory in this case)
|
||
|
if(parsedEntry[@"elementName"] && [parsedEntry[@"elementName"] isEqualToString:@"*"])
|
||
|
[parsedEntry removeObjectForKey:@"elementName"];
|
||
|
|
||
|
//if no namespace was given, use the parent one (no namespace means the namespace will be inherited)
|
||
|
//this will allow all namespaces "{*}" if the nodes in nodesToCheck don't have a parent at all
|
||
|
if((!parsedEntry[@"namespace"] || [parsedEntry[@"namespace"] isEqualToString:@""]) && ((MLXMLNode*)nodesToCheck[0]).parent)
|
||
|
parsedEntry[@"namespace"] = ((MLXMLNode*)nodesToCheck[0]).parent.attributes[@"xmlns"]; //all nodesToCheck have the same parent, just pick the first one
|
||
|
|
||
|
//"*" is just syntactic sugar for an empty namespace name which means "any namespace allowed"
|
||
|
//(but empty namespaces are only allowed in internal methods, which makes this sugar mandatory)
|
||
|
//don't confuse this with a query without namespace which will result in a query using the parent's namespace, not "any namespace allowed"!
|
||
|
if(parsedEntry[@"namespace"] && [parsedEntry[@"namespace"] isEqualToString:@"*"])
|
||
|
[parsedEntry removeObjectForKey:@"namespace"];
|
||
|
|
||
|
//element names can be negated
|
||
|
BOOL negatedElementName = NO;
|
||
|
if(parsedEntry[@"elementName"] && [parsedEntry[@"elementName"] characterAtIndex:0] == '!')
|
||
|
{
|
||
|
negatedElementName = YES;
|
||
|
parsedEntry[@"elementName"] = [parsedEntry[@"elementName"] substringFromIndex:1];
|
||
|
}
|
||
|
|
||
|
//iterate through nodesToCheck (containing only us, our parent's children or our own children)
|
||
|
//and check if they match the current path component (e.g. parsedEntry)
|
||
|
for(MLXMLNode* node in nodesToCheck)
|
||
|
{
|
||
|
//check element name and namespace (if given)
|
||
|
if(
|
||
|
(
|
||
|
(negatedElementName && ![parsedEntry[@"elementName"] isEqualToString:node.element]) ||
|
||
|
(!parsedEntry[@"elementName"] || [parsedEntry[@"elementName"] isEqualToString:node.element])
|
||
|
) &&
|
||
|
(!parsedEntry[@"namespace"] || [parsedEntry[@"namespace"] isEqualToString:node.attributes[@"xmlns"]])
|
||
|
) {
|
||
|
//check for attribute filters (if given)
|
||
|
if(parsedEntry[@"attributeFilters"] && [parsedEntry[@"attributeFilters"] count])
|
||
|
{
|
||
|
BOOL ok = YES;
|
||
|
for(NSDictionary* filter in parsedEntry[@"attributeFilters"])
|
||
|
{
|
||
|
if(node.attributes[filter[@"name"]])
|
||
|
{
|
||
|
NSArray* matches = [filter[@"value"] matchesInString:node.attributes[filter[@"name"]] options:0 range:NSMakeRange(0, [node.attributes[filter[@"name"]] length])];
|
||
|
if(![matches count])
|
||
|
{
|
||
|
ok = NO; //this node does *not* fullfill the attribute filter regex
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
ok = NO;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if(!ok)
|
||
|
continue; //this node does *not* fullfill the attribute filter regex
|
||
|
}
|
||
|
//check if we should process an extraction command (only allowed if we're at the end of the query)
|
||
|
if(parsedEntry[@"extractionCommand"])
|
||
|
{
|
||
|
//sanity check
|
||
|
if([rest length] > 0)
|
||
|
@throw [NSException exceptionWithName:@"RuntimeException" reason:@"Extraction commands are only allowed for terminal nodes of XML queries!" userInfo:@{
|
||
|
@"self": self,
|
||
|
@"node": node,
|
||
|
@"queryString": queryString,
|
||
|
@"pathComponent": pathComponent,
|
||
|
@"parsedEntry": parsedEntry
|
||
|
}];
|
||
|
|
||
|
id singleResult = nil;
|
||
|
if([parsedEntry[@"extractionCommand"] isEqualToString:@"#"] && node.data)
|
||
|
singleResult = [self processConversionCommand:parsedEntry[@"conversionCommand"] forXMLString:node.data];
|
||
|
else if([parsedEntry[@"extractionCommand"] isEqualToString:@"@"] && node.attributes[parsedEntry[@"attribute"]])
|
||
|
singleResult = [self processConversionCommand:parsedEntry[@"conversionCommand"] forXMLString:node.attributes[parsedEntry[@"attribute"]]];
|
||
|
else if([parsedEntry[@"extractionCommand"] isEqualToString:@"$"] && node.element)
|
||
|
singleResult = [self processConversionCommand:parsedEntry[@"conversionCommand"] forXMLString:node.element];
|
||
|
else if([parsedEntry[@"extractionCommand"] isEqualToString:@"\\"])
|
||
|
{
|
||
|
if(![node respondsToSelector:NSSelectorFromString(@"processDataFormQuery:")])
|
||
|
@throw [NSException exceptionWithName:@"RuntimeException" reason:@"Data form extractions can only be used on data forms! This exception means you have a bug somewhere else in your code (probably at the source of the element you are trying to use in your data form query)!" userInfo:@{
|
||
|
@"self": self,
|
||
|
@"node": node,
|
||
|
@"queryString": queryString,
|
||
|
@"pathComponent": pathComponent,
|
||
|
@"parsedEntry": parsedEntry
|
||
|
}];
|
||
|
//faster than NSMethodInvocation, but way less readable, see https://stackoverflow.com/a/20058585/3528174
|
||
|
id extraction = ((id (*)(id, SEL, NSString*))[node methodForSelector:NSSelectorFromString(@"processDataFormQuery:")])(node, NSSelectorFromString(@"processDataFormQuery:"), parsedEntry[@"dataFormQuery"]);
|
||
|
if(extraction) //only add this to our results if the data form query succeeded
|
||
|
{
|
||
|
//check if we try to operate a conversion command on something not a single extracted simple form field of type NSString
|
||
|
if(parsedEntry[@"conversionCommand"] && ![extraction isKindOfClass:[NSString class]])
|
||
|
@throw [NSException exceptionWithName:@"RuntimeException" reason:@"Conversion commands can not be used on data form extractions returning the whole data form or an NSArray/NSDictionary!" userInfo:@{
|
||
|
@"self": self,
|
||
|
@"node": node,
|
||
|
@"queryString": queryString,
|
||
|
@"pathComponent": pathComponent,
|
||
|
@"parsedEntry": parsedEntry
|
||
|
}];
|
||
|
singleResult = [self processConversionCommand:parsedEntry[@"conversionCommand"] forXMLString:(NSString*)extraction];
|
||
|
}
|
||
|
}
|
||
|
else if([parsedEntry[@"extractionCommand"] isEqualToString:@"@"] && [parsedEntry[@"attribute"] isEqualToString:@"@"])
|
||
|
{
|
||
|
if(parsedEntry[@"conversionCommand"])
|
||
|
@throw [NSException exceptionWithName:@"RuntimeException" reason:@"Conversion commands can not be used on attribute dict extractions (e.g. extraction command '@@')!" userInfo:@{
|
||
|
@"self": self,
|
||
|
@"node": node,
|
||
|
@"queryString": queryString,
|
||
|
@"pathComponent": pathComponent,
|
||
|
@"parsedEntry": parsedEntry
|
||
|
}];
|
||
|
singleResult = node.attributes;
|
||
|
}
|
||
|
if(singleResult)
|
||
|
[results addObject:singleResult];
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if(parsedEntry[@"conversionCommand"])
|
||
|
@throw [NSException exceptionWithName:@"RuntimeException" reason:@"Conversion commands are only allowed for terminal nodes of XML queries that use an extraction command!" userInfo:@{
|
||
|
@"self": self,
|
||
|
@"node": node,
|
||
|
@"queryString": queryString,
|
||
|
@"pathComponent": pathComponent,
|
||
|
@"parsedEntry": parsedEntry
|
||
|
}];
|
||
|
if([rest length] > 0) //we should descent to this node
|
||
|
[results addObjectsFromArray:[node find:rest arguments:args]]; //this will cache the subquery on this node, too
|
||
|
else //we should not descent to this node (we reached the end of our query)
|
||
|
[results addObject:node];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//DDLogVerbose(@"*** DEBUG(%@)[%@] ***\n%@\n%@\n%@", queryString, pathComponent, parsedEntry, results, nodesToCheck);
|
||
|
return [results copy]; //return readonly copy of results
|
||
|
}
|
||
|
|
||
|
-(id) processConversionCommand:(NSString*) command forXMLString:(NSString* _Nonnull) string
|
||
|
{
|
||
|
if(!string)
|
||
|
return nil;
|
||
|
if([command isEqualToString:@"bool"])
|
||
|
{
|
||
|
//xml bools as defined in xmpp core RFC
|
||
|
if([string isEqualToString:@"1"] || [string isEqualToString:@"true"])
|
||
|
return @YES;
|
||
|
else if([string isEqualToString:@"0"] || [string isEqualToString:@"false"])
|
||
|
return @NO;
|
||
|
else
|
||
|
return @NO; //no bool at all, return false
|
||
|
}
|
||
|
else if([command isEqualToString:@"int"])
|
||
|
return [NSNumber numberWithInteger:(NSInteger)[string integerValue]];
|
||
|
else if([command isEqualToString:@"uint"])
|
||
|
return [NSNumber numberWithUnsignedInteger:(NSUInteger)[string longLongValue]];
|
||
|
else if([command isEqualToString:@"double"])
|
||
|
return [NSNumber numberWithDouble:[string doubleValue]];
|
||
|
else if([command isEqualToString:@"datetime"])
|
||
|
return [HelperTools parseDateTimeString:string];
|
||
|
else if([command isEqualToString:@"base64"])
|
||
|
return [HelperTools dataWithBase64EncodedString:string];
|
||
|
else if([command isEqualToString:@"uuid"])
|
||
|
return [[NSUUID alloc] initWithUUIDString:string];
|
||
|
else if([command isEqualToString:@"uuidcast"])
|
||
|
{
|
||
|
NSUUID* uuid = [[NSUUID alloc] initWithUUIDString:string];
|
||
|
if(uuid != nil)
|
||
|
return uuid;
|
||
|
return [HelperTools stringToUUID:string];
|
||
|
}
|
||
|
else
|
||
|
return string;
|
||
|
}
|
||
|
|
||
|
-(NSMutableDictionary*) parseQueryEntry:(NSString* _Nonnull) entry arguments:(va_list*) args
|
||
|
{
|
||
|
va_list cacheKeyArgs;
|
||
|
va_copy(cacheKeyArgs, *args);
|
||
|
NSString* cacheKey = [NSString stringWithFormat:@"%@§§%@", entry, [[NSString alloc] initWithFormat:entry arguments:cacheKeyArgs]];
|
||
|
va_end(cacheKeyArgs);
|
||
|
#ifdef DEBUG_XMLQueryLanguage
|
||
|
DDLogVerbose(@"Cache key: %@", cacheKey);
|
||
|
#endif
|
||
|
|
||
|
//return results from cache if possible
|
||
|
NSDictionary* cacheEntry = [self.queryEntryCache objectForKey:cacheKey];
|
||
|
if(cacheEntry != nil)
|
||
|
{
|
||
|
#ifdef DEBUG_XMLQueryLanguage
|
||
|
DDLogDebug(@"Returning cached result: %@", cacheEntry);
|
||
|
#endif
|
||
|
return [cacheEntry mutableCopy];
|
||
|
}
|
||
|
|
||
|
NSMutableDictionary* retval = [NSMutableDictionary new];
|
||
|
NSArray* matches = [componentParserRegex matchesInString:entry options:0 range:NSMakeRange(0, [entry length])];
|
||
|
if(![matches count])
|
||
|
@throw [NSException exceptionWithName:@"RuntimeException" reason:@"Could not parse path component!" userInfo:@{
|
||
|
@"self": self,
|
||
|
@"queryEntry": entry
|
||
|
}];
|
||
|
NSTextCheckingResult* match = matches.firstObject;
|
||
|
NSRange namespaceRange = [match rangeAtIndex:2];
|
||
|
NSRange elementNameRange = [match rangeAtIndex:3];
|
||
|
NSRange attributeFilterRange = [match rangeAtIndex:4];
|
||
|
NSRange extractionCommandRange = [match rangeAtIndex:7];
|
||
|
NSRange conversionCommandRange = [match rangeAtIndex:9];
|
||
|
if(namespaceRange.location != NSNotFound)
|
||
|
retval[@"namespace"] = [entry substringWithRange:namespaceRange];
|
||
|
if(elementNameRange.location != NSNotFound)
|
||
|
retval[@"elementName"] = [entry substringWithRange:elementNameRange];
|
||
|
if(attributeFilterRange.location != NSNotFound && attributeFilterRange.length > 0)
|
||
|
{
|
||
|
retval[@"attributeFilters"] = [NSMutableArray new];
|
||
|
NSString* attributeFilters = [entry substringWithRange:attributeFilterRange];
|
||
|
#ifdef DEBUG_XMLQueryLanguage
|
||
|
DDLogDebug(@"Extracting attribute filters: '%@'...", attributeFilters);
|
||
|
#endif
|
||
|
NSArray* attributeFilterMatches = [attributeFilterRegex matchesInString:attributeFilters options:0 range:NSMakeRange(0, [attributeFilters length])];
|
||
|
for(NSTextCheckingResult* attributeFilterMatch in attributeFilterMatches)
|
||
|
{
|
||
|
NSRange attributeFilterNameRange = [attributeFilterMatch rangeAtIndex:1];
|
||
|
NSRange attributeFilterTypeRange = [attributeFilterMatch rangeAtIndex:2];
|
||
|
NSRange attributeFilterValueRange = [attributeFilterMatch rangeAtIndex:3];
|
||
|
if(attributeFilterNameRange.location == NSNotFound || attributeFilterTypeRange.location == NSNotFound || attributeFilterValueRange.location == NSNotFound)
|
||
|
@throw [NSException exceptionWithName:@"RuntimeException" reason:@"Attribute filter not complete!" userInfo:@{
|
||
|
@"self": self,
|
||
|
@"queryEntry": entry,
|
||
|
@"attributeFilters": attributeFilters
|
||
|
}];
|
||
|
|
||
|
NSString* attributeFilterName = [attributeFilters substringWithRange:attributeFilterNameRange];
|
||
|
unichar attributeFilterType = [[attributeFilters substringWithRange:attributeFilterTypeRange] characterAtIndex:0];
|
||
|
NSString* attributeFilterValue = [attributeFilters substringWithRange:attributeFilterValueRange];
|
||
|
|
||
|
NSString* attributeFilterValueRegexPattern;
|
||
|
if(attributeFilterType == '=') //verbatim comparison using format string interpolation
|
||
|
{
|
||
|
//substitute format string specifiers inside of our attribute filter string.
|
||
|
//use Holger's vsnprintf() which got pimped up to support %@ format specifier.
|
||
|
//use va_list* everywhere to make sure we move the same va_list pointer in every invocation here
|
||
|
//instead of starting with a fresh copy (which would always extract only the first variadic argument
|
||
|
//regardless of the position in the format string we are at).
|
||
|
char* dest = NULL;
|
||
|
if(rpl_vasprintf(&dest, [attributeFilterValue UTF8String], args) == -1)
|
||
|
[NSException raise:@"NSInternalInconsistencyException" format:@"failed malloc in MLXMLNode's usage of rpl_vasprintf" arguments:nil];
|
||
|
MLAssert(dest != NULL, @"dest should *never* be NULL!");
|
||
|
NSString* unescapedAttributeFilterValue = [NSString stringWithUTF8String:dest];
|
||
|
free(dest);
|
||
|
|
||
|
NSString* escapedAttributeFilterValue = [NSRegularExpression escapedPatternForString:unescapedAttributeFilterValue];
|
||
|
attributeFilterValueRegexPattern = [NSString stringWithFormat:@"^%@$", escapedAttributeFilterValue];
|
||
|
#ifdef DEBUG_XMLQueryLanguage
|
||
|
DDLogDebug(@"unescapedAttributeFilterValue: '%@'", unescapedAttributeFilterValue);
|
||
|
DDLogDebug(@"escapedAttributeFilterValue: '%@'", escapedAttributeFilterValue);
|
||
|
DDLogDebug(@"attributeFilterValueRegexPattern: '%@'", attributeFilterValueRegexPattern);
|
||
|
#endif
|
||
|
}
|
||
|
else if(attributeFilterType == '~') //raw regex comparison *without* format string interpolation
|
||
|
//you will have to include sring-start and string-end markers yourself as well as all other regex stuff
|
||
|
attributeFilterValueRegexPattern = attributeFilterValue;
|
||
|
else
|
||
|
@throw [NSException exceptionWithName:@"RuntimeException" reason:@"Internal attribute filter bug, this should never happen!" userInfo:@{
|
||
|
@"self": self,
|
||
|
@"queryEntry": entry,
|
||
|
@"attributeFilters": attributeFilters
|
||
|
}];
|
||
|
|
||
|
NSError* error;
|
||
|
[retval[@"attributeFilters"] addObject:@{
|
||
|
@"name": attributeFilterName,
|
||
|
//this regex will be cached in parsed form in the local cache of this method
|
||
|
@"value": [NSRegularExpression regularExpressionWithPattern:attributeFilterValueRegexPattern options:NSRegularExpressionCaseInsensitive error:&error]
|
||
|
}];
|
||
|
if(error)
|
||
|
@throw [NSException exceptionWithName:@"RuntimeException" reason:@"Attribute filter regex can not be compiled!" userInfo:@{
|
||
|
@"self": self,
|
||
|
@"queryEntry": entry,
|
||
|
@"filterType": @(attributeFilterType),
|
||
|
@"filterName": attributeFilterName,
|
||
|
@"filterValue": attributeFilterValue,
|
||
|
@"error": error
|
||
|
}];
|
||
|
}
|
||
|
#ifdef DEBUG_XMLQueryLanguage
|
||
|
DDLogDebug(@"Done extracting, attributeFilters are now: %@", retval[@"attributeFilters"]);
|
||
|
#endif
|
||
|
}
|
||
|
if(extractionCommandRange.location != NSNotFound)
|
||
|
{
|
||
|
NSString* extractionCommand = [entry substringWithRange:extractionCommandRange];
|
||
|
retval[@"extractionCommand"] = [extractionCommand substringToIndex:1];
|
||
|
unichar command = [extractionCommand characterAtIndex:0];
|
||
|
if(command == '@')
|
||
|
retval[@"attribute"] = [extractionCommand substringFromIndex:1];
|
||
|
if(command == '\\')
|
||
|
retval[@"dataFormQuery"] = [extractionCommand substringWithRange:NSMakeRange(1, extractionCommandRange.length-2)];
|
||
|
}
|
||
|
if(conversionCommandRange.location != NSNotFound)
|
||
|
retval[@"conversionCommand"] = [entry substringWithRange:conversionCommandRange];
|
||
|
[self.queryEntryCache setObject:[retval copy] forKey:cacheKey];
|
||
|
return retval;
|
||
|
}
|
||
|
|
||
|
+(NSString*) escapeForXMPP:(NSString*) targetString
|
||
|
{
|
||
|
NSMutableString* mutable = [targetString mutableCopy];
|
||
|
[mutable replaceOccurrencesOfString:@"&" withString:@"&" options:NSLiteralSearch range:NSMakeRange(0, mutable.length)];
|
||
|
[mutable replaceOccurrencesOfString:@"<" withString:@"<" options:NSLiteralSearch range:NSMakeRange(0, mutable.length)];
|
||
|
[mutable replaceOccurrencesOfString:@">" withString:@">" options:NSLiteralSearch range:NSMakeRange(0, mutable.length)];
|
||
|
[mutable replaceOccurrencesOfString:@"'" withString:@"'" options:NSLiteralSearch range:NSMakeRange(0, mutable.length)];
|
||
|
[mutable replaceOccurrencesOfString:@"\"" withString:@""" options:NSLiteralSearch range:NSMakeRange(0, mutable.length)];
|
||
|
return [mutable copy];
|
||
|
}
|
||
|
|
||
|
-(NSString*) XMLString
|
||
|
{
|
||
|
if(!_element)
|
||
|
return @""; // sanity check
|
||
|
|
||
|
//special handling of xml start tag
|
||
|
if([_element isEqualToString:@"__xml"])
|
||
|
return [NSString stringWithFormat:@"<?xml version='1.0'?>"];
|
||
|
|
||
|
NSMutableString* outputString = [NSMutableString new];
|
||
|
[outputString appendString:[NSString stringWithFormat:@"<%@", _element]];
|
||
|
|
||
|
//set attributes
|
||
|
MLXMLNode* parent = self.parent;
|
||
|
for(NSString* key in [_attributes allKeys])
|
||
|
{
|
||
|
//handle xmlns inheritance (don't add namespace to childs if it should be the same like the parent's one)
|
||
|
if([key isEqualToString:@"xmlns"] && parent && [[NSString stringWithFormat:@"%@", _attributes[@"xmlns"]] isEqualToString:[NSString stringWithFormat:@"%@", parent.attributes[@"xmlns"]]])
|
||
|
continue;
|
||
|
[outputString appendString:[NSString stringWithFormat:@" %@='%@'", key, [MLXMLNode escapeForXMPP:[NSString stringWithFormat:@"%@", _attributes[key]]]]];
|
||
|
}
|
||
|
|
||
|
if([_children count] || (_data && ![_data isEqualToString:@""]))
|
||
|
{
|
||
|
[outputString appendString:[NSString stringWithFormat:@">"]];
|
||
|
|
||
|
//set children here
|
||
|
for(MLXMLNode* child in _children)
|
||
|
[outputString appendString:[child XMLString]];
|
||
|
|
||
|
if(_data)
|
||
|
[outputString appendString:[MLXMLNode escapeForXMPP:_data]];
|
||
|
|
||
|
//dont close stream element
|
||
|
if(![_element isEqualToString:@"stream:stream"] && ![_element isEqualToString:@"/stream:stream"])
|
||
|
[outputString appendString:[NSString stringWithFormat:@"</%@>", _element]];
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
//dont close stream element
|
||
|
if(![_element isEqualToString:@"stream:stream"] && ![_element isEqualToString:@"/stream:stream"])
|
||
|
[outputString appendString:[NSString stringWithFormat:@"/>"]];
|
||
|
else
|
||
|
[outputString appendString:[NSString stringWithFormat:@">"]];
|
||
|
}
|
||
|
|
||
|
return (NSString*)outputString;
|
||
|
}
|
||
|
|
||
|
-(NSString*) description
|
||
|
{
|
||
|
return [self XMLString];
|
||
|
}
|
||
|
|
||
|
@end
|