Merge Request Queue (See issue #75):

* Introduces RKRequestCache for cacheing responses (supports ETag conditional GET, use cache if available, use cache on error, etc.) closes #75
    * Updates to Three20 layer to eliminate need for intermediary TTTableItem classes closes #76
    * Fixes to ensure iOS 3.x compatability:
        * Switched compiler to Clang
        * Updated conditional checks for UIBackgroundTask symbols to ensure runtime safety on iOS 3.x
        * Removed unnecessary linkage against UIKit and CoreFoundation from library targets
    * Fix for issue where RKRequest objects could become stuck in infinite loop within RKRequestQueue loadNextInQueue if you start
      a request and then cancel immediately. On cancel only decrement loadCount if the request has start loading. refs #122
This commit is contained in:
Blake Watters
2011-06-08 14:39:38 -04:00
parent 9416ad9cf6
commit f2ceefa012
42 changed files with 1829 additions and 268 deletions

View File

@@ -24,7 +24,7 @@
}
return self;
}
- (void)dealloc {
[_targetObjectID release];
_targetObjectID = nil;

14
Code/Network/NSData+MD5.h Normal file
View File

@@ -0,0 +1,14 @@
//
// NSData+MD5.h
// RestKit
//
// Created by Jeff Arena on 4/4/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
@interface NSData (MD5)
- (NSString*)MD5;
@end

30
Code/Network/NSData+MD5.m Normal file
View File

@@ -0,0 +1,30 @@
//
// NSData+MD5.m
// RestKit
//
// Created by Jeff Arena on 4/4/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
#import "NSData+MD5.h"
#import <CommonCrypto/CommonDigest.h>
@implementation NSData (MD5)
- (NSString*)MD5 {
// Create byte array of unsigned chars
unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH];
// Create 16 byte MD5 hash value, store in buffer
CC_MD5(self.bytes, self.length, md5Buffer);
// Convert unsigned char buffer to NSString of hex values
NSMutableString* output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[output appendFormat:@"%02x",md5Buffer[i]];
}
return output;
}
@end

View File

@@ -0,0 +1,14 @@
//
// NSString+MD5.h
// RestKit
//
// Created by Jeff Arena on 4/4/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
@interface NSString (MD5)
- (NSString*)MD5;
@end

View File

@@ -0,0 +1,33 @@
//
// NSString+MD5.m
// RestKit
//
// Created by Jeff Arena on 4/4/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
#import "NSString+MD5.h"
#import <CommonCrypto/CommonDigest.h>
@implementation NSString (MD5)
- (NSString*)MD5 {
// Create pointer to the string as UTF8
const char* ptr = [self UTF8String];
// Create byte array of unsigned chars
unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH];
// Create 16 byte MD5 hash value, store in buffer
CC_MD5(ptr, strlen(ptr), md5Buffer);
// Convert MD5 value in the buffer to NSString of hex values
NSMutableString* output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[output appendFormat:@"%02x",md5Buffer[i]];
}
return output;
}
@end

View File

