-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathOTRestModelMapper.m
More file actions
413 lines (367 loc) · 15.7 KB
/
OTRestModelMapper.m
File metadata and controls
413 lines (367 loc) · 15.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
//
// OTModelMapper.m
// OTRestFramework
//
// Created by Blake Watters on 8/14/09.
// Copyright 2009 Two Toasters. All rights reserved.
//
#import <objc/message.h>
#import "OTRestModelMapper.h"
#import "OTRestModelMapper_Private.h"
#import "ElementParser.h"
#import "JSON.h"
// Used for detecting property types at runtime
#import <objc/runtime.h>
@implementation OTRestModelMapper
@synthesize format = _format;
- (id)init {
if (self = [super init]) {
_elementToClassMappings = [[NSMutableDictionary alloc] init];
_format = OTRestMappingFormatXML;
}
return self;
}
- (void)dealloc {
[_elementToClassMappings release];
[super dealloc];
}
- (BOOL)mappingFromJSON {
return _format == OTRestMappingFormatJSON;
}
- (BOOL)mappingFromXML {
return _format == OTRestMappingFormatXML;
}
- (void)registerModel:(Class)aClass forElementNamed:(NSString*)elementName {
[_elementToClassMappings setObject:aClass forKey:elementName];
}
- (id)buildModelFromString:(NSString*)string {
id object = nil;
if ([self mappingFromJSON]) {
object = [self buildModelFromJSON:string];
} else if ([self mappingFromXML]) {
Element* e = [[[[[ElementParser alloc] init] autorelease] parseXML:string] firstChild];
object = [self buildModelFromXML:e];
} else {
[NSException raise:@"No Parsing Style Set" format:@"you must specify a valid mapping format"];
}
return object;
}
- (NSArray*)buildModelsFromString:(NSString*)string {
NSMutableArray* objects = [NSMutableArray array];
if ([self mappingFromJSON]) {
NSArray* collectionDicts = [[[[SBJSON alloc] init] autorelease] objectWithString:string];
for (NSDictionary* dict in collectionDicts) {
id object = [self buildModelFromJSONDictionary:dict];
[objects addObject:object];
}
} else if ([self mappingFromXML]) {
Element* collectionElement = [[[[[ElementParser alloc] init] autorelease] parseXML:string] firstChild];
for (Element* e in [collectionElement childElements]) {
id object = [self buildModelFromXML:e];
[objects addObject:object];
}
} else {
[NSException raise:@"No Parsing Style Set" format:@"you must specify a valid mapping format"];
}
return (NSArray*)objects;
}
#pragma mark -
#pragma mark shared parsing behavior
- (void)updateObject:(id)model ifNewPropertyPropertyValue:(id)propertyValue forPropertyNamed:(NSString*)propertyName {
id currentValue = [model valueForKey:propertyName];
if (nil == currentValue && nil == propertyValue) {
// Don't set the property, both are nil
} else if (nil == propertyValue || [propertyValue isKindOfClass:[NSNull class]]) {
// Clear out the value to reset it
[model setNilValueForKey:propertyName];
} else if (currentValue == nil || [currentValue isKindOfClass:[NSNull class]]) {
// Existing value was nil, just set the property and be happy
[model setValue:propertyValue forKey:propertyName];
} else {
SEL comparisonSelector;
if ([propertyValue isKindOfClass:[NSString class]]) {
comparisonSelector = @selector(isEqualToString:);
} else if ([propertyValue isKindOfClass:[NSNumber class]]) {
comparisonSelector = @selector(isEqualToNumber:);
} else if ([propertyValue isKindOfClass:[NSDate class]]) {
comparisonSelector = @selector(isEqualToDate:);
} else {
[NSException raise:@"NoComparisonSelectorFound" format:@"You need a comparison selector for %@ (%@)", propertyName, [propertyValue class]];
}
// Comparison magic using function pointers. See this page for details: http://www.red-sweater.com/blog/320/abusing-objective-c-with-class
// Original code courtesy of Greg Parker
// This is necessary because isEqualToNumber will return negative integer values that aren't coercable directly to BOOL's without help [sbw]
BOOL (*ComparisonSender)(id, SEL, id) = (BOOL (*)(id, SEL, id)) objc_msgSend;
BOOL areEqual = ComparisonSender(currentValue, comparisonSelector, propertyValue);
if (NO == areEqual) {
//NSLog(@"Setting property %@ to new value %@", propertyName, propertyValue);
[model setValue:propertyValue forKey:propertyName];
}
}
}
#pragma mark -
#pragma mark JSON Parsing
- (id)buildModelFromJSON:(NSString*)JSON {
SBJsonParser* parser = [[[SBJsonParser alloc] init] autorelease];
NSDictionary* jsonDict = [parser objectWithString:JSON];
if (jsonDict == nil) {
return nil;
}
return [self buildModelFromJSONDictionary:jsonDict];
}
- (id)buildModelFromJSONDictionary:(NSDictionary*)dict {
assert([[dict allKeys] count] == 1);
NSString* keyName = [[dict allKeys] objectAtIndex:0];
Class class = [_elementToClassMappings objectForKey:keyName];
return [self createOrUpdateInstanceOf:class fromJSONDictionary:[dict objectForKey:keyName]];
}
- (id)createOrUpdateInstanceOf:(Class)class fromJSONDictionary:(NSDictionary*)dict {
id object = nil;
if ([class respondsToSelector:@selector(findByPrimaryKey:)]) {
// TODO: factor to class method? incase it is not a number
NSNumber* pk = [dict objectForKey:[class primaryKey]];
object = [class findByPrimaryKey:pk];
}
// instantiate if object is nil
if (object == nil) {
if ([class respondsToSelector:@selector(newObject)]) {
object = [class newObject];
} else {
object = [[[class alloc] init] autorelease];
}
}
// check to see if we should hand the object the JSON to set it's own properties
// (custom implementation)
if ([object respondsToSelector:@selector(digestJSONDictionary:)]) {
[object digestJSONDictionary:dict];
} else {
// update attributes
[self setAttributes:object fromJSONDictionary:dict];
}
return object;
}
- (void)setAttributes:(id)object fromJSONDictionary:(NSDictionary*)dict {
[self setPropertiesOfModel:object fromJSONDictionary:dict];
[self setRelationshipsOfModel:object fromJSONDictionary:dict];
}
- (void)setPropertiesOfModel:(id)model fromJSONDictionary:(NSDictionary*)dict {
for (NSString* selector in [[model class] elementToPropertyMappings]) {
NSString* propertyName = [[[model class] elementToPropertyMappings] objectForKey:selector];
NSString* propertyType = [self typeNameForProperty:propertyName ofClass:[model class] typeHint:nil];
id propertyValue = [dict objectForKey:selector];
// Types of objects SBJSON does not handle:
if ([propertyType isEqualToString:@"NSDate"]) {
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
// Times coming back are in utc. we should convert them to the local timezone
// TODO: Make sure this is working correctly
[formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
[formatter setDateFormat:kRailsToXMLDateFormatterString];
propertyValue = [formatter dateFromString:propertyValue];
[formatter release];
}
[self updateObject:model ifNewPropertyPropertyValue:propertyValue forPropertyNamed:propertyName];
}
}
- (void)setRelationshipsOfModel:(id)model fromJSONDictionary:(NSDictionary*)dict {
for (NSString* selector in [[model class] elementToRelationshipMappings]) {
NSString* propertyName = [[[model class] elementToRelationshipMappings] objectForKey:selector];
if ([self isParentSelector:selector]) {
NSMutableSet* children = [NSMutableSet set];
// If the collection key doesn't appear, we will not set the collection to nil.
NSString* collectionKey = [self containingElementNameForSelector:selector];
// Used to figure out what class to map to, since we don't have element names for the dictionaries in the array
NSString* objectKey = [self childElementNameForSelelctor:selector];
NSArray* objects = [dict objectForKey:collectionKey];
if (objects != nil) {
for (NSDictionary* childDict in objects) {
Class class = [_elementToClassMappings objectForKey:objectKey];
[children addObject:[self createOrUpdateInstanceOf:class fromJSONDictionary:childDict]];
}
[model setValue:(NSSet*)children forKey:propertyName];
}
} else {
NSDictionary* objectDict = [dict objectForKey:selector];
Class class = [_elementToClassMappings objectForKey:selector];
id child = [self createOrUpdateInstanceOf:class fromJSONDictionary:objectDict];
[model setValue:child forKey:propertyName];
}
}
}
#pragma mark -
#pragma mark XML Parsing
- (id)buildModelFromXML:(Element*)XML {
if (XML == nil) {
return nil;
}
NSString* elementName = [XML key];
Class class = [_elementToClassMappings objectForKey:elementName];
if (class == nil) {
NSLog(@"Encountered an unmapped class while processing XML Element: %@", XML);
[NSException raise:@"NoClassMappingForModel" format:@"No Class Mapping for Element name '%@'", elementName];
}
id object = [self createOrUpdateInstanceOf:class fromXML:XML];
return object;
}
- (id)createOrUpdateInstanceOf:(Class)class fromXML:(Element*)XML {
id object = nil;
// Find by PK, if it responds to it
if ([class respondsToSelector:@selector(findByPrimaryKey:)]) {
// TODO: factor to class method? incase it is not a number
NSNumber* pk = [XML contentsNumberOfChildElement:[class primaryKeyElement]];
//NSLog(@"Attempting to find object by primary key %@ via primaryKeyElement %@", pk, [class primaryKeyElement]);
object = [class findByPrimaryKey:pk];
}
// instantiate if object is nil
if (object == nil) {
if ([class respondsToSelector:@selector(newObject)]) {
object = [class newObject];
} else {
object = [[[class alloc] init] autorelease];
}
}
// check to see if we should hand the object the xml to set it's own properties
// (custom implementation)
if ([object respondsToSelector:@selector(digestXML:)]) {
[object digestXML:XML];
} else {
// update attributes
[self setAttributes:object fromXML:XML];
}
return object;
}
- (void)setAttributes:(id)object fromXML:(Element*)XML {
[self setPropertiesOfModel:object fromXML:XML];
[self setRelationshipsOfModel:object fromXML:XML];
}
- (id)propertyValueForElement:(Element*)propertyElement type:(NSString*)type{
//NSString* typeHint = [propertyElement attribute:@"type"];
id propertyValue = nil;
if ([type isEqualToString:@"NSString"]) {
propertyValue = [propertyElement contentsText];
} else if ([type isEqualToString:@"NSNumber"]) {
propertyValue = [propertyElement contentsNumber];
} else if ([type isEqualToString:@"NSDecimalNumber"]) {
propertyValue = [NSDecimalNumber decimalNumberWithString:[propertyElement contentsText]];
} else if ([type isEqualToString:@"NSDate"]) {
NSString* dateString = [propertyElement contentsText];
if (nil != dateString) {
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
// Times coming back are in utc. we should convert them to the local timezone
// TODO: Need a way to handle date/time formats. Maybe part of the mapper?
[formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
[formatter setDateFormat:kRailsToXMLDateTimeFormatterString];
propertyValue = [formatter dateFromString:dateString];
if (nil == propertyValue) {
[formatter setDateFormat:kRailsToXMLDateFormatterString];
propertyValue = [formatter dateFromString:dateString];
}
[formatter release];
}
} else if ([type isEqualToString:@"nil"]) {
[NSException raise:@"PropertyTypeError" format:@"Don't know how to handle property type '%@'", type];
}
return propertyValue;
}
- (void)setPropertiesOfModel:(id)model fromXML:(Element*)XML {
for (NSString* selector in [[model class] elementToPropertyMappings]) {
NSString* propertyName = [[[model class] elementToPropertyMappings] objectForKey:selector];
Element* propertyElement = [XML selectElement:selector];
NSString* typeHint = [propertyElement attribute:@"type"];
NSString* propertyType = [self typeNameForProperty:propertyName ofClass:[model class] typeHint:typeHint];
//NSLog(@"The propertyType is %@", propertyType);
id propertyValue = [self propertyValueForElement:propertyElement type:propertyType]; // valueForElement instead???
if (typeHint) {
//NSLog(@"TypeHint is %@", typeHint);
if ([typeHint isEqualToString:@"boolean"]) {
// Booleans must be cast to NSNumber...
NSLog(@"Boolean value before cast: %@", propertyValue);
propertyValue = [NSNumber numberWithBool:[propertyValue boolValue]];
NSLog(@"Boolean value after cast: %@", propertyValue);
}
}
//NSLog(@"Trying potential update to %@ with value %@", propertyName, propertyValue);
[self updateObject:model ifNewPropertyPropertyValue:propertyValue forPropertyNamed:propertyName];
}
}
- (void)setRelationshipsOfModel:(id)model fromXML:(Element*)XML {
for (NSString* selector in [[[model class] elementToRelationshipMappings] allKeys]) {
NSString* propertyName = [[[model class] elementToRelationshipMappings] objectForKey:selector];
if ([self isParentSelector:selector]) {
NSMutableSet* children = [NSMutableSet set];
// If the parent element doesn't appear, we will not set the collection to nil.
NSString* containingElementName = [self containingElementNameForSelector:selector];
if ([XML selectElement:containingElementName] != nil) {
NSArray* childrenElements = [XML selectElements:selector];
for (Element* childElement in childrenElements) {
[children addObject:[self buildModelFromXML:childElement]];
}
[model setValue:(NSSet*)children forKey:propertyName];
}
} else {
Element* childElement = [XML selectElement:selector];
id child = [self buildModelFromXML:childElement];
[model setValue:child forKey:propertyName];
}
}
}
#pragma mark -
#pragma mark selector methods
- (BOOL)isParentSelector:(NSString*)key {
return !NSEqualRanges([key rangeOfString:@" > "], NSMakeRange(NSNotFound, 0));
}
- (NSString*)containingElementNameForSelector:(NSString*)selector {
return [[selector componentsSeparatedByString:@" > "] objectAtIndex:0];
}
- (NSString*)childElementNameForSelelctor:(NSString*)selector {
return [[selector componentsSeparatedByString:@" > "] objectAtIndex:1];
}
#pragma mark -
#pragma mark Property Type Methods
- (NSString*)propertyTypeFromAttributeString:(NSString*)attributeString {
NSString *type = [NSString string];
NSScanner *typeScanner = [NSScanner scannerWithString:attributeString];
[typeScanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"@"] intoString:NULL];
// we are not dealing with an object
if([typeScanner isAtEnd]) {
return @"NULL";
}
[typeScanner scanCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\"@"] intoString:NULL];
// this gets the actual object type
[typeScanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\""] intoString:&type];
return type;
}
- (NSDictionary *)propertyNamesAndTypesForClass:(Class)class {
NSMutableDictionary *propertyNames = [NSMutableDictionary dictionary];
//include superclass properties
Class currentClass = class;
while (currentClass != nil) {
// Get the raw list of properties
unsigned int outCount;
objc_property_t *propList = class_copyPropertyList(currentClass, &outCount);
// Collect the property names
int i;
NSString *propName;
for (i = 0; i < outCount; i++) {
// TODO: Add support for custom getter and setter methods
// property_getAttributes() returns everything we need to implement this...
// See: http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW5
objc_property_t * prop = propList + i;
NSString *type = [NSString stringWithCString:property_getAttributes(*prop) encoding:NSUTF8StringEncoding];
propName = [NSString stringWithCString:property_getName(*prop) encoding:NSUTF8StringEncoding];
if (![propName isEqualToString:@"_mapkit_hasPanoramaID"]) {
[propertyNames setObject:[self propertyTypeFromAttributeString:type] forKey:propName];
}
}
free(propList);
currentClass = [currentClass superclass];
}
return propertyNames;
}
- (NSString*)typeNameForProperty:(NSString*)property ofClass:(Class)class typeHint:(NSString*)typeHint {
if ([typeHint isEqualToString:@"boolean"]) {
return @"NSString";
} else {
return [[self propertyNamesAndTypesForClass:class] objectForKey:property];
}
}
@end