// // JSRunXmlParser.m // JSRun Import Framework // // Created by Jonathan on 8/29/06. // Copyright 2006 Jonathan Saggau. All rights reserved. // // TODO:: This should probably be about four different classes and a controller // This wouldn't be too hard as basically we would copy this class and handle just // certain types of tags in each mini-parser (each observing the shared parser). #import "JSRunXmlParser.h" #import "JSMORun.h" #import "JSMOEvent.h" #import "JSMODuration.h" #import "JSMODistance.h" #import "JSMOGoal.h" #import "JSMORunEvents.h" #import "JSMOUserEvents.h" #import "JSCommonMacros.h" #import "NSNumber+numberWithString.h" #import "NSDate+dateWithIPodXMLString.h" //the durations in the file are in millesecs #define XML_DURATION_MULTIPLIER .001 @interface JSRunXmlParser (PrivateAPI) - (NSXMLParser *)parser; - (void)setParser:(NSXMLParser *)aParser; - (void)setChildrensValue:(id)value forSelector:(SEL)selector; - (NSString *)getAndClearCurrentStringValue; - (void)ignoreAttribute:(NSString *)name; #pragma mark - #pragma mark Internal Accessors - (NSDictionary *)currentAttributes; - (void)setCurrentAttributes:(NSDictionary *)acurrentAttributes; - (JSMOGoal *)currentGoal; - (void)setCurrentGoal:(JSMOGoal *)aCurrentGoal; - (JSMOEvent *)currentEvent; - (void)setCurrentEvent:(JSMOEvent *)acurrentEvent; - (NSString *)nextString; - (void)setNextString:(NSString *)aNextString; - (BOOL)inGoalTag; - (void)setInGoalTag:(BOOL)flag; - (BOOL)inSnapshotTag; - (void)setInSnapshotTag:(BOOL)flag; - (BOOL)inSummaryTag; - (void)setInSummaryTag:(BOOL)flag; - (BOOL)inKmSplit; - (void)setInKmSplit:(BOOL)flag; - (BOOL)inMiSplit; - (void)setInMiSplit:(BOOL)flag; - (void)setRun:(JSMORun *)aRun; - (JSMORunEvents *)runEvents; - (void)setRunEvents:(JSMORunEvents *)aRunEvents; - (JSMOUserEvents *)userEvents; - (void)setUserEvents:(JSMOUserEvents *)anUserEvents; - (JSMOGoal *)currentGoal; - (void)setCurrentGoal:(JSMOGoal *)aCurrentGoal; - (JSMOEvent *)currentEvent; - (void)setCurrentEvent:(JSMOEvent *)aCurrentEvent; @end @implementation JSRunXmlParser - (id)init { NSURL *blankUrl = [[NSURL alloc] init]; self = [self initWithContentsOfURL:blankUrl]; return self; } - (id)initWithContentsOfURL:(NSURL *)aUrl { id appDelegate = [NSApp delegate]; //NSLog(@"appDelegate = %@", appDelegate); NSManagedObjectContext *aMoc; if ([appDelegate respondsToSelector:@selector(managedObjectContext)]) { aMoc = [[appDelegate managedObjectContext] retain]; if (!aMoc) NSLog(@"NO MOC!!"); } else { NSLog(@"appDelegate does not respond to managedObjectContext"); } self = [self initWithContentsOfURL:aUrl managedObjectContext:aMoc]; return self; } - (id)initWithContentsOfURL:(NSURL *)aUrl managedObjectContext:(NSManagedObjectContext *)aMoc; { self = [super init]; if (self != nil){ [aUrl retain]; url = aUrl; [aMoc retain]; moc = aMoc; parser = [[NSXMLParser alloc] initWithContentsOfURL:url]; [parser setDelegate:self]; currentAttributes = [[NSDictionary alloc] init]; run = nil; inGoalTag = NO; inSnapshotTag = NO; inSummaryTag = NO; inKmSplit = NO; inMiSplit = NO; currentGoal = nil; currentEvent = nil; currentStringValue = nil; } return self; } - (JSMOApp *) _fetchAppFromMOC { JSMOApp *app; NSEntityDescription * entity = [NSEntityDescription entityForName:@"App" inManagedObjectContext:moc]; NSPredicate * predicate; predicate = [NSPredicate predicateWithValue:YES]; NSFetchRequest * fetch = [[NSFetchRequest alloc] init]; [fetch setEntity: entity]; [fetch setPredicate: predicate]; NSError *error; NSArray * results = [moc executeFetchRequest:fetch error: &error]; if (!results) { [NSApp presentError:error]; return nil; } NSAssert(([results count] == 1), @"There should be one app"); app = [results objectAtIndex:0]; return app; } - (void)parse { JSMORun *newRun = [NSEntityDescription insertNewObjectForEntityForName:@"Run" inManagedObjectContext:moc]; runEvents = [NSEntityDescription insertNewObjectForEntityForName:@"RunEvents" inManagedObjectContext:moc]; userEvents = [NSEntityDescription insertNewObjectForEntityForName:@"UserEvents" inManagedObjectContext:moc]; [newRun setRunEvents:runEvents]; [newRun setUserEvents:userEvents]; [newRun setFilePath: [url path]]; [newRun setApp:[self _fetchAppFromMOC]]; [self setRun:newRun]; // set a clean run before parsing // this lets us use this parser over and over and keep pulling // the run attribute out when needed [parser parse]; } - (void)setChildrensValue:(id)value forSelector:(SEL)selector { if (inSnapshotTag) { if ([currentEvent respondsToSelector:selector]) { [currentEvent performSelector:selector withObject:value]; } return; } if (inGoalTag) { if ([currentGoal respondsToSelector:selector]) { [currentGoal performSelector:selector withObject:value]; } return; } if ([run respondsToSelector:selector]) { [run performSelector:selector withObject:value]; } } - (NSString *)getAndClearCurrentStringValue { NSString *tempStr = [currentStringValue copy]; currentStringValue = nil; [tempStr autorelease]; return tempStr; } - (void)ignoreAttribute:(NSString *)name; { //NSLog(@"Ignoring attribute ::: %@, with data ::: %@", name, [self getAndClearCurrentStringValue] ); [self getAndClearCurrentStringValue]; return; } #pragma mark - #pragma mark xmlParser delegate actions - (void)parserDidStartDocument:(NSXMLParser *)parser { } - (void)parserDidEndDocument:(NSXMLParser *)parser { // } - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { [self setCurrentAttributes:attributeDict]; if ( [elementName isEqualToString:@"runSummary"] ){ [self setInSummaryTag:YES]; return; } if ( [elementName isEqualToString:@"goal"] ){ [self setInGoalTag:YES]; [self setCurrentGoal:[NSEntityDescription insertNewObjectForEntityForName:@"Goal" inManagedObjectContext:moc]]; //make a new empty goal return; } if ( [elementName isEqualToString:@"snapShotList"] ){ [self setInSnapshotTag:YES]; NSString *eventType = [currentAttributes valueForKey:@"snapShotType"]; if ([eventType isEqualToString:@"userClick"]) { return; } if ([eventType isEqualToString:@"mileSplit"]) { [self setInMiSplit:YES]; [self setInKmSplit:NO]; [self setCurrentEvent:[NSEntityDescription insertNewObjectForEntityForName:@"Event" inManagedObjectContext:moc]]; [currentEvent setType:@"mileSplit"]; // these splits are at the end of the xml file, so we can get away // with this flipping 'round of BOOL values return; } if ([eventType isEqualToString:@"kmSplit"]) { [self setInKmSplit:YES]; [self setInMiSplit:NO]; [self setCurrentEvent:[NSEntityDescription insertNewObjectForEntityForName:@"Event" inManagedObjectContext:moc]]; [currentEvent setType:@"kmSplit"]; return; } //If we get here, we missed an attribute and should print a warning NSLog(@"Warning: parser MISSED Snapshot Type::: %@", elementName); [self setCurrentEvent:[NSEntityDescription insertNewObjectForEntityForName:@"Event" inManagedObjectContext:moc]]; // Just in case the new snapshot type puts garbage into our model, // we'll make a new empty one. } if ( [elementName isEqualToString:@"snapShot"] ){ if (! (inMiSplit || inKmSplit)){ // if we're not in the splits, were in a userClick // and we can make a new run event. [self setCurrentEvent:[NSEntityDescription insertNewObjectForEntityForName:@"Event" inManagedObjectContext:moc]]; NSString *eventType = [currentAttributes valueForKey:@"event"]; [currentEvent setType:eventType]; return; } } } - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { if ( [elementName isEqualToString:@"runSummary"] ){ //NSLog(@"runSummary"); [self setInSummaryTag:NO]; return; } if ( [elementName isEqualToString:@"snapShotList"] ){ //NSLog(@"snapShotList"); if (inKmSplit || inMiSplit){ //NSLog(@"InSPLIT"); [userEvents addNodesObjectAtEnd:currentEvent]; [self setCurrentEvent:nil]; } //NSLog(@"Not inSplit"); [self setInSnapshotTag:NO]; [self setInKmSplit:NO]; [self setInMiSplit:NO]; return; } if ( [elementName isEqualToString:@"snapShot"] ){ //NSLog(@"snapShot"); if (! (inMiSplit || inKmSplit)){ //NSLog(@"InSPLIT"); // if we're not in the splits, were in a userClick [userEvents addNodesObjectAtEnd:currentEvent]; [self setCurrentEvent:nil]; return; } //NSLog(@"Not InSPLIT"); return; } if ( [elementName isEqualToString:@"goal"] ){ //NSLog(@"goal"); [self setInGoalTag:NO]; //we're at the end of the goal section [run setGoal:currentGoal]; [self setCurrentGoal:nil]; return; } if ( [elementName isEqualToString:@"vers"] ){ //NSLog(@"vers"); NSNumber *theNumber; theNumber = [NSNumber numberWithString:[self getAndClearCurrentStringValue]]; // make an NSNumber from the current String value .. yea.. categories! [run setXmlVersion:theNumber]; return; } if ( [elementName isEqualToString:@"workoutName"] ){ //NSLog(@"workoutName"); [run setName:[self getAndClearCurrentStringValue]]; return; } if ( [elementName isEqualToString:@"time"] ){ //NSLog(@"time"); NSDate *startDate = [NSDate dateWithIPodXMLString:[self getAndClearCurrentStringValue]]; [run setStart:startDate]; return; } if ( [elementName isEqualToString:@"duration"] ){ //NSLog(@"duration"); NSNumber *durNum = [NSNumber numberWithString:[self getAndClearCurrentStringValue]]; float xmlDuration = [durNum floatValue]; float floatDur = xmlDuration * XML_DURATION_MULTIPLIER; // Note: the duration in the XML file is milleseconds if (inSnapshotTag){ JSMODuration *duration = [NSEntityDescription insertNewObjectForEntityForName:@"Duration" inManagedObjectContext:moc]; [duration setDurationValue:floatDur]; [currentEvent setDuration:duration]; return; } if (inGoalTag){ [currentGoal setDurationValue:floatDur]; return; } [run setDurationValue:floatDur]; return; } if ( [elementName isEqualToString:@"distance"] ){ //NSLog(@"distance"); NSString *dist = [self getAndClearCurrentStringValue]; NSNumber *distanceNumber = [NSNumber numberWithString:dist]; if (inSnapshotTag){ JSMODistance *distance = [NSEntityDescription insertNewObjectForEntityForName:@"Distance" inManagedObjectContext:moc]; [distance setDistance:distanceNumber]; [currentEvent setDistance:distance]; return; } if (inGoalTag){ [currentGoal setDistance:distanceNumber]; return; } [run setDistance:distanceNumber]; return; } if ( [elementName isEqualToString:@"calories"] ){ //NSLog(@"calories"); NSNumber *calories = [NSNumber numberWithString:[self getAndClearCurrentStringValue]]; [self setChildrensValue:calories forSelector:@selector(setCalories:)]; return; } if ( [elementName isEqualToString:@"templateName"] ){ //NSLog(@"templateName"); [run setTemplateName:[self getAndClearCurrentStringValue]]; return; } if ( [elementName isEqualToString:@"empedID"] ){ //NSLog(@"empedID"); [run setUserID:[self getAndClearCurrentStringValue]]; return; } if ( [elementName isEqualToString:@"weight"] ){ [run setWeight:[NSNumber numberWithString:[self getAndClearCurrentStringValue]]]; return; } if ( [elementName isEqualToString:@"extendedData"] ){ //NSLog(@"extendedData"); //the extendedData tag is where the graph data is stored NSArray *slushStringArray = [[self getAndClearCurrentStringValue] componentsSeparatedByString:@", "]; NSEnumerator *enumerator = [slushStringArray objectEnumerator]; NSString *currentStr; while (currentStr = [enumerator nextObject] ){ NSNumber *currentNum = [NSNumber numberWithString:currentStr]; currentEvent = [NSEntityDescription insertNewObjectForEntityForName:@"Event" inManagedObjectContext:moc]; JSMODistance *currentDistance = [NSEntityDescription insertNewObjectForEntityForName:@"Distance" inManagedObjectContext:moc]; [currentDistance setEvent:currentEvent]; [currentDistance setDistance:currentNum]; [runEvents addNodesObjectAtEnd:currentEvent]; } return; } // Below = tags we don't care about right now. // much of it is stuff we can calculate on our own. // if ( [elementName isEqualToString:@"extendedDataList"] || [elementName isEqualToString:@"durationString"] || [elementName isEqualToString:@"distanceString"] || [elementName isEqualToString:@"pace"] || [elementName isEqualToString:@"battery"] || [elementName isEqualToString:@"walkBegin"] || [elementName isEqualToString:@"walkEnd"] || [elementName isEqualToString:@"runBegin"] || [elementName isEqualToString:@"runEnd"] || [elementName isEqualToString:@"stepCounts"] || [elementName isEqualToString:@"templateID"] || [elementName isEqualToString:@"template"] || [elementName isEqualToString:@"device"] || [elementName isEqualToString:@"calibration"] || [elementName isEqualToString:@"userInfo"] || [elementName isEqualToString:@"startTime"] || [elementName isEqualToString:@"sportsData"] ) { [self ignoreAttribute:elementName]; return; } //If we get here, we missed an attribute and should print a warning //NSLog(@"parser MISSED Attribute::: %@", elementName); if (currentStringValue){ currentStringValue = nil; //we didn't nullify the old string if we get an unexpected element //and it needs to be null for the next element begin. } } - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { if (([string length] > 0) && (![string isEqualToString:@"\n"] )) { // we consider zero-length and just linesep strings to be empty. if (!currentStringValue) { currentStringValue = [[NSMutableString alloc] init]; } [currentStringValue appendString:string]; } } // This returns the string of the characters encountered thus far. You may not necessarily get the longest character run. The parser reserves the right to hand these to the delegate as potentially many calls in a row to -parser:foundCharacters: - (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError { NSLog(@"PARSER ERROR"); NSLog([parseError description] ); } // ...and this reports a fatal error to the delegate. The parser will stop parsing. #pragma mark - #pragma mark accessors //=========================================================== // parser //=========================================================== - (NSXMLParser *)parser { return parser; } - (void)setParser:(NSXMLParser *)aParser { if (parser != aParser) { [aParser retain]; [parser release]; parser = aParser; } } //=========================================================== // url //=========================================================== - (NSURL *)url { return url; } - (void)setUrl:(NSURL *)anUrl { if (url != anUrl) { [anUrl retain]; [url release]; url = anUrl; } } //=========================================================== // moc //=========================================================== - (NSManagedObjectContext *)moc { return moc; } - (void)setMoc:(NSManagedObjectContext *)aMoc { if (moc != aMoc) { [aMoc retain]; [moc release]; moc = aMoc; } } //=========================================================== // currentAttributes //=========================================================== - (NSDictionary *)currentAttributes { return currentAttributes; } - (void)setCurrentAttributes:(NSDictionary *)aCurrentAttributes { if (currentAttributes != aCurrentAttributes) { [aCurrentAttributes retain]; [currentAttributes release]; currentAttributes = aCurrentAttributes; } } //=========================================================== // nextString //=========================================================== - (NSString *)nextString { return nextString; } - (void)setNextString:(NSString *)aNextString { if (nextString != aNextString) { [aNextString retain]; [nextString release]; nextString = aNextString; } } //=========================================================== // inGoalTag //=========================================================== - (BOOL)inGoalTag { return inGoalTag; } - (void)setInGoalTag:(BOOL)flag { inGoalTag = flag; } //=========================================================== // inSnapshotTag //=========================================================== - (BOOL)inSnapshotTag { return inSnapshotTag; } - (void)setInSnapshotTag:(BOOL)flag { inSnapshotTag = flag; } //=========================================================== // inSummaryTag //=========================================================== - (BOOL)inSummaryTag { return inSummaryTag; } - (void)setInSummaryTag:(BOOL)flag { inSummaryTag = flag; } //=========================================================== // inKmSplit //=========================================================== - (BOOL)inKmSplit { return inKmSplit; } - (void)setInKmSplit:(BOOL)flag { inKmSplit = flag; } //=========================================================== // inMiSplit //=========================================================== - (BOOL)inMiSplit { return inMiSplit; } - (void)setInMiSplit:(BOOL)flag { inMiSplit = flag; } //=========================================================== // currentStringValue //=========================================================== - (NSMutableString *)currentStringValue { return currentStringValue; } - (void)setCurrentStringValue:(NSMutableString *)aCurrentStringValue { if (currentStringValue != aCurrentStringValue) { [aCurrentStringValue retain]; [currentStringValue release]; currentStringValue = aCurrentStringValue; } } //=========================================================== // run //=========================================================== - (JSMORun *)run { return run; } - (void)setRun:(JSMORun *)aRun { if (run != aRun) { [aRun retain]; [run release]; run = aRun; } } //=========================================================== // runEvents //=========================================================== - (JSMORunEvents *)runEvents { return runEvents; } - (void)setRunEvents:(JSMORunEvents *)aRunEvents { if (runEvents != aRunEvents) { [aRunEvents retain]; [runEvents release]; runEvents = aRunEvents; } } //=========================================================== // userEvents //=========================================================== - (JSMOUserEvents *)userEvents { return userEvents; } - (void)setUserEvents:(JSMOUserEvents *)anUserEvents { if (userEvents != anUserEvents) { [anUserEvents retain]; [userEvents release]; userEvents = anUserEvents; } } //=========================================================== // currentGoal //=========================================================== - (JSMOGoal *)currentGoal { return currentGoal; } - (void)setCurrentGoal:(JSMOGoal *)aCurrentGoal { if (currentGoal != aCurrentGoal) { [aCurrentGoal retain]; [currentGoal release]; currentGoal = aCurrentGoal; } } //=========================================================== // currentEvent //=========================================================== - (JSMOEvent *)currentEvent { return currentEvent; } - (void)setCurrentEvent:(JSMOEvent *)aCurrentEvent { if (currentEvent != aCurrentEvent) { [aCurrentEvent retain]; [currentEvent release]; currentEvent = aCurrentEvent; } } //=========================================================== // dealloc //=========================================================== - (void)dealloc { [self setParser:nil]; [self setUrl:nil]; [self setMoc:nil]; [self setCurrentAttributes:nil]; [self setNextString:nil]; [self setCurrentStringValue:nil]; [self setRun:nil]; [self setRunEvents:nil]; [self setUserEvents:nil]; [self setCurrentGoal:nil]; [self setCurrentEvent:nil]; [super dealloc]; } @end