TweetFollow Us on Twitter

HTTP POST Queries from Cocoa Applications

Volume Number: 19 (2003)
Issue Number: 4
Column Tag: Cocoa Development

HTTP POST Queries from Cocoa Applications

Integrating web content with desktop applications. Part 3 in a 3-part series

by Fritz Anderson

Introduction

In the first two articles in this series, we saw how easy it was to incorporate high-quality support for web content in Cocoa applications, so long as the request for the content could be fully expressed in the URL. More sophisticated requests, embodied in HTTP POST requests, are not directly available from the Cocoa API. Last month, we began exploring how to add such a facility to Cocoa, using the CFNetwork package of Core Foundation to build a formatted POST request.

This month, we will put what we've learned into an Objective-C class, that can be initialized with a URL, accept query parameters, present the query, and return the result.

As in the first two articles, our target web site will be Thomas, the Library of Congress's database of legislative history, at http://thomas.loc.gov/. We'll build a little application that takes the number of a Congress (for instance the 107th Congress that sat 2001-02) and the name of a member of the House of Representatives, and displays a web page listing all the measures that member sponsored.

Where we've Been

Last month's article already covered the use of Core Foundation's CFNetwork package to assemble and format a POST query. We saw how to marshal query parameters in an NSMutableDictionary, and how to marry the parameters and the necessary HTTP headers into a query packet using a CFHTTPMessage. The example code from last month resulted in an application that displayed the formatted query, ready for dispatch to a server.


Figure 1. Last month's BuildPOSTQuery application.

CFNetwork also provides a powerful facility for dispatching the query once it's built, and receiving the results. This month, we'll build an Objective-C class, FAWebPOSTQuery, around what we've already learned about building POST queries, and extend it so that getting web data from a POST can be as routine as the existing GET queries built into Cocoa.

Setting the Stage

Figure 2 shows the Interface Builder connections for the human-interface side of our test application. It's a tiny variation on the applications we've been building for the last two months--two fields for parameters, an NSTextView for results, an NSButton to set the query in motion, and, this time, an NSProgressIndicator to demonstrate that our program can do other things while the query is in-process. The Fetch button is wired to our controller object's doQuery: method. The controller code--the client of our FAWebPOSTQuery class--can be found in Listing 1.


Figure 2. The GET window modified for the POST example

The process of making a POST query can be divided into three parts: framing the query, posting the formatted query, and collecting the results. True to our plan, we find that doQuery: consists of only two method calls--frameQuery and postQuery.

Framing the query

The frameQuery method starts by harvesting the query parameters, and then creates the FAWebPostQuery object. The object needs a URL for initialization, because an FAWebPostQuery makes no sense without one, and optionally an NSDictionary of keys and values for the body of the query. As we did last month, we marshal the parameters of the query in the query object. Parameters that never vary can be supplied through a constant NSDictionary, while parameters that could differ with each query are set with the method setPostString:forKey:. Because POST queries are key-value lists, NSDictionary provides a natural analogy for specifying the body of the query. Using NSDictionary for initialization, and a method of the form set~:forKey: for management, was therefore an obvious choice in API design.

Sending the Query

POSTController's postQuery method is even simpler: It sends the message post to the FAWebPostQuery. That's it. The rest of the method sets an NSTimer to call our pendingTimer: method 20 times a second so we can run the NSProgressIndicator, and deactivates the Fetch button so we don't have to deal with more than one query at a time.

Receiving the Result

Now all the POSTController has to do is let the results roll in. FAWebPOSTQuery relies on an informal protocol, that its delegate must implement the method webPostQuery:completedWithResult:. When the query has finished--successfully or not--FAWebPOSTQuery returns the results and any result code to that method.

In the mean time, POSTController's timer method, and other methods on the UI thread, have been free to update the display and handle user events. So far as the user, and the code that uses FAWebPOSTQuery is concerned, the query takes place completely in the background.

Behind the Scenes

That's what happens so far as our UI testbed--FAWebPostQuery's client--is concerned. How is it done?

Last month we went over the issues in using CFHTTPMessage to format the POST query. Readers of that article should find the init... and setPostString:forKey: methods familiar, as well as the initial part of the post method, in which the CFHTTPMessage is finished off and readied for sending.

We could have the CFHTTPMessage turn over its serialized bytes, send them directly, and handle the rest of the transaction ourselves, but there is a much neater mechanism available, an elaboration of CFStream, the Core Foundation implementation of the stream-of-bytes data type.

A CFReadStream or CFWriteStream, alone, is a routine sort of abstract data type: It allows you to do sequential buffered reads or writes on files, sockets, or memory using calls similar to the POSIX read(2) and write(2) functions. In this case, the CFReadStream we will be using will serve as a read descriptor returning the bytes of the body of the query response, but that is only one of four roles the CFReadStream will be playing.


Figure 3. The roles of the CFReadStream

The CFReadStream will

  • Send the query message to the server.

  • Make callbacks to FAWebPostQuery code when events occur.

  • Be a client of the application's run loop, to get a share of processor time to do its work.

  • Yield bytes of the response to the query in response to a read call.

