Networking/QHTTPOperation.h

/*
    File:       QHTTPOperation.h
 
    Contains:   An NSOperation that runs an HTTP request.
 
    Written by: DTS
 
    Copyright:  Copyright (c) 2010 Apple Inc. All Rights Reserved.
 
    Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc.
                ("Apple") in consideration of your agreement to the following
                terms, and your use, installation, modification or
                redistribution of this Apple software constitutes acceptance of
                these terms.  If you do not agree with these terms, please do
                not use, install, modify or redistribute this Apple software.
 
                In consideration of your agreement to abide by the following
                terms, and subject to these terms, Apple grants you a personal,
                non-exclusive license, under Apple's copyrights in this
                original Apple software (the "Apple Software"), to use,
                reproduce, modify and redistribute the Apple Software, with or
                without modifications, in source and/or binary forms; provided
                that if you redistribute the Apple Software in its entirety and
                without modifications, you must retain this notice and the
                following text and disclaimers in all such redistributions of
                the Apple Software. Neither the name, trademarks, service marks
                or logos of Apple Inc. may be used to endorse or promote
                products derived from the Apple Software without specific prior
                written permission from Apple.  Except as expressly stated in
                this notice, no other rights or licenses, express or implied,
                are granted by Apple herein, including but not limited to any
                patent rights that may be infringed by your derivative works or
                by other works in which the Apple Software may be incorporated.
 
                The Apple Software is provided by Apple on an "AS IS" basis. 
                APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
                WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT,
                MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING
                THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
                COMBINATION WITH YOUR PRODUCTS.
 
                IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT,
                INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
                TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
                DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY
                OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
                OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY
                OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR
                OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF
                SUCH DAMAGE.
 
*/
 
#import "QRunLoopOperation.h"
 
/*
    QHTTPOperation is a general purpose NSOperation that runs an HTTP request. 
    You initialise it with an HTTP request and then, when you run the operation, 
    it sends the request and gathers the response.  It is quite a complex 
    object because it handles a wide variety of edge cases, but it's very 
    easy to use in simple cases:
 
    1. create the operation with the URL you want to get
    
    op = [[[QHTTPOperation alloc] initWithURL:url] autorelease];
    
    2. set up any non-default parameters, for example, set which HTTP 
       content types are acceptable
    
    op.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
    
    3. enqueue the operation
    
    [queue addOperation:op];
    
    4. finally, when the operation is done, use the lastResponse and 
       error properties to find out how things went
    
    As mentioned above, QHTTPOperation is very general purpose.  There are a 
    large number of configuration and result options available to you.
    
    o You can specify a NSURLRequest rather than just a URL.
    
    o You can configure the run loop and modes on which the NSURLConnection is 
      scheduled.
    
    o You can specify what HTTP status codes and content types are OK.
      
    o You can set an authentication delegate to handle authentication challenges.
    
    o You can accumulate responses in memory or in an NSOutputStream. 
    
    o For in-memory responses, you can specify a default response size 
      (used to size the response buffer) and a maximum response size 
      (to prevent unbounded memory use).
    
    o You can get at the last request and the last response, to track 
      redirects.
 
    o There are a variety of funky debugging options to simulator errors 
      and delays.
      
    Finally, it's perfectly reasonable to subclass QHTTPOperation to meet you 
    own specific needs.  Specifically, it's common for the subclass to 
    override -connection:didReceiveResponse: in order to setup the output 
    stream based on the specific details of the response.
*/
 
@protocol QHTTPOperationAuthenticationDelegate;
 
@interface QHTTPOperation : QRunLoopOperation /* <NSURLConnectionDelegate> */
{
    NSURLRequest *      _request;
    NSIndexSet *        _acceptableStatusCodes;
    NSSet *             _acceptableContentTypes;
    id<QHTTPOperationAuthenticationDelegate>    _authenticationDelegate;
    NSOutputStream *    _responseOutputStream;
    NSUInteger          _defaultResponseSize;
    NSUInteger          _maximumResponseSize;
    NSURLConnection *   _connection;
    BOOL                _firstData;
    NSMutableData *     _dataAccumulator;
    NSURLRequest *      _lastRequest;
    NSHTTPURLResponse * _lastResponse;
    NSData *            _responseBody;
#if ! defined(NDEBUG)
    NSError *           _debugError;
    NSTimeInterval      _debugDelay;
    NSTimer *           _debugDelayTimer;
#endif
}
 
