// // SPSXMLExporter.m // sequel-pro // // Created by Stuart Connolly (stuconnolly.com) on October 6, 2009. // Copyright (c) 2009 Stuart Connolly. All rights reserved. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following // conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // // More info at <https://github.com/sequelpro/sequelpro> #import "SPXMLExporter.h" #import "SPExportFile.h" #import "SPFileHandle.h" #import "SPExportUtilities.h" #import <SPMySQL/SPMySQL.h> @implementation SPXMLExporter @synthesize delegate; @synthesize xmlDataArray; @synthesize xmlTableName; @synthesize xmlNULLString; @synthesize xmlOutputIncludeStructure; @synthesize xmlOutputIncludeContent; @synthesize xmlFormat; /** * Initialise an instance of SPXMLExporter using the supplied delegate. * * @param exportDelegate The exporter delegate * * @return The initialised instance */ - (id)initWithDelegate:(NSObject<SPXMLExporterProtocol> *)exportDelegate { if ((self = [super init])) { SPExportDelegateConformsToProtocol(exportDelegate, @protocol(SPXMLExporterProtocol)); [self setDelegate:exportDelegate]; } return self; } /** * 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 { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; BOOL isTableExport = NO; NSArray *xmlRow = nil; NSArray *fieldNames = nil; NSString *dataConversionString = nil; // Result sets SPMySQLResult *statusResult = nil; SPMySQLResult *structureResult = nil; SPMySQLFastStreamingResult *streamingResult = nil; NSMutableArray *xmlTags = [NSMutableArray array]; NSMutableString *xmlString = [NSMutableString string]; NSMutableString *xmlItem = [NSMutableString string]; NSUInteger xmlRowCount = 0; double lastProgressValue = 0; NSUInteger i, totalRows, currentRowIndex, currentPoolDataLength; // Check to see if we have at least a table name or data array if ((![self xmlTableName] && ![self xmlDataArray]) || ([[self xmlTableName] length] == 0 && [[self xmlDataArray] count] == 0) || (([self xmlFormat] == SPXMLExportMySQLFormat) && ((![self xmlOutputIncludeStructure]) && (![self xmlOutputIncludeContent]))) || (([self xmlFormat] == SPXMLExportPlainFormat) && (![self xmlNULLString]))) { [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]; // Make a streaming request for the data if the data array isn't set if ((![self xmlDataArray]) && [self xmlTableName]) { isTableExport = YES; totalRows = [[connection getFirstFieldFromQuery:[NSString stringWithFormat:@"SELECT COUNT(1) FROM %@", [[self xmlTableName] backtickQuotedString]]] integerValue]; streamingResult = [connection streamingQueryString:[NSString stringWithFormat:@"SELECT * FROM %@", [[self xmlTableName] backtickQuotedString]] useLowMemoryBlockingStreaming:[self exportUsingLowMemoryBlockingStreaming]]; // Only include the structure if necessary if (([self xmlFormat] == SPXMLExportMySQLFormat) && [self xmlOutputIncludeStructure]) { structureResult = [connection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM %@", [[self xmlTableName] backtickQuotedString]]]; NSMutableString *escapedTableName = [NSMutableString stringWithString:[[self xmlTableName] tickQuotedString]]; [escapedTableName replaceOccurrencesOfString:@"\\" withString:@"\\\\\\\\" options:0 range:NSMakeRange(0, [escapedTableName length])]; statusResult = [connection queryString:[NSString stringWithFormat:@"SHOW TABLE STATUS LIKE %@", escapedTableName]]; if ([structureResult numberOfRows] && [statusResult numberOfRows]) { [xmlString appendFormat:@"\t<table_structure name=\"%@\">\n", [self xmlTableName]]; for (NSDictionary *row in structureResult) { [xmlString appendFormat:@"\t\t<field field=\"%@\" type=\"%@\" null=\"%@\" key=\"%@\" default=\"%@\" extra=\"%@\" />\n", [row objectForKey:@"Field"], [row objectForKey:@"Type"], [row objectForKey:@"Null"], [row objectForKey:@"Key"], [row objectForKey:@"Default"], [row objectForKey:@"Extra"]]; } NSDictionary *row = [statusResult getRowAsDictionary]; [xmlString appendFormat:@"\n\t\t<options name=\"%@\" engine=\"%@\" version=\"%@\" row_format=\"%@\" rows=\"%@\" avg_row_length=\"%@\" data_length=\"%@\" max_data_length=\"%@\" index_length=\"%@\" data_free=\"%@\" create_time=\"%@\" update_time=\"%@\" collation=\"%@\" create_options=\"%@\" comment=\"%@\" />\n", [row objectForKey:@"Name"], [row objectForKey:@"Engine"], [row objectForKey:@"Version"], [row objectForKey:@"Row_format"], [row objectForKey:@"Rows"], [row objectForKey:@"Avg_row_length"], [row objectForKey:@"Data_length"], [row objectForKey:@"Max_data_length"], [row objectForKey:@"Index_length"], [row objectForKey:@"Data_free"], [row objectForKey:@"Create_time"], [row objectForKey:@"Update_time"], [row objectForKey:@"Collation"], [row objectForKey:@"Create_options"], [row objectForKey:@"Comment"]]; [xmlString appendFormat:@"\t</table_structure>\n\n"]; } } if (([self xmlFormat] == SPXMLExportMySQLFormat) && [self xmlOutputIncludeContent]) { [xmlString appendFormat:@"\t<table_data name=\"%@\">\n\n", [self xmlTableName]]; } [[self exportOutputFile] writeData:[xmlString dataUsingEncoding:[self exportOutputEncoding]]]; } else { totalRows = [[self xmlDataArray] count]; } // Only proceed to export the content if this is not a table export or it is and include content is selected if ((!isTableExport) || (isTableExport && [self xmlOutputIncludeContent])) { // Set up an array of encoded field names as opening and closing tags fieldNames = ([self xmlDataArray]) ? NSArrayObjectAtIndex([self xmlDataArray], 0) : [streamingResult fieldNames]; for (i = 0; i < [fieldNames count]; i++) { [xmlTags addObject:[NSMutableArray array]]; [NSArrayObjectAtIndex(xmlTags, i) addObject:[NSString stringWithFormat:@"\t\t<%@>", [[NSArrayObjectAtIndex(fieldNames, i) description] HTMLEscapeString]]]; [NSArrayObjectAtIndex(xmlTags, i) addObject:[NSString stringWithFormat:@"</%@>\n", [[NSArrayObjectAtIndex(fieldNames, i) description] HTMLEscapeString]]]; } // If required, write an opening tag in the form of the table name if ([self xmlFormat] == SPXMLExportPlainFormat) { [[self exportOutputFile] 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 NSAutoreleasePool *xmlExportPool = [[NSAutoreleasePool alloc] init]; currentPoolDataLength = 0; // Inform the delegate that we are about to start writing the data to disk [delegate performSelectorOnMainThread:@selector(xmlExportProcessWillBeginWritingData:) withObject:self waitUntilDone:NO]; 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 getRowAsArray]; 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; } BOOL dataIsNULL = NO; id data = NSArrayObjectAtIndex(xmlRow, i); // Retrieve the contents of this tag if ([data isKindOfClass:[NSData class]]) { dataConversionString = [[NSString alloc] initWithData:data encoding:[self exportOutputEncoding]]; if (dataConversionString == nil) { dataConversionString = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; } [xmlItem setString:[NSString stringWithString:dataConversionString]]; [dataConversionString release]; } // Check for null value using a pointer comparison; as [NSNull null] is a singleton this works correctly. else if (data == [NSNull null]) { dataIsNULL = YES; if ([self xmlFormat] == SPXMLExportPlainFormat) { [xmlItem setString:[self xmlNULLString]]; } } else if ([data isKindOfClass:[SPMySQLGeometryData class]]) { [xmlItem setString:[data wktString]]; } else { [xmlItem setString:[data description]]; } if ([self xmlFormat] == SPXMLExportMySQLFormat) { [xmlString appendFormat:@"\t\t<field name=\"%@\"", [[NSArrayObjectAtIndex(fieldNames, i) description] HTMLEscapeString]]; if (dataIsNULL) { [xmlString appendString:@" xsi:nil=\"true\" />\n"]; } else { [xmlString appendFormat:@">%@</field>\n", [xmlItem HTMLEscapeString]]; } } else if ([self xmlFormat] == SPXMLExportPlainFormat) { // 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\n"]; // Record the total length for use with pool flushing currentPoolDataLength += [xmlString length]; // Write the row to the filehandle [[self exportOutputFile] writeData:[xmlString dataUsingEncoding:[self exportOutputEncoding]]]; // Update the progress counter and progress bar currentRowIndex++; // Update the progress if (totalRows && (currentRowIndex * ([self exportMaxProgress] / totalRows)) > lastProgressValue) { double 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]; // Drain the autorelease pool as required to keep memory usage low if (currentPoolDataLength > 250000) { [xmlExportPool release]; xmlExportPool = [[NSAutoreleasePool alloc] init]; } // If an array was supplied and we've processed all rows, break if ([self xmlDataArray] && totalRows == currentRowIndex) break; } if (([self xmlFormat] == SPXMLExportMySQLFormat) && isTableExport) { [[self exportOutputFile] writeData:[@"\t</table_data>\n\n" dataUsingEncoding:[self exportOutputEncoding]]]; } else if ([self xmlFormat] == SPXMLExportPlainFormat) { [[self exportOutputFile] writeData:[[NSString stringWithFormat:@"\t</%@>\n\n", ([self xmlTableName]) ? [[self xmlTableName] HTMLEscapeString] : @"custom"] dataUsingEncoding:[self exportOutputEncoding]]]; } [xmlExportPool release]; } // Write data to disk [[[self exportOutputFile] exportFileHandle] 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]; } #pragma mark - - (void)dealloc { if (xmlDataArray) SPClear(xmlDataArray); if (xmlTableName) SPClear(xmlTableName); if (xmlNULLString) SPClear(xmlNULLString); [super dealloc]; } @end