NSXMLParser and initWithStream

  • I'm trying to use the NSStream classes to parse incoming incremental XML data. The data is never a complete XML Document, but I want to receive and process it in incremental chunks based off how much ever the socket can read.

    Looking at the documentation for NSXMLParser, it seems like the initWithStream: method to initialize a NSXMLParser would be the perfect solution to my problem. I can initialize the parser with a NSInputStream and then call the parse method on NSXMLParser whenever I receive data over my socket which should in turn call the NSXMLParser delegates.

    However, I'm not seeing any of the delegates being called, the only method I see being called is the stream delegate stream:handleEvent:. There seems to be little to no examples of this API from Apple or other developers. I could use libxml2 directly to accomplish what I want, but I am hoping I am just using NSXMLParser incorrectly.

    Here is the source for what I am trying:

    ContentParser.h:

        @interface ContentParser : NSObject <NSStreamDelegate,
                                            NSXMLParserDelegate>
        {
          NSInputStream *inputStream;
          NSOutputStream *outputStream;
          NSMutableData *receivedData;
          NSXMLParser *xmlParser;
        }
        - (void)initStream;

    ContentParser.m:

        @implementation ContentParser

        - (void)initStream
        {
          CFReadStreamRef readStream;
          CFWriteStreamRef writeStream;

          CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault,
                                            (CFStringRef)@"<hostname>",
                                            <port>,
                                            &readStream,
                                            &writeStream);

          inputStream = (__bridge NSInputStream *)readStream;
          outputStream = (__bridge NSOutputStream *)writeStream;

          inputStream.delegate = self;
          outputStream.delegate = self;

          [inputStream  scheduleInRunLoop:[NSRunLoop currentRunLoop]
                                  forMode:NSDefaultRunLoopMode];
          [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
                                  forMode:NSDefaultRunLoopMode];

          [inputStream open];
          [outputStream open];

          xmlParser = [[NSXMLParser alloc] initWithStream:inputStream];
          [xmlParser setDelegate:self];
        }

        - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName
                                                namespaceURI:(NSString *)namespaceURI
                                              qualifiedName:(NSString *)qName
                                                  attributes:(NSDictionary *)attributeDict
        {
          NSLog(@"didStartElement: %@, attributeDict: %@", elementName, attributeDict);
        }

        - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
        {
          NSLog(@"foundCharacters: %@", string);
        }

        - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName
                                              namespaceURI:(NSString *)namespaceURI
                                            qualifiedName:(NSString *)qName
        {
          NSLog(@"didEndElement: %@", elementName);
        }

        - (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError
        {
          NSLog(@"Error %ld, Description: %@, Line: %ld, Column: %ld",
              [parseError code], [[parser parserError] localizedDescription],
              [parser lineNumber], [parser columnNumber]);
        }

        - (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
        {
          switch (eventCode) {
              case NSStreamEventHasBytesAvailable:
              {
                  if (stream == inputStream) {
                      NSInputStream *is = (NSInputStream *)stream;
                      if (receivedData == nil) {
                          receivedData = [[NSMutableData alloc] init];
                      }

                      uint8_t buf[1024];
                      NSInteger bytesRead = [is read:buf maxLength:1024];
                      if (bytesRead == -1) {
                          NSLog(@"Network read error");
                      } else if (bytesRead == 0) {
                          NSLog(@"No buffer received");
                      } else {
                          [receivedData appendBytes:buf length:bytesRead];
                          BOOL parserResult = [xmlParser parse];
                          if (parserResult == NO) {
                            NSLog(@"Unable to parse XML");
                          }
                      }
                  }
                  break;
              }
              default:
                  break;
            }
        }

        @end
  • On 27 May 2012, at 7:14 PM, John Drake wrote:

    > Looking at the documentation for NSXMLParser, it seems like the initWithStream: method to initialize a NSXMLParser would be the perfect solution to my problem. I can initialize the parser with a NSInputStream and then call the parse method on NSXMLParser whenever I receive data over my socket which should in turn call the NSXMLParser delegates.

    The documentation for -[NSXMLParser -initWithStream:] is sparse, but I see that the description is "Initializes the receiver with the XML contents from the specified stream _and parses it_" [EA]. I guess that there should be no need to call -[NSXMLParser parse] on the incoming data; the whole point of hooking the parser up to a stream should be that you don't have to manage the stream yourself, right?

    > However, I'm not seeing any of the delegates being called, the only method I see being called is the stream delegate stream:handleEvent:.

    I wonder if it's productive to make your ContentParser the delegate** of the input stream. It's the NSXMLParser that's interested in the incoming data. The convention in the Delegate pattern is that objects have at most one delegate. I do wonder why your setting of .delegate persists after you pass the stream into the parser.

    — F

    ** In Cocoa parlance, a "delegate" is an object that implements the methods of a delegate protocol (in this case your ContentParser is the delegate of the parser). The methods themselves are "delegate methods." The latter term may be awkward, but "delegate" has to be saved for "the object to which functionality is delegated."

    --
    Fritz Anderson
    Xcode 4 Unleashed: Don't bring your bathroom copy into the kitchen — were you raised in a barn?
    <http://x4u.manoverboard.org/>
  • I thought the same when I first read the documentation, and have tried not making ContentParser the delegate of the input stream as well as removing the call to -[NSXMLParser parse], however I still don't see any of the NSXMLParser delegates being called.
    I'm wondering if it's a run loop issue, and the delegates are not in the same thread the input stream and therefore don't get called? I don't know of any way to test which thread the stream is in or how to place the delegates in a different thread? 
    ----------------------------------------
    > Subject: Re: NSXMLParser and initWithStream
    > From: <fritza...>
    > Date: Mon, 28 May 2012 07:44:16 -0500
    > CC: <cocoa-dev...>
    > To: <smq4z1...>
    >
    > On 27 May 2012, at 7:14 PM, John Drake wrote:
    >
    >> Looking at the documentation for NSXMLParser, it seems like the initWithStream: method to initialize a NSXMLParser would be the perfect solution to my problem. I can initialize the parser with a NSInputStream and then call the parse method on NSXMLParser whenever I receive data over my socket which should in turn call the NSXMLParser delegates.
    >
    > The documentation for -[NSXMLParser -initWithStream:] is sparse, but I see that the description is "Initializes the receiver with the XML contents from the specified stream _and parses it_" [EA]. I guess that there should be no need to call -[NSXMLParser parse] on the incoming data; the whole point of hooking the parser up to a stream should be that you don't have to manage the stream yourself, right?
    >
    >> However, I'm not seeing any of the delegates being called, the only method I see being called is the stream delegate stream:handleEvent:.
    >
    >
    > I wonder if it's productive to make your ContentParser the delegate** of the input stream. It's the NSXMLParser that's interested in the incoming data. The convention in the Delegate pattern is that objects have at most one delegate. I do wonder why your setting of .delegate persists after you pass the stream into the parser.
    >
    > — F
    >
    > ** In Cocoa parlance, a "delegate" is an object that implements the methods of a delegate protocol (in this case your ContentParser is the delegate of the parser). The methods themselves are "delegate methods." The latter term may be awkward, but "delegate" has to be saved for "the object to which functionality is delegated."
    >
    > --
    > Fritz Anderson
    > Xcode 4 Unleashed: Don't bring your bathroom copy into the kitchen — were you raised in a barn?
    > <http://x4u.manoverboard.org/>
    >
  • I figured out what the problem was and answering it here incase anyone else runs into this problem in the future since +[NSXMLParser initWithStream] doesn't have a lot lot of documentation out there. 
    I needed to call -[NSXMLParser parse] right after I allocate NSXMLParser and set myself as delegate. But because it's a synchronous function, I need to call it another thread so I don't block the current thread and it can receive the NSStream events. I also don't need to make myself the delegate for NSInputStream.
    This can be done pretty simply using Grand Central Dispatch (GCD) like so:
    // alloc and init the xml parserxmlParser = [[NSXMLParser alloc] initWithStream:inputStream];[xmlParser setDelegate:self];
    // block to executedispatch_block_t dispatch_block = ^(void){    [xmlParser parse];};
    // create a queue with a unique namedispatch_queue_t dispatch_queue = dispatch_queue_create("parser.queue", NULL);
    // dispatch queuedispatch_async(dispatch_queue, dispatch_block);
    // cleanupdispatch_release(dispatch_queue);

    And here is the complete working example, just incase anyone wasn't able to follow what I posted above.
    ContentParser.h
    @interface ContentParser : NSObject <NSStreamDelegate,                                      NSXMLParserDelegate>{   NSInputStream *inputStream;   NSOutputStream *outputStream;   NSMutableData *receivedData;   NSXMLParser *xmlParser;}- (void)initStream;ContentParser.m
    @implementation ContentParser
    - (void)initStream{       CFReadStreamRef readStream;   CFWriteStreamRef writeStream;
       CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault,                                      (CFStringRef)@"<hostname>",                                      <port>,                                      &readStream,                                      &writeStream);
       inputStream = (__bridge NSInputStream *)readStream;   outputStream = (__bridge NSOutputStream *)writeStream;
       outputStream.delegate = self;
       [inputStream  scheduleInRunLoop:[NSRunLoop currentRunLoop]                           forMode:NSDefaultRunLoopMode];   [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]                            forMode:NSDefaultRunLoopMode];
       [inputStream open];   [outputStream open];
       xmlParser = [[NSXMLParser alloc] initWithStream:inputStream];   [xmlParser setDelegate:self];
       dispatch_block_t dispatch_block = ^(void)   {      [xmlParser parse];   };
       dispatch_queue_t dispatch_queue = dispatch_queue_create("parser.queue", NULL);   dispatch_async(dispatch_queue, dispatch_block);   dispatch_release(dispatch_queue);}
    - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName                                         namespaceURI:(NSString *)namespaceURI                                        qualifiedName:(NSString *)qName                                       attributes:(NSDictionary *)attributeDict{   NSLog(@"didStartElement: %@, attributeDict: %@", elementName, attributeDict);}
    - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{   NSLog(@"foundCharacters: %@", string);}
    - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName                                       namespaceURI:(NSString *)namespaceURI                                      qualifiedName:(NSString *)qName{   NSLog(@"didEndElement: %@", elementName);}
    - (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError{   NSLog(@"Error %ld, Description: %@, Line: %ld, Column: %ld",       [parseError code], [[parser parserError] localizedDescription],       [parser lineNumber], [parser columnNumber]);}

    - (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode{   switch (eventCode) {      case NSStreamEventHasSpaceAvailable:      {         /* write bytes to socket */         break;      }      default:         break;    }}
    @end
    ----------------------------------------
    > From: <smq4z1...>
    > Subject: NSXMLParser and initWithStream
    > Date: Sun, 27 May 2012 20:14:16 -0400
    > To: <cocoa-dev...>
    >
    > I'm trying to use the NSStream classes to parse incoming incremental XML data. The data is never a complete XML Document, but I want to receive and process it in incremental chunks based off how much ever the socket can read.
    >
    > Looking at the documentation for NSXMLParser, it seems like the initWithStream: method to initialize a NSXMLParser would be the perfect solution to my problem. I can initialize the parser with a NSInputStream and then call the parse method on NSXMLParser whenever I receive data over my socket which should in turn call the NSXMLParser delegates.
    >
    > However, I'm not seeing any of the delegates being called, the only method I see being called is the stream delegate stream:handleEvent:. There seems to be little to no examples of this API from Apple or other developers. I could use libxml2 directly to accomplish what I want, but I am hoping I am just using NSXMLParser incorrectly.
    >
    > Here is the source for what I am trying:
    >
    > ContentParser.h:
    >
    > @interface ContentParser : NSObject <NSStreamDelegate,
    > NSXMLParserDelegate>
    > {
    > NSInputStream *inputStream;
    > NSOutputStream *outputStream;
    > NSMutableData *receivedData;
    > NSXMLParser *xmlParser;
    > }
    > - (void)initStream;
    >
    > ContentParser.m:
    >
    > @implementation ContentParser
    >
    > - (void)initStream
    > {
    > CFReadStreamRef readStream;
    > CFWriteStreamRef writeStream;
    >
    > CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault,
    > (CFStringRef)@"<hostname>",
    > <port>,
    > &readStream,
    > &writeStream);
    >
    > inputStream = (__bridge NSInputStream *)readStream;
    > outputStream = (__bridge NSOutputStream *)writeStream;
    >
    > inputStream.delegate = self;
    > outputStream.delegate = self;
    >
    > [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
    > forMode:NSDefaultRunLoopMode];
    > [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
    > forMode:NSDefaultRunLoopMode];
    >
    > [inputStream open];
    > [outputStream open];
    >
    > xmlParser = [[NSXMLParser alloc] initWithStream:inputStream];
    > [xmlParser setDelegate:self];
    > }
    >
    > - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName
    > namespaceURI:(NSString *)namespaceURI
    > qualifiedName:(NSString *)qName
    > attributes:(NSDictionary *)attributeDict
    > {
    > NSLog(@"didStartElement: %@, attributeDict: %@", elementName, attributeDict);
    > }
    >
    > - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
    > {
    > NSLog(@"foundCharacters: %@", string);
    > }
    >
    > - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName
    > namespaceURI:(NSString *)namespaceURI
    > qualifiedName:(NSString *)qName
    > {
    > NSLog(@"didEndElement: %@", elementName);
    > }
    >
    > - (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError
    > {
    > NSLog(@"Error %ld, Description: %@, Line: %ld, Column: %ld",
    > [parseError code], [[parser parserError] localizedDescription],
    > [parser lineNumber], [parser columnNumber]);
    > }
    >
    >
    > - (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
    > {
    > switch (eventCode) {
    > case NSStreamEventHasBytesAvailable:
    > {
    > if (stream == inputStream) {
    > NSInputStream *is = (NSInputStream *)stream;
    > if (receivedData == nil) {
    > receivedData = [[NSMutableData alloc] init];
    > }
    >
    > uint8_t buf[1024];
    > NSInteger bytesRead = [is read:buf maxLength:1024];
    > if (bytesRead == -1) {
    > NSLog(@"Network read error");
    > } else if (bytesRead == 0) {
    > NSLog(@"No buffer received");
    > } else {
    > [receivedData appendBytes:buf length:bytesRead];
    > BOOL parserResult = [xmlParser parse];
    > if (parserResult == NO) {
    > NSLog(@"Unable to parse XML");
    > }
    > }
    > }
    > break;
    > }
    > default:
    > break;
    > }
    > }
    >
    > @end
  • On 31 May 2012, at 1:14 AM, John Drake wrote:

    > I figured out what the problem was and answering it here incase anyone else runs into this problem in the future since +[NSXMLParser initWithStream] doesn't have a lot lot of documentation out there. I needed to call -[NSXMLParser parse] right after I allocate NSXMLParser and set myself as delegate. But because it's a synchronous function, I need to call it another thread so I don't block the current thread and it can receive the NSStream events.

    Wow.

    That's really surprising, given that Apple is acutely aware that synchrony on the main thread is evil. Is stream support in NSXMLParser intended only for files?

    — F
previous month may 2012 next month
MTWTFSS
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
Go to today