Application/iTunesRSSImporter.m
/* |
Copyright (C) 2017 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
Downloads, parses, and imports the iTunes top songs RSS feed into Core Data. |
*/ |
#import "iTunesRSSImporter.h" |
#import "Song.h" |
#import "Category.h" |
#import "CategoryCache.h" |
#import <libxml/tree.h> |
// Function prototypes for SAX callbacks. This sample implements a minimal subset of SAX callbacks. |
// Depending on your application's needs, you might want to implement more callbacks. |
static void startElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI, int nb_namespaces, const xmlChar **namespaces, int nb_attributes, int nb_defaulted, const xmlChar **attributes); |
static void endElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI); |
static void charactersFoundSAX(void *context, const xmlChar *characters, int length); |
static void errorEncounteredSAX(void *context, const char *errorMessage, ...); |
// Forward reference. The structure is defined in full at the end of the file. |
static xmlSAXHandler simpleSAXHandlerStruct; |
#pragma mark - |
// Class extension for private properties and methods. |
@interface iTunesRSSImporter () <NSURLSessionDataDelegate> |
// Reference to the libxml parser context |
@property xmlParserCtxtPtr context; |
// The following state variables deal with getting character data from XML elements. This is a potentially expensive |
// operation. The character data in a given element may be delivered over the course of multiple callbacks, so that |
// data must be appended to a buffer. The optimal way of doing this is to use a C string buffer that grows exponentially. |
// When all the characters have been delivered, an NSString is constructed and the buffer is reset. |
@property BOOL storingCharacters; |
@property (nonatomic, strong) NSMutableData *characterBuffer; |
// Overall state of the importer, used to exit the run loop. |
@property BOOL done; |
// State variable used to determine whether or not to ignore a given XML element |
@property BOOL parsingASong; |
// The number of parsed songs is tracked so that the autorelease pool for the parsing thread can be periodically |
// emptied to keep the memory footprint under control. |
@property NSUInteger countForCurrentBatch; |
// A reference to the current song the importer is working with. |
@property (nonatomic, strong) Song *currentSong; |
@property (nonatomic, strong) NSURLSession *session; |
@property (nonatomic, strong) NSURLSessionDataTask *sessionTask; |
@property (nonatomic, strong) NSDateFormatter *dateFormatter; |
@property NSUInteger rankOfCurrentSong; |
@property (nonatomic, strong) NSManagedObjectContext *insertionContext; |
@property (nonatomic, strong) NSEntityDescription *songEntityDescription; |
@property (nonatomic, strong) CategoryCache *theCache; |
@end |
#pragma mark - |
static double lookuptime = 0; |
@implementation iTunesRSSImporter |
- (void)main { |
if (self.delegate && [self.delegate respondsToSelector:@selector(importerDidSave:)]) { |
[[NSNotificationCenter defaultCenter] addObserver:self.delegate |
selector:@selector(importerDidSave:) |
name:NSManagedObjectContextDidSaveNotification |
object:self.insertionContext]; |
} |
self.done = NO; |
_dateFormatter = [[NSDateFormatter alloc] init]; |
self.dateFormatter.dateStyle = NSDateFormatterLongStyle; |
self.dateFormatter.timeStyle = NSDateFormatterNoStyle; |
// necessary because iTunes RSS feed is not localized, so if the device region has been set to other than US |
// the date formatter must be set to US locale in order to parse the dates |
self.dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"US"]; |
_characterBuffer = [NSMutableData data]; |
// create the session with the request and start loading the data |
NSURLRequest *request = [NSURLRequest requestWithURL:self.iTunesURL]; |
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; |
_session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]]; |
_sessionTask = [self.session dataTaskWithRequest:request]; |
if (self.sessionTask != nil) { |
[self.sessionTask resume]; |
// This creates a context for "push" parsing in which chunks of data that are not "well balanced" can be passed |
// to the context for streaming parsing. The handler structure defined above will be used for all the parsing. |
// The second argument, self, will be passed as user data to each of the SAX handlers. The last three arguments |
// are left blank to avoid creating a tree in memory. |
// |
_context = xmlCreatePushParserCtxt(&simpleSAXHandlerStruct, (__bridge void *)(self), NULL, 0, NULL); |
do { |
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; |
} while (!self.done); |
// Display the total time spent finding a specific object for a relationship |
NSLog(@"lookup time %f", lookuptime); |
// Release resources used only in this thread. |
xmlFreeParserCtxt(self.context); |
_characterBuffer = nil; |
self.dateFormatter = nil; |
self.currentSong = nil; |
_theCache = nil; |
[self.insertionContext performBlockAndWait:^{ |
NSError *saveError = nil; |
NSAssert1([self.insertionContext save:&saveError], |
@"Unhandled error saving managed object context in import thread: %@", [saveError localizedDescription]); |
if (self.delegate && [self.delegate respondsToSelector:@selector(importerDidSave:)]) { |
[[NSNotificationCenter defaultCenter] removeObserver:self.delegate |
name:NSManagedObjectContextDidSaveNotification |
object:self.insertionContext]; |
} |
// Call our delegate to signify parse completion. |
if (self.delegate != nil && [self.delegate respondsToSelector:@selector(importerDidFinishParsingData:)]) { |
[self.delegate importerDidFinishParsingData:self]; |
} |
}]; |
} |
} |
- (NSManagedObjectContext *)insertionContext { |
if (_insertionContext == nil) { |
_insertionContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; |
_insertionContext.persistentStoreCoordinator = self.persistentStoreCoordinator; |
} |
return _insertionContext; |
} |
- (void)forwardError:(NSError *)error { |
if (self.delegate != nil && [self.delegate respondsToSelector:@selector(importer:didFailWithError:)]) { |
[self.delegate importer:self didFailWithError:error]; |
} |
} |
- (NSEntityDescription *)songEntityDescription { |
if (_songEntityDescription == nil) { |
_songEntityDescription = [NSEntityDescription entityForName:@"Song" inManagedObjectContext:self.insertionContext]; |
} |
return _songEntityDescription; |
} |
- (CategoryCache *)theCache { |
if (_theCache == nil) { |
_theCache = [[CategoryCache alloc] init]; |
_theCache.managedObjectContext = self.insertionContext; |
} |
return _theCache; |
} |
- (Song *)currentSong { |
if (_currentSong == nil) { |
_currentSong = [[Song alloc] initWithEntity:self.songEntityDescription insertIntoManagedObjectContext:self.insertionContext]; |
_currentSong.rank = @(++_rankOfCurrentSong); |
} |
return _currentSong; |
} |
#pragma mark - NSURLSessionDataDelegate methods |
// Sent when data is available for the delegate to consume. |
// |
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { |
// Process the downloaded chunk of data. |
xmlParseChunk(self.context, (const char *)data.bytes, (int)data.length, 0); |
} |
// Sent as the last message related to a specific task. |
// Error may be nil, which implies that no error occurred and this task is complete. |
// |
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { |
if (error != nil) { |
if (error.code == NSURLErrorAppTransportSecurityRequiresSecureConnection) |
{ |
// if you get error NSURLErrorAppTransportSecurityRequiresSecureConnection (-1022), |
// then your Info.plist has not been properly configured to match the target server. |
// |
abort(); |
} |
[self performSelectorOnMainThread:@selector(forwardError:) withObject:error waitUntilDone:NO]; |
} |
// Signal the context that parsing is complete by passing "1" as the last parameter. |
xmlParseChunk(self.context, NULL, 0, 1); |
_context = NULL; |
// Set the condition which ends the run loop. |
self.done = YES; |
} |
#pragma mark - Parsing support methods |
static const NSUInteger kImportBatchSize = 20; |
- (void)finishedCurrentSong { |
self.parsingASong = NO; |
self.currentSong = nil; |
self.countForCurrentBatch++; |
if (self.countForCurrentBatch == kImportBatchSize) { |
NSError *saveError = nil; |
NSAssert1([self.insertionContext save:&saveError], @"Unhandled error saving managed object context in import thread: %@", [saveError localizedDescription]); |
self.countForCurrentBatch = 0; |
} |
} |
/* |
Character data is appended to a buffer until the current element ends. |
*/ |
- (void)appendCharacters:(const char *)charactersFound length:(NSInteger)length { |
[self.characterBuffer appendBytes:charactersFound length:length]; |
} |
- (NSString *)currentString { |
// Create a string with the character data using UTF-8 encoding. UTF-8 is the default XML data encoding. |
NSString *currentString = [[NSString alloc] initWithData:self.characterBuffer encoding:NSUTF8StringEncoding]; |
self.characterBuffer.length = 0; |
return currentString; |
} |
@end |
#pragma mark - SAX Parsing Callbacks |
// The following constants are the XML element names and their string lengths for parsing comparison. |
// The lengths include the null terminator, to ensure exact matches. |
static const char *kName_Item = "item"; |
static const NSUInteger kLength_Item = 5; |
static const char *kName_Title = "title"; |
static const NSUInteger kLength_Title = 6; |
static const char *kName_Category = "category"; |
static const NSUInteger kLength_Category = 9; |
static const char *kName_Itms = "itms"; |
static const NSUInteger kLength_Itms = 5; |
static const char *kName_Artist = "artist"; |
static const NSUInteger kLength_Artist = 7; |
static const char *kName_Album = "album"; |
static const NSUInteger kLength_Album = 6; |
static const char *kName_ReleaseDate = "releasedate"; |
static const NSUInteger kLength_ReleaseDate = 12; |
/* |
This callback is invoked when the importer finds the beginning of a node in the XML. For this application, |
out parsing needs are relatively modest - we need only match the node name. An "item" node is a record of |
data about a song. In that case we create a new Song object. The other nodes of interest are several of the |
child nodes of the Song currently being parsed. For those nodes we want to accumulate the character data |
in a buffer. Some of the child nodes use a namespace prefix. |
*/ |
static void startElementSAX(void *parsingContext, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI, |
int nb_namespaces, const xmlChar **namespaces, int nb_attributes, int nb_defaulted, const xmlChar **attributes) { |
iTunesRSSImporter *importer = (__bridge iTunesRSSImporter *)parsingContext; |
// The second parameter to strncmp is the name of the element, which we known from the XML schema of the feed. |
// The third parameter to strncmp is the number of characters in the element name, plus 1 for the null terminator. |
if (prefix == NULL && !strncmp((const char *)localname, kName_Item, kLength_Item)) { |
importer.parsingASong = YES; |
} else if (importer.parsingASong && ( (prefix == NULL && (!strncmp((const char *)localname, kName_Title, kLength_Title) || !strncmp((const char *)localname, kName_Category, kLength_Category))) || ((prefix != NULL && !strncmp((const char *)prefix, kName_Itms, kLength_Itms)) && (!strncmp((const char *)localname, kName_Artist, kLength_Artist) || !strncmp((const char *)localname, kName_Album, kLength_Album) || !strncmp((const char *)localname, kName_ReleaseDate, kLength_ReleaseDate))) )) { |
importer.storingCharacters = YES; |
} |
} |
/* |
This callback is invoked when the parse reaches the end of a node. At that point we finish processing that node, |
if it is of interest to us. For "item" nodes, that means we have completed parsing a Song object. We pass the song |
to a method in the superclass which will eventually deliver it to the delegate. For the other nodes we |
care about, this means we have all the character data. The next step is to create an NSString using the buffer |
contents and store that with the current Song object. |
*/ |
static void endElementSAX(void *parsingContext, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI) { |
iTunesRSSImporter *importer = (__bridge iTunesRSSImporter *)parsingContext; |
if (importer.parsingASong == NO) return; |
if (prefix == NULL) { |
if (!strncmp((const char *)localname, kName_Item, kLength_Item)) { |
[importer finishedCurrentSong]; |
} else if (!strncmp((const char *)localname, kName_Title, kLength_Title)) { |
importer.currentSong.title = importer.currentString; |
} else if (!strncmp((const char *)localname, kName_Category, kLength_Category)) { |
double before = [NSDate timeIntervalSinceReferenceDate]; |
Category *category = [importer.theCache categoryWithName:importer.currentString]; |
double delta = [NSDate timeIntervalSinceReferenceDate] - before; |
lookuptime += delta; |
importer.currentSong.category = category; |
} |
} else if (!strncmp((const char *)prefix, kName_Itms, kLength_Itms)) { |
if (!strncmp((const char *)localname, kName_Artist, kLength_Artist)) { |
importer.currentSong.artist = importer.currentString; |
} else if (!strncmp((const char *)localname, kName_Album, kLength_Album)) { |
importer.currentSong.album = importer.currentString; |
} else if (!strncmp((const char *)localname, kName_ReleaseDate, kLength_ReleaseDate)) { |
NSString *dateString = importer.currentString; |
importer.currentSong.releaseDate = [importer.dateFormatter dateFromString:dateString]; |
} |
} |
importer.storingCharacters = NO; |
} |
/* |
This callback is invoked when the parser encounters character data inside a node. The importer class determines how to use the character data. |
*/ |
static void charactersFoundSAX(void *parsingContext, const xmlChar *characterArray, int numberOfCharacters) { |
iTunesRSSImporter *importer = (__bridge iTunesRSSImporter *)parsingContext; |
// A state variable, "storingCharacters", is set when nodes of interest begin and end. |
// This determines whether character data is handled or ignored. |
if (importer.storingCharacters == NO) return; |
[importer appendCharacters:(const char *)characterArray length:numberOfCharacters]; |
} |
/* |
A production application should include robust error handling as part of its parsing implementation. |
The specifics of how errors are handled depends on the application. |
*/ |
static void errorEncounteredSAX(void *parsingContext, const char *errorMessage, ...) { |
// Handle errors as appropriate for your application. |
NSCAssert(NO, @"Unhandled error encountered during SAX parse."); |
} |
// The handler struct has positions for a large number of callback functions. If NULL is supplied at a given position, |
// that callback functionality won't be used. Refer to libxml documentation at http://www.xmlsoft.org for more information |
// about the SAX callbacks. |
static xmlSAXHandler simpleSAXHandlerStruct = { |
NULL, /* internalSubset */ |
NULL, /* isStandalone */ |
NULL, /* hasInternalSubset */ |
NULL, /* hasExternalSubset */ |
NULL, /* resolveEntity */ |
NULL, /* getEntity */ |
NULL, /* entityDecl */ |
NULL, /* notationDecl */ |
NULL, /* attributeDecl */ |
NULL, /* elementDecl */ |
NULL, /* unparsedEntityDecl */ |
NULL, /* setDocumentLocator */ |
NULL, /* startDocument */ |
NULL, /* endDocument */ |
NULL, /* startElement*/ |
NULL, /* endElement */ |
NULL, /* reference */ |
charactersFoundSAX, /* characters */ |
NULL, /* ignorableWhitespace */ |
NULL, /* processingInstruction */ |
NULL, /* comment */ |
NULL, /* warning */ |
errorEncounteredSAX, /* error */ |
NULL, /* fatalError //: unused error() get all the errors */ |
NULL, /* getParameterEntity */ |
NULL, /* cdataBlock */ |
NULL, /* externalSubset */ |
XML_SAX2_MAGIC, // |
NULL, |
startElementSAX, /* startElementNs */ |
endElementSAX, /* endElementNs */ |
NULL, /* serror */ |
}; |
Copyright © 2017 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2017-03-23