Reworked Brendan Ribera's contributions around time zone handling to eliminate the use of transient

NSDateFormatters, added a preferredDateFormatter for use when serializing dates to strings,
replaced the use of the description method for date encoding to strings with invocation of the
preferredDateFormatter, added new attribute transformation strategy from NSDate -> NSString properties
(also using the preferred date formatter), and provided customization support for date handling globally
and on a per-mapping basis. closes #200, closes #313, closes #309, closes #308
This commit is contained in:
Blake Watters
2011-09-05 17:25:43 -04:00
parent ad754a9b2a
commit 54007c78d4
10 changed files with 388 additions and 49 deletions

View File

@@ -226,4 +226,4 @@
@end
#endif
#endif

View File

@@ -33,13 +33,14 @@ relationship. Relationships are processed using an object mapping as well.
*/
@interface RKObjectMapping : NSObject <RKObjectMappingDefinition> {
Class _objectClass;
NSMutableArray* _mappings;
NSMutableArray* _dateFormatStrings;
NSString* _rootKeyPath;
NSMutableArray *_mappings;
NSString *_rootKeyPath;
BOOL _setDefaultValueForMissingAttributes;
BOOL _setNilForMissingRelationships;
BOOL _forceCollectionMapping;
BOOL _performKeyValueValidation;
NSArray *_dateFormatters;
NSDateFormatter *_preferredDateFormatter;
}
/**
@@ -121,12 +122,32 @@ relationship. Relationships are processed using an object mapping as well.
*/
@property (nonatomic, assign) BOOL forceCollectionMapping;
/**
An array of date format strings to apply when mapping a
String attribute to a NSDate property. Each format string will be applied
until the date formatter does not return nil.
An array of NSDateFormatter objects to use when mapping string values
into NSDate attributes on the target objectClass. Each date formatter
will be invoked with the string value being mapped until one of the date
formatters does not return nil.
Defaults to the application-wide collection of date formatters configured via:
[RKObjectMapping setDefaultDateFormatters:]
@see [RKObjectMapping defaultDateFormatters]
*/
@property (nonatomic, retain) NSMutableArray* dateFormatStrings;
@property (nonatomic, retain) NSArray *dateFormatters;
/**
The NSDateFormatter instance for your application's preferred date
and time configuration. This date formatter will be used when generating
string representations of NSDate attributes (i.e. during serialization to
URL form encoded or JSON format).
Defaults to the application-wide preferred date formatter configured via:
[RKObjectMapping setPreferredDateFormatter:]
@see [RKObjectMapping preferredDateFormatter]
*/
@property (nonatomic, retain) NSDateFormatter *preferredDateFormatter;
/**
Returns an object mapping for the specified class that is ready for configuration
@@ -416,3 +437,78 @@ relationship. Relationships are processed using an object mapping as well.
- (Class)classForProperty:(NSString*)propertyName;
@end
/////////////////////////////////////////////////////////////////////////////
/**
Defines the inteface for configuring time and date formatting handling within RestKit
object mappings. For performance reasons, RestKit reuses a pool of date formatters rather
than constructing them at mapping time. This collection of date formatters can be configured
on a per-object mapping or application-wide basis using the static methods exposed in this
category.
*/
@interface RKObjectMapping (DateAndTimeFormatting)
/**
Returns the collection of default date formatters that will be used for all object mappings
that have not been configured specifically.
Out of the box, RestKit initializes the following default date formatters for you in the
UTC time zone:
* yyyy-MM-dd'T'HH:mm:ss'Z'
* MM/dd/yyyy
@return An array of NSDateFormatter objects used when mapping strings into NSDate attributes
*/
+ (NSArray *)defaultDateFormatters;
/**
Sets the collection of default date formatters to the specified array. The array should
contain configured instances of NSDateFormatter in the order in which you want them applied
during object mapping operations.
@param dateFormatters An array of date formatters to replace the existing defaults
@see defaultDateFormatters
*/
+ (void)setDefaultDateFormatters:(NSArray *)dateFormatters;
/**
Adds a date formatter instance to the default collection
@param dateFormatter An NSDateFormatter object to append to the end of the default formatters collection
@see defaultDateFormatters
*/
+ (void)addDefaultDateFormatter:(NSDateFormatter *)dateFormatter;
/**
Convenience method for quickly constructing a date formatter and adding it to the collection of default
date formatters
@param dateFormatString The dateFormat string to assign to the newly constructed NSDateFormatter instance
@param nilOrTimeZone The NSTimeZone object to configure on the NSDateFormatter instance. Defaults to UTC time.
@result A new NSDateFormatter will be appended to the defaultDateFormatters with the specified date format and time zone
@see NSDateFormatter
*/
+ (void)addDefaultDateFormatterForString:(NSString *)dateFormatString inTimeZone:(NSTimeZone *)nilOrTimeZone;
/**
Returns the preferred date formatter to use when generating NSString representations from NSDate attributes.
This type of transformation occurs when RestKit is mapping local objects into JSON or form encoded serializations
that do not have a native time construct.
Defaults to a date formatter configured for the UTC Time Zone with a format string of "yyyy-MM-dd HH:mm:ss Z"
@return The preferred NSDateFormatter to use when serializing dates into strings
*/
+ (NSDateFormatter *)preferredDateFormatter;
/**
Sets the preferred date formatter to use when generating NSString representations from NSDate attributes.
This type of transformation occurs when RestKit is mapping local objects into JSON or form encoded serializations
that do not have a native time construct.
@param dateFormatter The NSDateFormatter to configured as the new preferred instance
*/
+ (void)setPreferredDateFormatter:(NSDateFormatter *)dateFormatter;
@end