- (id)initWithRequest:(NSURLRequest *)request;      // designated
- (id)initWithURL:(NSURL *)url;                     // convenience, calls +[NSURLRequest requestWithURL:]
 
// Things that are configured by the init method and can't be changed.
 
@property (copy,   readonly)  NSURLRequest *        request;
@property (copy,   readonly)  NSURL *               URL;
 
// Things you can configure before queuing the operation.
 
// runLoopThread and runLoopModes inherited from QRunLoopOperation
@property (copy,   readwrite) NSIndexSet *          acceptableStatusCodes;  // default is nil, implying 200..299
@property (copy,   readwrite) NSSet *               acceptableContentTypes; // default is nil, implying anything is acceptable
@property (assign, readwrite) id<QHTTPOperationAuthenticationDelegate>  authenticationDelegate;
 
#if ! defined(NDEBUG)
@property (copy,   readwrite) NSError *             debugError;             // default is nil
@property (assign, readwrite) NSTimeInterval        debugDelay;             // default is none
#endif
 
// Things you can configure up to the point where you start receiving data. 
// Typically you would change these in -connection:didReceiveResponse:, but 
// it is possible to change them up to the point where -connection:didReceiveData: 
// is called for the first time (that is, you could override -connection:didReceiveData: 
// and change these before calling super).
 
// IMPORTANT: If you set a response stream, QHTTPOperation calls the response 
// stream synchronously.  This is fine for file and memory streams, but it would 
// not work well for other types of streams (like a bound pair).
 
@property (retain, readwrite) NSOutputStream *      responseOutputStream;   // defaults to nil, which puts response into responseBody
@property (assign, readwrite) NSUInteger            defaultResponseSize;    // default is 1 MB, ignored if responseOutputStream is set
@property (assign, readwrite) NSUInteger            maximumResponseSize;    // default is 4 MB, ignored if responseOutputStream is set
                                                                            // defaults are 1/4 of the above on embedded
 
// Things that are only meaningful after a response has been received;
 
@property (assign, readonly, getter=isStatusCodeAcceptable)  BOOL statusCodeAcceptable;
@property (assign, readonly, getter=isContentTypeAcceptable) BOOL contentTypeAcceptable;
 
// Things that are only meaningful after the operation is finished.
 
// error property inherited from QRunLoopOperation
@property (copy,   readonly)  NSURLRequest *        lastRequest;       
@property (copy,   readonly)  NSHTTPURLResponse *   lastResponse;       
 
@property (copy,   readonly)  NSData *              responseBody;   
 
@end
 
@interface QHTTPOperation (NSURLConnectionDelegate)
 
// QHTTPOperation implements all of these methods, so if you override them 
// you must consider whether or not to call super.
//
// These will be called on the operation's run loop thread.
 
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace;
    // Routes the request to the authentication delegate if it exists, otherwise 
    // just returns NO.
    
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
    // Routes the request to the authentication delegate if it exists, otherwise 
    // just cancels the challenge.
 
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response;
    // Latches the request and response in lastRequest and lastResponse.
    
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
    // Latches the response in lastResponse.
    
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
    // If this is the first chunk of data, it decides whether the data is going to be 
    // routed to memory (responseBody) or a stream (responseOutputStream) and makes the 
    // appropriate preparations.  For this and subsequent data it then actually shuffles 
    // the data to its destination.
    
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
    // Completes the operation with either no error (if the response status code is acceptable) 
    // or an error (otherwise).
    
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
    // Completes the operation with the error.
 
@end
 
@protocol QHTTPOperationAuthenticationDelegate <NSObject>
@required
 
// These are called on the operation's run loop thread and have the same semantics as their 
// NSURLConnection equivalents.  It's important to realise that there is no 
// didCancelAuthenticationChallenge callback (because NSURLConnection doesn't issue one to us).  
// Rather, an authentication delegate is expected to observe the operation and cancel itself 
// if the operation completes while the challenge is running.
 
- (BOOL)httpOperation:(QHTTPOperation *)operation canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace;
- (void)httpOperation:(QHTTPOperation *)operation didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
 
@end
 
extern NSString * kQHTTPOperationErrorDomain;
 
// positive error codes are HTML status codes (when they are not allowed via acceptableStatusCodes)
//
// 0 is, of course, not a valid error code
//
// negative error codes are errors from the module
 
enum {
    kQHTTPOperationErrorResponseTooLarge = -1, 
    kQHTTPOperationErrorOnOutputStream   = -2, 
    kQHTTPOperationErrorBadContentType   = -3
};