Each of these roles has to be initialized and (eventually) torn down.

Role 1: Sender of the query

The process starts with creating a CFReadStream for this transaction:
   replyStream = CFReadStreamCreateForHTTPRequest(
                                 kCFAllocatorDefault, message);
   CFRelease(message);
   message = NULL;

Because the CFReadStream will be responsible for sending the POST query, the first thing we do is to associate the query message with the stream. At this point, FAWebPostQuery has no further business with the query message, so it calls CFRelease to release its hold on it. This doesn't deallocate the message--the stream still retains a reference to it, and will release its reference later. The message will actually be sent when we call CFReadStreamOpen().

Role 2: Sender of event callbacks

Next, we initialize the stream's role as a sender of event messages to the FAWebPostQuery. The FAWebPostQuery will want to know when data arrives in response to the query, and when the query transaction has finished--successfully or not.

   BOOL   enqueued = CFReadStreamSetClient(replyStream,
                           kCFStreamEventHasBytesAvailable |
                              kCFStreamEventErrorOccurred |
                              kCFStreamEventEndEncountered,
                           MyReadCallback,
                           &cfContext);

CFReadStreamSetClient tells our reply stream what events we are interested in; that we want our function MyReadCallback to be called when they happen; and that we want certain context information passed to the callback function.

If you've done much programming with APIs that make callbacks--for instance, the NSTimer and NSNotification mechanisms in Cocoa--you're familiar with the custom of providing a "user info" pointer in the setup of the callback. It's a way to pass a pointer to an object or other helpful context information into your callback handler. The last parameter of CFReadStreamSetClient serves the same purpose, but instead of a simple generic pointer, this parameter must be a pointer to a CFStreamClientContext structure.

The reasoning behind this choice was this: The user info that you might want to pass through to a CFStream callback might be an ordinary pointer; it might be a reference-counted pointer to a Core Foundation object; or it might be a Cocoa object, which is also reference-counted, but by a different mechanism. The designers of the API decided that the CFStream should have a way to retain the user-info object if that is possible. (If you are done with the object, and the CFStream can retain and release it, you can release it immediately and not have the headache of guessing when it will be safe to release it later.) Therefore, you have to wrap the user-info pointer in a structure that includes pointers to functions that retain, release, and provide a CFString description of, the user-info data.

(Other Core Foundation APIs that define context structures allow you to pass NULL for the retain, release, and description function pointers if you do not want to define these operations. It is fair to assume the same rule applies to CFStreamClientContext, but at the time I write this, this part of the CFNetwork API had not yet been fully documented.)

FAWebPostQuery passes itself as the user-info object, and therefore provides C wrappers to its inherited retain, release, and description methods. Listing 2 provides the whole story.

Role 3: Run loop client

Now we are ready to set the CFReadStream for its third role, as a client of the application run loop.

   CFReadStreamScheduleWithRunLoop(replyStream,
                       CFRunLoopGetCurrent(),
                       kCFRunLoopCommonModes);

Veterans of Mac OS 9 and earlier are familiar with the event loop, the heart of a Macintosh program in the old operating system. At the head of the event loop is a call to WaitNextEvent(), which returns whenever user input or some other event occurs that the application must process; the rest of the loop is devoted to identifying what part of the application should handle the event, and dispatching control to that handler.

Every thread under Core Foundation and Cocoa has an event loop of its own. At base, it's a CFRunLoop, which is not toll-free bridged to the Cocoa NSRunLoop--you can get the underlying CFRunLoop from an NSRunLoop with the method getCFRunLoop. As with its Mac OS 9 cousin, it waits for events and dispatches them. Unlike the Mac OS 9 event loop, the details of the loop are hidden; the task of calling handlers is done automatically; and the gamut of "events" that can be handled--timer events, driver events, UI events--is practically unlimited.

A CFRunLoop waits, without consuming CPU time, until an event occurs that one of its registered clients can handle. Registered clients can include CFRunLoopTimers (or their toll-free equivalents, NSTimers), whose events reflect the expiration of their timers; or I/O objects like our CFReadStream, whose events include the arrival of data, end-of-data, or a fatal error. When the event occurs, the CFRunLoop calls the client's handler to respond to the event. In our case, CFReadStream will, in turn, call our callback function MyReadCallback. When the handler is done, control returns to the run loop, which returns to its sleep, waiting for the next event.

In this case, we register the reply stream with the current run loop. By specifying kCFRunLoopCommonModes, we ask that the stream's events be handled unless the event loop is put into a mode that explicitly restricts the handling of events. This is the usual way to register a run loop client.

With the call to CFReadStreamOpen(), the query is under way, and the response arrives over the next few seconds. Against the possibility it doesn't arrive at all, we set a timeout timer:

   timeoutTimer = [NSTimer
                     scheduledTimerWithTimeInterval: sPostTimeout
                     target: self
                     selector: @selector(messageTimedOut:)
                     userInfo: nil
                     repeats: NO];
   [timeoutTimer retain];

And, whenever the Thomas server gives evidence that it is alive--by sending us data--we restart the timeout clock with:

   [timeoutTimer setFireDate:
         [NSDate dateWithTimeIntervalSinceNow:
                     sPostTimeout]];

