Permalink
Browse files

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
  • Loading branch information...
1 parent ad754a9 commit 54007c78d4f2cf1863d3e8db6aef87f81e31b6c5 @blakewatters blakewatters committed Sep 5, 2011
@@ -226,4 +226,4 @@
@end
-#endif
+#endif
@@ -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 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) NSArray *dateFormatters;
+
/**
- 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.
+ 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) NSMutableArray* dateFormatStrings;
+@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
@@ -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 @@ - (id)init {
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 @@ - (id)init {
- (void)dealloc {
[_rootKeyPath release];
[_mappings release];
- [_dateFormatStrings release];
+ [_dateFormatters release];
+ [_preferredDateFormatter release];
[super dealloc];
}
@@ -256,4 +274,78 @@ - (Class)classForProperty:(NSString*)propertyName {
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
@@ -97,18 +97,16 @@ - (NSDate*)parseDateFromString:(NSString*)string {
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 @@ - (id)transformValue:(id)value atKeyPath:keyPath toType:(Class)destinationType {
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));
@@ -100,7 +100,9 @@ - (void)objectMappingOperation:(RKObjectMappingOperation *)operation didSetValue
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];
@@ -269,6 +269,7 @@
25DBB3A113A2486400CE90F1 /* RKLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 25DBB39F13A2486300CE90F1 /* RKLog.m */; };
25DBB3A413A2506900CE90F1 /* RKObjectMappingOperationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 25952DEF136C8F9C00D04F93 /* RKObjectMappingOperationSpec.m */; };
25DBB3A513A2506C00CE90F1 /* RKManagedObjectMappingOperationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 257FB70C1395DEB5003A628E /* RKManagedObjectMappingOperationSpec.m */; };
+ 25E5E66B1415665C00233720 /* RKObjectSerializerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F5356EC13785DA300132100 /* RKObjectSerializerSpec.m */; };
25E9682E13E6156100ABAE92 /* RKObjectMappingDefinition.h in Headers */ = {isa = PBXBuildFile; fileRef = 25E9682D13E6156100ABAE92 /* RKObjectMappingDefinition.h */; settings = {ATTRIBUTES = (Public, ); }; };
25F5182513724866009B2E22 /* RKObjectRelationshipMapping.h in Headers */ = {isa = PBXBuildFile; fileRef = 25F5182313724865009B2E22 /* RKObjectRelationshipMapping.h */; settings = {ATTRIBUTES = (Public, ); }; };
25F5182613724866009B2E22 /* RKObjectRelationshipMapping.m in Sources */ = {isa = PBXBuildFile; fileRef = 25F5182413724866009B2E22 /* RKObjectRelationshipMapping.m */; };
@@ -288,7 +289,6 @@
3F278A45139D2AFF009AC3FA /* RKRequestCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F278A3F139D2AFF009AC3FA /* RKRequestCache.m */; };
3F4EAF58134205CF00F944E4 /* RKXMLParserSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F4EAF57134205CF00F944E4 /* RKXMLParserSpec.m */; };
3F4EAF5B1342071B00F944E4 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F4EAF5A1342071B00F944E4 /* libxml2.dylib */; };
- 3F5356ED13785DA300132100 /* RKObjectSerializerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F5356EC13785DA300132100 /* RKObjectSerializerSpec.m */; };
3F6C3A2E10FE749C008F47C5 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F6C3A2D10FE749C008F47C5 /* Foundation.framework */; };
3F6C3A9610FE7524008F47C5 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F6C3A9510FE7524008F47C5 /* UIKit.framework */; };
3F71ED3413748536006281CA /* RKObjectMappingProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F71ED3213748536006281CA /* RKObjectMappingProvider.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -2248,7 +2248,6 @@
250C296C13411E60000A3551 /* RKRequestQueueSpec.m in Sources */,
25D1984313697FEF0090B617 /* RKManagedObjectLoaderSpec.m in Sources */,
25D1994E136BE7E00090B617 /* RKObjectMappingNextGenSpec.m in Sources */,
- 3F5356ED13785DA300132100 /* RKObjectSerializerSpec.m in Sources */,
25A1CA84137C37E300A7D5C9 /* RKManagedObjectThreadSafeInvocationSpec.m in Sources */,
25A1CB4713840E6100A7D5C9 /* RKParserRegistrySpec.m in Sources */,
257FB68913959884003A628E /* RKManagedObjectMappingSpec.m in Sources */,
@@ -2265,6 +2264,7 @@
252CF8C013E25FE00093BBD6 /* RKDynamicMappingModels.m in Sources */,
25769F0C1409C5A0003FCDBC /* RKChild.m in Sources */,
25769F0D1409C5A0003FCDBC /* RKParent.m in Sources */,
+ 25E5E66B1415665C00233720 /* RKObjectSerializerSpec.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Oops, something went wrong.

0 comments on commit 54007c7

Please sign in to comment.