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