Role 4: Read bytes from the stream

The remaining business of the FAWebPostQuery object is to handle the events that come from the reply CFReadStream, all of which come to the callback function MyReadCallback. As you'd expect of an event handler, this function is built around a switch statement keyed on the type of event that arrived. Because we passed the FAWebPostQuery object as the context when we registered the callback, we get it back as a void* in the third parameter of the function.

When the event is kCFStreamEventHasBytesAvailable, the read stream finally fulfills the one task for which it is named:

   UInt8      buffer[READ_SIZE];
   CFIndex   bytesRead = CFReadStreamRead(stream,
                         buffer,
                         READ_SIZE-1);
   //   leave 1 byte for a trailing null.
      
   if (bytesRead > 0) {
      //   Convert what was read to a C-string
      buffer[bytesRead] = 0;
      //   Append it to the reply string
      [object appendContentCString: buffer];
   }

Just as we would with a POSIX read() call, we pass the stream pointer, a buffer address, and the available length of the buffer to CFReadStreamRead. The number of bytes actually read is returned. Then the newly-arrived string can be appended to our results string.

Cleaning up

I decided to handle the other two events, kCFStreamEventErrorOccurred and kCFStreamEventEndEncountered in the same way. Either way, the query is over: All errors in CFReadStream handling are irrecoverable. In both cases, there are three tasks to complete: Harvest the last information the CFReadStream can yield; tear down the CFReadStream; and inform the client of the FAWebPostQuery that the query has finished.

We've noticed before that HTTP messages fall into two parts, header and body. The bytes returned to CFReadStreamRead while the response was read were, in fact, only the body of the response. The header of the response is kept as an attribute of the CFReadStream, and it is possible to get the response status code thus:

   CFHTTPMessageRef   reply =
      (CFHTTPMessageRef) CFReadStreamCopyProperty(
         replyStream,
         kCFStreamPropertyHTTPResponseHeader);
                  
   //   Pull the status code from the headers
   if (reply) {
      statusCode = 
         CFHTTPMessageGetResponseStatusCode(reply);
      CFRelease(reply);
   }

Now that we're done with the reply stream, we can release it from its various roles, and deallocate it. The first step is CFReadStreamClose(), which stops all further actions by the reply stream. Next, we tear down its role as a provider of callbacks to the FAWebPostQuery object (and release any retains it might have done on the query object) by calling CFReadStreamSetClient() with a NULL client value. Finally, we remove the reply stream from its role as a client to the run loop with CFReadStreamUnscheduleFromRunLoop(). The reply stream can now be released with CFRelease().

Conclusion

Those of us who met Cocoa for the first time in Mac OS X were amazed at the rich toolbox Apple laid at the feet of developers. The riches are still coming in the form of Core Foundation APIs like CFNetwork. Core Foundation can be daunting at first, but it's so well-designed that every Cocoa programmer should consider adding it to his repertoire.

Listing 1a: POSTController.h

//
//  POSTController.h
//  CocoaPOST
//
#import <Cocoa/Cocoa.h>
@class FAWebPostQuery;
class POSTController
@interface POSTController : NSObject {
   IBOutlet NSTextField *               memberName;
   IBOutlet NSTextField *               congressField;
   IBOutlet NSProgressIndicator *   progress;
   IBOutlet NSButton *                  fetchButton;
   IBOutlet NSTextView *               resultText;
   FAWebPostQuery *                        theQuery;
   NSTimer *                                 busyTimer;
}
- (IBAction) doQuery: (id) sender;
- (void) frameQuery;
- (void) postQuery;
- (void) webPostQuery: (FAWebPostQuery *) query
              completedWithResult: (int) code;
- (void) pendingTimer: (NSTimer *) aTimer;
@end

Listing 1b: PostController.m

//
//  POSTController.m
//  CocoaPOST
//
#import "POSTController.h"
#import "FAWebPostQuery.h"
sConstantDictionary, sThomasURL

Once initialized, these will be a dictionary and URL that constitute the parts of a member-name query 
that don't vary. The initialize class method will fill them in. They will be used in the frameQuery 
method to initialize new FAWebPostQuery objects.

static NSDictionary *   sConstantDictionary = nil;
static NSURL *               sThomasURL = nil;
class POSTController
@implementation POSTController
initialize
"Initialize" class methods are called automatically by the Objective-C runtime before any of the 
class's methods are executed. In this method, the static NSURL and NSDictionary that constitute the 
invariant parts of a Thomas query by member name are initialized.

+ (void) initialize
{
   if (! sConstantDictionary) {
      sConstantDictionary = [[NSDictionary alloc]
         initWithObjectsAndKeys:
         @"bimp", @"TYPE1",
         @"SPON", @"Sponfld",
         nil];
      sThomasURL = [[NSURL alloc]
         initWithString:  
            @"http://thomas.loc.gov/cgi-bin/bdquery"];
   }
}
doQuery
This is the action method for the window's "Fetch" button. It initializes a new FAWebPostQuery from 
the information entered in the window's fields, and then sends the query to the Thomas server.