@@ -11,6 +11,7 @@
#import "RKResponse.h"
#import "NSDictionary+RKRequestSerialization.h"
#import "RKReachabilityObserver.h"
#import "RKRequestCache.h"
/////////////////////////////////////////////////////////////////////////
@@ -127,6 +128,8 @@ NSString* RKPathAppendQueryParams(NSString* resourcePath, NSDictionary* queryPar
NSString* _serviceUnavailableAlertTitle;
NSString* _serviceUnavailableAlertMessage;
BOOL _serviceUnavailableAlertEnabled;
RKRequestCache* _cache;
RKRequestCachePolicy _cachePolicy;
}
/////////////////////////////////////////////////////////////////////////
@@ -224,6 +227,10 @@ NSString* RKPathAppendQueryParams(NSString* resourcePath, NSDictionary* queryPar
*/
@property(nonatomic, assign) BOOL serviceUnavailableAlertEnabled;
@property (nonatomic, retain) RKRequestCache* cache;
@property (nonatomic, assign) RKRequestCachePolicy cachePolicy;
/////////////////////////////////////////////////////////////////////////
/// @name Shared Client Instance
/////////////////////////////////////////////////////////////////////////

View File

@@ -32,7 +32,7 @@ NSString* RKMakeURLPath(NSString* resourcePath) {
NSString* RKMakePathWithObject(NSString* path, id object) {
NSMutableDictionary* substitutions = [NSMutableDictionary dictionary];
NSScanner* scanner = [NSScanner scannerWithString:path];
BOOL startsWithParentheses = [[path substringToIndex:1] isEqualToString:@"("];
while ([scanner isAtEnd] == NO) {
NSString* keyPath = nil;
@@ -49,20 +49,20 @@ NSString* RKMakePathWithObject(NSString* path, id object) {
}
}
}
if (0 == [substitutions count]) {
return path;
}
NSMutableString* interpolatedPath = [[path mutableCopy] autorelease];
for (NSString* find in substitutions) {
NSString* replace = [substitutions valueForKey:find];
[interpolatedPath replaceOccurrencesOfString:find
withString:replace
options:NSLiteralSearch
[interpolatedPath replaceOccurrencesOfString:find
withString:replace
options:NSLiteralSearch
range:NSMakeRange(0, [interpolatedPath length])];
}
return [NSString stringWithString:interpolatedPath];
}
@@ -83,6 +83,8 @@ NSString* RKPathAppendQueryParams(NSString* resourcePath, NSDictionary* queryPar
@synthesize serviceUnavailableAlertTitle = _serviceUnavailableAlertTitle;
@synthesize serviceUnavailableAlertMessage = _serviceUnavailableAlertMessage;
@synthesize serviceUnavailableAlertEnabled = _serviceUnavailableAlertEnabled;
@synthesize cache = _cache;
@synthesize cachePolicy = _cachePolicy;
+ (RKClient*)sharedClient {
return sharedClient;
@@ -95,6 +97,13 @@ NSString* RKPathAppendQueryParams(NSString* resourcePath, NSDictionary* queryPar
+ (RKClient*)clientWithBaseURL:(NSString*)baseURL {
RKClient* client = [[[RKClient alloc] init] autorelease];
NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@",
[[NSURL URLWithString:baseURL] host]];
NSString* cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
stringByAppendingPathComponent:cacheDirForClient];
client.cache = [[RKRequestCache alloc] initWithCachePath:cachePath
storagePolicy:RKRequestCacheStoragePolicyPermanently];
client.cachePolicy = RKRequestCachePolicyDefault;
client.baseURL = baseURL;
if (sharedClient == nil) {
[RKClient setSharedClient:client];
@@ -133,8 +142,9 @@ NSString* RKPathAppendQueryParams(NSString* resourcePath, NSDictionary* queryPar
self.password = nil;
self.serviceUnavailableAlertTitle = nil;
self.serviceUnavailableAlertMessage = nil;
self.cache = nil;
[_HTTPHeaders release];
[super dealloc];
}
@@ -170,6 +180,8 @@ NSString* RKPathAppendQueryParams(NSString* resourcePath, NSDictionary* queryPar
request.username = self.username;
request.password = self.password;
request.forceBasicAuthentication = self.forceBasicAuthentication;
request.cachePolicy = self.cachePolicy;
request.cache = self.cache;
}
- (void)setValue:(NSString*)value forHTTPHeaderField:(NSString*)header {
@@ -183,7 +195,7 @@ NSString* RKPathAppendQueryParams(NSString* resourcePath, NSDictionary* queryPar
[_baseURLReachabilityObserver release];
_baseURLReachabilityObserver = nil;
// Don't crash if baseURL is nil'd out (i.e. dealloc)
if (baseURL) {
NSURL* URL = [NSURL URLWithString:baseURL];

View File

@@ -11,5 +11,5 @@
NSString* const RKRequestSentNotification = @"RKRequestSentNotification";
NSString* const RKRequestDidLoadResponseNotification = @"RKRequestDidLoadResponseNotification";
NSString* const RKRequestFailedWithErrorNotification = @"RKRequestFailedWithErrorNotification";
NSString* const RKResponseReceivedNotification = @"RKRespongReceivedNotification";
NSString* const RKServiceDidBecomeUnavailableNotification = @"RKServiceDidBecomeUnavailableNotification";
NSString* const RKResponseReceivedNotification = @"RKResponseReceivedNotification";

View File

@@ -14,6 +14,8 @@
#import <CoreData/CoreData.h>
#import "RKRequestSerializable.h"
@class RKRequestCache;
/**
* HTTP methods for requests
*/
@@ -24,6 +26,28 @@ typedef enum RKRequestMethod {
RKRequestMethodDELETE
} RKRequestMethod;
/**
* Cache policy for determining how to use RKCache
*/
typedef enum {
// Never use the cache
RKRequestCachePolicyNone = 0,
// Load from the cache when we are offline
RKRequestCachePolicyLoadIfOffline = 1 << 0,
// Load from the cache if we encounter an error
RKRequestCachePolicyLoadOnError = 1 << 1,
// Load from the cache if we have data stored and the server returns a 304 (not modified) response
RKRequestCachePolicyEtag = 1 << 2,
// Load from the cache if we have data stored
RKRequestCachePolicyEnabled = 1 << 3,
RKRequestCachePolicyDefault = RKRequestCachePolicyEtag
} RKRequestCachePolicy;
/**
* Background Request Policy
*
@@ -55,9 +79,11 @@ typedef enum RKRequestBackgroundPolicy {
RKRequestMethod _method;
BOOL _isLoading;
BOOL _isLoaded;
RKRequestCachePolicy _cachePolicy;
BOOL _sentSynchronously;
BOOL _forceBasicAuthentication;
RKRequestBackgroundPolicy _backgroundPolicy;
RKRequestCache* _cache;
#if TARGET_OS_IPHONE
UIBackgroundTaskIdentifier _backgroundTaskIdentifier;
@@ -137,6 +163,11 @@ typedef enum RKRequestBackgroundPolicy {
*/
@property(nonatomic, readonly) NSString* HTTPMethod;
@property (nonatomic, readonly) NSString* cacheKey;
@property (nonatomic, assign) RKRequestCachePolicy cachePolicy;
@property (nonatomic, retain) RKRequestCache* cache;
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

View File

@@ -14,13 +14,16 @@
#import "RKClient.h"
#import "../Support/Support.h"
#import "RKURL.h"
#import "NSData+MD5.h"
#import "NSString+MD5.h"
#import "Logging.h"
#import "RKRequestCache.h"
@implementation RKRequest
@synthesize URL = _URL, URLRequest = _URLRequest, delegate = _delegate, additionalHTTPHeaders = _additionalHTTPHeaders,
params = _params, userData = _userData, username = _username, password = _password, method = _method,
forceBasicAuthentication = _forceBasicAuthentication;
forceBasicAuthentication = _forceBasicAuthentication, cachePolicy = _cachePolicy, cache = _cache;
#if TARGET_OS_IPHONE
@synthesize backgroundPolicy = _backgroundPolicy, backgroundTaskIdentifier = _backgroundTaskIdentifier;
@@ -35,19 +38,20 @@
if (self) {
_URL = [URL retain];
_URLRequest = [[NSMutableURLRequest alloc] initWithURL:_URL];
[_URLRequest setCachePolicy:NSURLRequestReloadIgnoringCacheData];
_connection = nil;
_isLoading = NO;
_isLoaded = NO;
_forceBasicAuthentication = NO;
_cachePolicy = RKRequestCachePolicyDefault;
}
return self;
}
- (id)initWithURL:(NSURL*)URL delegate:(id)delegate {
self = [self initWithURL:URL];
if (self) {
_delegate = delegate;
_delegate = delegate;
}
return self;
}
@@ -57,7 +61,11 @@
if (self) {
#if TARGET_OS_IPHONE
_backgroundPolicy = RKRequestBackgroundPolicyNone;
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
_backgroundTaskIdentifier = 0;
BOOL backgroundOK = &UIBackgroundTaskInvalid != NULL;
if (backgroundOK) {
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}
#endif
}
@@ -66,12 +74,11 @@
- (void)cleanupBackgroundTask {
#if TARGET_OS_IPHONE
if (UIBackgroundTaskInvalid == self.backgroundTaskIdentifier) {
BOOL backgroundOK = &UIBackgroundTaskInvalid != NULL;
if (backgroundOK && UIBackgroundTaskInvalid == self.backgroundTaskIdentifier) {
return;
}
NSLog(@"Cleaning up background task...");
UIApplication* app = [UIApplication sharedApplication];
if ([app respondsToSelector:@selector(beginBackgroundTaskWithExpirationHandler:)]) {
[app endBackgroundTask:_backgroundTaskIdentifier];
@@ -101,6 +108,8 @@
_username = nil;
[_password release];
_password = nil;
[_cache release];
_cache = nil;
// Cleanup a background task if there is any
[self cleanupBackgroundTask];
@@ -147,6 +156,13 @@
CFRelease(dummyRequest);
CFRelease(authorizationString);
}
if (self.cachePolicy & RKRequestCachePolicyEtag) {
NSString* etag = [self.cache etagForRequest:self];
if (etag) {
[_URLRequest setValue:etag forHTTPHeaderField:@"If-None-Match"];
}
}
}
// Setup the NSURLRequest. The request must be prepared right before dispatching
@@ -215,7 +231,16 @@
- (void)sendAsynchronously {
_sentSynchronously = NO;
if ([self shouldDispatchRequest]) {
if (self.cachePolicy & RKRequestCachePolicyEnabled) {
if ([self.cache hasResponseForRequest:self]) {
NSLog(@"Found cached content, loading...");
_isLoading = YES;
[self didFinishLoad:[self.cache responseForRequest:self]];
return;
}
}
if ([self shouldDispatchRequest]) {
#if TARGET_OS_IPHONE
// Background Request Policy support
UIApplication* app = [UIApplication sharedApplication];
@@ -252,12 +277,20 @@
[self fireAsynchronousRequest];
#endif
} else {
NSString* errorMessage = [NSString stringWithFormat:@"The client is unable to contact the resource at %@", [[self URL] absoluteString]];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
errorMessage, NSLocalizedDescriptionKey,
nil];
NSError* error = [NSError errorWithDomain:RKRestKitErrorDomain code:RKRequestBaseURLOfflineError userInfo:userInfo];
[self didFailLoadWithError:error];
if (_cachePolicy & RKRequestCachePolicyLoadIfOffline &&
[self.cache hasResponseForRequest:self]) {
_isLoading = YES;
[self didFinishLoad:[self.cache responseForRequest:self]];
} else {
NSString* errorMessage = [NSString stringWithFormat:@"The client is unable to contact the resource at %@", [[self URL] absoluteString]];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
errorMessage, NSLocalizedDescriptionKey,
nil];
NSError* error = [NSError errorWithDomain:RKRestKitErrorDomain code:RKRequestBaseURLOfflineError userInfo:userInfo];
[self didFailLoadWithError:error];
}
}
}
@@ -291,16 +324,24 @@
} else {
[self didFinishLoad:response];
}
} else {
NSString* errorMessage = [NSString stringWithFormat:@"The client is unable to contact the resource at %@", [[self URL] absoluteString]];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
errorMessage, NSLocalizedDescriptionKey,
nil];
error = [NSError errorWithDomain:RKRestKitErrorDomain code:RKRequestBaseURLOfflineError userInfo:userInfo];
[self didFailLoadWithError:error];
if (_cachePolicy & RKRequestCachePolicyLoadIfOffline &&
[self.cache hasResponseForRequest:self]) {
// TODO: Is this needed here? Or can we just return a nil response and everyone will be happy??
response = [[[RKResponse alloc] initWithSynchronousRequest:self URLResponse:URLResponse body:payload error:error] autorelease];
response = [self.cache responseForRequest:self];
} else {
NSString* errorMessage = [NSString stringWithFormat:@"The client is unable to contact the resource at %@", [[self URL] absoluteString]];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
errorMessage, NSLocalizedDescriptionKey,
nil];
error = [NSError errorWithDomain:RKRestKitErrorDomain code:RKRequestBaseURLOfflineError userInfo:userInfo];
[self didFailLoadWithError:error];
// TODO: Is this needed here? Or can we just return a nil response and everyone will be happy??
response = [[[RKResponse alloc] initWithSynchronousRequest:self URLResponse:URLResponse body:payload error:error] autorelease];
}
}
return response;
@@ -310,32 +351,50 @@
[self cancelAndInformDelegate:YES];
}
// TODO: Isn't this code duplicated higher up???
- (void)didFailLoadWithError:(NSError*)error {
_isLoading = NO;
if (_cachePolicy & RKRequestCachePolicyLoadOnError &&
[self.cache hasResponseForRequest:self]) {
if ([_delegate respondsToSelector:@selector(request:didFailLoadWithError:)]) {
[_delegate request:self didFailLoadWithError:error];
[self didFinishLoad:[self.cache responseForRequest:self]];
} else {
_isLoading = NO;
if ([_delegate respondsToSelector:@selector(request:didFailLoadWithError:)]) {
[_delegate request:self didFailLoadWithError:error];
}
[[NSNotificationCenter defaultCenter] postNotificationName:RKRequestFailedWithErrorNotification object:self userInfo:nil];
}
[[NSNotificationCenter defaultCenter] postNotificationName:RKRequestFailedWithErrorNotification object:self userInfo:nil];
}
- (void)didFinishLoad:(RKResponse*)response {
_isLoading = NO;
_isLoaded = YES;
RKLOG_NETWORK(RKLogLevelInfo, @"Status Code: %d", [response statusCode]);
RKLOG_NETWORK(RKLogLevelInfo, @"Body: %@", [response bodyAsString]);
if ([_delegate respondsToSelector:@selector(request:didLoadResponse:)]) {
[_delegate request:self didLoadResponse:response];
}
RKResponse* finalResponse = response;
if ((_cachePolicy & RKRequestCachePolicyEtag) && [response isNotModified]) {
finalResponse = [self.cache responseForRequest:self];
}
if (![response wasLoadedFromCache] && [response isSuccessful] && (_cachePolicy != RKRequestCachePolicyNone)) {
[self.cache storeResponse:response forRequest:self];
}
if ([_delegate respondsToSelector:@selector(request:didLoadResponse:)]) {
[_delegate request:self didLoadResponse:finalResponse];
}
if ([response isServiceUnavailable]) {
[[NSNotificationCenter defaultCenter] postNotificationName:RKServiceDidBecomeUnavailableNotification object:self];
}
NSDictionary* userInfo = [NSDictionary dictionaryWithObject:response forKey:@"response"];
[[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidLoadResponseNotification object:self userInfo:userInfo];
NSDictionary* userInfo = [NSDictionary dictionaryWithObject:finalResponse forKey:@"response"];
[[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidLoadResponseNotification object:self userInfo:userInfo];
// NOTE: This notification must be posted last as the request queue releases the request when it
// receives the notification
@@ -392,4 +451,12 @@
#endif
}
- (NSString*)cacheKey {
if (_method == RKRequestMethodDELETE) {
return nil;
}
NSString* compositCacheKey = [NSString stringWithFormat:@"%@-%d-%@", self.URL, _method, [_URLRequest HTTPBody]];
return [compositCacheKey MD5];
}
@end

View File

@@ -0,0 +1,57 @@
//
// RKRequestCache.h
// RestKit
//
// Created by Jeff Arena on 4/4/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
#import "RKRequest.h"
#import "RKResponse.h"
/**
* Storage policy. Determines if we clear the cache out when the app is shut down.
* Cache instance needs to register for
*/
typedef enum {
RKRequestCacheStoragePolicyDisabled, // The cache has been disabled. Attempts to store data will silently fail
RKRequestCacheStoragePolicyForDurationOfSession, // Cache data for the length of the session. Clear cache at app exit.
RKRequestCacheStoragePolicyPermanently // Cache data permanently, until explicitly expired or flushed
} RKRequestCacheStoragePolicy;
@interface RKRequestCache : NSObject {
NSString* _cachePath;
RKRequestCacheStoragePolicy _storagePolicy;
NSRecursiveLock* _cacheLock;
}
@property (nonatomic, readonly) NSString* cachePath; // Full path to the cache
@property (nonatomic, assign) RKRequestCacheStoragePolicy storagePolicy; // User can change storage policy.
+ (NSDateFormatter*)rfc1123DateFormatter;
- (id)initWithCachePath:(NSString*)cachePath storagePolicy:(RKRequestCacheStoragePolicy)storagePolicy;
- (NSString*)pathForRequest:(RKRequest*)request;
- (BOOL)hasResponseForRequest:(RKRequest*)request;
- (void)storeResponse:(RKResponse*)response forRequest:(RKRequest*)request;
//- (void)storeResponse:(RKResponse*)response forRequest:(RKRequest*)request expires:(NSTimeInterval)expirationAge;
- (RKResponse*)responseForRequest:(RKRequest*)request;
- (NSDictionary*)headersForRequest:(RKRequest*)request;
- (NSString*)etagForRequest:(RKRequest*)request;
- (void)invalidateRequest:(RKRequest*)request;
- (void)invalidateWithStoragePolicy:(RKRequestCacheStoragePolicy)storagePolicy;
- (void)invalidateAll;
@end

View File

@@ -0,0 +1,285 @@
//
// RKRequestCache.m
// RestKit
//
// Created by Jeff Arena on 4/4/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
#import "RKRequestCache.h"
static NSString* sessionCacheFolder = @"SessionStore";
static NSString* permanentCacheFolder = @"PermanentStore";
static NSString* headersExtension = @"headers";
static NSString* cacheDateHeaderKey = @"X-RESTKIT-CACHEDATE";
NSString* cacheResponseCodeKey = @"X-RESTKIT-CACHED-RESPONSE-CODE";
NSString* cacheMIMETypeKey = @"X-RESTKIT-CACHED-MIME-TYPE";
NSString* cacheURLKey = @"X-RESTKIT-CACHED-URL";
static NSDateFormatter* __rfc1123DateFormatter;
@implementation RKRequestCache
@synthesize storagePolicy = _storagePolicy;
+ (NSDateFormatter*)rfc1123DateFormatter {
if (__rfc1123DateFormatter == nil) {
__rfc1123DateFormatter = [[NSDateFormatter alloc] init];
[__rfc1123DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
[__rfc1123DateFormatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss 'GMT'"];
}
return __rfc1123DateFormatter;
}
- (id)initWithCachePath:(NSString*)cachePath storagePolicy:(RKRequestCacheStoragePolicy)storagePolicy {
if ((self = [super init])) {
_cachePath = [cachePath copy];
_cacheLock = [[NSRecursiveLock alloc] init];
NSFileManager* fileManager = [[NSFileManager alloc] init];
NSArray* pathArray = [NSArray arrayWithObjects:
_cachePath,
[_cachePath stringByAppendingPathComponent:sessionCacheFolder],
[_cachePath stringByAppendingPathComponent:permanentCacheFolder],
nil];
for (NSString* path in pathArray) {
BOOL isDirectory = NO;
BOOL fileExists = [fileManager fileExistsAtPath:path isDirectory:&isDirectory];
if (!fileExists) {
NSError* error = nil;
BOOL created = [fileManager createDirectoryAtPath:path
withIntermediateDirectories:NO
attributes:nil
error:&error];
if (!created || error != nil) {
NSLog(@"[RestKit] RKRequestCache: Failed to create cache directory at %@: error %@",
path, [error localizedDescription]);
}
} else if (!isDirectory) {
NSLog(@"[RestKit] RKRequestCache: Failed to create cache directory: Directory already exists: %@", path);
}
}
[fileManager release];
self.storagePolicy = storagePolicy;
}
return self;
}
- (void)dealloc {
[_cachePath release];
_cachePath = nil;
[_cacheLock release];
_cacheLock = nil;
[super dealloc];
}
- (NSString*)cachePath {
return _cachePath;
}
- (NSString*)pathForRequest:(RKRequest*)request {
[_cacheLock lock];
NSString* pathForRequest = nil;
if (_storagePolicy == RKRequestCacheStoragePolicyForDurationOfSession) {
pathForRequest = [[_cachePath stringByAppendingPathComponent:sessionCacheFolder]
stringByAppendingPathComponent:[request cacheKey]];
} else if (_storagePolicy == RKRequestCacheStoragePolicyPermanently) {
pathForRequest = [[_cachePath stringByAppendingPathComponent:permanentCacheFolder]
stringByAppendingPathComponent:[request cacheKey]];
}
[_cacheLock unlock];
return pathForRequest;
}
- (BOOL)hasResponseForRequest:(RKRequest*)request {
[_cacheLock lock];
BOOL hasEntryForRequest = NO;
NSFileManager* fileManager = [[NSFileManager alloc] init];
NSString* cachePath = [self pathForRequest:request];
hasEntryForRequest = ([fileManager fileExistsAtPath:cachePath] &&
[fileManager fileExistsAtPath:
[cachePath stringByAppendingPathExtension:headersExtension]]);
[fileManager release];
[_cacheLock unlock];
return hasEntryForRequest;
}
- (void)storeResponse:(RKResponse*)response forRequest:(RKRequest*)request {
[_cacheLock lock];
if ([self hasResponseForRequest:request]) {
[self invalidateRequest:request];
}
if (_storagePolicy != RKRequestCacheStoragePolicyDisabled) {
NSString* cachePath = [self pathForRequest:request];
if (cachePath) {
NSData* body = response.body;
if (body) {
[body writeToFile:cachePath atomically:NO];
}
NSMutableDictionary* headers = [response.allHeaderFields mutableCopy];
if (headers) {
// TODO: exponse this?
NSHTTPURLResponse* urlResponse = [response valueForKey:@"_httpURLResponse"];
// Cache Loaded Time
[headers setObject:[[RKRequestCache rfc1123DateFormatter] stringFromDate:[NSDate date]]
forKey:cacheDateHeaderKey];
// Cache status code
[headers setObject:[NSNumber numberWithInt:urlResponse.statusCode]
forKey:cacheResponseCodeKey];
// Cache MIME Type
[headers setObject:urlResponse.MIMEType
forKey:cacheMIMETypeKey];
// // Cache URL
[headers setObject:[urlResponse.URL absoluteString]
forKey:cacheURLKey];
// Save
[headers writeToFile:[cachePath stringByAppendingPathExtension:headersExtension]
atomically:YES];
}
[headers release];
}
}
[_cacheLock unlock];
}
- (RKResponse*)responseForRequest:(RKRequest*)request {
[_cacheLock lock];
RKResponse* response = nil;
NSString* cachePath = [self pathForRequest:request];
if (cachePath) {
NSData* responseData = [NSData dataWithContentsOfFile:cachePath];
NSDictionary* responseHeaders = [NSDictionary dictionaryWithContentsOfFile:
[cachePath stringByAppendingPathExtension:headersExtension]];
response = [[[RKResponse alloc] initWithRequest:request body:responseData headers:responseHeaders] autorelease];
}
[_cacheLock unlock];
return response;
}
- (NSDictionary*)headersForRequest:(RKRequest*)request {
NSString* cachePath = [self pathForRequest:request];
[_cacheLock lock];
NSDictionary* headers = nil;
if (cachePath) {
headers = [NSDictionary dictionaryWithContentsOfFile:
[cachePath stringByAppendingPathExtension:headersExtension]];
}
[_cacheLock unlock];
return headers;
}
- (NSString*)etagForRequest:(RKRequest*)request {
NSString* etag = nil;
NSDictionary* responseHeaders = [self headersForRequest:request];
[_cacheLock lock];
if (responseHeaders) {
for (NSString* responseHeader in responseHeaders) {
if ([[responseHeader uppercaseString] isEqualToString:[@"ETag" uppercaseString]]) {
etag = [responseHeaders objectForKey:responseHeader];
}
}
}
[_cacheLock unlock];
return etag;
}
- (void)invalidateRequest:(RKRequest*)request {
[_cacheLock lock];
NSString* cachePath = [self pathForRequest:request];
if (cachePath) {
NSFileManager* fileManager = [[NSFileManager alloc] init];
[fileManager removeItemAtPath:cachePath error:NULL];
[fileManager removeItemAtPath:[cachePath stringByAppendingPathExtension:headersExtension]
error:NULL];
[fileManager release];
}
[_cacheLock unlock];
}
- (void)invalidateWithStoragePolicy:(RKRequestCacheStoragePolicy)storagePolicy {
[_cacheLock lock];
if (_cachePath && storagePolicy != RKRequestCacheStoragePolicyDisabled) {
NSString* cachePath = nil;
if (storagePolicy == RKRequestCacheStoragePolicyForDurationOfSession) {
cachePath = [_cachePath stringByAppendingPathComponent:sessionCacheFolder];
} else {
cachePath = [_cachePath stringByAppendingPathComponent:permanentCacheFolder];
}
NSFileManager* fileManager = [[NSFileManager alloc] init];
BOOL isDirectory = NO;
BOOL fileExists = [fileManager fileExistsAtPath:cachePath isDirectory:&isDirectory];
if (fileExists && isDirectory) {
NSError* error = nil;
NSArray* cacheEntries = [fileManager contentsOfDirectoryAtPath:cachePath error:&error];
if (nil == error) {
for (NSString* cacheEntry in cacheEntries) {
NSString* cacheEntryPath = [cachePath stringByAppendingPathComponent:cacheEntry];
[fileManager removeItemAtPath:cacheEntryPath error:&error];
[fileManager removeItemAtPath:[cacheEntryPath stringByAppendingPathExtension:headersExtension]
error:&error];
if (nil != error) {
NSLog(@"[RestKit] RKRequestCache: Failed to delete cache entry for file: %@", cacheEntryPath);
}
}
} else {
NSLog(@"[RestKit] RKRequestCache: Failed to fetch list of cache entries for cache path: %@", cachePath);
}
}
[fileManager release];
}
[_cacheLock unlock];
}
- (void)invalidateAll {
[_cacheLock lock];
[self invalidateWithStoragePolicy:RKRequestCacheStoragePolicyForDurationOfSession];
[self invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently];
[_cacheLock unlock];
}
- (void)setStoragePolicy:(RKRequestCacheStoragePolicy)storagePolicy {
[self invalidateWithStoragePolicy:RKRequestCacheStoragePolicyForDurationOfSession];
if (storagePolicy == RKRequestCacheStoragePolicyDisabled) {
[self invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently];
}
_storagePolicy = storagePolicy;
}
@end

View File

@@ -70,14 +70,17 @@ static const NSTimeInterval kFlushDelay = 0.3;
name:RKRequestFailedWithErrorNotification
object:nil];
#if TARGET_OS_IPHONE
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(willTransitionToBackground)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(willTransitionToForeground)
name:UIApplicationWillEnterForegroundNotification
object:nil];
BOOL backgroundOK = &UIApplicationDidEnterBackgroundNotification != NULL;
if (backgroundOK) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(willTransitionToBackground)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(willTransitionToForeground)
name:UIApplicationWillEnterForegroundNotification
object:nil];
}
#endif
}
return self;
@@ -145,9 +148,9 @@ static const NSTimeInterval kFlushDelay = 0.3;
}
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
_queueTimer = nil;
NSArray* requestsCopy = [NSArray arrayWithArray:_requests];
for (RKRequest* request in requestsCopy) {
if (![request isLoading] && ![request isLoaded] && self.loadingCount < _concurrentRequestsLimit) {
@@ -187,7 +190,7 @@ static const NSTimeInterval kFlushDelay = 0.3;
}
_suspended = isSuspended;
if (!_suspended) {
[self loadNextInQueue];
} else if (_queueTimer) {

View File

@@ -14,6 +14,8 @@
NSHTTPURLResponse* _httpURLResponse;
NSMutableData* _body;
NSError* _failureError;
BOOL _loading;
NSDictionary* _responseHeaders;
}
/**
@@ -61,6 +63,11 @@
*/
- (id)initWithRequest:(RKRequest*)request;
/**
* Initialize a new response object from a cached request
*/
- (id)initWithRequest:(RKRequest*)request body:(NSData*)body headers:(NSDictionary*)headers;
/**
* Initializes a response object from the results of a synchronous request
*/
@@ -86,6 +93,11 @@
*/
- (NSString*)failureErrorDescription;
/**
* Indicates whether the response was loaded from RKCache
*/
- (BOOL)wasLoadedFromCache;
/**
* Indicates that the connection failed to reach the remote server. The details of the failure
* are available on the failureError reader.
@@ -137,6 +149,11 @@
*/
- (BOOL)isCreated;
/**
* Indicates an HTTP response code of 304
*/
- (BOOL)isNotModified;
/**
* Indicates an HTTP response code of 401
*/

View File

@@ -10,6 +10,10 @@
#import "RKNotifications.h"
#import "RKNetwork.h"
extern NSString* cacheResponseCodeKey;
extern NSString* cacheMIMETypeKey;
extern NSString* cacheURLKey;
@implementation RKResponse
@synthesize body = _body, request = _request, failureError = _failureError;
@@ -19,6 +23,8 @@
if (self) {
_body = [[NSMutableData alloc] init];
_failureError = nil;
_loading = NO;
_responseHeaders = nil;
}
return self;
@@ -35,6 +41,18 @@
return self;
}
- (id)initWithRequest:(RKRequest*)request body:(NSData*)body headers:(NSDictionary*)headers {
self = [self initWithRequest:request];
if (self) {
[_body release];
_body = nil;
_body = [body retain];
_responseHeaders = [headers retain];
}
return self;
}
- (id)initWithSynchronousRequest:(RKRequest*)request URLResponse:(NSURLResponse*)URLResponse body:(NSData*)body error:(NSError*)error {
self = [super init];
if (self) {
@@ -44,6 +62,7 @@
_httpURLResponse = [URLResponse retain];
_failureError = [error retain];
_body = [body retain];
_loading = NO;
}
return self;
@@ -51,8 +70,13 @@
- (void)dealloc {
[_httpURLResponse release];
_httpURLResponse = nil;
[_body release];
_body = nil;
[_failureError release];
_failureError = nil;
[_responseHeaders release];
_responseHeaders = nil;
[super dealloc];
}
@@ -75,6 +99,8 @@
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse *)response {
NSLog(@"NSHTTPURLResponse Status Code: %d", [response statusCode]);
NSLog(@"Headers: %@", [response allHeaderFields]);
_httpURLResponse = [response retain];
}
@@ -104,6 +130,10 @@
return [NSHTTPURLResponse localizedStringForStatusCode:[self statusCode]];
}
- (NSData*)body {
return _body;
}
- (NSString*)bodyAsString {
return [[[NSString alloc] initWithData:self.body encoding:NSUTF8StringEncoding] autorelease];
}
@@ -123,19 +153,35 @@
}
}
- (BOOL)wasLoadedFromCache {
return (_responseHeaders != nil);
}
- (NSURL*)URL {
if ([self wasLoadedFromCache]) {
return [NSURL URLWithString:[_responseHeaders valueForKey:cacheURLKey]];
}
return [_httpURLResponse URL];
}
- (NSString*)MIMEType {
if ([self wasLoadedFromCache]) {
return [_responseHeaders valueForKey:cacheMIMETypeKey];
}
return [_httpURLResponse MIMEType];
}
- (NSInteger)statusCode {
if ([self wasLoadedFromCache]) {
return [[_responseHeaders valueForKey:cacheResponseCodeKey] intValue];
}
return [_httpURLResponse statusCode];
}
- (NSDictionary*)allHeaderFields {
if ([self wasLoadedFromCache]) {
return _responseHeaders;
}
return [_httpURLResponse allHeaderFields];
}
@@ -156,7 +202,7 @@
}
- (BOOL)isSuccessful {
return ([self statusCode] >= 200 && [self statusCode] < 300);
return (([self statusCode] >= 200 && [self statusCode] < 300) || ([self wasLoadedFromCache]));
}
- (BOOL)isRedirection {
@@ -183,6 +229,10 @@
return ([self statusCode] == 201);
}
- (BOOL)isNotModified {
return ([self statusCode] == 304);
}
- (BOOL)isUnauthorized {
return ([self statusCode] == 401);
}
@@ -225,23 +275,30 @@
- (BOOL)isHTML {
NSString* contentType = [self contentType];
return contentType && ([contentType rangeOfString:@"text/html" options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0 ||
[self isXHTML]);
return (contentType && ([contentType rangeOfString:@"text/html"
options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0 ||
[self isXHTML]));
}
- (BOOL)isXHTML {
NSString* contentType = [self contentType];
return contentType && [contentType rangeOfString:@"application/xhtml+xml" options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0;
return (contentType &&
[contentType rangeOfString:@"application/xhtml+xml"
options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0);
}
- (BOOL)isXML {
NSString* contentType = [self contentType];
return contentType && [contentType rangeOfString:@"application/xml" options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0;
return (contentType &&
[contentType rangeOfString:@"application/xml"
options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0);
}
- (BOOL)isJSON {
NSString* contentType = [self contentType];
return contentType && [contentType rangeOfString:@"application/json" options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0;
return (contentType &&
[contentType rangeOfString:@"application/json"
options:NSCaseInsensitiveSearch|NSAnchoredSearch].length > 0);
}
@end

View File

@@ -32,7 +32,7 @@
_objectManager = objectManager;
[self.objectManager.client setupRequest:self];
}
return self;
}
@@ -130,6 +130,15 @@
mapper.delegate = self;
RKObjectMappingResult* result = [mapper performMapping];
if (nil == result && RKRequestMethodDELETE == self.method && [mapper.errors count] == 1) {
NSError* error = [mapper.errors objectAtIndex:0];
if (error.domain == RKRestKitErrorDomain && error.code == RKObjectMapperErrorUnmappableContent) {
// If this is a delete request, and the error is an "unmappable content" error, return an empty result
// because delete requests should allow for no objects to come back in the response (you just deleted the object).
result = [[[RKObjectMappingResult alloc] initWithDictionary:[NSDictionary dictionary]] autorelease];
}
}
if (nil == result) {
// TODO: Logging macros
NSLog(@"GOT MAPPING ERRORS: %@", mapper.errors);
@@ -239,13 +248,19 @@
- (void)didFailLoadWithError:(NSError*)error {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
if ([_delegate respondsToSelector:@selector(request:didFailLoadWithError:)]) {
[_delegate request:self didFailLoadWithError:error];
}
[(NSObject<RKObjectLoaderDelegate>*)_delegate objectLoader:self didFailWithError:error];
[self finalizeLoad:NO];
if (_cachePolicy & RKRequestCachePolicyLoadOnError &&
[[[RKClient sharedClient] cache] hasResponseForRequest:self]) {
[self didFinishLoad:[[[RKClient sharedClient] cache] responseForRequest:self]];
} else {
if ([_delegate respondsToSelector:@selector(request:didFailLoadWithError:)]) {
[_delegate request:self didFailLoadWithError:error];
}
[(NSObject<RKObjectLoaderDelegate>*)_delegate objectLoader:self didFailWithError:error];
[self finalizeLoad:NO];
}
[pool release];
}
@@ -253,7 +268,17 @@
// NOTE: We do NOT call super here. We are overloading the default behavior from RKRequest
- (void)didFinishLoad:(RKResponse*)response {
_response = [response retain];
if ((_cachePolicy & RKRequestCachePolicyEtag) && [response isNotModified]) {
[_response release];
_response = nil;
_response = [[[[RKClient sharedClient] cache] responseForRequest:self] retain];
}
if (![_response wasLoadedFromCache] && [_response isSuccessful] && (_cachePolicy != RKRequestCachePolicyNone)) {
[[[RKClient sharedClient] cache] storeResponse:_response forRequest:self];
}
if ([_delegate respondsToSelector:@selector(request:didLoadResponse:)]) {
[_delegate request:self didLoadResponse:response];
}

View File

@@ -139,7 +139,6 @@
[self addErrorWithCode:RKObjectMapperErrorObjectMappingTypeMismatch message:errorMessage keyPath:keyPath userInfo:nil];
return nil;
}
for (id mappableObject in mappableObjects) {
id destinationObject = [self objectWithMapping:mapping andData:mappableObject];
BOOL success = [self mapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping];

View File

@@ -1,19 +1,19 @@
//
// RKRequestFilterableTTModel.h
// RKFilterableObjectLoaderTTModel.h
// RestKit
//
// Created by Blake Watters on 2/12/10.
// Copyright 2010 Two Toasters. All rights reserved.
//
#import "RKRequestTTModel.h"
#import "RKObjectLoaderTTModel.h"
#import "../Support/RKSearchEngine.h"
/**
* Provides an interface for searching and filtering a collection
* of objects loaded from a remote source
*/
@interface RKRequestFilterableTTModel : RKRequestTTModel {
@interface RKFilterableObjectLoaderTTModel : RKObjectLoaderTTModel {
RKSearchEngine* _searchEngine;
NSPredicate* _predicate;
NSArray* _sortDescriptors;

View File

@@ -1,14 +1,14 @@
//
// RKRequestFilterableTTModel.m
// RKFilterableObjectLoaderTTModel.m
// RestKit
//
// Created by Blake Watters on 2/12/10.
// Copyright 2010 Two Toasters. All rights reserved.
//
#import "RKRequestFilterableTTModel.h"
#import "RKFilterableObjectLoaderTTModel.h"
@implementation RKRequestFilterableTTModel
@implementation RKFilterableObjectLoaderTTModel
@synthesize searchEngine = _searchEngine;
@synthesize predicate = _predicate;

View File

@@ -0,0 +1,20 @@
//
// RKMappableObjectTableItem.h
// RestKit
//
// Created by Blake Watters on 4/26/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
#import <Three20/Three20.h>
#import "../RestKit.h"
@interface RKMappableObjectTableItem : TTTableLinkedItem {
NSObject<RKObjectMappable>* _object;
}
@property (nonatomic, retain) NSObject<RKObjectMappable>* object;
+ (id)itemWithObject:(NSObject<RKObjectMappable>*)object;
@end

View File

@@ -0,0 +1,21 @@
//
// RKMappableObjectTableItem.m
// RestKit
//
// Created by Blake Watters on 4/26/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
#import "RKMappableObjectTableItem.h"
@implementation RKMappableObjectTableItem
@synthesize object = _object;
+ (id)itemWithObject:(NSObject<RKObjectMappable>*)object {
RKMappableObjectTableItem* tableItem = [self new];
tableItem.object = object;
return [tableItem autorelease];
}
@end

View File

@@ -1,5 +1,5 @@
//
// RKRequestTTModel.h
// RKObjectLoaderTTModel.h
// RestKit
//
// Created by Blake Watters on 2/9/10.
@@ -10,64 +10,23 @@
#import "../RestKit.h"
/**
* Generic class for loading a remote model using a RestKit request and supplying the model to a
* Generic class for loading a remote model using a RestKit object loader and supplying the model to a
* TTListDataSource subclass
*/
@interface RKRequestTTModel : TTModel <RKObjectLoaderDelegate> {
@interface RKObjectLoaderTTModel : TTModel <RKObjectLoaderDelegate> {
NSArray *_objects;
BOOL _isLoaded;
BOOL _isLoading;
BOOL _cacheLoaded;
BOOL _emptyReloadAttempted;
NSString* _resourcePath;
NSDictionary* _params;
RKRequestMethod _method;
Class _objectClass;
NSString* _keyPath;
RKObjectLoader* _objectLoader;
NSTimeInterval _refreshRate;
}
/**
* Domain objects loaded via this model
*/
@property (nonatomic, readonly) NSArray* objects;
/**
* The resourcePath used to create this model
*/
@property (nonatomic, readonly) NSString* resourcePath;
/**
* The NSDate object representing the last time this model was loaded.
*/
@property (nonatomic, readonly) NSDate* loadedTime;
/**
* Request parameters
*/
@property (nonatomic, retain) NSDictionary* params;
/**
* The HTTP method to load the models with. Defaults to RKRequestMethodGET
*/
@property (nonatomic, assign) RKRequestMethod method;
/**
* The rate at which this model should be refreshed after initial load.
* Defaults to the value returned by + (NSTimeInterval)defaultRefreshRate.
*/
@property (assign) NSTimeInterval refreshRate;
/**
* Init methods for creating new models
*/
- (id)initWithResourcePath:(NSString*)resourcePath;
- (id)initWithResourcePath:(NSString*)resourcePath params:(NSDictionary*)params;
- (id)initWithResourcePath:(NSString*)resourcePath params:(NSDictionary*)params objectClass:(Class)klass;
- (id)initWithResourcePath:(NSString*)resourcePath params:(NSDictionary*)params objectClass:(Class)klass keyPath:(NSString*)keyPath;
- (id)initWithResourcePath:(NSString*)resourcePath params:(NSDictionary*)params method:(RKRequestMethod)method;
/////////////////////////////////////////////////////////////////////////
/// @name Global Configuration
/////////////////////////////////////////////////////////////////////////
/**
* The NSDate representing the first time the app was run. This defaultLoadedTime
@@ -90,4 +49,44 @@
*/
+ (void)setDefaultRefreshRate:(NSTimeInterval)newDefaultRefreshRate;
/////////////////////////////////////////////////////////////////////////
/// @name Accessing Model Data
/////////////////////////////////////////////////////////////////////////
/**
* Domain objects loaded via this model
*/
@property (nonatomic, readonly) NSArray* objects;
/**
* The object loader responsible for loading the data accessed via this model
*/
@property (nonatomic, readonly) RKObjectLoader* objectLoader;
/**
* The NSDate object representing the last time this model was loaded.
*/
@property (nonatomic, readonly) NSDate* loadedTime;
/**
* The rate at which this model should be refreshed after initial load.
* Defaults to the value returned by + (NSTimeInterval)defaultRefreshRate.
*/
@property (assign) NSTimeInterval refreshRate;
+ (RKObjectLoaderTTModel*)modelWithObjectLoader:(RKObjectLoader*)objectLoader;
- (id)initWithObjectLoader:(RKObjectLoader*)objectLoader;
/**
* Load the model
*/
- (void)load;
//// TODO: Does this work?
//// Loads a new object loader
//- (void)changeModelWithObjectLoader:(RKObjectLoader*)objectLoader;
// [model didChange]
//
//- (void)filterWithPredicate:(NSPredicate*)predicate sortDescriptors:(NSArray*)sortDescriptors;
//- (void)searchWithText:(NSString*)text predicate: sortDescriptors:
@end

View File

@@ -1,12 +1,12 @@
//
// RKRequestTTModel.m
// RKObjectLoaderTTModel.m
// RestKit
//
// Created by Blake Watters on 2/9/10.
// Copyright 2010 Two Toasters. All rights reserved.
//
#import "RKRequestTTModel.h"
#import "RKObjectLoaderTTModel.h"
#import "RKManagedObjectStore.h"
#import "NSManagedObject+ActiveRecord.h"
#import "../Network/Network.h"
@@ -15,25 +15,22 @@
static NSTimeInterval defaultRefreshRate = NSTimeIntervalSince1970;
static NSString* const kDefaultLoadedTimeKey = @"RKRequestTTModelDefaultLoadedTimeKey";
@interface RKRequestTTModel (Private)
@interface RKObjectLoaderTTModel (Private)
@property (nonatomic, readonly) NSString* resourcePath;
- (void)clearLoadedTime;
- (void)saveLoadedTime;
- (BOOL)errorWarrantsOptionToGoOffline:(NSError*)error;
- (void)showAlertWithOptionToGoOfflineForError:(NSError*)error;
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex;
- (void)modelsDidLoad:(NSArray*)models;
- (void)load;
@end
@implementation RKRequestTTModel
@implementation RKObjectLoaderTTModel
@synthesize objects = _objects;
@synthesize resourcePath = _resourcePath;
@synthesize params = _params;
@synthesize method = _method;
@synthesize objectLoader = _objectLoader;
@synthesize refreshRate = _refreshRate;
+ (NSDate*)defaultLoadedTime {
@@ -54,44 +51,25 @@ static NSString* const kDefaultLoadedTimeKey = @"RKRequestTTModelDefaultLoadedTi
defaultRefreshRate = newDefaultRefreshRate;
}
- (id)initWithResourcePath:(NSString*)resourcePath {
+ (RKObjectLoaderTTModel*)modelWithObjectLoader:(RKObjectLoader*)objectLoader {
return [[[self alloc] initWithObjectLoader:objectLoader] autorelease];
}
- (id)initWithObjectLoader:(RKObjectLoader*)objectLoader {
self = [self init];
if (self) {
_resourcePath = [resourcePath retain];
}
return self;
if (self) {
// TODO: When allowing mutation of object loader, be sure to update...
_objectLoader = [objectLoader retain];
_objectLoader.delegate = self;
}
return self;
}
- (id)initWithResourcePath:(NSString*)resourcePath params:(NSDictionary*)params {
self = [self initWithResourcePath:resourcePath];
if (self) {
self.params = [params retain];
}
return self;
}
- (id)initWithResourcePath:(NSString*)resourcePath params:(NSDictionary*)params objectClass:(Class)klass {
self = [self initWithResourcePath:resourcePath params:params];
if (self) {
_objectClass = [klass retain];
}
return self;
}
- (id)initWithResourcePath:(NSString*)resourcePath params:(NSDictionary*)params objectClass:(Class)klass keyPath:(NSString*)keyPath {
self = [self initWithResourcePath:resourcePath params:params objectClass:klass];
if (self) {
_keyPath = [keyPath retain];
}
return self;
}
- (id)initWithResourcePath:(NSString*)resourcePath params:(NSDictionary*)params method:(RKRequestMethod)method {
self = [self initWithResourcePath:resourcePath params:params];
if (self) {
_method = method;
}
return self;
// TODO: Does this work?
// Loads a new object loader
- (void)updateModelWithObjectLoader:(RKObjectLoader*)objectLoader {
}
///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -100,14 +78,11 @@ static NSString* const kDefaultLoadedTimeKey = @"RKRequestTTModelDefaultLoadedTi
- (id)init {
self = [super init];
if (self) {
self.method = RKRequestMethodGET;
self.refreshRate = [RKRequestTTModel defaultRefreshRate];
self.params = nil;
self.refreshRate = [RKObjectLoaderTTModel defaultRefreshRate];
_cacheLoaded = NO;
_objects = nil;
_isLoaded = NO;
_isLoading = NO;
_resourcePath = nil;
_emptyReloadAttempted = NO;
}
return self;
@@ -117,19 +92,19 @@ static NSString* const kDefaultLoadedTimeKey = @"RKRequestTTModelDefaultLoadedTi
[[RKRequestQueue sharedQueue] cancelRequestsWithDelegate:self];
[_objects release];
_objects = nil;
[_resourcePath release];
_resourcePath = nil;
[_objectClass release];
_objectClass = nil;
[_keyPath release];
_keyPath = nil;
self.params = nil;
[_objectLoader release];
_objectLoader = nil;
[super dealloc];
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// TTModel
- (NSString*)resourcePath {
return self.objectLoader.resourcePath;
}
- (BOOL)isLoaded {
return _isLoaded;
}
@@ -146,7 +121,11 @@ static NSString* const kDefaultLoadedTimeKey = @"RKRequestTTModelDefaultLoadedTi
NSTimeInterval sinceNow = [self.loadedTime timeIntervalSinceNow];
if (![self isLoading] && !_emptyReloadAttempted && _objects && [_objects count] == 0) {
_emptyReloadAttempted = YES;
return YES;
// TODO: Returning YES from here causes the view to enter an infinite
// loading state if you switch data sources
// return YES;
return NO;
}
return (![self isLoading] && (-sinceNow > _refreshRate));
}
@@ -166,9 +145,9 @@ static NSString* const kDefaultLoadedTimeKey = @"RKRequestTTModelDefaultLoadedTi
}
- (NSDate*)loadedTime {
NSDate* loadedTime = [[NSUserDefaults standardUserDefaults] objectForKey:_resourcePath];
NSDate* loadedTime = [[NSUserDefaults standardUserDefaults] objectForKey:self.resourcePath];
if (loadedTime == nil) {
return [RKRequestTTModel defaultLoadedTime];
return [RKObjectLoaderTTModel defaultLoadedTime];
}
return loadedTime;
}
@@ -186,9 +165,6 @@ static NSString* const kDefaultLoadedTimeKey = @"RKRequestTTModelDefaultLoadedTi
- (void)objectLoader:(RKObjectLoader*)objectLoader didFailWithError:(NSError*)error {
_isLoading = NO;
[self didFailLoadWithError:error];
// if ([self errorWarrantsOptionToGoOffline:error]) {
// [self showAlertWithOptionToGoOfflineForError:error];
// }
}
- (void)objectLoaderDidLoadUnexpectedResponse:(RKObjectLoader*)objectLoader {
@@ -199,41 +175,15 @@ static NSString* const kDefaultLoadedTimeKey = @"RKRequestTTModelDefaultLoadedTi
[self didFailLoadWithError:error];
}
#pragma mark RKRequestTTModel (Private)
// TODO: Can we push this load time into RKRequestCache???
- (void)clearLoadedTime {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:_resourcePath];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:self.resourcePath];
}
- (void)saveLoadedTime {
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:_resourcePath];
}
- (BOOL)errorWarrantsOptionToGoOffline:(NSError*)error {
switch ([error code]) {
case NSURLErrorTimedOut:
case NSURLErrorCannotFindHost:
case NSURLErrorCannotConnectToHost:
case NSURLErrorNetworkConnectionLost:
case NSURLErrorDNSLookupFailed:
case NSURLErrorNotConnectedToInternet:
case NSURLErrorInternationalRoamingOff:
return YES;
break;
default:
return NO;
break;
}
}
- (void)showAlertWithOptionToGoOfflineForError:(NSError*)error {
UIAlertView* alert = [[[UIAlertView alloc] initWithTitle:TTLocalizedString(@"Network Error", @"")
message:[error localizedDescription]
delegate:nil
cancelButtonTitle:TTLocalizedString(@"OK", @"")
otherButtonTitles:nil] autorelease];
[alert show];
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.resourcePath];
}
- (void)modelsDidLoad:(NSArray*)models {
@@ -251,6 +201,7 @@ static NSString* const kDefaultLoadedTimeKey = @"RKRequestTTModelDefaultLoadedTi
// public
- (void)load {
Class managedObjectClass = NSClassFromString(@"RKManagedObject");
RKManagedObjectStore* store = [RKObjectManager sharedManager].objectStore;
NSArray* cacheFetchRequests = nil;
if (store.managedObjectCache) {
@@ -264,8 +215,8 @@ static NSString* const kDefaultLoadedTimeKey = @"RKRequestTTModelDefaultLoadedTi
_isLoading = YES;
[self didStartLoad];
[objectLoader send];
} else if (cacheFetchRequests && !_cacheLoaded) {
[self.objectLoader send];
} else if (cacheFetchRequests && !_cacheLoaded && managedObjectClass) {
_cacheLoaded = YES;
[self modelsDidLoad:[NSManagedObject objectsWithFetchRequests:cacheFetchRequests]];
}

View File

@@ -0,0 +1,24 @@
//
// RKTableViewDataSource.h
// RestKit
//
// Created by Blake Watters on 4/26/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
#import <Three20/Three20.h>
#import "../RestKit.h"
@interface RKTableViewDataSource : TTTableViewDataSource {
NSMutableDictionary* _objectToTableCellMappings;
}
// The objects loaded via the RKObjectLoaderTTModel instance...
@property (nonatomic, readonly) NSArray* modelObjects;
+ (id)dataSource;
- (void)registerCellClass:(Class)cellCell forObjectClass:(Class)objectClass; // TODO: Better method name??
// TODO: Delegate?
@end

View File

@@ -0,0 +1,84 @@
//
// RKTableViewDataSource.m
// RestKit
//
// Created by Blake Watters on 4/26/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
#import "RKTableViewDataSource.h"
#import "RKMappableObjectTableItem.h"
#import "RKObjectLoaderTTModel.h"
@implementation RKTableViewDataSource
+ (id)dataSource {
return [[self new] autorelease];
}
- (id)init {
self = [super init];
if (self) {
_objectToTableCellMappings = [NSMutableDictionary new];
}
return self;
}
- (void)setModel:(id<TTModel>)model {
if (NO == [model isKindOfClass:[RKObjectLoaderTTModel class]]) {
[NSException raise:nil format:@"RKTableViewDataSource is designed to work with RestKit TTModel implementations only"];
}
[super setModel:model];
}
- (void)dealloc {
[_objectToTableCellMappings release];
[super dealloc];
}
- (NSArray*)modelObjects {
return [(RKObjectLoaderTTModel*)self.model objects];
}
- (void)registerCellClass:(Class)cellCell forObjectClass:(Class)objectClass {
[_objectToTableCellMappings setObject:cellCell forKey:objectClass];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.modelObjects count];
}
- (Class)tableView:(UITableView*)tableView cellClassForObject:(id)object {
RKMappableObjectTableItem* tableItem = (RKMappableObjectTableItem*)object;
Class cellClass = [_objectToTableCellMappings objectForKey:[tableItem.object class]];
NSAssert(cellClass != nil, @"Must have a registered cell class for type");
return cellClass;
}
- (id)tableView:(UITableView*)tableView objectForRowAtIndexPath:(NSIndexPath*)indexPath {
if (indexPath.row < [self.modelObjects count]) {
NSObject<RKObjectMappable>* mappableObject = [self.modelObjects objectAtIndex:indexPath.row];
RKMappableObjectTableItem* tableItem = [RKMappableObjectTableItem itemWithObject:mappableObject];
return tableItem;
} else {
return nil;
}
}
- (NSIndexPath*)tableView:(UITableView*)tableView indexPathForObject:(id)object {
NSUInteger objectIndex = [self.modelObjects indexOfObject:object];
if (objectIndex != NSNotFound) {
return [NSIndexPath indexPathForRow:objectIndex inSection:0];
}
return nil;
}
- (NSString*)titleForEmpty {
return @"Empty!";
}
@end

View File

@@ -6,5 +6,7 @@
// Copyright 2010 Two Toasters. All rights reserved.
//
#import "RKRequestTTModel.h"
#import "RKRequestFilterableTTModel.h"
#import "RKObjectLoaderTTModel.h"
#import "RKFilterableObjectLoaderTTModel.h"
#import "RKMappableObjectTableItem.h"
#import "RKTableViewDataSource.h"

View File

@@ -79,10 +79,6 @@
253A09261255258500976E89 /* RKManagedObjectStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 253A086312551D8D00976E89 /* RKManagedObjectStore.h */; settings = {ATTRIBUTES = (Public, ); }; };
253A09271255258600976E89 /* RKManagedObjectStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 253A086412551D8D00976E89 /* RKManagedObjectStore.m */; };
253A092D125525EE00976E89 /* Three20.h in Headers */ = {isa = PBXBuildFile; fileRef = 253A08A512551D8D00976E89 /* Three20.h */; settings = {ATTRIBUTES = (Public, ); }; };
253A092E125525EF00976E89 /* RKRequestTTModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 253A08A412551D8D00976E89 /* RKRequestTTModel.m */; };
253A092F125525F000976E89 /* RKRequestTTModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 253A08A312551D8D00976E89 /* RKRequestTTModel.h */; settings = {ATTRIBUTES = (Public, ); }; };
253A0932125525F100976E89 /* RKRequestFilterableTTModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 253A08A012551D8D00976E89 /* RKRequestFilterableTTModel.m */; };
253A0933125525F100976E89 /* RKRequestFilterableTTModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 253A089F12551D8D00976E89 /* RKRequestFilterableTTModel.h */; settings = {ATTRIBUTES = (Public, ); }; };
253A09E612552B5300976E89 /* ObjectMapping.h in Headers */ = {isa = PBXBuildFile; fileRef = 253A09E512552B5300976E89 /* ObjectMapping.h */; settings = {ATTRIBUTES = (Public, ); }; };
253A09F612552BDC00976E89 /* Support.h in Headers */ = {isa = PBXBuildFile; fileRef = 253A09F512552BDC00976E89 /* Support.h */; settings = {ATTRIBUTES = (Public, ); }; };
253E04C513798D72005D2E15 /* RKErrorMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = 253E04C313798D72005D2E15 /* RKErrorMessage.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -234,6 +230,12 @@
3F032AAB10FFBC1F00F35142 /* RKResident.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F032AAA10FFBC1F00F35142 /* RKResident.m */; };
3F1912A712DF6B4800C077AD /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F19129712DF6B4800C077AD /* CFNetwork.framework */; };
3F1912AF12DF6B6200C077AD /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25E075981279D9AB00B22EC9 /* MobileCoreServices.framework */; };
3F278A40139D2AFF009AC3FA /* NSData+MD5.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F278A3A139D2AFF009AC3FA /* NSData+MD5.h */; };
3F278A41139D2AFF009AC3FA /* NSData+MD5.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F278A3B139D2AFF009AC3FA /* NSData+MD5.m */; };
3F278A42139D2AFF009AC3FA /* NSString+MD5.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F278A3C139D2AFF009AC3FA /* NSString+MD5.h */; };
3F278A43139D2AFF009AC3FA /* NSString+MD5.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F278A3D139D2AFF009AC3FA /* NSString+MD5.m */; };
3F278A44139D2AFF009AC3FA /* RKRequestCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F278A3E139D2AFF009AC3FA /* RKRequestCache.h */; settings = {ATTRIBUTES = (Public, ); }; };
3F278A45139D2AFF009AC3FA /* RKRequestCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F278A3F139D2AFF009AC3FA /* RKRequestCache.m */; };
3F4EAF58134205CF00F944E4 /* RKXMLParserSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F4EAF57134205CF00F944E4 /* RKXMLParserSpec.m */; };
3F4EAF5B1342071B00F944E4 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F4EAF5A1342071B00F944E4 /* libxml2.dylib */; };
3F5356ED13785DA300132100 /* RKObjectSerializerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F5356EC13785DA300132100 /* RKObjectSerializerSpec.m */; };
@@ -241,6 +243,14 @@
3F6C3A9610FE7524008F47C5 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F6C3A9510FE7524008F47C5 /* UIKit.framework */; };
3F71ED3413748536006281CA /* RKObjectMappingProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F71ED3213748536006281CA /* RKObjectMappingProvider.h */; settings = {ATTRIBUTES = (Public, ); }; };
3F71ED3513748536006281CA /* RKObjectMappingProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F71ED3313748536006281CA /* RKObjectMappingProvider.m */; };
3F741A5B139D74F1002E7304 /* RKFilterableObjectLoaderTTModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F741A53139D74F1002E7304 /* RKFilterableObjectLoaderTTModel.h */; settings = {ATTRIBUTES = (Public, ); }; };
3F741A5C139D74F1002E7304 /* RKFilterableObjectLoaderTTModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F741A54139D74F1002E7304 /* RKFilterableObjectLoaderTTModel.m */; };
3F741A5D139D74F1002E7304 /* RKMappableObjectTableItem.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F741A55139D74F1002E7304 /* RKMappableObjectTableItem.h */; settings = {ATTRIBUTES = (Public, ); }; };
3F741A5E139D74F1002E7304 /* RKMappableObjectTableItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F741A56139D74F1002E7304 /* RKMappableObjectTableItem.m */; };
3F741A5F139D74F1002E7304 /* RKObjectLoaderTTModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F741A57139D74F1002E7304 /* RKObjectLoaderTTModel.h */; settings = {ATTRIBUTES = (Public, ); }; };
3F741A60139D74F1002E7304 /* RKObjectLoaderTTModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F741A58139D74F1002E7304 /* RKObjectLoaderTTModel.m */; };
3F741A61139D74F1002E7304 /* RKTableViewDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F741A59139D74F1002E7304 /* RKTableViewDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; };
3F741A62139D74F1002E7304 /* RKTableViewDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F741A5A139D74F1002E7304 /* RKTableViewDataSource.m */; };
3FD12C851379AD64008B996A /* RKRouter.h in Headers */ = {isa = PBXBuildFile; fileRef = 3FD12C841379AD64008B996A /* RKRouter.h */; settings = {ATTRIBUTES = (Public, ); }; };
73057FD61331AD5E001908EE /* JSONKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 73057FC71331AA3D001908EE /* JSONKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
73057FD71331AD67001908EE /* JSONKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 73057FC81331AA3D001908EE /* JSONKit.m */; };
@@ -418,10 +428,6 @@
253A089B12551D8D00976E89 /* RestKit_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RestKit_Prefix.pch; sourceTree = "<group>"; };
253A089C12551D8D00976E89 /* RKSearchEngine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKSearchEngine.h; sourceTree = "<group>"; };
253A089D12551D8D00976E89 /* RKSearchEngine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKSearchEngine.m; sourceTree = "<group>"; };
253A089F12551D8D00976E89 /* RKRequestFilterableTTModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKRequestFilterableTTModel.h; sourceTree = "<group>"; };
253A08A012551D8D00976E89 /* RKRequestFilterableTTModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKRequestFilterableTTModel.m; sourceTree = "<group>"; };
253A08A312551D8D00976E89 /* RKRequestTTModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKRequestTTModel.h; sourceTree = "<group>"; };
253A08A412551D8D00976E89 /* RKRequestTTModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKRequestTTModel.m; sourceTree = "<group>"; };
253A08A512551D8D00976E89 /* Three20.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Three20.h; sourceTree = "<group>"; };
253A08AE12551EA500976E89 /* Network.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Network.h; sourceTree = "<group>"; };
253A08B61255212300976E89 /* RKJSONParserSBJSON.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKJSONParserSBJSON.m; sourceTree = "<group>"; };
@@ -603,6 +609,12 @@
3F032AAA10FFBC1F00F35142 /* RKResident.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKResident.m; sourceTree = "<group>"; };
3F19126812DF6B3200C077AD /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
3F19129712DF6B4800C077AD /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; };
3F278A3A139D2AFF009AC3FA /* NSData+MD5.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSData+MD5.h"; path = "Code/Network/NSData+MD5.h"; sourceTree = SOURCE_ROOT; };
3F278A3B139D2AFF009AC3FA /* NSData+MD5.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSData+MD5.m"; path = "Code/Network/NSData+MD5.m"; sourceTree = SOURCE_ROOT; };
3F278A3C139D2AFF009AC3FA /* NSString+MD5.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSString+MD5.h"; path = "Code/Network/NSString+MD5.h"; sourceTree = SOURCE_ROOT; };
3F278A3D139D2AFF009AC3FA /* NSString+MD5.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSString+MD5.m"; path = "Code/Network/NSString+MD5.m"; sourceTree = SOURCE_ROOT; };
3F278A3E139D2AFF009AC3FA /* RKRequestCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKRequestCache.h; path = Code/Network/RKRequestCache.h; sourceTree = SOURCE_ROOT; };
3F278A3F139D2AFF009AC3FA /* RKRequestCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKRequestCache.m; path = Code/Network/RKRequestCache.m; sourceTree = SOURCE_ROOT; };
3F4EAF57134205CF00F944E4 /* RKXMLParserSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKXMLParserSpec.m; sourceTree = "<group>"; };
3F4EAF5A1342071B00F944E4 /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = usr/lib/libxml2.dylib; sourceTree = SDKROOT; };
3F5356EC13785DA300132100 /* RKObjectSerializerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectSerializerSpec.m; sourceTree = "<group>"; };
@@ -615,6 +627,16 @@
3F7C8ECF1379815A00BEBC29 /* RKErrorMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKErrorMessage.h; sourceTree = "<group>"; };
3F7C8ED01379815A00BEBC29 /* RKErrorMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKErrorMessage.m; sourceTree = "<group>"; };
73057FC41331A8F6001908EE /* RKJSONParserJSONKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RKJSONParser+JSONKit.m"; sourceTree = "<group>"; };
3F741A53139D74F1002E7304 /* RKFilterableObjectLoaderTTModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKFilterableObjectLoaderTTModel.h; path = ../../../../../RestKit/Code/Three20/RKFilterableObjectLoaderTTModel.h; sourceTree = "<group>"; };
3F741A54139D74F1002E7304 /* RKFilterableObjectLoaderTTModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKFilterableObjectLoaderTTModel.m; path = ../../../../../RestKit/Code/Three20/RKFilterableObjectLoaderTTModel.m; sourceTree = "<group>"; };
3F741A55139D74F1002E7304 /* RKMappableObjectTableItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKMappableObjectTableItem.h; path = ../../../../../RestKit/Code/Three20/RKMappableObjectTableItem.h; sourceTree = "<group>"; };
3F741A56139D74F1002E7304 /* RKMappableObjectTableItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKMappableObjectTableItem.m; path = ../../../../../RestKit/Code/Three20/RKMappableObjectTableItem.m; sourceTree = "<group>"; };
3F741A57139D74F1002E7304 /* RKObjectLoaderTTModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKObjectLoaderTTModel.h; path = ../../../../../RestKit/Code/Three20/RKObjectLoaderTTModel.h; sourceTree = "<group>"; };
3F741A58139D74F1002E7304 /* RKObjectLoaderTTModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKObjectLoaderTTModel.m; path = ../../../../../RestKit/Code/Three20/RKObjectLoaderTTModel.m; sourceTree = "<group>"; };
3F741A59139D74F1002E7304 /* RKTableViewDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKTableViewDataSource.h; path = ../../../../../RestKit/Code/Three20/RKTableViewDataSource.h; sourceTree = "<group>"; };
3F741A5A139D74F1002E7304 /* RKTableViewDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKTableViewDataSource.m; path = ../../../../../RestKit/Code/Three20/RKTableViewDataSource.m; sourceTree = "<group>"; };
3FD12C841379AD64008B996A /* RKRouter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKRouter.h; path = Code/ObjectMapping/RKRouter.h; sourceTree = SOURCE_ROOT; };
73057FC41331A8F6001908EE /* RKJSONParserJSONKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKJSONParserJSONKit.m; sourceTree = "<group>"; };
73057FC71331AA3D001908EE /* JSONKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = JSONKit.h; path = Vendor/JSONKit/JSONKit.h; sourceTree = "<group>"; };
73057FC81331AA3D001908EE /* JSONKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = JSONKit.m; path = Vendor/JSONKit/JSONKit.m; sourceTree = "<group>"; };
73057FD11331AD2E001908EE /* libRestKitJSONParserJSONKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRestKitJSONParserJSONKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -830,6 +852,12 @@
253A086512551D8D00976E89 /* Network */ = {
isa = PBXGroup;
children = (
3F278A3A139D2AFF009AC3FA /* NSData+MD5.h */,
3F278A3B139D2AFF009AC3FA /* NSData+MD5.m */,
3F278A3C139D2AFF009AC3FA /* NSString+MD5.h */,
3F278A3D139D2AFF009AC3FA /* NSString+MD5.m */,
3F278A3E139D2AFF009AC3FA /* RKRequestCache.h */,
3F278A3F139D2AFF009AC3FA /* RKRequestCache.m */,
253A08AE12551EA500976E89 /* Network.h */,
253A086612551D8D00976E89 /* NSDictionary+RKRequestSerialization.h */,
253A086712551D8D00976E89 /* NSDictionary+RKRequestSerialization.m */,
@@ -886,6 +914,7 @@
25F5182413724866009B2E22 /* RKObjectRelationshipMapping.m */,
259C64BD137477D0001FE1C5 /* RKObjectMapper.h */,
259C64BE137477D0001FE1C5 /* RKObjectMapper.m */,
2530078D137838D30074F3FD /* RKObjectMapper_Private.h */,
3F71ED3213748536006281CA /* RKObjectMappingProvider.h */,
3F71ED3313748536006281CA /* RKObjectMappingProvider.m */,
257D2D6E13759D6F008E9649 /* RKObjectMappingResult.h */,
@@ -930,11 +959,15 @@
253A089E12551D8D00976E89 /* Three20 */ = {
isa = PBXGroup;
children = (
3F741A53139D74F1002E7304 /* RKFilterableObjectLoaderTTModel.h */,
3F741A54139D74F1002E7304 /* RKFilterableObjectLoaderTTModel.m */,
3F741A55139D74F1002E7304 /* RKMappableObjectTableItem.h */,
3F741A56139D74F1002E7304 /* RKMappableObjectTableItem.m */,
3F741A57139D74F1002E7304 /* RKObjectLoaderTTModel.h */,
3F741A58139D74F1002E7304 /* RKObjectLoaderTTModel.m */,
3F741A59139D74F1002E7304 /* RKTableViewDataSource.h */,
3F741A5A139D74F1002E7304 /* RKTableViewDataSource.m */,
253A08A512551D8D00976E89 /* Three20.h */,
253A089F12551D8D00976E89 /* RKRequestFilterableTTModel.h */,
253A08A012551D8D00976E89 /* RKRequestFilterableTTModel.m */,
253A08A312551D8D00976E89 /* RKRequestTTModel.h */,
253A08A412551D8D00976E89 /* RKRequestTTModel.m */,
);
path = Three20;
sourceTree = "<group>";
@@ -1308,8 +1341,10 @@
buildActionMask = 2147483647;
files = (
253A092D125525EE00976E89 /* Three20.h in Headers */,
253A092F125525F000976E89 /* RKRequestTTModel.h in Headers */,
253A0933125525F100976E89 /* RKRequestFilterableTTModel.h in Headers */,
3F741A5B139D74F1002E7304 /* RKFilterableObjectLoaderTTModel.h in Headers */,
3F741A5D139D74F1002E7304 /* RKMappableObjectTableItem.h in Headers */,
3F741A5F139D74F1002E7304 /* RKObjectLoaderTTModel.h in Headers */,
3F741A61139D74F1002E7304 /* RKTableViewDataSource.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1330,8 +1365,11 @@
73FE56C7126CB91600E0F30B /* RKURL.h in Headers */,
2538C05C12A6C44A0006903C /* RKRequestQueue.h in Headers */,
250C29FD134185D2000A3551 /* RKNetwork.h in Headers */,
3F278A44139D2AFF009AC3FA /* RKRequestCache.h in Headers */,
25A1CB3F138404CF00A7D5C9 /* RKRequestSerialization.h in Headers */,
257FB7061395DABB003A628E /* RKRequest_Internals.h in Headers */,
3F278A40139D2AFF009AC3FA /* NSData+MD5.h in Headers */,
3F278A42139D2AFF009AC3FA /* NSString+MD5.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1805,8 +1843,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
253A092E125525EF00976E89 /* RKRequestTTModel.m in Sources */,
253A0932125525F100976E89 /* RKRequestFilterableTTModel.m in Sources */,
3F741A5C139D74F1002E7304 /* RKFilterableObjectLoaderTTModel.m in Sources */,
3F741A5E139D74F1002E7304 /* RKMappableObjectTableItem.m in Sources */,
3F741A60139D74F1002E7304 /* RKObjectLoaderTTModel.m in Sources */,
3F741A62139D74F1002E7304 /* RKTableViewDataSource.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1826,6 +1866,9 @@
2538C05D12A6C44A0006903C /* RKRequestQueue.m in Sources */,
250C29FE134185D2000A3551 /* RKNetwork.m in Sources */,
25A1CB40138404EB00A7D5C9 /* RKRequestSerialization.m in Sources */,
3F278A41139D2AFF009AC3FA /* NSData+MD5.m in Sources */,
3F278A43139D2AFF009AC3FA /* NSString+MD5.m in Sources */,
3F278A45139D2AFF009AC3FA /* RKRequestCache.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -65,6 +65,7 @@
mapping.primaryKeyAttribute = @"railsID";
[mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"monkey.id" toKeyPath:@"railsID"]];
[RKHuman truncateAll];
RKHuman* human = [RKHuman object];
human.railsID = [NSNumber numberWithInt:123];
[store save];

View File

@@ -0,0 +1,14 @@
//
// NSData+MD5.h
// RestKit
//
// Created by Jeff Arena on 4/4/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
@interface NSData (MD5)
- (NSString*)MD5;
@end

View File

@@ -0,0 +1,30 @@
//
// NSData+MD5.m
// RestKit
//
// Created by Jeff Arena on 4/4/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
#import "NSData+MD5.h"
#import <CommonCrypto/CommonDigest.h>
@implementation NSData (MD5)
- (NSString*)MD5 {
// Create byte array of unsigned chars
unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH];
// Create 16 byte MD5 hash value, store in buffer
CC_MD5(self.bytes, self.length, md5Buffer);
// Convert unsigned char buffer to NSString of hex values
NSMutableString* output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[output appendFormat:@"%02x",md5Buffer[i]];
}
return output;
}
@end

View File

@@ -0,0 +1,14 @@
//
// NSString+MD5.h
// RestKit
//
// Created by Jeff Arena on 4/4/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
@interface NSString (MD5)
- (NSString*)MD5;
@end

View File

@@ -0,0 +1,33 @@
//
// NSString+MD5.m
// RestKit
//
// Created by Jeff Arena on 4/4/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
#import "NSString+MD5.h"
#import <CommonCrypto/CommonDigest.h>
@implementation NSString (MD5)
- (NSString*)MD5 {
// Create pointer to the string as UTF8
const char* ptr = [self UTF8String];
// Create byte array of unsigned chars
unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH];
// Create 16 byte MD5 hash value, store in buffer
CC_MD5(ptr, strlen(ptr), md5Buffer);
// Convert MD5 value in the buffer to NSString of hex values
NSMutableString* output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[output appendFormat:@"%02x",md5Buffer[i]];
}
return output;
}
@end

View File

@@ -29,5 +29,18 @@
RKReachabilityNetworkStatus status = [client.baseURLReachabilityObserver networkStatus];
[expectThat(status) shouldNot:be(RKReachabilityIndeterminate)];
}
- (void)itShouldSetTheCachePolicyOfTheRequest {
RKClient* client = [RKClient clientWithBaseURL:@"http://restkit.org"];
client.cachePolicy = RKRequestCachePolicyLoadIfOffline;
RKRequest* request = [client requestWithResourcePath:@"" delegate:nil];
[expectThat(request.cachePolicy) should:be(RKRequestCachePolicyLoadIfOffline)];
}
- (void)itShouldInitializeTheCacheOfTheRequest {
RKClient* client = [RKClient clientWithBaseURL:@"http://restkit.org"];
client.cache = [[[RKRequestCache alloc] init] autorelease];
RKRequest* request = [client requestWithResourcePath:@"" delegate:nil];
[expectThat(request.cache) should:be(client.cache)];
}
@end

View File

@@ -0,0 +1,53 @@
//
// RKRequestCache.h
// RestKit
//
// Created by Jeff Arena on 4/4/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
#import "RKRequest.h"
#import "RKResponse.h"
/**
* Storage policy. Determines if we clear the cache out when the app is shut down.
* Cache instance needs to register for
*/
typedef enum {
RKRequestCacheStoragePolicyDisabled, // The cache has been disabled. Attempts to store data will silently fail
RKRequestCacheStoragePolicyForDurationOfSession, // Cache data for the length of the session. Clear cache at app exit.
RKRequestCacheStoragePolicyPermanently // Cache data permanently, until explicitly expired or flushed
} RKRequestCacheStoragePolicy;
@interface RKRequestCache : NSObject {
NSString* _cachePath;
RKRequestCacheStoragePolicy _storagePolicy;
NSRecursiveLock* _cacheLock;
}
@property (nonatomic, readonly) NSString* cachePath; // Full path to the cache
@property (nonatomic, assign) RKRequestCacheStoragePolicy storagePolicy; // User can change storage policy.
+ (NSDateFormatter*)rfc1123DateFormatter;
- (id)initWithCachePath:(NSString*)cachePath storagePolicy:(RKRequestCacheStoragePolicy)storagePolicy;
- (NSString*)pathForRequest:(RKRequest*)request;
- (BOOL)hasResponseForRequest:(RKRequest*)request;
- (void)storeResponse:(RKResponse*)response forRequest:(RKRequest*)request;
//- (void)storeResponse:(RKResponse*)response forRequest:(RKRequest*)request expires:(NSTimeInterval)expirationAge;
- (RKResponse*)responseForRequest:(RKRequest*)request;
- (NSString*)etagForRequest:(RKRequest*)request;
- (void)invalidateRequest:(RKRequest*)request;
- (void)invalidateAll;
@end

View File

@@ -0,0 +1,289 @@
//
// RKRequestCache.m
// RestKit
//
// Created by Jeff Arena on 4/4/11.
// Copyright 2011 Two Toasters. All rights reserved.
//
#import "RKRequestCache.h"
static NSString* sessionCacheFolder = @"SessionStore";
static NSString* permanentCacheFolder = @"PermanentStore";
static NSString* headersExtension = @"headers";
static NSString* cacheDateHeaderKey = @"X-RESTKIT-CACHEDATE";
static NSDateFormatter* __rfc1123DateFormatter;
@implementation RKRequestCache
@synthesize storagePolicy = _storagePolicy;
+ (NSDateFormatter*)rfc1123DateFormatter {
if (__rfc1123DateFormatter == nil) {
__rfc1123DateFormatter = [[NSDateFormatter alloc] init];
[__rfc1123DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
[__rfc1123DateFormatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss 'GMT'"];
}
return __rfc1123DateFormatter;
}
- (id)initWithCachePath:(NSString*)cachePath storagePolicy:(RKRequestCacheStoragePolicy)storagePolicy {
if (self = [super init]) {
_cachePath = [cachePath copy];
_cacheLock = [[NSRecursiveLock alloc] init];
NSFileManager* fileManager = [[NSFileManager alloc] init];
NSArray* pathArray = [NSArray arrayWithObjects:
_cachePath,
[_cachePath stringByAppendingPathComponent:sessionCacheFolder],
[_cachePath stringByAppendingPathComponent:permanentCacheFolder],
nil];
for (NSString* path in pathArray) {
BOOL isDirectory = NO;
BOOL fileExists = [fileManager fileExistsAtPath:path isDirectory:&isDirectory];
if (!fileExists) {
NSError* error = nil;
BOOL created = [fileManager createDirectoryAtPath:path
withIntermediateDirectories:NO
attributes:nil
error:&error];
if (!created || error != nil) {
NSLog(@"[RestKit] RKRequestCache: Failed to create cache directory at %@: error %@",
path, [error localizedDescription]);
}
} else if (!isDirectory) {
NSLog(@"[RestKit] RKRequestCache: Failed to create cache directory: Directory already exists: %@", path);
}
}
[fileManager release];
self.storagePolicy = storagePolicy;
}
return self;
}
- (void)dealloc {
[_cachePath release];
_cachePath = nil;
[_cacheLock release];
_cacheLock = nil;
[super dealloc];
}
- (NSString*)cachePath {
return _cachePath;
}
- (NSString*)pathForRequest:(RKRequest*)request {
[_cacheLock lock];
NSString* pathForRequest = nil;
if (_storagePolicy == RKRequestCacheStoragePolicyForDurationOfSession) {
pathForRequest = [[_cachePath stringByAppendingPathComponent:sessionCacheFolder]
stringByAppendingPathComponent:[request cacheKey]];
} else if (_storagePolicy == RKRequestCacheStoragePolicyPermanently) {
pathForRequest = [[_cachePath stringByAppendingPathComponent:permanentCacheFolder]
stringByAppendingPathComponent:[request cacheKey]];
}
[_cacheLock unlock];
return pathForRequest;
}
- (BOOL)hasResponseForRequest:(RKRequest*)request {
[_cacheLock lock];
BOOL hasEntryForRequest = NO;
NSFileManager* fileManager = [[NSFileManager alloc] init];
NSString* cachePath = [self pathForRequest:request];
hasEntryForRequest = ([fileManager fileExistsAtPath:cachePath] &&
[fileManager fileExistsAtPath:
[cachePath stringByAppendingPathExtension:headersExtension]]);
[fileManager release];
[_cacheLock unlock];
return hasEntryForRequest;
}
- (void)storeResponse:(RKResponse*)response forRequest:(RKRequest*)request {
[_cacheLock lock];
if ([self hasResponseForRequest:request]) {
[self invalidateRequest:request];
}
if (_storagePolicy != RKRequestCacheStoragePolicyDisabled) {
NSString* cachePath = [self pathForRequest:request];
if (cachePath) {
NSData* body = response.body;
if (body) {
[body writeToFile:cachePath atomically:NO];
}
NSMutableDictionary* headers = [response.allHeaderFields mutableCopy];
NSLog(@"headers = %@", headers);
if (headers) {
[headers setObject:[[RKRequestCache rfc1123DateFormatter] stringFromDate:[NSDate date]]
forKey:cacheDateHeaderKey];
[headers writeToFile:[cachePath stringByAppendingPathExtension:headersExtension]
atomically:NO];
}
[headers release];
}
}
[_cacheLock unlock];
}
//- (void)storeResponse:(RKResponse*)response forRequest:(RKRequest*)request expires:(NSTimeInterval)expirationAge {
// [_cacheLock lock];
//
// if (_storagePolicy != RKRequestCacheStoragePolicyDisabled) {
// NSString* cachePath = [self pathForRequest:request];
// if (cachePath) {
// NSData* body = response.body;
// if (body) {
// [body writeToFile:cachePath atomically:NO];
// }
//
// NSMutableDictionary* headers = [response.allHeaderFields mutableCopy];
// if (headers) {
// if (expirationAge != 0) {
// [headers removeObjectForKey:@"Expires"];
// [headers setObject:[NSString stringWithFormat:@"max-age=%i",(int)expirationAge]
// forKey:@"Cache-Control"];
// }
// [headers setObject:[[RKRequestCache rfc1123DateFormatter] stringFromDate:[NSDate date]]
// forKey:cacheDateHeaderKey];
// [headers writeToFile:[cachePath stringByAppendingPathExtension:headersExtension]
// atomically:NO];
// }
// [headers release];
// }
// }
//
// [_cacheLock unlock];
//}
- (RKResponse*)responseForRequest:(RKRequest*)request {
[_cacheLock lock];
RKResponse* response = nil;
NSString* cachePath = [self pathForRequest:request];
if (cachePath) {
NSData* responseData = [NSData dataWithContentsOfFile:cachePath];
NSDictionary* responseHeaders = [NSDictionary dictionaryWithContentsOfFile:
[cachePath stringByAppendingPathExtension:headersExtension]];
response = [[[RKResponse alloc] initWithRequest:request body:responseData headers:responseHeaders] autorelease];
}
[_cacheLock unlock];
return response;
}
- (NSString*)etagForRequest:(RKRequest*)request {
NSString* etag = nil;
[_cacheLock lock];
NSString* cachePath = [self pathForRequest:request];
if (cachePath) {
NSDictionary* responseHeaders = [NSDictionary dictionaryWithContentsOfFile:
[cachePath stringByAppendingPathExtension:headersExtension]];
if (responseHeaders) {
for (NSString* responseHeader in responseHeaders) {
if ([responseHeader isEqualToString:@"Etag"]) {
etag = [responseHeaders objectForKey:responseHeader];
}
}
}
}
[_cacheLock unlock];
return etag;
}
- (void)invalidateRequest:(RKRequest*)request {
[_cacheLock lock];
NSString* cachePath = [self pathForRequest:request];
if (cachePath) {
NSFileManager* fileManager = [[NSFileManager alloc] init];
[fileManager removeItemAtPath:cachePath error:NULL];
[fileManager removeItemAtPath:[cachePath stringByAppendingPathExtension:headersExtension]
error:NULL];
[fileManager release];
}
[_cacheLock unlock];
}
- (void)invalidateWithStoragePolicy:(RKRequestCacheStoragePolicy)storagePolicy {
[_cacheLock lock];
if (_cachePath && storagePolicy != RKRequestCacheStoragePolicyDisabled) {
NSString* cachePath = nil;
if (storagePolicy == RKRequestCacheStoragePolicyForDurationOfSession) {
cachePath = [_cachePath stringByAppendingPathComponent:sessionCacheFolder];
} else {
cachePath = [_cachePath stringByAppendingPathComponent:permanentCacheFolder];
}
NSFileManager* fileManager = [[NSFileManager alloc] init];
BOOL isDirectory = NO;
BOOL fileExists = [fileManager fileExistsAtPath:cachePath isDirectory:&isDirectory];
if (fileExists && isDirectory) {
NSError* error = nil;
NSArray* cacheEntries = [fileManager contentsOfDirectoryAtPath:cachePath error:&error];
if (nil == error) {
for (NSString* cacheEntry in cacheEntries) {
NSString* cacheEntryPath = [cachePath stringByAppendingPathComponent:cacheEntry];
[fileManager removeItemAtPath:cacheEntryPath error:&error];
[fileManager removeItemAtPath:[cacheEntryPath stringByAppendingPathExtension:headersExtension]
error:&error];
if (nil != error) {
NSLog(@"[RestKit] RKRequestCache: Failed to delete cache entry for file: %@", cacheEntryPath);
}
}
} else {
NSLog(@"[RestKit] RKRequestCache: Failed to fetch list of cache entries for cache path: %@", cachePath);
}
}
[fileManager release];
}
[_cacheLock unlock];
}
- (void)invalidateAll {
[_cacheLock lock];
[self invalidateWithStoragePolicy:RKRequestCacheStoragePolicyForDurationOfSession];
[self invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently];
[_cacheLock unlock];
}
- (void)setStoragePolicy:(RKRequestCacheStoragePolicy)storagePolicy {
[self invalidateWithStoragePolicy:RKRequestCacheStoragePolicyForDurationOfSession];
if (storagePolicy == RKRequestCacheStoragePolicyDisabled) {
[self invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently];
}
_storagePolicy = storagePolicy;
}
@end

View File

@@ -13,6 +13,7 @@
@interface RKRequest (Private)
- (void)fireAsynchronousRequest;
- (void)shouldDispatchRequest;
@end
@interface RKRequestSpec : NSObject <UISpec> {
@@ -144,4 +145,237 @@
[expectThat([loader success]) should:be(YES)];
}
#pragma mark RKRequestCachePolicy Specs
- (void)itShouldSendTheRequestWhenTheCachePolicyIsNone {
RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
NSString* url = [NSString stringWithFormat:@"%@/etags", RKSpecGetBaseURL()];
NSURL* URL = [NSURL URLWithString:url];
RKRequest* request = [[RKRequest alloc] initWithURL:URL];
request.cachePolicy = RKRequestCachePolicyNone;
request.delegate = loader;
[request sendAsynchronously];
[loader waitForResponse];
[expectThat([loader success]) should:be(YES)];
}
- (void)itShouldCacheTheRequestHeadersAndBodyIncludingOurOwnCustomTimestampHeader {
NSString* baseURL = RKSpecGetBaseURL();
NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@",
[[NSURL URLWithString:baseURL] host]];
NSString* cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
stringByAppendingPathComponent:cacheDirForClient];
RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath
storagePolicy:RKRequestCacheStoragePolicyPermanently];
[cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently];
RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()];
NSURL* URL = [NSURL URLWithString:url];
RKRequest* request = [[RKRequest alloc] initWithURL:URL];
request.cachePolicy = RKRequestCachePolicyEtag;
request.cache = cache;
request.delegate = loader;
[request sendAsynchronously];
[loader waitForResponse];
[expectThat([loader success]) should:be(YES)];
NSDictionary* headers = [cache headersForRequest:request];
[expectThat([headers valueForKey:@"X-RESTKIT-CACHEDATE"]) shouldNot:be(nil)];
[expectThat([headers valueForKey:@"Etag"]) should:be(@"686897696a7c876b7e")];
[expectThat([[cache responseForRequest:request] bodyAsString]) should:be(@"This Should Get Cached")];
}
- (void)itShouldGenerateAUniqueCacheKeyBasedOnTheUrlTheMethodAndTheHTTPBody {
NSString* baseURL = RKSpecGetBaseURL();
NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@",
[[NSURL URLWithString:baseURL] host]];
NSString* cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
stringByAppendingPathComponent:cacheDirForClient];
RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath
storagePolicy:RKRequestCacheStoragePolicyPermanently];
[cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently];
NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()];
NSURL* URL = [NSURL URLWithString:url];
RKRequest* request = [[RKRequest alloc] initWithURL:URL];
request.cachePolicy = RKRequestCachePolicyEtag;
NSString* cacheKeyGET = [request cacheKey];
request.method = RKRequestMethodDELETE;
// Don't cache delete. cache key should be nil.
[expectThat([request cacheKey]) should:be(nil)];
request.method = RKRequestMethodPOST;
[expectThat([request cacheKey]) shouldNot:be(nil)];
[expectThat(cacheKeyGET) shouldNot:be([request cacheKey])];
request.params = [NSDictionary dictionaryWithObject:@"val" forKey:@"key"];
NSString* cacheKeyPOST = [request cacheKey];
[expectThat(cacheKeyPOST) shouldNot:be(nil)];
request.method = RKRequestMethodPUT;
[expectThat(cacheKeyPOST) shouldNot:be([request cacheKey])];
[expectThat([request cacheKey]) shouldNot:be(nil)];
}
- (void)itShouldLoadFromCacheWhenWeRecieveA304 {
NSString* baseURL = RKSpecGetBaseURL();
NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@",
[[NSURL URLWithString:baseURL] host]];
NSString* cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
stringByAppendingPathComponent:cacheDirForClient];
RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath
storagePolicy:RKRequestCacheStoragePolicyPermanently];
[cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently];
{
RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()];
NSURL* URL = [NSURL URLWithString:url];
RKRequest* request = [[RKRequest alloc] initWithURL:URL];
request.cachePolicy = RKRequestCachePolicyEtag;
request.cache = cache;
request.delegate = loader;
[request sendAsynchronously];
[loader waitForResponse];
[expectThat([loader success]) should:be(YES)];
[expectThat([loader.response bodyAsString]) should:be(@"This Should Get Cached")];
[expectThat([cache etagForRequest:request]) should:be(@"686897696a7c876b7e")];
[expectThat([loader.response wasLoadedFromCache]) should:be(NO)];
}
{
RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()];
NSURL* URL = [NSURL URLWithString:url];
RKRequest* request = [[RKRequest alloc] initWithURL:URL];
request.cachePolicy = RKRequestCachePolicyEtag;
request.cache = cache;
request.delegate = loader;
[request sendAsynchronously];
[loader waitForResponse];
[expectThat([loader success]) should:be(YES)];
[expectThat([loader.response bodyAsString]) should:be(@"This Should Get Cached")];
[expectThat([loader.response wasLoadedFromCache]) should:be(YES)];
}
}
- (void)itShouldLoadFromTheCacheIfThereIsAnError {
NSString* baseURL = RKSpecGetBaseURL();
NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@",
[[NSURL URLWithString:baseURL] host]];
NSString* cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
stringByAppendingPathComponent:cacheDirForClient];
RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath
storagePolicy:RKRequestCacheStoragePolicyPermanently];
[cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently];
{
RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()];
NSURL* URL = [NSURL URLWithString:url];
RKRequest* request = [[RKRequest alloc] initWithURL:URL];
request.cachePolicy = RKRequestCachePolicyEtag;
request.cache = cache;
request.delegate = loader;
[request sendAsynchronously];
[loader waitForResponse];
[expectThat([loader success]) should:be(YES)];
[expectThat([loader.response bodyAsString]) should:be(@"This Should Get Cached")];
[expectThat([loader.response wasLoadedFromCache]) should:be(NO)];
}
{
RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()];
NSURL* URL = [NSURL URLWithString:url];
RKRequest* request = [[RKRequest alloc] initWithURL:URL];
request.cachePolicy = RKRequestCachePolicyLoadOnError;
request.cache = cache;
request.delegate = loader;
[request didFailLoadWithError:[NSError errorWithDomain:@"Fake" code:0 userInfo:nil]];
[expectThat([loader.response bodyAsString]) should:be(@"This Should Get Cached")];
[expectThat([loader.response wasLoadedFromCache]) should:be(YES)];
}
}
- (void)itShouldLoadFromTheCacheIfWeAreOffline {
NSString* baseURL = RKSpecGetBaseURL();
NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@",
[[NSURL URLWithString:baseURL] host]];
NSString* cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
stringByAppendingPathComponent:cacheDirForClient];
RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath
storagePolicy:RKRequestCacheStoragePolicyPermanently];
[cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently];
{
RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()];
NSURL* URL = [NSURL URLWithString:url];
RKRequest* request = [[RKRequest alloc] initWithURL:URL];
request.cachePolicy = RKRequestCachePolicyEtag;
request.cache = cache;
request.delegate = loader;
[request sendAsynchronously];
[loader waitForResponse];
[expectThat([loader success]) should:be(YES)];
[expectThat([loader.response bodyAsString]) should:be(@"This Should Get Cached")];
[expectThat([loader.response wasLoadedFromCache]) should:be(NO)];
}
{
RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()];
NSURL* URL = [NSURL URLWithString:url];
RKRequest* request = [[RKRequest alloc] initWithURL:URL];
request.cachePolicy = RKRequestCachePolicyLoadIfOffline;
request.cache = cache;
request.delegate = loader;
id mock = [OCMockObject partialMockForObject:request];
BOOL returnValue = NO;
[[[mock expect] andReturnValue:OCMOCK_VALUE(returnValue)] shouldDispatchRequest];
[mock sendAsynchronously];
[expectThat([loader.response bodyAsString]) should:be(@"This Should Get Cached")];
[expectThat([loader.response wasLoadedFromCache]) should:be(YES)];
}
}
- (void)itShouldCacheTheStatusCodeMIMETypeAndURL {
NSString* baseURL = RKSpecGetBaseURL();
NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@",
[[NSURL URLWithString:baseURL] host]];
NSString* cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
stringByAppendingPathComponent:cacheDirForClient];
RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath
storagePolicy:RKRequestCacheStoragePolicyPermanently];
[cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently];
{
RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()];
NSURL* URL = [NSURL URLWithString:url];
RKRequest* request = [[RKRequest alloc] initWithURL:URL];
request.cachePolicy = RKRequestCachePolicyEtag;
request.cache = cache;
request.delegate = loader;
[request sendAsynchronously];
[loader waitForResponse];
[expectThat([loader success]) should:be(YES)];
[expectThat([loader.response bodyAsString]) should:be(@"This Should Get Cached")];
NSLog(@"Headers: %@", [cache headersForRequest:request]);
[expectThat([cache etagForRequest:request]) should:be(@"686897696a7c876b7e")];
[expectThat([loader.response wasLoadedFromCache]) should:be(NO)];
}
{
RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()];
NSURL* URL = [NSURL URLWithString:url];
RKRequest* request = [[RKRequest alloc] initWithURL:URL];
request.cachePolicy = RKRequestCachePolicyEtag;
request.cache = cache;
request.delegate = loader;
[request sendAsynchronously];
[loader waitForResponse];
[expectThat([loader success]) should:be(YES)];
[expectThat([loader.response wasLoadedFromCache]) should:be(YES)];
[expectThat(loader.response.statusCode) should:be(200)];
[expectThat(loader.response.MIMEType) should:be(@"text/html")];
[expectThat([loader.response.URL absoluteString]) should:be(@"http://localhost:4567/etags/cached")];
}
}
@end

