diff --git a/Classes/GDataDefines.h b/Classes/GDataDefines.h new file mode 100644 index 0000000..9446683 --- /dev/null +++ b/Classes/GDataDefines.h @@ -0,0 +1,188 @@ +/* Copyright (c) 2008 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +// +// GDataDefines.h +// + +// Ensure Apple's conditionals we depend on are defined. +#import + +// +// The developer may choose to define these in the project: +// +// #define GDATA_TARGET_NAMESPACE Xxx // preface all GData class names with Xxx (recommended for building plug-ins) +// #define GDATA_FOUNDATION_ONLY 1 // builds without AppKit or Carbon (default for iPhone builds) +// #define GDATA_SIMPLE_DESCRIPTIONS 1 // remove elaborate -description methods, reducing code size (default for iPhone release builds) +// #define STRIP_GDATA_FETCH_LOGGING 1 // omit http logging code (default for iPhone release builds) +// +// Mac developers may find GDATA_SIMPLE_DESCRIPTIONS and STRIP_GDATA_FETCH_LOGGING useful for +// reducing code size. +// + +// Define later OS versions when building on earlier versions +#ifndef MAC_OS_X_VERSION_10_5 + #define MAC_OS_X_VERSION_10_5 1050 +#endif +#ifndef MAC_OS_X_VERSION_10_6 + #define MAC_OS_X_VERSION_10_6 1060 +#endif + + +#ifdef GDATA_TARGET_NAMESPACE +// prefix all GData class names with GDATA_TARGET_NAMESPACE for this target + #import "GDataTargetNamespace.h" +#endif + +#if TARGET_OS_IPHONE // iPhone SDK + + #define GDATA_IPHONE 1 + +#endif + +#if GDATA_IPHONE + + #define GDATA_FOUNDATION_ONLY 1 + + #define GDATA_USES_LIBXML 1 + + #import "GDataXMLNode.h" + + #define NSXMLDocument GDataXMLDocument + #define NSXMLElement GDataXMLElement + #define NSXMLNode GDataXMLNode + #define NSXMLNodeKind GDataXMLNodeKind + #define NSXMLInvalidKind GDataXMLInvalidKind + #define NSXMLDocumentKind GDataXMLDocumentKind + #define NSXMLElementKind GDataXMLElementKind + #define NSXMLAttributeKind GDataXMLAttributeKind + #define NSXMLNamespaceKind GDataXMLNamespaceKind + #define NSXMLProcessingInstructionKind GDataXMLDocumentKind + #define NSXMLCommentKind GDataXMLCommentKind + #define NSXMLTextKind GDataXMLTextKind + #define NSXMLDTDKind GDataXMLDTDKind + #define NSXMLEntityDeclarationKind GDataXMLEntityDeclarationKind + #define NSXMLAttributeDeclarationKind GDataXMLAttributeDeclarationKind + #define NSXMLElementDeclarationKind GDataXMLElementDeclarationKind + #define NSXMLNotationDeclarationKind GDataXMLNotationDeclarationKind + + // properties used for retaining the XML tree in the classes that use them + #define kGDataXMLDocumentPropertyKey @"_XMLDocument" + #define kGDataXMLElementPropertyKey @"_XMLElement" +#endif + +// +// GDATA_ASSERT is like NSAssert, but takes a variable number of arguments: +// +// GDATA_ASSERT(condition, @"Problem in argument %@", argStr); +// +// GDATA_DEBUG_ASSERT is similar, but compiles in only for debug builds +// + +#ifndef GDATA_ASSERT + // we directly invoke the NSAssert handler so we can pass on the varargs + #if !defined(NS_BLOCK_ASSERTIONS) + #define GDATA_ASSERT(condition, ...) \ + do { \ + if (!(condition)) { \ + [[NSAssertionHandler currentHandler] \ + handleFailureInFunction:[NSString stringWithUTF8String:__PRETTY_FUNCTION__] \ + file:[NSString stringWithUTF8String:__FILE__] \ + lineNumber:__LINE__ \ + description:__VA_ARGS__]; \ + } \ + } while(0) + #else + #define GDATA_ASSERT(condition, ...) do { } while (0) + #endif // !defined(NS_BLOCK_ASSERTIONS) +#endif // GDATA_ASSERT + +#ifndef GDATA_DEBUG_ASSERT + #if DEBUG + #define GDATA_DEBUG_ASSERT(condition, ...) GDATA_ASSERT(condition, __VA_ARGS__) + #else + #define GDATA_DEBUG_ASSERT(condition, ...) do { } while (0) + #endif +#endif + +#ifndef GDATA_DEBUG_LOG + #if DEBUG + #define GDATA_DEBUG_LOG(...) NSLog(__VA_ARGS__) + #else + #define GDATA_DEBUG_LOG(...) do { } while (0) + #endif +#endif + + +// +// macro to allow fast enumeration when building for 10.5 or later, and +// reliance on NSEnumerator for 10.4 +// +#ifndef GDATA_FOREACH + #if TARGET_OS_IPHONE || (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5) + #define GDATA_FOREACH(element, collection) \ + for (element in collection) + #define GDATA_FOREACH_KEY(key, dict) \ + for (key in dict) + #else + #define GDATA_FOREACH(element, collection) \ + for (NSEnumerator* _ ## element ## _enum = [collection objectEnumerator]; \ + (element = [_ ## element ## _enum nextObject]) != nil; ) + #define GDATA_FOREACH_KEY(key, dict) \ + for (NSEnumerator* _ ## key ## _enum = [dict keyEnumerator]; \ + (key = [_ ## key ## _enum nextObject]) != nil; ) + #endif +#endif + + +// +// To reduce code size on iPhone release builds, we compile out the helpful +// description methods for GData objects +// +#ifndef GDATA_SIMPLE_DESCRIPTIONS + #if GDATA_IPHONE && !DEBUG + #define GDATA_SIMPLE_DESCRIPTIONS 1 + #else + #define GDATA_SIMPLE_DESCRIPTIONS 0 + #endif +#endif + +#ifndef STRIP_GDATA_FETCH_LOGGING + #if GDATA_IPHONE && !DEBUG + #define STRIP_GDATA_FETCH_LOGGING 1 + #else + #define STRIP_GDATA_FETCH_LOGGING 0 + #endif +#endif + + +// To simplify support for 64bit (and Leopard in general), we provide the type +// defines for non Leopard SDKs +#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4 + // NSInteger/NSUInteger and Max/Mins + #ifndef NSINTEGER_DEFINED + #if __LP64__ || NS_BUILD_32_LIKE_64 + typedef long NSInteger; + typedef unsigned long NSUInteger; + #else + typedef int NSInteger; + typedef unsigned int NSUInteger; + #endif + #define NSIntegerMax LONG_MAX + #define NSIntegerMin LONG_MIN + #define NSUIntegerMax ULONG_MAX + #define NSINTEGER_DEFINED 1 + #endif // NSINTEGER_DEFINED +#endif // MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4 diff --git a/Classes/GDataGatherInputStream.h b/Classes/GDataGatherInputStream.h new file mode 100644 index 0000000..a19fb09 --- /dev/null +++ b/Classes/GDataGatherInputStream.h @@ -0,0 +1,56 @@ +/* Copyright (c) 2007 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +// The GDataGatherInput stream is an input stream implementation that is to be +// instantiated with an NSArray of NSData objects. It works in the traditional +// scatter/gather vector I/O model. Rather than allocating a big NSData object +// to hold all of the data and performing a copy into that object, the +// GDataGatherInputStream will maintain a reference to the NSArray and read from +// each NSData in turn as the read method is called. You should not alter the +// underlying set of NSData objects until all read operations on this input +// stream have completed. + +#import +#import "GDataDefines.h" + +#undef GDATA_NSSTREAM_DELEGATE +#if TARGET_OS_MAC && !TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5) + #define GDATA_NSSTREAM_DELEGATE +#else + #define GDATA_NSSTREAM_DELEGATE +#endif + +@interface GDataGatherInputStream : NSInputStream GDATA_NSSTREAM_DELEGATE { + + NSArray* dataArray_; // NSDatas that should be "gathered" and streamed. + NSUInteger arrayIndex_; // Index in the array of the current NSData. + long long dataOffset_; // Offset in the current NSData we are processing. + + id delegate_; // WEAK, not retained: stream delegate, defaults to self + + // Since various undocumented methods get called on a stream, we'll + // use a 1-byte dummy stream object to handle all unexpected messages. + // Actual reads from the stream we will perform using the data array, not + // from the dummy stream. + NSInputStream* dummyStream_; + NSData* dummyData_; +} + ++ (NSInputStream *)streamWithArray:(NSArray *)dataArray; + +- (id)initWithArray:(NSArray *)dataArray; + +@end diff --git a/Classes/GDataGatherInputStream.m b/Classes/GDataGatherInputStream.m new file mode 100644 index 0000000..555eae8 --- /dev/null +++ b/Classes/GDataGatherInputStream.m @@ -0,0 +1,193 @@ +/* Copyright (c) 2007 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#import "GDataGatherInputStream.h" + +@implementation GDataGatherInputStream + ++ (NSInputStream *)streamWithArray:(NSArray *)dataArray { + return [[[self alloc] initWithArray:dataArray] autorelease]; +} + +- (id)initWithArray:(NSArray *)dataArray { + self = [super init]; + if (self) { + dataArray_ = [dataArray retain]; + arrayIndex_ = 0; + dataOffset_ = 0; + + [self setDelegate:self]; // An NSStream's default delegate should be self. + + // We use a dummy input stream to handle all the various undocumented + // messages the system sends to an input stream. + // + // Contrary to documentation, inputStreamWithData neither copies nor + // retains the data in Mac OS X 10.4, so we must retain it. + // (Radar 5167591) + + dummyData_ = [[NSData alloc] initWithBytes:"x" length:1]; + dummyStream_ = [[NSInputStream alloc] initWithData:dummyData_]; + } + return self; +} + +- (void)dealloc { + [dataArray_ release]; + [dummyStream_ release]; + [dummyData_ release]; + + [super dealloc]; +} + +- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len { + + NSUInteger bytesRead = 0; + NSUInteger bytesRemaining = len; + + // read bytes from the currently-indexed array + while ((bytesRemaining > 0) && (arrayIndex_ < [dataArray_ count])) { + + NSData* data = [dataArray_ objectAtIndex:arrayIndex_]; + + NSUInteger dataLen = [data length]; + NSUInteger dataBytesLeft = dataLen - (NSUInteger)dataOffset_; + + NSUInteger bytesToCopy = MIN(bytesRemaining, dataBytesLeft); + NSRange range = NSMakeRange((NSUInteger) dataOffset_, bytesToCopy); + + [data getBytes:(buffer + bytesRead) range:range]; + + bytesRead += bytesToCopy; + dataOffset_ += bytesToCopy; + bytesRemaining -= bytesToCopy; + + if (dataOffset_ == dataLen) { + dataOffset_ = 0; + arrayIndex_++; + } + } + + if (bytesRead == 0) { + // We are at the end our our stream, so we read all of the data on our + // dummy input stream to make sure it is in the "fully read" state. + uint8_t leftOverBytes[2]; + (void) [dummyStream_ read:leftOverBytes maxLength:sizeof(leftOverBytes)]; + } + + return bytesRead; +} + +- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len { + return NO; // We don't support this style of reading. +} + +- (BOOL)hasBytesAvailable { + // if we return no, the read never finishes, even if we've already + // delivered all the bytes + return YES; +} + +#pragma mark - + +// Pass other expected messages on to the dummy input stream + +- (void)open { + [dummyStream_ open]; +} + +- (void)close { + [dummyStream_ close]; + + // 10.4's NSURLConnection tends to retain streams needlessly, + // so we'll free up the data array right away + [dataArray_ release]; + dataArray_ = nil; +} + +- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent { + if (delegate_ != self) { + [delegate_ stream:self handleEvent:streamEvent]; + } +} + +- (id)delegate { + return delegate_; +} + +- (void)setDelegate:(id)delegate { + if (delegate == nil) { + delegate_ = self; + [dummyStream_ setDelegate:nil]; + } else { + delegate_ = delegate; + [dummyStream_ setDelegate:self]; + } +} + +- (id)propertyForKey:(NSString *)key { + return [dummyStream_ propertyForKey:key]; +} + +- (BOOL)setProperty:(id)property forKey:(NSString *)key { + return [dummyStream_ setProperty:property forKey:key]; +} + +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode { + [dummyStream_ scheduleInRunLoop:aRunLoop forMode:mode]; +} + +- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode { + [dummyStream_ removeFromRunLoop:aRunLoop forMode:mode]; +} + +- (NSStreamStatus)streamStatus { + return [dummyStream_ streamStatus]; +} +- (NSError *)streamError { + return [dummyStream_ streamError]; +} + +#pragma mark - + +// We'll forward all unexpected messages to our dummy stream + ++ (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { + return [NSInputStream methodSignatureForSelector:selector]; +} + ++ (void)forwardInvocation:(NSInvocation*)invocation { + [invocation invokeWithTarget:[NSInputStream class]]; +} + +- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { + return [dummyStream_ methodSignatureForSelector:(SEL)selector]; +} + +- (void)forwardInvocation:(NSInvocation*)invocation { + +#if 0 + // uncomment this section to see the messages the NSInputStream receives + SEL selector; + NSString *selName; + + selector=[invocation selector]; + selName=NSStringFromSelector(selector); + NSLog(@"-forwardInvocation: %@",selName); +#endif + + [invocation invokeWithTarget:dummyStream_]; +} + +@end diff --git a/Classes/GDataHTTPFetcher.h b/Classes/GDataHTTPFetcher.h new file mode 100755 index 0000000..941d154 --- /dev/null +++ b/Classes/GDataHTTPFetcher.h @@ -0,0 +1,466 @@ +/* Copyright (c) 2007 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// GDataHTTPFetcher.h +// + +// This is essentially a wrapper around NSURLConnection for POSTs and GETs. +// If setPostData: is called, then POST is assumed. +// +// When would you use this instead of NSURLConnection? +// +// - When you just want the result from a GET or POST +// - When you want the "standard" behavior for connections (redirection handling +// an so on) +// - When you want to avoid cookie collisions with Safari and other applications +// - When you want to provide if-modified-since headers +// - When you need to set a credential for the http +// - When you want to avoid changing WebKit's cookies +// +// This is assumed to be a one-shot fetch request; don't reuse the object +// for a second fetch. +// +// The fetcher may be created auto-released, in which case it will release +// itself after the fetch completion callback. The fetcher +// is implicitly retained as long as a connection is pending. +// +// But if you may need to cancel the fetcher, allocate it with initWithRequest: +// and have the delegate release the fetcher in the callbacks. +// +// Sample usage: +// +// NSURLRequest *request = [NSURLRequest requestWithURL:myURL]; +// GDataHTTPFetcher* myFetcher = [GDataHTTPFetcher httpFetcherWithRequest:request]; +// +// // optional post data +// [myFetcher setPostData:[postString dataUsingEncoding:NSUTF8StringEncoding]]; +// +// // optional dictionary, for persisting modified-dates and local cookie storage +// [myFetcher setFetchHistory:myMutableDictionary]; +// +// [myFetcher beginFetchWithDelegate:self +// didFinishSelector:@selector(myFetcher:finishedWithData:) +// didFailSelector:@selector(myFetcher:failedWithError:)]; +// +// Upon fetch completion, the callback selectors are invoked; they should have +// these signatures (you can use any callback method names you want so long as +// the signatures match these): +// +// - (void)myFetcher:(GDataHTTPFetcher *)fetcher finishedWithData:(NSData *)retrievedData; +// - (void)myFetcher:(GDataHTTPFetcher *)fetcher failedWithError:(NSError *)error; +// +// NOTE: Fetches may retrieve data from the server even though the server +// returned an error. The failure selector is called when the server +// status is >= 300, with an NSError having domain +// kGDataHTTPFetcherStatusDomain and code set to the server status. +// +// Status codes are at +// +// +// Proxies: +// +// Proxy handling is invisible so long as the system has a valid credential in +// the keychain, which is normally true (else most NSURL-based apps would have +// difficulty.) But when there is a proxy authetication error, the the fetcher +// will call the failedWithError: method with the NSURLChallenge in the error's +// userInfo. The error method can get the challenge info like this: +// +// NSURLAuthenticationChallenge *challenge +// = [[error userInfo] objectForKey:kGDataHTTPFetcherErrorChallengeKey]; +// BOOL isProxyChallenge = [[challenge protectionSpace] isProxy]; +// +// If a proxy error occurs, you can ask the user for the proxy username/password +// and call fetcher's setProxyCredential: to provide those for the +// next attempt to fetch. +// +// +// Cookies: +// +// There are three supported mechanisms for remembering cookies between fetches. +// +// By default, GDataHTTPFetcher uses a mutable array held statically to track +// cookies for all instantiated fetchers. This avoids server cookies being set +// by servers for the application from interfering with Safari cookie settings, +// and vice versa. The fetcher cookies are lost when the application quits. +// +// To rely instead on WebKit's global NSHTTPCookieStorage, call +// setCookieStorageMethod: with kGDataHTTPFetcherCookieStorageMethodSystemDefault. +// +// If you provide a fetch history dictionary (such as for periodic checks, +// described below) then the cookie storage mechanism is set to use the fetch +// history rather than the static storage. +// +// +// Fetching for periodic checks: +// +// The fetcher object can track "Last-modified" dates on returned data and +// provide an "If-modified-since" header. This allows the server to save +// bandwidth by providing a "Nothing changed" status message instead of response +// data. +// +// To get this behavior, provide a persistent mutable dictionary to +// setFetchHistory:, and look for the failedWithStatus: callback with code 304 +// (kGDataHTTPFetcherStatusNotModified) like this: +// +// - (void)myFetcher:(GDataHTTPFetcher *)fetcher failedWithStatus:(int)status data:(NSData *)data { +// if (status == kGDataHTTPFetcherStatusNotModified) { +// // |data| is empty; use the data from the previous finishedWithData: for this URL +// } else { +// // handle other server status code +// } +// } +// +// The fetchHistory mutable dictionary should be maintained by the client between +// fetches and given to each fetcher intended to have the If-modified-since header +// or the same cookie storage. +// +// +// Monitoring received data +// +// The optional received data selector can be set with setReceivedDataSelector: +// and should have the signature +// +// - (void)myFetcher:(GDataHTTPFetcher *)fetcher receivedData:(NSData *)dataReceivedSoFar; +// +// The bytes received so far are [dataReceivedSoFar length]. This number may go +// down if a redirect causes the download to begin again from a new server. +// +// If supplied by the server, the anticipated total download size is available as +// [[myFetcher response] expectedContentLength] (may be -1 for unknown +// download sizes.) +// +// +// Automatic retrying of fetches +// +// The fetcher can optionally create a timer and reattempt certain kinds of +// fetch failures (status codes 408, request timeout; 503, service unavailable; +// 504, gateway timeout; networking errors NSURLErrorTimedOut and +// NSURLErrorNetworkConnectionLost.) The user may set a retry selector to +// customize the type of errors which will be retried. +// +// Retries are done in an exponential-backoff fashion (that is, after 1 second, +// 2, 4, 8, and so on.) +// +// Enabling automatic retries looks like this: +// [myFetcher setIsRetryEnabled:YES]; +// +// With retries enabled, the success or failure callbacks are called only +// when no more retries will be attempted. Calling the fetcher's stopFetching +// method will terminate the retry timer, without the finished or failure +// selectors being invoked. +// +// Optionally, the client may set the maximum retry interval: +// [myFetcher setMaxRetryInterval:60.]; // in seconds; default is 600 seconds +// +// Also optionally, the client may provide a callback selector to determine +// if a status code or other error should be retried. +// [myFetcher setRetrySelector:@selector(myFetcher:willRetry:forError:)]; +// +// If set, the retry selector should have the signature: +// -(BOOL)fetcher:(GDataHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error +// and return YES to set the retry timer or NO to fail without additional +// fetch attempts. +// +// The retry method may return the |suggestedWillRetry| argument to get the +// default retry behavior. Server status codes are present in the +// error argument, and have the domain kGDataHTTPFetcherStatusDomain. The +// user's method may look something like this: +// +// -(BOOL)myFetcher:(GDataHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error { +// +// // perhaps examine [error domain] and [error code], or [fetcher retryCount] +// // +// // return YES to start the retry timer, NO to proceed to the failure +// // callback, or |suggestedWillRetry| to get default behavior for the +// // current error domain and code values. +// return suggestedWillRetry; +// } + + + +#pragma once + +#import + +#import "GDataDefines.h" + +#undef _EXTERN +#undef _INITIALIZE_AS +#ifdef GDATAHTTPFETCHER_DEFINE_GLOBALS +#define _EXTERN +#define _INITIALIZE_AS(x) =x +#else +#define _EXTERN extern +#define _INITIALIZE_AS(x) +#endif + +// notifications +_EXTERN NSString* const kGDataHTTPFetcherErrorDomain _INITIALIZE_AS(@"com.google.GDataHTTPFetcher"); +_EXTERN NSString* const kGDataHTTPFetcherStatusDomain _INITIALIZE_AS(@"com.google.HTTPStatus"); +_EXTERN NSString* const kGDataHTTPFetcherErrorChallengeKey _INITIALIZE_AS(@"challenge"); +_EXTERN NSString* const kGDataHTTPFetcherStatusDataKey _INITIALIZE_AS(@"data"); // data returned with a kGDataHTTPFetcherStatusDomain error + + +// fetch history mutable dictionary keys +_EXTERN NSString* const kGDataHTTPFetcherHistoryLastModifiedKey _INITIALIZE_AS(@"FetchHistoryLastModified"); +_EXTERN NSString* const kGDataHTTPFetcherHistoryDatedDataKey _INITIALIZE_AS(@"FetchHistoryDatedDataCache"); +_EXTERN NSString* const kGDataHTTPFetcherHistoryCookiesKey _INITIALIZE_AS(@"FetchHistoryCookies"); + +enum { + kGDataHTTPFetcherErrorDownloadFailed = -1, + kGDataHTTPFetcherErrorAuthenticationChallengeFailed = -2, + + kGDataHTTPFetcherStatusNotModified = 304 +}; + +enum { + kGDataHTTPFetcherCookieStorageMethodStatic = 0, + kGDataHTTPFetcherCookieStorageMethodFetchHistory = 1, + kGDataHTTPFetcherCookieStorageMethodSystemDefault = 2 +}; + +void AssertSelectorNilOrImplementedWithArguments(id obj, SEL sel, ...); + +// async retrieval of an http get or post +@interface GDataHTTPFetcher : NSObject { + NSMutableURLRequest *request_; + NSURLConnection *connection_; // while connection_ is non-nil, delegate_ is retained + NSMutableData *downloadedData_; + NSURLCredential *credential_; // username & password + NSURLCredential *proxyCredential_; // credential supplied to proxy servers + NSData *postData_; + NSInputStream *postStream_; + NSMutableData *loggedStreamData_; + NSURLResponse *response_; // set in connection:didReceiveResponse: + id delegate_; // WEAK (though retained during an open connection) + SEL finishedSEL_; // should by implemented by delegate + SEL statusFailedSEL_; // implemented by delegate if it needs separate network error callbacks + SEL networkFailedSEL_; // should be implemented by delegate + SEL receivedDataSEL_; // optional, set with setReceivedDataSelector + id userData_; // retained, if set by caller + NSMutableDictionary *properties_; // more data retained for caller + NSArray *runLoopModes_; // optional, for 10.5 and later + NSMutableDictionary *fetchHistory_; // if supplied by the caller, used for Last-Modified-Since checks and cookies + BOOL shouldCacheDatedData_; // if true, remembers and returns data marked with a last-modified date + int cookieStorageMethod_; // constant from above + + BOOL isRetryEnabled_; // user wants auto-retry + SEL retrySEL_; // optional; set with setRetrySelector + NSTimer *retryTimer_; + unsigned int retryCount_; + NSTimeInterval maxRetryInterval_; // default 600 seconds + NSTimeInterval minRetryInterval_; // random between 1 and 2 seconds + NSTimeInterval retryFactor_; // default interval multiplier is 2 + NSTimeInterval lastRetryInterval_; +} + +// create a fetcher +// +// httpFetcherWithRequest will return an autoreleased fetcher, but if +// the connection is successfully created, the connection should retain the +// fetcher for the life of the connection as well. So the caller doesn't have +// to retain the fetcher explicitly unless they want to be able to cancel it. ++ (GDataHTTPFetcher *)httpFetcherWithRequest:(NSURLRequest *)request; + +// designated initializer +- (id)initWithRequest:(NSURLRequest *)request; + +- (NSMutableURLRequest *)request; +- (void)setRequest:(NSURLRequest *)theRequest; + +// setting the credential is optional; it is used if the connection receives +// an authentication challenge +- (NSURLCredential *)credential; +- (void)setCredential:(NSURLCredential *)theCredential; + +// setting the proxy credential is optional; it is used if the connection +// receives an authentication challenge from a proxy +- (NSURLCredential *)proxyCredential; +- (void)setProxyCredential:(NSURLCredential *)theCredential; + + +// if post data or stream is not set, then a GET retrieval method is assumed +- (NSData *)postData; +- (void)setPostData:(NSData *)theData; + +// beware: In 10.4, NSInputStream fails to copy or retain +// the data it was initialized with, contrary to docs +- (NSInputStream *)postStream; +- (void)setPostStream:(NSInputStream *)theStream; + +- (int)cookieStorageMethod; +- (void)setCookieStorageMethod:(int)method; + +// returns cookies from the currently appropriate cookie storage +- (NSArray *)cookiesForURL:(NSURL *)theURL; + +// the delegate is not retained except during the connection +- (id)delegate; +- (void)setDelegate:(id)theDelegate; + +// the delegate's optional receivedData selector has a signature like: +// - (void)myFetcher:(GDataHTTPFetcher *)fetcher receivedData:(NSData *)dataReceivedSoFar; +- (SEL)receivedDataSelector; +- (void)setReceivedDataSelector:(SEL)theSelector; + + +// retrying; see comments at the top of the file. Calling +// setIsRetryEnabled(YES) resets the min and max retry intervals. +- (BOOL)isRetryEnabled; +- (void)setIsRetryEnabled:(BOOL)flag; + +// retry selector is optional for retries. +// +// If present, it should have the signature: +// -(BOOL)fetcher:(GDataHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error +// and return YES to cause a retry. See comments at the top of this file. +- (SEL)retrySelector; +- (void)setRetrySelector:(SEL)theSel; + +// retry intervals must be strictly less than maxRetryInterval, else +// they will be limited to maxRetryInterval and no further retries will +// be attempted. Setting maxRetryInterval to 0.0 will reset it to the +// default value, 600 seconds. +- (NSTimeInterval)maxRetryInterval; +- (void)setMaxRetryInterval:(NSTimeInterval)secs; + +// Starting retry interval. Setting minRetryInterval to 0.0 will reset it +// to a random value between 1.0 and 2.0 seconds. Clients should normally not +// call this except for unit testing. +- (NSTimeInterval)minRetryInterval; +- (void)setMinRetryInterval:(NSTimeInterval)secs; + +// Multiplier used to increase the interval between retries, typically 2.0. +// Clients should not need to call this. +- (double)retryFactor; +- (void)setRetryFactor:(double)multiplier; + +// number of retries attempted +- (unsigned int)retryCount; + +// interval delay to precede next retry +- (NSTimeInterval)nextRetryInterval; + +// Begin fetching the request (simplified interface) +// +// The delegate can optionally implement the finished and failure selectors +// or pass nil for them. +// +// Returns YES if the fetch is initiated. The delegate is retained between +// the beginFetch call until after the finish/fail callbacks. +// +// The failure selector is called for server statuses 300 or higher, with the +// status stored as the error object's code. +// +// finishedSEL has a signature like: +// - (void)fetcher:(GDataHTTPFetcher *)fetcher finishedWithData:(NSData *)data +// failedSEL has a signature like: +// - (void)fetcher:(GDataHTTPFetcher *)fetcher failedWithError:(NSError *)error +// + +- (BOOL)beginFetchWithDelegate:(id)delegate + didFinishSelector:(SEL)finishedSEL + didFailSelector:(SEL)failedSEL; + + +// Begin fetching the request (original interface) +// +// The delegate can optionally implement the finished, status failure, and +// network failure selectors, or pass nill for them. +// +// Returns YES if the fetch is initiated. The delegate is retained between +// the beginFetch call until after the finish/fail callbacks. +// +// The failure selector is called if the server returns status >= 300 +// +// finishedSEL has a signature like: +// - (void)fetcher:(GDataHTTPFetcher *)fetcher finishedWithData:(NSData *)data +// statusFailedSEL has a signature like: +// - (void)fetcher:(GDataHTTPFetcher *)fetcher failedWithStatus:(int)status data:(NSData *)data +// failedSEL has a signature like: +// - (void)fetcher:(GDataHTTPFetcher *)fetcher failedWithError:(NSError *)error +// + +- (BOOL)beginFetchWithDelegate:(id)delegate + didFinishSelector:(SEL)finishedSEL + didFailWithStatusSelector:(SEL)statusFailedSEL + didFailWithErrorSelector:(SEL)networkFailedSEL; + +// Returns YES if this is in the process of fetching a URL +- (BOOL)isFetching; + +// Cancel the fetch of the request that's currently in progress +- (void)stopFetching; + +// return the status code from the server response +- (NSInteger)statusCode; + +// return the http headers from the response +- (NSDictionary *)responseHeaders; + +// the response, once it's been received +- (NSURLResponse *)response; +- (void)setResponse:(NSURLResponse *)response; + +// if the caller supplies a mutable dictionary, it's used for Last-Modified-Since +// checks and for cookie storage +// side effect: setFetchHistory implicitly calls setCookieStorageMethod: +- (NSMutableDictionary *)fetchHistory; +- (void)setFetchHistory:(NSMutableDictionary *)fetchHistory; + +// for fetched data with a last-modified date, cache the data +// in the fetch history and return cached data instead of a 304 error +// Set this to NO if you want to handle status 304 (Not changed) rather than be +// delivered cached data from previous fetches. Default is NO. +// When a cache result is returned, the didFinish selector is called +// with the data, but [fetcher status] still returns 304. + +- (BOOL)shouldCacheDatedData; +- (void)setShouldCacheDatedData:(BOOL)flag; + +// delete last-modified dates and cached data from the fetch history +- (void)clearDatedDataHistory; + +// userData is retained for the convenience of the caller +- (id)userData; +- (void)setUserData:(id)theObj; + +// properties are retained for the convenience of the caller +- (void)setProperties:(NSDictionary *)dict; +- (NSDictionary *)properties; + +- (void)setProperty:(id)obj forKey:(NSString *)key; // pass nil obj to remove property +- (id)propertyForKey:(NSString *)key; + +// using the fetcher while a modal dialog is displayed requires setting the +// run-loop modes to include NSModalPanelRunLoopMode +// +// setting run loop modes does nothing if they are not supported, +// such as on 10.4 +- (NSArray *)runLoopModes; +- (void)setRunLoopModes:(NSArray *)modes; + ++ (BOOL)doesSupportRunLoopModes; ++ (NSArray *)defaultRunLoopModes; ++ (void)setDefaultRunLoopModes:(NSArray *)modes; + +// users who wish to replace GDataHTTPFetcher's use of NSURLConnection +// can do so globally here. The replacement should be a subclass of +// NSURLConnection. ++ (Class)connectionClass; ++ (void)setConnectionClass:(Class)theClass; + +@end diff --git a/Classes/GDataHTTPFetcher.m b/Classes/GDataHTTPFetcher.m new file mode 100755 index 0000000..b0711f4 --- /dev/null +++ b/Classes/GDataHTTPFetcher.m @@ -0,0 +1,1339 @@ +/* Copyright (c) 2007 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +// +// GDataHTTPFetcher.m +// + +#define GDATAHTTPFETCHER_DEFINE_GLOBALS 1 + +#import "GDataHTTPFetcher.h" +#import "GDataHTTPFetcherLogging.h" + +#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4 +@interface NSURLConnection (LeopardMethodsOnTigerBuilds) +- (id)initWithRequest:(NSURLRequest *)request delegate:(id)delegate startImmediately:(BOOL)startImmediately; +- (void)start; +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; +@end +#endif + +static NSString* const kGDataLastModifiedHeader = @"Last-Modified"; +static NSString* const kGDataIfModifiedSinceHeader = @"If-Modified-Since"; + +SEL const kUnifiedFailureCallback = (SEL) (void *) -1; + +static NSMutableArray* gGDataFetcherStaticCookies = nil; +static Class gGDataFetcherConnectionClass = nil; +static NSArray *gGDataFetcherDefaultRunLoopModes = nil; + +const NSTimeInterval kDefaultMaxRetryInterval = 60. * 10.; // 10 minutes + +@interface GDataHTTPFetcher (PrivateMethods) +- (void)setCookies:(NSArray *)newCookies + inArray:(NSMutableArray *)cookieStorageArray; +- (NSArray *)cookiesForURL:(NSURL *)theURL inArray:(NSMutableArray *)cookieStorageArray; +- (void)handleCookiesForResponse:(NSURLResponse *)response; + +- (BOOL)shouldRetryNowForStatus:(NSInteger)status error:(NSError *)error; +- (void)destroyRetryTimer; +- (void)beginRetryTimer; +- (void)primeRetryTimerWithNewTimeInterval:(NSTimeInterval)secs; +- (void)retryFetch; +@end + +@implementation GDataHTTPFetcher + ++ (GDataHTTPFetcher *)httpFetcherWithRequest:(NSURLRequest *)request { + return [[[GDataHTTPFetcher alloc] initWithRequest:request] autorelease]; +} + ++ (void)initialize { + if (!gGDataFetcherStaticCookies) { + gGDataFetcherStaticCookies = [[NSMutableArray alloc] init]; + } +} + +- (id)init { + return [self initWithRequest:nil]; +} + +- (id)initWithRequest:(NSURLRequest *)request { + if ((self = [super init]) != nil) { + + request_ = [request mutableCopy]; + + [self setCookieStorageMethod:kGDataHTTPFetcherCookieStorageMethodStatic]; + } + return self; +} + +- (void)dealloc { + [self stopFetching]; // releases connection_, destroys timers + + [request_ release]; + [downloadedData_ release]; + [credential_ release]; + [proxyCredential_ release]; + [postData_ release]; + [postStream_ release]; + [loggedStreamData_ release]; + [response_ release]; + [userData_ release]; + [properties_ release]; + [runLoopModes_ release]; + [fetchHistory_ release]; + + [super dealloc]; +} + +#pragma mark - + +// Updated fetched API +// +// Begin fetching the URL. The delegate is retained for the duration of +// the fetch connection. +// +// The delegate must provide and implement the finished and failed selectors. +// +// finishedSEL has a signature like: +// - (void)fetcher:(GDataHTTPFetcher *)fetcher finishedWithData:(NSData *)data +// failedSEL has a signature like: +// - (void)fetcher:(GDataHTTPFetcher *)fetcher failedWithError:(NSError *)error +// +// Server errors (status >= 300) are reported as the code of the error object. + +- (BOOL)beginFetchWithDelegate:(id)delegate + didFinishSelector:(SEL)finishedSEL + didFailSelector:(SEL)failedSEL { + + return [self beginFetchWithDelegate:delegate + didFinishSelector:finishedSEL + didFailWithStatusSelector:kUnifiedFailureCallback + didFailWithErrorSelector:failedSEL]; +} + +// Original fetcher API +// +// Begin fetching the URL. The delegate is retained for the duration of +// the fetch connection. +// +// The delegate must provide and implement the finished and failed selectors. +// +// finishedSEL has a signature like: +// - (void)fetcher:(GDataHTTPFetcher *)fetcher finishedWithData:(NSData *)data +// statusFailedSEL has a signature like: +// - (void)fetcher:(GDataHTTPFetcher *)fetcher failedWithStatus:(int)status data:(NSData *)data +// failedSEL has a signature like: +// - (void)fetcher:(GDataHTTPFetcher *)fetcher failedWithError:(NSError *)error + +- (BOOL)beginFetchWithDelegate:(id)delegate + didFinishSelector:(SEL)finishedSEL + didFailWithStatusSelector:(SEL)statusFailedSEL + didFailWithErrorSelector:(SEL)networkFailedSEL { + + AssertSelectorNilOrImplementedWithArguments(delegate, finishedSEL, @encode(GDataHTTPFetcher *), @encode(NSData *), 0); + AssertSelectorNilOrImplementedWithArguments(delegate, networkFailedSEL, @encode(GDataHTTPFetcher *), @encode(NSError *), 0); + AssertSelectorNilOrImplementedWithArguments(delegate, receivedDataSEL_, @encode(GDataHTTPFetcher *), @encode(NSData *), 0); + AssertSelectorNilOrImplementedWithArguments(delegate, retrySEL_, @encode(GDataHTTPFetcher *), @encode(BOOL), @encode(NSError *), 0); + + if (statusFailedSEL != kUnifiedFailureCallback) { + AssertSelectorNilOrImplementedWithArguments(delegate, statusFailedSEL, @encode(GDataHTTPFetcher *), @encode(int), @encode(NSData *), 0); + } + + if (connection_ != nil) { + NSAssert1(connection_ != nil, @"fetch object %@ being reused; this should never happen", self); + goto CannotBeginFetch; + } + + if (request_ == nil) { + NSAssert(request_ != nil, @"beginFetchWithDelegate requires a request"); + goto CannotBeginFetch; + } + + [downloadedData_ release]; + downloadedData_ = nil; + + [self setDelegate:delegate]; + finishedSEL_ = finishedSEL; + networkFailedSEL_ = networkFailedSEL; + statusFailedSEL_ = statusFailedSEL; + + NSString *effectiveHTTPMethod = [request_ valueForHTTPHeaderField:@"X-HTTP-Method-Override"]; + if (effectiveHTTPMethod == nil) { + effectiveHTTPMethod = [request_ HTTPMethod]; + } + BOOL isEffectiveHTTPGet = (effectiveHTTPMethod == nil + || [effectiveHTTPMethod isEqual:@"GET"]); + + if (postData_ || postStream_) { + if (isEffectiveHTTPGet) { + [request_ setHTTPMethod:@"POST"]; + isEffectiveHTTPGet = NO; + } + + if (postData_) { + [request_ setHTTPBody:postData_]; + } else { + + // if logging is enabled, it needs a buffer to accumulate data from any + // NSInputStream used for uploading. Logging will wrap the input + // stream with a stream that lets us keep a copy the data being read. + if ([GDataHTTPFetcher isLoggingEnabled] && postStream_ != nil) { + loggedStreamData_ = [[NSMutableData alloc] init]; + [self logCapturePostStream]; + } + + [request_ setHTTPBodyStream:postStream_]; + } + } + + if (fetchHistory_) { + + // If this URL is in the history, set the Last-Modified header field + + // if we have a history, we're tracking across fetches, so we don't + // want to pull results from a cache + [request_ setCachePolicy:NSURLRequestReloadIgnoringCacheData]; + + if (isEffectiveHTTPGet) { + NSDictionary* lastModifiedDict = [fetchHistory_ objectForKey:kGDataHTTPFetcherHistoryLastModifiedKey]; + NSString* urlString = [[request_ URL] absoluteString]; + NSString* lastModifiedStr = [lastModifiedDict objectForKey:urlString]; + + // servers don't want if-modified-since on anything but GETs + if (lastModifiedStr != nil) { + [request_ addValue:lastModifiedStr forHTTPHeaderField:kGDataIfModifiedSinceHeader]; + } + } + } + + // get cookies for this URL from our storage array, if + // we have a storage array + if (cookieStorageMethod_ != kGDataHTTPFetcherCookieStorageMethodSystemDefault) { + + NSArray *cookies = [self cookiesForURL:[request_ URL]]; + if ([cookies count]) { + + NSDictionary *headerFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + NSString *cookieHeader = [headerFields objectForKey:@"Cookie"]; // key used in header dictionary + if (cookieHeader) { + [request_ addValue:cookieHeader forHTTPHeaderField:@"Cookie"]; // header name + } + } + } + + // finally, start the connection + + Class connectionClass = [[self class] connectionClass]; + + NSArray *runLoopModes = nil; + + if ([[self class] doesSupportRunLoopModes]) { + + // use the connection-specific run loop modes, if they were provided, + // or else use the GDataHTTPFetcher default run loop modes, if any + if (runLoopModes_) { + runLoopModes = runLoopModes_; + } else { + runLoopModes = gGDataFetcherDefaultRunLoopModes; + } + } + + if ([runLoopModes count] == 0) { + + // if no run loop modes were specified, then we'll start the connection + // on the current run loop in the current mode + connection_ = [[connectionClass connectionWithRequest:request_ + delegate:self] retain]; + } else { + + // schedule on current run loop in the specified modes + connection_ = [[connectionClass alloc] initWithRequest:request_ + delegate:self + startImmediately:NO]; + NSEnumerator *modeEnumerator = [runLoopModes objectEnumerator]; + NSString *mode; + while ((mode = [modeEnumerator nextObject]) != nil) { + [connection_ scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:mode]; + } + [connection_ start]; + } + + if (!connection_) { + NSAssert(connection_ != nil, @"beginFetchWithDelegate could not create a connection"); + goto CannotBeginFetch; + } + + // we'll retain the delegate only during the outstanding connection (similar + // to what Cocoa does with performSelectorOnMainThread:) since we'd crash + // if the delegate was released in the interim. We don't retain the selector + // at other times, to avoid vicious retain loops. This retain is balanced in + // the -stopFetch method. + [delegate_ retain]; + + downloadedData_ = [[NSMutableData alloc] init]; + return YES; + +CannotBeginFetch: + + if (networkFailedSEL) { + + NSError *error = [NSError errorWithDomain:kGDataHTTPFetcherErrorDomain + code:kGDataHTTPFetcherErrorDownloadFailed + userInfo:nil]; + + [[self retain] autorelease]; // in case the callback releases us + + [delegate performSelector:networkFailedSEL + withObject:self + withObject:error]; + } + + return NO; +} + +// Returns YES if this is in the process of fetching a URL, or waiting to +// retry +- (BOOL)isFetching { + return (connection_ != nil || retryTimer_ != nil); +} + +// Returns the status code set in connection:didReceiveResponse: +- (NSInteger)statusCode { + + NSInteger statusCode; + + if (response_ != nil + && [response_ respondsToSelector:@selector(statusCode)]) { + + statusCode = [(NSHTTPURLResponse *)response_ statusCode]; + } else { + // Default to zero, in hopes of hinting "Unknown" (we can't be + // sure that things are OK enough to use 200). + statusCode = 0; + } + return statusCode; +} + +- (NSDictionary *)responseHeaders { + if (response_ != nil + && [response_ respondsToSelector:@selector(allHeaderFields)]) { + + NSDictionary *headers = [(NSHTTPURLResponse *)response_ allHeaderFields]; + return headers; + } + return nil; +} + +// Cancel the fetch of the URL that's currently in progress. +- (void)stopFetching { + [self destroyRetryTimer]; + + if (connection_) { + // in case cancelling the connection calls this recursively, we want + // to ensure that we'll only release the connection and delegate once, + // so first set connection_ to nil + + NSURLConnection* oldConnection = connection_; + connection_ = nil; + + // this may be called in a callback from the connection, so use autorelease + [oldConnection cancel]; + [oldConnection autorelease]; + + // balance the retain done when the connection was opened + [delegate_ release]; + } +} + +- (void)retryFetch { + + id holdDelegate = [[delegate_ retain] autorelease]; + + [self stopFetching]; + + [self beginFetchWithDelegate:holdDelegate + didFinishSelector:finishedSEL_ + didFailWithStatusSelector:statusFailedSEL_ + didFailWithErrorSelector:networkFailedSEL_]; +} + +#pragma mark NSURLConnection Delegate Methods + +// +// NSURLConnection Delegate Methods +// + +// This method just says "follow all redirects", which _should_ be the default behavior, +// According to file:///Developer/ADC%20Reference%20Library/documentation/Cocoa/Conceptual/URLLoadingSystem +// but the redirects were not being followed until I added this method. May be +// a bug in the NSURLConnection code, or the documentation. +// +// In OS X 10.4.8 and earlier, the redirect request doesn't +// get the original's headers and body. This causes POSTs to fail. +// So we construct a new request, a copy of the original, with overrides from the +// redirect. +// +// Docs say that if redirectResponse is nil, just return the redirectRequest. + +- (NSURLRequest *)connection:(NSURLConnection *)connection + willSendRequest:(NSURLRequest *)redirectRequest + redirectResponse:(NSURLResponse *)redirectResponse { + + if (redirectRequest && redirectResponse) { + NSMutableURLRequest *newRequest = [[request_ mutableCopy] autorelease]; + // copy the URL + NSURL *redirectURL = [redirectRequest URL]; + NSURL *url = [newRequest URL]; + + // disallow scheme changes (say, from https to http) + NSString *redirectScheme = [url scheme]; + NSString *newScheme = [redirectURL scheme]; + NSString *newResourceSpecifier = [redirectURL resourceSpecifier]; + + if ([redirectScheme caseInsensitiveCompare:@"http"] == NSOrderedSame + && newScheme != nil + && [newScheme caseInsensitiveCompare:@"https"] == NSOrderedSame) { + + // allow the change from http to https + redirectScheme = newScheme; + } + + NSString *newUrlString = [NSString stringWithFormat:@"%@:%@", + redirectScheme, newResourceSpecifier]; + + NSURL *newURL = [NSURL URLWithString:newUrlString]; + [newRequest setURL:newURL]; + + // any headers in the redirect override headers in the original. + NSDictionary *redirectHeaders = [redirectRequest allHTTPHeaderFields]; + if (redirectHeaders) { + NSEnumerator *enumerator = [redirectHeaders keyEnumerator]; + NSString *key; + while (nil != (key = [enumerator nextObject])) { + NSString *value = [redirectHeaders objectForKey:key]; + [newRequest setValue:value forHTTPHeaderField:key]; + } + } + redirectRequest = newRequest; + + // save cookies from the response + [self handleCookiesForResponse:redirectResponse]; + + // log the response we just received + [self setResponse:redirectResponse]; + [self logFetchWithError:nil]; + + // update the request for future logging + [self setRequest:redirectRequest]; +} + return redirectRequest; +} + +- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { + + // this method is called when the server has determined that it + // has enough information to create the NSURLResponse + // it can be called multiple times, for example in the case of a + // redirect, so each time we reset the data. + [downloadedData_ setLength:0]; + + [self setResponse:response]; + + // save cookies from the response + [self handleCookiesForResponse:response]; +} + + +// handleCookiesForResponse: handles storage of cookies for responses passed to +// connection:willSendRequest:redirectResponse: and connection:didReceiveResponse: +- (void)handleCookiesForResponse:(NSURLResponse *)response { + + if (cookieStorageMethod_ == kGDataHTTPFetcherCookieStorageMethodSystemDefault) { + + // do nothing special for NSURLConnection's default storage mechanism + + } else if ([response respondsToSelector:@selector(allHeaderFields)]) { + + // grab the cookies from the header as NSHTTPCookies and store them either + // into our static array or into the fetchHistory + + NSDictionary *responseHeaderFields = [(NSHTTPURLResponse *)response allHeaderFields]; + if (responseHeaderFields) { + + NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:responseHeaderFields + forURL:[response URL]]; + if ([cookies count] > 0) { + + NSMutableArray *cookieArray = nil; + + // static cookies are stored in gGDataFetcherStaticCookies; fetchHistory + // cookies are stored in fetchHistory_'s kGDataHTTPFetcherHistoryCookiesKey + + if (cookieStorageMethod_ == kGDataHTTPFetcherCookieStorageMethodStatic) { + + cookieArray = gGDataFetcherStaticCookies; + + } else if (cookieStorageMethod_ == kGDataHTTPFetcherCookieStorageMethodFetchHistory + && fetchHistory_ != nil) { + + cookieArray = [fetchHistory_ objectForKey:kGDataHTTPFetcherHistoryCookiesKey]; + if (cookieArray == nil) { + cookieArray = [NSMutableArray array]; + [fetchHistory_ setObject:cookieArray forKey:kGDataHTTPFetcherHistoryCookiesKey]; + } + } + + if (cookieArray) { + @synchronized(cookieArray) { + [self setCookies:cookies inArray:cookieArray]; + } + } + } + } + } +} + +-(void)connection:(NSURLConnection *)connection + didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { + + if ([challenge previousFailureCount] <= 2) { + + NSURLCredential *credential = credential_; + + if ([[challenge protectionSpace] isProxy] && proxyCredential_ != nil) { + credential = proxyCredential_; + } + + // Here, if credential is still nil, then we *could* try to get it from + // NSURLCredentialStorage's defaultCredentialForProtectionSpace:. + // We don't, because we're assuming: + // + // - for server credentials, we only want ones supplied by the program + // calling http fetcher + // - for proxy credentials, if one were necessary and available in the + // keychain, it would've been found automatically by NSURLConnection + // and this challenge delegate method never would've been called + // anyway + + if (credential) { + // try the credential + [[challenge sender] useCredential:credential + forAuthenticationChallenge:challenge]; + return; + } + } + + // If we don't have credentials, or we've already failed auth 3x, + // report the error, putting the challenge as a value in the userInfo + // dictionary + // + // put the challenge in the userInfo dictionary first so that cancelling + // doesn't release it yet + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:challenge + forKey:kGDataHTTPFetcherErrorChallengeKey]; + [[challenge sender] cancelAuthenticationChallenge:challenge]; + + + NSError *error = [NSError errorWithDomain:kGDataHTTPFetcherErrorDomain + code:kGDataHTTPFetcherErrorAuthenticationChallengeFailed + userInfo:userInfo]; + + [self connection:connection didFailWithError:error]; +} + + + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { + + [downloadedData_ appendData:data]; + + if (receivedDataSEL_) { + [delegate_ performSelector:receivedDataSEL_ + withObject:self + withObject:downloadedData_]; + } +} + +- (void)updateFetchHistory { + + if (fetchHistory_) { + + NSString* urlString = [[request_ URL] absoluteString]; + if ([response_ respondsToSelector:@selector(allHeaderFields)]) { + NSDictionary *headers = [(NSHTTPURLResponse *)response_ allHeaderFields]; + NSString* lastModifiedStr = [headers objectForKey:kGDataLastModifiedHeader]; + + // get the dictionary mapping URLs to last-modified dates + NSMutableDictionary* lastModifiedDict = [fetchHistory_ objectForKey:kGDataHTTPFetcherHistoryLastModifiedKey]; + if (!lastModifiedDict) { + lastModifiedDict = [NSMutableDictionary dictionary]; + [fetchHistory_ setObject:lastModifiedDict forKey:kGDataHTTPFetcherHistoryLastModifiedKey]; + } + + NSMutableDictionary* datedDataCache = nil; + if (shouldCacheDatedData_) { + // get the dictionary mapping URLs to cached, dated data + datedDataCache = [fetchHistory_ objectForKey:kGDataHTTPFetcherHistoryDatedDataKey]; + if (!datedDataCache) { + datedDataCache = [NSMutableDictionary dictionary]; + [fetchHistory_ setObject:datedDataCache forKey:kGDataHTTPFetcherHistoryDatedDataKey]; + } + } + + NSInteger statusCode = [self statusCode]; + if (statusCode != kGDataHTTPFetcherStatusNotModified) { + + // save this last modified date string for successful results (<300) + // If there's no last modified string, clear the dictionary + // entry for this URL. Also cache or delete the data, if appropriate + // (when datedDataCache is non-nil.) + if (lastModifiedStr && statusCode < 300) { + [lastModifiedDict setValue:lastModifiedStr forKey:urlString]; + [datedDataCache setValue:downloadedData_ forKey:urlString]; + } else { + [lastModifiedDict removeObjectForKey:urlString]; + [datedDataCache removeObjectForKey:urlString]; + } + } + } + } +} + +// for error 304's ("Not Modified") where we've cached the data, return status +// 200 ("OK") to the caller (but leave the fetcher status as 304) +// and copy the cached data to downloadedData_. +// For other errors or if there's no cached data, just return the actual status. +- (NSInteger)statusAfterHandlingNotModifiedError { + + NSInteger status = [self statusCode]; + if (status == kGDataHTTPFetcherStatusNotModified && shouldCacheDatedData_) { + + // get the dictionary of URLs and data + NSString* urlString = [[request_ URL] absoluteString]; + + NSDictionary* datedDataCache = [fetchHistory_ objectForKey:kGDataHTTPFetcherHistoryDatedDataKey]; + NSData* cachedData = [datedDataCache objectForKey:urlString]; + + if (cachedData) { + // copy our stored data, and forge the status to pass on to the delegate + [downloadedData_ setData:cachedData]; + status = 200; + } + } + return status; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection { + + [self updateFetchHistory]; + + [[self retain] autorelease]; // in case the callback releases us + + [self logFetchWithError:nil]; + + NSInteger status = [self statusAfterHandlingNotModifiedError]; + + // if there's an error status and the client gave us a status error + // selector, then call that selector + if (status >= 300 && statusFailedSEL_) { + + if ([self shouldRetryNowForStatus:status error:nil]) { + + [self beginRetryTimer]; + + } else if (statusFailedSEL_ == kUnifiedFailureCallback) { + + // not retrying, and no separate status callback, so call the + // sole failure selector + NSDictionary *userInfo = + [NSDictionary dictionaryWithObject:downloadedData_ + forKey:kGDataHTTPFetcherStatusDataKey]; + + NSError *error = [NSError errorWithDomain:kGDataHTTPFetcherStatusDomain + code:status + userInfo:userInfo]; + + [delegate_ performSelector:networkFailedSEL_ + withObject:self + withObject:error]; + + [self stopFetching]; + + } else { + // not retrying, call status failure callback + NSMethodSignature *signature = [delegate_ methodSignatureForSelector:statusFailedSEL_]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:statusFailedSEL_]; + [invocation setTarget:delegate_]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&status atIndex:3]; + [invocation setArgument:&downloadedData_ atIndex:4]; + [invocation invoke]; + + [self stopFetching]; + } + } else if (finishedSEL_) { + + // successful http status (under 300) + [delegate_ performSelector:finishedSEL_ + withObject:self + withObject:downloadedData_]; + [self stopFetching]; + } + +} + +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { + + [self logFetchWithError:error]; + + if ([self shouldRetryNowForStatus:0 error:error]) { + + [self beginRetryTimer]; + + } else { + + if (networkFailedSEL_) { + [[self retain] autorelease]; // in case the callback releases us + + [delegate_ performSelector:networkFailedSEL_ + withObject:self + withObject:error]; + } + + [self stopFetching]; + } +} + +#pragma mark Retries + +- (BOOL)isRetryError:(NSError *)error { + + struct retryRecord { + NSString *const domain; + int code; + }; + + // Previously we also retried for + // { NSURLErrorDomain, NSURLErrorNetworkConnectionLost } + // but at least on 10.4, once that happened, retries would keep failing + // with the same error. + + struct retryRecord retries[] = { + { kGDataHTTPFetcherStatusDomain, 408 }, // request timeout + { kGDataHTTPFetcherStatusDomain, 503 }, // service unavailable + { kGDataHTTPFetcherStatusDomain, 504 }, // request timeout + { NSURLErrorDomain, NSURLErrorTimedOut }, + { nil, 0 } + }; + + // NSError's isEqual always returns false for equal but distinct instances + // of NSError, so we have to compare the domain and code values explicitly + + for (int idx = 0; retries[idx].domain != nil; idx++) { + + if ([[error domain] isEqual:retries[idx].domain] + && [error code] == retries[idx].code) { + + return YES; + } + } + return NO; +} + + +// shouldRetryNowForStatus:error: returns YES if the user has enabled retries +// and the status or error is one that is suitable for retrying. "Suitable" +// means either the isRetryError:'s list contains the status or error, or the +// user's retrySelector: is present and returns YES when called. +- (BOOL)shouldRetryNowForStatus:(NSInteger)status + error:(NSError *)error { + + if ([self isRetryEnabled]) { + + if ([self nextRetryInterval] < [self maxRetryInterval]) { + + if (error == nil) { + // make an error for the status + error = [NSError errorWithDomain:kGDataHTTPFetcherStatusDomain + code:status + userInfo:nil]; + } + + BOOL willRetry = [self isRetryError:error]; + + if (retrySEL_) { + NSMethodSignature *signature = [delegate_ methodSignatureForSelector:retrySEL_]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:retrySEL_]; + [invocation setTarget:delegate_]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&willRetry atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invoke]; + + [invocation getReturnValue:&willRetry]; + } + + return willRetry; + } + } + + return NO; +} + +- (void)beginRetryTimer { + + NSTimeInterval nextInterval = [self nextRetryInterval]; + NSTimeInterval maxInterval = [self maxRetryInterval]; + + NSTimeInterval newInterval = MIN(nextInterval, maxInterval); + + [self primeRetryTimerWithNewTimeInterval:newInterval]; +} + +- (void)primeRetryTimerWithNewTimeInterval:(NSTimeInterval)secs { + + [self destroyRetryTimer]; + + lastRetryInterval_ = secs; + + retryTimer_ = [NSTimer scheduledTimerWithTimeInterval:secs + target:self + selector:@selector(retryTimerFired:) + userInfo:nil + repeats:NO]; + [retryTimer_ retain]; +} + +- (void)retryTimerFired:(NSTimer *)timer { + + [self destroyRetryTimer]; + + retryCount_++; + + [self retryFetch]; +} + +- (void)destroyRetryTimer { + + [retryTimer_ invalidate]; + [retryTimer_ autorelease]; + retryTimer_ = nil; +} + +- (unsigned int)retryCount { + return retryCount_; +} + +- (NSTimeInterval)nextRetryInterval { + // the next wait interval is the factor (2.0) times the last interval, + // but never less than the minimum interval + NSTimeInterval secs = lastRetryInterval_ * retryFactor_; + secs = MIN(secs, maxRetryInterval_); + secs = MAX(secs, minRetryInterval_); + + return secs; +} + +- (BOOL)isRetryEnabled { + return isRetryEnabled_; +} + +- (void)setIsRetryEnabled:(BOOL)flag { + + if (flag && !isRetryEnabled_) { + // We defer initializing these until the user calls setIsRetryEnabled + // to avoid seeding the random number generator if it's not needed. + // However, it means min and max intervals for this fetcher are reset + // as a side effect of calling setIsRetryEnabled. + // + // seed the random value, and make an initial retry interval + // random between 1.0 and 2.0 seconds + srandomdev(); + [self setMinRetryInterval:0.0]; + [self setMaxRetryInterval:kDefaultMaxRetryInterval]; + [self setRetryFactor:2.0]; + lastRetryInterval_ = 0.0; + } + isRetryEnabled_ = flag; +}; + +- (SEL)retrySelector { + return retrySEL_; +} + +- (void)setRetrySelector:(SEL)theSelector { + retrySEL_ = theSelector; +} + +- (NSTimeInterval)maxRetryInterval { + return maxRetryInterval_; +} + +- (void)setMaxRetryInterval:(NSTimeInterval)secs { + if (secs > 0) { + maxRetryInterval_ = secs; + } else { + maxRetryInterval_ = kDefaultMaxRetryInterval; + } +} + +- (double)minRetryInterval { + return minRetryInterval_; +} + +- (void)setMinRetryInterval:(NSTimeInterval)secs { + if (secs > 0) { + minRetryInterval_ = secs; + } else { + // set min interval to a random value between 1.0 and 2.0 seconds + // so that if multiple clients start retrying at the same time, they'll + // repeat at different times and avoid overloading the server + minRetryInterval_ = 1.0 + ((double)(random() & 0x0FFFF) / (double) 0x0FFFF); + } +} + +- (double)retryFactor { + return retryFactor_; +} + +- (void)setRetryFactor:(double)multiplier { + retryFactor_ = multiplier; +} + +#pragma mark Getters and Setters + +- (NSMutableURLRequest *)request { + return request_; +} + +- (void)setRequest:(NSURLRequest *)theRequest { + [request_ autorelease]; + request_ = [theRequest mutableCopy]; +} + +- (NSURLCredential *)credential { + return credential_; +} + +- (void)setCredential:(NSURLCredential *)theCredential { + [credential_ autorelease]; + credential_ = [theCredential retain]; +} + +- (NSURLCredential *)proxyCredential { + return proxyCredential_; +} + +- (void)setProxyCredential:(NSURLCredential *)theCredential { + [proxyCredential_ autorelease]; + proxyCredential_ = [theCredential retain]; +} + +- (NSData *)postData { + return postData_; +} + +- (void)setPostData:(NSData *)theData { + [postData_ autorelease]; + postData_ = [theData retain]; +} + +- (NSInputStream *)postStream { + return postStream_; +} + +- (void)setPostStream:(NSInputStream *)theStream { + [postStream_ autorelease]; + postStream_ = [theStream retain]; +} + +- (int)cookieStorageMethod { + return cookieStorageMethod_; +} + +- (void)setCookieStorageMethod:(int)method { + + cookieStorageMethod_ = method; + + if (method == kGDataHTTPFetcherCookieStorageMethodSystemDefault) { + [request_ setHTTPShouldHandleCookies:YES]; + } else { + [request_ setHTTPShouldHandleCookies:NO]; + } +} + +- (id)delegate { + return delegate_; +} + +- (void)setDelegate:(id)theDelegate { + + // we retain delegate_ only during the life of the connection + if (connection_) { + [delegate_ autorelease]; + delegate_ = [theDelegate retain]; + } else { + delegate_ = theDelegate; + } +} + +- (SEL)receivedDataSelector { + return receivedDataSEL_; +} + +- (void)setReceivedDataSelector:(SEL)theSelector { + receivedDataSEL_ = theSelector; +} + +- (NSURLResponse *)response { + return response_; +} + +- (void)setResponse:(NSURLResponse *)response { + [response_ autorelease]; + response_ = [response retain]; +} + +- (NSMutableDictionary *)fetchHistory { + return fetchHistory_; +} + +- (void)setFetchHistory:(NSMutableDictionary *)fetchHistory { + [fetchHistory_ autorelease]; + fetchHistory_ = [fetchHistory retain]; + + if (fetchHistory_ != nil) { + [self setCookieStorageMethod:kGDataHTTPFetcherCookieStorageMethodFetchHistory]; + } else { + [self setCookieStorageMethod:kGDataHTTPFetcherCookieStorageMethodStatic]; + } +} + +- (void)setShouldCacheDatedData:(BOOL)flag { + shouldCacheDatedData_ = flag; + if (!flag) { + [self clearDatedDataHistory]; + } +} + +- (BOOL)shouldCacheDatedData { + return shouldCacheDatedData_; +} + +// delete last-modified dates and cached data from the fetch history +- (void)clearDatedDataHistory { + [fetchHistory_ removeObjectForKey:kGDataHTTPFetcherHistoryLastModifiedKey]; + [fetchHistory_ removeObjectForKey:kGDataHTTPFetcherHistoryDatedDataKey]; +} + +- (id)userData { + return userData_; +} + +- (void)setUserData:(id)theObj { + [userData_ autorelease]; + userData_ = [theObj retain]; +} + +- (void)setProperties:(NSDictionary *)dict { + [properties_ autorelease]; + properties_ = [dict mutableCopy]; +} + +- (NSDictionary *)properties { + return properties_; +} + +- (void)setProperty:(id)obj forKey:(NSString *)key { + + if (properties_ == nil && obj != nil) { + properties_ = [[NSMutableDictionary alloc] init]; + } + + [properties_ setValue:obj forKey:key]; +} + +- (id)propertyForKey:(NSString *)key { + return [properties_ objectForKey:key]; +} + +- (NSArray *)runLoopModes { + return runLoopModes_; +} + +- (void)setRunLoopModes:(NSArray *)modes { + [runLoopModes_ autorelease]; + runLoopModes_ = [modes retain]; +} + ++ (BOOL)doesSupportRunLoopModes { + SEL sel = @selector(initWithRequest:delegate:startImmediately:); + return [NSURLConnection instancesRespondToSelector:sel]; +} + ++ (NSArray *)defaultRunLoopModes { + return gGDataFetcherDefaultRunLoopModes; +} + ++ (void)setDefaultRunLoopModes:(NSArray *)modes { + [gGDataFetcherDefaultRunLoopModes autorelease]; + gGDataFetcherDefaultRunLoopModes = [modes retain]; +} + ++ (Class)connectionClass { + if (gGDataFetcherConnectionClass == nil) { + gGDataFetcherConnectionClass = [NSURLConnection class]; + } + return gGDataFetcherConnectionClass; +} + ++ (void)setConnectionClass:(Class)theClass { + gGDataFetcherConnectionClass = theClass; +} + +#pragma mark Cookies + +// return a cookie from the array with the same name, domain, and path as the +// given cookie, or else return nil if none found +// +// Both the cookie being tested and all cookies in cookieStorageArray should +// be valid (non-nil name, domains, paths) +- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie + inArray:(NSArray *)cookieStorageArray { + + NSUInteger numberOfCookies = [cookieStorageArray count]; + NSString *name = [cookie name]; + NSString *domain = [cookie domain]; + NSString *path = [cookie path]; + + NSAssert3(name && domain && path, @"Invalid cookie (name:%@ domain:%@ path:%@)", + name, domain, path); + + for (NSUInteger idx = 0; idx < numberOfCookies; idx++) { + + NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx]; + + if ([[storedCookie name] isEqual:name] + && [[storedCookie domain] isEqual:domain] + && [[storedCookie path] isEqual:path]) { + + return storedCookie; + } + } + return nil; +} + +// remove any expired cookies from the array, excluding cookies with nil +// expirations +- (void)removeExpiredCookiesInArray:(NSMutableArray *)cookieStorageArray { + + // count backwards since we're deleting items from the array + for (NSInteger idx = [cookieStorageArray count] - 1; idx >= 0; idx--) { + + NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx]; + + NSDate *expiresDate = [storedCookie expiresDate]; + if (expiresDate && [expiresDate timeIntervalSinceNow] < 0) { + [cookieStorageArray removeObjectAtIndex:idx]; + } + } +} + + +// retrieve all cookies appropriate for the given URL, considering +// domain, path, cookie name, expiration, security setting. +// Side effect: removed expired cookies from the storage array +- (NSArray *)cookiesForURL:(NSURL *)theURL inArray:(NSMutableArray *)cookieStorageArray { + + [self removeExpiredCookiesInArray:cookieStorageArray]; + + NSMutableArray *foundCookies = [NSMutableArray array]; + + // we'll prepend "." to the desired domain, since we want the + // actual domain "nytimes.com" to still match the cookie domain ".nytimes.com" + // when we check it below with hasSuffix + NSString *host = [[theURL host] lowercaseString]; + NSString *path = [theURL path]; + NSString *scheme = [theURL scheme]; + + NSString *domain = nil; + BOOL isLocalhostRetrieval = NO; + + if ([host isEqual:@"localhost"]) { + isLocalhostRetrieval = YES; + } else { + if (host) { + domain = [@"." stringByAppendingString:host]; + } + } + + NSUInteger numberOfCookies = [cookieStorageArray count]; + for (NSUInteger idx = 0; idx < numberOfCookies; idx++) { + + NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx]; + + NSString *cookieDomain = [[storedCookie domain] lowercaseString]; + NSString *cookiePath = [storedCookie path]; + BOOL cookieIsSecure = [storedCookie isSecure]; + + BOOL domainIsOK; + + if (isLocalhostRetrieval) { + // prior to 10.5.6, the domain stored into NSHTTPCookies for localhost + // is "localhost.local" + domainIsOK = [cookieDomain isEqual:@"localhost"] + || [cookieDomain isEqual:@"localhost.local"]; + } else { + domainIsOK = [domain hasSuffix:cookieDomain]; + } + + BOOL pathIsOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath]; + BOOL secureIsOK = (!cookieIsSecure) || [scheme isEqual:@"https"]; + + if (domainIsOK && pathIsOK && secureIsOK) { + [foundCookies addObject:storedCookie]; + } + } + return foundCookies; +} + +// return cookies for the given URL using the current cookie storage method +- (NSArray *)cookiesForURL:(NSURL *)theURL { + + NSArray *cookies = nil; + NSMutableArray *cookieStorageArray = nil; + + if (cookieStorageMethod_ == kGDataHTTPFetcherCookieStorageMethodStatic) { + cookieStorageArray = gGDataFetcherStaticCookies; + } else if (cookieStorageMethod_ == kGDataHTTPFetcherCookieStorageMethodFetchHistory) { + cookieStorageArray = [fetchHistory_ objectForKey:kGDataHTTPFetcherHistoryCookiesKey]; + } else { + // kGDataHTTPFetcherCookieStorageMethodSystemDefault + cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:theURL]; + } + + if (cookieStorageArray) { + + @synchronized(cookieStorageArray) { + + // cookiesForURL returns a new array of immutable NSCookie objects + // from cookieStorageArray + cookies = [self cookiesForURL:theURL + inArray:cookieStorageArray]; + } + } + return cookies; +} + + +// add all cookies in the array |newCookies| to the storage array, +// replacing cookies in the storage array as appropriate +// Side effect: removes expired cookies from the storage array +- (void)setCookies:(NSArray *)newCookies + inArray:(NSMutableArray *)cookieStorageArray { + + [self removeExpiredCookiesInArray:cookieStorageArray]; + + NSEnumerator *newCookieEnum = [newCookies objectEnumerator]; + NSHTTPCookie *newCookie; + + while ((newCookie = [newCookieEnum nextObject]) != nil) { + + if ([[newCookie name] length] > 0 + && [[newCookie domain] length] > 0 + && [[newCookie path] length] > 0) { + + // remove the cookie if it's currently in the array + NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie + inArray:cookieStorageArray]; + if (oldCookie) { + [cookieStorageArray removeObject:oldCookie]; + } + + // make sure the cookie hasn't already expired + NSDate *expiresDate = [newCookie expiresDate]; + if ((!expiresDate) || [expiresDate timeIntervalSinceNow] > 0) { + [cookieStorageArray addObject:newCookie]; + } + + } else { + NSAssert1(NO, @"Cookie incomplete: %@", newCookie); + } + } +} +@end + +#ifdef GDATA_FOUNDATION_ONLY +#define Debugger() +#endif + +void AssertSelectorNilOrImplementedWithArguments(id obj, SEL sel, ...) { + + // verify that the object's selector is implemented with the proper + // number and type of arguments +#if DEBUG + va_list argList; + va_start(argList, sel); + + if (obj && sel) { + // check that the selector is implemented + if (![obj respondsToSelector:sel]) { + NSLog(@"\"%@\" selector \"%@\" is unimplemented or misnamed", + NSStringFromClass([obj class]), + NSStringFromSelector(sel)); + Debugger(); + } else { + const char *expectedArgType; + unsigned int argCount = 2; // skip self and _cmd + NSMethodSignature *sig = [obj methodSignatureForSelector:sel]; + + // check that each expected argument is present and of the correct type + while ((expectedArgType = va_arg(argList, const char*)) != 0) { + + if ([sig numberOfArguments] > argCount) { + const char *foundArgType = [sig getArgumentTypeAtIndex:argCount]; + + if(0 != strncmp(foundArgType, expectedArgType, strlen(expectedArgType))) { + NSLog(@"\"%@\" selector \"%@\" argument %d should be type %s", + NSStringFromClass([obj class]), + NSStringFromSelector(sel), (argCount - 2), expectedArgType); + Debugger(); + } + } + argCount++; + } + + // check that the proper number of arguments are present in the selector + if (argCount != [sig numberOfArguments]) { + NSLog( @"\"%@\" selector \"%@\" should have %d arguments", + NSStringFromClass([obj class]), + NSStringFromSelector(sel), (argCount - 2)); + Debugger(); + } + } + } + + va_end(argList); +#endif +} + + + + diff --git a/Classes/GDataHTTPFetcherLogging.h b/Classes/GDataHTTPFetcherLogging.h new file mode 100644 index 0000000..e3bff4d --- /dev/null +++ b/Classes/GDataHTTPFetcherLogging.h @@ -0,0 +1,70 @@ +/* Copyright (c) 2007 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#import "GDataHTTPFetcher.h" + +// GData HTTP Logging +// +// All traffic using GDataHTTPFetcher can be easily logged. Call +// +// [GDataHTTPFetcher setIsLoggingEnabled:YES]; +// +// to begin generating log files. +// +// Log files are put into a folder on the desktop called "GDataHTTPDebugLogs" +// unless another directory is specified with +setLoggingDirectory. +// +// In the iPhone simulator, the default logs location is the user's home +// directory. On the iPhone device, the default logs location is the +// application's documents directory on the device. +// +// Each run of an application gets a separate set of log files. An html +// file is generated to simplify browsing the run's http transactions. +// The html file includes javascript links for inline viewing of uploaded +// and downloaded data. +// +// A symlink is created in the logs folder to simplify finding the html file +// for the latest run of the application; the symlink is called +// +// AppName_http_log_newest.html +// +// For better viewing of XML logs, use Camino or Firefox rather than Safari. +// +// Projects may define STRIP_GDATA_FETCH_LOGGING to remove logging code. + +@interface GDataHTTPFetcher (GDataHTTPFetcherLogging) + +// Note: the default logs directory is ~/Desktop/GDataHTTPDebugLogs; it will be +// created as needed. If a custom directory is set, the directory should +// already exist. ++ (void)setLoggingDirectory:(NSString *)path; ++ (NSString *)loggingDirectory; + +// client apps can turn logging on and off ++ (void)setIsLoggingEnabled:(BOOL)flag; ++ (BOOL)isLoggingEnabled; + +// client apps can optionally specify process name and date string used in +// log file names ++ (void)setLoggingProcessName:(NSString *)str; ++ (NSString *)loggingProcessName; + ++ (void)setLoggingDateStamp:(NSString *)str; ++ (NSString *)loggingDateStamp; + +// internal; called by fetcher +- (void)logFetchWithError:(NSError *)error; +- (void)logCapturePostStream; +@end diff --git a/Classes/GDataHTTPFetcherLogging.m b/Classes/GDataHTTPFetcherLogging.m new file mode 100644 index 0000000..d0e4aab --- /dev/null +++ b/Classes/GDataHTTPFetcherLogging.m @@ -0,0 +1,827 @@ +/* Copyright (c) 2007 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +#import "GDataHTTPFetcherLogging.h" + +#if !STRIP_GDATA_FETCH_LOGGING + +#import "GDataProgressMonitorInputStream.h" + +// If logging isn't being stripped, make sure we have all the defines from +// GDataDefines.h +#import "GDataDefines.h" + +@interface GDataInputStreamLogger : GDataProgressMonitorInputStream +// GDataInputStreamLogger is wraps any NSInputStream used for +// uploading so we can capture a copy of the data for the log +@end + +// We don't invoke Leopard methods on 10.4, because we check if the methods are +// implemented before invoking it, but we need to be able to compile without +// warnings. +// These declarations mean if you target <=10.4, the methods will compile +// without complaint in this source, so you must test with +// -respondsToSelector:, too. +#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4 +@interface NSFileManager (LeopardMethodsOnTigerBuilds) +- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error; +- (BOOL)createSymbolicLinkAtPath:(NSString *)path + withDestinationPath:(NSString *)destPath error:(NSError **)error; +- (BOOL)createDirectoryAtPath:(NSString *)path + withIntermediateDirectories:(BOOL)createIntermediates + attributes:(NSDictionary *)attributes + error:(NSError **)error; +@end +#endif // MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4 + +#endif // !STRIP_GDATA_FETCH_LOGGING + +@implementation GDataHTTPFetcher (GDataHTTPFetcherLogging) + +// if STRIP_GDATA_FETCH_LOGGING is defined by the user's project then +// logging code will not be compiled into the framework + +#if STRIP_GDATA_FETCH_LOGGING +- (void)logFetchWithError:(NSError *)error {} + ++ (void)setLoggingDirectory:(NSString *)path {} ++ (NSString *)loggingDirectory {return nil;} + ++ (void)setIsLoggingEnabled:(BOOL)flag {} ++ (BOOL)isLoggingEnabled {return NO;} + ++ (void)setLoggingProcessName:(NSString *)str {} ++ (NSString *)loggingProcessName {return nil;} + ++ (void)setLoggingDateStamp:(NSString *)str {} ++ (NSString *)loggingDateStamp {return nil;} + +- (void)appendLoggedStreamData:(NSData *)newData {} +- (void)logCapturePostStream {} +#else + +// fetchers come and fetchers go, but statics are forever +static BOOL gIsLoggingEnabled = NO; +static NSString *gLoggingDirectoryPath = nil; +static NSString *gLoggingDateStamp = nil; +static NSString* gLoggingProcessName = nil; + ++ (void)setLoggingDirectory:(NSString *)path { + [gLoggingDirectoryPath autorelease]; + gLoggingDirectoryPath = [path copy]; +} + ++ (NSString *)loggingDirectory { + + if (!gLoggingDirectoryPath) { + NSArray *arr = nil; +#if GDATA_IPHONE && TARGET_IPHONE_SIMULATOR + // default to a directory called GDataHTTPDebugLogs into a sandbox-safe + // directory that a developer can find easily, the application home + arr = [NSArray arrayWithObject:NSHomeDirectory()]; +#elif GDATA_IPHONE + // Neither ~/Desktop nor ~/Home is writable on an actual iPhone device. + // Put it in ~/Documents. + arr = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, + NSUserDomainMask, YES); +#else + // default to a directory called GDataHTTPDebugLogs in the desktop folder + arr = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, + NSUserDomainMask, YES); +#endif + + if ([arr count] > 0) { + NSString *const kGDataLogFolderName = @"GDataHTTPDebugLogs"; + + NSString *desktopPath = [arr objectAtIndex:0]; + NSString *logsFolderPath = [desktopPath stringByAppendingPathComponent:kGDataLogFolderName]; + + BOOL doesFolderExist; + BOOL isDir = NO; + NSFileManager *fileManager = [NSFileManager defaultManager]; + doesFolderExist = [fileManager fileExistsAtPath:logsFolderPath + isDirectory:&isDir]; + + if (!doesFolderExist) { + // make the directory +#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 + // Compiling for 10.5 or later, just use the new api + doesFolderExist = [fileManager createDirectoryAtPath:logsFolderPath + withIntermediateDirectories:YES + attributes:nil + error:NULL]; +#else + // Check at runtime if we have the newer api and use that, otherwise, just + // use the older api (we avoid it to avoid console messages). + if ([fileManager respondsToSelector:@selector(createDirectoryAtPath:withIntermediateDirectories:attributes:error:)]) { + doesFolderExist = [fileManager createDirectoryAtPath:logsFolderPath + withIntermediateDirectories:YES + attributes:nil + error:NULL]; + } else { + doesFolderExist = [fileManager createDirectoryAtPath:logsFolderPath + attributes:nil]; + } +#endif + } + + if (doesFolderExist) { + // it's there; store it in the global + gLoggingDirectoryPath = [logsFolderPath copy]; + } + } + } + return gLoggingDirectoryPath; +} + ++ (void)setIsLoggingEnabled:(BOOL)flag { + gIsLoggingEnabled = flag; +} + ++ (BOOL)isLoggingEnabled { + return gIsLoggingEnabled; +} + ++ (void)setLoggingProcessName:(NSString *)str { + [gLoggingProcessName release]; + gLoggingProcessName = [str copy]; +} + ++ (NSString *)loggingProcessName { + + // get the process name (once per run) replacing spaces with underscores + if (!gLoggingProcessName) { + + NSString *procName = [[NSProcessInfo processInfo] processName]; + NSMutableString *loggingProcessName; + loggingProcessName = [[NSMutableString alloc] initWithString:procName]; + + [loggingProcessName replaceOccurrencesOfString:@" " + withString:@"_" + options:0 + range:NSMakeRange(0, [gLoggingProcessName length])]; + gLoggingProcessName = loggingProcessName; + } + return gLoggingProcessName; +} + ++ (void)setLoggingDateStamp:(NSString *)str { + [gLoggingDateStamp release]; + gLoggingDateStamp = [str copy]; +} + ++ (NSString *)loggingDateStamp { + // we'll pick one date stamp per run, so a run that starts at a later second + // will get a unique results html file + if (!gLoggingDateStamp) { + // produce a string like 08-21_01-41-23PM + + NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease]; + [formatter setFormatterBehavior:NSDateFormatterBehavior10_4]; + [formatter setDateFormat:@"M-dd_hh-mm-ssa"]; + + gLoggingDateStamp = [[formatter stringFromDate:[NSDate date]] retain] ; + } + return gLoggingDateStamp; +} + + +// formattedStringFromData returns a prettyprinted string for XML input, +// and a plain string for other input data +- (NSString *)formattedStringFromData:(NSData *)inputData { + +#if !GDATA_FOUNDATION_ONLY && !GDATA_SKIP_LOG_XMLFORMAT + // verify that this data starts with the bytes indicating XML + + NSString *const kXMLLintPath = @"/usr/bin/xmllint"; + static BOOL hasCheckedAvailability = NO; + static BOOL isXMLLintAvailable; + + if (!hasCheckedAvailability) { + NSFileManager *fileManager = [NSFileManager defaultManager]; + isXMLLintAvailable = [fileManager fileExistsAtPath:kXMLLintPath]; + hasCheckedAvailability = YES; + } + + if (isXMLLintAvailable + && [inputData length] > 5 + && strncmp([inputData bytes], " 0) { + // success + inputData = formattedData; + } + } +#else + // we can't call external tasks on the iPhone; leave the XML unformatted +#endif + + NSString *dataStr = [[[NSString alloc] initWithData:inputData + encoding:NSUTF8StringEncoding] autorelease]; + return dataStr; +} + + +- (NSString *)cleanParameterFollowing:(NSString *)paramName + fromString:(NSString *)originalStr { + // We don't want the password written to disk + // + // find "&Passwd=" in the string, and replace it and the stuff that + // follows it with "Passwd=_snip_" + + NSRange passwdRange = [originalStr rangeOfString:@"&Passwd="]; + if (passwdRange.location != NSNotFound) { + + // we found Passwd=; find the & that follows the parameter + NSUInteger origLength = [originalStr length]; + NSRange restOfString = NSMakeRange(passwdRange.location+1, + origLength - passwdRange.location - 1); + NSRange rangeOfFollowingAmp = [originalStr rangeOfString:@"&" + options:0 + range:restOfString]; + NSRange replaceRange; + if (rangeOfFollowingAmp.location == NSNotFound) { + // found no other & so replace to end of string + replaceRange = NSMakeRange(passwdRange.location, + rangeOfFollowingAmp.location - passwdRange.location); + } else { + // another parameter after &Passwd=foo + replaceRange = NSMakeRange(passwdRange.location, + rangeOfFollowingAmp.location - passwdRange.location); + } + + NSMutableString *result = [NSMutableString stringWithString:originalStr]; + NSString *replacement = [NSString stringWithFormat:@"%@_snip_", paramName]; + + [result replaceCharactersInRange:replaceRange withString:replacement]; + return result; + } + return originalStr; +} + +// stringFromStreamData creates a string given the supplied data +// +// If NSString can create a UTF-8 string from the data, then that is returned. +// +// Otherwise, this routine tries to find a MIME boundary at the beginning of +// the data block, and uses that to break up the data into parts. Each part +// will be used to try to make a UTF-8 string. For parts that fail, a +// replacement string showing the part header and <> is supplied +// in place of the binary data. + +- (NSString *)stringFromStreamData:(NSData *)data { + + if (data == nil) return nil; + + // optimistically, see if the whole data block is UTF-8 + NSString *streamDataStr = [self formattedStringFromData:data]; + if (streamDataStr) return streamDataStr; + + // Munge a buffer by replacing non-ASCII bytes with underscores, + // and turn that munged buffer an NSString. That gives us a string + // we can use with NSScanner. + NSMutableData *mutableData = [NSMutableData dataWithData:data]; + unsigned char *bytes = [mutableData mutableBytes]; + + for (unsigned int idx = 0; idx < [mutableData length]; idx++) { + if (bytes[idx] > 0x7F || bytes[idx] == 0) { + bytes[idx] = '_'; + } + } + + NSString *mungedStr = [[[NSString alloc] initWithData:mutableData + encoding:NSUTF8StringEncoding] autorelease]; + if (mungedStr != nil) { + + // scan for the boundary string + NSString *boundary = nil; + NSScanner *scanner = [NSScanner scannerWithString:mungedStr]; + + if ([scanner scanUpToString:@"\r\n" intoString:&boundary] + && [boundary hasPrefix:@"--"]) { + + // we found a boundary string; use it to divide the string into parts + NSArray *mungedParts = [mungedStr componentsSeparatedByString:boundary]; + + // look at each of the munged parts in the original string, and try to + // convert those into UTF-8 + NSMutableArray *origParts = [NSMutableArray array]; + NSUInteger offset = 0; + NSEnumerator *mungedPartsEnum = [mungedParts objectEnumerator]; + NSString *mungedPart; + while ((mungedPart = [mungedPartsEnum nextObject]) != nil) { + NSUInteger partSize = [mungedPart length]; + + NSRange range = NSMakeRange(offset, partSize); + NSData *origPartData = [data subdataWithRange:range]; + + NSString *origPartStr = [[[NSString alloc] initWithData:origPartData + encoding:NSUTF8StringEncoding] autorelease]; + if (origPartStr) { + // we could make this original part into UTF-8; use the string + [origParts addObject:origPartStr]; + } else { + // this part can't be made into UTF-8; scan the header, if we can + NSString *header = nil; + NSScanner *headerScanner = [NSScanner scannerWithString:mungedPart]; + if (![headerScanner scanUpToString:@"\r\n\r\n" intoString:&header]) { + // we couldn't find a header + header = @"";; + } + + // make a part string with the header and <> + NSString *binStr = [NSString stringWithFormat:@"\r%@\r<<%lu bytes>>\r", + header, (long)(partSize - [header length])]; + [origParts addObject:binStr]; + } + offset += partSize + [boundary length]; + } + + // rejoin the original parts + streamDataStr = [origParts componentsJoinedByString:boundary]; + } + } + + if (!streamDataStr) { + // give up; just make a string showing the uploaded bytes + streamDataStr = [NSString stringWithFormat:@"<<%u bytes>>", [data length]]; + } + return streamDataStr; +} + +// logFetchWithError is called following a successful or failed fetch attempt +// +// This method does all the work for appending to and creating log files + +- (void)logFetchWithError:(NSError *)error { + + if (![[self class] isLoggingEnabled]) return; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + + // TODO: (grobbins) add Javascript to display response data formatted in hex + + NSString *logDirectory = [[self class] loggingDirectory]; + NSString *processName = [[self class] loggingProcessName]; + NSString *dateStamp = [[self class] loggingDateStamp]; + + // each response's NSData goes into its own xml or txt file, though all + // responses for this run of the app share a main html file. This + // counter tracks all fetch responses for this run of the app. + static int zResponseCounter = 0; + zResponseCounter++; + + // file name for the html file containing plain text in a to fit inside our iframe + responseDataUnformattedFileName = [responseBaseName stringByAppendingPathExtension:@"html"]; + NSString *textFilePath = [logDirectory stringByAppendingPathComponent:responseDataUnformattedFileName]; + + NSString* wrapFmt = @""; + NSString* wrappedStr = [NSString stringWithFormat:wrapFmt, dataStr]; + { + NSError *wrappedStrError = nil; + [wrappedStr writeToFile:textFilePath + atomically:NO + encoding:NSUTF8StringEncoding + error:&wrappedStrError]; + if (wrappedStrError) { + NSLog(@"%@ logging write error:%@", [self class], wrappedStrError); + } + } + + // now determine the extension for the "formatted" file, which is really + // the raw data written with an appropriate extension + + // for known file types, we'll write the data to a file with the + // appropriate extension + if ([dataStr hasPrefix:@"%@ HTTP fetch log %@", + processName, dateStamp]; + } + + // write style sheets for each hideable element; each style sheet is + // customized with our current response number, since they'll share + // the html page with other responses + NSString *styleFormat = @"\n"; + + [outputHTML appendFormat:styleFormat, requestHeadersName]; + [outputHTML appendFormat:styleFormat, postDataName]; + [outputHTML appendFormat:styleFormat, responseHeadersName]; + [outputHTML appendFormat:styleFormat, responseDataDivName]; + + if (!didFileExist) { + // write javascript functions. The first one shows/hides the layer + // containing the iframe. + NSString *scriptFormat = @"\n"; + [outputHTML appendString:scriptFormat]; + + // the second function is passed the src file; if it's what's shown, it + // toggles the iframe's visibility. If some other src is shown, it shows + // the iframe and loads the new source. Note we want to load the source + // whenever we show the iframe too since Firefox seems to format it wrong + // when showing it if we don't reload it. + NSString *toggleIFScriptFormat = @"\n\n\n"; + [outputHTML appendString:toggleIFScriptFormat]; + } + + // now write the visible html elements + + // write the date & time + [outputHTML appendFormat:@"%@
", [[NSDate date] description]]; + + // write the request URL + [outputHTML appendFormat:@"request: %@ URL: %@
\n", + [request HTTPMethod], [request URL]]; + + // write the request headers, toggleable + NSDictionary *requestHeaders = [request allHTTPHeaderFields]; + if ([requestHeaders count]) { + NSString *requestHeadersFormat = @"" + "request headers (%d)
%@

\n"; + [outputHTML appendFormat:requestHeadersFormat, + requestHeadersName, // layer name + [requestHeaders count], + requestHeadersName, + [requestHeaders description]]; // description gives a human-readable dump + } else { + [outputHTML appendString:@"Request headers: none
"]; + } + + // write the request post data, toggleable + NSData *postData = postData_; + if (loggedStreamData_) { + postData = loggedStreamData_; + } + + if ([postData length]) { + NSString *postDataFormat = @"" + "posted data (%d bytes)
%@

\n"; + NSString *postDataStr = [self stringFromStreamData:postData]; + if (postDataStr) { + NSString *postDataTextAreaFmt = @"
%@
"; + if ([postDataStr rangeOfString:@"<"].location != NSNotFound) { + postDataTextAreaFmt = @""; + } + NSString *cleanedPostData = [self cleanParameterFollowing:@"&Passwd=" + fromString:postDataStr]; + NSString *postDataTextArea = [NSString stringWithFormat: + postDataTextAreaFmt, cleanedPostData]; + + [outputHTML appendFormat:postDataFormat, + postDataName, // layer name + [postData length], + postDataName, + postDataTextArea]; + } + } else { + // no post data + } + + // write the response status, MIME type, URL + if (response) { + NSString *statusString = @""; + if ([response respondsToSelector:@selector(statusCode)]) { + NSInteger status = [(NSHTTPURLResponse *)response statusCode]; + statusString = @"200"; + if (status != 200) { + // purple for errors + statusString = [NSString stringWithFormat:@"%ld", + (long)status]; + } + } + + // show the response URL only if it's different from the request URL + NSString *responseURLStr = @""; + NSURL *responseURL = [response URL]; + + if (responseURL && ![responseURL isEqual:[request URL]]) { + NSString *responseURLFormat = @"
response URL:" + " %@"; + responseURLStr = [NSString stringWithFormat:responseURLFormat, + [responseURL absoluteString]]; + } + + NSDictionary *responseHeaders = nil; + if ([response respondsToSelector:@selector(allHeaderFields)]) { + responseHeaders = [(NSHTTPURLResponse *)response allHeaderFields]; + } + [outputHTML appendFormat:@"response: status: %@ " + "   MIMEType: %@%@
\n", + statusString, + [response MIMEType], + responseURLStr, + responseHeaders ? [responseHeaders description] : @""]; + + // write the response headers, toggleable + if ([responseHeaders count]) { + + NSString *cookiesSet = [responseHeaders objectForKey:@"Set-Cookie"]; + + NSString *responseHeadersFormat = @"response headers (%d) %@
%@
" + "

\n"; + [outputHTML appendFormat:responseHeadersFormat, + responseHeadersName, + [responseHeaders count], + (cookiesSet ? @"sets cookies" : @""), + responseHeadersName, + [responseHeaders description]]; + + } else { + [outputHTML appendString:@"Response headers: none
\n"]; + } + } + + // error + if (error) { + [outputHTML appendFormat:@"error: %@
\n", [error description]]; + } + + // write the response data. We have links to show formatted and text + // versions, but they both show it in the same iframe, and both + // links also toggle visible/hidden + if (responseDataFormattedFileName || responseDataUnformattedFileName) { + + // response data, toggleable links -- formatted and text versions + if (responseDataFormattedFileName) { + [outputHTML appendFormat:@"response data (%d bytes) formatted %@ ", + responseDataLength, + [responseDataFormattedFileName pathExtension]]; + + // inline (iframe) link + NSString *responseInlineFormattedDataNameFormat = @"  inline\n"; + [outputHTML appendFormat:responseInlineFormattedDataNameFormat, + responseDataDivName, // div ID + dataIFrameID, // iframe ID (for reloading) + responseDataFormattedFileName]; // src to reload + + // plain link (so the user can command-click it into another tab) + [outputHTML appendFormat:@"  stand-alone
\n", + [responseDataFormattedFileName + stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + } + if (responseDataUnformattedFileName) { + [outputHTML appendFormat:@"response data (%d bytes) plain text ", + responseDataLength]; + + // inline (iframe) link + NSString *responseInlineDataNameFormat = @"  inline \n"; + [outputHTML appendFormat:responseInlineDataNameFormat, + responseDataDivName, // div ID + dataIFrameID, // iframe ID (for reloading) + responseDataUnformattedFileName]; // src to reload + + // plain link (so the user can command-click it into another tab) + [outputHTML appendFormat:@"  stand-alone
\n", + [responseDataUnformattedFileName + stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + } + + // make the iframe + NSString *divHTMLFormat = @"
%@

\n"; + NSString *src = responseDataFormattedFileName ? + responseDataFormattedFileName : responseDataUnformattedFileName; + NSString *escapedSrc = [src stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSString *iframeFmt = @" \n"; + NSString *dataIFrameHTML = [NSString stringWithFormat:iframeFmt, + escapedSrc, dataIFrameID, escapedSrc, src]; + [outputHTML appendFormat:divHTMLFormat, + responseDataDivName, dataIFrameHTML]; + } else { + // could not parse response data; just show the length of it + [outputHTML appendFormat:@"Response data: %d bytes \n", + responseDataLength]; + } + + [outputHTML appendString:@"

"]; + + // append the HTML to the main output file + const char* htmlBytes = [outputHTML UTF8String]; + NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:htmlPath + append:YES]; + [stream open]; + [stream write:(const uint8_t *) htmlBytes maxLength:strlen(htmlBytes)]; + [stream close]; + + // make a symlink to the latest html + NSString *symlinkName = [NSString stringWithFormat:@"%@_http_log_newest.html", + processName]; + NSString *symlinkPath = [logDirectory stringByAppendingPathComponent:symlinkName]; + +#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 + // Compiling for 10.5 or later, just use the new apis + [fileManager removeItemAtPath:symlinkPath error:NULL]; + [fileManager createSymbolicLinkAtPath:symlinkPath + withDestinationPath:htmlPath + error:NULL]; +#else + // Check at runtime if we have the newer api and use that, otherwise, just + // use the older api (we avoid it to avoid console messages). + if ([fileManager respondsToSelector:@selector(removeItemAtPath:error:)]) { + [fileManager removeItemAtPath:symlinkPath error:NULL]; + } else { + [fileManager removeFileAtPath:symlinkPath handler:nil]; + } + if ([fileManager respondsToSelector:@selector(createSymbolicLinkAtPath:withDestinationPath:error:)]) { + [fileManager createSymbolicLinkAtPath:symlinkPath + withDestinationPath:htmlPath + error:NULL]; + } else { + [fileManager createSymbolicLinkAtPath:symlinkPath pathContent:htmlPath]; + } +#endif +} + +- (void)logCapturePostStream { + + // This is called when beginning a fetch. The caller should have already + // verified that logging is enabled, and should have allocated + // loggedStreamData_ as a mutable object. + + // If we're logging, we need to wrap the upload stream with our monitor + // stream subclass that will call us back with the bytes being read from the + // stream + + // our wrapper will retain the old post stream + [postStream_ autorelease]; + + // length can be + postStream_ = [GDataInputStreamLogger inputStreamWithStream:postStream_ + length:0]; + [postStream_ retain]; + [(GDataInputStreamLogger *)postStream_ setMonitorDelegate:self]; + + // we don't really want monitoring callbacks; our subclass will be + // calling our appendLoggedStreamData: method at every read instead + [(GDataInputStreamLogger *)postStream_ setMonitorSelector:nil]; +} + +- (void)appendLoggedStreamData:(NSData *)newData { + [loggedStreamData_ appendData:newData]; +} + +#endif // !STRIP_GDATA_FETCH_LOGGING +@end + +#if !STRIP_GDATA_FETCH_LOGGING +@implementation GDataInputStreamLogger +- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len { + + // capture the read stream data, and pass it to the delegate to append to + NSInteger result = [super read:buffer maxLength:len]; + if (result >= 0) { + NSData *data = [NSData dataWithBytes:buffer length:result]; + [monitorDelegate_ appendLoggedStreamData:data]; + } + return result; +} +@end +#endif // !STRIP_GDATA_FETCH_LOGGING diff --git a/Classes/GDataMIMEDocument.h b/Classes/GDataMIMEDocument.h new file mode 100644 index 0000000..3be0736 --- /dev/null +++ b/Classes/GDataMIMEDocument.h @@ -0,0 +1,51 @@ +/* Copyright (c) 2007 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +// This is a simple class to create a MIME document. To use, allocate +// a new GDataMIMEDocument and start adding parts as necessary. When you are +// done adding parts, call generateInputStream to get an NSInputStream +// containing the contents of your MIME document. +// +// A good reference for MIME is http://en.wikipedia.org/wiki/MIME + +#import + +#import "GDataDefines.h" + +@interface GDataMIMEDocument : NSObject { + NSMutableArray* parts_; // Contains an ordered set of MimeParts + unsigned long long length_; // Length in bytes of the document. +} + ++ (GDataMIMEDocument *)MIMEDocument; + +// Adds a new part to this mime document with the given headers and body. The +// headers keys and values should be NSStrings +- (void)addPartWithHeaders:(NSDictionary *)headers + body:(NSData *)body; + +// An inputstream that can be used to efficiently read the contents of the +// mime document. +- (void)generateInputStream:(NSInputStream **)outStream + length:(unsigned long long*)outLength + boundary:(NSString **)outBoundary; + +// ------ UNIT TESTING ONLY BELOW ------ + +// For unittesting only, seeds the random number generator +- (void)seedRandomWith:(unsigned int)seed; + +@end diff --git a/Classes/GDataMIMEDocument.m b/Classes/GDataMIMEDocument.m new file mode 100644 index 0000000..8b941e5 --- /dev/null +++ b/Classes/GDataMIMEDocument.m @@ -0,0 +1,279 @@ +/* Copyright (c) 2007 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +#import "GDataMIMEDocument.h" +#import "GDataGatherInputStream.h" + +// memsrch +// +// Helper routine to search for the existence of a set of bytes (needle) within +// a presumed larger set of bytes (haystack). +// +static BOOL memsrch(const unsigned char* needle, NSUInteger needle_len, + const unsigned char* haystack, NSUInteger haystack_len); + +@interface GDataMIMEPart : NSObject { + NSData* headerData_; // Header content including the ending "\r\n". + NSData* bodyData_; // The body data. +} + ++ (GDataMIMEPart *)partWithHeaders:(NSDictionary *)headers body:(NSData *)body; +- (id)initWithHeaders:(NSDictionary *)headers body:(NSData *)body; +- (BOOL)containsBytes:(const unsigned char *)bytes length:(NSUInteger)length; +- (NSData *)header; +- (NSData *)body; +- (NSUInteger)length; +@end + +@implementation GDataMIMEPart + ++ (GDataMIMEPart *)partWithHeaders:(NSDictionary *)headers body:(NSData *)body { + + return [[[self alloc] initWithHeaders:headers + body:body] autorelease]; +} + +- (id)initWithHeaders:(NSDictionary *)headers + body:(NSData *)body { + + if ((self = [super init]) != nil) { + + bodyData_ = [body retain]; + + // generate the header data by coalescing the dictionary as + // lines of "key: value\r\m" + NSMutableString* headerString = [NSMutableString string]; + + // sort the header keys so we have a deterministic order for + // unit testing + SEL sortSel = @selector(caseInsensitiveCompare:); + NSArray *sortedKeys = [[headers allKeys] sortedArrayUsingSelector:sortSel]; + + NSEnumerator* keyEnum = [sortedKeys objectEnumerator]; + NSString* key; + while ((key = [keyEnum nextObject]) != nil) { + NSString* value = [headers objectForKey:key]; + +#if DEBUG + // look for troublesome characters in the header keys & values + static NSCharacterSet *badChars = nil; + if (!badChars) { + badChars = [[NSCharacterSet characterSetWithCharactersInString:@":\r\n"] retain]; + } + + NSRange badRange = [key rangeOfCharacterFromSet:badChars]; + NSAssert1(badRange.location == NSNotFound, @"invalid key: %@", key); + + badRange = [value rangeOfCharacterFromSet:badChars]; + NSAssert1(badRange.location == NSNotFound, @"invalid value: %@", value); +#endif + + [headerString appendFormat:@"%@: %@\r\n", key, value]; + } + + // headers end with an extra blank line + [headerString appendString:@"\r\n"]; + + headerData_ = [[headerString dataUsingEncoding:NSUTF8StringEncoding] retain]; + } + return self; +} + +- (void) dealloc { + [headerData_ release]; + [bodyData_ release]; + [super dealloc]; +} + +// Returns true if the parts contents contain the given set of bytes. +// +// NOTE: We assume that the 'bytes' we are checking for do not contain "\r\n", +// so we don't need to check the concatenation of the header and body bytes. +- (BOOL)containsBytes:(const unsigned char*)bytes length:(NSUInteger)length { + + // This uses custom memsrch() rather than strcpy because the encoded data may + // contain null values. + return memsrch(bytes, length, [headerData_ bytes], [headerData_ length]) || + memsrch(bytes, length, [bodyData_ bytes], [bodyData_ length]); +} + +- (NSData *)header { + return headerData_; +} + +- (NSData *)body { + return bodyData_; +} + +- (NSUInteger)length { + return [headerData_ length] + [bodyData_ length]; +} +@end + +@implementation GDataMIMEDocument + ++ (GDataMIMEDocument *)MIMEDocument { + return [[[self alloc] init] autorelease]; +} + +- (id)init { + if ((self = [super init]) != nil) { + + parts_ = [[NSMutableArray alloc] init]; + + // Seed the random number generator used to generate mime boundaries + srandomdev(); + } + return self; +} + +- (void)dealloc { + [parts_ release]; + [super dealloc]; +} + +// Adds a new part to this mime document with the given headers and body. +- (void)addPartWithHeaders:(NSDictionary *)headers + body:(NSData *)body { + + GDataMIMEPart* part = [GDataMIMEPart partWithHeaders:headers body:body]; + [parts_ addObject:part]; +} + +// For unit testing only, seeds the random number generator so that we will +// have reproducible boundary strings. +- (void)seedRandomWith:(unsigned int)seed { + + srandom(seed); +} + + +// Computes the mime boundary to use. This should only be called +// after all the desired document parts have been added since it must compute +// a boundary that does not exist in the document data. +- (NSString *)uniqueBoundary { + + // use an easily-readable boundary string + NSString *const kBaseBoundary = @"END_OF_PART"; + + NSMutableString *boundary = [NSMutableString stringWithString:kBaseBoundary]; + + // if the boundary isn't unique, append random numbers, up to 10 attempts; + // if that's still not unique, use a random number sequence instead, + // and call it good + BOOL didCollide = FALSE; + + const int maxTries = 10; // Arbitrarily chosen maximum attempts. + for (int tries = 0; tries < maxTries; ++tries) { + + NSData* data = [boundary dataUsingEncoding:NSUTF8StringEncoding]; + + GDataMIMEPart* part; + NSEnumerator* enumerator = [parts_ objectEnumerator]; + + while ((part = [enumerator nextObject]) != nil) { + didCollide = [part containsBytes:[data bytes] length:[data length]]; + if (didCollide) break; + } + + if (!didCollide) break; // we're fine, no more attempts needed + + // try again with a random number appended + boundary = [NSString stringWithFormat:@"%@_%08x", kBaseBoundary, random()]; + } + + if (didCollide) { + // fallback... two random numbers + boundary = [NSString stringWithFormat:@"%08x_tedborg_%08x", + random(), random()]; + } + + return boundary; +} + +- (void)generateInputStream:(NSInputStream **)outStream + length:(unsigned long long*)outLength + boundary:(NSString **)outBoundary { + + // The input stream is of the form: + // --boundary + // [part_1_headers] + // [part_1_data] + // --boundary + // [part_2_headers] + // [part_2_data] + // --boundary-- + + // First we set up our boundary NSData objects. + NSString *boundary = [self uniqueBoundary]; + + NSString *mainBoundary = [NSString stringWithFormat:@"\r\n--%@\r\n", boundary]; + NSString *endBoundary = [NSString stringWithFormat:@"\r\n--%@--\r\n", boundary]; + + NSData *mainBoundaryData = [mainBoundary dataUsingEncoding:NSUTF8StringEncoding]; + NSData *endBoundaryData = [endBoundary dataUsingEncoding:NSUTF8StringEncoding]; + + // Now we add them all in proper order to our dataArray. + NSMutableArray* dataArray = [NSMutableArray array]; + unsigned long long length = 0; + + NSEnumerator* partEnumerator = [parts_ objectEnumerator]; + GDataMIMEPart* part; + while ((part = [partEnumerator nextObject]) != nil) { + + [dataArray addObject:mainBoundaryData]; + [dataArray addObject:[part header]]; + [dataArray addObject:[part body]]; + + length += [part length] + [mainBoundaryData length]; + } + + [dataArray addObject:endBoundaryData]; + length += [endBoundaryData length]; + + if (outLength) *outLength = length; + if (outStream) *outStream = [GDataGatherInputStream streamWithArray:dataArray]; + if (outBoundary) *outBoundary = boundary; +} + +@end + + +// memsrch - Return TRUE if needle is found in haystack, else FALSE. +static BOOL memsrch(const unsigned char* needle, NSUInteger needleLen, + const unsigned char* haystack, NSUInteger haystackLen) { + + // This is a simple approach. We start off by assuming that both memchr() and + // memcmp are implemented efficiently on the given platform. We search for an + // instance of the first char of our needle in the haystack. If the remaining + // size could fit our needle, then we memcmp to see if it occurs at this point + // in the haystack. If not, we move on to search for the first char again, + // starting from the next character in the haystack. + const unsigned char* ptr = haystack; + NSUInteger remain = haystackLen; + while ((ptr = memchr(ptr, needle[0], remain)) != 0) { + remain = haystackLen - (ptr - haystack); + if (remain < needleLen) { + return FALSE; + } + if (memcmp(ptr, needle, needleLen) == 0) { + return TRUE; + } + ptr++; + remain--; + } + return FALSE; +} diff --git a/Classes/GDataProgressMonitorInputStream.h b/Classes/GDataProgressMonitorInputStream.h new file mode 100644 index 0000000..39c2526 --- /dev/null +++ b/Classes/GDataProgressMonitorInputStream.h @@ -0,0 +1,71 @@ +/* Copyright (c) 2007 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +#import + +// The monitored input stream calls back into the monitor delegate +// with the number of bytes and total size +// +// - (void)inputStream:(GDataProgressMonitorInputStream *)stream +// hasDeliveredByteCount:(unsigned long long)numberOfBytesRead +// ofTotalByteCount:(unsigned long long)dataLength; + +#undef GDATA_NSSTREAM_DELEGATE +#if TARGET_OS_MAC && !TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED > 1050) +#define GDATA_NSSTREAM_DELEGATE +#else +#define GDATA_NSSTREAM_DELEGATE +#endif + +@interface GDataProgressMonitorInputStream : NSInputStream GDATA_NSSTREAM_DELEGATE { + + NSInputStream *inputStream_; // encapsulated stream that does the work + + unsigned long long dataSize_; // size of data in the source + unsigned long long numBytesRead_; // bytes read from the input stream so far + + id monitorDelegate_; // WEAK, not retained + SEL monitorSelector_; + + id monitorSource_; // WEAK, not retained +} + +// length is passed to the progress callback; it may be zero +// if the progress callback can handle that ++ (id)inputStreamWithStream:(NSInputStream *)input + length:(unsigned long long)length; + +- (id)initWithStream:(NSInputStream *)input + length:(unsigned long long)length; + +// the monitor is called when bytes have been read +// +// monitorDelegate should respond to a selector with a signature matching: +// +// - (void)inputStream:(GDataProgressMonitorInputStream *)stream +// hasDeliveredBytes:(unsigned long long)numReadSoFar +// ofTotalBytes:(unsigned long long)total + +- (void)setMonitorDelegate:(id)monitorDelegate; // not retained +- (id)monitorDelegate; + +- (void)setMonitorSelector:(SEL)monitorSelector; +- (SEL)monitorSelector; + +// the source lets the delegate know the source of this input stream +- (void)setMonitorSource:(id)source; // not retained +- (id)monitorSource; + +@end + diff --git a/Classes/GDataProgressMonitorInputStream.m b/Classes/GDataProgressMonitorInputStream.m new file mode 100644 index 0000000..de82192 --- /dev/null +++ b/Classes/GDataProgressMonitorInputStream.m @@ -0,0 +1,181 @@ +/* Copyright (c) 2007 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#import "GDataDefines.h" +#import "GDataProgressMonitorInputStream.h" + + +@implementation GDataProgressMonitorInputStream + +// we'll forward all unhandled messages to the NSInputStream class +// or to the encapsulated input stream. This is needed +// for all messages sent to NSInputStream which aren't +// handled by our superclass; that includes various private run +// loop calls. ++ (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { + return [NSInputStream methodSignatureForSelector:selector]; +} + ++ (void)forwardInvocation:(NSInvocation*)invocation { + [invocation invokeWithTarget:[NSInputStream class]]; +} + +- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { + return [inputStream_ methodSignatureForSelector:selector]; +} + +- (void)forwardInvocation:(NSInvocation*)invocation { + [invocation invokeWithTarget:inputStream_]; +} + +#pragma mark - + ++ (id)inputStreamWithStream:(NSInputStream *)input + length:(unsigned long long)length { + + return [[[self alloc] initWithStream:input + length:length] autorelease]; +} + +- (id)initWithStream:(NSInputStream *)input + length:(unsigned long long)length { + + if ((self = [super init]) != nil) { + + inputStream_ = [input retain]; + dataSize_ = length; + } + return self; +} + +- (id)init { + if ((self = [super init])) { + } + return self; +} + +- (void)dealloc { + [inputStream_ release]; + [super dealloc]; +} + +#pragma mark - + + +- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len { + + NSInteger numRead = [inputStream_ read:buffer maxLength:len]; + + if (numRead > 0) { + + numBytesRead_ += numRead; + + if (monitorDelegate_ && monitorSelector_) { + + // call the monitor delegate with the number of bytes read and the + // total bytes read + + NSMethodSignature *signature = [monitorDelegate_ methodSignatureForSelector:monitorSelector_]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:monitorSelector_]; + [invocation setTarget:monitorDelegate_]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&numBytesRead_ atIndex:3]; + [invocation setArgument:&dataSize_ atIndex:4]; + [invocation invoke]; + } + } + return numRead; +} + +- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len { + return [inputStream_ getBuffer:buffer length:len]; +} + +- (BOOL)hasBytesAvailable { + return [inputStream_ hasBytesAvailable]; +} + +#pragma mark Standard messages + +// Pass expected messages to our encapsulated stream. +// +// We want our encapsulated NSInputStream to handle the standard messages; +// we don't want the superclass to handle them. +- (void)open { + [inputStream_ open]; +} + +- (void)close { + [inputStream_ close]; +} + +- (id)delegate { + return [inputStream_ delegate]; +} + +- (void)setDelegate:(id)delegate { + [inputStream_ setDelegate:delegate]; +} + +- (id)propertyForKey:(NSString *)key { + return [inputStream_ propertyForKey:key]; +} +- (BOOL)setProperty:(id)property forKey:(NSString *)key { + return [inputStream_ setProperty:property forKey:key]; +} + +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode { + [inputStream_ scheduleInRunLoop:aRunLoop forMode:mode]; +} +- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode { + [inputStream_ removeFromRunLoop:aRunLoop forMode:mode]; +} + +- (NSStreamStatus)streamStatus { + return [inputStream_ streamStatus]; +} + +- (NSError *)streamError { + return [inputStream_ streamError]; +} + +#pragma mark Setters and getters + +- (void)setMonitorDelegate:(id)monitorDelegate { + monitorDelegate_ = monitorDelegate; // non-retained +} + +- (id)monitorDelegate { + return monitorDelegate_; +} + +- (void)setMonitorSelector:(SEL)monitorSelector { + monitorSelector_ = monitorSelector; +} + +- (SEL)monitorSelector { + return monitorSelector_; +} + +- (void)setMonitorSource:(id)source { + monitorSource_ = source; // non-retained +} + +- (id)monitorSource { + return monitorSource_; +} + +@end diff --git a/Classes/GDataXMLNode.h b/Classes/GDataXMLNode.h new file mode 100644 index 0000000..acaa741 --- /dev/null +++ b/Classes/GDataXMLNode.h @@ -0,0 +1,184 @@ +/* Copyright (c) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// These node, element, and document classes implement a subset of the methods +// provided by NSXML. While NSXML behavior is mimicked as much as possible, +// there are important differences. +// +// The biggest difference is that, since this is based on libxml2, there +// is no retain model for the underlying node data. Rather than copy every +// node obtained from a parse tree (which would have a substantial memory +// impact), we rely on weak references, and it is up to the code that +// created a document to retain it for as long as any +// references rely on nodes inside that document tree. + + +#import + +// libxml includes require that the target Header Search Paths contain +// +// /usr/include/libxml2 +// +// and Other Linker Flags contain +// +// -lxml2 + +#import +#import +#import +#import +#import + +#import "GDataDefines.h" + +// Nomenclature for method names: +// +// Node = GData node +// XMLNode = xmlNodePtr +// +// So, for example: +// + (id)nodeConsumingXMLNode:(xmlNodePtr)theXMLNode; + +@class NSArray, NSDictionary, NSError, NSString, NSURL; +@class GDataXMLElement, GDataXMLDocument; + +enum { + GDataXMLInvalidKind = 0, + GDataXMLDocumentKind, + GDataXMLElementKind, + GDataXMLAttributeKind, + GDataXMLNamespaceKind, + GDataXMLProcessingInstructionKind, + GDataXMLCommentKind, + GDataXMLTextKind, + GDataXMLDTDKind, + GDataXMLEntityDeclarationKind, + GDataXMLAttributeDeclarationKind, + GDataXMLElementDeclarationKind, + GDataXMLNotationDeclarationKind +}; + +typedef NSUInteger GDataXMLNodeKind; + +@interface GDataXMLNode : NSObject { +@protected + // NSXMLNodes can have a namespace URI or prefix even if not part + // of a tree; xmlNodes cannot. When we create nodes apart from + // a tree, we'll store the dangling prefix or URI in the xmlNode's name, + // like + // "prefix:name" + // or + // "{http://uri}:name" + // + // We will fix up the node's namespace and name (and those of any children) + // later when adding the node to a tree with addChild: or addAttribute:. + // See fixUpNamespacesForNode:. + + xmlNodePtr xmlNode_; // may also be an xmlAttrPtr or xmlNsPtr + BOOL shouldFreeXMLNode_; // if yes, xmlNode_ will be free'd in dealloc + + // cached values + NSString *cachedName_; + NSArray *cachedChildren_; + NSArray *cachedAttributes_; +} + ++ (GDataXMLElement *)elementWithName:(NSString *)name; ++ (GDataXMLElement *)elementWithName:(NSString *)name stringValue:(NSString *)value; ++ (GDataXMLElement *)elementWithName:(NSString *)name URI:(NSString *)value; + ++ (id)attributeWithName:(NSString *)name stringValue:(NSString *)value; ++ (id)attributeWithName:(NSString *)name URI:(NSString *)attributeURI stringValue:(NSString *)value; + ++ (id)namespaceWithName:(NSString *)name stringValue:(NSString *)value; + ++ (id)textWithStringValue:(NSString *)value; + +- (NSString *)stringValue; +- (void)setStringValue:(NSString *)str; + +- (NSUInteger)childCount; +- (NSArray *)children; +- (GDataXMLNode *)childAtIndex:(unsigned)index; + +- (NSString *)localName; +- (NSString *)name; +- (NSString *)prefix; +- (NSString *)URI; + +- (GDataXMLNodeKind)kind; + +- (NSString *)XMLString; + ++ (NSString *)localNameForName:(NSString *)name; ++ (NSString *)prefixForName:(NSString *)name; + +// This implementation of nodesForXPath registers namespaces only from the +// current node +- (NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error; + +// access to the underlying libxml node; be sure to release the cached values +// if you change the underlying tree at all +- (xmlNodePtr)XMLNode; +- (void)releaseCachedValues; + +@end + + +@interface GDataXMLElement : GDataXMLNode + +- (id)initWithXMLString:(NSString *)str error:(NSError **)error; + +- (NSArray *)namespaces; +- (void)setNamespaces:(NSArray *)namespaces; +- (void)addNamespace:(GDataXMLNode *)aNamespace; + +- (void)addChild:(GDataXMLNode *)child; +- (void)removeChild:(GDataXMLNode *)child; + +- (NSArray *)elementsForName:(NSString *)name; +- (NSArray *)elementsForLocalName:(NSString *)localName URI:(NSString *)URI; + +- (NSArray *)attributes; +- (GDataXMLNode *)attributeForName:(NSString *)name; +- (GDataXMLNode *)attributeForLocalName:(NSString *)name URI:(NSString *)attributeURI; +- (void)addAttribute:(GDataXMLNode *)attribute; + +- (NSString *)resolvePrefixForNamespaceURI:(NSString *)namespaceURI; + +@end + +@interface GDataXMLDocument : NSObject { +@protected + xmlDoc* xmlDoc_; // strong; always free'd in dealloc +} + +- (id)initWithXMLString:(NSString *)str options:(unsigned int)mask error:(NSError **)error; +- (id)initWithData:(NSData *)data options:(unsigned int)mask error:(NSError **)error; +- (id)initWithRootElement:(GDataXMLElement *)element; + +- (GDataXMLElement *)rootElement; + +- (NSData *)XMLData; + +- (void)setVersion:(NSString *)version; +- (void)setCharacterEncoding:(NSString *)encoding; + +// This implementation of nodesForXPath registers namespaces only from the +// document's root node +- (NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error; + +- (NSString *)description; +@end diff --git a/Classes/GDataXMLNode.m b/Classes/GDataXMLNode.m new file mode 100644 index 0000000..2175a3e --- /dev/null +++ b/Classes/GDataXMLNode.m @@ -0,0 +1,1783 @@ +/* Copyright (c) 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#import "GDataXMLNode.h" + +@class NSArray, NSDictionary, NSError, NSString, NSURL; +@class GDataXMLElement, GDataXMLDocument; + + +static const int kGDataXMLParseOptions = (XML_PARSE_NOCDATA | XML_PARSE_NOBLANKS); + +// dictionary key callbacks for string cache +static const void *StringCacheKeyRetainCallBack(CFAllocatorRef allocator, const void *str); +static void StringCacheKeyReleaseCallBack(CFAllocatorRef allocator, const void *str); +static CFStringRef StringCacheKeyCopyDescriptionCallBack(const void *str); +static Boolean StringCacheKeyEqualCallBack(const void *str1, const void *str2); +static CFHashCode StringCacheKeyHashCallBack(const void *str); + +// isEqual: has the fatal flaw that it doesn't deal well with the received +// being nil. We'll use this utility instead. + +// Static copy of AreEqualOrBothNil from GDataObject.m, so that using +// GDataXMLNode does not require pulling in all of GData. +static BOOL AreEqualOrBothNilPrivate(id obj1, id obj2) { + if (obj1 == obj2) { + return YES; + } + if (obj1 && obj2) { + return [obj1 isEqual:obj2]; + } + return NO; +} + + +// convert NSString* to xmlChar* +// +// the "Get" part implies that ownership remains with str + +static xmlChar* GDataGetXMLString(NSString *str) { + xmlChar* result = (xmlChar *)[str UTF8String]; + return result; +} + +// Make a fake qualified name we use as local name internally in libxml +// data structures when there's no actual namespace node available to point to +// from an element or attribute node +// +// Returns an autoreleased NSString* + +static NSString *GDataFakeQNameForURIAndName(NSString *theURI, NSString *name) { + + NSString *localName = [GDataXMLNode localNameForName:name]; + NSString *fakeQName = [NSString stringWithFormat:@"{%@}:%@", + theURI, localName]; + return fakeQName; +} + + +// libxml2 offers xmlSplitQName2, but that searches forwards. Since we may +// be searching for a whole URI shoved in as a prefix, like +// {http://foo}:name +// we'll search for the prefix in backwards from the end of the qualified name +// +// returns a copy of qname as the local name if there's no prefix +static xmlChar *SplitQNameReverse(const xmlChar *qname, xmlChar **prefix) { + + // search backwards for a colon + int qnameLen = xmlStrlen(qname); + for (int idx = qnameLen - 1; idx >= 0; idx--) { + + if (qname[idx] == ':') { + + // found the prefix; copy the prefix, if requested + if (prefix != NULL) { + if (idx > 0) { + *prefix = xmlStrsub(qname, 0, idx); + } else { + *prefix = NULL; + } + } + + if (idx < qnameLen - 1) { + // return a copy of the local name + xmlChar *localName = xmlStrsub(qname, idx + 1, qnameLen - idx - 1); + return localName; + } else { + return NULL; + } + } + } + + // no colon found, so the qualified name is the local name + xmlChar *qnameCopy = xmlStrdup(qname); + return qnameCopy; +} + +@interface GDataXMLNode (PrivateMethods) + +// consuming a node implies it will later be freed when the instance is +// dealloc'd; borrowing it implies that ownership and disposal remain the +// job of the supplier of the node + ++ (id)nodeConsumingXMLNode:(xmlNodePtr)theXMLNode; +- (id)initConsumingXMLNode:(xmlNodePtr)theXMLNode; + ++ (id)nodeBorrowingXMLNode:(xmlNodePtr)theXMLNode; +- (id)initBorrowingXMLNode:(xmlNodePtr)theXMLNode; + +// getters of the underlying node +- (xmlNodePtr)XMLNode; +- (xmlNodePtr)XMLNodeCopy; + +// search for an underlying attribute +- (GDataXMLNode *)attributeForXMLNode:(xmlAttrPtr)theXMLNode; + +// return an NSString for an xmlChar*, using our strings cache in the +// document +- (NSString *)stringFromXMLString:(const xmlChar *)chars; + +// setter/getter of the dealloc flag for the underlying node +- (BOOL)shouldFreeXMLNode; +- (void)setShouldFreeXMLNode:(BOOL)flag; + +@end + +@interface GDataXMLElement (PrivateMethods) + ++ (void)fixUpNamespacesForNode:(xmlNodePtr)nodeToFix + graftingToTreeNode:(xmlNodePtr)graftPointNode; +@end + +@implementation GDataXMLNode + ++ (void)load { + xmlInitParser(); +} + +// Note on convenience methods for making stand-alone element and +// attribute nodes: +// +// Since we're making a node from scratch, we don't +// have any namespace info. So the namespace prefix, if +// any, will just be slammed into the node name. +// We'll rely on the -addChild method below to remove +// the namespace prefix and replace it with a proper ns +// pointer. + ++ (GDataXMLElement *)elementWithName:(NSString *)name { + + xmlNodePtr theNewNode = xmlNewNode(NULL, // namespace + GDataGetXMLString(name)); + if (theNewNode) { + // succeeded + return [self nodeConsumingXMLNode:theNewNode]; + } + return nil; +} + ++ (GDataXMLElement *)elementWithName:(NSString *)name stringValue:(NSString *)value { + + xmlNodePtr theNewNode = xmlNewNode(NULL, // namespace + GDataGetXMLString(name)); + if (theNewNode) { + + xmlNodePtr textNode = xmlNewText(GDataGetXMLString(value)); + if (textNode) { + + xmlNodePtr temp = xmlAddChild(theNewNode, textNode); + if (temp) { + // succeeded + return [self nodeConsumingXMLNode:theNewNode]; + } + } + + // failed; free the node and any children + xmlFreeNode(theNewNode); + } + return nil; +} + ++ (GDataXMLElement *)elementWithName:(NSString *)name URI:(NSString *)theURI { + + // since we don't know a prefix yet, shove in the whole URI; we'll look for + // a proper namespace ptr later when addChild calls fixUpNamespacesForNode + + NSString *fakeQName = GDataFakeQNameForURIAndName(theURI, name); + + xmlNodePtr theNewNode = xmlNewNode(NULL, // namespace + GDataGetXMLString(fakeQName)); + if (theNewNode) { + return [self nodeConsumingXMLNode:theNewNode]; + } + return nil; +} + ++ (id)attributeWithName:(NSString *)name stringValue:(NSString *)value { + + xmlChar *xmlName = GDataGetXMLString(name); + xmlChar *xmlValue = GDataGetXMLString(value); + + xmlAttrPtr theNewAttr = xmlNewProp(NULL, // parent node for the attr + xmlName, xmlValue); + if (theNewAttr) { + return [self nodeConsumingXMLNode:(xmlNodePtr) theNewAttr]; + } + + return nil; +} + ++ (id)attributeWithName:(NSString *)name URI:(NSString *)attributeURI stringValue:(NSString *)value { + + // since we don't know a prefix yet, shove in the whole URI; we'll look for + // a proper namespace ptr later when addChild calls fixUpNamespacesForNode + + NSString *fakeQName = GDataFakeQNameForURIAndName(attributeURI, name); + + xmlChar *xmlName = GDataGetXMLString(fakeQName); + xmlChar *xmlValue = GDataGetXMLString(value); + + xmlAttrPtr theNewAttr = xmlNewProp(NULL, // parent node for the attr + xmlName, xmlValue); + if (theNewAttr) { + return [self nodeConsumingXMLNode:(xmlNodePtr) theNewAttr]; + } + + return nil; +} + ++ (id)textWithStringValue:(NSString *)value { + + xmlNodePtr theNewText = xmlNewText(GDataGetXMLString(value)); + if (theNewText) { + return [self nodeConsumingXMLNode:theNewText]; + } + return nil; +} + ++ (id)namespaceWithName:(NSString *)name stringValue:(NSString *)value { + + xmlChar *href = GDataGetXMLString(value); + xmlChar *prefix; + + if ([name length] > 0) { + prefix = GDataGetXMLString(name); + } else { + // default namespace is represented by a nil prefix + prefix = nil; + } + + xmlNsPtr theNewNs = xmlNewNs(NULL, // parent node + href, prefix); + if (theNewNs) { + return [self nodeConsumingXMLNode:(xmlNodePtr) theNewNs]; + } + return nil; +} + ++ (id)nodeConsumingXMLNode:(xmlNodePtr)theXMLNode { + Class theClass; + + if (theXMLNode->type == XML_ELEMENT_NODE) { + theClass = [GDataXMLElement class]; + } else { + theClass = [GDataXMLNode class]; + } + return [[[theClass alloc] initConsumingXMLNode:theXMLNode] autorelease]; +} + +- (id)initConsumingXMLNode:(xmlNodePtr)theXMLNode { + self = [super init]; + if (self) { + xmlNode_ = theXMLNode; + shouldFreeXMLNode_ = YES; + } + return self; +} + ++ (id)nodeBorrowingXMLNode:(xmlNodePtr)theXMLNode { + Class theClass; + if (theXMLNode->type == XML_ELEMENT_NODE) { + theClass = [GDataXMLElement class]; + } else { + theClass = [GDataXMLNode class]; + } + + return [[[theClass alloc] initBorrowingXMLNode:theXMLNode] autorelease]; +} + +- (id)initBorrowingXMLNode:(xmlNodePtr)theXMLNode { + self = [super init]; + if (self) { + xmlNode_ = theXMLNode; + shouldFreeXMLNode_ = NO; + } + return self; +} + +- (void)releaseCachedValues { + + [cachedName_ release]; + cachedName_ = nil; + + [cachedChildren_ release]; + cachedChildren_ = nil; + + [cachedAttributes_ release]; + cachedAttributes_ = nil; +} + + +// convert xmlChar* to NSString* +// +// returns an autoreleased NSString*, from the current node's document strings +// cache if possible +- (NSString *)stringFromXMLString:(const xmlChar *)chars { + +#if DEBUG + NSCAssert(chars != NULL, @"GDataXMLNode sees an unexpected empty string"); +#endif + if (chars == NULL) return nil; + + CFMutableDictionaryRef cacheDict = NULL; + + NSString *result = nil; + + if (xmlNode_ != NULL + && (xmlNode_->type == XML_ELEMENT_NODE + || xmlNode_->type == XML_ATTRIBUTE_NODE + || xmlNode_->type == XML_TEXT_NODE)) { + // there is no xmlDocPtr in XML_NAMESPACE_DECL nodes, + // so we can't cache the text of those + + // look for a strings cache in the document + // + // the cache is in the document's user-defined _private field + + if (xmlNode_->doc != NULL) { + + cacheDict = xmlNode_->doc->_private; + + if (cacheDict) { + + // this document has a strings cache + result = (NSString *) CFDictionaryGetValue(cacheDict, chars); + if (result) { + // we found the xmlChar string in the cache; return the previously + // allocated NSString, rather than allocate a new one + return result; + } + } + } + } + + // allocate a new NSString for this xmlChar* + result = [NSString stringWithUTF8String:(const char *) chars]; + if (cacheDict) { + // save the string in the document's string cache + CFDictionarySetValue(cacheDict, chars, result); + } + + return result; +} + +- (void)dealloc { + + if (xmlNode_ && shouldFreeXMLNode_) { + xmlFreeNode(xmlNode_); + } + + [self releaseCachedValues]; + [super dealloc]; +} + +#pragma mark - + +- (void)setStringValue:(NSString *)str { + if (xmlNode_ != NULL && str != nil) { + + if (xmlNode_->type == XML_NAMESPACE_DECL) { + + // for a namespace node, the value is the namespace URI + xmlNsPtr nsNode = (xmlNsPtr)xmlNode_; + + if (nsNode->href != NULL) xmlFree((char *)nsNode->href); + + nsNode->href = xmlStrdup(GDataGetXMLString(str)); + + } else { + + // attribute or element node + + // do we need to call xmlEncodeSpecialChars? + xmlNodeSetContent(xmlNode_, GDataGetXMLString(str)); + } + } +} + +- (NSString *)stringValue { + + NSString *str = nil; + + if (xmlNode_ != NULL) { + + if (xmlNode_->type == XML_NAMESPACE_DECL) { + + // for a namespace node, the value is the namespace URI + xmlNsPtr nsNode = (xmlNsPtr)xmlNode_; + + str = [self stringFromXMLString:(nsNode->href)]; + + } else { + + // attribute or element node + xmlChar* chars = xmlNodeGetContent(xmlNode_); + if (chars) { + + str = [self stringFromXMLString:chars]; + + xmlFree(chars); + } + } + } + return str; +} + +- (NSString *)XMLString { + + NSString *str = nil; + + if (xmlNode_ != NULL) { + + xmlBufferPtr buff = xmlBufferCreate(); + if (buff) { + + xmlDocPtr doc = NULL; + int level = 0; + int format = 0; + + int result = xmlNodeDump(buff, doc, xmlNode_, level, format); + + if (result > -1) { + str = [[[NSString alloc] initWithBytes:(xmlBufferContent(buff)) + length:(xmlBufferLength(buff)) + encoding:NSUTF8StringEncoding] autorelease]; + } + xmlBufferFree(buff); + } + } + + // remove leading and trailing whitespace + NSCharacterSet *ws = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + NSString *trimmed = [str stringByTrimmingCharactersInSet:ws]; + return trimmed; +} + +- (NSString *)localName { + NSString *str = nil; + + if (xmlNode_ != NULL) { + + str = [self stringFromXMLString:(xmlNode_->name)]; + + // if this is part of a detached subtree, str may have a prefix in it + str = [[self class] localNameForName:str]; + } + return str; +} + +- (NSString *)prefix { + + NSString *str = nil; + + if (xmlNode_ != NULL) { + + // the default namespace's prefix is an empty string, though libxml + // represents it as NULL for ns->prefix + str = @""; + + if (xmlNode_->ns != NULL && xmlNode_->ns->prefix != NULL) { + str = [self stringFromXMLString:(xmlNode_->ns->prefix)]; + } + } + return str; +} + +- (NSString *)URI { + + NSString *str = nil; + + if (xmlNode_ != NULL) { + + if (xmlNode_->ns != NULL && xmlNode_->ns->href != NULL) { + str = [self stringFromXMLString:(xmlNode_->ns->href)]; + } + } + return str; +} + +- (NSString *)qualifiedName { + // internal utility + + NSString *str = nil; + + if (xmlNode_ != NULL) { + if (xmlNode_->type == XML_NAMESPACE_DECL) { + + // name of a namespace node + xmlNsPtr nsNode = (xmlNsPtr)xmlNode_; + + // null is the default namespace; one is the loneliest number + if (nsNode->prefix == NULL) { + str = @""; + } + else { + str = [self stringFromXMLString:(nsNode->prefix)]; + } + + } else if (xmlNode_->ns != NULL && xmlNode_->ns->prefix != NULL) { + + // name of a non-namespace node + + // has a prefix + char *qname; + if (asprintf(&qname, "%s:%s", (const char *)xmlNode_->ns->prefix, + xmlNode_->name) != -1) { + str = [self stringFromXMLString:(const xmlChar *)qname]; + free(qname); + } + } else { + // lacks a prefix + str = [self stringFromXMLString:(xmlNode_->name)]; + } + } + + return str; +} + +- (NSString *)name { + + if (cachedName_ != nil) { + return cachedName_; + } + + NSString *str = [self qualifiedName]; + + cachedName_ = [str retain]; + + return str; +} + ++ (NSString *)localNameForName:(NSString *)name { + if (name != nil) { + + NSRange range = [name rangeOfString:@":"]; + if (range.location != NSNotFound) { + + // found a colon + if (range.location + 1 < [name length]) { + NSString *localName = [name substringFromIndex:(range.location + 1)]; + return localName; + } + } + } + return name; +} + ++ (NSString *)prefixForName:(NSString *)name { + if (name != nil) { + + NSRange range = [name rangeOfString:@":"]; + if (range.location != NSNotFound) { + + NSString *prefix = [name substringToIndex:(range.location)]; + return prefix; + } + } + return nil; +} + +- (NSUInteger)childCount { + + if (cachedChildren_ != nil) { + return [cachedChildren_ count]; + } + + if (xmlNode_ != NULL) { + + unsigned int count = 0; + + xmlNodePtr currChild = xmlNode_->children; + + while (currChild != NULL) { + ++count; + currChild = currChild->next; + } + return count; + } + return 0; +} + +- (NSArray *)children { + + if (cachedChildren_ != nil) { + return cachedChildren_; + } + + NSMutableArray *array = nil; + + if (xmlNode_ != NULL) { + + xmlNodePtr currChild = xmlNode_->children; + + while (currChild != NULL) { + GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:currChild]; + + if (array == nil) { + array = [NSMutableArray arrayWithObject:node]; + } else { + [array addObject:node]; + } + + currChild = currChild->next; + } + + cachedChildren_ = [array retain]; + } + return array; +} + +- (GDataXMLNode *)childAtIndex:(unsigned)index { + + NSArray *children = [self children]; + + if ([children count] > index) { + + return [children objectAtIndex:index]; + } + return nil; +} + +- (GDataXMLNodeKind)kind { + if (xmlNode_ != NULL) { + xmlElementType nodeType = xmlNode_->type; + switch (nodeType) { + case XML_ELEMENT_NODE: return GDataXMLElementKind; + case XML_ATTRIBUTE_NODE: return GDataXMLAttributeKind; + case XML_TEXT_NODE: return GDataXMLTextKind; + case XML_CDATA_SECTION_NODE: return GDataXMLTextKind; + case XML_ENTITY_REF_NODE: return GDataXMLEntityDeclarationKind; + case XML_ENTITY_NODE: return GDataXMLEntityDeclarationKind; + case XML_PI_NODE: return GDataXMLProcessingInstructionKind; + case XML_COMMENT_NODE: return GDataXMLCommentKind; + case XML_DOCUMENT_NODE: return GDataXMLDocumentKind; + case XML_DOCUMENT_TYPE_NODE: return GDataXMLDocumentKind; + case XML_DOCUMENT_FRAG_NODE: return GDataXMLDocumentKind; + case XML_NOTATION_NODE: return GDataXMLNotationDeclarationKind; + case XML_HTML_DOCUMENT_NODE: return GDataXMLDocumentKind; + case XML_DTD_NODE: return GDataXMLDTDKind; + case XML_ELEMENT_DECL: return GDataXMLElementDeclarationKind; + case XML_ATTRIBUTE_DECL: return GDataXMLAttributeDeclarationKind; + case XML_ENTITY_DECL: return GDataXMLEntityDeclarationKind; + case XML_NAMESPACE_DECL: return GDataXMLNamespaceKind; + case XML_XINCLUDE_START: return GDataXMLProcessingInstructionKind; + case XML_XINCLUDE_END: return GDataXMLProcessingInstructionKind; + case XML_DOCB_DOCUMENT_NODE: return GDataXMLDocumentKind; + } + } + return GDataXMLInvalidKind; +} + +- (NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error { + + NSMutableArray *array = nil; + + // xmlXPathNewContext requires a doc for its context, but if our elements + // are created from GDataXMLElement's initWithXMLString there may not be + // a document. (We may later decide that we want to stuff the doc used + // there into a GDataXMLDocument and retain it, but we don't do that now.) + // + // We'll temporarily make a document to use for the xpath context. + + xmlDocPtr tempDoc = NULL; + xmlNodePtr topParent = NULL; + + if (xmlNode_->doc == NULL) { + tempDoc = xmlNewDoc(NULL); + if (tempDoc) { + // find the topmost node of the current tree to make the root of + // our temporary document + topParent = xmlNode_; + while (topParent->parent != NULL) { + topParent = topParent->parent; + } + xmlDocSetRootElement(tempDoc, topParent); + } + } + + if (xmlNode_ != NULL && xmlNode_->doc != NULL) { + + xmlXPathContextPtr xpathCtx = xmlXPathNewContext(xmlNode_->doc); + if (xpathCtx) { + + // anchor at our current node + xpathCtx->node = xmlNode_; + + // register the namespaces of this node, if it's an element, or of + // this node's root element, if it's a document + xmlNodePtr nsNodePtr = xmlNode_; + if (xmlNode_->type == XML_DOCUMENT_NODE) { + nsNodePtr = xmlDocGetRootElement((xmlDocPtr) xmlNode_); + } + + // step through the namespaces, if any, and register each with the + // xpath context + if (nsNodePtr != NULL) { + for (xmlNsPtr nsPtr = nsNodePtr->ns; nsPtr != NULL; nsPtr = nsPtr->next) { + + // default namespace is nil in the tree but an empty string when + // registering + const xmlChar* prefix = nsPtr->prefix; + if (prefix == NULL) prefix = (xmlChar*) ""; + + int result = xmlXPathRegisterNs(xpathCtx, prefix, nsPtr->href); + if (result != 0) { +#if DEBUG + NSCAssert(result == 0, @"GDataXMLNode XPath namespace problem"); +#endif + } + } + } + + // now evaluate the path + xmlXPathObjectPtr xpathObj; + xpathObj = xmlXPathEval(GDataGetXMLString(xpath), xpathCtx); + if (xpathObj) { + + // we have some result from the search + array = [NSMutableArray array]; + + xmlNodeSetPtr nodeSet = xpathObj->nodesetval; + if (nodeSet) { + + // add each node in the result set to our array + for (int index = 0; index < nodeSet->nodeNr; index++) { + + xmlNodePtr currNode = nodeSet->nodeTab[index]; + + GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:currNode]; + if (node) { + [array addObject:node]; + } + } + } + xmlXPathFreeObject(xpathObj); + } + xmlXPathFreeContext(xpathCtx); + } + + if (array == nil) { + + // provide an error + // + // TODO(grobbins) obtain better xpath and libxml errors + const char *msg = xpathCtx->lastError.str1; + NSDictionary *userInfo = nil; + if (msg) { + userInfo = [NSDictionary dictionaryWithObject:[NSString stringWithUTF8String:msg] + forKey:@"error"]; + } + *error = [NSError errorWithDomain:@"com.google.GDataXML" + code:xpathCtx->lastError.code + userInfo:userInfo]; + } + } + + if (tempDoc != NULL) { + xmlUnlinkNode(topParent); + xmlSetTreeDoc(topParent, NULL); + xmlFreeDoc(tempDoc); + } + return array; +} + +- (NSString *)description { + int nodeType = (xmlNode_ ? (int)xmlNode_->type : -1); + + return [NSString stringWithFormat:@"%@ 0x%lX: {type:%d name:%@ xml:\"%@\"}", + [self class], self, nodeType, [self name], [self XMLString]]; +} + +- (id)copyWithZone:(NSZone *)zone { + + xmlNodePtr nodeCopy = [self XMLNodeCopy]; + + if (nodeCopy != NULL) { + return [[[self class] alloc] initConsumingXMLNode:nodeCopy]; + } + return nil; +} + +- (BOOL)isEqual:(GDataXMLNode *)other { + if (self == other) return YES; + if (![other isKindOfClass:[GDataXMLNode class]]) return NO; + + return [self XMLNode] == [other XMLNode] + || ([self kind] == [other kind] + && AreEqualOrBothNilPrivate([self name], [other name]) + && [[self children] count] == [[other children] count]); + +} + +- (NSUInteger)hash { + return (NSUInteger) (void *) [GDataXMLNode class]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { + return [super methodSignatureForSelector:selector]; +} + +#pragma mark - + +- (xmlNodePtr)XMLNodeCopy { + if (xmlNode_ != NULL) { + + // Note: libxml will create a new copy of namespace nodes (xmlNs records) + // and attach them to this copy in order to keep namespaces within this + // node subtree copy value. + + xmlNodePtr nodeCopy = xmlCopyNode(xmlNode_, 1); // 1 = recursive + return nodeCopy; + } + return NULL; +} + +- (xmlNodePtr)XMLNode { + return xmlNode_; +} + +- (BOOL)shouldFreeXMLNode { + return shouldFreeXMLNode_; +} + +- (void)setShouldFreeXMLNode:(BOOL)flag { + shouldFreeXMLNode_ = flag; +} + +@end + + + +@implementation GDataXMLElement + +- (id)initWithXMLString:(NSString *)str error:(NSError **)error { + self = [super init]; + if (self) { + + const char *utf8Str = [str UTF8String]; + // NOTE: We are assuming a string length that fits into an int + xmlDocPtr doc = xmlReadMemory(utf8Str, (int)strlen(utf8Str), NULL, // URL + NULL, // encoding + kGDataXMLParseOptions); + if (doc == NULL) { + if (error) { + // TODO(grobbins) use xmlSetGenericErrorFunc to capture error + } + } else { + // copy the root node from the doc + xmlNodePtr root = xmlDocGetRootElement(doc); + if (root) { + xmlNode_ = xmlCopyNode(root, 1); // 1: recursive + } + xmlFreeDoc(doc); + } + + + if (xmlNode_ == NULL) { + // failure + if (error) { + *error = [NSError errorWithDomain:@"com.google.GDataXML" + code:-1 + userInfo:nil]; + } + [self release]; + return nil; + } + } + return self; +} + +- (NSArray *)namespaces { + + NSMutableArray *array = nil; + + if (xmlNode_ != NULL && xmlNode_->nsDef != NULL) { + + xmlNsPtr currNS = xmlNode_->nsDef; + while (currNS != NULL) { + + // add this prefix/URI to the list, unless it's the implicit xml prefix + if (!xmlStrEqual(currNS->prefix, (const xmlChar *) "xml")) { + GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:(xmlNodePtr) currNS]; + + if (array == nil) { + array = [NSMutableArray arrayWithObject:node]; + } else { + [array addObject:node]; + } + } + + currNS = currNS->next; + } + } + return array; +} + +- (void)setNamespaces:(NSArray *)namespaces { + + if (xmlNode_ != NULL) { + + [self releaseCachedValues]; + + // remove previous namespaces + if (xmlNode_->nsDef) { + xmlFreeNsList(xmlNode_->nsDef); + xmlNode_->nsDef = NULL; + } + + // add a namespace for each object in the array + NSEnumerator *enumerator = [namespaces objectEnumerator]; + GDataXMLNode *namespace; + while ((namespace = [enumerator nextObject]) != nil) { + + xmlNsPtr ns = (xmlNsPtr) [namespace XMLNode]; + if (ns) { + (void)xmlNewNs(xmlNode_, ns->href, ns->prefix); + } + } + + // we may need to fix this node's own name; the graft point is where + // the namespace search starts, so that points to this node too + [[self class] fixUpNamespacesForNode:xmlNode_ + graftingToTreeNode:xmlNode_]; + } +} + +- (void)addNamespace:(GDataXMLNode *)aNamespace { + + if (xmlNode_ != NULL) { + + [self releaseCachedValues]; + + xmlNsPtr ns = (xmlNsPtr) [aNamespace XMLNode]; + if (ns) { + (void)xmlNewNs(xmlNode_, ns->href, ns->prefix); + + // we may need to fix this node's own name; the graft point is where + // the namespace search starts, so that points to this node too + [[self class] fixUpNamespacesForNode:xmlNode_ + graftingToTreeNode:xmlNode_]; + } + } +} + +- (void)addChild:(GDataXMLNode *)child { + if ([child kind] == GDataXMLAttributeKind) { + [self addAttribute:child]; + return; + } + + if (xmlNode_ != NULL) { + + [self releaseCachedValues]; + + xmlNodePtr childNodeCopy = [child XMLNodeCopy]; + if (childNodeCopy) { + + xmlNodePtr resultNode = xmlAddChild(xmlNode_, childNodeCopy); + if (resultNode == NULL) { + + // failed to add + xmlFreeNode(childNodeCopy); + + } else { + // added this child subtree successfully; see if it has + // previously-unresolved namespace prefixes that can now be fixed up + [[self class] fixUpNamespacesForNode:childNodeCopy + graftingToTreeNode:xmlNode_]; + } + } + } +} + +- (void)removeChild:(GDataXMLNode *)child { + // this is safe for attributes too + if (xmlNode_ != NULL) { + + [self releaseCachedValues]; + + xmlNodePtr node = [child XMLNode]; + + xmlUnlinkNode(node); + + // if the child node was borrowing its xmlNodePtr, then we need to + // explicitly free it, since there is probably no owning object that will + // free it on dealloc + if (![child shouldFreeXMLNode]) { + xmlFreeNode(node); + } + } +} + +- (NSArray *)elementsForName:(NSString *)name { + + NSString *desiredName = name; + + if (xmlNode_ != NULL) { + + NSString *prefix = [[self class] prefixForName:desiredName]; + if (prefix) { + + xmlChar* desiredPrefix = GDataGetXMLString(prefix); + + xmlNsPtr foundNS = xmlSearchNs(xmlNode_->doc, xmlNode_, desiredPrefix); + if (foundNS) { + + // we found a namespace; fall back on elementsForLocalName:URI: + // to get the elements + NSString *desiredURI = [self stringFromXMLString:(foundNS->href)]; + NSString *localName = [[self class] localNameForName:desiredName]; + + NSArray *nsArray = [self elementsForLocalName:localName URI:desiredURI]; + return nsArray; + } + } + + // no namespace found for the node's prefix; try an exact match + // for the name argument, including any prefix + NSMutableArray *array = nil; + + // walk our list of cached child nodes + NSArray *children = [self children]; + + for (GDataXMLNode *child in children) { + + xmlNodePtr currNode = [child XMLNode]; + + // find all children which are elements with the desired name + if (currNode->type == XML_ELEMENT_NODE) { + + NSString *qName = [child name]; + if ([qName isEqual:name]) { + + if (array == nil) { + array = [NSMutableArray arrayWithObject:child]; + } else { + [array addObject:child]; + } + } + } + } + return array; + } + return nil; +} + +- (NSArray *)elementsForLocalName:(NSString *)localName URI:(NSString *)URI { + + NSMutableArray *array = nil; + + if (xmlNode_ != NULL && xmlNode_->children != NULL) { + + xmlChar* desiredNSHref = GDataGetXMLString(URI); + xmlChar* requestedLocalName = GDataGetXMLString(localName); + xmlChar* expectedLocalName = requestedLocalName; + + // resolve the URI at the parent level, since usually children won't + // have their own namespace definitions, and we don't want to try to + // resolve it once for every child + xmlNsPtr foundParentNS = xmlSearchNsByHref(xmlNode_->doc, xmlNode_, desiredNSHref); + if (foundParentNS == NULL) { + NSString *fakeQName = GDataFakeQNameForURIAndName(URI, localName); + expectedLocalName = GDataGetXMLString(fakeQName); + } + + NSArray *children = [self children]; + + for (GDataXMLNode *child in children) { + + xmlNodePtr currChildPtr = [child XMLNode]; + + // find all children which are elements with the desired name and + // namespace, or with the prefixed name and a null namespace + if (currChildPtr->type == XML_ELEMENT_NODE) { + + // normally, we can assume the resolution done for the parent will apply + // to the child, as most children do not define their own namespaces + xmlNsPtr childLocalNS = foundParentNS; + xmlChar* childDesiredLocalName = expectedLocalName; + + if (currChildPtr->nsDef != NULL) { + // this child has its own namespace definitons; do a fresh resolve + // of the namespace starting from the child, and see if it differs + // from the resolve done starting from the parent. If the resolve + // finds a different namespace, then override the desired local + // name just for this child. + childLocalNS = xmlSearchNsByHref(xmlNode_->doc, currChildPtr, desiredNSHref); + if (childLocalNS != foundParentNS) { + + // this child does indeed have a different namespace resolution + // result than was found for its parent + if (childLocalNS == NULL) { + // no namespace found + NSString *fakeQName = GDataFakeQNameForURIAndName(URI, localName); + childDesiredLocalName = GDataGetXMLString(fakeQName); + } else { + // a namespace was found; use the original local name requested, + // not a faked one expected from resolving the parent + childDesiredLocalName = requestedLocalName; + } + } + } + + // check if this child's namespace and local name are what we're + // seeking + if (currChildPtr->ns == childLocalNS + && currChildPtr->name != NULL + && xmlStrEqual(currChildPtr->name, childDesiredLocalName)) { + + if (array == nil) { + array = [NSMutableArray arrayWithObject:child]; + } else { + [array addObject:child]; + } + } + } + } + // we return nil, not an empty array, according to docs + } + return array; +} + +- (NSArray *)attributes { + + if (cachedAttributes_ != nil) { + return cachedAttributes_; + } + + NSMutableArray *array = nil; + + if (xmlNode_ != NULL && xmlNode_->properties != NULL) { + + xmlAttrPtr prop = xmlNode_->properties; + while (prop != NULL) { + + GDataXMLNode *node = [GDataXMLNode nodeBorrowingXMLNode:(xmlNodePtr) prop]; + if (array == nil) { + array = [NSMutableArray arrayWithObject:node]; + } else { + [array addObject:node]; + } + + prop = prop->next; + } + + cachedAttributes_ = [array retain]; + } + return array; +} + +- (void)addAttribute:(GDataXMLNode *)attribute { + + if (xmlNode_ != NULL) { + + [self releaseCachedValues]; + + xmlAttrPtr attrPtr = (xmlAttrPtr) [attribute XMLNode]; + if (attrPtr) { + + // ignore this if an attribute with the name is already present, + // similar to NSXMLNode's addAttribute + xmlAttrPtr oldAttr; + + if (attrPtr->ns == NULL) { + oldAttr = xmlHasProp(xmlNode_, attrPtr->name); + } else { + oldAttr = xmlHasNsProp(xmlNode_, attrPtr->name, attrPtr->ns->href); + } + + if (oldAttr == NULL) { + + xmlNsPtr newPropNS = NULL; + + // if this attribute has a namespace, search for a matching namespace + // on the node we're adding to + if (attrPtr->ns != NULL) { + + newPropNS = xmlSearchNsByHref(xmlNode_->doc, xmlNode_, attrPtr->ns->href); + if (newPropNS == NULL) { + // make a new namespace on the parent node, and use that for the + // new attribute + newPropNS = xmlNewNs(xmlNode_, attrPtr->ns->href, attrPtr->ns->prefix); + } + } + + // copy the attribute onto this node + xmlChar *value = xmlNodeGetContent((xmlNodePtr) attrPtr); + xmlAttrPtr newProp = xmlNewNsProp(xmlNode_, newPropNS, attrPtr->name, value); + if (newProp != NULL) { + // we made the property, so clean up the property's namespace + + [[self class] fixUpNamespacesForNode:(xmlNodePtr)newProp + graftingToTreeNode:xmlNode_]; + } + + if (value != NULL) { + xmlFree(value); + } + } + } + } +} + +- (GDataXMLNode *)attributeForXMLNode:(xmlAttrPtr)theXMLNode { + // search the cached attributes list for the GDataXMLNode with + // the underlying xmlAttrPtr + NSArray *attributes = [self attributes]; + + for (GDataXMLNode *attr in attributes) { + + if (theXMLNode == (xmlAttrPtr) [attr XMLNode]) { + return attr; + } + } + + return nil; +} + +- (GDataXMLNode *)attributeForName:(NSString *)name { + + if (xmlNode_ != NULL) { + + xmlAttrPtr attrPtr = xmlHasProp(xmlNode_, GDataGetXMLString(name)); + if (attrPtr == NULL) { + + // can we guarantee that xmlAttrPtrs always have the ns ptr and never + // a namespace as part of the actual attribute name? + + // split the name and its prefix, if any + xmlNsPtr ns = NULL; + NSString *prefix = [[self class] prefixForName:name]; + if (prefix) { + + // find the namespace for this prefix, and search on its URI to find + // the xmlNsPtr + name = [[self class] localNameForName:name]; + ns = xmlSearchNs(xmlNode_->doc, xmlNode_, GDataGetXMLString(prefix)); + } + + const xmlChar* nsURI = ((ns != NULL) ? ns->href : NULL); + attrPtr = xmlHasNsProp(xmlNode_, GDataGetXMLString(name), nsURI); + } + + if (attrPtr) { + GDataXMLNode *attr = [self attributeForXMLNode:attrPtr]; + return attr; + } + } + return nil; +} + +- (GDataXMLNode *)attributeForLocalName:(NSString *)localName + URI:(NSString *)attributeURI { + + if (xmlNode_ != NULL) { + + const xmlChar* name = GDataGetXMLString(localName); + const xmlChar* nsURI = GDataGetXMLString(attributeURI); + + xmlAttrPtr attrPtr = xmlHasNsProp(xmlNode_, name, nsURI); + + if (attrPtr == NULL) { + // if the attribute is in a tree lacking the proper namespace, + // the local name may include the full URI as a prefix + NSString *fakeQName = GDataFakeQNameForURIAndName(attributeURI, localName); + const xmlChar* xmlFakeQName = GDataGetXMLString(fakeQName); + + attrPtr = xmlHasProp(xmlNode_, xmlFakeQName); + } + + if (attrPtr) { + GDataXMLNode *attr = [self attributeForXMLNode:attrPtr]; + return attr; + } + } + return nil; +} + +- (NSString *)resolvePrefixForNamespaceURI:(NSString *)namespaceURI { + + if (xmlNode_ != NULL) { + + xmlChar* desiredNSHref = GDataGetXMLString(namespaceURI); + + xmlNsPtr foundNS = xmlSearchNsByHref(xmlNode_->doc, xmlNode_, desiredNSHref); + if (foundNS) { + + // we found the namespace + if (foundNS->prefix != NULL) { + NSString *prefix = [self stringFromXMLString:(foundNS->prefix)]; + return prefix; + } else { + // empty prefix is default namespace + return @""; + } + } + } + return nil; +} + +#pragma mark Namespace fixup routines + ++ (void)deleteNamespacePtr:(xmlNsPtr)namespaceToDelete + fromXMLNode:(xmlNodePtr)node { + + // utilty routine to remove a namespace pointer from an element's + // namespace definition list. This is just removing the nsPtr + // from the singly-linked list, the node's namespace definitions. + xmlNsPtr currNS = node->nsDef; + xmlNsPtr prevNS = NULL; + + while (currNS != NULL) { + xmlNsPtr nextNS = currNS->next; + + if (namespaceToDelete == currNS) { + + // found it; delete it from the head of the node's ns definition list + // or from the next field of the previous namespace + + if (prevNS != NULL) prevNS->next = nextNS; + else node->nsDef = nextNS; + + xmlFreeNs(currNS); + return; + } + prevNS = currNS; + currNS = nextNS; + } +} + ++ (void)fixQualifiedNamesForNode:(xmlNodePtr)nodeToFix + graftingToTreeNode:(xmlNodePtr)graftPointNode { + + // Replace prefix-in-name with proper namespace pointers + // + // This is an inner routine for fixUpNamespacesForNode: + // + // see if this node's name lacks a namespace and is qualified, and if so, + // see if we can resolve the prefix against the parent + // + // The prefix may either be normal, "gd:foo", or a URI + // "{http://blah.com/}:foo" + + if (nodeToFix->ns == NULL) { + xmlNsPtr foundNS = NULL; + + xmlChar* prefix = NULL; + xmlChar* localName = SplitQNameReverse(nodeToFix->name, &prefix); + if (localName != NULL) { + if (prefix != NULL) { + + // if the prefix is wrapped by { and } then it's a URI + int prefixLen = xmlStrlen(prefix); + if (prefixLen > 2 + && prefix[0] == '{' + && prefix[prefixLen - 1] == '}') { + + // search for the namespace by URI + xmlChar* uri = xmlStrsub(prefix, 1, prefixLen - 2); + + if (uri != NULL) { + foundNS = xmlSearchNsByHref(graftPointNode->doc, graftPointNode, uri); + + xmlFree(uri); + } + } + } + + if (foundNS == NULL) { + // search for the namespace by prefix, even if the prefix is nil + // (nil prefix means to search for the default namespace) + foundNS = xmlSearchNs(graftPointNode->doc, graftPointNode, prefix); + } + + if (foundNS != NULL) { + // we found a namespace, so fix the ns pointer and the local name + xmlSetNs(nodeToFix, foundNS); + xmlNodeSetName(nodeToFix, localName); + } + + if (prefix != NULL) { + xmlFree(prefix); + prefix = NULL; + } + + xmlFree(localName); + } + } +} + ++ (void)fixDuplicateNamespacesForNode:(xmlNodePtr)nodeToFix + graftingToTreeNode:(xmlNodePtr)graftPointNode + namespaceSubstitutionMap:(NSMutableDictionary *)nsMap { + + // Duplicate namespace removal + // + // This is an inner routine for fixUpNamespacesForNode: + // + // If any of this node's namespaces are already defined at the graft point + // level, add that namespace to the map of namespace substitutions + // so it will be replaced in the children below the nodeToFix, and + // delete the namespace record + + if (nodeToFix->type == XML_ELEMENT_NODE) { + + // step through the namespaces defined on this node + xmlNsPtr definedNS = nodeToFix->nsDef; + while (definedNS != NULL) { + + // see if this namespace is already defined higher in the tree, + // with both the same URI and the same prefix; if so, add a mapping for + // it + xmlNsPtr foundNS = xmlSearchNsByHref(graftPointNode->doc, graftPointNode, + definedNS->href); + if (foundNS != NULL + && foundNS != definedNS + && xmlStrEqual(definedNS->prefix, foundNS->prefix)) { + + // store a mapping from this defined nsPtr to the one found higher + // in the tree + [nsMap setObject:[NSValue valueWithPointer:foundNS] + forKey:[NSValue valueWithPointer:definedNS]]; + + // remove this namespace from the ns definition list of this node; + // all child elements and attributes referencing this namespace + // now have a dangling pointer and must be updated (that is done later + // in this method) + // + // before we delete this namespace, move our pointer to the + // next one + xmlNsPtr nsToDelete = definedNS; + definedNS = definedNS->next; + + [self deleteNamespacePtr:nsToDelete fromXMLNode:nodeToFix]; + + } else { + // this namespace wasn't a duplicate; move to the next + definedNS = definedNS->next; + } + } + } + + // if this node's namespace is one we deleted, update it to point + // to someplace better + if (nodeToFix->ns != NULL) { + + NSValue *currNS = [NSValue valueWithPointer:nodeToFix->ns]; + NSValue *replacementNS = [nsMap objectForKey:currNS]; + + if (replacementNS != nil) { + xmlNsPtr replaceNSPtr = [replacementNS pointerValue]; + + xmlSetNs(nodeToFix, replaceNSPtr); + } + } +} + + + ++ (void)fixUpNamespacesForNode:(xmlNodePtr)nodeToFix + graftingToTreeNode:(xmlNodePtr)graftPointNode + namespaceSubstitutionMap:(NSMutableDictionary *)nsMap { + + // This is the inner routine for fixUpNamespacesForNode:graftingToTreeNode: + // + // This routine fixes two issues: + // + // Because we can create nodes with qualified names before adding + // them to the tree that declares the namespace for the prefix, + // we need to set the node namespaces after adding them to the tree. + // + // Because libxml adds namespaces to nodes when it copies them, + // we want to remove redundant namespaces after adding them to + // a tree. + // + // If only the Mac's libxml had xmlDOMWrapReconcileNamespaces, it could do + // namespace cleanup for us + + // We only care about fixing names of elements and attributes + if (nodeToFix->type != XML_ELEMENT_NODE + && nodeToFix->type != XML_ATTRIBUTE_NODE) return; + + // Do the fixes + [self fixQualifiedNamesForNode:nodeToFix + graftingToTreeNode:graftPointNode]; + + [self fixDuplicateNamespacesForNode:nodeToFix + graftingToTreeNode:graftPointNode + namespaceSubstitutionMap:nsMap]; + + if (nodeToFix->type == XML_ELEMENT_NODE) { + + // when fixing element nodes, recurse for each child element and + // for each attribute + xmlNodePtr currChild = nodeToFix->children; + while (currChild != NULL) { + [self fixUpNamespacesForNode:currChild + graftingToTreeNode:graftPointNode + namespaceSubstitutionMap:nsMap]; + currChild = currChild->next; + } + + xmlAttrPtr currProp = nodeToFix->properties; + while (currProp != NULL) { + [self fixUpNamespacesForNode:(xmlNodePtr)currProp + graftingToTreeNode:graftPointNode + namespaceSubstitutionMap:nsMap]; + currProp = currProp->next; + } + } +} + ++ (void)fixUpNamespacesForNode:(xmlNodePtr)nodeToFix + graftingToTreeNode:(xmlNodePtr)graftPointNode { + + // allocate the namespace map that will be passed + // down on recursive calls + NSMutableDictionary *nsMap = [NSMutableDictionary dictionary]; + + [self fixUpNamespacesForNode:nodeToFix + graftingToTreeNode:graftPointNode + namespaceSubstitutionMap:nsMap]; +} + +@end + + +@interface GDataXMLDocument (PrivateMethods) +- (void)addStringsCacheToDoc; +@end + +@implementation GDataXMLDocument + +- (id)initWithXMLString:(NSString *)str options:(unsigned int)mask error:(NSError **)error { + + NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding]; + GDataXMLDocument *doc = [self initWithData:data options:mask error:error]; + return doc; +} + +- (id)initWithData:(NSData *)data options:(unsigned int)mask error:(NSError **)error { + + self = [super init]; + if (self) { + + const char *baseURL = NULL; + const char *encoding = NULL; + + // NOTE: We are assuming [data length] fits into an int. + xmlDoc_ = xmlReadMemory([data bytes], (int)[data length], baseURL, encoding, + kGDataXMLParseOptions); // TODO(grobbins) map option values + if (xmlDoc_ == NULL) { + if (error) { + *error = [NSError errorWithDomain:@"com.google.GDataXML" + code:-1 + userInfo:nil]; + // TODO(grobbins) use xmlSetGenericErrorFunc to capture error + [self release]; + } + return nil; + } else { + if (error) *error = NULL; + + [self addStringsCacheToDoc]; + } + } + + return self; +} + +- (id)initWithRootElement:(GDataXMLElement *)element { + + self = [super init]; + if (self) { + + xmlDoc_ = xmlNewDoc(NULL); + + (void) xmlDocSetRootElement(xmlDoc_, [element XMLNodeCopy]); + + [self addStringsCacheToDoc]; + } + + return self; +} + +- (void)addStringsCacheToDoc { + // utility routine for init methods + +#if DEBUG + NSCAssert(xmlDoc_ != NULL && xmlDoc_->_private == NULL, + @"GDataXMLDocument cache creation problem"); +#endif + + // add a strings cache as private data for the document + // + // we'll use plain C pointers (xmlChar*) as the keys, and NSStrings + // as the values + CFIndex capacity = 0; // no limit + + CFDictionaryKeyCallBacks keyCallBacks = { + 0, // version + StringCacheKeyRetainCallBack, + StringCacheKeyReleaseCallBack, + StringCacheKeyCopyDescriptionCallBack, + StringCacheKeyEqualCallBack, + StringCacheKeyHashCallBack + }; + + CFMutableDictionaryRef dict = CFDictionaryCreateMutable( + kCFAllocatorDefault, capacity, + &keyCallBacks, &kCFTypeDictionaryValueCallBacks); + + // we'll use the user-defined _private field for our cache + xmlDoc_->_private = dict; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ 0x%lX", [self class], self]; +} + +- (void)dealloc { + if (xmlDoc_ != NULL) { + // release the strings cache + // + // since it's a CF object, were anyone to use this in a GC environment, + // this would need to be released in a finalize method, too + if (xmlDoc_->_private != NULL) { + CFRelease(xmlDoc_->_private); + } + + xmlFreeDoc(xmlDoc_); + } + [super dealloc]; +} + +#pragma mark - + +- (GDataXMLElement *)rootElement { + GDataXMLElement *element = nil; + + if (xmlDoc_ != NULL) { + xmlNodePtr rootNode = xmlDocGetRootElement(xmlDoc_); + if (rootNode) { + element = [GDataXMLElement nodeBorrowingXMLNode:rootNode]; + } + } + return element; +} + +- (NSData *)XMLData { + + if (xmlDoc_ != NULL) { + xmlChar *buffer = NULL; + int bufferSize = 0; + + xmlDocDumpMemory(xmlDoc_, &buffer, &bufferSize); + + if (buffer) { + NSData *data = [NSData dataWithBytes:buffer + length:bufferSize]; + xmlFree(buffer); + return data; + } + } + return nil; +} + +- (void)setVersion:(NSString *)version { + + if (xmlDoc_ != NULL) { + if (xmlDoc_->version != NULL) { + // version is a const char* so we must cast + xmlFree((char *) xmlDoc_->version); + xmlDoc_->version = NULL; + } + + if (version != nil) { + xmlDoc_->version = xmlStrdup(GDataGetXMLString(version)); + } + } +} + +- (void)setCharacterEncoding:(NSString *)encoding { + + if (xmlDoc_ != NULL) { + if (xmlDoc_->encoding != NULL) { + // version is a const char* so we must cast + xmlFree((char *) xmlDoc_->encoding); + xmlDoc_->encoding = NULL; + } + + if (encoding != nil) { + xmlDoc_->encoding = xmlStrdup(GDataGetXMLString(encoding)); + } + } +} + +- (NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error { + if (xmlDoc_ != NULL) { + NSXMLNode *docNode = [GDataXMLElement nodeBorrowingXMLNode:(xmlNodePtr)xmlDoc_]; + NSArray *array = [docNode nodesForXPath:xpath error:error]; + return array; + } + return nil; +} + +@end + +// +// Dictionary key callbacks for our C-string to NSString cache dictionary +// +static const void *StringCacheKeyRetainCallBack(CFAllocatorRef allocator, const void *str) { + // copy the key + xmlChar* key = xmlStrdup(str); + return key; +} + +static void StringCacheKeyReleaseCallBack(CFAllocatorRef allocator, const void *str) { + // free the key + char *chars = (char *)str; + xmlFree((char *) chars); +} + +static CFStringRef StringCacheKeyCopyDescriptionCallBack(const void *str) { + // make a CFString from the key + CFStringRef cfStr = CFStringCreateWithCString(kCFAllocatorDefault, + (const char *)str, + kCFStringEncodingUTF8); + return cfStr; +} + +static Boolean StringCacheKeyEqualCallBack(const void *str1, const void *str2) { + // compare the key strings + if (str1 == str2) return true; + + int result = xmlStrcmp(str1, str2); + return (result == 0); +} + +static CFHashCode StringCacheKeyHashCallBack(const void *str) { + + // dhb hash, per http://www.cse.yorku.ca/~oz/hash.html + CFHashCode hash = 5381; + int c; + const char *chars = (const char *)str; + + while ((c = *chars++) != 0) { + hash = ((hash << 5) + hash) + c; + } + return hash; +} diff --git a/mhdl2010.xcodeproj/project.pbxproj b/mhdl2010.xcodeproj/project.pbxproj index 21a8c19..d32a83c 100755 --- a/mhdl2010.xcodeproj/project.pbxproj +++ b/mhdl2010.xcodeproj/project.pbxproj @@ -70,6 +70,13 @@ D95F2A741232D04800B90FFC /* SBJsonBase.m in Sources */ = {isa = PBXBuildFile; fileRef = D95F2A6D1232D04800B90FFC /* SBJsonBase.m */; }; D95F2A751232D04800B90FFC /* SBJsonParser.m in Sources */ = {isa = PBXBuildFile; fileRef = D95F2A6F1232D04800B90FFC /* SBJsonParser.m */; }; D95F2A761232D04800B90FFC /* SBJsonWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = D95F2A711232D04800B90FFC /* SBJsonWriter.m */; }; + D95F2B571232D30800B90FFC /* GDataHTTPFetcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D95F2B561232D30800B90FFC /* GDataHTTPFetcher.m */; }; + D95F2B6F1232D36600B90FFC /* GDataXMLNode.m in Sources */ = {isa = PBXBuildFile; fileRef = D95F2B6E1232D36600B90FFC /* GDataXMLNode.m */; }; + D95F2B7C1232D39C00B90FFC /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = D95F2B7B1232D39C00B90FFC /* libxml2.dylib */; }; + D95F2B9B1232D41A00B90FFC /* GDataHTTPFetcherLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = D95F2B9A1232D41A00B90FFC /* GDataHTTPFetcherLogging.m */; }; + D95F2BA51232D42E00B90FFC /* GDataMIMEDocument.m in Sources */ = {isa = PBXBuildFile; fileRef = D95F2BA21232D42E00B90FFC /* GDataMIMEDocument.m */; }; + D95F2BA61232D42E00B90FFC /* GDataProgressMonitorInputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = D95F2BA41232D42E00B90FFC /* GDataProgressMonitorInputStream.m */; }; + D95F2BAE1232D43A00B90FFC /* GDataGatherInputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = D95F2BAD1232D43A00B90FFC /* GDataGatherInputStream.m */; }; DC6640030F83B3EA000B3E49 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC6640020F83B3EA000B3E49 /* AudioToolbox.framework */; }; DC6640050F83B3EA000B3E49 /* OpenAL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC6640040F83B3EA000B3E49 /* OpenAL.framework */; }; DCCBF1B70F6022AE0040855A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCCBF1B60F6022AE0040855A /* CoreGraphics.framework */; }; @@ -316,6 +323,20 @@ D95F2A6F1232D04800B90FFC /* SBJsonParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SBJsonParser.m; sourceTree = ""; }; D95F2A701232D04800B90FFC /* SBJsonWriter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SBJsonWriter.h; sourceTree = ""; }; D95F2A711232D04800B90FFC /* SBJsonWriter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SBJsonWriter.m; sourceTree = ""; }; + D95F2B551232D30800B90FFC /* GDataHTTPFetcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GDataHTTPFetcher.h; sourceTree = ""; }; + D95F2B561232D30800B90FFC /* GDataHTTPFetcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GDataHTTPFetcher.m; sourceTree = ""; }; + D95F2B621232D31900B90FFC /* GDataDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GDataDefines.h; sourceTree = ""; }; + D95F2B6D1232D36600B90FFC /* GDataXMLNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GDataXMLNode.h; sourceTree = ""; }; + D95F2B6E1232D36600B90FFC /* GDataXMLNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GDataXMLNode.m; sourceTree = ""; }; + D95F2B7B1232D39C00B90FFC /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = usr/lib/libxml2.dylib; sourceTree = SDKROOT; }; + D95F2B991232D41A00B90FFC /* GDataHTTPFetcherLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GDataHTTPFetcherLogging.h; sourceTree = ""; }; + D95F2B9A1232D41A00B90FFC /* GDataHTTPFetcherLogging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GDataHTTPFetcherLogging.m; sourceTree = ""; }; + D95F2BA11232D42E00B90FFC /* GDataMIMEDocument.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GDataMIMEDocument.h; sourceTree = ""; }; + D95F2BA21232D42E00B90FFC /* GDataMIMEDocument.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GDataMIMEDocument.m; sourceTree = ""; }; + D95F2BA31232D42E00B90FFC /* GDataProgressMonitorInputStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GDataProgressMonitorInputStream.h; sourceTree = ""; }; + D95F2BA41232D42E00B90FFC /* GDataProgressMonitorInputStream.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GDataProgressMonitorInputStream.m; sourceTree = ""; }; + D95F2BAC1232D43A00B90FFC /* GDataGatherInputStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GDataGatherInputStream.h; sourceTree = ""; }; + D95F2BAD1232D43A00B90FFC /* GDataGatherInputStream.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GDataGatherInputStream.m; sourceTree = ""; }; DC6640020F83B3EA000B3E49 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; DC6640040F83B3EA000B3E49 /* OpenAL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenAL.framework; path = System/Library/Frameworks/OpenAL.framework; sourceTree = SDKROOT; }; DCCBF1B60F6022AE0040855A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -499,6 +520,7 @@ 506EDB88102F4C4000A389B3 /* libz.dylib in Frameworks */, 506EDBA5102F4C9F00A389B3 /* AVFoundation.framework in Frameworks */, 506EE1A91030508200A389B3 /* libcocos2d libraries.a in Frameworks */, + D95F2B7C1232D39C00B90FFC /* libxml2.dylib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -539,6 +561,7 @@ 50F414EB1069373D002A0D5E /* Resources */, 29B97323FDCFA39411CA2CEA /* Frameworks */, 19C28FACFE9D520D11CA2CBB /* Products */, + D95F2B7B1232D39C00B90FFC /* libxml2.dylib */, ); name = CustomTemplate; sourceTree = ""; @@ -571,6 +594,19 @@ 2D500B1D0D5A766B00DBA0E3 /* Classes */ = { isa = PBXGroup; children = ( + D95F2BAC1232D43A00B90FFC /* GDataGatherInputStream.h */, + D95F2BAD1232D43A00B90FFC /* GDataGatherInputStream.m */, + D95F2BA11232D42E00B90FFC /* GDataMIMEDocument.h */, + D95F2BA21232D42E00B90FFC /* GDataMIMEDocument.m */, + D95F2BA31232D42E00B90FFC /* GDataProgressMonitorInputStream.h */, + D95F2BA41232D42E00B90FFC /* GDataProgressMonitorInputStream.m */, + D95F2B991232D41A00B90FFC /* GDataHTTPFetcherLogging.h */, + D95F2B9A1232D41A00B90FFC /* GDataHTTPFetcherLogging.m */, + D95F2B6D1232D36600B90FFC /* GDataXMLNode.h */, + D95F2B6E1232D36600B90FFC /* GDataXMLNode.m */, + D95F2B551232D30800B90FFC /* GDataHTTPFetcher.h */, + D95F2B621232D31900B90FFC /* GDataDefines.h */, + D95F2B561232D30800B90FFC /* GDataHTTPFetcher.m */, D95F2A631232D03F00B90FFC /* JSON */, E0F80F5D120A0182005866B8 /* GameConfig.h */, 506EDC2F102F528A00A389B3 /* HelloWorldScene.h */, @@ -1101,6 +1137,12 @@ D95F2A741232D04800B90FFC /* SBJsonBase.m in Sources */, D95F2A751232D04800B90FFC /* SBJsonParser.m in Sources */, D95F2A761232D04800B90FFC /* SBJsonWriter.m in Sources */, + D95F2B571232D30800B90FFC /* GDataHTTPFetcher.m in Sources */, + D95F2B6F1232D36600B90FFC /* GDataXMLNode.m in Sources */, + D95F2B9B1232D41A00B90FFC /* GDataHTTPFetcherLogging.m in Sources */, + D95F2BA51232D42E00B90FFC /* GDataMIMEDocument.m in Sources */, + D95F2BA61232D42E00B90FFC /* GDataProgressMonitorInputStream.m in Sources */, + D95F2BAE1232D43A00B90FFC /* GDataGatherInputStream.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1304,6 +1346,7 @@ GCC_VERSION = com.apple.compilers.llvmgcc42; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = /usr/include/libxml2/; IPHONEOS_DEPLOYMENT_TARGET = 3.2; ONLY_ACTIVE_ARCH = YES; PREBINDING = NO;