- (IBAction) doQuery: (id) sender
{
   [self frameQuery];   //   Initialize the query
   [self postQuery];   //   Execute the query
}
stopTimer
A convenience method that checks to see if a query-pending timer is active, and if so, unschedules 
it from the run loop.

- (void) stopTimer
{
   if (busyTimer) {
      [busyTimer invalidate];
      busyTimer = nil;
   }
}
dealloc
The standard release-of-resources handler for deallocation time.
- (void) dealloc
{
   [self stopTimer];
   [theQuery release];
}
frameQuery
Harvest the number of the Congress and the name of the Congressman from the respective fields, and 
initialize an FAWebPostQuery to suit.

- (void) frameQuery
{
   int         congress = [congressField intValue];
   NSString *   member = [memberName stringValue];
   theQuery = [[FAWebPostQuery alloc]
                  initWithServerURL: sThomasURL
                         postData: sConstantDictionary];
   [theQuery setPostString: 
            [NSString stringWithFormat: @"d%d", congress]
      forKey: [NSString stringWithFormat: @"Dbd%d",
                                                            congress]];
   [theQuery setPostString:
            [NSString stringWithFormat: @"/bss/d%dquery.html",
                                                            congress]
      forKey: @"srch"];
   [theQuery setPostString: member forKey: @"HMEMB"];
   
   [theQuery setDelegate: self];
}
postQuery
Tells the FAWebPostQuery we're done with preparation, and that it should send the query. We start an 
NSTimer to get periodic opportunities to animate our progress indicator while the query is in 
progress. Finally, we deactivate the "Fetch" button, because I don't want to support multiple or 
interrupted queries.

- (void) postQuery
{
   [theQuery post];
   busyTimer = [NSTimer scheduledTimerWithTimeInterval: 0.05
                  target: self
                  selector: @selector(pendingTimer:)
                  userInfo: nil
                  repeats: YES];
   [fetchButton setEnabled: NO];
}
webPostQuery:completedWithResult:
The query has completed. If it resulted in an error, inform the user. Otherwise, harvest the HTML in 
the reply and display it. This is the required method from the FAWebPostDelegate informal protocol.

- (void) webPostQuery: (FAWebPostQuery *) query
  completedWithResult: (int) code
{
   [self stopTimer];
   if (code == 200) {
       NSString *               result = [theQuery replyContent];
         NSData *               theHTML = [result dataUsingEncoding:
                                             NSASCIIStringEncoding];
      NSAttributedString *   styledText = 
            [[NSAttributedString alloc] 
                  initWithHTML: theHTML documentAttributes: nil];
      [[resultText textStorage]
            setAttributedString: styledText];
   }
   [theQuery release];
   theQuery = nil;
   [fetchButton setEnabled: YES];
}
pendingTimer:
The callback for the timer that this POSTController runs while awaiting completion of the query. It 
is here solely to run the progress bar, as a demonstration that the program is free to do other work 
while the query is being processed. The FAWebPostQuery object keeps a timer of its own for timeouts 
on the query.

- (void) pendingTimer: (NSTimer *) aTimer
{
   [progress animate: nil];
}
@end

Listing 2a: FAWebPostQuery.h

//
//  FAWebPostQuery.h
//  Jefferson
//
#import <Foundation/Foundation.h>
enum {
   FAWebPostIncomplete = -1,
   FAWebPostNotReplied = -2,
   FAWebPostReplyInProgress = -3,
   FAWebPostInvalid = -4,
   FAWebPostTimedOut = -5
};

FAWebPostAlreadyPosted
An exception that is thrown if an attempt is made to dispatch an FAWebPostQuery that has already sent 
its query.

extern NSString * const   FAWebPostAlreadyPosted;
class FAWebPostQuery
@interface FAWebPostQuery : NSObject {
   CFHTTPMessageRef            message;
   CFReadStreamRef            replyStream;
   NSMutableDictionary *   postData;
   int                              statusCode;
   CFMutableStringRef         cfReplyContent;
   NSTimer *                     timeoutTimer;
   NSObject *                     delegate;
   CFStreamClientContext   cfContext;
}
- (id) initWithServerURL: (NSURL *) server;
- (id) initWithServerURL: (NSURL *) server 
                     postData: (NSDictionary *) initialData;
- (void) setPostString: (NSString *) string
                        forKey: (NSString *) key;
- (void) post;
- (void) cancel;
- (int) statusCode;
- (NSString *) replyContent;
- (NSObject *) delegate;
- (void) setDelegate: (NSObject *) aDelegate;
@end

FAWebPostDelegate
This is an informal protocol that must be implemented by any object that is passed to the 
setDelegate: method of an FAWebPostQuery. It declares the signature of the callback message that is 
sent to the delegate object when the query either completes or fails.

@interface NSObject (FAWebPostDelegate)
- (void) webPostQuery: (FAWebPostQuery *) query
   completedWithResult: (int) code;
@end

Listing 2b: FAWebPostQuery.m

//
//  FAWebPostQuery.m
//  Jefferson
//
#import "FAWebPostQuery.h"
#import   "httpFlattening.h"
#define   READ_SIZE         1024
NSString * const   FAWebPostAlreadyPosted
               = @"FAWebPostAlreadyPosted";
