diff options
Diffstat (limited to 'Source')
-rw-r--r-- | Source/SPCSVExporter.h | 11 | ||||
-rw-r--r-- | Source/SPCSVExporter.m | 281 | ||||
-rw-r--r-- | Source/SPExportController.h | 19 | ||||
-rw-r--r-- | Source/SPExportController.m | 22 | ||||
-rw-r--r-- | Source/SPExporter.h | 16 | ||||
-rw-r--r-- | Source/SPExporter.m | 3 |
6 files changed, 345 insertions, 7 deletions
diff --git a/Source/SPCSVExporter.h b/Source/SPCSVExporter.h index 48f0c424..31942cd2 100644 --- a/Source/SPCSVExporter.h +++ b/Source/SPCSVExporter.h @@ -43,7 +43,11 @@ NSString *csvEnclosingCharacterString; NSString *csvEscapeString; NSString *csvLineEndingString; + NSString *csvNULLString; NSArray *csvTableColumnNumericStatus; + + // CSV encoding + NSStringEncoding csvOutputEncoding; } @property (readwrite, retain) NSFileHandle *csvFileHandle; @@ -51,13 +55,18 @@ @property (readwrite, retain) NSArray *csvDataArray; @property (readwrite, retain) MCPResult *csvDataResult; -@property (readwrite) BOOL csvOutputFieldNames; +@property (readwrite, assign) BOOL csvOutputFieldNames; @property (readwrite, retain) NSString *csvFieldSeparatorString; @property (readwrite, retain) NSString *csvEnclosingCharacterString; @property (readwrite, retain) NSString *csvEscapeString; @property (readwrite, retain) NSString *csvLineEndingString; +@property (readwrite, retain) NSString *csvNULLString; @property (readwrite, retain) NSArray *csvTableColumnNumericStatus; +@property (readwrite, assign) NSStringEncoding csvOutputEncoding; + - (id)initWithFileHandle:(NSFileHandle *)fileHandle; +- (BOOL)startCSVExport; + @end diff --git a/Source/SPCSVExporter.m b/Source/SPCSVExporter.m index 15cf6827..c4a33fb0 100644 --- a/Source/SPCSVExporter.m +++ b/Source/SPCSVExporter.m @@ -24,6 +24,13 @@ // More info at <http://code.google.com/p/sequel-pro/> #import "SPCSVExporter.h" +#import "SPArrayAdditions.h" + +@interface SPCSVExporter (PrivateAPI) + +- (void)startCSVExportInBackgroundThread; + +@end @implementation SPCSVExporter @@ -37,8 +44,11 @@ @synthesize csvEnclosingCharacterString; @synthesize csvEscapeString; @synthesize csvLineEndingString; +@synthesize csvNULLString; @synthesize csvTableColumnNumericStatus; +@synthesize csvOutputEncoding; + /** * Initialize an instance of the exporter using the supplied file handle. */ @@ -51,4 +61,275 @@ return self; } +/** + * + */ +- (BOOL)startCSVExport +{ + // Check that we have all the required info before starting the export + if ((![self csvFileHandle]) || + (![self csvOutputFieldNames]) || + (![self csvFieldSeparatorString]) || + (![self csvEscapeString]) || + (![self csvLineEndingString]) || + (![self csvTableColumnNumericStatus]) || + (![self csvOutputEncoding])) + { + return NO; + } + + // Check that the CSV output options are not just empty strings or empty arrays + if ((![[self csvFieldSeparatorString] isEqualToString:@""]) || + (![[self csvEscapeString] isEqualToString:@""]) || + (![[self csvLineEndingString] isEqualToString:@""]) || + ([[self csvTableColumnNumericStatus] count] != 0)) + { + return NO; + } + + // Check that we have at least some data to export + if ((![self csvDataArray]) && (![self csvDataResult])) return NO; + + // Tell the delegate that we are starting the export process + if (delegate && [delegate respondsToSelector:@selector(exportProcessDidStart:)]) { + [delegate exportProcessDidStart:self]; + } + + // Start the export in a new thread + [NSThread detachNewThreadSelector:@selector(startCSVExportInBackgroundThread) toTarget:self withObject:nil]; + + // Tell the delegate that the export process has ended + if (delegate && [delegate respondsToSelector:@selector(exportProcessDidEnd:)]) { + [delegate exportProcessDidEnd:self]; + } + + return YES; +} + +/** + * Dealloc + */ +- (void)dealloc +{ + [csvFileHandle release], csvFileHandle = nil; + [csvDataArray release], csvDataArray = nil; + [csvDataResult release], csvDataResult = nil; + [csvFieldSeparatorString release], csvFieldSeparatorString = nil; + [csvEnclosingCharacterString release], csvEnclosingCharacterString = nil; + [csvEscapeString release], csvEscapeString = nil; + [csvLineEndingString release], csvLineEndingString = nil; + [csvNULLString release], csvNULLString = nil; + [csvTableColumnNumericStatus release], csvTableColumnNumericStatus = nil; + + [super dealloc]; +} + +@end + +@implementation SPCSVExporter (PrivateAPI) + +/** + * Starts the export process in a background thread. + */ +- (void)startCSVExportInBackgroundThread +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + NSMutableArray *csvRow = [NSMutableArray array]; + NSMutableString *csvCell = [NSMutableString string]; + NSMutableString *csvString = [NSMutableString string]; + + NSScanner *csvNumericTester; + NSString *escapedEscapeString, *escapedFieldSeparatorString, *escapedEnclosingString, *escapedLineEndString, *dataConversionString; + + BOOL csvCellIsNumeric; + BOOL quoteFieldSeparators = [[self csvEnclosingCharacterString] isEqualToString:@""]; + + NSUInteger i, j, startingRow, totalRows; + + if ([self csvDataResult] != nil && [[self csvDataResult] numOfRows]) [[self csvDataResult] dataSeek:0]; + + // Detect and restore special characters being used as terminating or line end strings + NSMutableString *tempSeparatorString = [NSMutableString stringWithString:[self csvFieldSeparatorString]]; + + NSUInteger tempSeparatorStringLength = [tempSeparatorString length]; + + // Escape tabs, line endings and carriage returns + [tempSeparatorString replaceOccurrencesOfString:@"\\t" withString:@"\t" + options:NSLiteralSearch + range:NSMakeRange(0, tempSeparatorStringLength)]; + + [tempSeparatorString replaceOccurrencesOfString:@"\\n" withString:@"\n" + options:NSLiteralSearch + range:NSMakeRange(0, tempSeparatorStringLength)]; + + [tempSeparatorString replaceOccurrencesOfString:@"\\r" withString:@"\r" + options:NSLiteralSearch + range:NSMakeRange(0, tempSeparatorStringLength)]; + + // Set the new field separator string + [self setCsvFieldSeparatorString:[NSString stringWithString:tempSeparatorString]]; + + NSMutableString *tempLineEndString = [NSMutableString stringWithString:[self csvLineEndingString]]; + + NSUInteger tempLineEndStringLength = [tempLineEndString length]; + + // Escape tabs, line endings and carriage returns + [tempLineEndString replaceOccurrencesOfString:@"\\t" withString:@"\t" + options:NSLiteralSearch + range:NSMakeRange(0, tempLineEndStringLength)]; + + + [tempLineEndString replaceOccurrencesOfString:@"\\n" withString:@"\n" + options:NSLiteralSearch + range:NSMakeRange(0, tempLineEndStringLength)]; + + [tempLineEndString replaceOccurrencesOfString:@"\\r" withString:@"\r" + options:NSLiteralSearch + range:NSMakeRange(0, tempLineEndStringLength)]; + + // Set the new line ending string + [self setCsvLineEndingString:[NSString stringWithString:tempLineEndString]]; + + // Set up escaped versions of strings for substitution within the loop + escapedEscapeString = [[self csvEscapeString] stringByAppendingString:[self csvEscapeString]]; + escapedFieldSeparatorString = [[self csvEscapeString] stringByAppendingString:[self csvFieldSeparatorString]]; + escapedEnclosingString = [[self csvEscapeString] stringByAppendingString:[self csvEnclosingCharacterString]]; + escapedLineEndString = [[self csvEscapeString] stringByAppendingString:[self csvLineEndingString]]; + + // Determine the total number of rows and starting row depending on supplied data format + if ([self csvDataArray] == nil) { + startingRow = [self csvOutputFieldNames] ? -1 : 0; + totalRows = [[self csvDataResult] numOfRows]; + } + else { + startingRow = [self csvOutputFieldNames] ? 0 : 1; + totalRows = [[self csvDataArray] count]; + } + + // Walk through the supplied data constructing the CSV string + for (i = startingRow; i < totalRows; i++) + { + // Update the progress value + if (totalRows) [self setProgressValue:(((i + 1) * 100) / totalRows)]; + + // Retrieve the row from the supplied data + if ([self csvDataArray] == nil) { + // Header row + [csvRow setArray:(i == -1) ? [[self csvDataResult] fetchFieldNames] : [[self csvDataResult] fetchRowAsArray]]; + } + else { + [csvRow setArray:NSArrayObjectAtIndex([self csvDataArray], i)]; + } + + [csvString setString:@""]; + + for (j = 0; j < [csvRow count]; j++) + { + // For NULL objects supplied from a queryResult, add an unenclosed null string as per prefs + if ([[csvRow objectAtIndex:j] isKindOfClass:[NSNull class]]) { + [csvString appendString:[self csvNULLString]]; + + if (j < [csvRow count] - 1) [csvString appendString:[self csvFieldSeparatorString]]; + + continue; + } + + // Retrieve the contents of this cell + if ([NSArrayObjectAtIndex(csvRow, j) isKindOfClass:[NSData class]]) { + dataConversionString = [[NSString alloc] initWithData:NSArrayObjectAtIndex(csvRow, j) encoding:csvOutputEncoding]; + + if (dataConversionString == nil) { + dataConversionString = [[NSString alloc] initWithData:NSArrayObjectAtIndex(csvRow, j) encoding:NSASCIIStringEncoding]; + } + + [csvCell setString:[NSString stringWithString:dataConversionString]]; + [dataConversionString release]; + } + else { + [csvCell setString:[NSArrayObjectAtIndex(csvRow, j) description]]; + } + + // For NULL values supplied via an array add the unenclosed null string as set in preferences + if ([csvCell isEqualToString:[self csvNULLString]]) { + [csvString appendString:[self csvNULLString]]; + } + // Add empty strings as a pair of enclosing characters. + else if ([csvCell length] == 0) { + [csvString appendString:[self csvEnclosingCharacterString]]; + [csvString appendString:[self csvEnclosingCharacterString]]; + + } + else { + // Test whether this cell contains a number + if ([NSArrayObjectAtIndex(csvRow, j) isKindOfClass:[NSData class]]) { + csvCellIsNumeric = NO; + } + // If an array of bools supplying information as to whether the column is numeric has been supplied, use it. + else if ([self csvTableColumnNumericStatus] != nil) { + csvCellIsNumeric = [NSArrayObjectAtIndex([self csvTableColumnNumericStatus], j) boolValue]; + } + // Or fall back to testing numeric content via an NSScanner. + else { + csvNumericTester = [NSScanner scannerWithString:csvCell]; + csvCellIsNumeric = [csvNumericTester scanFloat:nil] && + [csvNumericTester isAtEnd] && + ([csvCell characterAtIndex:0] != '0' || + [csvCell length] == 1 || + ([csvCell length] > 1 && + [csvCell characterAtIndex:1] == '.')); + } + + // Escape any occurrences of the escaping character + [csvCell replaceOccurrencesOfString:[self csvEscapeString] + withString:escapedEscapeString + options:NSLiteralSearch + range:NSMakeRange(0, [csvCell length])]; + + // Escape any occurrences of the enclosure string + if (![[self csvEscapeString] isEqualToString:[self csvEnclosingCharacterString]]) { + [csvCell replaceOccurrencesOfString:[self csvEnclosingCharacterString] + withString:escapedEnclosingString + options:NSLiteralSearch + range:NSMakeRange(0, [csvCell length])]; + } + + // Escape occurrences of the line end character + [csvCell replaceOccurrencesOfString:[self csvLineEndingString] + withString:escapedLineEndString + options:NSLiteralSearch + range:NSMakeRange(0, [csvCell length])]; + + // If the string isn't quoted or otherwise enclosed, escape occurrences of the field separators + if (quoteFieldSeparators || csvCellIsNumeric) { + [csvCell replaceOccurrencesOfString:[self csvFieldSeparatorString] + withString:escapedFieldSeparatorString + options:NSLiteralSearch + range:NSMakeRange(0, [csvCell length])]; + } + + // Write out the cell data by appending strings - this is significantly faster than stringWithFormat. + if (csvCellIsNumeric) { + [csvString appendString:csvCell]; + } + else { + [csvString appendString:[self csvEnclosingCharacterString]]; + [csvString appendString:csvCell]; + [csvString appendString:[self csvEnclosingCharacterString]]; + } + } + + if (j < ([csvRow count] - 1)) [csvString appendString:[self csvFieldSeparatorString]]; + } + + // Append the line ending to the string for this row + [csvString appendString:[self csvLineEndingString]]; + + // Write it to the fileHandle + [csvFileHandle writeData:[csvString dataUsingEncoding:[self csvOutputEncoding]]]; + } + + [pool release]; +} + @end diff --git a/Source/SPExportController.h b/Source/SPExportController.h index 63d1c78b..8e901f9e 100644 --- a/Source/SPExportController.h +++ b/Source/SPExportController.h @@ -25,6 +25,25 @@ #import <Cocoa/Cocoa.h> #import <MCPKit/MCPKit.h> +// Export type constants +enum { + SP_SQL_EXPORT = 1, + SP_CSV_EXPORT = 2, + SP_XML_EXPORT = 3, + SP_PDF_EXPORT = 4, + SP_HTML_EXPORT = 5, + SP_EXCEL_EXPORT = 6 +}; +typedef NSUInteger SPExportType; + +// Export source constants +enum { + SP_FILTERED_EXPORT = 1, + SP_CUSTOM_QUERY_EXPORT = 2, + SP_TABLE_EXPORT = 3 +}; +typedef NSUInteger SPExportSource; + @interface SPExportController : NSObject { // Table Document diff --git a/Source/SPExportController.m b/Source/SPExportController.m index f89e727d..15e47e61 100644 --- a/Source/SPExportController.m +++ b/Source/SPExportController.m @@ -73,6 +73,18 @@ if (returnCode == NSOKButton) { // First determine what type of export the user selected + SPExportType exportType = 0; + + for (NSToolbarItem *item in [exportToolbar items]) + { + if ([[item itemIdentifier] isEqualToString:[exportToolbar selectedItemIdentifier]]) { + exportType = [item tag]; + break; + } + } + + // Determine what data to use (filtered result, custom query result or selected tables) + SPExportSource exportSource = ([exportInputMatrix selectedRow] + 1); } } @@ -86,7 +98,7 @@ - (void)loadTables { - int i; + NSUInteger i; [tables removeAllObjects]; @@ -130,12 +142,12 @@ return [tables count]; } -- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex +- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { return NSArrayObjectAtIndex([tables objectAtIndex:rowIndex], ([[aTableColumn identifier] isEqualToString:@"switch"]) ? 0 : 1); } -- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex +- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { [[tables objectAtIndex:rowIndex] replaceObjectAtIndex:0 withObject:anObject]; } @@ -153,7 +165,7 @@ return (aTableView == exportTableList); } -- (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex +- (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { [aCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; } @@ -166,7 +178,7 @@ NSMutableArray *items = [NSMutableArray arrayWithCapacity:6]; for (NSToolbarItem *item in [toolbar items]) - { + { [items addObject:[item itemIdentifier]]; } diff --git a/Source/SPExporter.h b/Source/SPExporter.h index bf405f58..70feb915 100644 --- a/Source/SPExporter.h +++ b/Source/SPExporter.h @@ -42,8 +42,22 @@ * used within a separate thread can be reclaimed immediately after the thread completes its cycle and it's * autorelease pool is released). */ + +@interface NSObject (SPExporterDelegate) + +- (void)exportProcessDidStart:(id)exporter; +- (void)exportProcessDidEnd:(id)exporter; + +@end + @interface SPExporter : NSObject +{ + id delegate; + + double progressValue; +} -// Implement functionality common to all exporters here +@property (readwrite, assign) id delegate; +@property (readwrite, assign) double progressValue; @end diff --git a/Source/SPExporter.m b/Source/SPExporter.m index fc5a231b..f8f2cba0 100644 --- a/Source/SPExporter.m +++ b/Source/SPExporter.m @@ -27,4 +27,7 @@ @implementation SPExporter +@synthesize delegate; +@synthesize progressValue; + @end |