aboutsummaryrefslogtreecommitdiffstats
path: root/Source/SPDataImport.m
diff options
context:
space:
mode:
Diffstat (limited to 'Source/SPDataImport.m')
-rw-r--r--Source/SPDataImport.m1438
1 files changed, 1438 insertions, 0 deletions
diff --git a/Source/SPDataImport.m b/Source/SPDataImport.m
new file mode 100644
index 00000000..7b55c812
--- /dev/null
+++ b/Source/SPDataImport.m
@@ -0,0 +1,1438 @@
+//
+// $Id$
+//
+// SPDataImport.m
+// sequel-pro
+//
+// Created by lorenz textor (lorenz@textor.ch) on Wed May 01 2002.
+// Copyright (c) 2002-2003 Lorenz Textor. 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 "SPDataImport.h"
+#import "SPDatabaseDocument.h"
+#import "SPTablesList.h"
+#import "SPTableStructure.h"
+#import "SPTableContent.h"
+#import "SPCustomQuery.h"
+#import "SPGrowlController.h"
+#import "SPSQLParser.h"
+#import "SPCSVParser.h"
+#import "SPTableData.h"
+#import "SPStringAdditions.h"
+#import "SPArrayAdditions.h"
+#import "RegexKitLite.h"
+#import "SPConstants.h"
+#import "SPAlertSheets.h"
+#import "SPFieldMapperController.h"
+#import "SPMainThreadTrampoline.h"
+#import "SPNotLoaded.h"
+#import "SPFileHandle.h"
+
+@implementation SPDataImport
+
+#pragma mark -
+#pragma mark Initialisation
+
+/**
+ * Init.
+ */
+- (id)init
+{
+ if ((self = [super init])) {
+
+ nibObjectsToRelease = [[NSMutableArray alloc] init];
+ fieldMappingArray = nil;
+ fieldMappingGlobalValueArray = nil;
+ fieldMappingTableColumnNames = nil;
+ fieldMappingTableDefaultValues = nil;
+ fieldMappingImportArray = nil;
+ csvImportTailString = nil;
+ csvImportHeaderString = nil;
+ csvImportMethodHasTail = NO;
+ fieldMappingImportArrayIsPreview = NO;
+ fieldMappingArrayHasGlobalVariables = NO;
+ importMethodIsUpdate = NO;
+ insertRemainingRowsAfterUpdate = NO;
+ numberOfImportDataColumns = 0;
+
+ prefs = nil;
+ lastFilename = nil;
+ _mainNibLoaded = NO;
+ }
+
+ return self;
+}
+
+/**
+ * UI setup.
+ */
+- (void)awakeFromNib
+{
+ if (_mainNibLoaded) return;
+ _mainNibLoaded = YES;
+
+ // Load the import accessory view, retaining a reference to the top-level objects that need releasing.
+ NSArray *importAccessoryTopLevelObjects = nil;
+ NSNib *nibLoader = [[NSNib alloc] initWithNibNamed:@"ImportAccessory" bundle:[NSBundle mainBundle]];
+ [nibLoader instantiateNibWithOwner:self topLevelObjects:&importAccessoryTopLevelObjects];
+ [nibObjectsToRelease addObjectsFromArray:importAccessoryTopLevelObjects];
+ [nibLoader release];
+}
+
+#pragma mark -
+#pragma mark IBAction methods
+
+/**
+ * Cancels the current operation.
+ */
+- (IBAction)cancelProgressBar:(id)sender
+{
+ progressCancelled = YES;
+}
+
+/**
+ * Common method for ending modal sessions
+ */
+- (IBAction)closeSheet:(id)sender
+{
+ [NSApp endSheet:[sender window] returnCode:[sender tag]];
+ [[sender window] orderOut:self];
+}
+
+/**
+ * Convenience method for closing and restoring the progress sheet to default state.
+ */
+- (void)closeAndStopProgressSheet
+{
+ if (![NSThread isMainThread]) {
+ [self performSelectorOnMainThread:@selector(closeAndStopProgressSheet) withObject:nil waitUntilDone:YES];
+ return;
+ }
+
+ [NSApp endSheet:singleProgressSheet];
+ [singleProgressSheet orderOut:nil];
+ [[singleProgressBar onMainThread] stopAnimation:self];
+ [[singleProgressBar onMainThread] setMaxValue:100];
+}
+
+/**
+ * When the compression setting on export is altered, update the filename
+ * and if appropriate the required extension.
+ */
+- (IBAction)updateExportCompressionSetting:(id)sender
+{
+ if (exportMode == SPExportingSQL) {
+ if ([sender state] == NSOnState) {
+ [currentExportPanel setAllowedFileTypes:[NSArray arrayWithObjects:[NSString stringWithFormat:@"%@.gz", SPFileExtensionSQL], @"gz", nil]];
+
+ // if file name text view is the first responder re-select the path name only without '.sql.gz'
+ if([[currentExportPanel firstResponder] isKindOfClass:[NSTextView class]]) {
+ NSTextView *filenameTextView = (NSTextView *)[currentExportPanel firstResponder];
+ if([filenameTextView selectedRange].length > 4 && [[filenameTextView string] hasSuffix:[NSString stringWithFormat:@".%@.gz", SPFileExtensionSQL]]) {
+ NSRange selRange = [filenameTextView selectedRange];
+ selRange.length -= 4;
+ [filenameTextView setSelectedRange:selRange];
+ }
+ }
+
+ } else {
+ [currentExportPanel setAllowedFileTypes:[NSArray arrayWithObject:SPFileExtensionSQL]];
+ }
+
+ [prefs setBool:([sender state] == NSOnState) forKey:SPSQLExportUseCompression];
+ }
+}
+
+#pragma mark -
+#pragma mark Import methods
+
+/**
+ * Invoked when user clicks on an ImportFromClipboard menuitem.
+ */
+- (void)importFromClipboard
+{
+
+ // clipboard textview with no wrapping
+ const CGFloat LargeNumberForText = 1.0e7;
+ [[importFromClipboardTextView textContainer] setContainerSize:NSMakeSize(LargeNumberForText, LargeNumberForText)];
+ [[importFromClipboardTextView textContainer] setWidthTracksTextView:NO];
+ [[importFromClipboardTextView textContainer] setHeightTracksTextView:NO];
+ [importFromClipboardTextView setAutoresizingMask:NSViewNotSizable];
+ [importFromClipboardTextView setMaxSize:NSMakeSize(LargeNumberForText, LargeNumberForText)];
+ [importFromClipboardTextView setHorizontallyResizable:YES];
+ [importFromClipboardTextView setVerticallyResizable:YES];
+ [importFromClipboardTextView setFont:[NSFont fontWithName:@"Monaco" size:11.0f]];
+
+ if([[[NSPasteboard generalPasteboard] stringForType:NSStringPboardType] length] > 4000)
+ [importFromClipboardTextView setString:[[[[NSPasteboard generalPasteboard] stringForType:NSStringPboardType] substringToIndex:4000] stringByAppendingString:@"\n…"]];
+ else
+ [importFromClipboardTextView setString:[[NSPasteboard generalPasteboard] stringForType:NSStringPboardType]];
+
+ // Preset the accessory view with prefs defaults
+ [importFieldsTerminatedField setStringValue:[prefs objectForKey:SPCSVImportFieldTerminator]];
+ [importLinesTerminatedField setStringValue:[prefs objectForKey:SPCSVImportLineTerminator]];
+ [importFieldsEscapedField setStringValue:[prefs objectForKey:SPCSVImportFieldEscapeCharacter]];
+ [importFieldsEnclosedField setStringValue:[prefs objectForKey:SPCSVImportFieldEnclosedBy]];
+ [importFieldNamesSwitch setState:[[prefs objectForKey:SPCSVImportFirstLineIsHeader] boolValue]];
+ [importFromClipboardAccessoryView addSubview:importCSVView];
+
+ [NSApp beginSheet:importFromClipboardSheet
+ modalForWindow:[tableDocumentInstance parentWindow]
+ modalDelegate:self
+ didEndSelector:@selector(openPanelDidEnd:returnCode:contextInfo:)
+ contextInfo:@"importFromClipboard"];
+}
+
+/**
+ * Invoked when user clicks on an import menuitem.
+ */
+- (void)importFile
+{
+ // prepare open panel and accessory view
+ NSOpenPanel *openPanel = [NSOpenPanel openPanel];
+
+ // Preset the accessory view with prefs defaults
+ [importFieldsTerminatedField setStringValue:[prefs objectForKey:SPCSVImportFieldTerminator]];
+ [importLinesTerminatedField setStringValue:[prefs objectForKey:SPCSVImportLineTerminator]];
+ [importFieldsEscapedField setStringValue:[prefs objectForKey:SPCSVImportFieldEscapeCharacter]];
+ [importFieldsEnclosedField setStringValue:[prefs objectForKey:SPCSVImportFieldEnclosedBy]];
+ [importFieldNamesSwitch setState:[[prefs objectForKey:SPCSVImportFirstLineIsHeader] boolValue]];
+
+ [openPanel setAccessoryView:importCSVView];
+ [openPanel setDelegate:self];
+ if ([prefs valueForKey:@"importFormatPopupValue"]) {
+ [importFormatPopup selectItemWithTitle:[prefs valueForKey:@"importFormatPopupValue"]];
+ [self changeFormat:self];
+ }
+
+ // Show openPanel
+ [openPanel beginSheetForDirectory:[prefs objectForKey:@"openPath"]
+ file:[lastFilename lastPathComponent]
+ modalForWindow:[tableDocumentInstance parentWindow]
+ modalDelegate:self
+ didEndSelector:@selector(openPanelDidEnd:returnCode:contextInfo:)
+ contextInfo:nil];
+}
+
+/**
+ * Shows/hides the CSV options accessory view based on the selected format.
+ */
+- (IBAction)changeFormat:(id)sender
+{
+ [importCSVBox setHidden:![[[importFormatPopup selectedItem] title] isEqualToString:@"CSV"]];
+}
+
+/**
+ * Starts the import process on a background thread.
+ */
+- (void)importBackgroundProcess:(NSString*)filename
+{
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ NSString *fileType = [[importFormatPopup selectedItem] title];
+
+ // Use the appropriate processing function for the file type
+ if ([fileType isEqualToString:@"SQL"])
+ [self importSQLFile:filename];
+ else if ([fileType isEqualToString:@"CSV"])
+ [self importCSVFile:filename];
+
+ [pool release];
+}
+
+/**
+ *
+ */
+- (void)importSQLFile:(NSString *)filename
+{
+ NSAutoreleasePool *importPool;
+ SPFileHandle *sqlFileHandle;
+ NSMutableData *sqlDataBuffer;
+ const unsigned char *sqlDataBufferBytes;
+ NSData *fileChunk;
+ NSString *sqlString;
+ SPSQLParser *sqlParser;
+ NSString *query;
+ NSMutableString *errors = [NSMutableString string];
+ NSInteger fileChunkMaxLength = 1024 * 1024;
+ NSUInteger fileTotalLength = 0;
+ NSUInteger fileProcessedLength = 0;
+ NSInteger queriesPerformed = 0;
+ NSInteger dataBufferLength = 0;
+ NSInteger dataBufferPosition = 0;
+ NSInteger dataBufferLastQueryEndPosition = 0;
+ BOOL fileIsCompressed;
+ BOOL importSQLAsUTF8 = YES;
+ BOOL allDataRead = NO;
+ NSStringEncoding sqlEncoding = NSUTF8StringEncoding;
+ NSCharacterSet *whitespaceAndNewlineCharset = [NSCharacterSet whitespaceAndNewlineCharacterSet];
+
+ // Start the notification timer to allow notifications to be shown even if frontmost for long queries
+ [[SPGrowlController sharedGrowlController] setVisibilityForNotificationName:@"Import Finished"];
+
+ // Open a filehandle for the SQL file
+ sqlFileHandle = [SPFileHandle fileHandleForReadingAtPath:filename];
+ if (!sqlFileHandle) {
+ SPBeginAlertSheet(NSLocalizedString(@"Import Error title", @"Import Error"),
+ NSLocalizedString(@"OK button label", @"OK button"),
+ nil, nil, [tableDocumentInstance parentWindow], self, nil, nil,
+ NSLocalizedString(@"SQL file open error", @"The SQL file you selected could not be found or read."));
+ if([filename hasPrefix:SPImportClipboardTempFileNamePrefix])
+ [[NSFileManager defaultManager] removeItemAtPath:filename error:nil];
+ return;
+ }
+ fileIsCompressed = [sqlFileHandle isCompressed];
+
+ // Grab the file length
+ fileTotalLength = [[[[NSFileManager defaultManager] attributesOfItemAtPath:filename error:NULL] objectForKey:NSFileSize] longLongValue];
+ if (!fileTotalLength) fileTotalLength = 1;
+
+ // Reset progress interface
+ [errorsView setString:@""];
+ [[singleProgressTitle onMainThread] setStringValue:NSLocalizedString(@"Importing SQL", @"text showing that the application is importing SQL")];
+ [[singleProgressText onMainThread] setStringValue:NSLocalizedString(@"Reading...", @"text showing that app is reading dump")];
+ [[singleProgressBar onMainThread] setIndeterminate:NO];
+ [[singleProgressBar onMainThread] setMaxValue:fileTotalLength];
+ [[singleProgressBar onMainThread] setUsesThreadedAnimation:YES];
+ [[singleProgressBar onMainThread] startAnimation:self];
+
+ // Open the progress sheet
+ [[NSApp onMainThread] beginSheet:singleProgressSheet modalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:nil contextInfo:nil];
+ [[singleProgressSheet onMainThread] makeKeyWindow];
+
+ [tableDocumentInstance setQueryMode:SPImportExportQueryMode];
+
+ // Read in the file in a loop
+ sqlParser = [[SPSQLParser alloc] init];
+ [sqlParser setDelimiterSupport:YES];
+ sqlDataBuffer = [[NSMutableData alloc] init];
+ importPool = [[NSAutoreleasePool alloc] init];
+ while (1) {
+ if (progressCancelled) break;
+
+ @try {
+ fileChunk = [sqlFileHandle readDataOfLength:fileChunkMaxLength];
+ }
+
+ // Report file read errors, and bail
+ @catch (NSException *exception) {
+ [self closeAndStopProgressSheet];
+ SPBeginAlertSheet(NSLocalizedString(@"SQL read error title", @"File read error"),
+ NSLocalizedString(@"OK", @"OK button"),
+ nil, nil, [tableDocumentInstance parentWindow], self, nil, nil,
+ [NSString stringWithFormat:NSLocalizedString(@"SQL read error", @"An error occurred when reading the file.\n\nOnly %ld queries were executed.\n\n(%@)"), (long)queriesPerformed, [exception reason]]);
+ [sqlParser release];
+ [sqlDataBuffer release];
+ [importPool drain];
+ [tableDocumentInstance setQueryMode:SPInterfaceQueryMode];
+ if([filename hasPrefix:SPImportClipboardTempFileNamePrefix])
+ [[NSFileManager defaultManager] removeItemAtPath:filename error:nil];
+ return;
+ }
+
+ // If no data returned, end of file - set a marker to ensure full processing
+ if (!fileChunk || ![fileChunk length]) {
+ allDataRead = YES;
+
+ // Otherwise add the data to the read/parse buffer
+ } else {
+ [sqlDataBuffer appendData:fileChunk];
+ }
+
+ // Step through the data buffer, identifying line endings to parse the data with
+ sqlDataBufferBytes = [sqlDataBuffer bytes];
+ dataBufferLength = [sqlDataBuffer length];
+ for ( ; dataBufferPosition < dataBufferLength || allDataRead; dataBufferPosition++) {
+ if (sqlDataBufferBytes[dataBufferPosition] == 0x0A || sqlDataBufferBytes[dataBufferPosition] == 0x0D || allDataRead) {
+
+ // Keep reading through any other line endings
+ while (dataBufferPosition + 1 < dataBufferLength
+ && (sqlDataBufferBytes[dataBufferPosition+1] == 0x0A
+ || sqlDataBufferBytes[dataBufferPosition+1] == 0x0D))
+ {
+ dataBufferPosition++;
+ }
+
+ // Try to generate a NSString with the resulting data
+ if (importSQLAsUTF8) {
+ sqlString = [[NSString alloc] initWithData:[sqlDataBuffer subdataWithRange:NSMakeRange(dataBufferLastQueryEndPosition, dataBufferPosition - dataBufferLastQueryEndPosition)]
+ encoding:NSUTF8StringEncoding];
+ if (!sqlString) {
+ importSQLAsUTF8 = NO;
+ sqlEncoding = [MCPConnection encodingForMySQLEncoding:[[tableDocumentInstance connectionEncoding] UTF8String]];
+ }
+ }
+ if (!importSQLAsUTF8) {
+ sqlString = [[NSString alloc] initWithData:[sqlDataBuffer subdataWithRange:NSMakeRange(dataBufferLastQueryEndPosition, dataBufferPosition - dataBufferLastQueryEndPosition)]
+ encoding:[MCPConnection encodingForMySQLEncoding:[[tableDocumentInstance connectionEncoding] UTF8String]]];
+ if (!sqlString) {
+ [self closeAndStopProgressSheet];
+ SPBeginAlertSheet(NSLocalizedString(@"SQL read error title", @"File read error"),
+ NSLocalizedString(@"OK", @"OK button"),
+ nil, nil, [tableDocumentInstance parentWindow], self, nil, nil,
+ [NSString stringWithFormat:NSLocalizedString(@"SQL encoding read error", @"An error occurred when reading the file, as it could not be read in either UTF-8 or %@.\n\nOnly %ld queries were executed."), [[tableDocumentInstance connectionEncoding] UTF8String], (long)queriesPerformed]);
+ [sqlParser release];
+ [sqlDataBuffer release];
+ [importPool drain];
+ [tableDocumentInstance setQueryMode:SPInterfaceQueryMode];
+ if([filename hasPrefix:SPImportClipboardTempFileNamePrefix])
+ [[NSFileManager defaultManager] removeItemAtPath:filename error:nil];
+ return;
+ }
+ }
+
+ // Add the NSString segment to the SQL parser and release it
+ [sqlParser appendString:sqlString];
+ [sqlString release];
+
+ if (allDataRead) break;
+
+ // Increment the query end position marker
+ dataBufferLastQueryEndPosition = dataBufferPosition;
+ }
+ }
+
+ // Trim the data buffer if part of it was used
+ if (dataBufferLastQueryEndPosition) {
+ [sqlDataBuffer setData:[sqlDataBuffer subdataWithRange:NSMakeRange(dataBufferLastQueryEndPosition, dataBufferLength - dataBufferLastQueryEndPosition)]];
+ dataBufferPosition -= dataBufferLastQueryEndPosition;
+ dataBufferLastQueryEndPosition = 0;
+ }
+
+ // Before entering the following loop, check that we actually have a connection. If not, bail.
+ if (![mySQLConnection isConnected]) {
+ if([filename hasPrefix:SPImportClipboardTempFileNamePrefix])
+ [[NSFileManager defaultManager] removeItemAtPath:filename error:nil];
+ return;
+ }
+
+ // Extract and process any complete SQL queries that can be found in the strings parsed so far
+ while (query = [sqlParser trimAndReturnStringToCharacter:';' trimmingInclusively:YES returningInclusively:NO]) {
+ if (progressCancelled) break;
+ fileProcessedLength += [query lengthOfBytesUsingEncoding:sqlEncoding] + 1;
+
+ // Skip blank or whitespace-only queries to avoid errors
+ query = [query stringByTrimmingCharactersInSet:whitespaceAndNewlineCharset];
+ if (![query length]) continue;
+
+ // Run the query
+ [mySQLConnection queryString:query usingEncoding:sqlEncoding streamingResult:NO];
+
+ // Check for any errors
+ if ([mySQLConnection queryErrored] && ![[mySQLConnection getLastErrorMessage] isEqualToString:@"Query was empty"]) {
+ [errors appendString:[NSString stringWithFormat:NSLocalizedString(@"[ERROR in query %ld] %@\n", @"error text when multiple custom query failed"), (long)(queriesPerformed+1), [mySQLConnection getLastErrorMessage]]];
+ }
+
+ // Increment the processed queries count
+ queriesPerformed++;
+
+ // Update the progress bar
+ if (fileIsCompressed) {
+ [singleProgressBar setDoubleValue:[sqlFileHandle realDataReadLength]];
+ [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Imported %@ of SQL", @"SQL import progress text where total size is unknown"),
+ [NSString stringForByteSize:fileProcessedLength]]];
+ } else {
+ [singleProgressBar setDoubleValue:fileProcessedLength];
+ [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Imported %@ of %@", @"SQL import progress text"),
+ [NSString stringForByteSize:fileProcessedLength], [NSString stringForByteSize:fileTotalLength]]];
+ }
+ }
+
+ // If all the data has been read, break out of the processing loop
+ if (allDataRead) break;
+
+ // Reset the autorelease pool
+ [importPool drain];
+ importPool = [[NSAutoreleasePool alloc] init];
+ }
+
+ // If any text remains in the SQL parser, it's an unterminated query - execute it.
+ query = [sqlParser stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ if ([query length] && !progressCancelled) {
+
+ // Run the query
+ [mySQLConnection queryString:query usingEncoding:sqlEncoding streamingResult:NO];
+
+ // Check for any errors
+ if ([mySQLConnection queryErrored] && ![[mySQLConnection getLastErrorMessage] isEqualToString:@"Query was empty"]) {
+ [errors appendString:[NSString stringWithFormat:NSLocalizedString(@"[ERROR in query %ld] %@\n", @"error text when multiple custom query failed"), (long)(queriesPerformed+1), [mySQLConnection getLastErrorMessage]]];
+ }
+
+ // Increment the processed queries count
+ queriesPerformed++;
+ }
+
+ // Clean up
+ [sqlParser release];
+ [sqlDataBuffer release];
+ [importPool drain];
+ [tableDocumentInstance setQueryMode:SPInterfaceQueryMode];
+ if([filename hasPrefix:SPImportClipboardTempFileNamePrefix])
+ [[NSFileManager defaultManager] removeItemAtPath:filename error:nil];
+
+ // Close progress sheet
+ [self closeAndStopProgressSheet];
+
+ // Display any errors
+ if ([errors length]) {
+ [self showErrorSheetWithMessage:errors];
+ }
+
+ // Update available databases
+ [tableDocumentInstance setDatabases:self];
+
+ // Update current selected database
+ [[tableDocumentInstance onMainThread] refreshCurrentDatabase];
+
+ // Update current database tables
+ [tablesListInstance updateTables:self];
+
+ // Query the structure of all databases in the background
+ [NSThread detachNewThreadSelector:@selector(queryDbStructureWithUserInfo:) toTarget:mySQLConnection withObject:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], @"forceUpdate", nil]];
+
+ // Import finished Growl notification
+ [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Import Finished"
+ description:[NSString stringWithFormat:NSLocalizedString(@"Finished importing %@",@"description for finished importing growl notification"), [filename lastPathComponent]]
+ document:tableDocumentInstance
+ notificationName:@"Import Finished"];
+}
+
+/**
+ *
+ */
+- (void)importCSVFile:(NSString *)filename
+{
+ NSAutoreleasePool *importPool;
+ NSFileHandle *csvFileHandle;
+ NSMutableData *csvDataBuffer;
+ const unsigned char *csvDataBufferBytes;
+ NSData *fileChunk;
+ NSString *csvString;
+ SPCSVParser *csvParser;
+ NSMutableString *query;
+ NSMutableString *errors = [NSMutableString string];
+ NSMutableString *insertBaseString = [NSMutableString string];
+ NSMutableString *insertRemainingBaseString = [NSMutableString string];
+ NSMutableArray *parsedRows = [[NSMutableArray alloc] init];
+ NSMutableArray *parsePositions = [[NSMutableArray alloc] init];
+ NSArray *csvRowArray;
+ NSInteger fileChunkMaxLength = 256 * 1024;
+ NSInteger csvRowsPerQuery = 50;
+ NSUInteger csvRowsThisQuery;
+ NSUInteger fileTotalLength = 0;
+ NSInteger rowsImported = 0;
+ NSInteger dataBufferLength = 0;
+ NSInteger dataBufferPosition = 0;
+ NSInteger dataBufferLastQueryEndPosition = 0;
+ NSInteger i;
+ BOOL allDataRead = NO;
+ BOOL insertBaseStringHasEntries;
+
+ NSStringEncoding csvEncoding = [MCPConnection encodingForMySQLEncoding:[[tableDocumentInstance connectionEncoding] UTF8String]];
+
+ fieldMappingArray = nil;
+ fieldMappingGlobalValueArray = nil;
+
+ // Start the notification timer to allow notifications to be shown even if frontmost for long queries
+ [[SPGrowlController sharedGrowlController] setVisibilityForNotificationName:@"Import Finished"];
+
+ // Open a filehandle for the CSV file
+ csvFileHandle = [NSFileHandle fileHandleForReadingAtPath:filename];
+ if (!csvFileHandle) {
+ SPBeginAlertSheet(NSLocalizedString(@"Import Error title", @"Import Error"),
+ NSLocalizedString(@"OK button label", @"OK button"),
+ nil, nil, [tableDocumentInstance parentWindow], self, nil, nil,
+ NSLocalizedString(@"CSV file open error", @"The CSV file you selected could not be found or read."));
+ if([filename hasPrefix:SPImportClipboardTempFileNamePrefix])
+ [[NSFileManager defaultManager] removeItemAtPath:filename error:nil];
+ return;
+ }
+
+ // Grab the file length
+ fileTotalLength = [[[[NSFileManager defaultManager] attributesOfItemAtPath:filename error:NULL] objectForKey:NSFileSize] longLongValue];
+ if (!fileTotalLength) fileTotalLength = 1;
+
+ // Reset progress interface
+ [errorsView setString:@""];
+ [[singleProgressTitle onMainThread] setStringValue:NSLocalizedString(@"Importing CSV", @"text showing that the application is importing CSV")];
+ [[singleProgressText onMainThread] setStringValue:NSLocalizedString(@"Reading...", @"text showing that app is reading dump")];
+ [[singleProgressBar onMainThread] setIndeterminate:YES];
+ [[singleProgressBar onMainThread] setUsesThreadedAnimation:YES];
+ [[singleProgressBar onMainThread] startAnimation:self];
+
+ // Open the progress sheet
+ [[NSApp onMainThread] beginSheet:singleProgressSheet modalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:nil contextInfo:nil];
+ [[singleProgressSheet onMainThread] makeKeyWindow];
+
+ [tableDocumentInstance setQueryMode:SPImportExportQueryMode];
+
+ // Read in the file in a loop. The loop actually needs to perform three tasks: read in
+ // CSV data and parse them into row arrays; present the field mapping interface once it
+ // has some data to show within the interface; and use the field mapping data to construct
+ // and send queries to the server. The loop is mainly to perform the first of these; the
+ // other two must therefore be performed where possible.
+ csvParser = [[SPCSVParser alloc] init];
+
+ // Store settings in prefs
+ [prefs setObject:[importFieldsEnclosedField stringValue] forKey:SPCSVImportFieldEnclosedBy];
+ [prefs setObject:[importFieldsEscapedField stringValue] forKey:SPCSVImportFieldEscapeCharacter];
+ [prefs setObject:[importLinesTerminatedField stringValue] forKey:SPCSVImportLineTerminator];
+ [prefs setObject:[importFieldsTerminatedField stringValue] forKey:SPCSVImportFieldTerminator];
+ [prefs setBool:[importFieldNamesSwitch state] forKey:SPCSVImportFirstLineIsHeader];
+
+ // Take CSV import setting from accessory view
+ [csvParser setFieldTerminatorString:[importFieldsTerminatedField stringValue] convertDisplayStrings:YES];
+ [csvParser setLineTerminatorString:[importLinesTerminatedField stringValue] convertDisplayStrings:YES];
+ [csvParser setFieldQuoteString:[importFieldsEnclosedField stringValue] convertDisplayStrings:YES];
+ [csvParser setEscapeString:[importFieldsEscapedField stringValue] convertDisplayStrings:YES];
+ [csvParser setNullReplacementString:[prefs objectForKey:SPNullValue]];
+
+ csvDataBuffer = [[NSMutableData alloc] init];
+ importPool = [[NSAutoreleasePool alloc] init];
+ while (1) {
+ if (progressCancelled) break;
+
+ @try {
+ fileChunk = [csvFileHandle readDataOfLength:fileChunkMaxLength];
+ }
+
+ // Report file read errors, and bail
+ @catch (NSException *exception) {
+ [self closeAndStopProgressSheet];
+ SPBeginAlertSheet(NSLocalizedString(@"CSV read error title", @"File read error"),
+ NSLocalizedString(@"OK", @"OK button"),
+ nil, nil, [tableDocumentInstance parentWindow], self, nil, nil,
+ [NSString stringWithFormat:NSLocalizedString(@"CSV read error", @"An error occurred when reading the file.\n\nOnly %ld rows were imported.\n\n(%@)"), (long)rowsImported, [exception reason]]);
+ [csvParser release];
+ [csvDataBuffer release];
+ [parsedRows release];
+ [parsePositions release];
+ if(csvImportTailString) [csvImportTailString release], csvImportTailString = nil;
+ if(csvImportHeaderString) [csvImportHeaderString release], csvImportHeaderString = nil;
+ if(fieldMappingArray) [fieldMappingArray release], fieldMappingArray = nil;
+ if(fieldMappingGlobalValueArray) [fieldMappingGlobalValueArray release], fieldMappingGlobalValueArray = nil;
+ if(fieldMappingTableColumnNames) [fieldMappingTableColumnNames release], fieldMappingTableColumnNames = nil;
+ if(fieldMappingTableDefaultValues) [fieldMappingTableDefaultValues release], fieldMappingTableDefaultValues = nil;
+ if(fieldMapperOperator) [fieldMapperOperator release], fieldMapperOperator = nil;
+ [importPool drain];
+ [tableDocumentInstance setQueryMode:SPInterfaceQueryMode];
+ if([filename hasPrefix:SPImportClipboardTempFileNamePrefix])
+ [[NSFileManager defaultManager] removeItemAtPath:filename error:nil];
+ return;
+ }
+
+ // If no data returned, end of file - set a marker to ensure full processing
+ if (!fileChunk || ![fileChunk length]) {
+ allDataRead = YES;
+
+ // Otherwise add the data to the read/parse buffer
+ } else {
+ [csvDataBuffer appendData:fileChunk];
+ }
+
+ // Step through the data buffer, identifying line endings to parse the data with
+ csvDataBufferBytes = [csvDataBuffer bytes];
+ dataBufferLength = [csvDataBuffer length];
+ for ( ; dataBufferPosition < dataBufferLength || allDataRead; dataBufferPosition++) {
+ if (csvDataBufferBytes[dataBufferPosition] == 0x0A || csvDataBufferBytes[dataBufferPosition] == 0x0D || allDataRead) {
+
+ // Keep reading through any other line endings
+ while (dataBufferPosition + 1 < dataBufferLength
+ && (csvDataBufferBytes[dataBufferPosition+1] == 0x0A
+ || csvDataBufferBytes[dataBufferPosition+1] == 0x0D))
+ {
+ dataBufferPosition++;
+ }
+
+ // Try to generate a NSString with the resulting data
+ csvString = [[NSString alloc] initWithData:[csvDataBuffer subdataWithRange:NSMakeRange(dataBufferLastQueryEndPosition, dataBufferPosition - dataBufferLastQueryEndPosition)] encoding:csvEncoding];
+ if (!csvString) {
+ [self closeAndStopProgressSheet];
+ SPBeginAlertSheet(NSLocalizedString(@"CSV read error title", @"File read error"),
+ NSLocalizedString(@"OK", @"OK button"),
+ nil, nil, [tableDocumentInstance parentWindow], self, nil, nil,
+ [NSString stringWithFormat:NSLocalizedString(@"CSV encoding read error", @"An error occurred when reading the file, as it could not be read using %@.\n\nOnly %ld rows were imported."), [[tableDocumentInstance connectionEncoding] UTF8String], (long)rowsImported]);
+ [csvParser release];
+ [csvDataBuffer release];
+ [parsedRows release];
+ [parsePositions release];
+ if(csvImportTailString) [csvImportTailString release], csvImportTailString = nil;
+ if(csvImportHeaderString) [csvImportHeaderString release], csvImportHeaderString = nil;
+ if(fieldMappingArray) [fieldMappingArray release], fieldMappingArray = nil;
+ if(fieldMappingGlobalValueArray) [fieldMappingGlobalValueArray release], fieldMappingGlobalValueArray = nil;
+ if(fieldMappingTableColumnNames) [fieldMappingTableColumnNames release], fieldMappingTableColumnNames = nil;
+ if(fieldMappingTableDefaultValues) [fieldMappingTableDefaultValues release], fieldMappingTableDefaultValues = nil;
+ if(fieldMapperOperator) [fieldMapperOperator release], fieldMapperOperator = nil;
+ [importPool drain];
+ [tableDocumentInstance setQueryMode:SPInterfaceQueryMode];
+ if([filename hasPrefix:SPImportClipboardTempFileNamePrefix])
+ [[NSFileManager defaultManager] removeItemAtPath:filename error:nil];
+ return;
+ }
+
+ // Add the NSString segment to the CSV parser and release it
+ [csvParser appendString:csvString];
+ [csvString release];
+
+ if (allDataRead) break;
+
+ // Increment the buffer end position marker
+ dataBufferLastQueryEndPosition = dataBufferPosition;
+ }
+ }
+
+ // Trim the data buffer if part of it was used
+ if (dataBufferLastQueryEndPosition) {
+ [csvDataBuffer setData:[csvDataBuffer subdataWithRange:NSMakeRange(dataBufferLastQueryEndPosition, dataBufferLength - dataBufferLastQueryEndPosition)]];
+ dataBufferPosition -= dataBufferLastQueryEndPosition;
+ dataBufferLastQueryEndPosition = 0;
+ }
+
+ // Extract and process any full CSV rows found so far. Also trigger processing if all
+ // rows have been read, in order to ensure short files are still processed.
+ while ((csvRowArray = [csvParser getRowAsArrayAndTrimString:YES stringIsComplete:allDataRead]) || (allDataRead && [parsedRows count])) {
+
+ // If valid, add the row array and length to local storage
+ if (csvRowArray) {
+ [parsedRows addObject:csvRowArray];
+ [parsePositions addObject:[NSNumber numberWithUnsignedInteger:[csvParser totalLengthParsed]]];
+ }
+
+ // If we have no field mapping array, and either the first hundred rows or all
+ // the rows, request the field mapping from the user.
+ if (!fieldMappingArray
+ && ([parsedRows count] >= 100 || (!csvRowArray && allDataRead)))
+ {
+ [self closeAndStopProgressSheet];
+ if (![self buildFieldMappingArrayWithData:parsedRows isPreview:!allDataRead ofSoureFile:filename]) {
+ [csvParser release];
+ [csvDataBuffer release];
+ [parsedRows release];
+ [parsePositions release];
+ if(csvImportTailString) [csvImportTailString release], csvImportTailString = nil;
+ if(csvImportHeaderString) [csvImportHeaderString release], csvImportHeaderString = nil;
+ if(fieldMappingArray) [fieldMappingArray release], fieldMappingArray = nil;
+ if(fieldMappingGlobalValueArray) [fieldMappingGlobalValueArray release], fieldMappingGlobalValueArray = nil;
+ if(fieldMappingTableColumnNames) [fieldMappingTableColumnNames release], fieldMappingTableColumnNames = nil;
+ if(fieldMappingTableDefaultValues) [fieldMappingTableDefaultValues release], fieldMappingTableDefaultValues = nil;
+ if(fieldMapperOperator) [fieldMapperOperator release], fieldMapperOperator = nil;
+ [importPool drain];
+ [tableDocumentInstance setQueryMode:SPInterfaceQueryMode];
+ if([filename hasPrefix:SPImportClipboardTempFileNamePrefix])
+ [[NSFileManager defaultManager] removeItemAtPath:filename error:nil];
+ return;
+ }
+
+ // Reset progress interface and open the progress sheet
+ [[singleProgressBar onMainThread] setIndeterminate:NO];
+ [[singleProgressBar onMainThread] setMaxValue:fileTotalLength];
+ [[singleProgressBar onMainThread] startAnimation:self];
+ [[NSApp onMainThread] beginSheet:singleProgressSheet modalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:nil contextInfo:nil];
+ [[singleProgressSheet onMainThread] makeKeyWindow];
+
+ // Set up the field names import string for INSERT or REPLACE INTO
+ [insertBaseString appendString:csvImportHeaderString];
+ if(!importMethodIsUpdate) {
+ [insertBaseString appendString:[selectedTableTarget backtickQuotedString]];
+ [insertBaseString appendString:@" ("];
+ insertBaseStringHasEntries = NO;
+ for (i = 0; i < [fieldMappingArray count]; i++) {
+ if ([NSArrayObjectAtIndex(fieldMapperOperator, i) integerValue] == 0) {
+ if (insertBaseStringHasEntries) [insertBaseString appendString:@","];
+ else insertBaseStringHasEntries = YES;
+ [insertBaseString appendString:[NSArrayObjectAtIndex(fieldMappingTableColumnNames, i) backtickQuotedString]];
+ }
+ }
+ [insertBaseString appendString:@") VALUES\n"];
+ }
+
+ // Remove the header row from the data set if appropriate
+ if ([importFieldNamesSwitch state] == NSOnState) {
+ [parsedRows removeObjectAtIndex:0];
+ [parsePositions removeObjectAtIndex:0];
+ }
+ }
+ if (!fieldMappingArray) continue;
+
+ // Before entering the following loop, check that we actually have a connection. If not, bail.
+ if (![mySQLConnection isConnected]) {
+ [self closeAndStopProgressSheet];
+ [csvParser release];
+ [csvDataBuffer release];
+ [parsedRows release];
+ [parsePositions release];
+ if(csvImportTailString) [csvImportTailString release], csvImportTailString = nil;
+ if(csvImportHeaderString) [csvImportHeaderString release], csvImportHeaderString = nil;
+ if(fieldMappingArray) [fieldMappingArray release], fieldMappingArray = nil;
+ if(fieldMappingGlobalValueArray) [fieldMappingGlobalValueArray release], fieldMappingGlobalValueArray = nil;
+ if(fieldMappingTableColumnNames) [fieldMappingTableColumnNames release], fieldMappingTableColumnNames = nil;
+ if(fieldMappingTableDefaultValues) [fieldMappingTableDefaultValues release], fieldMappingTableDefaultValues = nil;
+ if(fieldMapperOperator) [fieldMapperOperator release], fieldMapperOperator = nil;
+ [importPool drain];
+ [tableDocumentInstance setQueryMode:SPInterfaceQueryMode];
+ if([filename hasPrefix:SPImportClipboardTempFileNamePrefix])
+ [[NSFileManager defaultManager] removeItemAtPath:filename error:nil];
+ return;
+ }
+
+ // If we have more than the csvRowsPerQuery amount, or if we're at the end of the
+ // available data, construct and run a query.
+ while ([parsedRows count] >= csvRowsPerQuery
+ || (!csvRowArray && allDataRead && [parsedRows count]))
+ {
+ if (progressCancelled) break;
+ csvRowsThisQuery = 0;
+ if(!importMethodIsUpdate) {
+ query = [[NSMutableString alloc] initWithString:insertBaseString];
+ for (i = 0; i < csvRowsPerQuery && i < [parsedRows count]; i++) {
+ if (i > 0) [query appendString:@",\n"];
+ [query appendString:[[self mappedValueStringForRowArray:[parsedRows objectAtIndex:i]] description]];
+ csvRowsThisQuery++;
+ if ([query length] > 250000) break;
+ }
+
+ // Perform the query
+ if(csvImportMethodHasTail)
+ [mySQLConnection queryString:[NSString stringWithFormat:@"%@ %@", query, csvImportTailString]];
+ else
+ [mySQLConnection queryString:query];
+ [query release];
+ } else {
+ if(insertRemainingRowsAfterUpdate) {
+ [insertRemainingBaseString setString:@"INSERT INTO "];
+ [insertRemainingBaseString appendString:[selectedTableTarget backtickQuotedString]];
+ [insertRemainingBaseString appendString:@" ("];
+ insertBaseStringHasEntries = NO;
+ for (i = 0; i < [fieldMappingArray count]; i++) {
+ if ([NSArrayObjectAtIndex(fieldMapperOperator, i) integerValue] == 0) {
+ if (insertBaseStringHasEntries) [insertBaseString appendString:@","];
+ else insertBaseStringHasEntries = YES;
+ [insertRemainingBaseString appendString:[NSArrayObjectAtIndex(fieldMappingTableColumnNames, i) backtickQuotedString]];
+ }
+ }
+ [insertRemainingBaseString appendString:@") VALUES\n"];
+ }
+ for (i = 0; i < [parsedRows count]; i++) {
+ if (progressCancelled) break;
+
+ query = [[NSMutableString alloc] initWithString:insertBaseString];
+ [query appendString:[self mappedUpdateSetStatementStringForRowArray:[parsedRows objectAtIndex:i]]];
+
+ // Perform the query
+ if(csvImportMethodHasTail)
+ [mySQLConnection queryString:[NSString stringWithFormat:@"%@ %@", query, csvImportTailString]];
+ else
+ [mySQLConnection queryString:query];
+ [query release];
+
+ if ([mySQLConnection queryErrored]) {
+ [tableDocumentInstance showConsole:nil];
+ [errors appendString:[NSString stringWithFormat:
+ NSLocalizedString(@"[ERROR in row %ld] %@\n", @"error text when reading of csv file gave errors"),
+ (long)(rowsImported+1),[mySQLConnection getLastErrorMessage]]];
+ }
+
+ if ( insertRemainingRowsAfterUpdate && ![mySQLConnection affectedRows]) {
+ query = [[NSMutableString alloc] initWithString:insertRemainingBaseString];
+ [query appendString:[self mappedValueStringForRowArray:[parsedRows objectAtIndex:i]]];
+
+ // Perform the query
+ if(csvImportMethodHasTail)
+ [mySQLConnection queryString:[NSString stringWithFormat:@"%@ %@", query, csvImportTailString]];
+ else
+ [mySQLConnection queryString:query];
+ [query release];
+
+ if ([mySQLConnection queryErrored]) {
+ [errors appendString:[NSString stringWithFormat:
+ NSLocalizedString(@"[ERROR in row %ld] %@\n", @"error text when reading of csv file gave errors"),
+ (long)(rowsImported+1),[mySQLConnection getLastErrorMessage]]];
+ }
+ }
+
+ rowsImported++;
+ csvRowsThisQuery++;
+ [singleProgressBar setDoubleValue:[[parsePositions objectAtIndex:i] doubleValue]];
+ [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Imported %@ of %@", @"SQL import progress text"),
+ [NSString stringForByteSize:[[parsePositions objectAtIndex:i] longValue]], [NSString stringForByteSize:fileTotalLength]]];
+ }
+ }
+ // If an error occurred, run the queries individually to get exact line errors
+ if (!importMethodIsUpdate && [mySQLConnection queryErrored]) {
+ [tableDocumentInstance showConsole:nil];
+ for (i = 0; i < csvRowsThisQuery; i++) {
+ if (progressCancelled) break;
+ query = [[NSMutableString alloc] initWithString:insertBaseString];
+ [query appendString:[self mappedValueStringForRowArray:[parsedRows objectAtIndex:i]]];
+
+ // Perform the query
+ if(csvImportMethodHasTail)
+ [mySQLConnection queryString:[NSString stringWithFormat:@"%@ %@", query, csvImportTailString]];
+ else
+ [mySQLConnection queryString:query];
+ [query release];
+
+ if ([mySQLConnection queryErrored]) {
+ [errors appendString:[NSString stringWithFormat:
+ NSLocalizedString(@"[ERROR in row %ld] %@\n", @"error text when reading of csv file gave errors"),
+ (long)(rowsImported+1),[mySQLConnection getLastErrorMessage]]];
+ }
+ rowsImported++;
+ [singleProgressBar setDoubleValue:[[parsePositions objectAtIndex:i] doubleValue]];
+ [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Imported %@ of %@", @"SQL import progress text"),
+ [NSString stringForByteSize:[[parsePositions objectAtIndex:i] longValue]], [NSString stringForByteSize:fileTotalLength]]];
+ }
+ } else {
+ rowsImported += csvRowsThisQuery;
+ [singleProgressBar setDoubleValue:[[parsePositions objectAtIndex:csvRowsThisQuery-1] doubleValue]];
+ [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Imported %@ of %@", @"SQL import progress text"),
+ [NSString stringForByteSize:[[parsePositions objectAtIndex:csvRowsThisQuery-1] longValue]], [NSString stringForByteSize:fileTotalLength]]];
+ }
+
+ // Update the arrays
+ [parsedRows removeObjectsInRange:NSMakeRange(0, csvRowsThisQuery)];
+ [parsePositions removeObjectsInRange:NSMakeRange(0, csvRowsThisQuery)];
+ }
+ }
+
+ // If all the data has been read, break out of the processing loop
+ if (allDataRead) break;
+
+ // Reset the autorelease pool
+ [importPool drain];
+ importPool = [[NSAutoreleasePool alloc] init];
+ }
+
+ // Clean up
+ [csvParser release];
+ [csvDataBuffer release];
+ [parsedRows release];
+ [parsePositions release];
+ if(csvImportTailString) [csvImportTailString release], csvImportTailString = nil;
+ if(csvImportHeaderString) [csvImportHeaderString release], csvImportHeaderString = nil;
+ if(fieldMappingArray) [fieldMappingArray release], fieldMappingArray = nil;
+ if(fieldMappingGlobalValueArray) [fieldMappingGlobalValueArray release], fieldMappingGlobalValueArray = nil;
+ if(fieldMappingTableColumnNames) [fieldMappingTableColumnNames release], fieldMappingTableColumnNames = nil;
+ if(fieldMappingTableDefaultValues) [fieldMappingTableDefaultValues release], fieldMappingTableDefaultValues = nil;
+ if(fieldMapperOperator) [fieldMapperOperator release], fieldMapperOperator = nil;
+ [importPool drain];
+ [tableDocumentInstance setQueryMode:SPInterfaceQueryMode];
+ if([filename hasPrefix:SPImportClipboardTempFileNamePrefix])
+ [[NSFileManager defaultManager] removeItemAtPath:filename error:nil];
+
+ // Close progress sheet
+ [self closeAndStopProgressSheet];
+
+ // Display any errors
+ if ([errors length]) {
+ [self showErrorSheetWithMessage:errors];
+ }
+
+ // Import finished Growl notification
+ [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Import Finished"
+ description:[NSString stringWithFormat:NSLocalizedString(@"Finished importing %@",@"description for finished importing growl notification"), [filename lastPathComponent]]
+ document:tableDocumentInstance
+ notificationName:@"Import Finished"];
+
+ // If the table selected for import is also selected in the content view,
+ // update the content view - on the main thread to avoid crashes.
+ if ([tablesListInstance tableName] && [selectedTableTarget isEqualToString:[tablesListInstance tableName]]) {
+ if ([[tableDocumentInstance selectedToolbarItemIdentifier] isEqualToString:SPMainToolbarTableContent]) {
+ [tableContentInstance performSelectorOnMainThread:@selector(reloadTable:) withObject:nil waitUntilDone:YES];
+ } else {
+ [tablesListInstance setContentRequiresReload:YES];
+ }
+ }
+}
+
+/**
+ *
+ */
+- (void)openPanelDidEnd:(id)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo
+{
+
+ // if contextInfo == nil NSOpenPanel else importFromClipboardPanel
+
+ // save values to preferences
+ if(contextInfo == nil)
+ [prefs setObject:[(NSOpenPanel*)sheet directory] forKey:@"openPath"];
+ else
+ [importFromClipboardTextView setString:@""];
+
+ [prefs setObject:[[importFormatPopup selectedItem] title] forKey:@"importFormatPopupValue"];
+
+ // close NSOpenPanel sheet
+ if(contextInfo == nil)
+ [sheet orderOut:self];
+
+ // check if user canceled
+ if (returnCode != NSOKButton)
+ return;
+
+ // Reset progress cancelled from any previous runs
+ progressCancelled = NO;
+
+ NSString *importFileName;
+
+ // File path from NSOpenPanel
+ if(contextInfo == nil)
+ {
+ if(lastFilename) [lastFilename release]; lastFilename = nil;
+ lastFilename = [[NSString stringWithString:[(NSOpenPanel*)sheet filename]] retain];
+ importFileName = [NSString stringWithString:lastFilename];
+ if(lastFilename == nil || ![lastFilename length]) {
+ NSBeep();
+ return;
+ }
+ }
+
+ // Import from Clipboard
+ else
+ {
+ importFileName = [NSString stringWithFormat:@"%@%@", SPImportClipboardTempFileNamePrefix,
+ [[NSDate date] descriptionWithCalendarFormat:@"%H%M%S"
+ timeZone:nil
+ locale:[[NSUserDefaults standardUserDefaults] dictionaryRepresentation]]];
+
+ // Write clipboard content to temp file using the connection encoding
+
+ NSStringEncoding encoding;
+ if ([[[importFormatPopup selectedItem] title] isEqualToString:@"SQL"])
+ encoding = NSUTF8StringEncoding;
+ else
+ encoding = [MCPConnection encodingForMySQLEncoding:[[tableDocumentInstance connectionEncoding] UTF8String]];
+
+ if(![[[NSPasteboard generalPasteboard] stringForType:NSStringPboardType] writeToFile:importFileName atomically:NO encoding:encoding error:nil]) {
+ NSBeep();
+ NSLog(@"Couldn't write clipboard content to temporary file.");
+ return;
+ }
+ }
+
+ if(importFileName == nil) return;
+
+ // begin import process
+ [NSThread detachNewThreadSelector:@selector(importBackgroundProcess:) toTarget:self withObject:importFileName];
+}
+
+/**
+ *
+ */
+- (void)startSQLImportProcessWithFile:(NSString *)filename
+{
+ [importFormatPopup selectItemWithTitle:@"SQL"];
+ [NSThread detachNewThreadSelector:@selector(importBackgroundProcess:) toTarget:self withObject:filename];
+}
+
+/**
+ * Sets up the field mapping array, and asks the user to provide a field mapping to an
+ * appropriate table; on success, constructs the field mapping array into the global variable,
+ * and returns true. On failure, displays error messages itself, and returns false.
+ * Takes an array of data to show when selecting the field mapping, and an indicator of whether
+ * that dataset is complete or a preview of the full data set.
+ */
+- (BOOL) buildFieldMappingArrayWithData:(NSArray *)importData isPreview:(BOOL)dataIsPreviewData ofSoureFile:(NSString*)filename
+{
+
+ // Ensure data was provided, or alert than an import error occurred and return false.
+ if (![importData count]) {
+ [self closeAndStopProgressSheet];
+ SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"),
+ NSLocalizedString(@"OK", @"OK button"),
+ nil, nil,
+ [tableDocumentInstance parentWindow], self,
+ nil, nil,
+ NSLocalizedString(@"Could not parse file as CSV", @"Error when we can't parse/split file as CSV")
+ );
+ return FALSE;
+ }
+
+ // Sanity check the first row of the CSV to prevent hang loops caused by wrong line ending entry
+ if ([[importData objectAtIndex:0] count] > 512) {
+ [self closeAndStopProgressSheet];
+ SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"),
+ NSLocalizedString(@"OK", @"OK button"),
+ nil, nil,
+ [tableDocumentInstance parentWindow], self,
+ nil, nil,
+ NSLocalizedString(@"The CSV was read as containing more than 512 columns, more than the maximum columns permitted for speed reasons by Sequel Pro.\n\nThis usually happens due to errors reading the CSV; please double-check the CSV to be imported and the line endings and escape characters at the bottom of the CSV selection dialog.", @"Error when CSV appears to have too many columns to import, probably due to line ending mismatch")
+ );
+ return FALSE;
+ }
+ fieldMappingImportArrayIsPreview = dataIsPreviewData;
+
+ // If there's no tables to select, error
+ if (![[tablesListInstance allTableNames] count]) {
+ [self closeAndStopProgressSheet];
+ SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"),
+ NSLocalizedString(@"OK", @"OK button"),
+ nil, nil,
+ [tableDocumentInstance parentWindow], self,
+ nil, nil,
+ NSLocalizedString(@"Can't import CSV data into a database without any tables!", @"error text when trying to import csv data, but we have no tables in the db")
+ );
+ return FALSE;
+ }
+
+ // Set the import array
+ if (fieldMappingImportArray) [fieldMappingImportArray release];
+ fieldMappingImportArray = [[NSArray alloc] initWithArray:importData];
+ numberOfImportDataColumns = [[importData objectAtIndex:0] count];
+
+ fieldMapperSheetStatus = 1;
+ fieldMappingArrayHasGlobalVariables = NO;
+
+ // Init the field mapper controller
+ fieldMapperController = [[SPFieldMapperController alloc] initWithDelegate:self];
+ [fieldMapperController setConnection:mySQLConnection];
+ [fieldMapperController setSourcePath:filename];
+ [fieldMapperController setImportDataArray:fieldMappingImportArray hasHeader:[importFieldNamesSwitch state] isPreview:fieldMappingImportArrayIsPreview];
+
+ // Show field mapper sheet and set the focus to it
+ [[NSApp onMainThread] beginSheet:[fieldMapperController window]
+ modalForWindow:[tableDocumentInstance parentWindow]
+ modalDelegate:self
+ didEndSelector:@selector(fieldMapperDidEndSheet:returnCode:contextInfo:)
+ contextInfo:nil];
+
+ [[[fieldMapperController window] onMainThread] makeKeyWindow];
+
+ // Wait for field mapper sheet
+ while (fieldMapperSheetStatus == 1)
+ usleep(100000);
+
+ // Get mapping settings and preset some global variables
+ fieldMapperOperator = [[NSArray arrayWithArray:[fieldMapperController fieldMapperOperator]] retain];
+ fieldMappingArray = [[NSArray arrayWithArray:[fieldMapperController fieldMappingArray]] retain];
+ selectedTableTarget = [NSString stringWithString:[fieldMapperController selectedTableTarget]];
+ selectedImportMethod = [NSString stringWithString:[fieldMapperController selectedImportMethod]];
+ fieldMappingTableColumnNames = [[NSArray arrayWithArray:[fieldMapperController fieldMappingTableColumnNames]] retain];
+ fieldMappingGlobalValueArray = [[NSArray arrayWithArray:[fieldMapperController fieldMappingGlobalValueArray]] retain];
+ fieldMappingTableDefaultValues = [[NSArray arrayWithArray:[fieldMapperController fieldMappingTableDefaultValues]] retain];
+ csvImportHeaderString = [[NSString stringWithString:[fieldMapperController importHeaderString]] retain];
+ csvImportTailString = [[NSString stringWithString:[fieldMapperController onupdateString]] retain];
+ fieldMappingArrayHasGlobalVariables = [fieldMapperController globalValuesInUsage];
+ csvImportMethodHasTail = ([csvImportTailString length] == 0) ? NO : YES;
+ insertRemainingRowsAfterUpdate = [fieldMapperController insertRemainingRowsAfterUpdate];
+ importMethodIsUpdate = ([selectedImportMethod isEqualToString:@"UPDATE"]) ? YES : NO;
+
+ // Error checking
+ if( ![fieldMapperOperator count]
+ || ![fieldMappingArray count]
+ || ![selectedImportMethod length]
+ || ![selectedTableTarget length]
+ || ![csvImportHeaderString length])
+ {
+ if(fieldMapperController) [fieldMapperController release];
+ NSBeep();
+ return FALSE;
+ }
+
+ [importFieldNamesSwitch setState:[fieldMapperController importFieldNamesHeader]];
+ [prefs setBool:[importFieldNamesSwitch state] forKey:SPCSVImportFirstLineIsHeader];
+
+ if(fieldMapperController) [fieldMapperController release];
+
+ if(fieldMapperSheetStatus == 2)
+ return YES;
+ else
+ return NO;
+}
+
+/**
+ *
+ */
+- (void)fieldMapperDidEndSheet:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
+{
+ [sheet orderOut:self];
+ fieldMapperSheetStatus = (returnCode) ? 2 : 3;
+}
+
+/**
+ * Construct the SET and WHERE clause for a CSV row, based on the field mapping array
+ * for the import method "UPDATE".
+ */
+- (NSString *)mappedUpdateSetStatementStringForRowArray:(NSArray *)csvRowArray
+{
+
+ NSMutableString *setString = [NSMutableString stringWithString:@""];
+ NSMutableString *whereString = [NSMutableString stringWithString:@"WHERE "];
+
+ NSInteger i;
+ NSInteger mapColumn;
+ id cellData;
+ NSInteger mappingArrayCount = [fieldMappingArray count];
+
+ for (i = 0; i < mappingArrayCount; i++) {
+
+ // Skip unmapped columns
+ if ([NSArrayObjectAtIndex(fieldMapperOperator, i) integerValue] == 1 ) continue;
+
+ mapColumn = [NSArrayObjectAtIndex(fieldMappingArray, i) integerValue];
+
+ // SET clause
+ if ([NSArrayObjectAtIndex(fieldMapperOperator, i) integerValue] == 0 ) {
+ if ([setString length] > 1) [setString appendString:@","];
+ [setString appendString:[NSArrayObjectAtIndex(fieldMappingTableColumnNames, i) backtickQuotedString]];
+ [setString appendString:@"="];
+ // Append the data
+ // - check for global values
+ if(fieldMappingArrayHasGlobalVariables && mapColumn >= numberOfImportDataColumns) {
+ // Global variables are coming wrapped in ' ' if there're not marked as SQL
+ [setString appendString:NSArrayObjectAtIndex(fieldMappingGlobalValueArray, mapColumn)];
+ } else {
+ cellData = NSArrayObjectAtIndex(csvRowArray, mapColumn);
+
+ // If import column isn't specified import the table column default value
+ if ([cellData isSPNotLoaded])
+ cellData = NSArrayObjectAtIndex(fieldMappingTableDefaultValues, i);
+
+ if (cellData == [NSNull null]) {
+ [setString appendString:@"NULL"];
+ } else {
+ [setString appendString:@"'"];
+ [setString appendString:[mySQLConnection prepareString:cellData]];
+ [setString appendString:@"'"];
+ }
+ }
+ }
+ // WHERE clause
+ else if ([NSArrayObjectAtIndex(fieldMapperOperator, i) integerValue] == 2 )
+ {
+ if ([whereString length] > 7) [whereString appendString:@" AND "];
+ [whereString appendString:[NSArrayObjectAtIndex(fieldMappingTableColumnNames, i) backtickQuotedString]];
+ // Append the data
+ // - check for global values
+ if(fieldMappingArrayHasGlobalVariables && mapColumn >= numberOfImportDataColumns) {
+ // Global variables are coming wrapped in ' ' if there're not marked as SQL
+ [whereString appendString:@"="];
+ [whereString appendString:NSArrayObjectAtIndex(fieldMappingGlobalValueArray, mapColumn)];
+ } else {
+ cellData = NSArrayObjectAtIndex(csvRowArray, mapColumn);
+
+ // If import column isn't specified import the table column default value
+ if ([cellData isSPNotLoaded])
+ cellData = NSArrayObjectAtIndex(fieldMappingTableDefaultValues, i);
+
+ if (cellData == [NSNull null]) {
+ [whereString appendString:@" IS NULL"];
+ } else {
+ [whereString appendString:@"="];
+ [whereString appendString:@"'"];
+ [whereString appendString:[mySQLConnection prepareString:cellData]];
+ [whereString appendString:@"'"];
+ }
+ }
+ }
+ }
+
+ return [NSString stringWithFormat:@"%@ %@", setString, whereString];
+}
+
+/**
+ * Construct the VALUES string for a CSV row, based on the field mapping array - including
+ * surrounding brackets but not including the VALUES keyword.
+ */
+- (NSString *)mappedValueStringForRowArray:(NSArray *)csvRowArray
+{
+ NSMutableString *valueString = [NSMutableString stringWithString:@"("];
+ NSInteger i;
+ NSInteger mapColumn;
+ id cellData;
+ NSInteger mappingArrayCount = [fieldMappingArray count];
+
+ for (i = 0; i < mappingArrayCount; i++) {
+
+ // Skip unmapped columns
+ if ([NSArrayObjectAtIndex(fieldMapperOperator, i) integerValue] > 0) continue;
+
+ mapColumn = [NSArrayObjectAtIndex(fieldMappingArray, i) integerValue];
+
+ if ([valueString length] > 1) [valueString appendString:@","];
+
+ // Append the data
+ // - check for global values
+ if(fieldMappingArrayHasGlobalVariables && mapColumn >= numberOfImportDataColumns) {
+ // Global variables are coming wrapped in ' ' if there're not marked as SQL
+ [valueString appendString:NSArrayObjectAtIndex(fieldMappingGlobalValueArray, mapColumn)];
+ } else {
+ cellData = NSArrayObjectAtIndex(csvRowArray, mapColumn);
+
+ // If import column isn't specified import the table column default value
+ if ([cellData isSPNotLoaded])
+ cellData = NSArrayObjectAtIndex(fieldMappingTableDefaultValues, i);
+
+ if (cellData == [NSNull null]) {
+ [valueString appendString:@"NULL"];
+ } else {
+ [valueString appendString:@"'"];
+ [valueString appendString:[mySQLConnection prepareString:cellData]];
+ [valueString appendString:@"'"];
+ }
+ }
+ }
+
+ [valueString appendString:@")"];
+
+ return valueString;
+}
+
+#pragma mark -
+#pragma mark Import delegate notifications
+
+/**
+ * Called when the selection within an open/save panel changes.
+ */
+- (void)panelSelectionDidChange:(id)sender
+{
+ NSArray *selectedFilenames = [sender filenames];
+ NSString *pathExtension;
+
+ // If a single file is selected and the extension is recognised, change the format dropdown automatically
+ if ( [selectedFilenames count] != 1 ) return;
+ pathExtension = [[[selectedFilenames objectAtIndex:0] pathExtension] uppercaseString];
+
+ // If a file has extension ".gz", indicating gzip, fetch the next extension
+ if ([pathExtension isEqualToString:@"GZ"]) {
+ NSMutableString *pathString = [NSMutableString stringWithString:[selectedFilenames objectAtIndex:0]];
+ [pathString deleteCharactersInRange:NSMakeRange([pathString length]-3, 3)];
+ pathExtension = [[pathString pathExtension] uppercaseString];
+ }
+
+ if ([pathExtension isEqualToString:@"SQL"]) {
+ [importFormatPopup selectItemWithTitle:@"SQL"];
+ [self changeFormat:self];
+ } else if ([pathExtension isEqualToString:@"CSV"]) {
+ [importFormatPopup selectItemWithTitle:@"CSV"];
+ [self changeFormat:self];
+
+ // Try to detect the line endings using "file"
+ NSTask *fileTask = [[NSTask alloc] init];
+ NSPipe *filePipe = [[NSPipe alloc] init];
+
+ [fileTask setLaunchPath:@"/usr/bin/file"];
+ [fileTask setArguments:[NSArray arrayWithObjects:@"-L", @"-b", [selectedFilenames objectAtIndex:0], nil]];
+ [fileTask setStandardOutput:filePipe];
+ NSFileHandle *fileHandle = [filePipe fileHandleForReading];
+
+ [fileTask launch];
+
+ NSString *fileCheckOutput = [[NSString alloc] initWithData:[fileHandle readDataToEndOfFile] encoding:NSASCIIStringEncoding];
+ if (fileCheckOutput && [fileCheckOutput length]) {
+ NSString *lineEndingString = [fileCheckOutput stringByMatching:@"with ([A-Z]{2,4}) line terminators" capture:1L];
+ if (!lineEndingString && [fileCheckOutput isMatchedByRegex:@"text"]) lineEndingString = @"LF";
+ if (lineEndingString) {
+ if ([lineEndingString isEqualToString:@"LF"]) [importLinesTerminatedField setStringValue:@"\\n"];
+ else if ([lineEndingString isEqualToString:@"CR"]) [importLinesTerminatedField setStringValue:@"\\r"];
+ else if ([lineEndingString isEqualToString:@"CRLF"]) [importLinesTerminatedField setStringValue:@"\\r\\n"];
+ }
+ }
+ if (fileCheckOutput) [fileCheckOutput release];
+
+ [fileTask release];
+ [filePipe release];
+ }
+}
+
+#pragma mark -
+#pragma mark Other
+
+/**
+ * Sets the connection (received from SPDatabaseDocument) and makes things that have to be done only once.
+ */
+- (void)setConnection:(MCPConnection *)theConnection
+{
+ NSButtonCell *switchButton = [[NSButtonCell alloc] init];
+
+ prefs = [[NSUserDefaults standardUserDefaults] retain];
+
+ mySQLConnection = theConnection;
+
+ // Set up the interface
+ [switchButton setButtonType:NSSwitchButton];
+ [switchButton setControlSize:NSSmallControlSize];
+ [switchButton release];
+
+ if ([prefs boolForKey:SPUseMonospacedFonts]) {
+ [errorsView setFont:[NSFont fontWithName:SPDefaultMonospacedFontName size:[NSFont smallSystemFontSize]]];
+ } else {
+ [errorsView setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ }
+}
+
+/**
+ *
+ */
+- (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar
+{
+ NSArray *array = [toolbar items];
+ NSMutableArray *items = [NSMutableArray arrayWithCapacity:6];
+
+ for (NSToolbarItem *item in array)
+ {
+ [items addObject:[item itemIdentifier]];
+ }
+
+ return items;
+}
+
+/**
+ *
+ */
+- (void)showErrorSheetWithMessage:(NSString*)message
+{
+ if (![NSThread isMainThread]) {
+ [self performSelectorOnMainThread:@selector(showErrorSheetWithMessage:) withObject:message waitUntilDone:YES];
+ return;
+ }
+
+ [errorsView setString:message];
+ [NSApp beginSheet:errorsSheet
+ modalForWindow:[tableDocumentInstance parentWindow]
+ modalDelegate:self
+ didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
+ contextInfo:nil];
+ [errorsSheet makeKeyWindow];
+}
+
+/**
+ *
+ */
+- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
+{
+ [sheet orderOut:self];
+}
+
+#pragma mark -
+
+/**
+ * Dealloc.
+ */
+- (void)dealloc
+{
+ if (fieldMappingImportArray) [fieldMappingImportArray release];
+ if (lastFilename) [lastFilename release];
+ if (prefs) [prefs release];
+
+ for (id retainedObject in nibObjectsToRelease) [retainedObject release];
+
+ [nibObjectsToRelease release];
+
+ [super dealloc];
+}
+
+@end