static CFTimeInterval   sPostTimeout = 15.0;
//   A template for HTTP stream-client contexts
static CFStreamClientContext   sContext = {
   0, nil,
   CFClientRetain, 
   CFClientRelease, 
   CFClientDescribeCopy
};
   
class FAWebPostQuery
@implementation FAWebPostQuery

timeoutInterval
A class method that returns the interval, in seconds, after which an HTTP connection is considered to 
have timed out. When a query is posted, the reply must begin arriving within this time interval, and 
gaps between batches of data may not last longer. If the timer runs out, the connection is closed and 
the query fails with the error status FAWebPostTimedOut.

+ (CFTimeInterval) timeoutInterval
{ return sPostTimeout; }
setTimeoutInterval
Sets the length, in seconds, for all timeout intervals beginning after this class method is called.
+ (void) setTimeoutInterval: (CFTimeInterval) newInterval
{
   sPostTimeout = newInterval;
}

CFClientRetain
A glue function bridging the Objective-C FAWebPostQuery object to Core Foundation. A pointer to this 
function goes into the retain field of the client context for the HTTP CFStream that services the 
reply to the query.

void *
CFClientRetain(void *   selfPtr)
{
   FAWebPostQuery *   object
            = (FAWebPostQuery *) selfPtr;
            
   return [object retain];
}

CFClientRelease
A glue function bridging the Objective-C FAWebPostQuery object to Core Foundation. A pointer to this 
function goes into the release field of the client context for the HTTP CFStream that services the
reply to the query.

void
CFClientRelease(void *   selfPtr)
{
   FAWebPostQuery *   object
            = (FAWebPostQuery *) selfPtr;
            
   [object release];
}

CFClientDescribeCopy
A glue function bridging the Objective-C FAWebPostQuery object to Core Foundation. A pointer to this 
function goes into the copyDescription field of the client context for the HTTP CFStream that 
services the reply to the query.

CFStringRef
CFClientDescribeCopy(void *   selfPtr)
{
   FAWebPostQuery *   object
            = (FAWebPostQuery *) selfPtr;
            
   return (CFStringRef) [[object description] retain];
}

getResultCode
An internal-use method, called when the reply stream has indicated that it has either finished or 
experienced a fatal error. Retrieves the http header from the reply stream, if possible, and the 
http result code from the header. Sets the FAWebPostQuery's status code to the result code.

- (void) getResultCode
{
   if (replyStream) {
      //   Get the reply headers
      CFHTTPMessageRef   reply =
         (CFHTTPMessageRef) CFReadStreamCopyProperty(
            replyStream,
            kCFStreamPropertyHTTPResponseHeader);
                  
      //   Pull the status code from the headers
      if (reply) {
         statusCode = 
            CFHTTPMessageGetResponseStatusCode(reply);
         CFRelease(reply);
      }
   }
}

closeOutMessaging
An internal-use method, called when the CFReadStream that manages the queery reply is no longer 
needed--either because the whole reply has been received or because the request has failed. This 
method tears down the stream, the original POST query, and the timeout timer.

- (void) closeOutMessaging
{
   if (replyStream) {
      //   Close the read stream.
      CFReadStreamClose(replyStream);
      //   Deregister the callback client (learned this from WWDC session 805)
      CFReadStreamSetClient(replyStream, 0, NULL, NULL);
      //   Take the stream out of the run loop
      CFReadStreamUnscheduleFromRunLoop(
               replyStream,
               CFRunLoopGetCurrent(),
               kCFRunLoopCommonModes);
      //   Deallocate the stream pointer
      CFRelease(replyStream);
      //   Throw the spent pointer away
      replyStream = NULL;
   }
   
   if (timeoutTimer) {
      [timeoutTimer invalidate];
      [timeoutTimer release];
      timeoutTimer = nil;
   }
}

informDelegateOfCompletion
This method gets called when the query has completed, successfully or not, after the network streams 
have been torn down. If this object's client has set a delegate, inform the delegate of completion 
through the method webPostQuery:completedWithResult:.

- (void) informDelegateOfCompletion
{
   if (delegate) {
      NSAssert(
         [delegate respondsToSelector:
            @selector(webPostQuery:completedWithResult:)],
         @"A web-POST query delegate must implement "
         @"webPostQuery:completedWithResult:");
      [delegate webPostQuery: self 
         completedWithResult: statusCode];
   }
}

appendContentCString:
An internal method called by MyReadCallback. It appends the C string it is passed to the 
CFMutableString that keeps the body of the reply to the query. Passing this message sets this 
object's status to in-progress, and restarts the timeout timer.

- (void) appendContentCString: (char *) cString
{
   CFStringAppendCString(cfReplyContent,
                                 cString,
                                 kCFStringEncodingASCII);
statusCode = FAWebPostReplyInProgress;
   //   Refresh the timeout timer.
   [timeoutTimer setFireDate:
         [NSDate dateWithTimeIntervalSinceNow:
                     sPostTimeout]];
}

