diff options
Diffstat (limited to 'Source')
44 files changed, 4640 insertions, 509 deletions
diff --git a/Source/SPCSVExporter.h b/Source/SPCSVExporter.h index 5c78be04..b0c6c46b 100644 --- a/Source/SPCSVExporter.h +++ b/Source/SPCSVExporter.h @@ -25,34 +25,93 @@ #import <Cocoa/Cocoa.h> -#import "MCPKit.h" #import "SPExporter.h" +#import "SPCSVExporterProtocol.h" +@class SPTableData; + +/** + * @class SPCSVExporter SPCSVExporter.m + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * CSV exporter class. + */ @interface SPCSVExporter : SPExporter -{ - // CSV data +{ + /** + * Exporter delegate + */ + NSObject <SPCSVExporterProtocol> *delegate; + + /** + * Data array + */ NSArray *csvDataArray; - MCPStreamingResult *csvDataResult; - // CSV options + /** + * Table name + */ + NSString *csvTableName; + + /** + * Output field names + */ BOOL csvOutputFieldNames; + + /** + * CSV field separator string + */ NSString *csvFieldSeparatorString; + + /** + * CSV enclosing character string + */ NSString *csvEnclosingCharacterString; + + /** + * CSV escape string + */ NSString *csvEscapeString; + + /** + * CSV line ending string + */ NSString *csvLineEndingString; + + /** + * CSV NULL string + */ NSString *csvNULLString; - NSArray *csvTableColumnNumericStatus; + + /** + * Table data + */ + SPTableData *csvTableData; } -@property (readwrite, retain) NSArray *csvDataArray; -@property (readwrite, retain) MCPStreamingResult *csvDataResult; +@property(readwrite, assign) NSObject <SPCSVExporterProtocol> *delegate; + +@property(readwrite, retain) NSArray *csvDataArray; +@property(readwrite, retain) NSString *csvTableName; + +@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) SPTableData *csvTableData; -@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; +/** + * Initialise an instance of SPCSVExporter using the supplied delegate. + * + * @param exportDelegate The exporter delegate + * + * @return The initialised instance + */ +- (id)initWithDelegate:(NSObject *)exportDelegate; @end diff --git a/Source/SPCSVExporter.m b/Source/SPCSVExporter.m index 4937e7c9..e6342e68 100644 --- a/Source/SPCSVExporter.m +++ b/Source/SPCSVExporter.m @@ -23,24 +23,44 @@ // // More info at <http://code.google.com/p/sequel-pro/> +#import <MCPKit/MCPKit.h> + #import "SPCSVExporter.h" #import "SPArrayAdditions.h" +#import "SPStringAdditions.h" +#import "SPFileHandle.h" +#import "SPTableData.h" +#import "SPExportUtilities.h" @implementation SPCSVExporter +@synthesize delegate; @synthesize csvDataArray; -@synthesize csvDataResult; - +@synthesize csvTableName; @synthesize csvOutputFieldNames; @synthesize csvFieldSeparatorString; @synthesize csvEnclosingCharacterString; @synthesize csvEscapeString; @synthesize csvLineEndingString; @synthesize csvNULLString; -@synthesize csvTableColumnNumericStatus; +@synthesize csvTableData; + +/** + * Initialise an instance of SPCSVExporter using the supplied delegate. + */ +- (id)initWithDelegate:(NSObject *)exportDelegate +{ + if ((self = [super init])) { + SPExportDelegateConformsToProtocol(exportDelegate, @protocol(SPCSVExporterProtocol)); + + [self setDelegate:exportDelegate]; + } + + return self; +} /** - * Start the CSV data conversion process. This method is automatically called when an instance of this object + * Start the CSV export process. This method is automatically called when an instance of this class * is placed on an NSOperationQueue. Do not call it directly as there is no manual multithreading. */ - (void)main @@ -49,44 +69,61 @@ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSMutableString *csvString = [NSMutableString string]; - NSMutableString *csvData = [NSMutableString string]; NSMutableString *csvCellString = [NSMutableString string]; + + NSMutableArray *tableColumnNumericStatus = [NSMutableArray array]; - NSArray *csvRow; - NSScanner *csvNumericTester; + NSArray *csvRow = nil; + NSScanner *csvNumericTester = nil; + MCPStreamingResult *streamingResult = nil; NSString *escapedEscapeString, *escapedFieldSeparatorString, *escapedEnclosingString, *escapedLineEndString, *dataConversionString; id csvCell; BOOL csvCellIsNumeric; BOOL quoteFieldSeparators = [[self csvEnclosingCharacterString] isEqualToString:@""]; - NSUInteger i, totalRows, csvCellCount = 0; + NSUInteger i, totalRows, lastProgressValue, csvCellCount = 0; + + // Check to see if we have at least a table name or data array + if ((![self csvTableName]) && (![self csvDataArray]) || + ([[self csvTableName] isEqualToString:@""]) && ([[self csvDataArray] count] == 0)) + { + [pool release]; + return; + } // Check that we have all the required info before starting the export if ((![self csvOutputFieldNames]) || (![self csvFieldSeparatorString]) || (![self csvEscapeString]) || - (![self csvLineEndingString]) || - (![self csvTableColumnNumericStatus])) + (![self csvLineEndingString])) { + [pool release]; return; } - // Check that the CSV output options are not just empty strings or empty arrays + // Check that the CSV output options are not just empty strings if (([[self csvFieldSeparatorString] isEqualToString:@""]) || ([[self csvEscapeString] isEqualToString:@""]) || - ([[self csvLineEndingString] isEqualToString:@""]) || - ([[self csvTableColumnNumericStatus] count] == 0)) + ([[self csvLineEndingString] isEqualToString:@""])) { + [pool release]; return; } - - // Check that we have at least some data to export - if ((![self csvDataArray]) && (![self csvDataResult])) return; - + + // Inform the delegate that the export process is about to begin + [delegate performSelectorOnMainThread:@selector(csvExportProcessWillBegin:) withObject:self waitUntilDone:NO]; + // Mark the process as running [self setExportProcessIsRunning:YES]; + lastProgressValue = 0; + + // Make a streaming request for the data if the data array isn't set + if ((![self csvDataArray]) && [self csvTableName]) { + streamingResult = [connection streamingQueryString:[NSString stringWithFormat:@"SELECT * FROM %@", [[self csvTableName] backtickQuotedString]] useLowMemoryBlockingStreaming:[self exportUsingLowMemoryBlockingStreaming]]; + } + // Detect and restore special characters being used as terminating or line end strings NSMutableString *tempSeparatorString = [NSMutableString stringWithString:[self csvFieldSeparatorString]]; @@ -135,17 +172,62 @@ // headers as the first row, decide whether to skip the first row. NSUInteger currentRowIndex = 0; - [csvData setString:@""]; + [csvString setString:@""]; + if ([self csvDataArray]) totalRows = [[self csvDataArray] count]; if (([self csvDataArray]) && (![self csvOutputFieldNames])) currentRowIndex++; + if ([self csvTableName] && (![self csvDataArray])) { + + NSDictionary *tableDetails = [[NSDictionary alloc] init]; + + // Determine whether the supplied table is actually a table or a view via the CREATE TABLE command, and get the table details + MCPResult *queryResult = [connection queryString:[NSString stringWithFormat:@"SHOW CREATE TABLE %@", [[self csvTableName] backtickQuotedString]]]; + + [queryResult setReturnDataAsStrings:YES]; + + if ([queryResult numOfRows]) { + tableDetails = [NSDictionary dictionaryWithDictionary:[queryResult fetchRowAsDictionary]]; + + tableDetails = [NSDictionary dictionaryWithDictionary:([tableDetails objectForKey:@"Create View"]) ? [[self csvTableData] informationForView:[self csvTableName]] : [[self csvTableData] informationForTable:[self csvTableName]]]; + } + + // Retrieve the table details via the data class, and use it to build an array containing column numeric status + for (NSDictionary *column in [tableDetails objectForKey:@"columns"]) + { + NSString *tableColumnTypeGrouping = [column objectForKey:@"typegrouping"]; + + [tableColumnNumericStatus addObject:[NSNumber numberWithBool:([tableColumnTypeGrouping isEqualToString:@"bit"] || + [tableColumnTypeGrouping isEqualToString:@"integer"] || + [tableColumnTypeGrouping isEqualToString:@"float"])]]; + } + + [tableDetails release]; + } + // Drop into the processing loop NSAutoreleasePool *csvExportPool = [[NSAutoreleasePool alloc] init]; NSUInteger currentPoolDataLength = 0; + // Inform the delegate that we are about to start writing the data to disk + [delegate performSelectorOnMainThread:@selector(csvExportProcessWillBeginWritingData:) withObject:self waitUntilDone:NO]; + while (1) { + // Check for cancellation flag + if ([self isCancelled]) { + if (streamingResult) { + [connection cancelCurrentQuery]; + [streamingResult cancelResultLoad]; + } + + [csvExportPool release]; + [pool release]; + + return; + } + // Retrieve the next row from the supplied data, either directly from the array... if ([self csvDataArray]) { csvRow = NSArrayObjectAtIndex([self csvDataArray], currentRowIndex); @@ -154,11 +236,11 @@ else { // If still requested to read the field names, get the field names if ([self csvOutputFieldNames]) { - csvRow = [[self csvDataResult] fetchFieldNames]; + csvRow = [streamingResult fetchFieldNames]; [self setCsvOutputFieldNames:NO]; } else { - csvRow = [[self csvDataResult] fetchNextRowAsArray]; + csvRow = [streamingResult fetchNextRowAsArray]; if (!csvRow) break; } @@ -171,6 +253,14 @@ for (i = 0 ; i < csvCellCount; i++) { + // Check for cancellation flag + if ([self isCancelled]) { + [csvExportPool release]; + [pool release]; + + return; + } + csvCell = NSArrayObjectAtIndex(csvRow, i); // For NULL objects supplied from a queryResult, add an unenclosed null string as per prefs @@ -208,8 +298,8 @@ } else { // If an array of bools supplying information as to whether the column is numeric has been supplied, use it. - if ([self csvTableColumnNumericStatus] != nil) { - csvCellIsNumeric = [NSArrayObjectAtIndex([self csvTableColumnNumericStatus], i) boolValue]; + if ([tableColumnNumericStatus count] > 0) { + csvCellIsNumeric = [NSArrayObjectAtIndex(tableColumnNumericStatus, i) boolValue]; } // Otherwise, first test whether this cell contains data else if ([NSArrayObjectAtIndex(csvRow, i) isKindOfClass:[NSData class]]) { @@ -270,40 +360,49 @@ } // Append the line ending to the string for this row, and record the length processed for pool flushing - [csvString appendString:[self csvLineEndingString]]; - [csvData appendString:csvString]; - + [csvString appendString:[self csvLineEndingString]]; currentPoolDataLength += [csvString length]; + // Write it to the fileHandle + [[self exportOutputFileHandle] writeData:[csvString dataUsingEncoding:[self exportOutputEncoding]]]; + currentRowIndex++; - // Update the progress value - if (totalRows) [self setExportProgressValue:(((i + 1) * 100) / totalRows)]; + // Update the progress + if (totalRows && (currentRowIndex * ([self exportMaxProgress] / totalRows)) > lastProgressValue) { + + NSInteger progress = (currentRowIndex * ([self exportMaxProgress] / totalRows)); + + [self setExportProgressValue:progress]; + + lastProgressValue = progress; + } + + // Inform the delegate that the export's progress has been updated + [delegate performSelectorOnMainThread:@selector(csvExportProcessProgressUpdated:) withObject:self waitUntilDone:NO]; // If an array was supplied and we've processed all rows, break if ([self csvDataArray] && (totalRows == currentRowIndex)) break; // Drain the autorelease pool as required to keep memory usage low if (currentPoolDataLength > 250000) { - [csvExportPool drain]; + [csvExportPool release]; csvExportPool = [[NSAutoreleasePool alloc] init]; } } - - // Assign the resulting CSV data to the expoter's export data - [self setExportData:csvData]; + + // Write data to disk + [[self exportOutputFileHandle] synchronizeFile]; // Mark the process as not running [self setExportProcessIsRunning:NO]; - // Call the delegate's didEndSelector while passing this exporter to it - [[self delegate] performSelectorOnMainThread:[self didEndSelector] withObject:self waitUntilDone:NO]; + // Inform the delegate that the export process is complete + [delegate performSelectorOnMainThread:@selector(csvExportProcessComplete:) withObject:self waitUntilDone:NO]; [pool release]; } - @catch(NSException *e) { - - } + @catch(NSException *e) {} } /** @@ -311,14 +410,15 @@ */ - (void)dealloc { - [csvDataArray release], csvDataArray = nil; - [csvDataResult release], csvDataResult = nil; + if (csvDataArray) [csvDataArray release], csvDataArray = nil; + if (csvTableName) [csvTableName release], csvTableName = 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; + [csvTableData release], csvTableData = nil; [super dealloc]; } diff --git a/Source/SPCSVExporterDelegate.h b/Source/SPCSVExporterDelegate.h new file mode 100644 index 00000000..11170e62 --- /dev/null +++ b/Source/SPCSVExporterDelegate.h @@ -0,0 +1,38 @@ +// +// $Id$ +// +// SPCSVExporterDelegate.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on March 21, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPExportController.h" +#import "SPCSVExporterProtocol.h" + +/** + * @category SPCSVExporterDelegate SPCSVExporterDelegate.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * CSV exporter delegate category. + */ +@interface SPExportController (SPCSVExporterDelegate) <SPCSVExporterProtocol> + +@end diff --git a/Source/SPCSVExporterDelegate.m b/Source/SPCSVExporterDelegate.m new file mode 100644 index 00000000..ed5f4514 --- /dev/null +++ b/Source/SPCSVExporterDelegate.m @@ -0,0 +1,141 @@ +// +// $Id$ +// +// SPCSVExporterDelegate.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on March 21, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import <Cocoa/Cocoa.h> + +#import "SPCSVExporter.h" +#import "SPCSVExporterDelegate.h" +#import "TableDocument.h" +#import "SPMainThreadTrampoline.h" +#import "SPFileHandle.h" + +@implementation SPExportController (SPCSVExporterDelegate) + +/** + * + */ +- (void)csvExportProcessWillBegin:(SPCSVExporter *)exporter +{ + [[exportProgressText onMainThread] displayIfNeeded]; + + [[exportProgressIndicator onMainThread] setIndeterminate:YES]; + [[exportProgressIndicator onMainThread] setUsesThreadedAnimation:YES]; + [[exportProgressIndicator onMainThread] startAnimation:self]; + + // Only update the progress text if this is a table export + if (exportSource == SPTableExport) { + // Update the current table export index + currentTableExportIndex = (exportTableCount - [exporters count]); + + [[exportProgressText onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %lu of %lu (%@): Fetching data...", @"export label showing that the app is fetching data for a specific table"), currentTableExportIndex, exportTableCount, [exporter csvTableName]]]; + } + else { + [[exportProgressText onMainThread] setStringValue:NSLocalizedString(@"Fetching data...", @"export label showing that the app is fetching data")]; + } + + [[exportProgressText onMainThread] displayIfNeeded]; +} + +/** + * + */ +- (void)csvExportProcessComplete:(SPCSVExporter *)exporter +{ + NSUInteger exportCount = [exporters count]; + + // If required add the next exporter to the operation queue + if ((exportCount > 0) && (exportSource == SPTableExport)) { + + // If we're only exporting to a single file then write a header for the next table + if (!exportToMultipleFiles) { + + // If we're exporting multiple tables to a single file then append some space and the next table's + // name, but only if there is at least 2 exportes left. + [[exporter exportOutputFileHandle] writeData:[[NSString stringWithFormat:@"%@%@%@ %@%@%@", + [exporter csvLineEndingString], + [exporter csvLineEndingString], + NSLocalizedString(@"Table", @"csv export table heading"), + [(SPCSVExporter *)[exporters objectAtIndex:0] csvTableName], + [exporter csvLineEndingString], + [exporter csvLineEndingString]] dataUsingEncoding:[exporter exportOutputEncoding]]]; + } + // Otherwise close the file handle of the exporter that just finished + // ensuring it's data is written to disk. + else { + [[exporter exportOutputFileHandle] closeFile]; + } + + [operationQueue addOperation:[exporters objectAtIndex:0]]; + + // Remove the exporter we just added to the operation queue from our list of exporters + // so we know it's already been done. + [exporters removeObjectAtIndex:0]; + } + // Otherwise if the exporter list is empty, close the progress sheet + else { + // Close the last exporter's file handle + [[exporter exportOutputFileHandle] closeFile]; + + [NSApp endSheet:exportProgressWindow returnCode:0]; + [exportProgressWindow orderOut:self]; + + // Restore query mode + [tableDocumentInstance setQueryMode:SPInterfaceQueryMode]; + + // Display Growl notification + [self displayExportFinishedGrowlNotification]; + } +} + +/** + * + */ +- (void)csvExportProcessWillBeginWritingData:(SPCSVExporter *)exporter +{ + // Only update the progress text if this is a table export + if (exportSource == SPTableExport) { + [[exportProgressText onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %lu of %lu (%@): Writing data...", @"export label showing app if writing data for a specific table"), currentTableExportIndex, exportTableCount, [exporter csvTableName]]]; + } + else { + [[exportProgressText onMainThread] setStringValue:NSLocalizedString(@"Writing data...", @"export label showing app is writing data")]; + } + + [[exportProgressText onMainThread] displayIfNeeded]; + + [[exportProgressIndicator onMainThread] stopAnimation:self]; + [[exportProgressIndicator onMainThread] setUsesThreadedAnimation:NO]; + [[exportProgressIndicator onMainThread] setIndeterminate:NO]; + [[exportProgressIndicator onMainThread] setDoubleValue:0]; +} + +/** + * + */ +- (void)csvExportProcessProgressUpdated:(SPCSVExporter *)exporter +{ + [exportProgressIndicator setDoubleValue:[exporter exportProgressValue]]; +} + +@end diff --git a/Source/SPCSVExporterProtocol.h b/Source/SPCSVExporterProtocol.h new file mode 100644 index 00000000..67bc470c --- /dev/null +++ b/Source/SPCSVExporterProtocol.h @@ -0,0 +1,65 @@ +// +// $Id$ +// +// SPCSVExporterProtocol.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 15, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +@class SPCSVExporter; + +/** + * @protocol SPCSVExporterProtocol SPCSVExporterProtocol.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * CSV exporter delegate protocol. + */ +@protocol SPCSVExporterProtocol + +/** + * Called when the CSV export process is about to begin. + * + * @param SPCSVExporter The expoter calling the method. + */ +- (void)csvExportProcessWillBegin:(SPCSVExporter *)exporter; + +/** + * Called when the CSV export process is complete. + * + * @param SPCSVExporter The expoter calling the method. + */ +- (void)csvExportProcessComplete:(SPCSVExporter *)exporter; + +/** + * Called when the progress of the CSV export process is updated. + * + * @param SPCSVExporter The expoter calling the method. + */ +- (void)csvExportProcessProgressUpdated:(SPCSVExporter *)exporter; + +/** + * Called when the CSV export process is about to begin writing data to disk. + * + * @param SPCSVExporter The expoter calling the method. + */ +- (void)csvExportProcessWillBeginWritingData:(SPCSVExporter *)exporter; + +@end diff --git a/Source/SPConstants.h b/Source/SPConstants.h index d893b825..1fdcf68a 100644 --- a/Source/SPConstants.h +++ b/Source/SPConstants.h @@ -36,24 +36,46 @@ typedef enum { } SPViewMode; // Query modes -typedef enum { +enum { SPInterfaceQueryMode = 0, SPCustomQueryQueryMode = 1, SPImportExportQueryMode = 2 -} SPQueryMode; +}; +typedef NSUInteger SPQueryMode; // Connection types -typedef enum { +enum { SPTCPIPConnection = 0, SPSocketConnection = 1, SPSSHTunnelConnection = 2 -} SPConnectionType; +}; +typedef NSUInteger SPConnectionType; + +// Export type constants +enum { + SPSQLExport = 1, + SPCSVExport = 2, + SPXMLExport = 3, + SPPDFExport = 4, + SPHTMLExport = 5, + SPExcelExport = 6, + SPDotExport = 7 +}; +typedef NSUInteger SPExportType; + +// Export source constants +enum { + SPFilteredExport = 1, + SPQueryExport = 2, + SPTableExport = 3 +}; +typedef NSUInteger SPExportSource; // Table row count query usage levels typedef enum { - SPRowCountFetchNever = 0, - SPRowCountFetchIfCheap = 1, - SPRowCountFetchAlways = 2 + SPRowCountFetchNever = 0, + SPRowCountFetchIfCheap = 1, + SPRowCountFetchAlways = 2 } SPRowCountQueryUsageLevels; // Export type @@ -246,6 +268,7 @@ extern NSString *SPCSVImportFirstLineIsHeader; extern NSString *SPCSVFieldImportMappingAlignment; extern NSString *SPImportClipboardTempFileNamePrefix; extern NSString *SPSQLExportUseCompression; +extern NSString *SPNoBOMforSQLdumpFile; // Misc extern NSString *SPContentFilters; @@ -253,7 +276,6 @@ extern NSString *SPDocumentTaskEndNotification; extern NSString *SPDocumentTaskStartNotification; extern NSString *SPFieldEditorSheetFont; extern NSString *SPLastSQLFileEncoding; -extern NSString *SPNoBOMforSQLdumpFile; extern NSString *SPPrintBackground; extern NSString *SPPrintImagePreviews; extern NSString *SPQueryFavorites; @@ -273,6 +295,7 @@ extern NSString *SPDocumentationURL; extern NSString *SPContactURL; extern NSString *SPKeyboardShortcutsURL; extern NSString *SPMySQLSearchURL; +extern NSString *SPDevURL; extern NSString *SPGettingConnectedDocURL; // Toolbar constants diff --git a/Source/SPConstants.m b/Source/SPConstants.m index 3f5bf15a..536d4cfb 100644 --- a/Source/SPConstants.m +++ b/Source/SPConstants.m @@ -147,6 +147,7 @@ NSString *SPCSVImportLineTerminator = @"CSVImportLineTerminator"; NSString *SPCSVFieldImportMappingAlignment = @"CSVFieldImportMappingAlignment"; NSString *SPImportClipboardTempFileNamePrefix = @"/tmp/_SP_ClipBoard_Import_File_"; NSString *SPSQLExportUseCompression = @"SQLExportUseCompression"; +NSString *SPNoBOMforSQLdumpFile = @"NoBOMforSQLdumpFile"; // Misc NSString *SPContentFilters = @"ContentFilters"; @@ -154,7 +155,6 @@ NSString *SPDocumentTaskEndNotification = @"DocumentTaskEnded"; NSString *SPDocumentTaskStartNotification = @"DocumentTaskStarted"; NSString *SPFieldEditorSheetFont = @"FieldEditorSheetFont"; NSString *SPLastSQLFileEncoding = @"lastSqlFileEncoding"; -NSString *SPNoBOMforSQLdumpFile = @"NoBOMforSQLdumpFile"; NSString *SPPrintBackground = @"PrintBackground"; NSString *SPPrintImagePreviews = @"PrintImagePreviews"; NSString *SPQueryFavorites = @"queryFavorites"; @@ -174,6 +174,7 @@ NSString *SPDocumentationURL = @"http://www.sequelpro.com/do NSString *SPContactURL = @"http://www.sequelpro.com/docs/Contact_the_developers"; NSString *SPKeyboardShortcutsURL = @"http://www.sequelpro.com/docs/Keyboard_Shortcuts"; NSString *SPMySQLSearchURL = @"http://search.mysql.com/search?q=%@&site=refman-%@"; +NSString *SPDevURL = @"http://code.google.com/p/sequel-pro/"; NSString *SPGettingConnectedDocURL = @"http://www.sequelpro.com/docs/Getting_Connected"; // Toolbar constants diff --git a/Source/SPDotExporter.h b/Source/SPDotExporter.h new file mode 100644 index 00000000..338acc37 --- /dev/null +++ b/Source/SPDotExporter.h @@ -0,0 +1,96 @@ +// +// $Id$ +// +// SPDotExporter.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 17, 2010 +// Copyright (c) 2009 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import <Cocoa/Cocoa.h> + +#import "SPExporter.h" +#import "SPDotExporterProtocol.h" + +@class SPTableData; + +/** + * @class SPXMLExporter SPXMLExporter.m + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * XML exporter class. + */ +@interface SPDotExporter : SPExporter +{ + /** + * Exporter delegate + */ + NSObject <SPDotExporterProtocol> *delegate; + + /** + * Table information + */ + NSArray *dotExportTables; + + /** + * Current table + */ + NSString *dotExportCurrentTable; + + /** + * Table data + */ + SPTableData *dotTableData; + + /** + * Database host + */ + NSString *dotDatabaseHost; + + /** + * Database name + */ + NSString *dotDatabaseName; + + /** + * Database version + */ + NSString *dotDatabaseVersion; +} + +@property(readwrite, assign) NSObject *delegate; +@property(readwrite, retain) NSArray *dotExportTables; +@property(readwrite, retain) NSString *dotExportCurrentTable; +@property(readwrite, retain) SPTableData *dotTableData; + +@property(readwrite, retain) NSString *dotDatabaseHost; +@property(readwrite, retain) NSString *dotDatabaseName; +@property(readwrite, retain) NSString *dotDatabaseVersion; + +/** + * Initialise an instance of SPDotExporter using the supplied delegate. + * + * @param exportDelegate The exporter delegate + * + * @return The initialised instance + */ +- (id)initWithDelegate:(NSObject *)exportDelegate; + +@end diff --git a/Source/SPDotExporter.m b/Source/SPDotExporter.m new file mode 100644 index 00000000..cbb132d9 --- /dev/null +++ b/Source/SPDotExporter.m @@ -0,0 +1,227 @@ +// +// $Id$ +// +// SPDotExporter.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 17, 2010 +// Copyright (c) 2009 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPDotExporter.h" +#import "SPConstants.h" +#import "SPFileHandle.h" +#import "SPArrayAdditions.h" +#import "SPTableData.h" +#import "SPExportUtilities.h" + +@implementation SPDotExporter + +@synthesize delegate; +@synthesize dotExportTables; +@synthesize dotExportCurrentTable; +@synthesize dotTableData; +@synthesize dotDatabaseHost; +@synthesize dotDatabaseName; +@synthesize dotDatabaseVersion; + +/** + * Initialise an instance of SPDotExporter using the supplied delegate. + */ +- (id)initWithDelegate:(NSObject *)exportDelegate +{ + if ((self = [super init])) { + SPExportDelegateConformsToProtocol(exportDelegate, @protocol(SPDotExporterProtocol)); + + [self setDelegate:exportDelegate]; + [self setDotExportCurrentTable:nil]; + } + + return self; +} + +/** + * Start the Dot schema export process. This method is automatically called when an instance of this class + * is placed on an NSOperationQueue. Do not call it directly as there is no manual multithreading. + */ +- (void)main +{ + @try { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + NSMutableString *metaString = [NSMutableString string]; + + // Check that we have all the required info before starting the export + if ((![self dotExportTables]) || (![self dotTableData]) || ([[self dotExportTables] count] == 0)) { + [pool release]; + return; + } + + // Inform the delegate that the export process is about to begin + [delegate performSelectorOnMainThread:@selector(dotExportProcessWillBegin:) withObject:self waitUntilDone:NO]; + + // Mark the process as running + [self setExportProcessIsRunning:YES]; + + [metaString setString:@"// ************************************************************\n"]; + [metaString appendString:@"// Generated by: Sequel Pro\n"]; + [metaString appendString:[NSString stringWithFormat:@"// Version %@\n//\n", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]]]; + [metaString appendString:[NSString stringWithFormat:@"// %@\n// %@\n//\n", SPHomePageURL, SPDevURL]]; + [metaString appendString:[NSString stringWithFormat:@"// Host: %@ (MySQL %@)\n", [self dotDatabaseHost], [self dotDatabaseVersion]]]; + [metaString appendString:[NSString stringWithFormat:@"// Database: %@\n", [self dotDatabaseName]]]; + [metaString appendString:[NSString stringWithFormat:@"// Generation Time: %@\n", [NSDate date]]]; + [metaString appendString:@"// ************************************************************\n\n"]; + + [metaString appendString:@"digraph \"Database Structure\" {\n"]; + [metaString appendString:[NSString stringWithFormat:@"\tlabel = \"ER Diagram: %@\";\n", [self dotDatabaseName]]]; + [metaString appendString:@"\tlabelloc = t;\n"]; + [metaString appendString:@"\tcompound = true;\n"]; + [metaString appendString:@"\tnode [ shape = record ];\n"]; + [metaString appendString:@"\tfontname = \"Helvetica\";\n"]; + [metaString appendString:@"\tranksep = 1.25;\n"]; + [metaString appendString:@"\tratio = 0.7;\n"]; + [metaString appendString:@"\trankdir = LR;\n"]; + + // Write information to the file + [[self exportOutputFileHandle] writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]]; + + NSMutableArray *fkInfo = [[NSMutableArray alloc] init]; + + // Process the tables + for (NSInteger i = 0; i < [[self dotExportTables] count]; i++) + { + // Check for cancellation flag + if ([self isCancelled]) { + [pool release]; + return; + } + + NSString *tableName = NSArrayObjectAtIndex([self dotExportTables], i); + NSDictionary *tableInfo = [[self dotTableData] informationForTable:tableName]; + + // Set the current table + [self setDotExportCurrentTable:tableName]; + + // Inform the delegate that we are about to start fetcihing data for the current table + [delegate performSelectorOnMainThread:@selector(dotExportProcessWillBeginFetchingData:) withObject:self waitUntilDone:NO]; + + NSString *hdrColor = @"#DDDDDD"; + + if ([[tableInfo objectForKey:@"type"] isEqualToString:@"View"]) { + hdrColor = @"#DDDDFF"; + } + + [metaString setString:[NSString stringWithFormat:@"\tsubgraph \"table_%@\" {\n", tableName]]; + [metaString appendString:@"\t\tnode [ shape = \"plaintext\" ];\n"]; + [metaString appendString:[NSString stringWithFormat:@"\t\t\"%@\" [ label=<\n", tableName]]; + [metaString appendString:@"\t\t\t<TABLE BORDER=\"0\" CELLSPACING=\"0\" CELLBORDER=\"1\">\n"]; + [metaString appendString:[NSString stringWithFormat:@"\t\t\t<TR><TD COLSPAN=\"3\" BGCOLOR=\"%@\">%@</TD></TR>\n", hdrColor, tableName]]; + + // Get the column info + NSArray *columnInfo = [tableInfo objectForKey:@"columns"]; + + for (NSInteger j = 0; j < [columnInfo count]; j++ ) + { + [metaString appendString:[NSString stringWithFormat:@"\t\t\t<TR><TD COLSPAN=\"3\" PORT=\"%@\">%@:<FONT FACE=\"Helvetica-Oblique\" POINT-SIZE=\"10\">%@</FONT></TD></TR>\n", [[columnInfo objectAtIndex:j] objectForKey:@"name"], [[columnInfo objectAtIndex:j] objectForKey:@"name"], [[columnInfo objectAtIndex:j] objectForKey:@"type"]]]; + } + + [metaString appendString:@"\t\t\t</TABLE>>\n"]; + [metaString appendString:@"\t\t];\n"]; + [metaString appendString:@"\t}\n"]; + + [[self exportOutputFileHandle] writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]]; + + // see about relations + columnInfo = [tableInfo objectForKey:@"constraints"]; + + for (NSInteger j = 0; j < [columnInfo count]; j++) + { + // Check for cancellation flag + if ([self isCancelled]) { + [pool release]; + return; + } + + // Get the column references. Currently the columns themselves are an array, + // while reference columns and tables are comma separated if there are more than + // one. Only use the first of each for the time being. + NSArray *ccols = [NSArrayObjectAtIndex(columnInfo, j) objectForKey:@"columns"]; + NSString *ccol = NSArrayObjectAtIndex(columnInfo, 0); + NSString *rcol = [NSArrayObjectAtIndex(columnInfo, j) objectForKey:@"ref_columns"]; + + NSString *extra = @""; + + if ([ccols count] > 1) { + extra = @" [ arrowhead=crow, arrowtail=odiamond ]"; + rcol = NSArrayObjectAtIndex([rcol componentsSeparatedByString:@","], 0); + } + + [fkInfo addObject:[NSString stringWithFormat:@"%@:%@ -> %@:%@ %@", tableName, ccol, [NSArrayObjectAtIndex(columnInfo, j) objectForKey:@"ref_table"], rcol, extra]]; + } + } + + // Inform the delegate that we are about to start fetching relations data for the current table + [delegate performSelectorOnMainThread:@selector(dotExportProcessWillBeginFetchingRelationsData:) withObject:self waitUntilDone:NO]; + + [metaString setString:@"edge [ arrowhead=inv, arrowtail=normal, style=dashed, color=\"#444444\" ];\n"]; + + // Get the relations + for (NSInteger i = 0; i < [fkInfo count]; i++) + { + [metaString appendString:[NSString stringWithFormat:@"%@;\n", [fkInfo objectAtIndex:i]]]; + } + + [fkInfo release]; + + [metaString appendString:@"}\n"]; + + // Write information to the file + [[self exportOutputFileHandle] writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]]; + + // Write data to disk + [[self exportOutputFileHandle] closeFile]; + + // Mark the process as not running + [self setExportProcessIsRunning:NO]; + + // Inform the delegate that the export process is complete + [delegate performSelectorOnMainThread:@selector(dotExportProcessComplete:) withObject:self waitUntilDone:NO]; + + [pool release]; + } + @catch (NSException *e) { } +} + +/** + * Dealloc + */ +- (void)dealloc +{ + delegate = nil; + + [dotExportTables release], dotExportTables = nil; + [dotExportCurrentTable release], dotExportCurrentTable = nil; + [dotTableData release], dotTableData = nil; + [dotDatabaseHost release], dotDatabaseHost = nil; + [dotDatabaseName release], dotDatabaseName = nil; + [dotDatabaseVersion release], dotDatabaseVersion = nil; + + [super dealloc]; +} + +@end diff --git a/Source/SPDotExporterDelegate.h b/Source/SPDotExporterDelegate.h new file mode 100644 index 00000000..6661bca5 --- /dev/null +++ b/Source/SPDotExporterDelegate.h @@ -0,0 +1,38 @@ +// +// $Id$ +// +// SPDotExporterDelegate.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 17, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPExportController.h" +#import "SPDotExporterProtocol.h" + +/** + * @category SPDotExporterDelegate SPDotExporterDelegate.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * Dot exporter delegate category. + */ +@interface SPExportController (SPDotExporterDelegate) <SPDotExporterProtocol> + +@end diff --git a/Source/SPDotExporterDelegate.m b/Source/SPDotExporterDelegate.m new file mode 100644 index 00000000..ce74e408 --- /dev/null +++ b/Source/SPDotExporterDelegate.m @@ -0,0 +1,98 @@ +// +// $Id$ +// +// SPDotExporterDelegate.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 17, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPDotExporterDelegate.h" +#import "SPDotExporter.h" +#import "TableDocument.h" +#import "SPMainThreadTrampoline.h" + +@implementation SPExportController (SPDotExporterDelegate) + +/** + * + */ +- (void)dotExportProcessWillBegin:(SPDotExporter *)exporter +{ + [[exportProgressTitle onMainThread] setStringValue:NSLocalizedString(@"Exporting Dot File", @"text showing that the application is exporting a Dot file")]; + [[exportProgressText onMainThread] setStringValue:NSLocalizedString(@"Dumping...", @"text showing that app is writing dump")]; + + [[exportProgressTitle onMainThread] displayIfNeeded]; + [[exportProgressText onMainThread] displayIfNeeded]; +} + +/** + * + */ +- (void)dotExportProcessComplete:(SPDotExporter *)exporter +{ + [NSApp endSheet:exportProgressWindow returnCode:0]; + [exportProgressWindow orderOut:self]; + + [tableDocumentInstance setQueryMode:SPInterfaceQueryMode]; + + // Restore the connection encoding to it's pre-export value + [tableDocumentInstance setConnectionEncoding:[NSString stringWithFormat:@"%@%@", sqlPreviousConnectionEncoding, (sqlPreviousConnectionEncodingViaLatin1) ? @"-" : @""] reloadingViews:NO]; + + // Display Growl notification + [self displayExportFinishedGrowlNotification]; +} + +/** + * + */ +- (void)dotExportProcessProgressUpdated:(SPDotExporter *)exporter +{ + +} + +/** + * + */ +- (void)dotExportProcessWillBeginFetchingData:(SPDotExporter *)exporter +{ + // Update the current table export index + currentTableExportIndex = (exportTableCount - [exporters count]); + + [[exportProgressText onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %lu of %lu (%@): Fetching data...", @"export label showing that the app is fetching data for a specific table"), currentTableExportIndex, exportTableCount, [exporter dotExportCurrentTable]]]; + + [[exportProgressText onMainThread] displayIfNeeded]; + + [[exportProgressIndicator onMainThread] stopAnimation:self]; + [[exportProgressIndicator onMainThread] setUsesThreadedAnimation:NO]; + [[exportProgressIndicator onMainThread] setIndeterminate:NO]; + [[exportProgressIndicator onMainThread] setDoubleValue:0]; +} + +/** + * + */ +- (void)dotExportProcessWillBeginFetchingRelationsData:(SPDotExporter *)exporter +{ + [[exportProgressText onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %lu of %lu (%@): Fetching relations data...", @"export label showing app is fetching relations data for a specific table"), currentTableExportIndex, exportTableCount, [exporter dotExportCurrentTable]]]; + + [[exportProgressText onMainThread] displayIfNeeded]; +} + +@end diff --git a/Source/SPDotExporterProtocol.h b/Source/SPDotExporterProtocol.h new file mode 100644 index 00000000..5d2c3db3 --- /dev/null +++ b/Source/SPDotExporterProtocol.h @@ -0,0 +1,72 @@ +// +// $Id$ +// +// SPDotExporterProtocol.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 15, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +@class SPDotExporter; + +/** + * @protocol SPDotExporterProtocol SPDotExporterProtocol.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * Dot schema exporter delegate protocol. + */ +@protocol SPDotExporterProtocol + +/** + * Called when the dot export process is about to begin. + * + * @param SPDotExpoter The expoter calling the method. + */ +- (void)dotExportProcessWillBegin:(SPDotExporter *)exporter; + +/** + * Called when the dot export process is complete. + * + * @param SPDotExpoter The expoter calling the method. + */ +- (void)dotExportProcessComplete:(SPDotExporter *)exporter; + +/** + * Called when the progress of the dot export process is updated. + * + * @param SPDotExpoter The expoter calling the method. + */ +- (void)dotExportProcessProgressUpdated:(SPDotExporter *)exporter; + +/** + * Called when the dot export process is about to begin fetching data from the database. + * + * @param SPDotExpoter The expoter calling the method. + */ +- (void)dotExportProcessWillBeginFetchingData:(SPDotExporter *)exporter; + +/** + * Called when the dot export process is about to begin writing data to disk. + * + * @param SPDotExpoter The expoter calling the method. + */ +- (void)dotExportProcessWillBeginFetchingRelationsData:(SPDotExporter *)exporter; + +@end diff --git a/Source/SPExportController.h b/Source/SPExportController.h index 773b678d..0865fef3 100644 --- a/Source/SPExportController.h +++ b/Source/SPExportController.h @@ -5,6 +5,7 @@ // sequel-pro // // Created by Ben Perry (benperry.com.au) on 21/02/09. +// Modified by Stuart Connolly (stuconnolly.com) // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -23,118 +24,231 @@ // More info at <http://code.google.com/p/sequel-pro/> #import <Cocoa/Cocoa.h> -#import <MCPKit/MCPKit.h> -#import "SPExporterDataAccess.h" +#import "SPConstants.h" -#import "SPLogger.h" +@class MCPConnection, BWAnchoredButtonBar; -// 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 <SPExporterDataAccess> +/** + * @class SPExportController SPExportController.m + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * Export controller. + */ +@interface SPExportController : NSWindowController { - // Table document + // Controllers IBOutlet id tableDocumentInstance; - - // Tables list + IBOutlet id tableContentInstance; + IBOutlet id customQueryInstance; IBOutlet id tablesListInstance; - - // Table data IBOutlet id tableDataInstance; // Export window - IBOutlet id exportWindow; - IBOutlet id exportToolbar; - IBOutlet id exportPathField; - IBOutlet id exportTableList; - IBOutlet id exportTabBar; - IBOutlet id exportInputMatrix; - IBOutlet id exportFilePerTableCheck; - IBOutlet id exportFilePerTableNote; - IBOutlet id exportProcessLowMemory; + IBOutlet NSButton *exportButton; + IBOutlet NSToolbar *exportToolbar; + IBOutlet NSTextField *exportPathField; + IBOutlet NSTableView *exportTableList; + IBOutlet NSTabView *exportTabBar; + IBOutlet NSMatrix *exportInputMatrix; + IBOutlet NSButton *exportFilePerTableCheck; + IBOutlet NSTextField *exportFilePerTableNote; + IBOutlet NSButton *exportSelectAllTablesButton; + IBOutlet NSButton *exportDeselectAllTablesButton; + IBOutlet NSButton *exportRefreshTablesButton; + IBOutlet NSScrollView *exportTablelistScrollView; + + // Errors sheet + IBOutlet NSWindow *errorsWindow; + IBOutlet NSTextView *errorsTextView; + + // Advanced options view + IBOutlet NSButton *exportAdvancedOptionsViewButton; + IBOutlet NSView *exportAdvancedOptionsView; + IBOutlet NSButton *exportAdvancedOptionsViewLabelButton; + IBOutlet NSButton *exportUseUTF8BOMButton; + IBOutlet NSButton *exportCompressOutputFile; + IBOutlet NSButton *exportProcessLowMemoryButton; + IBOutlet NSTextField *exportCSVNULLValuesAsTextField; + + IBOutlet BWAnchoredButtonBar *exportTableListButtonBar; // Export progress sheet - IBOutlet id exportProgressWindow; - IBOutlet id exportProgressTitle; - IBOutlet id exportProgressText; - IBOutlet id exportProgressIndicator; + IBOutlet NSWindow *exportProgressWindow; + IBOutlet NSTextField *exportProgressTitle; + IBOutlet NSTextField *exportProgressText; + IBOutlet NSProgressIndicator *exportProgressIndicator; + + // Custom filename view + IBOutlet NSView *exportCustomFilenameView; + IBOutlet NSButton *exportCustomFilenameButton; + IBOutlet NSTokenField *exportCustomFilenameTokenField; + IBOutlet NSTokenField *exportCustomFilenameTokensField; + IBOutlet NSTextField *exportCustomFilenameExampleTextField; // SQL - IBOutlet id exportSQLIncludeStructureCheck; - IBOutlet id exportSQLIncludeDropSyntaxCheck; - IBOutlet id exportSQLIncludeErrorsCheck; + IBOutlet NSButton *exportSQLIncludeStructureCheck; + IBOutlet NSButton *exportSQLIncludeDropSyntaxCheck; + IBOutlet NSButton *exportSQLIncludeContentCheck; + IBOutlet NSButton *exportSQLIncludeErrorsCheck; + IBOutlet NSButton *exportSQLBLOBFieldsAsHexCheck; // Excel - IBOutlet id exportExcelSheetOrFilePerTableMatrix; + IBOutlet NSMatrix *exportExcelSheetOrFilePerTableMatrix; // CSV - IBOutlet id exportCSVIncludeFieldNamesCheck; - IBOutlet id exportCSVFieldsTerminatedField; - IBOutlet id exportCSVFieldsWrappedField; - IBOutlet id exportCSVFieldsEscapedField; - IBOutlet id exportCSVLinesTerminatedField; + IBOutlet NSButton *exportCSVIncludeFieldNamesCheck; + IBOutlet NSComboBox *exportCSVFieldsTerminatedField; + IBOutlet NSComboBox *exportCSVFieldsWrappedField; + IBOutlet NSComboBox *exportCSVFieldsEscapedField; + IBOutlet NSComboBox *exportCSVLinesTerminatedField; // HTML - IBOutlet id exportHTMLIncludeStructureCheck; - IBOutlet id exportHTMLIncludeHeadAndBodyTagsCheck; - - // XML - IBOutlet id exportXMLIncludeStructureCheck; + IBOutlet NSButton *exportHTMLIncludeStructureCheck; // PDF - IBOutlet id exportPDFIncludeStructureCheck; - - // Token name view - IBOutlet id tokenNameView; - IBOutlet id tokenNameField; - IBOutlet id tokenNameTokensField; - IBOutlet id exampleNameLabel; + IBOutlet NSButton *exportPDFIncludeStructureCheck; - // Cancellation flag + /** + * Cancellation flag + */ BOOL exportCancelled; - // Current database's tables + /** + * Multi-file export flag + */ + BOOL exportToMultipleFiles; + + /** + * Create custom filename flag + */ + BOOL createCustomFilename; + + /** + * Number of tables being exported + */ + NSUInteger exportTableCount; + + /** + * Index of the current table being exported + */ + NSUInteger currentTableExportIndex; + + /** + * Export type label + */ + NSString *exportTypeLabel; + + /** + * Export filename + */ + NSString *exportFilename; + + /** + * Current database's tables + */ NSMutableArray *tables; - // Database connection + /** + * Database connection + */ MCPConnection *connection; - // Concurrent operation queue + /** + * Concurrent operation queue + */ NSOperationQueue *operationQueue; - // Table/export operation mapping - NSMutableDictionary *tableExportMapping; + /** + * Exporters + */ + NSMutableArray *exporters; + + /** + * Global export file handle + */ + NSFileHandle *exportFileHandle; + + /** + * Export type + */ + SPExportType exportType; + + /** + * Export source + */ + SPExportSource exportSource; + + /** + * Available filename tokens + */ + NSString *availableFilenameTokens; + + /** + * Display advanced view flag + */ + BOOL showAdvancedView; + + /** + * User defaults + */ + NSUserDefaults *prefs; + + /** + * Current toolbar item + */ + NSToolbarItem *currentToolbarItem; + + /** + * Previous connection encoding + */ + NSString *sqlPreviousConnectionEncoding; + + /** + * Previous connection encoding was via Latin1 + */ + BOOL sqlPreviousConnectionEncodingViaLatin1; - // Top-level nib objects that require releasing on dealloc - NSMutableArray *nibObjectsToRelease; + NSInteger heightOffset; + NSUInteger windowMinWidth; + NSUInteger windowMinHeigth; } -@property (readwrite, assign) BOOL exportCancelled; -@property (readwrite, assign) MCPConnection *connection; +/** + * @property exportCancelled Export cancellation flag + */ +@property(readwrite, assign) BOOL exportCancelled; + +/** + * @property exportToMultipleFiles Export to multiple files flag + */ +@property(readwrite, assign) BOOL exportToMultipleFiles; + +/** + * @property connection Database connection + */ +@property(readwrite, assign) MCPConnection *connection; -// IB action methods - (void)export; +- (void)exportTables:(NSArray *)table asFormat:(SPExportType)format; +- (void)openExportErrorsSheetWithString:(NSString *)errors; +- (void)displayExportFinishedGrowlNotification; +- (NSString *)expandCustomFilenameFormatFromString:(NSString *)format usingTableName:(NSString *)table; + +// IB action methods - (IBAction)closeSheet:(id)sender; - (IBAction)switchTab:(id)sender; - (IBAction)switchInput:(id)sender; - (IBAction)cancelExport:(id)sender; - (IBAction)changeExportOutputPath:(id)sender; +- (IBAction)refreshTableList:(id)sender; +- (IBAction)selectDeselectAllTables:(id)sender; +- (IBAction)toggleCustomFilenameFormat:(id)sender; +- (IBAction)toggleAdvancedExportOptionsView:(id)sender; + +- (IBAction)toggleSQLIncludeStructure:(id)sender; +- (IBAction)toggleSQLIncludeContent:(id)sender; +- (IBAction)toggleSQLIncludeDropSyntax:(id)sender; @end diff --git a/Source/SPExportController.m b/Source/SPExportController.m index 559e8071..83c87ea6 100644 --- a/Source/SPExportController.m +++ b/Source/SPExportController.m @@ -5,6 +5,7 @@ // sequel-pro // // Created by Ben Perry (benperry.com.au) on 21/02/09. +// Modified by Stuart Connolly (stuconnolly.com) // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -22,39 +23,68 @@ // // More info at <http://code.google.com/p/sequel-pro/> +#import <MCPKit/MCPKit.h> + #import "SPExportController.h" -#import "SPCSVExporter.h" +#import "SPExportInitializer.h" #import "TablesList.h" #import "SPTableData.h" -#import "TableDocument.h" +#import "TableContent.h" #import "SPArrayAdditions.h" #import "SPStringAdditions.h" #import "SPConstants.h" +#import "SPGrowlController.h" @interface SPExportController (PrivateAPI) -- (void)_initializeExportUsingSelectedOptions; -- (BOOL)_exportTables:(NSArray *)exportTables asType:(SPExportType)type toMultipleFiles:(BOOL)multipleFiles; +- (void)_toggleExportButton; +- (void)_resizeWindowByHeightDelta:(NSInteger)delta; @end @implementation SPExportController @synthesize connection; +@synthesize exportToMultipleFiles; @synthesize exportCancelled; +#pragma mark - +#pragma mark Initialization + /** * Initializes an instance of SPExportController */ - (id)init { - if ((self = [super init])) { + if (self = [super initWithWindowNibName:@"ExportDialog"]) { + [self setExportCancelled:NO]; + [self setExportToMultipleFiles:YES]; + + exportType = 0; + exportTableCount = 0; + currentTableExportIndex = 0; + + exportFilename = @""; + exportTypeLabel = @""; + + createCustomFilename = NO; + sqlPreviousConnectionEncodingViaLatin1 = NO; tables = [[NSMutableArray alloc] init]; + exporters = [[NSMutableArray alloc] init]; operationQueue = [[NSOperationQueue alloc] init]; - tableExportMapping = [NSMutableDictionary dictionary]; - nibObjectsToRelease = [[NSMutableArray alloc] init]; + + showAdvancedView = NO; + + heightOffset = 0; + windowMinWidth = [[self window] minSize].width; + windowMinHeigth = [[self window] minSize].height; + + prefs = [NSUserDefaults standardUserDefaults]; + + // Default filename tokens + availableFilenameTokens = @"host,database,table,date,time"; } return self; @@ -64,9 +94,12 @@ * Upon awakening select the first toolbar item */ - (void)awakeFromNib -{ +{ // Upon awakening select the SQL tab [exportToolbar setSelectedItemIdentifier:[[[exportToolbar items] objectAtIndex:0] itemIdentifier]]; + + // Select the 'selected tables' option + [exportInputMatrix selectCellAtRow:2 column:0]; } #pragma mark - @@ -76,52 +109,166 @@ * Display the export window allowing the user to select what and of what type to export. */ - (void)export -{ +{ + [self exportTables:nil asFormat:0]; +} - // If the dialog hasn't been loaded yet, do so, retaining a reference to the top-level objects that need releasing. - if (!exportWindow) - { - NSArray *exportDialogTopLevelObjects = nil; - NSNib *nibLoader = [[NSNib alloc] initWithNibNamed:@"ExportDialog" bundle:[NSBundle mainBundle]]; - [nibLoader instantiateNibWithOwner:self topLevelObjects:&exportDialogTopLevelObjects]; - [nibObjectsToRelease addObjectsFromArray:exportDialogTopLevelObjects]; - [nibLoader release]; +/** + * Displays the export window with the supplied tables and export type/format selected. + */ +- (void)exportTables:(NSArray *)exportTables asFormat:(SPExportType)format +{ + [self refreshTableList:self]; + + if (exportTables && format) { + + // Select the correct tab according to the supplied export type + [exportToolbar setSelectedItemIdentifier:[[[exportToolbar items] objectAtIndex:(format - 1)] itemIdentifier]]; + + // Select the 'selected tables' source option + [exportInputMatrix selectCellAtRow:2 column:0]; + + // Disable all tables + for (NSMutableArray *table in tables) + { + [table replaceObjectAtIndex:1 withObject:[NSNumber numberWithBool:NO]]; + [table replaceObjectAtIndex:2 withObject:[NSNumber numberWithBool:NO]]; + [table replaceObjectAtIndex:3 withObject:[NSNumber numberWithBool:NO]]; + } + + // Select the supplied tables + for (NSMutableArray *table in tables) + { + for (NSString *exportTable in exportTables) + { + if ([exportTable isEqualToString:[table objectAtIndex:0]]) { + [table replaceObjectAtIndex:1 withObject:[NSNumber numberWithBool:YES]]; + [table replaceObjectAtIndex:2 withObject:[NSNumber numberWithBool:YES]]; + [table replaceObjectAtIndex:3 withObject:[NSNumber numberWithBool:YES]]; + } + } + } + + [exportTableList reloadData]; + + // Ensure interface validation + [self switchTab:[[exportToolbar items] objectAtIndex:(format - 1)]]; } - NSUInteger i; + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES); - [tables removeAllObjects]; + // If found the set the default path to the user's desktop, otherwise use their home directory + [exportPathField setStringValue:([paths count] > 0) ? [paths objectAtIndex:0] : NSHomeDirectory()]; - MCPResult *queryResult = (MCPResult *)[[self connection] listTables]; + [NSApp beginSheet:[self window] + modalForWindow:[tableDocumentInstance parentWindow] + modalDelegate:self + didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) + contextInfo:nil]; +} + +/** + * Opens the errors sheet and displays the supplied errors string. + */ +- (void)openExportErrorsSheetWithString:(NSString *)errors +{ + [errorsTextView setString:@""]; + [errorsTextView setString:errors]; - if ([queryResult numOfRows]) [queryResult dataSeek:0]; + [NSApp beginSheet:errorsWindow + modalForWindow:[tableDocumentInstance parentWindow] + modalDelegate:self + didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) + contextInfo:nil]; +} + +/** + * Displays the export finished Growl notification. + */ +- (void)displayExportFinishedGrowlNotification +{ + // Export finished Growl notification + [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Export Finished" + description:[NSString stringWithFormat:NSLocalizedString(@"Finished exporting to %@", @"description for finished exporting growl notification"), exportFilename] + window:[tableDocumentInstance parentWindow] + notificationName:@"Export Finished"]; +} + +/** + * Expands the custom filename format based on the selected tokens. + */ +- (NSString *)expandCustomFilenameFormatFromString:(NSString *)format usingTableName:(NSString *)table +{ + NSMutableString *string = [NSMutableString stringWithString:format]; - for ( i = 0 ; i < [queryResult numOfRows] ; i++ ) - { - [tables addObject:[NSMutableArray arrayWithObjects: - [NSNumber numberWithBool:YES], - NSArrayObjectAtIndex([queryResult fetchRowAsArray], 0), - nil]]; + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + + [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; + + [dateFormatter setDateStyle:NSDateFormatterShortStyle]; + [dateFormatter setTimeStyle:NSDateFormatterNoStyle]; + + [string replaceOccurrencesOfString:@"host" withString:[tableDocumentInstance host] + options:NSLiteralSearch + range:NSMakeRange(0, [string length])]; + + [string replaceOccurrencesOfString:@"database" withString:[tableDocumentInstance database] + options:NSLiteralSearch + range:NSMakeRange(0, [string length])]; + + if (table) { + [string replaceOccurrencesOfString:@"table" withString:table + options:NSLiteralSearch + range:NSMakeRange(0, [string length])]; + } + else { + [string replaceOccurrencesOfString:@"table" withString:@"" + options:NSLiteralSearch + range:NSMakeRange(0, [string length])]; } - [exportTableList reloadData]; - - [exportPathField setStringValue:NSHomeDirectory()]; + [string replaceOccurrencesOfString:@"date" withString:[dateFormatter stringFromDate:[NSDate date]] + options:NSLiteralSearch + range:NSMakeRange(0, [string length])]; - [NSApp beginSheet:exportWindow - modalForWindow:[tableDocumentInstance parentWindow] - modalDelegate:self - didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) - contextInfo:nil]; + [dateFormatter setDateStyle:NSDateFormatterNoStyle]; + [dateFormatter setTimeStyle:NSDateFormatterShortStyle]; + + [string replaceOccurrencesOfString:@"time" withString:[dateFormatter stringFromDate:[NSDate date]] + options:NSLiteralSearch + range:NSMakeRange(0, [string length])]; + + // Strip comma separators + [string replaceOccurrencesOfString:@"," withString:@"" + options:NSLiteralSearch + range:NSMakeRange(0, [string length])]; + + // Replace colons with hyphens + [string replaceOccurrencesOfString:@":" withString:@"-" + options:NSLiteralSearch + range:NSMakeRange(0, [string length])]; + + [dateFormatter release]; + + return string; } /** - * Closes the export dialog + * Closes the export dialog. */ - (IBAction)closeSheet:(id)sender { - [NSApp endSheet:exportWindow returnCode:[sender tag]]; - [exportWindow orderOut:self]; + if ([sender window] == [self window]) { + + // Close the advanced options view if it's open + [exportAdvancedOptionsView setHidden:YES]; + [exportAdvancedOptionsViewButton setState:NSOffState]; + + [self _resizeWindowByHeightDelta:0]; + } + + [NSApp endSheet:[sender window] returnCode:[sender tag]]; + [[sender window] orderOut:self]; } /** @@ -130,20 +277,70 @@ - (IBAction)switchTab:(id)sender { if ([sender isKindOfClass:[NSToolbarItem class]]) { - [exportTabBar selectTabViewItemWithIdentifier:[[sender label] lowercaseString]]; + + currentToolbarItem = sender; + + NSString *label = [[currentToolbarItem label] lowercaseString]; + + [exportTabBar selectTabViewItemWithIdentifier:label]; + + BOOL isSQL = [label isEqualToString:@"sql"]; + BOOL isCSV = [label isEqualToString:@"csv"]; + BOOL isXML = [label isEqualToString:@"xml"]; + BOOL isHTML = [label isEqualToString:@"html"]; + BOOL isPDF = [label isEqualToString:@"pdf"]; + BOOL isDot = [label isEqualToString:@"dot"]; + + BOOL disable = (isCSV || isXML || isHTML || isPDF || isDot); + + [exportFilePerTableCheck setHidden:(isSQL || isDot)]; + [exportFilePerTableNote setHidden:(isSQL || isDot)]; + + [exportTableList setEnabled:(!isDot)]; + [exportSelectAllTablesButton setEnabled:(!isDot)]; + [exportDeselectAllTablesButton setEnabled:(!isDot)]; + [exportRefreshTablesButton setEnabled:(!isDot)]; + + [[exportInputMatrix cellAtRow:2 column:0] setEnabled:(!isDot)]; + + if (isDot) { + // Disable all source checkboxes + [[exportInputMatrix cellAtRow:0 column:0] setEnabled:NO]; + [[exportInputMatrix cellAtRow:1 column:0] setEnabled:NO]; + } + else { + // Enable/disable the 'filtered result' and 'query result' options + [[exportInputMatrix cellAtRow:0 column:0] setEnabled:((disable) && ([[tableContentInstance currentResult] count] > 1))]; + [[exportInputMatrix cellAtRow:1 column:0] setEnabled:((disable) && ([[customQueryInstance currentResult] count] > 1))]; + } - [exportFilePerTableCheck setHidden:[[sender label] isEqualToString:@"Excel"]]; - [exportFilePerTableNote setHidden:[[sender label] isEqualToString:@"Excel"]]; + [[exportTableList tableColumnWithIdentifier:@"structure"] setHidden:disable]; + [[exportTableList tableColumnWithIdentifier:@"drop"] setHidden:disable]; + + [[[exportTableList tableColumnWithIdentifier:@"content"] headerCell] setStringValue:(disable) ? @"" : @"C"]; + + [exportCSVNULLValuesAsTextField setStringValue:[prefs stringForKey:SPNullValue]]; } } /** - * + * Enables/disables and shows/hides various interface controls depending on the selected item. */ - (IBAction)switchInput:(id)sender { if ([sender isKindOfClass:[NSMatrix class]]) { - [exportTableList setEnabled:([[sender selectedCell] tag] == 3)]; + + BOOL isSelectedTables = ([[sender selectedCell] tag] == SPTableExport); + + [exportFilePerTableCheck setHidden:(!isSelectedTables)]; + [exportFilePerTableNote setHidden:(!isSelectedTables)]; + + [exportTableList setEnabled:isSelectedTables]; + [exportSelectAllTablesButton setEnabled:isSelectedTables]; + [exportDeselectAllTablesButton setEnabled:isSelectedTables]; + [exportRefreshTablesButton setEnabled:isSelectedTables]; + + availableFilenameTokens = ([[sender selectedCell] tag] == SPQueryExport) ? @"host,database,date,time" : @"host,database,table,date,time"; } } @@ -164,24 +361,161 @@ } /** - * + * Opens the open panel when user selects to change the output path. */ - (IBAction)changeExportOutputPath:(id)sender { + [exportCustomFilenameTokenField setStringValue:@""]; + [exportCustomFilenameTokensField setStringValue:availableFilenameTokens]; + NSOpenPanel *panel = [NSOpenPanel openPanel]; [panel setCanChooseFiles:NO]; [panel setCanChooseDirectories:YES]; [panel setCanCreateDirectories:YES]; + [panel setAccessoryView:exportCustomFilenameView]; + + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES); - [panel beginSheetForDirectory:NSHomeDirectory() + [panel beginSheetForDirectory:([paths count] > 0) ? [paths objectAtIndex:0] : NSHomeDirectory() file:nil - modalForWindow:exportWindow + modalForWindow:[self window] modalDelegate:self didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo:nil]; } +/** + * Refreshes the table list. + */ +- (IBAction)refreshTableList:(id)sender +{ + NSUInteger i; + + [tables removeAllObjects]; + + // For all modes, retrieve table and view names + NSArray *tablesAndViews = [tablesListInstance allTableAndViewNames]; + + for (id itemName in tablesAndViews) { + [tables addObject:[NSMutableArray arrayWithObjects: + itemName, + [NSNumber numberWithBool:YES], + [NSNumber numberWithBool:YES], + [NSNumber numberWithBool:YES], + [NSNumber numberWithInt:SPTableTypeTable], + nil]]; + } + + // For SQL only, add procedures and functions + if ([[[currentToolbarItem label] lowercaseString] isEqualToString:@"sql"]) { + NSArray *procedures = [tablesListInstance allProcedureNames]; + + for (id procName in procedures) + { + [tables addObject:[NSMutableArray arrayWithObjects: + procName, + [NSNumber numberWithBool:YES], + [NSNumber numberWithBool:YES], + [NSNumber numberWithBool:YES], + [NSNumber numberWithInt:SPTableTypeProc], + nil]]; + } + + NSArray *functions = [tablesListInstance allFunctionNames]; + + for (id funcName in functions) + { + [tables addObject:[NSMutableArray arrayWithObjects: + funcName, + [NSNumber numberWithBool:YES], + [NSNumber numberWithBool:YES], + [NSNumber numberWithBool:YES], + [NSNumber numberWithInt:SPTableTypeFunc], + nil]]; + } + } + + [exportTableList reloadData]; +} + +/** + * Selects or de-selects all tables. + */ +- (IBAction)selectDeselectAllTables:(id)sender +{ + [self refreshTableList:self]; + + for (NSMutableArray *table in tables) + { + [table replaceObjectAtIndex:2 withObject:[NSNumber numberWithBool:[sender tag]]]; + } + + [exportTableList reloadData]; + + [self _toggleExportButton]; +} + +/** + * Toggles the state of the custom filename format token fields. + */ +- (IBAction)toggleCustomFilenameFormat:(id)sender +{ + [exportCustomFilenameTokenField setEnabled:[sender state]]; + [exportCustomFilenameTokensField setEnabled:[sender state]]; +} + +/** + * Toggles the display of the advanced options box. + */ +- (IBAction)toggleAdvancedExportOptionsView:(id)sender +{ + showAdvancedView = !showAdvancedView; + + if (showAdvancedView) { + [exportAdvancedOptionsViewButton setState:NSOnState]; + [self _resizeWindowByHeightDelta:([exportAdvancedOptionsView frame].size.height + 10)]; + [exportAdvancedOptionsView setHidden:NO]; + } + else { + [exportAdvancedOptionsViewButton setState:NSOffState]; + [self _resizeWindowByHeightDelta:0]; + [exportAdvancedOptionsView setHidden:YES]; + } +} + +/** + * Toggles the export button when choosing to include or table structures in an SQL export. + */ +- (IBAction)toggleSQLIncludeStructure:(id)sender +{ + [[exportTableList tableColumnWithIdentifier:@"structure"] setHidden:(![sender state])]; + + [self _toggleExportButton]; +} + +/** + * Toggles the export button when choosing to include or exclude table contents in an SQL export. + */ +- (IBAction)toggleSQLIncludeContent:(id)sender +{ + [sender setTag:[sender state]]; + + [self selectDeselectAllTables:sender]; + + [self _toggleExportButton]; +} + +/** + * Toggles the export button when choosing to include or exclude table drop syntax in an SQL export. + */ +- (IBAction)toggleSQLIncludeDropSyntax:(id)sender +{ + [[exportTableList tableColumnWithIdentifier:@"drop"] setHidden:(![sender state])]; + + [self _toggleExportButton]; +} + #pragma mark - #pragma mark Table view datasource methods @@ -190,30 +524,32 @@ return [tables count]; } -- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex -{ - return NSArrayObjectAtIndex([tables objectAtIndex:rowIndex], ([[aTableColumn identifier] isEqualToString:@"switch"]) ? 0 : 1); +- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex +{ + return NSArrayObjectAtIndex([tables objectAtIndex:rowIndex], [exportTableList columnWithIdentifier:[tableColumn identifier]]); } -- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex -{ - [[tables objectAtIndex:rowIndex] replaceObjectAtIndex:0 withObject:anObject]; +- (void)tableView:(NSTableView *)tableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex +{ + [[tables objectAtIndex:rowIndex] replaceObjectAtIndex:[exportTableList columnWithIdentifier:[tableColumn identifier]] withObject:anObject]; + + [self _toggleExportButton]; } #pragma mark - #pragma mark Table view delegate methods -- (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(NSInteger)rowIndex +- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)rowIndex { - return (aTableView != exportTableList); + return (tableView != exportTableList); } -- (BOOL)tableView:(NSTableView *)aTableView shouldTrackCell:(NSCell *)cell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +- (BOOL)tableView:(NSTableView *)tableView shouldTrackCell:(NSCell *)cell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex { - return (aTableView == exportTableList); + return (tableView == exportTableList); } -- (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex +- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex { [aCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; } @@ -223,7 +559,7 @@ - (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar { - NSMutableArray *items = [NSMutableArray arrayWithCapacity:6]; + NSMutableArray *items = [NSMutableArray array]; for (NSToolbarItem *item in [toolbar items]) { @@ -234,21 +570,16 @@ } #pragma mark - -#pragma mark SPExporterDataAccess protocol methods +#pragma mark Text field delegate methods -/** - * This method is part of the SPExporterDataAccess protocol. It is called when an expoter complete it's data - * conversion process and the operation is effectively complete. The resulting data can be accessed via - * SPExporter's exportData method. - */ -- (void)exporterDataConversionProcessComplete:(SPExporter *)exporter -{ - // Do something with the data... - - // If there are no more operations in the queue, close the progress sheet - if ([[operationQueue operations] count] == 0) { - [NSApp endSheet:exportProgressWindow returnCode:0]; - [exportProgressWindow orderOut:self]; +- (void)controlTextDidChange:(NSNotification *)notification +{ + if ([notification object] == exportCustomFilenameTokenField) { + + // Create the table name, but since this is only an example, use the first table in the list + NSString *filename = [self expandCustomFilenameFormatFromString:[exportCustomFilenameTokenField stringValue] usingTableName:[[tablesListInstance tables] objectAtIndex:1]]; + + [exportCustomFilenameExampleTextField setStringValue:[NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"Example", @"example label"), filename]]; } } @@ -256,7 +587,7 @@ #pragma mark Other /** - * Invoked when the user + * Invoked when the user dismissing the export dialog and starts the export process if required. */ - (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { @@ -264,12 +595,12 @@ if (returnCode == NSOKButton) { // Initialize the export after half a second to give the export sheet a chance to close - [self performSelector:@selector(_initializeExportUsingSelectedOptions) withObject:nil afterDelay:0.5]; + [self performSelector:@selector(initializeExportUsingSelectedOptions) withObject:nil afterDelay:0.5]; } } /** - * Invoked when the user dismisses the save panel. Updates the selected directory is they clicked OK. + * Invoked when the user dismisses the save panel. Updates the selected directory if they clicked OK. */ - (void)savePanelDidEnd:(NSSavePanel *)panel returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { @@ -286,247 +617,104 @@ - (void)dealloc { [tables release], tables = nil; + [exporters release], exporters = nil; [operationQueue release], operationQueue = nil; - for (id retainedObject in nibObjectsToRelease) [retainedObject release]; - [nibObjectsToRelease release], nibObjectsToRelease = nil; + + if (sqlPreviousConnectionEncoding) [sqlPreviousConnectionEncoding release], sqlPreviousConnectionEncoding = nil; [super dealloc]; } -@end - -@implementation SPExportController (PrivateAPI) +#pragma mark - +#pragma mark Private API /** - * + * Enables or disables the export button based on the state of various interface controls. */ -- (void)_initializeExportUsingSelectedOptions +- (void)_toggleExportButton { - // First determine what type of export the user selected - SPExportType exportType = 0; + NSString *label = [[currentToolbarItem label] lowercaseString]; - 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 table(s)) for the export operation - SPExportSource exportSource = ([exportInputMatrix selectedRow] + 1); - - NSMutableArray *exportTables = [NSMutableArray array]; - - // Get the data depending on the source - switch (exportSource) - { - case SP_FILTERED_EXPORT: - - break; - case SP_CUSTOM_QUERY_EXPORT: - - break; - case SP_TABLE_EXPORT: - // Create an array of tables to export - for (NSMutableArray *table in tables) - { - if ([[table objectAtIndex:0] boolValue]) { - [exportTables addObject:[table objectAtIndex:1]]; - } + BOOL isSQL = [label isEqualToString:@"sql"]; + BOOL isCSV = [label isEqualToString:@"csv"]; + BOOL isXML = [label isEqualToString:@"xml"]; + BOOL isHTML = [label isEqualToString:@"html"]; + BOOL isPDF = [label isEqualToString:@"pdf"]; + + if (isCSV || isXML || isHTML || isPDF) { + [exportButton setEnabled:NO]; + + // Only enable the button if at least one table is selected + for (NSArray *table in tables) + { + if ([NSArrayObjectAtIndex(table, 2) boolValue]) { + [exportButton setEnabled:YES]; + break; } - - break; + } } - - // Begin the export based on the type - switch (exportSource) - { - case SP_FILTERED_EXPORT: - - break; - case SP_CUSTOM_QUERY_EXPORT: - - break; - case SP_TABLE_EXPORT: - [self _exportTables:exportTables asType:exportType toMultipleFiles:[exportFilePerTableCheck state]]; - break; + else if (isSQL) { + BOOL structureEnabled = [exportSQLIncludeStructureCheck state]; + BOOL contentEnabled = [exportSQLIncludeContentCheck state]; + BOOL dropEnabled = [exportSQLIncludeDropSyntaxCheck state]; + + // Disable if all are unchecked + if ((!contentEnabled) && (!structureEnabled) && (!dropEnabled)) { + [exportButton setEnabled:NO]; + } + // Disable if structure is unchecked, but content and drop are as dropping a table then trying to insert + // into it is obviously an error + else if (contentEnabled && (!structureEnabled) && (dropEnabled)) { + [exportButton setEnabled:NO]; + } + else { + [exportButton setEnabled:(contentEnabled || (structureEnabled || dropEnabled))]; + } } } /** - * Exports the contents' of the supplied array of tables. Note that this method currently only supports - * exporting in CSV and XML formats. + * Resizes the export window's height by the supplied delta, while retaining the position of + * all interface controls. */ -- (BOOL)_exportTables:(NSArray *)exportTables asType:(SPExportType)type toMultipleFiles:(BOOL)multipleFiles +- (void)_resizeWindowByHeightDelta:(NSInteger)delta { - NSUInteger i; + NSUInteger scrollMask = [exportTablelistScrollView autoresizingMask]; + NSUInteger buttonBarMask = [exportTableListButtonBar autoresizingMask]; + NSUInteger tabBarMask = [exportTabBar autoresizingMask]; + NSUInteger buttonMask = [exportAdvancedOptionsViewButton autoresizingMask]; + NSUInteger textFieldMask = [exportAdvancedOptionsViewLabelButton autoresizingMask]; + NSUInteger advancedViewMask = [exportAdvancedOptionsView autoresizingMask]; - NSMutableString *errors = [NSMutableString string]; + NSRect frame = [[self window] frame]; - NSDictionary *tableDetails = nil; - //NSStringEncoding encoding = [[self connection] encoding]; + [exportTablelistScrollView setAutoresizingMask:NSViewNotSizable | NSViewMinYMargin]; + [exportTableListButtonBar setAutoresizingMask:NSViewNotSizable | NSViewMinYMargin]; + [exportTabBar setAutoresizingMask:NSViewNotSizable | NSViewMinYMargin]; + [exportAdvancedOptionsViewButton setAutoresizingMask:NSViewNotSizable | NSViewMinYMargin]; + [exportAdvancedOptionsViewLabelButton setAutoresizingMask:NSViewNotSizable | NSViewMinYMargin]; + [exportAdvancedOptionsView setAutoresizingMask:NSViewNotSizable | NSViewMinYMargin]; - // Reset the interface - [exportProgressTitle setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Exporting %@", @"text showing that the application is importing a supplied format"), @"CSV"]]; - [exportProgressText setStringValue:NSLocalizedString(@"Writing...", @"text showing that app is writing text file")]; - [exportProgressText displayIfNeeded]; - [exportProgressIndicator setDoubleValue:0]; - [exportProgressIndicator displayIfNeeded]; + NSInteger newMinHeight = (windowMinHeigth - heightOffset + delta < windowMinHeigth) ? windowMinHeigth : windowMinHeigth - heightOffset + delta; - // Open the progress sheet - [NSApp beginSheet:exportProgressWindow - modalForWindow:[tableDocumentInstance parentWindow] - modalDelegate:self - didEndSelector:nil - contextInfo:nil]; - - // Add a dump header to the dump file - NSMutableString *csvLineEnd = [NSMutableString stringWithString:[exportCSVLinesTerminatedField stringValue]]; - - [csvLineEnd replaceOccurrencesOfString:@"\\t" withString:@"\t" - options:NSLiteralSearch - range:NSMakeRange(0, [csvLineEnd length])]; - - [csvLineEnd replaceOccurrencesOfString:@"\\n" withString:@"\n" - options:NSLiteralSearch - range:NSMakeRange(0, [csvLineEnd length])]; + [[self window] setMinSize:NSMakeSize(windowMinWidth, newMinHeight)]; - [csvLineEnd replaceOccurrencesOfString:@"\\r" withString:@"\r" - options:NSLiteralSearch - range:NSMakeRange(0, [csvLineEnd length])]; + frame.origin.y += heightOffset; + frame.size.height -= heightOffset; - NSUInteger tableCount = [exportTables count]; + heightOffset = delta; - // If - if ((type == SP_CSV_EXPORT) && (!multipleFiles) && (tableCount > 1)) { - - } + frame.origin.y -= heightOffset; + frame.size.height += heightOffset; - /*if ([exportTables count] > 1) { - [infoString setString:[NSString stringWithFormat:@"Host: %@ Database: %@ Generation Time: %@%@%@", - [tableDocumentInstance host], [tableDocumentInstance database], [NSDate date], csvLineEnd, csvLineEnd]]; - }*/ - - // Loop through the tables - for (i = 0 ; i < tableCount; i++) - { - if ([self exportCancelled]) break; - - // Update the progress text and reset the progress bar to indeterminate status - NSString *tableName = [exportTables objectAtIndex:i]; - - [exportProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %lu of %l (%@): fetching data...", @"text showing that app is fetching data for table dump"), (unsigned long)(i + 1), (unsigned long)tableCount, tableName]]; - [exportProgressText displayIfNeeded]; - - [exportProgressIndicator setIndeterminate:YES]; - [exportProgressIndicator setUsesThreadedAnimation:YES]; - [exportProgressIndicator startAnimation:self]; - - // For CSV exports of more than one table, output the name of the table - /*if (tableCount > 1) { - [fileHandle writeData:[[NSString stringWithFormat:@"Table %@%@%@", tableName, csvLineEnd, csvLineEnd] dataUsingEncoding:encoding]]; - }*/ - - // Determine whether this table is a table or a view via the create table command, and get the table details - MCPResult *queryResult = [connection queryString:[NSString stringWithFormat:@"SHOW CREATE TABLE %@", [tableName backtickQuotedString]]]; - [queryResult setReturnDataAsStrings:YES]; - - if ([queryResult numOfRows]) { - tableDetails = [NSDictionary dictionaryWithDictionary:[queryResult fetchRowAsDictionary]]; - - tableDetails = [NSDictionary dictionaryWithDictionary:([tableDetails objectForKey:@"Create View"]) ? [tableDataInstance informationForView:tableName] : [tableDataInstance informationForTable:tableName]]; - } - - // Retrieve the table details via the data class, and use it to build an array containing column numeric status - NSMutableArray *tableColumnNumericStatus = [NSMutableArray array]; - - for (NSDictionary *column in [tableDetails objectForKey:@"columns"]) - { - NSString *tableColumnTypeGrouping = [column objectForKey:@"typegrouping"]; - - [tableColumnNumericStatus addObject:[NSNumber numberWithBool:([tableColumnTypeGrouping isEqualToString:@"bit"] || - [tableColumnTypeGrouping isEqualToString:@"integer"] || - [tableColumnTypeGrouping isEqualToString:@"float"])]]; - } - - // Use low memory export? - BOOL useLowMemoryBlockingStreaming = ([exportProcessLowMemory state] == NSOnState); - - // Make a streaming request for the data - MCPStreamingResult *queryResultStreaming = [connection streamingQueryString:[NSString stringWithFormat:@"SELECT * FROM %@", [tableName backtickQuotedString]] useLowMemoryBlockingStreaming:useLowMemoryBlockingStreaming]; - - // Note any errors during retrieval - if ([connection queryErrored]) { - [errors appendString:[NSString stringWithFormat:@"%@\n", [connection getLastErrorMessage]]]; - } - - SPExporter *exporter = nil; - SPCSVExporter *csvExporter = nil; - - // Based on the type of export create a new instance of the corresponding exporter and set it's specific options - switch (type) - { - case SP_SQL_EXPORT: - - break; - case SP_CSV_EXPORT: - csvExporter = [[SPCSVExporter alloc] initWithDelegate:self]; - - [csvExporter setCsvOutputFieldNames:[exportCSVIncludeFieldNamesCheck state]]; - [csvExporter setCsvFieldSeparatorString:[exportCSVFieldsTerminatedField stringValue]]; - [csvExporter setCsvEnclosingCharacterString:[exportCSVFieldsWrappedField stringValue]]; - [csvExporter setCsvLineEndingString:[exportCSVLinesTerminatedField stringValue]]; - [csvExporter setCsvEscapeString:[exportCSVFieldsEscapedField stringValue]]; - - [csvExporter setExportOutputEncoding:[MCPConnection encodingForMySQLEncoding:[[tableDocumentInstance connectionEncoding] UTF8String]]]; - [csvExporter setCsvNULLString:[[NSUserDefaults standardUserDefaults] objectForKey:SPNullValue]]; - - [csvExporter setCsvTableColumnNumericStatus:tableColumnNumericStatus]; - - // Assign the data to the exporter - [csvExporter setCsvDataResult:queryResultStreaming]; - - exporter = csvExporter; - - break; - case SP_XML_EXPORT: - - break; - case SP_PDF_EXPORT: - - break; - case SP_HTML_EXPORT: - - break; - case SP_EXCEL_EXPORT: - - break; - } - - // Update the progress text and set the progress bar back to determinate - [exportProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %lu of %lu (%@): Writing...", @"text showing that app is writing data for table export"), (unsigned long)(i + 1), (unsigned long)tableCount, tableName]]; - [exportProgressText displayIfNeeded]; - - [exportProgressIndicator stopAnimation:self]; - [exportProgressIndicator setUsesThreadedAnimation:NO]; - [exportProgressIndicator setIndeterminate:NO]; - [exportProgressIndicator setDoubleValue:0]; - [exportProgressIndicator displayIfNeeded]; - - // Start the actual data conversion process by placing the exporter on the operation queue. - // Note that although it is highly likely there is no guarantee that the operation will executed - // as soon as it's placed on the queue. There may be a delay if the queue is already executing it's - // maximum number of concurrent operations. See the docs for more details. - [operationQueue addOperation:exporter]; - - if (csvExporter) [csvExporter release]; - - // Add a spacer to the file - //[fileHandle writeData:[[NSString stringWithFormat:@"%@%@%@", csvLineEnd, csvLineEnd, csvLineEnd] dataUsingEncoding:encoding]]; - } + [[self window] setFrame:frame display:YES animate:YES]; - return YES; + [exportTablelistScrollView setAutoresizingMask:scrollMask]; + [exportTableListButtonBar setAutoresizingMask:buttonBarMask]; + [exportTabBar setAutoresizingMask:tabBarMask]; + [exportAdvancedOptionsViewButton setAutoresizingMask:buttonMask]; + [exportAdvancedOptionsViewLabelButton setAutoresizingMask:textFieldMask]; + [exportAdvancedOptionsView setAutoresizingMask:advancedViewMask]; } @end diff --git a/Source/SPExportInitializer.h b/Source/SPExportInitializer.h new file mode 100644 index 00000000..4d86f619 --- /dev/null +++ b/Source/SPExportInitializer.h @@ -0,0 +1,51 @@ +// +// $Id$ +// +// SPExportInitializer.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on March 31, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import <Cocoa/Cocoa.h> + +#import "SPExportController.h" + +@class SPCSVExporter, SPSQLExporter, SPXMLExporter, SPFileHandle; + +/** + * @category SPExportInitializer SPExportInitializer.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * Export initializer category. + */ +@interface SPExportController (SPExportInitializer) + +- (void)initializeExportUsingSelectedOptions; + +- (void)exportTables:(NSArray *)exportTables orDataArray:(NSArray *)dataArray; + +- (SPCSVExporter *)initializeCSVExporterForTable:(NSString *)table orDataArray:(NSArray *)dataArray; +- (SPXMLExporter *)initializeXMLExporterForTable:(NSString *)table orDataArray:(NSArray *)dataArray; + +- (void)writeXMLHeaderToFileHandle:(SPFileHandle *)fileHandle; +- (SPFileHandle *)getFileHandleForFilePath:(NSString *)filePath; + +@end diff --git a/Source/SPExportInitializer.m b/Source/SPExportInitializer.m new file mode 100644 index 00000000..0ceccbd6 --- /dev/null +++ b/Source/SPExportInitializer.m @@ -0,0 +1,626 @@ +// +// $Id$ +// +// SPExporterInitializer.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on March 31, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import <MCPKit/MCPKit.h> + +#import "SPExportInitializer.h" +#import "SPStringAdditions.h" +#import "SPTableData.h" +#import "TableDocument.h" +#import "TablesList.h" +#import "SPGrowlController.h" +#import "SPMainThreadTrampoline.h" +#import "TableDocument.h" +#import "CustomQuery.h" +#import "SPFileHandle.h" +#import "SPAlertSheets.h" + +#import "SPCSVExporter.h" +#import "SPSQLExporter.h" +#import "SPXMLExporter.h" +#import "SPDotExporter.h" + +@implementation SPExportController (SPExportInitializer) + +/** + * Initializes the export process by analysing the selected criteria. + */ +- (void)initializeExportUsingSelectedOptions +{ + NSArray *dataArray = nil; + + // Get rid of the cached connection encoding + if (sqlPreviousConnectionEncoding) [sqlPreviousConnectionEncoding release], sqlPreviousConnectionEncoding = nil; + + createCustomFilename = ([exportCustomFilenameButton state] && (![[exportCustomFilenameTokenField stringValue] isEqualToString:@""])); + + // First determine what type of export the user selected + 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 table(s)) for the export operation + exportSource = (exportType == SPDotExport) ? SPTableExport : ([exportInputMatrix selectedRow] + 1); + + NSMutableArray *exportTables = [NSMutableArray array]; + + // Set whether or not we are to export to multiple files + [self setExportToMultipleFiles:[exportFilePerTableCheck state]]; + + // Get the data depending on the source + switch (exportSource) + { + case SPFilteredExport: + dataArray = [tableContentInstance currentResult]; + break; + case SPQueryExport: + dataArray = [customQueryInstance currentResult]; + break; + case SPTableExport: + // Create an array of tables to export + for (NSMutableArray *table in tables) + { + if (exportType == SPSQLExport) { + if ([[table objectAtIndex:1] boolValue] || [[table objectAtIndex:2] boolValue] || [[table objectAtIndex:3] boolValue]) { + [exportTables addObject:table]; + } + } + else if (exportType == SPDotExport) { + [exportTables addObject:[table objectAtIndex:0]]; + } + else { + if ([[table objectAtIndex:2] boolValue]) { + [exportTables addObject:[table objectAtIndex:0]]; + } + } + } + + break; + } + + // Set the export type label + switch (exportType) + { + case SPSQLExport: + exportTypeLabel = @"SQL"; + break; + case SPCSVExport: + exportTypeLabel = @"CSV"; + break; + case SPXMLExport: + exportTypeLabel = @"XML"; + break; + case SPDotExport: + exportTypeLabel = @"Dot"; + break; + } + + // Begin the export based on the source + switch (exportSource) + { + case SPFilteredExport: + case SPQueryExport: + [self exportTables:nil orDataArray:dataArray]; + break; + case SPTableExport: + [self exportTables:exportTables orDataArray:nil]; + break; + } +} + +/** + * Exports the contents of the supplied array of tables or data array. + */ +- (void)exportTables:(NSArray *)exportTables orDataArray:(NSArray *)dataArray +{ + NSUInteger i; + SPFileHandle *singleFileHandle = nil; + BOOL singleFileHeaderHasBeenWritten = NO; + + // Change query logging mode + [tableDocumentInstance setQueryMode:SPImportExportQueryMode]; + + // Start the notification timer to allow notifications to be shown even if frontmost for long queries + [[SPGrowlController sharedGrowlController] setVisibilityForNotificationName:@"Export Finished"]; + + // Reset the interface + [[exportProgressTitle onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Exporting %@", @"text showing that the application is importing a supplied format"), exportTypeLabel]]; + [[exportProgressText onMainThread] setStringValue:NSLocalizedString(@"Writing...", @"text showing that app is writing text file")]; + + [[exportProgressText onMainThread] displayIfNeeded]; + [[exportProgressIndicator onMainThread] setDoubleValue:0]; + [[exportProgressIndicator onMainThread] displayIfNeeded]; + + // Open the progress sheet + [NSApp beginSheet:exportProgressWindow + modalForWindow:[tableDocumentInstance parentWindow] + modalDelegate:self + didEndSelector:nil + contextInfo:nil]; + + // CSV export + if (exportType == SPCSVExport) { + + SPCSVExporter *csvExporter = nil; + + // If the user has selected to only export to a single file or this is a filtered or custom query + // export, create the single file now and assign it to all subsequently created exporters. + if ((![self exportToMultipleFiles]) || (exportSource == SPFilteredExport) || (exportSource == SPQueryExport)) { + + NSString *filename = @""; + + // Create custom filename if required + if (createCustomFilename) { + filename = [self expandCustomFilenameFormatFromString:[exportCustomFilenameTokenField stringValue] usingTableName:nil]; + } + else { + // Determine what the file name should be + switch (exportSource) + { + case SPFilteredExport: + filename = [NSString stringWithFormat:@"%@_view", [tableDocumentInstance table]]; + break; + case SPQueryExport: + filename = @"query_result"; + break; + case SPTableExport: + filename = [tableDocumentInstance database]; + break; + } + } + + singleFileHandle = [self getFileHandleForFilePath:[[exportPathField stringValue] stringByAppendingPathComponent:filename]]; + } + + // Start the export process depending on the data source + if (exportSource == SPTableExport) { + + // Cache the number of tables being exported + exportTableCount = [exportTables count]; + + // Loop through the tables, creating an exporter for each + for (NSString *table in exportTables) + { + csvExporter = [self initializeCSVExporterForTable:table orDataArray:nil]; + + // If required create a single file handle for all CSV exports + if (![self exportToMultipleFiles]) { + [csvExporter setExportOutputFileHandle:singleFileHandle]; + + if (!singleFileHeaderHasBeenWritten) { + + NSMutableString *lineEnding = [NSMutableString stringWithString:[exportCSVLinesTerminatedField stringValue]]; + + // Escape tabs, line endings and carriage returns + [lineEnding replaceOccurrencesOfString:@"\\t" withString:@"\t" + options:NSLiteralSearch + range:NSMakeRange(0, [lineEnding length])]; + + + [lineEnding replaceOccurrencesOfString:@"\\n" withString:@"\n" + options:NSLiteralSearch + range:NSMakeRange(0, [lineEnding length])]; + + [lineEnding replaceOccurrencesOfString:@"\\r" withString:@"\r" + options:NSLiteralSearch + range:NSMakeRange(0, [lineEnding length])]; + + // Write the file header and the first table name + [singleFileHandle writeData:[[NSMutableString stringWithFormat:@"%@: %@ %@: %@ %@: %@%@%@%@ %@%@%@", + NSLocalizedString(@"Host", @"csv export host heading"), + [tableDocumentInstance host], + NSLocalizedString(@"Database", @"csv export database heading"), + [tableDocumentInstance database], + NSLocalizedString(@"Generation Time", @"csv export generation time heading"), + [NSDate date], + lineEnding, + lineEnding, + NSLocalizedString(@"Table", @"csv export table heading"), + table, + lineEnding, + lineEnding] dataUsingEncoding:[connection encoding]]]; + + singleFileHeaderHasBeenWritten = YES; + } + } + + [exporters addObject:csvExporter]; + } + } + else { + csvExporter = [self initializeCSVExporterForTable:nil orDataArray:dataArray]; + + [csvExporter setExportOutputFileHandle:singleFileHandle]; + + [exporters addObject:csvExporter]; + } + } + // SQL export + else if (exportType == SPSQLExport) { + + // Cache the number of tables being exported + exportTableCount = [exportTables count]; + + SPSQLExporter *sqlExporter = [[SPSQLExporter alloc] initWithDelegate:self]; + + [sqlExporter setSqlDatabaseHost:[tableDocumentInstance host]]; + [sqlExporter setSqlDatabaseName:[tableDocumentInstance database]]; + [sqlExporter setSqlDatabaseVersion:[tableDocumentInstance mySQLVersion]]; + + [sqlExporter setSqlOutputIncludeUTF8BOM:[exportUseUTF8BOMButton state]]; + [sqlExporter setSqlOutputEncodeBLOBasHex:[exportSQLBLOBFieldsAsHexCheck state]]; + [sqlExporter setSqlOutputCompressFile:[exportCompressOutputFile state]]; + [sqlExporter setSqlOutputIncludeErrors:[exportSQLIncludeErrorsCheck state]]; + + // Set generic properties + [sqlExporter setConnection:connection]; + [sqlExporter setExportOutputEncoding:[connection encoding]]; + [sqlExporter setExportUsingLowMemoryBlockingStreaming:[exportProcessLowMemoryButton state]]; + + // Cache the current connection encoding then change it to UTF-8 to allow SQL dumps to work + sqlPreviousConnectionEncoding = [[NSString alloc] initWithString:[tableDocumentInstance connectionEncoding]]; + sqlPreviousConnectionEncodingViaLatin1 = [tableDocumentInstance connectionEncodingViaLatin1:nil]; + + [tableDocumentInstance setConnectionEncoding:@"utf8" reloadingViews:NO]; + + NSMutableArray *tableTypes = [[NSMutableArray alloc] init]; + NSMutableDictionary *infoDict = [[NSMutableDictionary alloc] init]; + + // Build the table information dictionary as well as the table array with item type + for (NSArray *table in exportTables) + { + [infoDict setObject:[tableDataInstance informationForTable:[table objectAtIndex:0]] forKey:[table objectAtIndex:0]]; + } + + [sqlExporter setSqlTableInformation:infoDict]; + [sqlExporter setSqlExportTables:exportTables]; + + // Set the exporter's max progress + [sqlExporter setExportMaxProgress:((NSInteger)[exportProgressIndicator bounds].size.width)]; + + // Set the progress bar's max value + [exportProgressIndicator setMaxValue:[sqlExporter exportMaxProgress]]; + + [infoDict release]; + [tableTypes release]; + + NSString *filename = @""; + + // Create custom filename if required + filename = (createCustomFilename) ? [self expandCustomFilenameFormatFromString:[exportCustomFilenameTokenField stringValue] usingTableName:nil] : [NSString stringWithFormat:@"%@_%@", [tableDocumentInstance database], [[NSDate date] descriptionWithCalendarFormat:@"%Y-%m-%d" timeZone:nil locale:nil]]; + + SPFileHandle *fileHandle = [self getFileHandleForFilePath:[[exportPathField stringValue] stringByAppendingPathComponent:[filename stringByAppendingPathExtension:([exportCompressOutputFile state]) ? @"gz" : @"sql"]]]; + + [sqlExporter setExportOutputFileHandle:fileHandle]; + + [exporters addObject:sqlExporter]; + + [sqlExporter release]; + } + // XML export + else if (exportType == SPXMLExport) { + + SPXMLExporter *xmlExporter = nil; + + // If the user has selected to only export to a single file or this is a filtered or custom query + // export, create the single file now and assign it to all subsequently created exporters. + if ((![self exportToMultipleFiles]) || (exportSource == SPFilteredExport) || (exportSource == SPQueryExport)) { + + NSString *filename = @""; + + // Create custom filename if required + if (createCustomFilename) { + filename = [self expandCustomFilenameFormatFromString:[exportCustomFilenameTokenField stringValue] usingTableName:nil]; + } + else { + // Determine what the file name should be + switch (exportSource) + { + case SPFilteredExport: + filename = [NSString stringWithFormat:@"%@_view", [tableDocumentInstance table]]; + break; + case SPQueryExport: + filename = @"query_result"; + break; + case SPTableExport: + filename = [tableDocumentInstance database]; + break; + } + } + + singleFileHandle = [self getFileHandleForFilePath:[[exportPathField stringValue] stringByAppendingPathComponent:[filename stringByAppendingPathExtension:@"xml"]]]; + + // Write the file header + [self writeXMLHeaderToFileHandle:singleFileHandle]; + } + + // Start the export process depending on the data source + if (exportSource == SPTableExport) { + + // Cache the number of tables being exported + exportTableCount = [exportTables count]; + + // Loop through the tables, creating an exporter for each + for (NSString *table in exportTables) + { + xmlExporter = [self initializeXMLExporterForTable:table orDataArray:nil]; + + // If required create a single file handle for all XML exports + if (![self exportToMultipleFiles]) { + [xmlExporter setExportOutputFileHandle:singleFileHandle]; + + if (!singleFileHeaderHasBeenWritten) { + + // Write the file header + [self writeXMLHeaderToFileHandle:singleFileHandle]; + + singleFileHeaderHasBeenWritten = YES; + } + } + + [exporters addObject:xmlExporter]; + } + } + else { + xmlExporter = [self initializeXMLExporterForTable:nil orDataArray:dataArray]; + + [xmlExporter setExportOutputFileHandle:singleFileHandle]; + + [exporters addObject:xmlExporter]; + } + } + // Dot export + else if (exportType == SPDotExport) { + + // Cache the number of tables being exported + exportTableCount = [exportTables count]; + + SPDotExporter *dotExporter = [[SPDotExporter alloc] initWithDelegate:self]; + + [dotExporter setDotTableData:tableDataInstance]; + + [dotExporter setDotDatabaseHost:[tableDocumentInstance host]]; + [dotExporter setDotDatabaseName:[tableDocumentInstance database]]; + [dotExporter setDotDatabaseVersion:[tableDocumentInstance mySQLVersion]]; + + // Set generic properties + [dotExporter setConnection:connection]; + [dotExporter setExportOutputEncoding:[connection encoding]]; + [dotExporter setExportUsingLowMemoryBlockingStreaming:[exportProcessLowMemoryButton state]]; + + // Cache the current connection encoding then change it to UTF-8 to allow SQL dumps to work + sqlPreviousConnectionEncoding = [[NSString alloc] initWithString:[tableDocumentInstance connectionEncoding]]; + sqlPreviousConnectionEncodingViaLatin1 = [tableDocumentInstance connectionEncodingViaLatin1:nil]; + + [tableDocumentInstance setConnectionEncoding:@"utf8" reloadingViews:NO]; + + [dotExporter setDotExportTables:exportTables]; + + // Set the exporter's max progress + [dotExporter setExportMaxProgress:(NSInteger)[exportProgressIndicator bounds].size.width]; + + // Set the progress bar's max value + [exportProgressIndicator setMaxValue:[dotExporter exportMaxProgress]]; + + NSString *filename = @""; + + // Create custom filename if required + if (createCustomFilename) { + filename = [self expandCustomFilenameFormatFromString:[exportCustomFilenameTokenField stringValue] usingTableName:nil]; + } + else { + filename = [tableDocumentInstance database]; + } + + SPFileHandle *fileHandle = [self getFileHandleForFilePath:[[exportPathField stringValue] stringByAppendingPathComponent:[filename stringByAppendingPathExtension:@"dot"]]]; + + [dotExporter setExportOutputFileHandle:fileHandle]; + + [exporters addObject:dotExporter]; + + [dotExporter release]; + } + + // Add the first exporter to the operation queue + [operationQueue addOperation:[exporters objectAtIndex:0]]; + + // Remove the exporter we just added to the operation queue from our list of exporters + // so we know it's already been done. + [exporters removeObjectAtIndex:0]; +} + +/** + * Initialises a CSV exporter for the supplied table name or data array. + */ +- (SPCSVExporter *)initializeCSVExporterForTable:(NSString *)table orDataArray:(NSArray *)dataArray +{ + NSString *filename = @""; + SPFileHandle *fileHandle = nil; + + SPCSVExporter *csvExporter = [[SPCSVExporter alloc] initWithDelegate:self]; + + // Depeding on the export source, set the table name or data array + if (exportSource == SPTableExport) { + [csvExporter setCsvTableName:table]; + } + else { + [csvExporter setCsvDataArray:dataArray]; + } + + [csvExporter setCsvTableData:tableDataInstance]; + + [csvExporter setCsvOutputFieldNames:[exportCSVIncludeFieldNamesCheck state]]; + [csvExporter setCsvFieldSeparatorString:[exportCSVFieldsTerminatedField stringValue]]; + [csvExporter setCsvEnclosingCharacterString:[exportCSVFieldsWrappedField stringValue]]; + [csvExporter setCsvLineEndingString:[exportCSVLinesTerminatedField stringValue]]; + [csvExporter setCsvEscapeString:[exportCSVFieldsEscapedField stringValue]]; + [csvExporter setCsvNULLString:[exportCSVNULLValuesAsTextField stringValue]]; + + // If required create separate files + if ([self exportToMultipleFiles]) { + + if (createCustomFilename) { + + // Create custom filename based on the selected format + filename = [self expandCustomFilenameFormatFromString:[exportCustomFilenameTokenField stringValue] usingTableName:table]; + + // If the user chose to use a custom filename format and we exporting to multiple files, make + // sure the table name is included to ensure the output files are unique. + filename = ([[exportCustomFilenameTokenField stringValue] rangeOfString:@"table" options:NSLiteralSearch].location == NSNotFound) ? [filename stringByAppendingFormat:@"_%@", table] : filename; + } + else { + filename = table; + } + + fileHandle = [self getFileHandleForFilePath:[[exportPathField stringValue] stringByAppendingPathComponent:filename]]; + + [csvExporter setExportOutputFileHandle:fileHandle]; + } + + // Set generic properties + [csvExporter setConnection:connection]; + [csvExporter setExportOutputEncoding:[connection encoding]]; + [csvExporter setExportMaxProgress:((NSInteger)[exportProgressIndicator bounds].size.width)]; + [csvExporter setExportUsingLowMemoryBlockingStreaming:[exportProcessLowMemoryButton state]]; + + // Set the progress bar's max value + [exportProgressIndicator setMaxValue:[csvExporter exportMaxProgress]]; + + return [csvExporter autorelease]; +} + +/** + * Initialises a XML exporter for the supplied table name or data array. + */ +- (SPXMLExporter *)initializeXMLExporterForTable:(NSString *)table orDataArray:(NSArray *)dataArray +{ + NSString *filename = @""; + SPFileHandle *fileHandle = nil; + + SPXMLExporter *xmlExporter = [[SPXMLExporter alloc] initWithDelegate:self]; + + // Depeding on the export source, set the table name or data array + if (exportSource == SPTableExport) { + [xmlExporter setXmlTableName:table]; + } + else { + [xmlExporter setXmlDataArray:dataArray]; + } + + // Regardless of the export source, set exporter's table name as it's used in the output + // of table and table content exports. + [xmlExporter setXmlTableName:[tablesListInstance tableName]]; + + // If required create separate files + if ((exportSource == SPTableExport) && exportToMultipleFiles && (exportTableCount > 0)) { + filename = [[exportPathField stringValue] stringByAppendingPathComponent:table]; + + fileHandle = [self getFileHandleForFilePath:filename]; + + // Write the file header + [self writeXMLHeaderToFileHandle:fileHandle]; + + [xmlExporter setExportOutputFileHandle:fileHandle]; + } + + // Set generic properties + [xmlExporter setConnection:connection]; + [xmlExporter setExportOutputEncoding:[connection encoding]]; + [xmlExporter setExportMaxProgress:((NSInteger)[exportProgressIndicator bounds].size.width)]; + [xmlExporter setExportUsingLowMemoryBlockingStreaming:[exportProcessLowMemoryButton state]]; + + // Set the progress bar's max value + [exportProgressIndicator setMaxValue:[xmlExporter exportMaxProgress]]; + + return [xmlExporter autorelease]; +} + +/** + * Writes the XML file header to the supplied file handle. + */ +- (void)writeXMLHeaderToFileHandle:(SPFileHandle *)fileHandle +{ + NSMutableString *header = [NSMutableString string]; + + [header setString:@"<?xml version=\"1.0\"?>\n\n"]; + [header appendString:@"<!--\n-\n"]; + [header appendString:@"- Sequel Pro XML dump\n"]; + [header appendString:[NSString stringWithFormat:@"- Version %@\n-\n", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]]]; + [header appendString:[NSString stringWithFormat:@"- %@\n- %@\n-\n", SPHomePageURL, SPDevURL]]; + [header appendString:[NSString stringWithFormat:@"- Host: %@ (MySQL %@)\n", [tableDocumentInstance host], [tableDocumentInstance mySQLVersion]]]; + [header appendString:[NSString stringWithFormat:@"- Database: %@\n", [tableDocumentInstance database]]]; + [header appendString:[NSString stringWithFormat:@"- Generation Time: %@\n", [NSDate date]]]; + [header appendString:@"-\n-->\n\n"]; + + if (exportSource == SPTableExport) { + [header appendString:[NSString stringWithFormat:@"<%@>\n\n", [[tableDocumentInstance database] HTMLEscapeString]]]; + } + + [fileHandle writeData:[header dataUsingEncoding:NSUTF8StringEncoding]]; +} + +/** + * Returns a file handle for writing at the supplied path. + */ +- (SPFileHandle *)getFileHandleForFilePath:(NSString *)filePath +{ + SPFileHandle *fileHandle = nil; + NSFileManager *fileManager = [NSFileManager defaultManager]; + + if ([fileManager fileExistsAtPath:filePath]) { + if ((![fileManager isWritableFileAtPath:filePath]) || (!(fileHandle = [SPFileHandle fileHandleForWritingAtPath:filePath]))) { + SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocumentInstance parentWindow], self, nil, nil, + NSLocalizedString(@"Couldn't replace the file. Be sure that you have the necessary privileges.", @"message of panel when file cannot be replaced")); + return nil; + } + } + // Otherwise attempt to create a file + else { + if (![fileManager createFileAtPath:filePath contents:[NSData data] attributes:nil]) { + SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocumentInstance parentWindow], self, nil, nil, + NSLocalizedString(@"Couldn't write to file. Be sure that you have the necessary privileges.", @"message of panel when file cannot be written")); + return nil; + } + + // Retrieve a filehandle for the file, attempting to delete it on failure. + fileHandle = [SPFileHandle fileHandleForWritingAtPath:filePath]; + + if (!fileHandle) { + [[NSFileManager defaultManager] removeFileAtPath:filePath handler:nil]; + + SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [tableDocumentInstance parentWindow], self, nil, nil, + NSLocalizedString(@"Couldn't write to file. Be sure that you have the necessary privileges.", @"message of panel when file cannot be written")); + return nil; + } + } + + return fileHandle; +} + +@end diff --git a/Source/SPExportUtilities.h b/Source/SPExportUtilities.h new file mode 100644 index 00000000..2c7c1e09 --- /dev/null +++ b/Source/SPExportUtilities.h @@ -0,0 +1,30 @@ +// +// $Id$ +// +// SPExportUtilities.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 24, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +@interface SPExportUtilities : NSObject + +void SPExportDelegateConformsToProtocol(NSObject *delegate, Protocol *protocol); + +@end diff --git a/Source/SPExportUtilities.m b/Source/SPExportUtilities.m new file mode 100644 index 00000000..bf532bb2 --- /dev/null +++ b/Source/SPExportUtilities.m @@ -0,0 +1,40 @@ +// +// $Id$ +// +// SPExportUtilities.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 24, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPExportUtilities.h" + +@implementation SPExportUtilities + +void SPExportDelegateConformsToProtocol(NSObject *delegate, Protocol *protocol) +{ + // Check that the the supplied delegate conforms to the supplied protocol, if not throw an exception + if (![delegate conformsToProtocol:protocol]) { + @throw [NSException exceptionWithName:@"Protocol Conformance" + reason:[NSString stringWithFormat:@"The supplied delegate does not conform to the protocol '%@'.", NSStringFromProtocol(protocol)] + userInfo:nil]; + } +} + +@end diff --git a/Source/SPExporter.h b/Source/SPExporter.h index 1f52e5db..805173c9 100644 --- a/Source/SPExporter.h +++ b/Source/SPExporter.h @@ -25,9 +25,11 @@ #import <Cocoa/Cocoa.h> -#import "SPExporterDataAccess.h" - /** + * @class SPExporter SPExporter.m + * + * @author Stuart Connolly http://stuconnolly.com/ + * * This class is designed to be the base class of all data exporters and provide basic functionality * common to each of them. Each data exporter (i.e. CSV, SQL, XML, etc.) should be implemented as a subclass * of this class, with the end result being a modular export architecture separated by export type. All exporters @@ -47,28 +49,35 @@ * once the exporter instance is placed on the operation queue once its ready to be run. */ +@class MCPConnection, SPFileHandle; + @interface SPExporter : NSOperation -{ - id <SPExporterDataAccess> delegate; - SEL didEndSelector; +{ + MCPConnection *connection; double exportProgressValue; BOOL exportProcessIsRunning; + BOOL exportUsingLowMemoryBlockingStreaming; NSString *exportData; + SPFileHandle *exportOutputFileHandle; NSStringEncoding exportOutputEncoding; + + NSInteger exportMaxProgress; } -@property (readwrite, assign) id delegate; -@property (readwrite, assign) SEL didEndSelector; -@property (readwrite, assign) double exportProgressValue; +@property(readwrite, retain) MCPConnection *connection; + +@property(readwrite, assign) double exportProgressValue; -@property (readwrite, assign) BOOL exportProcessIsRunning; +@property(readwrite, assign) BOOL exportProcessIsRunning; +@property(readwrite, assign) BOOL exportUsingLowMemoryBlockingStreaming; -@property (readwrite, retain) NSString *exportData; -@property (readwrite, assign) NSStringEncoding exportOutputEncoding; +@property(readwrite, retain) NSString *exportData; +@property(readwrite, retain) SPFileHandle *exportOutputFileHandle; +@property(readwrite, assign) NSStringEncoding exportOutputEncoding; -- (id)initWithDelegate:(id)exportDelegate; +@property(readwrite, assign) NSInteger exportMaxProgress; @end diff --git a/Source/SPExporter.m b/Source/SPExporter.m index cf764b61..b7d9f242 100644 --- a/Source/SPExporter.m +++ b/Source/SPExporter.m @@ -27,21 +27,21 @@ @implementation SPExporter -@synthesize delegate; -@synthesize didEndSelector; +@synthesize connection; @synthesize exportProgressValue; @synthesize exportProcessIsRunning; +@synthesize exportUsingLowMemoryBlockingStreaming; @synthesize exportData; +@synthesize exportOutputFileHandle; @synthesize exportOutputEncoding; +@synthesize exportMaxProgress; /** - * Initialise an instance of SPCSVExporter using the supplied delegate and set some default values. + * Initialise an instance of SPExporter, while setting some default values. */ -- (id)initWithDelegate:(id)exportDelegate +- (id)init { - if ((self = [super init])) { - [self setDelegate:exportDelegate]; - + if ((self = [super init])) { [self setExportProgressValue:0]; [self setExportProcessIsRunning:NO]; @@ -50,8 +50,6 @@ // Default the output encoding to UTF-8 [self setExportOutputEncoding:NSUTF8StringEncoding]; - - [self setDidEndSelector:@selector(exporterDataConversionProcessComplete:)]; } return self; @@ -62,7 +60,9 @@ */ - (void)main { - @throw [NSException exceptionWithName:@"NSOperation main() call" reason:@"Can't call NSOperation's main() method in SPExpoter, must be overriden in subclass." userInfo:nil]; + @throw [NSException exceptionWithName:@"NSOperation main() Call" + reason:@"Can't call NSOperation's main() method in SPExpoter, must be overriden in subclass." + userInfo:nil]; } /** @@ -70,7 +70,9 @@ */ - (void)dealloc { - [exportData release], exportData = nil; + if (exportData) [exportData release], exportData = nil; + if (connection) [connection release], connection = nil; + if (exportOutputFileHandle) [exportOutputFileHandle release], exportOutputFileHandle = nil; [super dealloc]; } diff --git a/Source/SPHTMLExporter.h b/Source/SPHTMLExporter.h new file mode 100644 index 00000000..e58f8c80 --- /dev/null +++ b/Source/SPHTMLExporter.h @@ -0,0 +1,57 @@ +// +// $Id$ +// +// SPHTMLExporter.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 24, 2010 +// Copyright (c) 2009 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import <Cocoa/Cocoa.h> + +#import "SPExporter.h" +#import "SPHTMLExporterProtocol.h" + +/** + * @class SPHTMLExporter SPHTMLExporter.m + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * HTML exporter class. + */ +@interface SPHTMLExporter : SPExporter +{ + /** + * Exporter delegate + */ + NSObject <SPHTMLExporterProtocol> *delegate; +} + +@property(readwrite, assign) NSObject *delegate; + +/** + * Initialise an instance of SPHTMLExporter using the supplied delegate. + * + * @param exportDelegate The exporter delegate + * + * @return The initialised instance + */ +- (id)initWithDelegate:(NSObject *)exportDelegate; + +@end diff --git a/Source/SPHTMLExporter.m b/Source/SPHTMLExporter.m new file mode 100644 index 00000000..45f41c54 --- /dev/null +++ b/Source/SPHTMLExporter.m @@ -0,0 +1,62 @@ +// +// $Id$ +// +// SPHTMLExporter.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 24, 2010 +// Copyright (c) 2009 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPHTMLExporter.h" +#import "SPExportUtilities.h" + +@implementation SPHTMLExporter + +@synthesize delegate; + +/** + * Initialise an instance of SPHTMLExporter using the supplied delegate. + */ +- (id)initWithDelegate:(NSObject *)exportDelegate +{ + if ((self = [super init])) { + SPExportDelegateConformsToProtocol(exportDelegate, @protocol(SPHTMLExporterProtocol)); + + [self setDelegate:exportDelegate]; + } + + return self; +} + +/** + * Start the HTML export process. This method is automatically called when an instance of this class + * is placed on an NSOperationQueue. Do not call it directly as there is no manual multithreading. + */ +- (void)main +{ + @try { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + [pool release]; + + } + @catch (NSException *e) { } +} + +@end diff --git a/Source/SPHTMLExporterDelegate.h b/Source/SPHTMLExporterDelegate.h new file mode 100644 index 00000000..5cace8af --- /dev/null +++ b/Source/SPHTMLExporterDelegate.h @@ -0,0 +1,38 @@ +// +// $Id$ +// +// SPHTMLExporterDelegate.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 24, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPExportController.h" +#import "SPHTMLExporterProtocol.h" + +/** + * @category SPHTMLExporterDelegate SPHTMLExporterDelegate.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * HTML exporter delegate category. + */ +@interface SPExportController (SPHTMLExporterDelegate) <SPHTMLExporterProtocol> + +@end diff --git a/Source/SPHTMLExporterDelegate.m b/Source/SPHTMLExporterDelegate.m new file mode 100644 index 00000000..e4f02180 --- /dev/null +++ b/Source/SPHTMLExporterDelegate.m @@ -0,0 +1,54 @@ +// +// $Id$ +// +// SPHTMLExporterDelegate.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 24, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPHTMLExporterDelegate.h" + +@implementation SPExportController (SPHTMLExporterDelegate) + +/** + * + */ +- (void)htmlExportProcessWillBegin:(SPHTMLExporter *)exporter +{ + +} + +/** + * + */ +- (void)htmlExportProcessComplete:(SPHTMLExporter *)exporter +{ + +} + +/** + * + */ +- (void)htmlExportProcessWillBeginWritingData:(SPHTMLExporter *)exporter +{ + +} + +@end diff --git a/Source/SPHTMLExporterProtocol.h b/Source/SPHTMLExporterProtocol.h new file mode 100644 index 00000000..f5c7328e --- /dev/null +++ b/Source/SPHTMLExporterProtocol.h @@ -0,0 +1,60 @@ +// +// $Id$ +// +// SPHTMLExporterProtocol.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 24, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import <Cocoa/Cocoa.h> + +@class SPHTMLExporter; + +/** + * @protocol SPHTMLExporterProtocol SPHTMLExporterProtocol.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * HTML exporter delegate protocol. + */ +@protocol SPHTMLExporterProtocol + +/** + * Called when the HTML export process is about to begin. + * + * @param SPHTMLExporter The expoter calling the method. + */ +- (void)htmlExportProcessWillBegin:(SPHTMLExporter *)exporter; + +/** + * Called when the HTML export process is complete. + * + * @param SPHTMLExporter The expoter calling the method. + */ +- (void)htmlExportProcessComplete:(SPHTMLExporter *)exporter; + +/** + * Called when the HTML export process is about to begin writing data to disk. + * + * @param SPHTMLExporter The expoter calling the method. + */ +- (void)htmlExportProcessWillBeginWritingData:(SPHTMLExporter *)exporter; + +@end diff --git a/Source/SPPDFExporter.h b/Source/SPPDFExporter.h new file mode 100644 index 00000000..62283c43 --- /dev/null +++ b/Source/SPPDFExporter.h @@ -0,0 +1,57 @@ +// +// $Id$ +// +// SPPDFExporter.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 24, 2010 +// Copyright (c) 2009 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import <Cocoa/Cocoa.h> + +#import "SPExporter.h" +#import "SPPDFExporterProtocol.h" + +/** + * @class SPPDFExporter SPPDFExporter.m + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * PDF exporter class. + */ +@interface SPPDFExporter : SPExporter +{ + /** + * Exporter delegate + */ + NSObject <SPPDFExporterProtocol> *delegate; +} + +@property(readwrite, assign) NSObject *delegate; + +/** + * Initialise an instance of SPPDFExporter using the supplied delegate. + * + * @param exportDelegate The exporter delegate + * + * @return The initialised instance + */ +- (id)initWithDelegate:(NSObject *)exportDelegate; + +@end diff --git a/Source/SPPDFExporter.m b/Source/SPPDFExporter.m new file mode 100644 index 00000000..a850e2bf --- /dev/null +++ b/Source/SPPDFExporter.m @@ -0,0 +1,61 @@ +// +// $Id$ +// +// SPPDFExporter.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 24, 2010 +// Copyright (c) 2009 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPPDFExporter.h" +#import "SPExportUtilities.h" + +@implementation SPPDFExporter + +@synthesize delegate; + +/** + * Initialise an instance of SPPDFExporter using the supplied delegate. + */ +- (id)initWithDelegate:(NSObject *)exportDelegate +{ + if ((self = [super init])) { + SPExportDelegateConformsToProtocol(exportDelegate, @protocol(SPPDFExporterProtocol)); + + [self setDelegate:exportDelegate]; + } + + return self; +} + +/** + * Start the PDF export process. This method is automatically called when an instance of this class + * is placed on an NSOperationQueue. Do not call it directly as there is no manual multithreading. + */ +- (void)main +{ + @try { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + [pool release]; + } + @catch (NSException *e) { } +} + +@end diff --git a/Source/SPPDFExporterDelegate.h b/Source/SPPDFExporterDelegate.h new file mode 100644 index 00000000..ef17a741 --- /dev/null +++ b/Source/SPPDFExporterDelegate.h @@ -0,0 +1,38 @@ +// +// $Id$ +// +// SPPDFExporterDelegate.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 24, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPExportController.h" +#import "SPPDFExporterProtocol.h" + +/** + * @category SPPDFExporterDelegate SPPDFExporterDelegate.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * PDF exporter delegate category. + */ +@interface SPExportController (SPPDFExporterDelegate) <SPPDFExporterProtocol> + +@end diff --git a/Source/SPExporterDataAccess.h b/Source/SPPDFExporterDelegate.m index ab44cd39..e838adee 100644 --- a/Source/SPExporterDataAccess.h +++ b/Source/SPPDFExporterDelegate.m @@ -1,11 +1,11 @@ // // $Id$ // -// SPExporterDataAccess.h +// SPPDFExporterDelegate.m // sequel-pro // -// Created by Stuart Connolly (stuconnolly.com) on October 6, 2009 -// Copyright (c) 2009 Stuart Connolly. All rights reserved. +// Created by Stuart Connolly (stuconnolly.com) on April 24, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -23,16 +23,32 @@ // // More info at <http://code.google.com/p/sequel-pro/> -#import <Cocoa/Cocoa.h> +#import "SPPDFExporterDelegate.h" -@class SPExporter; +@implementation SPExportController (SPPDFExporterDelegate) -@protocol SPExporterDataAccess +/** + * + */ +- (void)pdfExportProcessWillBegin:(SPPDFExporter *)exporter +{ + +} + +/** + * + */ +- (void)pdfExportProcessComplete:(SPPDFExporter *)exporter +{ + +} /** - * This method called when an expoter complete it's data conversion process and the operation is effectively - * complete. The resulting data can be accessed via SPExporter's exportData method. + * */ -- (void)exporterDataConversionProcessComplete:(SPExporter *)exporter; +- (void)pdfExportProcessWillBeginWritingData:(SPPDFExporter *)exporter +{ + +} @end diff --git a/Source/SPPDFExporterProtocol.h b/Source/SPPDFExporterProtocol.h new file mode 100644 index 00000000..4381451e --- /dev/null +++ b/Source/SPPDFExporterProtocol.h @@ -0,0 +1,60 @@ +// +// $Id$ +// +// SPPDFExporterProtocol.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 24, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import <Cocoa/Cocoa.h> + +@class SPPDFExporter; + +/** + * @protocol SPPDFExporterProtocol SPPDFExporterProtocol.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * PDF exporter delegate protocol. + */ +@protocol SPPDFExporterProtocol + +/** + * Called when the PDF export process is about to begin. + * + * @param SPHTMLExporter The expoter calling the method. + */ +- (void)pdfExportProcessWillBegin:(SPPDFExporter *)exporter; + +/** + * Called when the PDF export process is complete. + * + * @param SPHTMLExporter The expoter calling the method. + */ +- (void)pdfExportProcessComplete:(SPPDFExporter *)exporter; + +/** + * Called when the PDF export process is about to begin writing data to disk. + * + * @param SPHTMLExporter The expoter calling the method. + */ +- (void)pdfExportProcessWillBeginWritingData:(SPPDFExporter *)exporter; + +@end diff --git a/Source/SPSQLExporter.h b/Source/SPSQLExporter.h index 7a2e9006..6b4702ce 100644 --- a/Source/SPSQLExporter.h +++ b/Source/SPSQLExporter.h @@ -26,7 +26,107 @@ #import <Cocoa/Cocoa.h> #import "SPExporter.h" +#import "SPSQLExporterProtocol.h" +/** + * @class SPSQLExporter SPSQLExporter.m + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * SQL exporter class. + */ @interface SPSQLExporter : SPExporter +{ + NSObject <SPSQLExporterProtocol> *delegate; + + /** + * Tables + */ + NSArray *sqlExportTables; + + /** + * Database host + */ + NSString *sqlDatabaseHost; + + /** + * Database name + */ + NSString *sqlDatabaseName; + + /** + * Database version + */ + NSString *sqlDatabaseVersion; + + /** + * Current table + */ + NSString *sqlExportCurrentTable; + + /** + * Export errors + */ + NSString *sqlExportErrors; + + /** + * Include UTF-8 BOM + */ + BOOL sqlOutputIncludeUTF8BOM; + + /** + * Encode BLOB fields as Hex data + */ + BOOL sqlOutputEncodeBLOBasHex; + + /** + * Include export errors + */ + BOOL sqlOutputIncludeErrors; + + /** + * Compress output + */ + BOOL sqlOutputCompressFile; + + /** + * Table information + */ + NSDictionary *sqlTableInformation; +} + +@property(readwrite, assign) NSObject *delegate; + +@property(readwrite, retain) NSArray *sqlExportTables; + +@property(readwrite, retain) NSString *sqlDatabaseHost; +@property(readwrite, retain) NSString *sqlDatabaseName; +@property(readwrite, retain) NSString *sqlDatabaseVersion; + +@property(readwrite, retain) NSString *sqlExportCurrentTable; +@property(readwrite, retain) NSString *sqlExportErrors; + +@property(readwrite, assign) BOOL sqlOutputIncludeUTF8BOM; +@property(readwrite, assign) BOOL sqlOutputEncodeBLOBasHex; +@property(readwrite, assign) BOOL sqlOutputIncludeErrors; +@property(readwrite, assign) BOOL sqlOutputCompressFile; + +@property (readwrite, retain) NSDictionary *sqlTableInformation; + +/** + * Initialise an instance of SPSQLExporter using the supplied delegate. + * + * @param exportDelegate The exporter delegate + * + * @return The initialised instance + */ +- (id)initWithDelegate:(NSObject *)exportDelegate; + +/** + * Returns whether or not any export errors occurred. + * + * @return A BOOL indicating the occurrence of errors + */ +- (BOOL)didExportErrorsOccur; @end diff --git a/Source/SPSQLExporter.m b/Source/SPSQLExporter.m index db9e4532..9b7cbaf6 100644 --- a/Source/SPSQLExporter.m +++ b/Source/SPSQLExporter.m @@ -23,26 +23,794 @@ // // More info at <http://code.google.com/p/sequel-pro/> +#import <MCPKit/MCPKit.h> + #import "SPSQLExporter.h" +#import "TablesList.h" +#import "SPConstants.h" +#import "SPArrayAdditions.h" +#import "SPStringAdditions.h" +#import "SPFileHandle.h" +#import "SPExportUtilities.h" + +@interface SPSQLExporter (PrivateAPI) + +- (NSString *)_createViewPlaceholderSyntaxForView:(NSString *)viewName; + +@end @implementation SPSQLExporter +@synthesize delegate; +@synthesize sqlExportTables; +@synthesize sqlDatabaseHost; +@synthesize sqlDatabaseName; +@synthesize sqlDatabaseVersion; +@synthesize sqlExportCurrentTable; +@synthesize sqlExportErrors; +@synthesize sqlOutputIncludeUTF8BOM; +@synthesize sqlOutputEncodeBLOBasHex; +@synthesize sqlOutputIncludeErrors; +@synthesize sqlOutputCompressFile; +@synthesize sqlTableInformation; + +/** + * Initialise an instance of SPSQLExporter using the supplied delegate. + */ +- (id)initWithDelegate:(NSObject *)exportDelegate +{ + if ((self = [super init])) { + SPExportDelegateConformsToProtocol(exportDelegate, @protocol(SPSQLExporterProtocol)); + + [self setDelegate:exportDelegate]; + [self setSqlExportCurrentTable:nil]; + } + + return self; +} + /** - * Start the SQL data conversion process. This method is automatically called when an instance of this object + * Start the SQL export process. This method is automatically called when an instance of this class * is placed on an NSOperationQueue. Do not call it directly as there is no manual multithreading. */ - (void)main { @try { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + NSAutoreleasePool *sqlExportPool = [[NSAutoreleasePool alloc] init]; + + MCPResult *queryResult; + MCPStreamingResult *streamingResult; + + NSArray *row; + NSString *tableName; + NSDictionary *tableDetails; + NSMutableArray *tableColumnNumericStatus; + SPTableType tableType = SPTableTypeTable; + + id createTableSyntax = nil; + NSUInteger i, j, t, s, rowCount, queryLength, lastProgressValue; + + BOOL sqlOutputIncludeStructure; + BOOL sqlOutputIncludeContent; + BOOL sqlOutputIncludeDropSyntax; + + NSMutableArray *tables = [NSMutableArray array]; + NSMutableArray *procs = [NSMutableArray array]; + NSMutableArray *funcs = [NSMutableArray array]; + + NSMutableString *metaString = [NSMutableString string]; + NSMutableString *cellValue = [NSMutableString string]; + NSMutableString *errors = [[NSMutableString alloc] init]; + NSMutableString *sqlString = [[NSMutableString alloc] init]; + + NSMutableDictionary *viewSyntaxes = [NSMutableDictionary dictionary]; + + // Check that we have all the required info before starting the export + if ((![self sqlExportTables]) || ([[self sqlExportTables] count] == 0) || + (![self sqlTableInformation]) || ([[self sqlTableInformation] count] == 0) || + (![self sqlDatabaseHost]) || ([[self sqlDatabaseHost] isEqualToString:@""]) || + (![self sqlDatabaseName]) || ([[self sqlDatabaseName] isEqualToString:@""]) || + (![self sqlDatabaseVersion] || ([[self sqlDatabaseName] isEqualToString:@""]))) + { + [pool release]; + return; + } + + // Inform the delegate that the export process is about to begin + [delegate performSelectorOnMainThread:@selector(sqlExportProcessWillBegin:) withObject:self waitUntilDone:NO]; + + // Mark the process as running + [self setExportProcessIsRunning:YES]; + + // Clear errors + [self setSqlExportErrors:@""]; + + // Copy over the selected item names into tables in preparation for iteration + NSMutableArray *targetArray; + + for (NSArray *item in [self sqlExportTables]) + { + // Check for cancellation flag + if ([self isCancelled]) { + [pool release]; + return; + } + + switch ([NSArrayObjectAtIndex(item, 4) intValue]) { + case SPTableTypeProc: + targetArray = procs; + break; + case SPTableTypeFunc: + targetArray = funcs; + break; + default: + targetArray = tables; + break; + } + + [targetArray addObject:item]; + } + + // If required write the UTF-8 Byte Order Mark + if ([self sqlOutputIncludeUTF8BOM]) { + [metaString setString:@"\xef\xbb\xbf"]; + [metaString appendString:@"# ************************************************************\n"]; + } + else { + [metaString setString:@"# ************************************************************\n"]; + } + + // If required set the file handle to compress it's output + [[self exportOutputFileHandle] setShouldWriteWithGzipCompression:[self sqlOutputCompressFile]]; + + // Add the dump header to the dump file + [metaString appendString:@"# Sequel Pro SQL dump\n"]; + [metaString appendString:[NSString stringWithFormat:@"# Version %@\n#\n", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]]]; + [metaString appendString:[NSString stringWithFormat:@"# %@\n# %@\n#\n", SPHomePageURL, SPDevURL]]; + [metaString appendString:[NSString stringWithFormat:@"# Host: %@ (MySQL %@)\n", [self sqlDatabaseHost], [self sqlDatabaseVersion]]]; + [metaString appendString:[NSString stringWithFormat:@"# Database: %@\n", [self sqlDatabaseName]]]; + [metaString appendString:[NSString stringWithFormat:@"# Generation Time: %@\n", [NSDate date]]]; + [metaString appendString:@"# ************************************************************\n\n\n"]; + + // Add commands to store the client encodings used when importing and set to UTF8 to preserve data + [metaString appendString:@"/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n"]; + [metaString appendString:@"/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n"]; + [metaString appendString:@"/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n"]; + [metaString appendString:@"/*!40101 SET NAMES utf8 */;\n"]; + + // Add commands to store and disable unique checks, foreign key checks, mode and notes where supported. + // Include trailing semicolons to ensure they're run individually. Use MySQL-version based comments. + //if (sqlOutputIncludeDropSyntax) { + //[metaString appendString:@"/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n"]; + //} + + [metaString appendString:@"/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n"]; + [metaString appendString:@"/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n"]; + [metaString appendString:@"/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n\n"]; + + [[self exportOutputFileHandle] writeData:[metaString dataUsingEncoding:[self exportOutputEncoding]]]; + + // Loop through the selected tables + for (NSArray *table in [self sqlExportTables]) + { + // Check for cancellation flag + if ([self isCancelled]) { + [pool release]; + return; + } + + tableName = NSArrayObjectAtIndex(table, 0); + + sqlOutputIncludeStructure = [NSArrayObjectAtIndex(table, 1) boolValue]; + sqlOutputIncludeContent = [NSArrayObjectAtIndex(table, 2) boolValue]; + sqlOutputIncludeDropSyntax = [NSArrayObjectAtIndex(table, 3) boolValue]; + + // Set the current table + [self setSqlExportCurrentTable:tableName]; + + // Inform the delegate that we are about to start fetcihing data for the current table + [delegate performSelectorOnMainThread:@selector(sqlExportProcessWillBeginFetchingData:) withObject:self waitUntilDone:NO]; + + lastProgressValue = 0; + + // Add the name of table + [[self exportOutputFileHandle] writeData:[[NSString stringWithFormat:@"# Dump of table %@\n# ------------------------------------------------------------\n\n", tableName] dataUsingEncoding:[self exportOutputEncoding]]]; + + // Determine whether this table is a table or a view via the CREATE TABLE command, and keep the create table syntax + queryResult = [connection queryString:[NSString stringWithFormat:@"SHOW CREATE TABLE %@", [tableName backtickQuotedString]]]; + + [queryResult setReturnDataAsStrings:YES]; + + if ([queryResult numOfRows]) { + tableDetails = [[NSDictionary alloc] initWithDictionary:[queryResult fetchRowAsDictionary]]; + + if ([tableDetails objectForKey:@"Create View"]) { + [viewSyntaxes setValue:[[[[tableDetails objectForKey:@"Create View"] copy] autorelease] createViewSyntaxPrettifier] forKey:tableName]; + createTableSyntax = [self _createViewPlaceholderSyntaxForView:tableName]; + tableType = SPTableTypeView; + } + else { + createTableSyntax = [[[tableDetails objectForKey:@"Create Table"] copy] autorelease]; + tableType = SPTableTypeTable; + } + + [tableDetails release]; + } + + if ([connection queryErrored]) { + [errors appendString:[NSString stringWithFormat:@"%@\n", [connection getLastErrorMessage]]]; + + if ([self sqlOutputIncludeErrors]) { + [[self exportOutputFileHandle] writeData:[[NSString stringWithFormat:@"# Error: %@\n", [connection getLastErrorMessage]] dataUsingEncoding:NSUTF8StringEncoding]]; + } + } + + // Add a 'DROP TABLE' command if required + if (sqlOutputIncludeDropSyntax) + [[self exportOutputFileHandle] writeData:[[NSString stringWithFormat:@"DROP %@ IF EXISTS %@;\n\n", ((tableType == SPTableTypeTable) ? @"TABLE" : @"VIEW"), [tableName backtickQuotedString]] + dataUsingEncoding:[self exportOutputEncoding]]]; + + + // Add the create syntax for the table if specified in the export dialog + if (sqlOutputIncludeStructure && createTableSyntax) { + + if ([createTableSyntax isKindOfClass:[NSData class]]) { + createTableSyntax = [[[NSString alloc] initWithData:createTableSyntax encoding:[self exportOutputEncoding]] autorelease]; + } + + [[self exportOutputFileHandle] writeData:[createTableSyntax dataUsingEncoding:NSUTF8StringEncoding]]; + [[self exportOutputFileHandle] writeData:[[NSString stringWithString:@";\n\n"] dataUsingEncoding:NSUTF8StringEncoding]]; + } + + // Add the table content if required + if (sqlOutputIncludeContent && (tableType == SPTableTypeTable)) { + + // Retrieve the table details via the data class, and use it to build an array containing column numeric status + tableDetails = [NSDictionary dictionaryWithDictionary:[[self sqlTableInformation] objectForKey:tableName]]; + + NSUInteger colCount = [[tableDetails objectForKey:@"columns"] count]; + + tableColumnNumericStatus = [NSMutableArray arrayWithCapacity:colCount]; + + for (j = 0; j < colCount; j++) + { + // Check for cancellation flag + if ([self isCancelled]) { + [pool release]; + return; + } + + NSString *tableColumnTypeGrouping = [NSArrayObjectAtIndex([tableDetails objectForKey:@"columns"], j) objectForKey:@"typegrouping"]; + + [tableColumnNumericStatus addObject:[NSNumber numberWithBool:([tableColumnTypeGrouping isEqualToString:@"bit"] || [tableColumnTypeGrouping isEqualToString:@"integer"] || [tableColumnTypeGrouping isEqualToString:@"float"])]]; + } + + // Retrieve the number of rows in the table for progress bar drawing + rowCount = [NSArrayObjectAtIndex([[connection queryString:[NSString stringWithFormat:@"SELECT COUNT(1) FROM %@", [tableName backtickQuotedString]]] fetchRowAsArray], 0) integerValue]; + + // Set up a result set in streaming mode + streamingResult = [[connection streamingQueryString:[NSString stringWithFormat:@"SELECT * FROM %@", [tableName backtickQuotedString]] useLowMemoryBlockingStreaming:([self exportUsingLowMemoryBlockingStreaming])] retain]; + + NSArray *fieldNames = [streamingResult fetchFieldNames]; + + // Inform the delegate that we are about to start writing data for the current table + [delegate performSelectorOnMainThread:@selector(sqlExportProcessWillBeginWritingData:) withObject:self waitUntilDone:NO]; + + if (rowCount) { + queryLength = 0; + + // Lock the table for writing and disable keys if supported + [metaString setString:@""]; + [metaString appendString:[NSString stringWithFormat:@"LOCK TABLES %@ WRITE;\n", [tableName backtickQuotedString]]]; + [metaString appendString:[NSString stringWithFormat:@"/*!40000 ALTER TABLE %@ DISABLE KEYS */;\n\n", [tableName backtickQuotedString]]]; + + [[self exportOutputFileHandle] writeData:[metaString dataUsingEncoding:[self exportOutputEncoding]]]; + + // Construct the start of the insertion command + [[self exportOutputFileHandle] writeData:[[NSString stringWithFormat:@"INSERT INTO %@ (%@)\nVALUES\n\t(", + [tableName backtickQuotedString], [fieldNames componentsJoinedAndBacktickQuoted]] dataUsingEncoding:NSUTF8StringEncoding]]; + + // Iterate through the rows to construct a VALUES group for each + j = 0; + + sqlExportPool = [[NSAutoreleasePool alloc] init]; + + // Inform the delegate that we are about to start writing the data to disk + [delegate performSelectorOnMainThread:@selector(sqlExportProcessWillBeginWritingData:) withObject:self waitUntilDone:NO]; + + while (row = [streamingResult fetchNextRowAsArray]) + { + // Check for cancellation flag + if ([self isCancelled]) { + [connection cancelCurrentQuery]; + [streamingResult cancelResultLoad]; + [sqlExportPool release]; + [pool release]; + + return; + } + + j++; + [sqlString setString:@""]; + + // Update the progress + if ((j * ([self exportMaxProgress] / rowCount)) > lastProgressValue) { + + NSInteger progress = (j * ([self exportMaxProgress] / rowCount)); + + [self setExportProgressValue:progress]; + + lastProgressValue = progress; + + // Inform the delegate that the export's progress has been updated + [delegate performSelectorOnMainThread:@selector(sqlExportProcessProgressUpdated:) withObject:self waitUntilDone:NO]; + } + + for (t = 0; t < colCount; t++) + { + // Check for cancellation flag + if ([self isCancelled]) { + [sqlExportPool release]; + [pool release]; + + return; + } + + // Add NULL values directly to the output row + if ([NSArrayObjectAtIndex(row, t) isMemberOfClass:[NSNull class]]) { + [sqlString appendString:@"NULL"]; + } + // Add data types directly as hex data + else if ([NSArrayObjectAtIndex(row, t) isKindOfClass:[NSData class]]) { + + if ([self sqlOutputEncodeBLOBasHex]) { + [sqlString appendString:@"X'"]; + [sqlString appendString:[connection prepareBinaryData:NSArrayObjectAtIndex(row, t)]]; + } + else { + [sqlString appendString:@"'"]; + + NSString *data = [[NSString alloc] initWithData:NSArrayObjectAtIndex(row, t) encoding:[self exportOutputEncoding]]; + + if (data == nil) { + data = [[NSString alloc] initWithData:NSArrayObjectAtIndex(row, t) encoding:NSASCIIStringEncoding]; + } + + [sqlString appendString:data]; + + [data release]; + } + + [sqlString appendString:@"'"]; + } + else { + [cellValue setString:[NSArrayObjectAtIndex(row, t) description]]; + + // Add empty strings as a pair of quotes + if ([cellValue length] == 0) { + [sqlString appendString:@"''"]; + } + else { + // If this is a numeric column type, add the number directly. + if ([NSArrayObjectAtIndex(tableColumnNumericStatus, t) boolValue]) { + [sqlString appendString:cellValue]; + } + // Otherwise add a quoted string with special characters escaped + else { + [sqlString appendString:@"'"]; + [sqlString appendString:[connection prepareString:cellValue]]; + [sqlString appendString:@"'"]; + } + } + } + + // Add the field separator if this isn't the last cell in the row + if (t != ([row count] - 1)) [sqlString appendString:@","]; + } + + queryLength += [sqlString length]; + + // Close this VALUES group and set up the next one if appropriate + if (j != rowCount) { + + // Add a new INSERT starter command every ~250k of data + if (queryLength > 250000) { + [sqlString appendString:[NSString stringWithFormat:@");\n\nINSERT INTO %@ (%@)\nVALUES\n\t(", + [tableName backtickQuotedString], [fieldNames componentsJoinedAndBacktickQuoted]]]; + queryLength = 0; + + // Use the opportunity to drain and reset the autorelease pool + [sqlExportPool release]; + sqlExportPool = [[NSAutoreleasePool alloc] init]; + } + else { + [sqlString appendString:@"),\n\t("]; + } + } + else { + [sqlString appendString:@")"]; + } + + // Write this row to the file + [[self exportOutputFileHandle] writeData:[sqlString dataUsingEncoding:NSUTF8StringEncoding]]; + } + + // Complete the command + [[self exportOutputFileHandle] writeData:[[NSString stringWithString:@";\n\n"] dataUsingEncoding:NSUTF8StringEncoding]]; + + // Unlock the table and re-enable keys if supported + [metaString setString:@""]; + [metaString appendString:[NSString stringWithFormat:@"/*!40000 ALTER TABLE %@ ENABLE KEYS */;\n", [tableName backtickQuotedString]]]; + [metaString appendString:@"UNLOCK TABLES;\n"]; + + [[self exportOutputFileHandle] writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]]; + + // Drain the autorelease pool + [sqlExportPool release]; + } + + if ([connection queryErrored]) { + [errors appendString:[NSString stringWithFormat:@"%@\n", [connection getLastErrorMessage]]]; + + if ([self sqlOutputIncludeErrors]) { + [[self exportOutputFileHandle] writeData:[[NSString stringWithFormat:@"# Error: %@\n", [connection getLastErrorMessage]] + dataUsingEncoding:NSUTF8StringEncoding]]; + } + } + + // Release the result set + [streamingResult release]; + + queryResult = [connection queryString:[NSString stringWithFormat:@"/*!50003 SHOW TRIGGERS WHERE `Table` = %@ */;", [tableName tickQuotedString]]]; + + [queryResult setReturnDataAsStrings:YES]; + + if ([queryResult numOfRows]) { + + [metaString setString:@"\n"]; + [metaString appendString:@"DELIMITER ;;\n"]; + + for (s = 0; s < [queryResult numOfRows]; s++) + { + // Check for cancellation flag + if ([self isCancelled]) { + [pool release]; + return; + } + + NSDictionary *triggers = [[NSDictionary alloc] initWithDictionary:[queryResult fetchRowAsDictionary]]; + + // Definer is user@host but we need to escape it to `user`@`host` + NSArray *triggersDefiner = [[triggers objectForKey:@"Definer"] componentsSeparatedByString:@"@"]; + + NSString *escapedDefiner = [NSString stringWithFormat:@"%@@%@", + [NSArrayObjectAtIndex(triggersDefiner, 0) backtickQuotedString], + [NSArrayObjectAtIndex(triggersDefiner, 1) backtickQuotedString] + ]; + + [metaString appendString:[NSString stringWithFormat:@"/*!50003 SET SESSION SQL_MODE=\"%@\" */;;\n", [triggers objectForKey:@"sql_mode"]]]; + [metaString appendString:@"/*!50003 CREATE */ "]; + [metaString appendString:[NSString stringWithFormat:@"/*!50017 DEFINER=%@ */ ", escapedDefiner]]; + [metaString appendString:[NSString stringWithFormat:@"/*!50003 TRIGGER %@ %@ %@ ON %@ FOR EACH ROW %@ */;;\n", + [[triggers objectForKey:@"Trigger"] backtickQuotedString], + [triggers objectForKey:@"Timing"], + [triggers objectForKey:@"Event"], + [[triggers objectForKey:@"Table"] backtickQuotedString], + [triggers objectForKey:@"Statement"] + ]]; + + [triggers release]; + } + + [metaString appendString:@"DELIMITER ;\n"]; + [metaString appendString:@"/*!50003 SET SESSION SQL_MODE=@OLD_SQL_MODE */;\n"]; + + [[self exportOutputFileHandle] writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]]; + } + + if ([connection queryErrored]) { + [errors appendString:[NSString stringWithFormat:@"%@\n", [connection getLastErrorMessage]]]; + + if ([self sqlOutputIncludeErrors]) { + [[self exportOutputFileHandle] writeData:[[NSString stringWithFormat:@"# Error: %@\n", [connection getLastErrorMessage]] + dataUsingEncoding:NSUTF8StringEncoding]]; + } + } + + } + + // Add an additional separator between tables + [[self exportOutputFileHandle] writeData:[[NSString stringWithString:@"\n\n"] dataUsingEncoding:NSUTF8StringEncoding]]; + } + // Process any deferred views, adding commands to delete the placeholder tables and add the actual views + for (tableName in viewSyntaxes) + { + // Check for cancellation flag + if ([self isCancelled]) { + [pool release]; + return; + } + + [metaString setString:@"\n\n"]; + [metaString appendFormat:@"DROP TABLE %@;\n", [tableName backtickQuotedString]]; + [metaString appendFormat:@"%@;\n", [viewSyntaxes objectForKey:tableName]]; + + [[self exportOutputFileHandle] writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]]; + } + // Export procedures and functions + for (NSString *procedureType in [NSArray arrayWithObjects:@"PROCEDURE", @"FUNCTION", nil]) + { + // Check for cancellation flag + if ([self isCancelled]) { + [pool release]; + return; + } + + // Retrieve the array of selected procedures or functions, and skip export if not selected + NSMutableArray *items; + + if ([procedureType isEqualToString:@"PROCEDURE"]) items = procs; + else items = funcs; + + if ([items count] == 0) continue; + + // Retrieve the definitions + queryResult = [connection queryString:[NSString stringWithFormat:@"/*!50003 SHOW %@ STATUS WHERE `Db` = %@ */;", procedureType, + [[self sqlDatabaseName] tickQuotedString]]]; + + [queryResult setReturnDataAsStrings:YES]; + + if ([queryResult numOfRows]) { + + [metaString setString:@"\n"]; + [metaString appendString:@"--\n"]; + [metaString appendString:[NSString stringWithFormat:@"-- Dumping routines (%@) for database %@\n", procedureType, + [[self sqlDatabaseName] tickQuotedString]]]; + + [metaString appendString:@"--\n"]; + [metaString appendString:@"DELIMITER ;;\n"]; + + // Loop through the definitions, exporting if enabled + for (s = 0; s < [queryResult numOfRows]; s++) + { + // Check for cancellation flag + if ([self isCancelled]) { + [pool release]; + return; + } + + NSDictionary *proceduresList = [[NSDictionary alloc] initWithDictionary:[queryResult fetchRowAsDictionary]]; + NSString *procedureName = [NSString stringWithFormat:@"%@", [proceduresList objectForKey:@"Name"]]; + + // Only proceed if the item was selected for export + if (![items containsObject:procedureName]) { + [proceduresList release]; + continue; + } + + // Only proceed if the item is in the list of items + for (NSArray *item in items) + { + // Check for cancellation flag + if ([self isCancelled]) { + [pool release]; + return; + } + + if ([NSArrayObjectAtIndex(item, 0) isEqualToString:procedureName]) { + sqlOutputIncludeStructure = [NSArrayObjectAtIndex(item, 1) boolValue]; + sqlOutputIncludeContent = [NSArrayObjectAtIndex(item, 2) boolValue]; + sqlOutputIncludeDropSyntax = [NSArrayObjectAtIndex(item, 3) boolValue]; + } + } + + // Add the 'DROP' command if required + if (sqlOutputIncludeDropSyntax) { + [metaString appendString:[NSString stringWithFormat:@"/*!50003 DROP %@ IF EXISTS %@ */;;\n", procedureType, + [procedureName backtickQuotedString]]]; + } + + // Only continue if the 'CREATE SYNTAX' is required + if (sqlOutputIncludeStructure) { + [proceduresList release]; + continue; + } + + // Definer is user@host but we need to escape it to `user`@`host` + NSArray *procedureDefiner = [[proceduresList objectForKey:@"Definer"] componentsSeparatedByString:@"@"]; + + NSString *escapedDefiner = [NSString stringWithFormat:@"%@@%@", + [NSArrayObjectAtIndex(procedureDefiner, 0) backtickQuotedString], + [NSArrayObjectAtIndex(procedureDefiner, 1) backtickQuotedString] + ]; + + MCPResult *createProcedureResult = [connection queryString:[NSString stringWithFormat:@"/*!50003 SHOW CREATE %@ %@ */;;", procedureType, + [procedureName backtickQuotedString]]]; + + [createProcedureResult setReturnDataAsStrings:YES]; + + NSDictionary *procedureInfo = [[NSDictionary alloc] initWithDictionary:[createProcedureResult fetchRowAsDictionary]]; + + [metaString appendString:[NSString stringWithFormat:@"/*!50003 SET SESSION SQL_MODE=\"%@\"*/;;\n", [procedureInfo objectForKey:@"sql_mode"]]]; + + NSString *createProcedure = [procedureInfo objectForKey:[NSString stringWithFormat:@"Create %@", [procedureType capitalizedString]]]; + NSRange procedureRange = [createProcedure rangeOfString:procedureType options:NSCaseInsensitiveSearch]; + NSString *procedureBody = [createProcedure substringFromIndex:procedureRange.location]; + + // /*!50003 CREATE*/ /*!50020 DEFINER=`sequelpro`@`%`*/ /*!50003 PROCEDURE `p`() + // BEGIN + // /* This procedure does nothing */ + // END */;; + // + // Build the CREATE PROCEDURE string to include MySQL Version limiters + [metaString appendString:[NSString stringWithFormat:@"/*!50003 CREATE*/ /*!50020 DEFINER=%@*/ /*!50003 %@ */;;\n", escapedDefiner, procedureBody]]; + + [procedureInfo release]; + [proceduresList release]; + + [metaString appendString:@"/*!50003 SET SESSION SQL_MODE=@OLD_SQL_MODE */;;\n"]; + } + + [metaString appendString:@"DELIMITER ;\n"]; + + [[self exportOutputFileHandle] writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]]; + } + + if ([connection queryErrored]) { + [errors appendString:[NSString stringWithFormat:@"%@\n", [connection getLastErrorMessage]]]; + + if ([self sqlOutputIncludeErrors]) { + [[self exportOutputFileHandle] writeData:[[NSString stringWithFormat:@"# Error: %@\n", [connection getLastErrorMessage]] dataUsingEncoding:NSUTF8StringEncoding]]; + } + } + + } + + // Restore unique checks, foreign key checks, and other settings saved at the start + [metaString setString:@"\n"]; + [metaString appendString:@"/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n"]; + [metaString appendString:@"/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n"]; + [metaString appendString:@"/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n"]; + + //if (sqlOutputIncludeDropSyntax) { + //[metaString appendString:@"/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n"]; + //} + + // Restore the client encoding to the original encoding before import + [metaString appendString:@"/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n"]; + [metaString appendString:@"/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n"]; + [metaString appendString:@"/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n"]; + + // Write footer-type information to the file + [[self exportOutputFileHandle] writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]]; + + // Set export errors + [self setSqlExportErrors:errors]; + + [errors release]; + [sqlString release]; + + // Close the file + [[self exportOutputFileHandle] closeFile]; + + // Mark the process as not running + [self setExportProcessIsRunning:NO]; + + // Inform the delegate that the export process is complete + [delegate performSelectorOnMainThread:@selector(sqlExportProcessComplete:) withObject:self waitUntilDone:NO]; [pool release]; } - @catch (NSException *e) { + @catch (NSException *e) {} +} + +/** + * Returns whether or not any export errors occurred by examing the length of the errors string. + */ +- (BOOL)didExportErrorsOccur +{ + return [[self sqlExportErrors] length]; +} + +/** + * Retrieve information for a view and use that to construct a CREATE TABLE string for an equivalent basic + * table. Allows the construction of placeholder tables to resolve view interdependencies in dumps. + */ +- (NSString *)_createViewPlaceholderSyntaxForView:(NSString *)viewName +{ + NSInteger i, j; + NSMutableString *placeholderSyntax; + + // Get structured information for the view via the SPTableData parsers + NSDictionary *viewInformation = [[self sqlTableInformation] objectForKey:viewName]; + + if (!viewInformation) return nil; + + NSArray *viewColumns = [viewInformation objectForKey:@"columns"]; + + // Set up the start of the placeholder string and initialise an empty field string + placeholderSyntax = [[NSMutableString alloc] initWithFormat:@"CREATE TABLE %@ (\n", [viewName backtickQuotedString]]; + + NSMutableString *fieldString = [[NSMutableString alloc] init]; + + // Loop through the columns, creating an appropriate column definition for each and appending it to the syntax string + for (i = 0; i < [viewColumns count]; i++) + { + NSDictionary *column = NSArrayObjectAtIndex(viewColumns, i); + + [fieldString setString:[[column objectForKey:@"name"] backtickQuotedString]]; + + // Add the type and length information as appropriate + if ([column objectForKey:@"length"]) { + [fieldString appendFormat:@" %@(%@)", [column objectForKey:@"type"], [column objectForKey:@"length"]]; + } + else if ([column objectForKey:@"values"]) { + [fieldString appendFormat:@" %@(", [column objectForKey:@"type"]]; + + for (j = 0; j < [[column objectForKey:@"values"] count]; j++) + { + [fieldString appendFormat:@"'%@'%@", [connection prepareString:NSArrayObjectAtIndex([column objectForKey:@"values"], j)], ((j + 1) == [[column objectForKey:@"values"] count]) ? @"" : @","]; + } + + [fieldString appendString:@")"]; + } + else { + [fieldString appendFormat:@" %@", [column objectForKey:@"type"]]; + } + + // Field specification details + if ([[column objectForKey:@"unsigned"] integerValue] == 1) [fieldString appendString:@" UNSIGNED"]; + if ([[column objectForKey:@"zerofill"] integerValue] == 1) [fieldString appendString:@" ZEROFILL"]; + if ([[column objectForKey:@"binary"] integerValue] == 1) [fieldString appendString:@" BINARY"]; + if ([[column objectForKey:@"null"] integerValue] == 0) [fieldString appendString:@" NOT NULL"]; + // Provide the field default if appropriate + if ([column objectForKey:@"default"]) { + + // Some MySQL server versions show a default of NULL for NOT NULL columns - don't export those + if ([column objectForKey:@"default"] == [NSNull null]) { + if ([[column objectForKey:@"null"] integerValue]) { + [fieldString appendString:@" DEFAULT NULL"]; + } + } + else if ([[column objectForKey:@"type"] isEqualToString:@"TIMESTAMP"] && [column objectForKey:@"default"] != [NSNull null] && [[[column objectForKey:@"default"] uppercaseString] isEqualToString:@"CURRENT_TIMESTAMP"]) { + [fieldString appendString:@" DEFAULT CURRENT_TIMESTAMP"]; + } + else { + [fieldString appendFormat:@" DEFAULT '%@'", [connection prepareString:[column objectForKey:@"default"]]]; + } + } + + // Extras aren't required for the temp table + // Add the field string to the syntax string + [placeholderSyntax appendFormat:@" %@%@\n", fieldString, (i == [viewColumns count] - 1) ? @"" : @","]; } + + // Append the remainder of the table string + [placeholderSyntax appendString:@") ENGINE=MyISAM;"]; + + // Clean up and return + [fieldString release]; + + return [placeholderSyntax autorelease]; +} + +/** + * Dealloc + */ +- (void)dealloc +{ + [sqlExportTables release], sqlExportTables = nil; + [sqlDatabaseHost release], sqlDatabaseHost = nil; + [sqlDatabaseName release], sqlDatabaseName = nil; + [sqlExportCurrentTable release], sqlExportCurrentTable = nil; + [sqlDatabaseVersion release], sqlDatabaseVersion = nil; + [sqlTableInformation release], sqlTableInformation = nil; + + [super dealloc]; } @end diff --git a/Source/SPSQLExporterDelegate.h b/Source/SPSQLExporterDelegate.h new file mode 100644 index 00000000..1a439cd2 --- /dev/null +++ b/Source/SPSQLExporterDelegate.h @@ -0,0 +1,38 @@ +// +// $Id$ +// +// SPSQLExporterDelegate.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on March 28, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPExportController.h" +#import "SPSQLExporterProtocol.h" + +/** + * @category SPSQLExporterDelegate SPSQLExporterDelegate.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * SQL exporter delegate category. + */ +@interface SPExportController (SPSQLExporterDelegate) <SPSQLExporterProtocol> + +@end diff --git a/Source/SPSQLExporterDelegate.m b/Source/SPSQLExporterDelegate.m new file mode 100644 index 00000000..b9afe724 --- /dev/null +++ b/Source/SPSQLExporterDelegate.m @@ -0,0 +1,105 @@ +// +// $Id$ +// +// SPSQLExporterDelegate.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on March 28, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPSQLExporterDelegate.h" +#import "SPSQLExporter.h" +#import "TableDocument.h" +#import "SPMainThreadTrampoline.h" + +@implementation SPExportController (SPSQLExporterDelegate) + +/** + * + */ +- (void)sqlExportProcessWillBegin:(SPSQLExporter *)exporter +{ + [[exportProgressTitle onMainThread] setStringValue:NSLocalizedString(@"Exporting SQL", @"text showing that the application is exporting SQL")]; + [[exportProgressText onMainThread] setStringValue:NSLocalizedString(@"Dumping...", @"text showing that app is writing dump")]; + + [[exportProgressTitle onMainThread] displayIfNeeded]; + [[exportProgressText onMainThread] displayIfNeeded]; +} + +/** + * + */ +- (void)sqlExportProcessComplete:(SPSQLExporter *)exporter +{ + [NSApp endSheet:exportProgressWindow returnCode:0]; + [exportProgressWindow orderOut:self]; + + [tableDocumentInstance setQueryMode:SPInterfaceQueryMode]; + + // Restore the connection encoding to it's pre-export value + [tableDocumentInstance setConnectionEncoding:[NSString stringWithFormat:@"%@%@", sqlPreviousConnectionEncoding, (sqlPreviousConnectionEncodingViaLatin1) ? @"-" : @""] reloadingViews:NO]; + + // Display Growl notification + [self displayExportFinishedGrowlNotification]; + + // Check for errors and display the errors sheet if necessary + if ([exporter didExportErrorsOccur]) { + [self openExportErrorsSheetWithString:[exporter sqlExportErrors]]; + } +} + +/** + * + */ +- (void)sqlExportProcessProgressUpdated:(SPSQLExporter *)exporter +{ + //NSLog(@"updating: %f", [exporter exportProgressValue]); + + [exportProgressIndicator setDoubleValue:[exporter exportProgressValue]]; +} + +/** + * + */ +- (void)sqlExportProcessWillBeginFetchingData:(SPSQLExporter *)exporter +{ + // Update the current table export index + currentTableExportIndex = (exportTableCount - [exporters count]); + + [[exportProgressText onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %lu of %lu (%@): Fetching data...", @"export label showing that the app is fetching data for a specific table"), currentTableExportIndex, exportTableCount, [exporter sqlExportCurrentTable]]]; + + [[exportProgressText onMainThread] displayIfNeeded]; + + [[exportProgressIndicator onMainThread] stopAnimation:self]; + [[exportProgressIndicator onMainThread] setUsesThreadedAnimation:NO]; + [[exportProgressIndicator onMainThread] setIndeterminate:NO]; + [[exportProgressIndicator onMainThread] setDoubleValue:0]; +} + +/** + * + */ +- (void)sqlExportProcessWillBeginWritingData:(SPSQLExporter *)exporter +{ + [[exportProgressText onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %lu of %lu (%@): Writing data...", @"export label showing app if writing data for a specific table"), currentTableExportIndex, exportTableCount, [exporter sqlExportCurrentTable]]]; + + [[exportProgressText onMainThread] displayIfNeeded]; +} + +@end diff --git a/Source/SPSQLExporterProtocol.h b/Source/SPSQLExporterProtocol.h new file mode 100644 index 00000000..2998e1c9 --- /dev/null +++ b/Source/SPSQLExporterProtocol.h @@ -0,0 +1,72 @@ +// +// $Id$ +// +// SPSQLExporterProtocol.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 15, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +@class SPSQLExporter; + +/** + * @protocol SPSQLExporterProtocol SPSQLExporterProtocol.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * SQL exporter delegate protocol. + */ +@protocol SPSQLExporterProtocol + +/** + * Called when the SQL export process is about to begin. + * + * @param SPSQLExporter The expoter calling the method. + */ +- (void)sqlExportProcessWillBegin:(SPSQLExporter *)exporter; + +/** + * Called when the SQL export process is complete. + * + * @param SPSQLExporter The expoter calling the method. + */ +- (void)sqlExportProcessComplete:(SPSQLExporter *)exporter; + +/** + * alled when the progress of the SQL export process is updated. + * + * @param SPSQLExporter The expoter calling the method. + */ +- (void)sqlExportProcessProgressUpdated:(SPSQLExporter *)exporter; + +/** + * Called when the SQL export process is about to begin fetching data from the database. + * + * @param SPSQLExporter The expoter calling the method. + */ +- (void)sqlExportProcessWillBeginFetchingData:(SPSQLExporter *)exporter; + +/** + * Called when the SQL export process is about to begin writing data to disk. + * + * @param SPSQLExporter The expoter calling the method. + */ +- (void)sqlExportProcessWillBeginWritingData:(SPSQLExporter *)exporter; + +@end diff --git a/Source/SPStringAdditions.h b/Source/SPStringAdditions.h index 3f7c9066..1bcf158f 100644 --- a/Source/SPStringAdditions.h +++ b/Source/SPStringAdditions.h @@ -59,14 +59,15 @@ static inline id NSMutableAttributedStringAttributeAtIndex (NSMutableAttributedS + (NSString *)stringForByteSize:(long long)byteSize; + (NSString *)stringForTimeInterval:(CGFloat)timeInterval; +- (NSString *)HTMLEscapeString; - (NSString *)backtickQuotedString; - (NSString *)tickQuotedString; - (NSString *)replaceUnderscoreWithSpace; - (NSArray *)lineRangesForRange:(NSRange)aRange; - (NSString *)createViewSyntaxPrettifier; -- (NSString *)stringByRemovingCharactersInSet:(NSCharacterSet*) charSet options:(NSUInteger) mask; -- (NSString *)stringByRemovingCharactersInSet:(NSCharacterSet*) charSet; +- (NSString *)stringByRemovingCharactersInSet:(NSCharacterSet*)charSet options:(NSUInteger)mask; +- (NSString *)stringByRemovingCharactersInSet:(NSCharacterSet*)charSet; - (CGFloat)levenshteinDistanceWithWord:(NSString *)stringB; diff --git a/Source/SPStringAdditions.m b/Source/SPStringAdditions.m index c36e55c1..de1fdf5e 100644 --- a/Source/SPStringAdditions.m +++ b/Source/SPStringAdditions.m @@ -82,12 +82,9 @@ return [numberFormatter stringFromNumber:[NSNumber numberWithDouble:size]]; } - -// ------------------------------------------------------------------------------- -// stringForTimeInterval: -// -// Returns a human readable version string of the supplied time interval. -// ------------------------------------------------------------------------------- +/** + * Returns a human readable version string of the supplied time interval. + */ + (NSString *)stringForTimeInterval:(CGFloat)timeInterval { NSNumberFormatter *numberFormatter = [[[NSNumberFormatter alloc] init] autorelease]; @@ -145,42 +142,64 @@ return [numberFormatter stringFromNumber:[NSNumber numberWithDouble:timeInterval]]; } +/** + * Escapes HTML special characters. + */ +- (NSString *)HTMLEscapeString +{ + NSMutableString *mutableString = [NSMutableString stringWithString:self]; + + [mutableString replaceOccurrencesOfString:@"&" withString:@"&" + options:NSLiteralSearch + range:NSMakeRange(0, [mutableString length])]; + + [mutableString replaceOccurrencesOfString:@"<" withString:@"<" + options:NSLiteralSearch + range:NSMakeRange(0, [mutableString length])]; + + [mutableString replaceOccurrencesOfString:@">" withString:@">" + options:NSLiteralSearch + range:NSMakeRange(0, [mutableString length])]; + + [mutableString replaceOccurrencesOfString:@"\"" withString:@""" + options:NSLiteralSearch + range:NSMakeRange(0, [mutableString length])]; + + return [NSString stringWithString:mutableString]; +} -// ------------------------------------------------------------------------------- -// backtickQuotedString -// -// Returns the string quoted with backticks as required for MySQL identifiers -// eg.: tablename => `tablename` -// my`table => `my``table` -// ------------------------------------------------------------------------------- +/** + * Returns the string quoted with backticks as required for MySQL identifiers + * eg.: tablename => `tablename` + * my`table => `my``table` + */ - (NSString *)backtickQuotedString { return [NSString stringWithFormat: @"`%@`", [self stringByReplacingOccurrencesOfString:@"`" withString:@"``"]]; } -// ------------------------------------------------------------------------------- -// tickQuotedString -// -// Returns the string quoted with ticks as required for MySQL identifiers -// eg.: tablename => 'tablename' -// my'table => 'my''table' -// ------------------------------------------------------------------------------- +/** + * Returns the string quoted with ticks as required for MySQL identifiers + * eg.: tablename => 'tablename' + * my'table => 'my''table' + */ - (NSString *)tickQuotedString { return [NSString stringWithFormat: @"'%@'", [self stringByReplacingOccurrencesOfString:@"'" withString:@"''"]]; } +/** + * + */ - (NSString *)replaceUnderscoreWithSpace { return [self stringByReplacingOccurrencesOfString:@"_" withString:@" "]; } -// ------------------------------------------------------------------------------- -// createViewSyntaxPrettifier -// -// Returns a 'CREATE VIEW SYNTAX' string a bit more readable -// If the string doesn't match it returns the unchanged string. -// ------------------------------------------------------------------------------- +/** + * Returns a 'CREATE VIEW SYNTAX' string a bit more readable + * If the string doesn't match it returns the unchanged string. + */ - (NSString *)createViewSyntaxPrettifier { NSRange searchRange = NSMakeRange(0, [self length]); @@ -219,16 +238,13 @@ return(tblSyntax); } - -// ------------------------------------------------------------------------------- -// lineRangesForRange -// -// Returns an array of serialised NSRanges, each representing a line within the string -// which is at least partially covered by the NSRange supplied. -// Each line includes the line termination character(s) for the line. As per -// lineRangeForRange, lines are split by CR, LF, CRLF, U+2028 (Unicode line separator), -// or U+2029 (Unicode paragraph separator). -// ------------------------------------------------------------------------------- +/** + * Returns an array of serialised NSRanges, each representing a line within the string + * which is at least partially covered by the NSRange supplied. + * Each line includes the line termination character(s) for the line. As per + * lineRangeForRange, lines are split by CR, LF, CRLF, U+2028 (Unicode line separator), + * or U+2029 (Unicode paragraph separator). + */ - (NSArray *)lineRangesForRange:(NSRange)aRange { NSMutableArray *lineRangesArray = [NSMutableArray array]; @@ -252,7 +268,6 @@ return lineRangesArray; } - #if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_5 /* * componentsSeparatedByCharactersInSet: @@ -286,8 +301,10 @@ } #endif - -- (NSString *)stringByRemovingCharactersInSet:(NSCharacterSet*) charSet options:(NSUInteger) mask +/** + * + */ +- (NSString *)stringByRemovingCharactersInSet:(NSCharacterSet *)charSet options:(NSUInteger)mask { NSRange range; NSMutableString* newString = [NSMutableString string]; @@ -316,13 +333,17 @@ return newString; } - -- (NSString *)stringByRemovingCharactersInSet:(NSCharacterSet*) charSet +/** + * + */ +- (NSString *)stringByRemovingCharactersInSet:(NSCharacterSet *)charSet { return [self stringByRemovingCharactersInSet:charSet options:0]; } -// calculate the distance between two string case-insensitively +/** + * Calculate the distance between two string case-insensitively + */ - (CGFloat)levenshteinDistanceWithWord:(NSString *)stringB { // normalize strings @@ -368,10 +389,13 @@ return distance; } + return 0.0; } -// return the minimum of a, b and c +/** + * Returns the minimum of a, b and c + */ - (NSInteger)smallestOf:(NSInteger)a andOf:(NSInteger)b andOf:(NSInteger)c { NSInteger min = a; diff --git a/Source/SPXMLExporter.h b/Source/SPXMLExporter.h index dba81cba..a9e6c2dd 100644 --- a/Source/SPXMLExporter.h +++ b/Source/SPXMLExporter.h @@ -26,7 +26,45 @@ #import <Cocoa/Cocoa.h> #import "SPExporter.h" +#import "SPXMLExporterProtocol.h" +/** + * @class SPXMLExporter SPXMLExporter.m + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * XML exporter class. + */ @interface SPXMLExporter : SPExporter +{ + /** + * Exporter delegate + */ + NSObject <SPXMLExporterProtocol> *delegate; + + /** + * Data array + */ + NSArray *xmlDataArray; + + /** + * Table name + */ + NSString *xmlTableName; +} + +@property(readwrite, assign) NSObject *delegate; + +@property(readwrite, retain) NSArray *xmlDataArray; +@property(readwrite, retain) NSString *xmlTableName; + +/** + * Initialise an instance of SPXMLExporter using the supplied delegate. + * + * @param exportDelegate The exporter delegate + * + * @return The initialised instance + */ +- (id)initWithDelegate:(NSObject *)exportDelegate; @end diff --git a/Source/SPXMLExporter.m b/Source/SPXMLExporter.m index dabfe263..94cfec05 100644 --- a/Source/SPXMLExporter.m +++ b/Source/SPXMLExporter.m @@ -23,26 +23,236 @@ // // More info at <http://code.google.com/p/sequel-pro/> +#import <MCPKit/MCPKit.h> + #import "SPXMLExporter.h" +#import "SPArrayAdditions.h" +#import "SPStringAdditions.h" +#import "SPFileHandle.h" +#import "SPConstants.h" +#import "SPExportUtilities.h" @implementation SPXMLExporter +@synthesize delegate; +@synthesize xmlDataArray; +@synthesize xmlTableName; + +/** + * Initialise an instance of SPXMLExporter using the supplied delegate. + */ +- (id)initWithDelegate:(NSObject *)exportDelegate +{ + if ((self = [super init])) { + SPExportDelegateConformsToProtocol(exportDelegate, @protocol(SPXMLExporterProtocol)); + + [self setDelegate:exportDelegate]; + } + + return self; +} + /** - * Start the SQL data conversion process. This method is automatically called when an instance of this object + * Start the XML export process. This method is automatically called when an instance of this class * is placed on an NSOperationQueue. Do not call it directly as there is no manual multithreading. */ - (void)main { @try { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + NSAutoreleasePool *xmlExportPool = [[NSAutoreleasePool alloc] init]; + NSArray *xmlRow = nil; + NSString *dataConversionString = nil; + MCPStreamingResult *streamingResult = nil; + NSMutableArray *xmlTags = [NSMutableArray array]; + NSMutableString *xmlString = [NSMutableString string]; + NSMutableString *xmlItem = [NSMutableString string]; - [pool release]; - } - @catch (NSException *e) { + NSUInteger xmlRowCount = 0; + NSUInteger i, totalRows, currentRowIndex, lastProgressValue, currentPoolDataLength; + + // Check to see if we have at least a table name or data array + if ((![self xmlTableName]) && (![self xmlDataArray]) || + ([[self xmlTableName] isEqualToString:@""]) && ([[self xmlDataArray] count] == 0)) + { + [pool release]; + return; + } + + // Inform the delegate that the export process is about to begin + [delegate performSelectorOnMainThread:@selector(xmlExportProcessWillBegin:) withObject:self waitUntilDone:NO]; + + // Mark the process as running + [self setExportProcessIsRunning:YES]; + + lastProgressValue = 0; + + // Make a streaming request for the data if the data array isn't set + if ((![self xmlDataArray]) && [self xmlTableName]) { + totalRows = [[[[connection queryString:[NSString stringWithFormat:@"SELECT COUNT(1) FROM %@", [[self xmlTableName] backtickQuotedString]]] fetchRowAsArray] objectAtIndex:0] integerValue]; + streamingResult = [connection streamingQueryString:[NSString stringWithFormat:@"SELECT * FROM %@", [[self xmlTableName] backtickQuotedString]] useLowMemoryBlockingStreaming:[self exportUsingLowMemoryBlockingStreaming]]; + } + else { + totalRows = [[self xmlDataArray] count]; + } + + // Set up an array of encoded field names as opening and closing tags + xmlRow = ([self xmlDataArray]) ? [[self xmlDataArray] objectAtIndex:0] : [streamingResult fetchFieldNames]; + + for (i = 0; i < [xmlRow count]; i++) + { + [xmlTags addObject:[NSMutableArray array]]; + + [[xmlTags objectAtIndex:i] addObject:[NSString stringWithFormat:@"\t\t<%@>", [[[xmlRow objectAtIndex:i] description] HTMLEscapeString]]]; + [[xmlTags objectAtIndex:i] addObject:[NSString stringWithFormat:@"</%@>\n", [[[xmlRow objectAtIndex:i] description] HTMLEscapeString]]]; + } + [[self exportOutputFileHandle] writeData:[xmlString dataUsingEncoding:[self exportOutputEncoding]]]; + + // Write an opening tag in the form of the table name + [[self exportOutputFileHandle] writeData:[[NSString stringWithFormat:@"\t<%@>\n", ([self xmlTableName]) ? [[self xmlTableName] HTMLEscapeString] : @"custom"] dataUsingEncoding:[self exportOutputEncoding]]]; + + // Set up the starting row, which is 0 for streaming result sets and + // 1 for supplied arrays which include the column headers as the first row. + currentRowIndex = 0; + + if ([self xmlDataArray]) currentRowIndex++; + + // Drop into the processing loop + xmlExportPool = [[NSAutoreleasePool alloc] init]; + + currentPoolDataLength = 0; + + while (1) + { + // Check for cancellation flag + if ([self isCancelled]) { + if (streamingResult) { + [connection cancelCurrentQuery]; + [streamingResult cancelResultLoad]; + } + + [xmlExportPool release]; + [pool release]; + + return; + } + + // Retrieve the next row from the supplied data, either directly from the array... + if ([self xmlDataArray]) { + xmlRow = NSArrayObjectAtIndex([self xmlDataArray], currentRowIndex); + } + // Or by reading an appropriate row from the streaming result + else { + xmlRow = [streamingResult fetchNextRowAsArray]; + + if (!xmlRow) break; + } + + // Get the cell count if we don't already have it stored + if (!xmlRowCount) xmlRowCount = [xmlRow count]; + + // Construct the row + [xmlString setString:@"\t<row>\n"]; + + for (i = 0; i < xmlRowCount; i++) + { + // Check for cancellation flag + if ([self isCancelled]) { + if (streamingResult) { + [connection cancelCurrentQuery]; + [streamingResult cancelResultLoad]; + } + + [xmlExportPool release]; + [pool release]; + + return; + } + + // Retrieve the contents of this tag + if ([NSArrayObjectAtIndex(xmlRow, i) isKindOfClass:[NSData class]]) { + dataConversionString = [[NSString alloc] initWithData:NSArrayObjectAtIndex(xmlRow, i) encoding:[self exportOutputEncoding]]; + + if (dataConversionString == nil) { + dataConversionString = [[NSString alloc] initWithData:NSArrayObjectAtIndex(xmlRow, i) encoding:NSASCIIStringEncoding]; + } + + [xmlItem setString:[NSString stringWithString:dataConversionString]]; + [dataConversionString release]; + } + else { + [xmlItem setString:[NSArrayObjectAtIndex(xmlRow, i) description]]; + } + + // Add the opening and closing tag and the contents to the XML string + [xmlString appendString:NSArrayObjectAtIndex(NSArrayObjectAtIndex(xmlTags, i), 0)]; + [xmlString appendString:[xmlItem HTMLEscapeString]]; + [xmlString appendString:NSArrayObjectAtIndex(NSArrayObjectAtIndex(xmlTags, i), 1)]; + } + + [xmlString appendString:@"\t</row>\n"]; + + // Record the total length for use with pool flushing + currentPoolDataLength += [xmlString length]; + + // Write the row to the filehandle + [[self exportOutputFileHandle] writeData:[xmlString dataUsingEncoding:[self exportOutputEncoding]]]; + + // Update the progress counter and progress bar + currentRowIndex++; + + // Update the progress + if (totalRows && (currentRowIndex * ([self exportMaxProgress] / totalRows)) > lastProgressValue) { + + NSInteger progress = (currentRowIndex * ([self exportMaxProgress] / totalRows)); + + [self setExportProgressValue:progress]; + + lastProgressValue = progress; + } + + // Inform the delegate that the export's progress has been updated + [delegate performSelectorOnMainThread:@selector(xmlExportProcessProgressUpdated:) withObject:self waitUntilDone:NO]; + + // If an array was supplied and we've processed all rows, break + if ([self xmlDataArray] && totalRows == currentRowIndex) break; + + // Drain the autorelease pool as required to keep memory usage low + if (currentPoolDataLength > 250000) { + [xmlExportPool release]; + xmlExportPool = [[NSAutoreleasePool alloc] init]; + } + } + + // Write the closing tag for the table + [[self exportOutputFileHandle] writeData:[[NSString stringWithFormat:@"\t</%@>\n\n", ([self xmlTableName]) ? [[self xmlTableName] HTMLEscapeString] : @"custom"] dataUsingEncoding:[self exportOutputEncoding]]]; + + // Write data to disk + [[self exportOutputFileHandle] synchronizeFile]; + + // Mark the process as not running + [self setExportProcessIsRunning:NO]; + + // Inform the delegate that the export process is complete + [delegate performSelectorOnMainThread:@selector(xmlExportProcessComplete:) withObject:self waitUntilDone:NO]; + + [pool release]; } + @catch (NSException *e) { } +} + +/** + * Dealloc + */ +- (void)dealloc +{ + if (xmlDataArray) [xmlDataArray release], xmlDataArray = nil; + if (xmlTableName) [xmlTableName release], xmlTableName = nil; + + [super dealloc]; } @end diff --git a/Source/SPXMLExporterDelegate.h b/Source/SPXMLExporterDelegate.h new file mode 100644 index 00000000..720a8c9b --- /dev/null +++ b/Source/SPXMLExporterDelegate.h @@ -0,0 +1,38 @@ +// +// $Id$ +// +// SPXMLExporterDelegate.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 6, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPExportController.h" +#import "SPXMLExporterProtocol.h" + +/** + * @category SPXMLExporterDelegate SPXMLExporterDelegate.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * XML exporter delegate category. + */ +@interface SPExportController (SPXMLExporterDelegate) <SPXMLExporterProtocol> + +@end diff --git a/Source/SPXMLExporterDelegate.m b/Source/SPXMLExporterDelegate.m new file mode 100644 index 00000000..b8edaaf9 --- /dev/null +++ b/Source/SPXMLExporterDelegate.m @@ -0,0 +1,133 @@ +// +// $Id$ +// +// SPXMLExporterDelegate.m +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 6, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPXMLExporterDelegate.h" +#import "SPXMLExporter.h" +#import "SPMainThreadTrampoline.h" +#import "TableDocument.h" +#import "SPFileHandle.h" +#import "SPStringAdditions.h" + +@implementation SPExportController (SPXMLExporterDelegate) + +/** + * + */ +- (void)xmlExportProcessWillBegin:(SPXMLExporter *)exporter +{ + [[exportProgressText onMainThread] displayIfNeeded]; + + [[exportProgressIndicator onMainThread] setIndeterminate:YES]; + [[exportProgressIndicator onMainThread] setUsesThreadedAnimation:YES]; + [[exportProgressIndicator onMainThread] startAnimation:self]; + + // Only update the progress text if this is a table export + if (exportSource == SPTableExport) { + // Update the current table export index + currentTableExportIndex = (exportTableCount - [exporters count]); + + [[exportProgressText onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %lu of %lu (%@): Fetching data...", @"export label showing that the app is fetching data for a specific table"), currentTableExportIndex, exportTableCount, [exporter xmlTableName]]]; + } + else { + [[exportProgressText onMainThread] setStringValue:NSLocalizedString(@"Fetching data...", @"export label showing that the app is fetching data")]; + } + + [[exportProgressText onMainThread] displayIfNeeded]; +} + +/** + * + */ +- (void)xmlExportProcessComplete:(SPXMLExporter *)exporter +{ + NSUInteger exportCount = [exporters count]; + + // If required add the next exporter to the operation queue + if ((exportCount > 0) && (exportSource == SPTableExport)) { + + // If we're exporting to multiple files then close the file handle of the exporter + // that just finished, ensuring its data is written to disk. + if (exportToMultipleFiles) { + [[exporter exportOutputFileHandle] writeData:[[NSString stringWithFormat:@"</%@>\n", [[tableDocumentInstance database] HTMLEscapeString]] dataUsingEncoding:[connection encoding]]]; + + [[exporter exportOutputFileHandle] closeFile]; + } + + [operationQueue addOperation:[exporters objectAtIndex:0]]; + + // Remove the exporter we just added to the operation queue from our list of exporters + // so we know it's already been done. + [exporters removeObjectAtIndex:0]; + } + // Otherwise if the exporter list is empty, close the progress sheet + else { + if (exportSource == SPTableExport) { + [[exporter exportOutputFileHandle] writeData:[[NSString stringWithFormat:@"</%@>\n", [[tableDocumentInstance database] HTMLEscapeString]] dataUsingEncoding:[connection encoding]]]; + } + + // Close the last exporter's file handle + [[exporter exportOutputFileHandle] closeFile]; + + [NSApp endSheet:exportProgressWindow returnCode:0]; + [exportProgressWindow orderOut:self]; + + // Restore query mode + [tableDocumentInstance setQueryMode:SPInterfaceQueryMode]; + + // Display Growl notification + [self displayExportFinishedGrowlNotification]; + } +} + +/** + * + */ +- (void)xmlExportProcessProgressUpdated:(SPXMLExporter *)exporter +{ + // Only update the progress text if this is a table export + if (exportSource == SPTableExport) { + [[exportProgressText onMainThread] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %lu of %lu (%@): Writing data...", @"export label showing app if writing data for a specific table"), currentTableExportIndex, exportTableCount, [exporter xmlTableName]]]; + } + else { + [[exportProgressText onMainThread] setStringValue:NSLocalizedString(@"Writing data...", @"export label showing app is writing data")]; + } + + [[exportProgressText onMainThread] displayIfNeeded]; + + [[exportProgressIndicator onMainThread] stopAnimation:self]; + [[exportProgressIndicator onMainThread] setUsesThreadedAnimation:NO]; + [[exportProgressIndicator onMainThread] setIndeterminate:NO]; + [[exportProgressIndicator onMainThread] setDoubleValue:0]; +} + +/** + * + */ +- (void)xmlExportProcessWillBeginWritingData:(SPXMLExporter *)exporter +{ + [exportProgressIndicator setDoubleValue:[exporter exportProgressValue]]; +} + +@end diff --git a/Source/SPXMLExporterProtocol.h b/Source/SPXMLExporterProtocol.h new file mode 100644 index 00000000..cac692c7 --- /dev/null +++ b/Source/SPXMLExporterProtocol.h @@ -0,0 +1,65 @@ +// +// $Id$ +// +// SPXMLExporterProtocol.h +// sequel-pro +// +// Created by Stuart Connolly (stuconnolly.com) on April 15, 2010 +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +@class SPXMLExporter; + +/** + * @protocol SPXMLExporterProtocol SPXMLExporterProtocol.h + * + * @author Stuart Connolly http://stuconnolly.com/ + * + * SQL exporter delegate protocol. + */ +@protocol SPXMLExporterProtocol + +/** + * Called when the XML export process is about to begin. + * + * @param SPXMLExporter The expoter calling the method. + */ +- (void)xmlExportProcessWillBegin:(SPXMLExporter *)exporter; + +/** + * Called when the XML export process is complete. + * + * @param SPXMLExporter The expoter calling the method. + */ +- (void)xmlExportProcessComplete:(SPXMLExporter *)exporter; + +/** + * Called when the progress of the XML export process is updated. + * + * @param SPXMLExporter The expoter calling the method. + */ +- (void)xmlExportProcessProgressUpdated:(SPXMLExporter *)exporter; + +/** + * Called when the XML export process is about to begin writing data to disk. + * + * @param SPXMLExporter The expoter calling the method. + */ +- (void)xmlExportProcessWillBeginWritingData:(SPXMLExporter *)exporter; + +@end diff --git a/Source/TableDocument.h b/Source/TableDocument.h index b8478f98..5b6697a6 100644 --- a/Source/TableDocument.h +++ b/Source/TableDocument.h @@ -256,6 +256,7 @@ - (IBAction)copyCreateTableSyntaxFromSheet:(id)sender; - (IBAction)focusOnTableContentFilter:(id)sender; - (IBAction)focusOnTableListFilter:(id)sender; +- (IBAction)exportSelectedTablesAs:(id)sender; // Other methods - (void) setQueryMode:(NSInteger)theQueryMode; diff --git a/Source/TableDocument.m b/Source/TableDocument.m index d0637eed..ce87a1ef 100644 --- a/Source/TableDocument.m +++ b/Source/TableDocument.m @@ -2283,6 +2283,14 @@ [tablesListInstance performSelector:@selector(makeTableListFilterHaveFocus) withObject:nil afterDelay:0.1]; } +/** + * Exports the selected tables in the chosen file format. + */ +- (IBAction)exportSelectedTablesAs:(id)sender +{ + [exportControllerInstance exportTables:[tablesListInstance selectedTableItems] asFormat:[sender tag]]; +} + #pragma mark - #pragma mark Other Methods @@ -3159,9 +3167,18 @@ return YES; } } + + // Data export + if ([menuItem action] == @selector(export:)) { + return (([self database] != nil) && ([[tablesListInstance tables] count] > 1)); + } + + // Selected tables data export + if ([menuItem action] == @selector(exportSelectedTablesAs:)) { + return (([self database] != nil) && ([[[tablesListInstance valueForKeyPath:@"tablesListView"] selectedRowIndexes] count])); + } if ([menuItem action] == @selector(import:) || - [menuItem action] == @selector(export:) || [menuItem action] == @selector(exportMultipleTables:) || [menuItem action] == @selector(removeDatabase:) || [menuItem action] == @selector(copyDatabase:) || |