Files
RestKit/Code/Network/RKPaginator.m

296 lines
10 KiB
Objective-C

//
// RKPaginator.m
// RestKit
//
// Created by Blake Watters on 12/29/11.
// Copyright (c) 2009-2012 RestKit. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#import "RKPaginator.h"
#import "RKMappingOperation.h"
#import "RKObjectRequestOperation.h"
#import "RKManagedObjectRequestOperation.h"
#import "SOCKit.h"
#import "RKLog.h"
#import "RKPathUtilities.h"
#import "RKHTTPUtilities.h"
static NSUInteger RKPaginatorDefaultPerPage = 25;
// Private interface
@interface RKPaginator ()
@property (nonatomic, copy) NSURLRequest *request;
@property (nonatomic, strong) Class HTTPOperationClass;
@property (nonatomic, strong) RKObjectRequestOperation *objectRequestOperation;
@property (nonatomic, copy) NSArray *responseDescriptors;
@property (nonatomic, assign, readwrite) NSUInteger currentPage;
@property (nonatomic, assign, readwrite) NSUInteger pageCount;
@property (nonatomic, assign, readwrite) NSUInteger objectCount;
@property (nonatomic, assign, readwrite) BOOL loaded;
@property (nonatomic, strong, readwrite) RKMappingResult *mappingResult;
@property (nonatomic, strong, readwrite) NSError *error;
// iOS 5.x compatible proxy attributes
@property (nonatomic, assign, readwrite) NSNumber *perPageNumber;
@property (nonatomic, assign, readwrite) NSNumber *currentPageNumber;
@property (nonatomic, assign, readwrite) NSNumber *pageCountNumber;
@property (nonatomic, assign, readwrite) NSNumber *objectCountNumber;
@property (nonatomic, copy) void (^successBlock)(RKPaginator *paginator, NSArray *objects, NSUInteger page);
@property (nonatomic, copy) void (^failureBlock)(RKPaginator *paginator, NSError *error);
@end
@implementation RKPaginator
- (id)initWithRequest:(NSURLRequest *)request
paginationMapping:(RKObjectMapping *)paginationMapping
responseDescriptors:(NSArray *)responseDescriptors;
{
NSParameterAssert(request);
NSParameterAssert(paginationMapping);
NSParameterAssert(responseDescriptors);
NSAssert([paginationMapping.objectClass isSubclassOfClass:[RKPaginator class]], @"The paginationMapping must have a target object class of `RKPaginator`");
self = [super init];
if (self) {
self.HTTPOperationClass = [RKHTTPRequestOperation class];
self.request = request;
self.paginationMapping = paginationMapping;
self.responseDescriptors = responseDescriptors;
self.currentPage = NSNotFound;
self.pageCount = NSNotFound;
self.objectCount = NSNotFound;
self.perPage = RKPaginatorDefaultPerPage;
self.loaded = NO;
}
return self;
}
- (void)dealloc
{
[self.objectRequestOperation cancel];
}
- (NSURL *)patternURL
{
return self.request.URL;
}
- (NSURL *)URL
{
NSString *pathAndQueryString = RKPathAndQueryStringFromURLRelativeToURL(self.patternURL, nil);
NSString *interpolatedString = RKPathFromPatternWithObject(pathAndQueryString, self);
return [NSURL URLWithString:interpolatedString relativeToURL:self.request.URL];
}
- (void)setHTTPOperationClass:(Class)operationClass
{
NSAssert(operationClass == nil || [operationClass isSubclassOfClass:[RKHTTPRequestOperation class]], @"The HTTP operation class must be a subclass of `RKHTTPRequestOperation`");
_HTTPOperationClass = operationClass;
}
- (void)setCompletionBlockWithSuccess:(void (^)(RKPaginator *paginator, NSArray *objects, NSUInteger page))success
failure:(void (^)(RKPaginator *paginator, NSError *error))failure
{
self.successBlock = success;
self.failureBlock = failure;
}
// Private. Public consumers can rely on isLoaded
- (BOOL)hasCurrentPage
{
return _currentPage != NSNotFound;
}
- (BOOL)hasPageCount
{
return _pageCount != NSNotFound;
}
- (BOOL)hasObjectCount
{
return _objectCount != NSNotFound;
}
- (NSUInteger)currentPage
{
// Referenced during initial load, so we don't rely on isLoaded.
NSAssert([self hasCurrentPage], @"Current page has not been initialized.");
return _currentPage;
}
- (BOOL)hasNextPage
{
NSAssert(self.isLoaded, @"Cannot determine hasNextPage: paginator is not loaded.");
NSAssert([self hasPageCount], @"Cannot determine hasNextPage: page count is not known.");
return self.currentPage < self.pageCount;
}
- (BOOL)hasPreviousPage
{
NSAssert(self.isLoaded, @"Cannot determine hasPreviousPage: paginator is not loaded.");
return self.currentPage > 1;
}
#pragma mark - Action methods
- (void)loadNextPage
{
[self loadPage:self.currentPage + 1];
}
- (void)loadPreviousPage
{
[self loadPage:self.currentPage - 1];
}
- (void)loadPage:(NSUInteger)pageNumber
{
NSAssert(self.responseDescriptors, @"Cannot perform a load with nil response descriptors.");
NSAssert(! self.objectRequestOperation, @"Cannot perform a load while one is already in progress.");
self.currentPage = pageNumber;
NSMutableURLRequest *mutableRequest = [self.request mutableCopy];
mutableRequest.URL = self.URL;
if (self.managedObjectContext) {
RKHTTPRequestOperation *requestOperation = [[self.HTTPOperationClass alloc] initWithRequest:mutableRequest];
RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithHTTPRequestOperation:requestOperation responseDescriptors:self.responseDescriptors];
managedObjectRequestOperation.managedObjectContext = self.managedObjectContext;
managedObjectRequestOperation.managedObjectCache = self.managedObjectCache;
managedObjectRequestOperation.fetchRequestBlocks = self.fetchRequestBlocks;
managedObjectRequestOperation.deletesOrphanedObjects = NO;
self.objectRequestOperation = managedObjectRequestOperation;
} else {
self.objectRequestOperation = [[RKObjectRequestOperation alloc] initWithRequest:mutableRequest responseDescriptors:self.responseDescriptors];
}
// Add KVO to ensure notification of loaded state prior to execution of completion block
[self.objectRequestOperation addObserver:self forKeyPath:@"isFinished" options:0 context:nil];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
[self.objectRequestOperation setWillMapDeserializedResponseBlock:^id(id deserializedResponseBody) {
NSError *error = nil;
RKMappingOperation *mappingOperation = [[RKMappingOperation alloc] initWithSourceObject:deserializedResponseBody destinationObject:self mapping:self.paginationMapping];
BOOL success = [mappingOperation performMapping:&error];
if (!success) {
self.pageCount = 0;
self.currentPage = 0;
RKLogError(@"Paginator didn't map info to compute page count. Assuming no pages.");
} else if (self.perPage && [self hasObjectCount]) {
float objectCountFloat = self.objectCount;
self.pageCount = ceilf(objectCountFloat / self.perPage);
RKLogInfo(@"Paginator objectCount: %ld pageCount: %ld", (long)self.objectCount, (long)self.pageCount);
} else {
RKLogError(@"Paginator perPage set is 0.");
}
return deserializedResponseBody;
}];
[self.objectRequestOperation setCompletionBlockWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
if (self.successBlock) {
self.successBlock(self, [mappingResult array], self.currentPage);
}
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
if (self.failureBlock) {
self.failureBlock(self, error);
}
}];
#pragma clang diagnostic pop
if (self.operationQueue) {
[self.operationQueue addOperation:self.objectRequestOperation];
} else {
[self.objectRequestOperation start];
}
}
- (void)waitUntilFinished
{
[self.objectRequestOperation waitUntilFinished];
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@: %p patternURL=%@ isLoaded=%@ perPage=%ld currentPage=%@ pageCount=%@ objectCount=%@>",
NSStringFromClass([self class]), self, self.patternURL, self.isLoaded ? @"YES" : @"NO", (long) self.perPage,
[self hasCurrentPage] ? @(self.currentPage) : @"???",
[self hasPageCount] ? @(self.pageCount) : @"???",
[self hasObjectCount] ? @(self.objectCount) : @"???"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"isFinished"] && [self.objectRequestOperation isFinished]) {
self.loaded = (self.objectRequestOperation.mappingResult != nil);
self.mappingResult = self.objectRequestOperation.mappingResult;
self.error = self.objectRequestOperation.error;
self.objectRequestOperation = nil;
[object removeObserver:self forKeyPath:@"isFinished"];
}
}
- (void)cancel
{
[self.objectRequestOperation cancel];
}
#pragma mark - iOS 5 proxy attributes
- (NSNumber *)perPageNumber
{
return [NSNumber numberWithUnsignedInteger:self.perPage];
}
- (void)setPerPageNumber:(NSNumber *)perPageNumber
{
self.perPage = [perPageNumber unsignedIntegerValue];
}
- (NSNumber *)currentPageNumber
{
return [NSNumber numberWithUnsignedInteger:self.currentPage];
}
- (void)setCurrentPageNumber:(NSNumber *)currentPageNumber
{
self.currentPage = [currentPageNumber unsignedIntegerValue];
}
- (NSNumber *)pageCountNumber
{
return [NSNumber numberWithUnsignedInteger:self.pageCount];
}
- (void)setPageCountNumber:(NSNumber *)pageCountNumber
{
self.pageCount = [pageCountNumber unsignedIntegerValue];
}
- (NSNumber *)objectCountNumber
{
return [NSNumber numberWithUnsignedInteger:self.objectCount];
}
- (void)setObjectCountNumber:(NSNumber *)objectCountNumber
{
self.objectCount = [objectCountNumber unsignedIntegerValue];
}
@end