MyReadCallback
This is the registered event callback for the CFReadStream that manages sending the query and 
receiving the reply. If data has arrived in the reply, the data is taken from the stream and 
accumulated. If the transaction ends because of error or success, a final result code is set, 
the CFReadStream is torn down, and the registered client, if any is informed.

void
MyReadCallback(CFReadStreamRef   stream,
                               CFStreamEventType   type,
                               void *            userData)
{
   FAWebPostQuery *   object = 
                     (FAWebPostQuery *) userData;
   
   switch (type) {
   case kCFStreamEventHasBytesAvailable: {
      UInt8      buffer[READ_SIZE];
      CFIndex   bytesRead = CFReadStreamRead(stream,
                                                 buffer, READ_SIZE-1);
      //   leave 1 byte for a trailing null.
      
      if (bytesRead > 0) {
         //   Convert what was read to a C-string
         buffer[bytesRead] = 0;
         //   Append it to the reply string
         [object appendContentCString: buffer];
      }      
   }
      break;
   case kCFStreamEventErrorOccurred:
   case kCFStreamEventEndEncountered:
      [object getResultCode];
      [object closeOutMessaging];
      [object informDelegateOfCompletion];
      break;
   default:
      break;
   }
}

messageTimedOut:
The callback for the internal timeout timer. This method gets called only in the exceptional case of 
the remote server not responding within the specified time. It's a fatal error, and causes the 
connection to be torn down and the delegate (if any) notified.

- (void) messageTimedOut: (NSTimer *) theTimer
{
   statusCode = FAWebPostTimedOut;
   [self closeOutMessaging];
   [self informDelegateOfCompletion];
}
- (id) initWithServerURL: (NSURL *) server
{
   return [self initWithServerURL: server postData: nil];
}
- (id) initWithServerURL: (NSURL *) server
                        postData: (NSDictionary *) initialData
{
   replyStream = NULL;
   cfReplyContent = CFStringCreateMutable(
                                       kCFAllocatorDefault, 0);
   statusCode = FAWebPostIncomplete;
   cfContext = sContext;
   cfContext.info = self;
   timeoutTimer = nil;
   if (initialData)
      postData = [[NSMutableDictionary alloc]
                              initWithDictionary: initialData];
   else
      postData = [[NSMutableDictionary alloc]
                                                initWithCapacity: 8];
   if (!postData) {
      [self release];
      return nil;
      }
   
   //   Set up the POST message and its headers
   message = CFHTTPMessageCreateRequest(
                                    kCFAllocatorDefault,
                                    CFSTR("POST"),
                                    (CFURLRef) server,
                                    kCFHTTPVersion1_1);
   if (!message) {
      [self release];
      return nil;
      }
   CFHTTPMessageSetHeaderFieldValue(message,
                        CFSTR("User-Agent"),
                        CFSTR("Generic/1.0 (Mac_PowerPC)"));
   CFHTTPMessageSetHeaderFieldValue(message,
               CFSTR("Content-Type"),
               CFSTR("application/x-www-form-urlencoded"));
   CFHTTPMessageSetHeaderFieldValue(message,
               CFSTR("Host"), (CFStringRef) [server host]);
   CFHTTPMessageSetHeaderFieldValue(message,
                        CFSTR("Accept"), CFSTR("text/html"));
   return self;
}
- (void) setPostString: (NSString *) string
                     forKey: (NSString *) key
{
   [postData setObject: string forKey: key];
}
- (void) dealloc
{
   if (message) {
      CFRelease(message);
      message = NULL;
   }
   
   [postData release];
   if (cfReplyContent) {
      CFRelease(cfReplyContent);
      cfReplyContent = NULL;
   }
   
   if (timeoutTimer) {
      [timeoutTimer invalidate];
      [timeoutTimer dealloc];
      timeoutTimer = nil;
   }
}
- (void) post
{
   if (statusCode != FAWebPostIncomplete)
      [NSException raise:   FAWebPostAlreadyPosted
            format: @"This query has already been posted "
                        @"and either answered or refused."];
   statusCode = FAWebPostNotReplied;
   //   String-out the postData dictionary
   NSString *   postString = [postData webFormEncoded];
   NSData *   postStringData = [postString
               dataUsingEncoding: kCFStringEncodingASCII
               allowLossyConversion: YES];
   //   Put the post data in the body of the query
   CFHTTPMessageSetBody(message, 
                   (CFDataRef) postStringData);
   //   Now that we know how long the query body is, put the length in the header
   CFHTTPMessageSetHeaderFieldValue(message,
                                           CFSTR("Content-Length"),
       (CFStringRef) [NSString stringWithFormat: @"%d",
                                            [postStringData length]]);
           
   //   Initialize the CFReadStream that will make the request and manage the reply
   replyStream = CFReadStreamCreateForHTTPRequest(
                                 kCFAllocatorDefault, message);
   //   I have no further business with message
   CFRelease(message);
   message = NULL;
   
   //   Register the CFReadStream's callback client
   BOOL   enqueued = CFReadStreamSetClient(replyStream,
                     kCFStreamEventHasBytesAvailable |
                        kCFStreamEventErrorOccurred |
                        kCFStreamEventEndEncountered,
                     MyReadCallback,
                     &cfContext);
   //      Schedule the CFReadStream for service by the current run loop
   CFReadStreamScheduleWithRunLoop(replyStream,
                       CFRunLoopGetCurrent(),
                       kCFRunLoopCommonModes);
   //   Fire off the request
   CFReadStreamOpen(replyStream);
   //   Watch for timeout
   timeoutTimer = [NSTimer
                     scheduledTimerWithTimeInterval: sPostTimeout
                     target: self
                     selector: @selector(messageTimedOut:)
                     userInfo: nil
                     repeats: NO];
   [timeoutTimer retain];
}
- (void) cancel
{
   NSAssert(replyStream,
               @"The program should prevent cancelling "
               @"when no query is in progress.");
   [self closeOutMessaging];
   statusCode = FAWebPostInvalid;
}
- (int) statusCode { return statusCode; }
- (NSString *) replyContent {
   return (NSString *) cfReplyContent;
}
- (NSObject *) delegate { return delegate; }
- (void) setDelegate: (NSObject *) aDelegate
{ delegate = aDelegate; }
@end

