//
// 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
#import "SPXMLExporter.h"
#import "SPExportFile.h"
#import "SPFileHandle.h"
#import "SPExportUtilities.h"
#import
@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 *)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\n", [self xmlTableName]];
for (NSDictionary *row in structureResult)
{
[xmlString appendFormat:@"\t\t\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\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\n\n"];
}
}
if (([self xmlFormat] == SPXMLExportMySQLFormat) && [self xmlOutputIncludeContent]) {
[xmlString appendFormat:@"\t\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\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\n"];
}
else {
[xmlString appendFormat:@">%@\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
\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\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) [xmlDataArray release], xmlDataArray = nil;
if (xmlTableName) [xmlTableName release], xmlTableName = nil;
if (xmlNULLString) [xmlNULLString release], xmlNULLString = nil;
[super dealloc];
}
@end