View File

@@ -14,17 +14,35 @@
// Constants
NSString* const RKObjectMappingNestingAttributeKeyName = @"<RK_NESTING_ATTRIBUTE>";
// Default NSTimeZone
static NSTimeZone* defaultTimeZone = nil;
@implementation RKObjectMapping
@synthesize objectClass = _objectClass;
@synthesize mappings = _mappings;
@synthesize dateFormatStrings = _dateFormatStrings;
@synthesize dateFormatters = _dateFormatters;
@synthesize preferredDateFormatter = _preferredDateFormatter;
@synthesize rootKeyPath = _rootKeyPath;
@synthesize setDefaultValueForMissingAttributes = _setDefaultValueForMissingAttributes;
@synthesize setNilForMissingRelationships = _setNilForMissingRelationships;
@synthesize forceCollectionMapping = _forceCollectionMapping;
@synthesize performKeyValueValidation = _performKeyValueValidation;
+ (NSTimeZone *)defaultTimeZone {
if (defaultTimeZone) {
return defaultTimeZone;
} else {
return [NSTimeZone defaultTimeZone];
}
}
+ (void)setDefaultTimeZone:(NSTimeZone *)timeZone {
[timeZone retain];
[defaultTimeZone release];
defaultTimeZone = timeZone;
}
+ (id)mappingForClass:(Class)objectClass {
RKObjectMapping* mapping = [self new];
mapping.objectClass = objectClass;
@@ -55,7 +73,6 @@ NSString* const RKObjectMappingNestingAttributeKeyName = @"<RK_NESTING_ATTRIBUTE
self = [super init];
if (self) {
_mappings = [NSMutableArray new];
_dateFormatStrings = [[NSMutableArray alloc] initWithObjects:@"yyyy-MM-dd'T'HH:mm:ss'Z'", @"MM/dd/yyyy", nil];
self.setDefaultValueForMissingAttributes = NO;
self.setNilForMissingRelationships = NO;
self.forceCollectionMapping = NO;
@@ -68,7 +85,8 @@ NSString* const RKObjectMappingNestingAttributeKeyName = @"<RK_NESTING_ATTRIBUTE
- (void)dealloc {
[_rootKeyPath release];
[_mappings release];
[_dateFormatStrings release];
[_dateFormatters release];
[_preferredDateFormatter release];
[super dealloc];
}
@@ -256,4 +274,78 @@ NSString* const RKObjectMappingNestingAttributeKeyName = @"<RK_NESTING_ATTRIBUTE
return [[RKObjectPropertyInspector sharedInspector] typeForProperty:propertyName ofClass:self.objectClass];
}
#pragma mark - Date and Time
- (NSDateFormatter *)preferredDateFormatter {
return _preferredDateFormatter ? _preferredDateFormatter : [RKObjectMapping preferredDateFormatter];
}
- (NSArray *)dateFormatters {
return _dateFormatters ? _dateFormatters : [RKObjectMapping defaultDateFormatters];
}
@end
/////////////////////////////////////////////////////////////////////////////
static NSMutableArray *defaultDateFormatters = nil;
static NSDateFormatter *preferredDateFormatter = nil;
@implementation RKObjectMapping (DateAndTimeFormatting)
+ (NSArray *)defaultDateFormatters {
if (!defaultDateFormatters) {
defaultDateFormatters = [[NSMutableArray alloc] initWithCapacity:2];
// Setup the default formatters
[self addDefaultDateFormatterForString:@"yyyy-MM-dd'T'HH:mm:ss'Z'" inTimeZone:nil];
[self addDefaultDateFormatterForString:@"MM/dd/yyyy" inTimeZone:nil];
}
return defaultDateFormatters;
}
+ (void)setDefaultDateFormatters:(NSArray *)dateFormatters {
[defaultDateFormatters release];
defaultDateFormatters = nil;
if (dateFormatters) {
defaultDateFormatters = [[NSMutableArray alloc] initWithArray:dateFormatters];
}
}
+ (void)addDefaultDateFormatter:(NSDateFormatter *)dateFormatter {
[self defaultDateFormatters];
[defaultDateFormatters addObject:dateFormatter];
}
+ (void)addDefaultDateFormatterForString:(NSString *)dateFormatString inTimeZone:(NSTimeZone *)nilOrTimeZone {
NSDateFormatter *dateFormatter = [NSDateFormatter new];
dateFormatter.dateFormat = dateFormatString;
if (nilOrTimeZone) {
dateFormatter.timeZone = nilOrTimeZone;
} else {
dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
}
[self addDefaultDateFormatter:dateFormatter];
}
+ (NSDateFormatter *)preferredDateFormatter {
if (!preferredDateFormatter) {
// A date formatter that matches the output of [NSDate description]
preferredDateFormatter = [NSDateFormatter new];
[preferredDateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss Z"];
preferredDateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
}
return preferredDateFormatter;
}
+ (void)setPreferredDateFormatter:(NSDateFormatter *)dateFormatter {
[dateFormatter retain];
[preferredDateFormatter release];
preferredDateFormatter = dateFormatter;
}
@end

View File

@@ -97,18 +97,16 @@ BOOL RKObjectIsValueEqualToValue(id sourceValue, id destinationValue) {
RKLogTrace(@"Transforming string value '%@' to NSDate...", string);
NSDate* date = nil;
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
formatter.timeZone = [NSTimeZone localTimeZone];
for (NSString* formatString in self.objectMapping.dateFormatStrings) {
[formatter setDateFormat:formatString];
date = [formatter dateFromString:string];
if (date) {
for (NSDateFormatter *dateFormatter in self.objectMapping.dateFormatters) {
@synchronized(dateFormatter) {
date = [dateFormatter dateFromString:string];
}
if (date) {
break;
}
}
[formatter release];
return date;
}
return date;
}
- (id)transformValue:(id)value atKeyPath:keyPath toType:(Class)destinationType {
@@ -162,6 +160,14 @@ BOOL RKObjectIsValueEqualToValue(id sourceValue, id destinationValue) {
return ([value boolValue] ? @"true" : @"false");
} else if ([destinationType isSubclassOfClass:[NSString class]] && [value respondsToSelector:@selector(stringValue)]) {
return [value stringValue];
} else if ([destinationType isSubclassOfClass:[NSString class]] && [value isKindOfClass:[NSDate class]]) {
// NSDate -> NSString
// Transform using the preferred date formatter
NSString* dateString = nil;
@synchronized(self.objectMapping.preferredDateFormatter) {
dateString = [self.objectMapping.preferredDateFormatter stringFromDate:value];
}
return dateString;
}
RKLogWarning(@"Failed transformation of value at keyPath '%@'. No strategy for transforming from '%@' to '%@'", keyPath, NSStringFromClass([value class]), NSStringFromClass(destinationType));

View File

@@ -100,7 +100,9 @@
if ([value isKindOfClass:[NSDate class]]) {
// Date's are not natively serializable, must be encoded as a string
transformedValue = [value description];
@synchronized(self.mapping.preferredDateFormatter) {
transformedValue = [self.mapping.preferredDateFormatter stringFromDate:value];
}
} else if ([value isKindOfClass:[NSDecimalNumber class]]) {
// Precision numbers are serialized as strings to work around Javascript notation limits
transformedValue = [(NSDecimalNumber*)value stringValue];