Fritz Anderson has been programming and writing about the Macintosh since 1984. He works (and seeks work) as a consultant in Chicago. You can reach him at fritza@manoverboard.org.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

BusyContacts 1.6.4 - Fast, efficient con...
BusyContacts is a contact manager for OS X that makes creating, finding, and managing contacts faster and more efficient. It brings to contact management the same power, flexibility, and sharing... Read more
Steam 4.0 - Multiplayer and communicatio...
Steam is a digital distribution, digital rights management, multiplayer and communications platform developed by Valve Corporation. It is used to distribute a large number of games and related media... Read more
OmniGraffle Pro 7.19.3 - Create diagrams...
OmniGraffle Pro helps you draw beautiful diagrams, family trees, flow charts, org charts, layouts, and (mathematically speaking) any other directed or non-directed graphs. We've had people use... Read more
OmniGraffle 7.19.3 - Create diagrams, fl...
OmniGraffle helps you draw beautiful diagrams, family trees, flow charts, org charts, layouts, and (mathematically speaking) any other directed or non-directed graphs. We've had people use Graffle to... Read more
Hopper Disassembler 5.3.3- - Binary disa...
Hopper Disassembler is a binary disassembler, decompiler, and debugger for 32- and 64-bit executables. It will let you disassemble any binary you want, and provide you all the information about its... Read more
calibre 5.35.0 - Complete e-book library...
Calibre is a complete e-book library manager. Organize your collection, convert your books to multiple formats, and sync with all of your devices. Let Calibre be your multi-tasking digital librarian... Read more
Sound Studio 4.10.0 - Robust audio recor...
Sound Studio lets you easily record and professionally edit audio on your Mac. Easily rip vinyls and digitize cassette tapes, or record lectures and voice memos. Prepare for live shows with live... Read more
Sparkle Pro 4.0 - Visual website creator...
Sparkle Pro will change your mind if you thought building websites wasn't for you. Sparkle is the intuitive site builder that lets you create sites for your online portfolio, team or band pages, or... Read more
Dropbox 140.4.1951 - Cloud backup and sy...
Dropbox for Mac is a file hosting service that provides cloud storage, file synchronization, personal cloud, and client software. It is a modern workspace that allows you to get to all of your files... Read more
FotoMagico 6.0.5 - Powerful slideshow cr...
FotoMagico lets you create professional slideshows from your photos and music with just a few, simple mouse clicks. It sports a very clean and intuitive yet powerful user interface. High image... Read more

Latest Forum Discussions

See All

Best iPhone Game Updates: ‘Garena Free F...
Hello everyone, and welcome to the week! It’s time once again for our look back at the noteworthy updates of the last seven days. I got busted last week for not including the obligatory free-to-play matching puzzle game update of the week, and my... | Read more »
‘Horizon Chase’ China Spirit DLC Release...
Following the release of the excellent reveal of the Horizon Chase Senna Forever expansion, the game will be getting a new DLC on mobile platforms today. Today, the Horizon Chase China Spirit DLC pack will release on iOS and Android bringing in 9... | Read more »
‘PUZZLED’ from SNK and Hamster Is Out No...
Following ZED BLADE ACA NeoGeo earlier this month, SNK has brought over another game in the ACA NeoGeo series to both iOS and Android in the form of PUZZLED. SNK and Hamster originally brought the series to mobile with Samurai Shodown IV, Alpha... | Read more »
A House Full of Covid – The TouchArcade...
It’s been a rough week as both of our young children tested positive for Covid, and since recording this early on Friday my wife has tested positive now too. Thankfully the kids seemed to recover fairly quickly and are mostly back to normal, and I... | Read more »
TouchArcade Game of the Week: ‘Krispee S...
Krispee Street is a new hidden object game from Frosty Pop that is based on their popular and almost painfully sweet webcomic Krispee. This is one of the latest titles to be added to the Netflix Games catalog, which means you’ll need to log into... | Read more »
SwitchArcade Round-Up: ‘Escape Lala’, ‘B...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for January 21st, 2022. In today’s article, we’ve got a lot of new releases. A lot. There were eight on the schedule when I went to bed last night. There were twenty-four when I woke up... | Read more »
Beta Testers Needed for Huge Version 2.0...
Ya’ll remember Dungeon Raid, right? The phenomenal matching RPG hybrid that launched on mobile more than a decade ago, but was more or less abandoned by its developer only to die a slow death on the App Store before the 32-bit Appocalypse finally... | Read more »
‘Ark Legends’ Gives Players a Chance to...
It’s Airpods and Amazon gift cards galore as Melting Games opens pre-registration for Ark Legends. The upcoming mobile RPG is giving away tons of in-game goodies such as gold, energy, iron core, hero summon chest and rare iron core to players who... | Read more »
‘Nickelodeon Extreme Tennis’ Out Now on...
Nickelodeon Extreme Tennis () from Old Skull Games and Nickelodeon is this week’s new Apple Arcade release. Nickelodeon Extreme Tennis features characters from old and new Nickelodeon shows including SpongeBob, TMNT, and many more. The tennis game... | Read more »
SwitchArcade Round-Up: ‘RPGolf Legends’,...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for January 20th, 2022. In today’s article, we’ve got a massive amount of new releases to check out. We’ve got summaries of all of them, from heaven to hell. We also have the lists of... | Read more »

