From 5ef3f0df72d89ad85680a02048c27e2460e2c685 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 22 Apr 2015 22:41:58 +0200 Subject: Move some complex logic into its own class so it will easier to write some unit tests and refactor some of the code. --- Source/SPTableContent.m | 167 ++------------------------ Source/SPTableFilterParser.h | 61 ++++++++++ Source/SPTableFilterParser.m | 225 +++++++++++++++++++++++++++++++++++ sequel-pro.xcodeproj/project.pbxproj | 6 + 4 files changed, 303 insertions(+), 156 deletions(-) create mode 100644 Source/SPTableFilterParser.h create mode 100644 Source/SPTableFilterParser.m diff --git a/Source/SPTableContent.m b/Source/SPTableContent.m index 7b60d152..308c101b 100644 --- a/Source/SPTableContent.m +++ b/Source/SPTableContent.m @@ -61,6 +61,7 @@ #endif #import "SPCustomQuery.h" #import "SPThreadAdditions.h" +#import "SPTableFilterParser.h" #import #import @@ -1104,8 +1105,6 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper BOOL caseSensitive = (([[[NSApp onMainThread] currentEvent] modifierFlags] & (NSShiftKeyMask|NSControlKeyMask|NSAlternateKeyMask|NSCommandKeyMask)) > 0); - NSString *filterString; - if(contentFilters == nil) { NSLog(@"Fatal error while retrieving content filters. No filters found."); NSBeep(); @@ -1133,160 +1132,16 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper return nil; } - - NSUInteger numberOfArguments = [[filter objectForKey:@"NumberOfArguments"] integerValue]; - - BOOL suppressLeadingTablePlaceholder = NO; - if([filter objectForKey:@"SuppressLeadingFieldPlaceholder"]) - suppressLeadingTablePlaceholder = YES; - - // argument if Filter requires only one argument - NSMutableString *argument = [[NSMutableString alloc] initWithString:[argumentField stringValue]]; - - // If the filter field is empty and the selected filter does not require - // only one argument, then no filtering is required - return nil. - if (![argument length] && numberOfArguments == 1) { - [argument release]; - return nil; - } - - // arguments if Filter requires two arguments - NSMutableString *firstBetweenArgument = [[NSMutableString alloc] initWithString:[firstBetweenField stringValue]]; - NSMutableString *secondBetweenArgument = [[NSMutableString alloc] initWithString:[secondBetweenField stringValue]]; - - // If filter requires two arguments and either of the argument fields are empty - // return nil. - if (numberOfArguments == 2) { - if (([firstBetweenArgument length] == 0) || ([secondBetweenArgument length] == 0)) { - [argument release]; - [firstBetweenArgument release]; - [secondBetweenArgument release]; - return nil; - } - } - - // Retrieve actual WHERE clause - NSMutableString *clause = [[NSMutableString alloc] init]; - [clause setString:[filter objectForKey:@"Clause"]]; - - [clause replaceOccurrencesOfRegex:@"(? 0) - [clause replaceOccurrencesOfRegex:@"%" withString:@"%%"]; - [clause flushCachedRegexData]; - - // Replace placeholder ${} by %@ - NSRange matchedRange; - NSString *re = @"(? 2) { - NSLog(@"Filter with more than 2 arguments is not yet supported."); - NSBeep(); - } - } - } else { - if (numberOfArguments == 2) { - filterString = [NSString stringWithFormat:@"%@ %@", - [[fieldField titleOfSelectedItem] backtickQuotedString], - [NSString stringWithFormat:clause, - [self escapeFilterArgument:firstBetweenArgument againstClause:clause], - [self escapeFilterArgument:secondBetweenArgument againstClause:clause]]]; - } else if (numberOfArguments == 1) { - filterString = [NSString stringWithFormat:@"%@ %@", - [[fieldField titleOfSelectedItem] backtickQuotedString], - [NSString stringWithFormat:clause, [self escapeFilterArgument:argument againstClause:clause]]]; - } else { - filterString = [NSString stringWithFormat:@"%@ %@", - [[fieldField titleOfSelectedItem] backtickQuotedString], clause]; - if(numberOfArguments > 2) { - NSLog(@"Filter with more than 2 arguments is not yet supported."); - NSBeep(); - } - } - } - - [argument release]; - [firstBetweenArgument release]; - [secondBetweenArgument release]; - [clause release]; - - // Return the filter string - return filterString; -} - -/** - * Escape argument by looking for used quoting strings in a clause. Attempt to - * be smart - use a single escape for most clauses, doubling up for LIKE clauses. - * Also attempt to not escape what look like common escape sequences - \n, \r, \t. - * - * @param argument The to be used filter argument which should be be escaped - * - * @param clause The entire WHERE filter clause - * - */ -- (NSString *)escapeFilterArgument:(NSString *)argument againstClause:(NSString *)clause -{ - BOOL clauseIsLike = [clause isMatchedByRegex:@"(?i)\\blike\\b.*?%(?!@)"]; - NSString *recognizedEscapeSequences, *escapeSequence, *regexTerm; - NSMutableString *arg = [argument mutableCopy]; - - // Determine the character set not to escape slashes before, and the escape depth - if (clauseIsLike) { - recognizedEscapeSequences = @"nrt_%"; - escapeSequence = @"\\\\\\\\\\\\\\\\"; - } else { - recognizedEscapeSequences = @"nrt"; - escapeSequence = @"\\\\\\\\"; - } - regexTerm = [NSString stringWithFormat:@"(\\\\)(?![%@])", recognizedEscapeSequences]; - - // Escape slashes appropriately - [arg replaceOccurrencesOfRegex:regexTerm withString:escapeSequence]; - [arg flushCachedRegexData]; - - // Get quote sign for escaping - this should work for 99% of all cases - NSString *quoteSign = [clause stringByMatching:@"([\"'])[^\\1]*?%@[^\\1]*?\\1" capture:1L]; - - // Escape argument - if(quoteSign != nil && [quoteSign length] == 1) { - [arg replaceOccurrencesOfRegex:[NSString stringWithFormat:@"(%@)", quoteSign] withString:@"\\\\$1"]; - [arg flushCachedRegexData]; - } - - return [arg autorelease]; + + SPTableFilterParser *parser = [[[SPTableFilterParser alloc] initWithFilterClause:[filter objectForKey:@"Clause"] numberOfArguments:[[filter objectForKey:@"NumberOfArguments"] integerValue]] autorelease]; + [parser setArgument:[argumentField stringValue]]; + [parser setFirstBetweenArgument:[firstBetweenField stringValue]]; + [parser setSecondBetweenArgument:[secondBetweenField stringValue]]; + [parser setSuppressLeadingTablePlaceholder:(!![filter objectForKey:@"SuppressLeadingFieldPlaceholder"])]; + [parser setCaseSensitive:caseSensitive]; + [parser setCurrentField:[fieldField titleOfSelectedItem]]; + + return [parser filterString]; } /** diff --git a/Source/SPTableFilterParser.h b/Source/SPTableFilterParser.h new file mode 100644 index 00000000..069c802c --- /dev/null +++ b/Source/SPTableFilterParser.h @@ -0,0 +1,61 @@ +// +// SPTableFilterParser.h +// sequel-pro +// +// Created by Max Lohrmann on 22.04.15. +// Relocated from existing files. Previous copyright applies. +// +// 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 + +@interface SPTableFilterParser : NSObject +{ + NSString *_clause; + NSUInteger numberOfArguments; + + NSString *_currentField; + NSString *_argument; + NSString *_firstBetweenArgument; + NSString *_secondsBetweenArgument; + + BOOL caseSensitive; + BOOL suppressLeadingTablePlaceholder; +} + +- (id)initWithFilterClause:(NSString *)filter numberOfArguments:(NSUInteger)numArgs; + +@property(readonly) NSString *clause; +@property(readonly) NSUInteger numberOfArguments; + +@property BOOL suppressLeadingTablePlaceholder; +@property BOOL caseSensitive; +@property(copy) NSString *currentField; +@property(copy) NSString *argument; +@property(copy) NSString *firstBetweenArgument; +@property(copy) NSString *secondBetweenArgument; + +- (NSString *)filterString; + +@end diff --git a/Source/SPTableFilterParser.m b/Source/SPTableFilterParser.m new file mode 100644 index 00000000..96ba8368 --- /dev/null +++ b/Source/SPTableFilterParser.m @@ -0,0 +1,225 @@ +// +// SPTableFilterParser.m +// sequel-pro +// +// Created by Max Lohrmann on 22.04.15. +// Relocated from existing files. Previous copyright applies. +// +// 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 "SPTableFilterParser.h" +#import "RegexKitLite.h" + +@interface SPTableFilterParser (Private) ++ (NSString *)escapeFilterArgument:(NSString *)argument againstClause:(NSString *)clause; +@end + +@implementation SPTableFilterParser + +@synthesize suppressLeadingTablePlaceholder = suppressLeadingTablePlaceholder; +@synthesize caseSensitive = caseSensitive; +@synthesize currentField = _currentField; +@synthesize argument = _argument; +@synthesize firstBetweenArgument = _firstBetweenArgument; +@synthesize secondBetweenArgument = _secondBetweenArgument; +@synthesize numberOfArguments = numberOfArguments; +@synthesize clause = _clause; + +- (id)initWithFilterClause:(NSString *)filter numberOfArguments:(NSUInteger)numArgs +{ + self = [super init]; + if (self) { + numberOfArguments = numArgs; + _clause = [filter copy]; + } + return self; +} + +- (void)dealloc +{ + SPClear(_clause); + + [self setCurrentField:nil]; + [self setArgument:nil]; + [self setFirstBetweenArgument:nil]; + [self setSecondBetweenArgument:nil]; + + [super dealloc]; +} + +- (NSString *)filterString +{ + NSString *filterString; + + // argument if Filter requires only one argument + NSMutableString *argument = [[NSMutableString alloc] initWithString:_argument]; + + // If the filter field is empty and the selected filter does not require + // only one argument, then no filtering is required - return nil. + if (![argument length] && numberOfArguments == 1) { + [argument release]; + return nil; + } + + // arguments if Filter requires two arguments + NSMutableString *firstBetweenArgument = [[NSMutableString alloc] initWithString:_firstBetweenArgument]; + NSMutableString *secondBetweenArgument = [[NSMutableString alloc] initWithString:_secondBetweenArgument]; + + // If filter requires two arguments and either of the argument fields are empty + // return nil. + if (numberOfArguments == 2) { + if (([firstBetweenArgument length] == 0) || ([secondBetweenArgument length] == 0)) { + [argument release]; + [firstBetweenArgument release]; + [secondBetweenArgument release]; + return nil; + } + } + + // Retrieve actual WHERE clause + NSMutableString *clause = [[NSMutableString alloc] init]; + [clause setString:_clause]; + + [clause replaceOccurrencesOfRegex:@"(? 0) + [clause replaceOccurrencesOfRegex:@"%" withString:@"%%"]; + [clause flushCachedRegexData]; + + // Replace placeholder ${} by %@ + NSRange matchedRange; + NSString *re = @"(? 2) { + SPLog(@"Filter with more than 2 arguments is not yet supported."); + NSBeep(); + } + } + } else { + if (numberOfArguments == 2) { + filterString = [NSString stringWithFormat:@"%@ %@", + [_currentField backtickQuotedString], + [NSString stringWithFormat:clause, + [[self class] escapeFilterArgument:firstBetweenArgument againstClause:clause], + [[self class] escapeFilterArgument:secondBetweenArgument againstClause:clause]]]; + } else if (numberOfArguments == 1) { + filterString = [NSString stringWithFormat:@"%@ %@", + [_currentField backtickQuotedString], + [NSString stringWithFormat:clause, [[self class] escapeFilterArgument:argument againstClause:clause]]]; + } else { + filterString = [NSString stringWithFormat:@"%@ %@", + [_currentField backtickQuotedString], clause]; + if(numberOfArguments > 2) { + SPLog(@"Filter with more than 2 arguments is not yet supported."); + NSBeep(); + } + } + } + + [argument release]; + [firstBetweenArgument release]; + [secondBetweenArgument release]; + [clause release]; + + // Return the filter string + return filterString; +} + +/** + * Escape argument by looking for used quoting strings in a clause. Attempt to + * be smart - use a single escape for most clauses, doubling up for LIKE clauses. + * Also attempt to not escape what look like common escape sequences - \n, \r, \t. + * + * @param argument The to be used filter argument which should be be escaped + * + * @param clause The entire WHERE filter clause + * + */ ++ (NSString *)escapeFilterArgument:(NSString *)argument againstClause:(NSString *)clause +{ + BOOL clauseIsLike = [clause isMatchedByRegex:@"(?i)\\blike\\b.*?%(?!@)"]; + NSString *recognizedEscapeSequences, *escapeSequence, *regexTerm; + NSMutableString *arg = [argument mutableCopy]; + + // Determine the character set not to escape slashes before, and the escape depth + if (clauseIsLike) { + recognizedEscapeSequences = @"nrt_%"; + escapeSequence = @"\\\\\\\\\\\\\\\\"; + } else { + recognizedEscapeSequences = @"nrt"; + escapeSequence = @"\\\\\\\\"; + } + regexTerm = [NSString stringWithFormat:@"(\\\\)(?![%@])", recognizedEscapeSequences]; + + // Escape slashes appropriately + [arg replaceOccurrencesOfRegex:regexTerm withString:escapeSequence]; + [arg flushCachedRegexData]; + + // Get quote sign for escaping - this should work for 99% of all cases + NSString *quoteSign = [clause stringByMatching:@"([\"'])[^\\1]*?%@[^\\1]*?\\1" capture:1L]; + + // Escape argument + if(quoteSign != nil && [quoteSign length] == 1) { + [arg replaceOccurrencesOfRegex:[NSString stringWithFormat:@"(%@)", quoteSign] withString:@"\\\\$1"]; + [arg flushCachedRegexData]; + } + + return [arg autorelease]; +} + + +@end diff --git a/sequel-pro.xcodeproj/project.pbxproj b/sequel-pro.xcodeproj/project.pbxproj index 9d5f6ffd..9b319e1e 100644 --- a/sequel-pro.xcodeproj/project.pbxproj +++ b/sequel-pro.xcodeproj/project.pbxproj @@ -179,6 +179,7 @@ 4DECC48F0EC2B436008D359E /* Sparkle.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 4DECC3320EC2A170008D359E /* Sparkle.framework */; }; 4DECC4910EC2B436008D359E /* Growl.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 4DECC3340EC2A170008D359E /* Growl.framework */; }; 501B1D181728A3DA0017C92E /* SPCharsetCollationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 501B1D171728A3DA0017C92E /* SPCharsetCollationHelper.m */; }; + 503B02CA1AE82C5E0060CAB1 /* SPTableFilterParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 503B02C91AE82C5E0060CAB1 /* SPTableFilterParser.m */; }; 503CDBB21ACDC204004F8A2F /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 503CDBB11ACDC204004F8A2F /* Quartz.framework */; }; 506CE9311A311C6C0039F736 /* SPTableContentFilterController.m in Sources */ = {isa = PBXBuildFile; fileRef = 506CE9301A311C6C0039F736 /* SPTableContentFilterController.m */; }; 50A9F8B119EAD4B90053E571 /* SPGotoDatabaseController.m in Sources */ = {isa = PBXBuildFile; fileRef = 50A9F8B019EAD4B90053E571 /* SPGotoDatabaseController.m */; }; @@ -887,6 +888,8 @@ 4DECC3340EC2A170008D359E /* Growl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Growl.framework; path = Frameworks/Growl.framework; sourceTree = ""; }; 501B1D161728A3DA0017C92E /* SPCharsetCollationHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPCharsetCollationHelper.h; sourceTree = ""; }; 501B1D171728A3DA0017C92E /* SPCharsetCollationHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPCharsetCollationHelper.m; sourceTree = ""; }; + 503B02C81AE82C5E0060CAB1 /* SPTableFilterParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPTableFilterParser.h; sourceTree = ""; }; + 503B02C91AE82C5E0060CAB1 /* SPTableFilterParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPTableFilterParser.m; sourceTree = ""; }; 503CDBB11ACDC204004F8A2F /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; }; 506CE92F1A311C6C0039F736 /* SPTableContentFilterController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPTableContentFilterController.h; sourceTree = ""; }; 506CE9301A311C6C0039F736 /* SPTableContentFilterController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPTableContentFilterController.m; sourceTree = ""; }; @@ -2551,6 +2554,8 @@ 1755A25C16B33BEA00B35787 /* SPSyntaxParser.h */, 50D3C3501A77135F00B5429C /* SPParserUtils.c */, 50D3C3511A77135F00B5429C /* SPParserUtils.h */, + 503B02C81AE82C5E0060CAB1 /* SPTableFilterParser.h */, + 503B02C91AE82C5E0060CAB1 /* SPTableFilterParser.m */, ); name = Parsing; sourceTree = ""; @@ -3166,6 +3171,7 @@ BC9F0881100FCF2C00A80D32 /* SPFieldEditorController.m in Sources */, 58D2E229101222670063EF1D /* SPTextAndLinkCell.m in Sources */, BC05F1C5101241DF008A97F8 /* YRKSpinningProgressIndicator.m in Sources */, + 503B02CA1AE82C5E0060CAB1 /* SPTableFilterParser.m in Sources */, 4D90B79A101E0CDF00D116A1 /* SPUserManager.m in Sources */, 4D90B79E101E0CF200D116A1 /* SPUserManager.xcdatamodel in Sources */, 4D90B79F101E0CF200D116A1 /* SPUserMO.m in Sources */, -- cgit v1.2.3