View File

@@ -18,7 +18,6 @@
@interface RKObjectManagerSpec : RKSpec {
RKObjectManager* _objectManager;
RKSpecResponseLoader* _responseLoader;
}
@end
@@ -67,8 +66,6 @@
RKObjectRouter* router = [[[RKObjectRouter alloc] init] autorelease];
[router routeClass:[RKHuman class] toResourcePath:@"/humans" forMethod:RKRequestMethodPOST];
_objectManager.router = router;
_responseLoader = [[RKSpecResponseLoader alloc] init];
}
- (void)itShouldSetTheAcceptHeaderAppropriatelyForTheFormat {
@@ -85,30 +82,33 @@
// that we just need a way to save the context before we begin mapping or something
// on success. Always saving means that we can abandon objects on failure...
[_objectManager.objectStore save];
[_objectManager postObject:temporaryHuman delegate:_responseLoader];
[_responseLoader waitForResponse];
RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
[_objectManager postObject:temporaryHuman delegate:loader];
[loader waitForResponse];
RKHuman* human = (RKHuman*)[_responseLoader.objects objectAtIndex:0];
RKHuman* human = (RKHuman*)[loader.objects objectAtIndex:0];
assertThat(human, is(equalTo(temporaryHuman)));
assertThat(human.railsID, is(equalToInt(1)));
}
// TODO: Move to Core Data specific spec file...
- (void)itShouldLoadAHuman {
[_objectManager loadObjectsAtResourcePath:@"/JSON/humans/1.json" delegate:_responseLoader];
_responseLoader.timeout = 200;
[_responseLoader waitForResponse];
[expectThat(_responseLoader.failureError) should:be(nil)];
assertThat(_responseLoader.objects, isNot(empty()));
RKHuman* blake = (RKHuman*)[_responseLoader.objects objectAtIndex:0];
RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
[_objectManager loadObjectsAtResourcePath:@"/JSON/humans/1.json" delegate:loader];
loader.timeout = 200;
[loader waitForResponse];
[expectThat(loader.failureError) should:be(nil)];
assertThat(loader.objects, isNot(empty()));
RKHuman* blake = (RKHuman*)[loader.objects objectAtIndex:0];
NSLog(@"Blake: %@ (name = %@)", blake, blake.name);
[expectThat(blake.name) should:be(@"Blake Watters")];
}
- (void)itShouldLoadAllHumans {
[_objectManager loadObjectsAtResourcePath:@"/JSON/humans/all.json" delegate:_responseLoader];
[_responseLoader waitForResponse];
NSArray* humans = (NSArray*) _responseLoader.objects;
RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
[_objectManager loadObjectsAtResourcePath:@"/JSON/humans/all.json" delegate:loader];
[loader waitForResponse];
NSArray* humans = (NSArray*) loader.objects;
[expectThat([humans count]) should:be(2)];
[expectThat([[humans objectAtIndex:0] class]) should:be([RKHuman class])];
}
@@ -116,9 +116,10 @@
- (void)itShouldHandleConnectionFailures {
NSString* localBaseURL = [NSString stringWithFormat:@"http://127.0.0.1:3001"];
RKObjectManager* modelManager = [RKObjectManager objectManagerWithBaseURL:localBaseURL];
[modelManager loadObjectsAtResourcePath:@"/JSON/humans/1" delegate:_responseLoader];
[_responseLoader waitForResponse];
[expectThat(_responseLoader.success) should:be(NO)];
RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
[modelManager loadObjectsAtResourcePath:@"/JSON/humans/1" delegate:loader];
[loader waitForResponse];
[expectThat(loader.success) should:be(NO)];
}
- (void)itShouldPOSTAnObject {

View File

@@ -49,6 +49,9 @@ RKObjectManager* RKSpecNewObjectManager() {
[RKClient setSharedClient:objectManager.client];
RKSpecNewRequestQueue();
// This allows the manager to determine state.
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
return objectManager;
}

View File

@@ -0,0 +1,22 @@
module RestKit
module Network
class ETags < Sinatra::Base
get '/etags' do
"Success!"
end
get '/etags/cached' do
tag = "686897696a7c876b7e"
if tag == request.env["HTTP_IF_NONE_MATCH"]
status 304
""
else
headers "ETag" => tag
"This Should Get Cached"
end
end
end
end
end

View File

@@ -10,10 +10,12 @@ Debugger.start
# Import the RestKit Spec server
$: << File.join(File.expand_path(File.dirname(__FILE__)), 'lib')
require 'restkit/network/authentication'
require 'restkit/network/etags'
class RestKit::SpecServer < Sinatra::Base
self.app_file = __FILE__
use RestKit::Network::Authentication
use RestKit::Network::ETags
configure do
set :logging, true

View File

@@ -1,7 +1,8 @@
//
// JSONKit.h
// http://github.com/johnezang/JSONKit
// Licensed under the terms of the BSD License, as specified below.
// Dual licensed under either the terms of the BSD License, or alternatively
// under the terms of the Apache License, Version 2.0, as specified below.
//
/*
@@ -36,6 +37,22 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
Copyright 2011 John Engelhart
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.
*/
#include <stddef.h>
#include <stdint.h>
#include <limits.h>
@@ -109,10 +126,11 @@ enum {
typedef JKFlags JKParseOptionFlags;
enum {
JKSerializeOptionNone = 0,
JKSerializeOptionPretty = (1 << 0), // Not implemented yet...
JKSerializeOptionEscapeUnicode = (1 << 1),
JKSerializeOptionValidFlags = (JKSerializeOptionPretty | JKSerializeOptionEscapeUnicode),
JKSerializeOptionNone = 0,
JKSerializeOptionPretty = (1 << 0),
JKSerializeOptionEscapeUnicode = (1 << 1),
JKSerializeOptionEscapeForwardSlashes = (1 << 4),
JKSerializeOptionValidFlags = (JKSerializeOptionPretty | JKSerializeOptionEscapeUnicode | JKSerializeOptionEscapeForwardSlashes),
};
typedef JKFlags JKSerializeOptionFlags;
@@ -153,7 +171,11 @@ typedef struct JKParseState JKParseState; // Opaque internal, private type.
@end
@interface NSString (JSONKit)
////////////
#pragma mark Deserializing methods
////////////
@interface NSString (JSONKitDeserializing)
- (id)objectFromJSONString;
- (id)objectFromJSONStringWithParseOptions:(JKParseOptionFlags)parseOptionFlags;
- (id)objectFromJSONStringWithParseOptions:(JKParseOptionFlags)parseOptionFlags error:(NSError **)error;
@@ -162,7 +184,7 @@ typedef struct JKParseState JKParseState; // Opaque internal, private type.
- (id)mutableObjectFromJSONStringWithParseOptions:(JKParseOptionFlags)parseOptionFlags error:(NSError **)error;
@end
@interface NSData (JSONKit)
@interface NSData (JSONKitDeserializing)
// The NSData MUST be UTF8 encoded JSON.
- (id)objectFromJSONData;
- (id)objectFromJSONDataWithParseOptions:(JKParseOptionFlags)parseOptionFlags;
@@ -172,20 +194,54 @@ typedef struct JKParseState JKParseState; // Opaque internal, private type.
- (id)mutableObjectFromJSONDataWithParseOptions:(JKParseOptionFlags)parseOptionFlags error:(NSError **)error;
@end
@interface NSArray (JSONKit)
- (NSData *)JSONData;
- (NSData *)JSONDataWithOptions:(JKSerializeOptionFlags)serializeOptions error:(NSError **)error;
- (NSString *)JSONString;
- (NSString *)JSONStringWithOptions:(JKSerializeOptionFlags)serializeOptions error:(NSError **)error;
////////////
#pragma mark Serializing methods
////////////
@interface NSString (JSONKitSerializing)
// Convenience methods for those that need to serialize the receiving NSString (i.e., instead of having to serialize a NSArray with a single NSString, you can "serialize to JSON" just the NSString).
// Normally, a string that is serialized to JSON has quotation marks surrounding it, which you may or may not want when serializing a single string, and can be controlled with includeQuotes:
// includeQuotes:YES `a "test"...` -> `"a \"test\"..."`
// includeQuotes:NO `a "test"...` -> `a \"test\"...`
- (NSData *)JSONData; // Invokes JSONDataWithOptions:JKSerializeOptionNone includeQuotes:YES
- (NSData *)JSONDataWithOptions:(JKSerializeOptionFlags)serializeOptions includeQuotes:(BOOL)includeQuotes error:(NSError **)error;
- (NSString *)JSONString; // Invokes JSONStringWithOptions:JKSerializeOptionNone includeQuotes:YES
- (NSString *)JSONStringWithOptions:(JKSerializeOptionFlags)serializeOptions includeQuotes:(BOOL)includeQuotes error:(NSError **)error;
@end
@interface NSDictionary (JSONKit)
@interface NSArray (JSONKitSerializing)
- (NSData *)JSONData;
- (NSData *)JSONDataWithOptions:(JKSerializeOptionFlags)serializeOptions error:(NSError **)error;
- (NSData *)JSONDataWithOptions:(JKSerializeOptionFlags)serializeOptions serializeUnsupportedClassesUsingDelegate:(id)delegate selector:(SEL)selector error:(NSError **)error;
- (NSString *)JSONString;
- (NSString *)JSONStringWithOptions:(JKSerializeOptionFlags)serializeOptions error:(NSError **)error;
- (NSString *)JSONStringWithOptions:(JKSerializeOptionFlags)serializeOptions serializeUnsupportedClassesUsingDelegate:(id)delegate selector:(SEL)selector error:(NSError **)error;
@end
@interface NSDictionary (JSONKitSerializing)
- (NSData *)JSONData;
- (NSData *)JSONDataWithOptions:(JKSerializeOptionFlags)serializeOptions error:(NSError **)error;
- (NSData *)JSONDataWithOptions:(JKSerializeOptionFlags)serializeOptions serializeUnsupportedClassesUsingDelegate:(id)delegate selector:(SEL)selector error:(NSError **)error;
- (NSString *)JSONString;
- (NSString *)JSONStringWithOptions:(JKSerializeOptionFlags)serializeOptions error:(NSError **)error;
- (NSString *)JSONStringWithOptions:(JKSerializeOptionFlags)serializeOptions serializeUnsupportedClassesUsingDelegate:(id)delegate selector:(SEL)selector error:(NSError **)error;
@end
#ifdef __BLOCKS__
@interface NSArray (JSONKitSerializingBlockAdditions)
- (NSData *)JSONDataWithOptions:(JKSerializeOptionFlags)serializeOptions serializeUnsupportedClassesUsingBlock:(id(^)(id object))block error:(NSError **)error;
- (NSString *)JSONStringWithOptions:(JKSerializeOptionFlags)serializeOptions serializeUnsupportedClassesUsingBlock:(id(^)(id object))block error:(NSError **)error;
@end
@interface NSDictionary (JSONKitSerializingBlockAdditions)
- (NSData *)JSONDataWithOptions:(JKSerializeOptionFlags)serializeOptions serializeUnsupportedClassesUsingBlock:(id(^)(id object))block error:(NSError **)error;
- (NSString *)JSONStringWithOptions:(JKSerializeOptionFlags)serializeOptions serializeUnsupportedClassesUsingBlock:(id(^)(id object))block error:(NSError **)error;
@end
#endif
#endif // __OBJC__
#endif // _JSONKIT_H_

View File

@@ -1005,7 +1005,8 @@ static void _JKDictionaryAddObject(JKDictionary *dictionary, NSUInteger keyHash,
static JKHashTableEntry *_JKDictionaryHashTableEntryForKey(JKDictionary *dictionary, id aKey) {
NSCParameterAssert((dictionary != NULL) && (dictionary->entry != NULL) && (dictionary->count <= dictionary->capacity));
if(aKey == NULL) { return(NULL); }
// Guard against a divide by zero here.
if(aKey == NULL || 0 == dictionary->capacity) { return(NULL); }
NSUInteger keyHash = CFHash(aKey), keyEntry = (keyHash % dictionary->capacity), idx = 0UL;
JKHashTableEntry *atEntry = NULL;
for(idx = 0UL; idx < dictionary->capacity; idx++) {