Price Scanner via MacPrices.net

Verizon’s 2022 iPad promo: $100-$310 off any...
Verizon has cellular-capable iPads on sale for $100-$310 off MSRP when purchased with an Unlimited service plan. Sale price is applied to your account monthly over a 24 or 30 month period, depending... Read more
Sunday Sale: Apple AirPods are on sale for up...
Amazon has Apple AirPods on sale for $10-$100 off MSRP today, depending on the model. All are in stock today with free delivery: – AirPods Max headphones (Blue): $449 $100 off MSRP – AirPods Max... Read more
These Apple resellers are offering 13″ M1 Mac...
Apple resellers are offering discounts on 13″ MacBook Pros with M1 Apple Silicon processors ranging up to $150 off MSRP. Here’s where to get one today: (1): Apple’s 13″ MacBook Pros with M1 Apple... Read more
Amazon lowers prices on select 13″ M1 MacBook...
Amazon has select Apple 13″ M1 MacBook Airs on sale for $150 off MSRP this weekend, starting at only $849. Their prices are the lowest available for new MacBook Airs today. Stock may come and go, so... Read more
Apple has 13″ M1 MacBook Airs back in stock s...
Apple has restocked a full line of 13″ M1 MacBook Airs, Certified Refurbished, starting at only $849 and up to $190 off original MSRP. These are the cheapest M1-powered MacBooks for sale today at... Read more
In stock and on sale! 16″ 10-Core M1 Pro MacB...
Amazon has new 16″ 10-Core/512GB M1 Pro MacBook Pros in stock today and on sale for $50 off MSRP including free shipping. Their prices are the lowest available for new M1 Pro 16″ MacBook Pro from any... Read more
Deal Alert!: 14″ M1 Pro with 10-Core CPU in s...
Amazon has the new 14″ M1 Pro MacBook Pro with a 10-Core CPU and 16-Core GPU in stock today and on sale for $2299.99 including free shipping. Their price is $200 off Apple’s standard MSRP, and it’s... Read more
Apple has 24-inch M1 iMacs (8-Core CPU/8-Core...
Apple has restocked a wide array of 24-inch M1 iMacs with 8-Core CPUs and 8-Core GPUs in their Certified Refurbished store. Models are available starting at only $1269 and range up to $260 off... Read more
Select 24″ M1 iMacs are on sale for $100 off...
Sales of Apple’s new 24″ M1 iMacs have been rare since its introduction, perhaps due to global supply issues. However, B&H is offering a $100 discount on select 24″ iMacs, and they’re in stock... Read more
M1 Mac minis are back in stock today at Apple...
Apple has M1-powered Mac minis available in their Certified Refurbished section starting at only $589 and up to $140 off MSRP. Each mini comes with Apple’s one-year warranty, and shipping is free: –... Read more

Jobs Board

Registered Nurse (RN) Employee Health PSJH -...
…is calling for a Registered Nurse (RN) Employee Health PSJH to our location in Apple Valley, CA.** We are seeking a Registered Nurse (RN) Employee Health PSJH to be Read more
Systems Administrator - Pearson (United State...
…and troubleshoot Windows operating systems (workstation and server), laptop computers, Apple iPads, Chromebooks and printers** + **Administer and troubleshoot all Read more
IT Assistant Level 1- IT Desktop Support Anal...
…providing tier-1 or better IT help desk support in a large Windows and Apple environment * Experience using IT Service Desk Management Software * Knowledge of IT Read more
Human Resources Business Partner PSJH - Provi...
…**is calling a** **Human Resources Business Partner, PSJH** **to our location in Apple Valley, CA.** **Applicants that meet qualifications will receive a text with Read more
Manager Community Health Investment Programs...
…is calling a Manager Community Health Investment Programs PSJH to our location in Apple Valley, CA.** **Qualified candidates will be invited to do a self-paced video Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.