// // 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 "JSRunModel.h" #import "JSRunEvent.h" #import "JSRunGoal.h" #import "JSRunDistanceHistory.h" #import "NSNumberAdditions.h" #import "NSDateAdditions.h" #define XML_DURATION_MULTIPLIER .001 //the durations in the file are in millesecs #define LogMethod() NSLog(@"-[%@ %s]", self, _cmd) @interface JSRunXmlParser (PrivateAPI) - (NSXMLParser *)parser; - (void)setParser:(NSXMLParser *)aParser; - (void)setChildrensValue:(id)value forSelector:(SEL)selector; - (NSString *)getAndClearCurrentStringValue; - (void)ignoreAttribute:(NSString *)name; @end @implementation JSRunXmlParser - (id)init { //NSString *filePath = [@"~/Desktop/testIpodWorkout.xml" stringByExpandingTildeInPath]; NSURL *blankUrl = [[NSURL alloc] init]; self = [self initWithContentsOfURL:blankUrl]; return self; } - (id)initWithContentsOfURL:(NSURL *)aUrl { self = [super init]; if (self != nil){ [aUrl retain]; url = aUrl; parser = [[NSXMLParser alloc] initWithContentsOfURL:url]; [parser setDelegate:self]; currentAttributes = [[NSDictionary alloc] init]; run = [[JSRunModel alloc] init]; inGoalTag = NO; inSnapshotTag = NO; inSummaryTag = NO; inKmSplit = NO; inMiSplit = NO; currentGoal = nil; currentRunEvent = nil; currentStringValue = nil; } return self; } - (void)parse { JSRunModel *newRun = [[[JSRunModel alloc] init] autorelease]; [self setRun:newRun]; // set a clean run before parsing // this lets us use this over and over and keep pulling // the run attribute out when needed [parser parse]; } - (void)setChildrensValue:(id)value forSelector:(SEL)selector { if (inSnapshotTag) { if ([currentRunEvent respondsToSelector:selector]) { [currentRunEvent 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:[[[JSRunGoal alloc] init] autorelease]]; //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 setCurrentRunEvent:[[[JSRunEvent alloc] init] autorelease]]; [currentRunEvent 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 setCurrentRunEvent:[[[JSRunEvent alloc] init] autorelease]]; [currentRunEvent setType:@"kmSplit"]; return; } //If we get here, we missed an attribute and should print a warning NSLog(@"parser MISSED Snapshot Type::: %@", elementName); [self setCurrentRunEvent:[[[JSRunEvent alloc] init] autorelease]]; // 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 setCurrentRunEvent:[[[JSRunEvent alloc] init] autorelease]]; NSString *eventType = [currentAttributes valueForKey:@"event"]; [currentRunEvent setType:eventType]; return; } } } - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { if ( [elementName isEqualToString:@"runSummary"] ){ [self setInSummaryTag:NO]; return; } if ( [elementName isEqualToString:@"snapShotList"] ){ if (inKmSplit || inMiSplit){ [run addRunEvent:currentRunEvent]; [self setCurrentRunEvent:nil]; } [self setInSnapshotTag:NO]; [self setInKmSplit:NO]; [self setInMiSplit:NO]; return; } if ( [elementName isEqualToString:@"snapShot"] ){ if (! (inMiSplit || inKmSplit)){ // if we're not in the splits, were in a userClick [run addRunEvent:currentRunEvent]; [self setCurrentRunEvent:nil]; return; } return; } if ( [elementName isEqualToString:@"goal"] ){ [self setInGoalTag:NO]; //we're at the end of the goal section [run setGoal:currentGoal]; [self setCurrentGoal:nil]; return; } if ( [elementName isEqualToString:@"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"] ){ [run setName:[self getAndClearCurrentStringValue]]; return; } if ( [elementName isEqualToString:@"time"] ){ NSDate *startDate = [NSDate DateWithIPodXMLString:[self getAndClearCurrentStringValue]]; //see NSDateAdditions.h [run setStart:startDate]; return; } if ( [elementName isEqualToString:@"duration"] ){ NSNumber *durNum = [NSNumber numberWithString:[self getAndClearCurrentStringValue]]; float xmlDuration = [durNum floatValue]; float floatDur = xmlDuration * XML_DURATION_MULTIPLIER; NSNumber *duration = [NSNumber numberWithFloat:floatDur]; // the duration in the XML file is milleseconds if (inSnapshotTag){ [currentRunEvent addDuration:duration]; return; } if (inGoalTag){ [currentGoal setDuration:duration]; return; } [run setDuration:duration]; return; } if ( [elementName isEqualToString:@"distance"] ){ NSString *dist = [self getAndClearCurrentStringValue]; NSNumber *distance = [NSNumber numberWithString:dist]; if (inSnapshotTag){ [currentRunEvent addDistance:distance]; return; } if (inGoalTag){ [currentGoal setDistance:distance]; return; } [run setDistance:distance]; return; } if ( [elementName isEqualToString:@"calories"] ){ NSNumber *calories = [NSNumber numberWithString:[self getAndClearCurrentStringValue]]; [self setChildrensValue:calories forSelector:@selector(setCalories:)]; return; } if ( [elementName isEqualToString:@"templateName"] ){ [run setTemplateName:[self getAndClearCurrentStringValue]]; return; } if ( [elementName isEqualToString:@"empedID"] ){ [run setUserID:[self getAndClearCurrentStringValue]]; return; } if ( [elementName isEqualToString:@"weight"] ){ [run setWeight:[NSNumber numberWithString:[self getAndClearCurrentStringValue]]]; return; } if ( [elementName isEqualToString:@"extendedData"] ){ JSRunDistanceHistory *history = [[[JSRunDistanceHistory alloc] init] autorelease]; NSArray *slushStringArray = [[self getAndClearCurrentStringValue] componentsSeparatedByString:@", "]; NSMutableArray *slushNumberArray = [[[NSMutableArray alloc] initWithCapacity:[slushStringArray count]] autorelease]; NSEnumerator *enumerator; NSString *string; enumerator = [slushStringArray objectEnumerator]; while (string = [enumerator nextObject] ){ [slushNumberArray addObject:[NSNumber numberWithString:string]]; } //enumerate the string values and turn 'em into numbers [history setDistances:[NSArray arrayWithArray:slushNumberArray]]; [history setAttributes:currentAttributes]; [run setJSRunDistanceHistory:history]; 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 } } - (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 { LogMethod(); 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 #pragma mark private //=========================================================== // parser //=========================================================== - (NSXMLParser *)parser { return parser; } - (void)setParser:(NSXMLParser *)aParser { if (parser != aParser){ [aParser retain]; [parser release]; parser = aParser; } } #pragma mark public //=========================================================== // url //=========================================================== - (NSURL *)url { return url; } - (void)setUrl:(NSURL *)anUrl { if (url != anUrl){ [anUrl retain]; [url release]; url = anUrl; [self setParser:[[[NSXMLParser alloc] initWithContentsOfURL:url] autorelease]]; } } //=========================================================== // run //=========================================================== - (JSRunModel *)run { return run; } - (void)setRun:(JSRunModel *)aRun { if (run != aRun){ [aRun retain]; [run release]; run = aRun; } } //=========================================================== // 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; } //=========================================================== // currentGoal //=========================================================== - (JSRunGoal *)currentGoal { return currentGoal; } - (void)setCurrentGoal:(JSRunGoal *)aCurrentGoal { if (currentGoal != aCurrentGoal){ [aCurrentGoal retain]; [currentGoal release]; currentGoal = aCurrentGoal; } } //=========================================================== // currentRunEvent //=========================================================== - (JSRunEvent *)currentRunEvent { return currentRunEvent; } - (void)setCurrentRunEvent:(JSRunEvent *)aCurrentRunEvent { if (currentRunEvent != aCurrentRunEvent){ [aCurrentRunEvent retain]; [currentRunEvent release]; currentRunEvent = aCurrentRunEvent; } } //=========================================================== // dealloc //=========================================================== - (void)dealloc { currentStringValue = nil; [self setParser:nil]; [self setRun:nil]; [self setCurrentAttributes:nil]; [self setNextString:nil]; [self setCurrentGoal:nil]; [self setCurrentRunEvent:nil]; [super dealloc]; } @end