// // $Id$ // // SPExportController.m // sequel-pro // // Created by Ben Perry (benperry.com.au) on 21/02/09. // // 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 #import "SPExportController.h" #import "SPCSVExporter.h" #import "TablesList.h" #import "SPTableData.h" #import "TableDocument.h" #import "SPArrayAdditions.h" #import "SPStringAdditions.h" #import "SPConstants.h" @interface SPExportController (PrivateAPI) - (void)_initializeExportUsingSelectedOptions; - (BOOL)_exportTables:(NSArray *)exportTables asType:(SPExportType)type toMultipleFiles:(BOOL)multipleFiles; @end @implementation SPExportController @synthesize connection; @synthesize exportCancelled; /** * Initializes an instance of SPExportController */ - (id)init { if ((self = [super init])) { [self setExportCancelled:NO]; tables = [[NSMutableArray alloc] init]; operationQueue = [[NSOperationQueue alloc] init]; tableExportMapping = [NSMutableDictionary dictionary]; nibObjectsToRelease = [[NSMutableArray alloc] init]; } return self; } /** * Upon awakening select the first toolbar item */ - (void)awakeFromNib { // Upon awakening select the SQL tab [exportToolbar setSelectedItemIdentifier:[[[exportToolbar items] objectAtIndex:0] itemIdentifier]]; } #pragma mark - #pragma mark IB action methods /** * Display the export window allowing the user to select what and of what type to export. */ - (void)export { // 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]; } NSUInteger i; [tables removeAllObjects]; MCPResult *queryResult = (MCPResult *)[[self connection] listTables]; if ([queryResult numOfRows]) [queryResult dataSeek:0]; for ( i = 0 ; i < [queryResult numOfRows] ; i++ ) { [tables addObject:[NSMutableArray arrayWithObjects: [NSNumber numberWithBool:YES], NSArrayObjectAtIndex([queryResult fetchRowAsArray], 0), nil]]; } [exportTableList reloadData]; [exportPathField setStringValue:NSHomeDirectory()]; [NSApp beginSheet:exportWindow modalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:nil]; } /** * Closes the export dialog */ - (IBAction)closeSheet:(id)sender { [NSApp endSheet:exportWindow returnCode:[sender tag]]; [exportWindow orderOut:self]; } /** * Change the selected toolbar item. */ - (IBAction)switchTab:(id)sender { if ([sender isKindOfClass:[NSToolbarItem class]]) { [exportTabBar selectTabViewItemWithIdentifier:[[sender label] lowercaseString]]; [exportFilePerTableCheck setHidden:[[sender label] isEqualToString:@"Excel"]]; [exportFilePerTableNote setHidden:[[sender label] isEqualToString:@"Excel"]]; } } /** * */ - (IBAction)switchInput:(id)sender { if ([sender isKindOfClass:[NSMatrix class]]) { [exportTableList setEnabled:([[sender selectedCell] tag] == 3)]; } } /** * Cancel's the export operation by stopping the current table export loop and marking any current SPExporter * NSOperation subclasses as cancelled. */ - (IBAction)cancelExport:(id)sender { [self setExportCancelled:YES]; // Cancel all of the currently running operations [operationQueue cancelAllOperations]; // Close the progress sheet [NSApp endSheet:exportProgressWindow returnCode:0]; [exportProgressWindow orderOut:self]; } /** * */ - (IBAction)changeExportOutputPath:(id)sender { NSOpenPanel *panel = [NSOpenPanel openPanel]; [panel setCanChooseFiles:NO]; [panel setCanChooseDirectories:YES]; [panel setCanCreateDirectories:YES]; [panel beginSheetForDirectory:NSHomeDirectory() file:nil modalForWindow:exportWindow modalDelegate:self didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo:nil]; } #pragma mark - #pragma mark Table view datasource methods - (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView; { return [tables count]; } - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { return NSArrayObjectAtIndex([tables objectAtIndex:rowIndex], ([[aTableColumn identifier] isEqualToString:@"switch"]) ? 0 : 1); } - (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { [[tables objectAtIndex:rowIndex] replaceObjectAtIndex:0 withObject:anObject]; } #pragma mark - #pragma mark Table view delegate methods - (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(NSInteger)rowIndex { return (aTableView != exportTableList); } - (BOOL)tableView:(NSTableView *)aTableView shouldTrackCell:(NSCell *)cell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { return (aTableView == exportTableList); } - (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { [aCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; } #pragma mark - #pragma mark Toolbar delegate methods - (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar { NSMutableArray *items = [NSMutableArray arrayWithCapacity:6]; for (NSToolbarItem *item in [toolbar items]) { [items addObject:[item itemIdentifier]]; } return items; } #pragma mark - #pragma mark SPExporterDataAccess protocol 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]; } } #pragma mark - #pragma mark Other /** * Invoked when the user */ - (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { // Perform the export 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]; } } /** * Invoked when the user dismisses the save panel. Updates the selected directory is they clicked OK. */ - (void)savePanelDidEnd:(NSSavePanel *)panel returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { if (returnCode == NSOKButton) { [exportPathField setStringValue:[panel directory]]; } } #pragma mark - /** * Dealloc */ - (void)dealloc { [tables release], tables = nil; [operationQueue release], operationQueue = nil; for (id retainedObject in nibObjectsToRelease) [retainedObject release]; [nibObjectsToRelease release], nibObjectsToRelease = nil; [super dealloc]; } @end @implementation SPExportController (PrivateAPI) /** * */ - (void)_initializeExportUsingSelectedOptions { // First determine what type of export the user selected SPExportType exportType = 0; for (NSToolbarItem *item in [exportToolbar items]) { if ([[item itemIdentifier] isEqualToString:[exportToolbar selectedItemIdentifier]]) { exportType = [item tag]; break; } } // Determine what data to use (filtered result, custom query result or selected 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]]; } } 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; } } /** * Exports the contents' of the supplied array of tables. Note that this method currently only supports * exporting in CSV and XML formats. */ - (BOOL)_exportTables:(NSArray *)exportTables asType:(SPExportType)type toMultipleFiles:(BOOL)multipleFiles { NSUInteger i; NSMutableString *errors = [NSMutableString string]; NSDictionary *tableDetails = nil; //NSStringEncoding encoding = [[self connection] encoding]; // 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]; // 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])]; [csvLineEnd replaceOccurrencesOfString:@"\\r" withString:@"\r" options:NSLiteralSearch range:NSMakeRange(0, [csvLineEnd length])]; NSUInteger tableCount = [exportTables count]; // If if ((type == SP_CSV_EXPORT) && (!multipleFiles) && (tableCount > 1)) { } /*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]]; } return YES; } @end