aboutsummaryrefslogtreecommitdiffstats
path: root/Source/SPRuleFilterController.m
diff options
context:
space:
mode:
Diffstat (limited to 'Source/SPRuleFilterController.m')
-rw-r--r--Source/SPRuleFilterController.m1355
1 files changed, 1355 insertions, 0 deletions
diff --git a/Source/SPRuleFilterController.m b/Source/SPRuleFilterController.m
new file mode 100644
index 00000000..baba3eb7
--- /dev/null
+++ b/Source/SPRuleFilterController.m
@@ -0,0 +1,1355 @@
+//
+// SPRuleFilterController.m
+// sequel-pro
+//
+// Created by Max Lohrmann on 04.05.18.
+// Copyright (c) 2018 Max Lohrmann. 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 "SPRuleFilterController.h"
+#import "SPQueryController.h"
+#import "SPDatabaseDocument.h"
+#import "RegexKitLite.h"
+#import "SPContentFilterManager.h"
+#import "SPFunctions.h"
+#import "SPTableFilterParser.h"
+
+typedef NS_ENUM(NSInteger, RuleNodeType) {
+ RuleNodeTypeColumn,
+ RuleNodeTypeString,
+ RuleNodeTypeOperator,
+ RuleNodeTypeArgument,
+ RuleNodeTypeConnector,
+};
+
+NSString * const SPRuleFilterHeightChangedNotification = @"SPRuleFilterHeightChanged";
+
+/**
+ * The type of filter rule that the current item represents.
+ */
+const NSString * const SerFilterClass = @"filterClass";
+/**
+ * The current rule is a group row (an "AND" or "OR" expression with children)
+ */
+const NSString * const SerFilterClassGroup = @"groupNode";
+/**
+ * The current rule is a filter expression
+ */
+const NSString * const SerFilterClassExpression = @"expressionNode";
+/**
+ * Group Nodes only:
+ * Indicates whether the group is a conjunction.
+ * If YES, the children will be combined using "AND", otherwise using "OR".
+ */
+const NSString * const SerFilterGroupIsConjunction = @"isConjunction";
+/**
+ * Group Nodes only:
+ * An array of child filter rules (which again can be group or expression rules)
+ */
+const NSString * const SerFilterGroupChildren = @"children";
+/**
+ * Expression Nodes only:
+ * The name of the column to filter in (left side expression)
+ *
+ * Legacy names:
+ * @"filterField", fieldField
+ */
+const NSString * const SerFilterExprColumn = @"column";
+/**
+ * Expression Nodes only:
+ * The data type grouping of the column for applicable filters
+ */
+const NSString * const SerFilterExprType = @"filterType";
+/**
+ * Expression Nodes only:
+ * The title of the filter operator to apply
+ *
+ * Legacy names:
+ * @"filterComparison", compareField
+ */
+const NSString * const SerFilterExprComparison = @"filterComparison";
+/**
+ * Expression Nodes only:
+ * The values to apply the filter with (an array of 0 or more elements)
+ *
+ * Legacy names:
+ * @"filterValue", argumentField
+ * @"firstBetweenField", @"secondBetweenField", firstBetweenField, secondBetweenField
+ */
+const NSString * const SerFilterExprValues = @"filterValues";
+/**
+ * Expression Nodes only:
+ * the filter definition dictionary (as in ContentFilters.plist)
+ * for the filter represented by SerFilterExprComparison.
+ *
+ * This item is not designed to be serialized to disk
+ */
+const NSString * const SerFilterExprDefinition = @"_filterDefinition";
+
+#pragma mark -
+
+@interface RuleNode : NSObject {
+ RuleNodeType type;
+}
+@property(assign, nonatomic) RuleNodeType type;
+@end
+
+@interface ColumnNode : RuleNode {
+ NSString *name;
+ NSString *typegrouping;
+ NSArray *operatorCache;
+}
+@property(copy, nonatomic) NSString *name;
+@property(copy, nonatomic) NSString *typegrouping;
+@property(retain, nonatomic) NSArray *operatorCache;
+@end
+
+@interface StringNode : RuleNode {
+ NSString *value;
+}
+@property(copy, nonatomic) NSString *value;
+@end
+
+@interface OpNode : RuleNode {
+ // Note: The main purpose of this field is to have @"=" for column A and @"=" for column B to return NO in -isEqual:
+ // because otherwise NSRuleEditor will get confused and blow up.
+ ColumnNode *parentColumn;
+ NSDictionary *settings;
+ NSDictionary *filter;
+}
+@property (assign, nonatomic) ColumnNode *parentColumn;
+@property (retain, nonatomic) NSDictionary *settings;
+@property (retain, nonatomic) NSDictionary *filter;
+@end
+
+@interface ArgNode : RuleNode {
+ NSDictionary *filter;
+ NSUInteger argIndex;
+ NSString *initialValue;
+}
+@property (copy, nonatomic) NSString *initialValue;
+@property (retain, nonatomic) NSDictionary *filter;
+@property (assign, nonatomic) NSUInteger argIndex;
+@end
+
+@interface ConnectorNode : RuleNode {
+ NSDictionary *filter;
+ NSUInteger labelIndex;
+}
+@property (retain, nonatomic) NSDictionary *filter;
+@property (assign, nonatomic) NSUInteger labelIndex;
+@end
+
+#pragma mark -
+
+@interface SPRuleFilterController () <NSRuleEditorDelegate>
+
+@property (readwrite, assign, nonatomic) CGFloat preferredHeight;
+
+// This is the binding used by NSRuleEditor for the current state
+@property (retain, nonatomic) NSMutableArray *model;
+
+- (NSArray *)_compareTypesForColumn:(ColumnNode *)colNode;
+- (IBAction)_textFieldAction:(id)sender;
+- (IBAction)_editFiltersAction:(id)sender;
+- (void)_contentFiltersHaveBeenUpdated:(NSNotification *)notification;
++ (NSDictionary *)_flattenSerializedFilter:(NSDictionary *)in;
+static BOOL SerIsGroup(NSDictionary *dict);
+- (NSDictionary *)_serializedFilterIncludingFilterDefinition:(BOOL)includeDefinition;
++ (void)_writeFilterTree:(NSDictionary *)in toString:(NSMutableString *)out wrapInParenthesis:(BOOL)wrap binary:(BOOL)isBINARY error:(NSError **)err;
+- (NSMutableDictionary *)_restoreSerializedFilter:(NSDictionary *)serialized;
+static void _addIfNotNil(NSMutableArray *array, id toAdd);
+- (ColumnNode *)_columnForName:(NSString *)name;
+- (OpNode *)_operatorNamed:(NSString *)title forColumn:(ColumnNode *)col;
+- (BOOL)_focusOnFieldInSubtree:(NSDictionary *)dict;
+- (void)_resize;
+- (void)openContentFilterManagerForFilterType:(NSString *)filterType;
+- (IBAction)filterTable:(id)sender;
+
+@end
+
+@implementation SPRuleFilterController
+
+@synthesize model = model;
+@synthesize preferredHeight = preferredHeight;
+@synthesize target = target;
+@synthesize action = action;
+
+- (instancetype)init
+{
+ if((self = [super init])) {
+ columns = [[NSMutableArray alloc] init];
+ model = [[NSMutableArray alloc] init];
+ preferredHeight = 0.0;
+ target = nil;
+ action = NULL;
+
+ // Init default filters for Content Browser
+ contentFilters = [[NSMutableDictionary alloc] init];
+ numberOfDefaultFilters = [[NSMutableDictionary alloc] init];
+
+ NSError *readError = nil;
+ NSString *filePath = [NSBundle pathForResource:@"ContentFilters.plist" ofType:nil inDirectory:[[NSBundle mainBundle] bundlePath]];
+ NSData *defaultFilterData = [NSData dataWithContentsOfFile:filePath
+ options:NSMappedRead
+ error:&readError];
+
+ if(defaultFilterData && !readError) {
+ NSDictionary *defaultFilterDict = [NSPropertyListSerialization propertyListWithData:defaultFilterData
+ options:NSPropertyListMutableContainersAndLeaves
+ format:NULL
+ error:&readError];
+
+ if(defaultFilterDict && !readError) {
+ [contentFilters setDictionary:defaultFilterDict];
+ }
+ }
+
+ if (readError) {
+ NSLog(@"Error while reading 'ContentFilters.plist':\n%@", readError);
+ NSBeep();
+ }
+ else {
+ [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"number"] count]] forKey:@"number"];
+ [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"date"] count]] forKey:@"date"];
+ [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"string"] count]] forKey:@"string"];
+ [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"spatial"] count]] forKey:@"spatial"];
+ }
+ }
+ return self;
+}
+
+- (void)awakeFromNib
+{
+ [filterRuleEditor bind:@"rows" toObject:self withKeyPath:@"model" options:nil];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(_contentFiltersHaveBeenUpdated:)
+ name:SPContentFiltersHaveBeenUpdatedNotification
+ object:nil];
+}
+
+- (void)focusFirstInputField
+{
+ for(NSDictionary *rootItem in model) {
+ if([self _focusOnFieldInSubtree:rootItem]) return;
+ }
+}
+
+- (BOOL)_focusOnFieldInSubtree:(NSDictionary *)dict
+{
+ //if we are a simple row we might have an input field ourself, otherwise search among our children
+ if([[dict objectForKey:@"rowType"] unsignedIntegerValue] == NSRuleEditorRowTypeSimple) {
+ for(id obj in [dict objectForKey:@"displayValues"]) {
+ if([obj isKindOfClass:[NSTextField class]]) {
+ [[(NSTextField *)obj window] makeFirstResponder:obj];
+ return YES;
+ }
+ }
+ }
+ else {
+ for(NSDictionary *child in [dict objectForKey:@"subrows"]) {
+ if([self _focusOnFieldInSubtree:child]) return YES;
+ }
+ }
+ return NO;
+}
+
+- (void)setColumns:(NSArray *)dataColumns;
+{
+ [self willChangeValueForKey:@"model"]; // manual KVO is needed for filter rule editor to notice change
+ [model removeAllObjects];
+ [self didChangeValueForKey:@"model"];
+
+ [columns removeAllObjects];
+
+ //without a table there is nothing to filter
+ if(dataColumns) {
+ //sort column names if enabled
+ NSArray *columnDefinitions = dataColumns;
+ if ([[NSUserDefaults standardUserDefaults] boolForKey:SPAlphabeticalTableSorting]) {
+ NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
+ columnDefinitions = [columnDefinitions sortedArrayUsingDescriptors:@[sortDescriptor]];
+ }
+
+ // get the columns
+ for (NSDictionary *colDef in columnDefinitions) {
+ ColumnNode *node = [[ColumnNode alloc] init];
+ [node setName:[colDef objectForKey:@"name"]];
+ [node setTypegrouping:[colDef objectForKey:@"typegrouping"]];
+ [columns addObject:node];
+ [node release];
+ }
+ }
+
+ // make the rule editor reload the criteria
+ [filterRuleEditor reloadCriteria];
+
+ // disable UI if no criteria exist
+ [self setEnabled:([columns count] != 0)];
+}
+
+- (NSInteger)ruleEditor:(NSRuleEditor *)editor numberOfChildrenForCriterion:(nullable id)criterion withRowType:(NSRuleEditorRowType)rowType
+{
+ // nil criterion is always the first element in a row, compound rows are only for "AND"/"OR" groups
+ if(!criterion && rowType == NSRuleEditorRowTypeCompound) {
+ return 2;
+ }
+ else if(!criterion && rowType == NSRuleEditorRowTypeSimple) {
+ return [columns count];
+ }
+ else if(rowType == NSRuleEditorRowTypeSimple) {
+ // the children of the columns are their operators
+ RuleNodeType type = [(RuleNode *)criterion type];
+ if(type == RuleNodeTypeColumn) {
+ ColumnNode *node = (ColumnNode *)criterion;
+ if(![node operatorCache]) {
+ NSArray *ops = [self _compareTypesForColumn:node];
+ [node setOperatorCache:ops];
+ }
+ return [[node operatorCache] count];
+ }
+ // the first child of an operator is the first argument (if it has one)
+ else if(type == RuleNodeTypeOperator) {
+ OpNode *node = (OpNode *)criterion;
+ NSInteger numOfArgs = [[[node filter] objectForKey:@"NumberOfArguments"] integerValue];
+ return (numOfArgs > 0) ? 1 : 0;
+ }
+ // the child of an argument can only be the conjunction label if more arguments follow
+ else if(type == RuleNodeTypeArgument) {
+ ArgNode *node = (ArgNode *)criterion;
+ NSInteger numOfArgs = [[[node filter] objectForKey:@"NumberOfArguments"] integerValue];
+ return (numOfArgs > [node argIndex]+1) ? 1 : 0;
+ }
+ // the child of a conjunction is the next argument, if we have one
+ else if(type == RuleNodeTypeConnector) {
+ ConnectorNode *node = (ConnectorNode *)criterion;
+ NSInteger numOfArgs = [[[node filter] objectForKey:@"NumberOfArguments"] integerValue];
+ return (numOfArgs > [node labelIndex]+1) ? 1 : 0;
+ }
+ }
+ return 0;
+}
+
+- (id)ruleEditor:(NSRuleEditor *)editor child:(NSInteger)index forCriterion:(nullable id)criterion withRowType:(NSRuleEditorRowType)rowType
+{
+ // nil criterion is always the first element in a row, compound rows are only for "AND"/"OR" groups
+ if(!criterion && rowType == NSRuleEditorRowTypeCompound) {
+ StringNode *node = [[StringNode alloc] init];
+ switch(index) {
+ case 0: [node setValue:@"AND"]; break;
+ case 1: [node setValue:@"OR"]; break;
+ }
+ return [node autorelease];
+ }
+ // this is the column field
+ else if(!criterion && rowType == NSRuleEditorRowTypeSimple) {
+ return [columns objectAtIndex:index];
+ }
+ else if(rowType == NSRuleEditorRowTypeSimple) {
+ // the children of the columns are their operators
+ RuleNodeType type = [(RuleNode *) criterion type];
+ if (type == RuleNodeTypeColumn) {
+ return [[criterion operatorCache] objectAtIndex:index];
+ }
+ // the first child of an operator is the first argument
+ else if(type == RuleNodeTypeOperator) {
+ NSDictionary *filter = [(OpNode *)criterion filter];
+ if([[filter objectForKey:@"NumberOfArguments"] integerValue]) {
+ ArgNode *arg = [[ArgNode alloc] init];
+ [arg setFilter:filter];
+ [arg setArgIndex:0];
+ return [arg autorelease];
+ }
+ }
+ // the child of an argument can only be the conjunction label if more arguments follow
+ else if(type == RuleNodeTypeArgument) {
+ NSDictionary *filter = [(ArgNode *)criterion filter];
+ NSUInteger argIndex = [(ArgNode *)criterion argIndex];
+ if([[filter objectForKey:@"NumberOfArguments"] integerValue] > argIndex +1) {
+ ConnectorNode *node = [[ConnectorNode alloc] init];
+ [node setFilter:filter];
+ [node setLabelIndex:argIndex]; // label 0 follows argument 0
+ return [node autorelease];
+ }
+ }
+ // the child of a conjunction is the next argument, if we have one
+ else if(type == RuleNodeTypeConnector) {
+ ConnectorNode *node = (ConnectorNode *)criterion;
+ NSInteger numOfArgs = [[[node filter] objectForKey:@"NumberOfArguments"] integerValue];
+ if(numOfArgs > [node labelIndex]+1) {
+ ArgNode *arg = [[ArgNode alloc] init];
+ [arg setFilter:[node filter]];
+ [arg setArgIndex:([node labelIndex]+1)];
+ return [arg autorelease];
+ }
+ }
+ }
+ return nil;
+}
+
+- (id)ruleEditor:(NSRuleEditor *)editor displayValueForCriterion:(id)criterion inRow:(NSInteger)row
+{
+ switch([(RuleNode *)criterion type]) {
+ case RuleNodeTypeString: return [(StringNode *)criterion value];
+ case RuleNodeTypeColumn: return [(ColumnNode *)criterion name];
+ case RuleNodeTypeOperator: {
+ OpNode *node = (OpNode *)criterion;
+ NSMenuItem *item;
+ if ([[[node settings] objectForKey:@"isSeparator"] boolValue]) {
+ item = [NSMenuItem separatorItem];
+ }
+ else {
+ item = [[NSMenuItem alloc] initWithTitle:[[node settings] objectForKey:@"title"] action:NULL keyEquivalent:@""];
+ [item setToolTip:[[node settings] objectForKey:@"tooltip"]];
+ [item setTag:[[[node settings] objectForKey:@"tag"] integerValue]];
+ //TODO the following seems to be mentioned exactly nowhere on the internet/in documentation, but without it NSMenuItems won't work properly, even though Apple says they are supported
+ [item setRepresentedObject:@{
+ @"item": node,
+ @"value": [item title],
+ // this one is needed by the "Edit filters…" item for context
+ @"filterType": SPBoxNil([[node settings] objectForKey:@"filterType"]),
+ }];
+ // override the default action from the rule editor if given (used to open the edit content filters sheet)
+ id _target = [[node settings] objectForKey:@"target"];
+ SEL _action = (SEL)[(NSValue *)[[node settings] objectForKey:@"action"] pointerValue];
+ if(_target && _action) {
+ [item setTarget:_target];
+ [item setAction:_action];
+ }
+ [item autorelease];
+ }
+ return item;
+ }
+ case RuleNodeTypeArgument: {
+ //an argument is a textfield
+ ArgNode *node = (ArgNode *)criterion;
+ NSTextField *textField = [[NSTextField alloc] init];
+ [[textField cell] setSendsActionOnEndEditing:YES];
+ [[textField cell] setUsesSingleLineMode:YES];
+ [textField setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ [textField sizeToFit];
+ [textField setTarget:self];
+ [textField setAction:@selector(_textFieldAction:)];
+ if([node initialValue]) [textField setStringValue:[node initialValue]];
+ NSRect frame = [textField frame];
+ //adjust width, to make the field wider
+ frame.size.width = 500; //TODO determine a good width (possibly from the field type size) - how to access the rule editors bounds?
+ [textField setFrame:frame];
+ return [textField autorelease];
+ }
+ case RuleNodeTypeConnector: {
+ // a simple string for once
+ ConnectorNode *node = (ConnectorNode *)criterion;
+ NSArray* labels = [[node filter] objectForKey:@"ConjunctionLabels"];
+ return (labels && [labels count] == 1)? [labels objectAtIndex:0] : @"";
+ }
+ }
+
+ return nil;
+}
+
+- (IBAction)_textFieldAction:(id)sender
+{
+ // if the action was caused by pressing return or enter, trigger filtering
+ NSEvent *event = [NSApp currentEvent];
+ if(event && [event type] == NSKeyDown && ([event keyCode] == 36 || [event keyCode] == 76)) {
+ [self filterTable:nil];
+ }
+}
+
+- (IBAction)filterTable:(id)sender
+{
+ if(target && action) [target performSelector:action withObject:self];
+}
+
+- (void)_resize
+{
+ // The situation with the sizing is a bit f'ed up:
+ // - When -ruleEditorRowsDidChange: is invoked the NSRuleEditor has not yet updated its required frame size
+ // - We can't use KVO on -frame either, because SPTableContent will update the container size which
+ // ultimately also updates the NSRuleEditor's frame, causing a loop
+ // - Calling -sizeToFit works, but only when the NSRuleEditor is growing. It won't shrink
+ // after removing rows.
+ // - -intrinsicContentSize is what we want, but that method is 10.7+, so on 10.6 let's do the
+ // easiest workaround (note that both -intrinsicContentSize and -sizeToFit internally use -[NSRuleEditor _minimumFrameHeight])
+ CGFloat wantsHeight;
+ if([filterRuleEditor respondsToSelector:@selector(intrinsicContentSize)]) {
+ NSSize sz = [filterRuleEditor intrinsicContentSize];
+ wantsHeight = sz.height;
+ }
+ else {
+ wantsHeight = [filterRuleEditor rowHeight] * [filterRuleEditor numberOfRows];
+ }
+ if(wantsHeight != preferredHeight) {
+ [self setPreferredHeight:wantsHeight];
+ [[NSNotificationCenter defaultCenter] postNotificationName:SPRuleFilterHeightChangedNotification object:self];
+ }
+}
+
+- (void)ruleEditorRowsDidChange:(NSNotification *)notification
+{
+ //TODO find a better way to trigger resize
+ // We can't do this here, because it will cause rows to jump around when removing them (the add case works fine, though)
+ [self performSelector:@selector(_resize) withObject:nil afterDelay:0.2];
+ //[self _resize];
+}
+
+- (void)dealloc
+{
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ SPClear(model);
+ SPClear(columns);
+ SPClear(contentFilters);
+ SPClear(numberOfDefaultFilters);
+ [super dealloc];
+}
+
+/**
+ * Sets the compare types for the filter and the appropriate formatter for the textField
+ */
+- (NSArray *)_compareTypesForColumn:(ColumnNode *)colNode
+{
+ if(contentFilters == nil
+ || ![contentFilters objectForKey:@"number"]
+ || ![contentFilters objectForKey:@"string"]
+ || ![contentFilters objectForKey:@"date"]) {
+ NSLog(@"Error while setting filter types.");
+ NSBeep();
+ return @[];
+ }
+
+ NSString *fieldTypeGrouping;
+ if([colNode typegrouping]) {
+ fieldTypeGrouping = [NSString stringWithString:[colNode typegrouping]];
+ }
+ else {
+ return @[];
+ }
+
+ NSMutableArray *compareItems = [NSMutableArray array];
+
+ NSString *compareType;
+
+ if ( [fieldTypeGrouping isEqualToString:@"date"] ) {
+ compareType = @"date";
+
+ /*
+ if ([fieldType isEqualToString:@"timestamp"]) {
+ [argumentField setFormatter:[[NSDateFormatter alloc]
+ initWithDateFormat:@"%Y-%m-%d %H:%M:%S" allowNaturalLanguage:YES]];
+ }
+ if ([fieldType isEqualToString:@"datetime"]) {
+ [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%Y-%m-%d %H:%M:%S" allowNaturalLanguage:YES]];
+ }
+ if ([fieldType isEqualToString:@"date"]) {
+ [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%Y-%m-%d" allowNaturalLanguage:YES]];
+ }
+ if ([fieldType isEqualToString:@"time"]) {
+ [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%H:%M:%S" allowNaturalLanguage:YES]];
+ }
+ if ([fieldType isEqualToString:@"year"]) {
+ [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%Y" allowNaturalLanguage:YES]];
+ }
+ */
+
+ // TODO: A bug in the framework previously meant enum fields had to be treated as string fields for the purposes
+ // of comparison - this can now be split out to support additional comparison fucntionality if desired.
+ }
+ else if ([fieldTypeGrouping isEqualToString:@"string"] || [fieldTypeGrouping isEqualToString:@"binary"]
+ || [fieldTypeGrouping isEqualToString:@"textdata"] || [fieldTypeGrouping isEqualToString:@"blobdata"]
+ || [fieldTypeGrouping isEqualToString:@"enum"]) {
+
+ compareType = @"string";
+ // [argumentField setFormatter:nil];
+
+ }
+ else if ([fieldTypeGrouping isEqualToString:@"bit"] || [fieldTypeGrouping isEqualToString:@"integer"]
+ || [fieldTypeGrouping isEqualToString:@"float"]) {
+ compareType = @"number";
+ // [argumentField setFormatter:numberFormatter];
+
+ }
+ else if ([fieldTypeGrouping isEqualToString:@"geometry"]) {
+ compareType = @"spatial";
+
+ }
+ else {
+ compareType = @"";
+ NSBeep();
+ NSLog(@"ERROR: unknown type for comparision: in %@", fieldTypeGrouping);
+ }
+
+ // Add IS NULL and IS NOT NULL as they should always be available
+ // [compareField addItemWithTitle:@"IS NULL"];
+ // [compareField addItemWithTitle:@"IS NOT NULL"];
+
+ // Remove user-defined filters first
+ if([numberOfDefaultFilters objectForKey:compareType]) {
+ NSUInteger cycles = [[contentFilters objectForKey:compareType] count] - [[numberOfDefaultFilters objectForKey:compareType] integerValue];
+ while(cycles > 0) {
+ [[contentFilters objectForKey:compareType] removeLastObject];
+ cycles--;
+ }
+ }
+
+ NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+
+#ifndef SP_CODA /* content filters */
+ // Load global user-defined content filters
+ if([prefs objectForKey:SPContentFilters]
+ && [contentFilters objectForKey:compareType]
+ && [[prefs objectForKey:SPContentFilters] objectForKey:compareType])
+ {
+ [[contentFilters objectForKey:compareType] addObjectsFromArray:[[prefs objectForKey:SPContentFilters] objectForKey:compareType]];
+ }
+
+ // Load doc-based user-defined content filters
+ if([[SPQueryController sharedQueryController] contentFilterForFileURL:[tableDocumentInstance fileURL]]) {
+ id filters = [[SPQueryController sharedQueryController] contentFilterForFileURL:[tableDocumentInstance fileURL]];
+ if([filters objectForKey:compareType])
+ [[contentFilters objectForKey:compareType] addObjectsFromArray:[filters objectForKey:compareType]];
+ }
+#endif
+
+ NSUInteger i = 0;
+ if([contentFilters objectForKey:compareType]) {
+ for (id filter in [contentFilters objectForKey:compareType]) {
+ // Create the tooltip
+ NSString *tooltip;
+ if ([filter objectForKey:@"Tooltip"])
+ tooltip = [filter objectForKey:@"Tooltip"];
+ else {
+ NSMutableString *tip = [[NSMutableString alloc] init];
+ if ([filter objectForKey:@"Clause"] && [(NSString *) [filter objectForKey:@"Clause"] length]) {
+ [tip setString:[[filter objectForKey:@"Clause"] stringByReplacingOccurrencesOfRegex:@"(?<!\\\\)(\\$\\{.*?\\})" withString:@"[arg]"]];
+ if ([tip isMatchedByRegex:@"(?<!\\\\)\\$BINARY"]) {
+ [tip replaceOccurrencesOfRegex:@"(?<!\\\\)\\$BINARY" withString:@""];
+ [tip appendString:NSLocalizedString(@"\n\nPress ⇧ for binary search (case-sensitive).", @"\n\npress shift for binary search tooltip message")];
+ }
+ [tip flushCachedRegexData];
+ [tip replaceOccurrencesOfRegex:@"(?<!\\\\)\\$CURRENT_FIELD" withString:[[colNode name] backtickQuotedString]];
+ [tip flushCachedRegexData];
+ tooltip = [NSString stringWithString:tip];
+ } else {
+ tooltip = @"";
+ }
+ [tip release];
+ }
+
+ OpNode *node = [[OpNode alloc] init];
+ [node setParentColumn:colNode];
+ [node setSettings:@{
+ @"title": ([filter objectForKey:@"MenuLabel"] ? [filter objectForKey:@"MenuLabel"] : @"not specified"),
+ @"tooltip": tooltip,
+ @"tag": @(i),
+ @"filterType": compareType,
+ }];
+ [node setFilter:filter];
+ [compareItems addObject:node];
+ [node release];
+ i++;
+ }
+ }
+
+ {
+ OpNode *node = [[OpNode alloc] init];
+ [node setParentColumn:colNode];
+ [node setSettings:@{
+ @"isSeparator": @YES,
+ }];
+ [compareItems addObject:node];
+ [node release];
+ }
+
+ {
+ OpNode *node = [[OpNode alloc] init];
+ [node setParentColumn:colNode];
+ [node setSettings:@{
+ @"title": NSLocalizedString(@"Edit Filters…", @"edit filter"),
+ @"tooltip": NSLocalizedString(@"Edit user-defined Filters…", @"edit user-defined filter"),
+ @"tag": @(i),
+ @"target": self,
+ @"action": [NSValue valueWithPointer:@selector(_editFiltersAction:)],
+ @"filterType": compareType,
+ }];
+ [compareItems addObject:node];
+ [node release];
+ }
+
+ return compareItems;
+}
+
+- (IBAction)_editFiltersAction:(id)sender
+{
+ if([sender isKindOfClass:[NSMenuItem class]]) {
+ NSMenuItem *menuItem = (NSMenuItem *)sender;
+ NSString *filterType = [(NSDictionary *)[menuItem representedObject] objectForKey:@"filterType"];
+ if([filterType unboxNull]) [self openContentFilterManagerForFilterType:filterType];
+ }
+}
+
+- (void)openContentFilterManagerForFilterType:(NSString *)filterType
+{
+ // init query favorites controller
+#ifndef SP_CODA
+ [[NSUserDefaults standardUserDefaults] synchronize];
+#endif
+ if(contentFilterManager) [contentFilterManager release];
+ contentFilterManager = [[SPContentFilterManager alloc] initWithDatabaseDocument:tableDocumentInstance forFilterType:filterType];
+
+ // Open query favorite manager
+ [NSApp beginSheet:[contentFilterManager window]
+ modalForWindow:[tableDocumentInstance parentWindow]
+ modalDelegate:contentFilterManager
+ didEndSelector:nil
+ contextInfo:nil];
+}
+
+- (void)_contentFiltersHaveBeenUpdated:(NSNotification *)notification
+{
+ //tell the rule editor to reload its criteria
+ [filterRuleEditor reloadCriteria];
+}
+
+- (BOOL)isEmpty
+{
+ return ([[self model] count] == 0);
+}
+
+- (void)addFilterExpression
+{
+ [filterRuleEditor insertRowAtIndex:0 withType:NSRuleEditorRowTypeSimple asSubrowOfRow:-1 animate:NO];
+}
+
+- (NSRuleEditor *)view
+{
+ return filterRuleEditor;
+}
+
+- (BOOL)isEnabled
+{
+ return enabled;
+}
+
+- (void)setEnabled:(BOOL)_enabled
+{
+ enabled = _enabled;
+ [filterButton setEnabled:_enabled];
+ [filterRuleEditor setEnabled:_enabled];
+}
+
+- (NSString *)sqlWhereExpressionWithBinary:(BOOL)isBINARY error:(NSError **)err
+{
+ NSMutableString *filterString = [[NSMutableString alloc] init];
+ NSError *innerError = nil;
+
+ @autoreleasepool {
+ //get the serialized filter and try to optimise it
+ NSDictionary *filterTree = [[self class] _flattenSerializedFilter:[self _serializedFilterIncludingFilterDefinition:YES]];
+
+ // build it recursively
+ [[self class] _writeFilterTree:filterTree toString:filterString wrapInParenthesis:NO binary:isBINARY error:&innerError];
+
+ [innerError retain]; // carry the error (if any) outside of the scope of the autoreleasepool
+ }
+
+ if(innerError) {
+ [filterString release];
+ if(err) *err = [innerError autorelease];
+ return nil;
+ }
+
+ if(err) *err = nil;
+
+ NSString *out = [filterString copy];
+ [filterString release];
+
+ return [out autorelease];
+}
+
+- (NSDictionary *)serializedFilter
+{
+ return [self _serializedFilterIncludingFilterDefinition:NO];
+}
+
+- (NSDictionary *)_serializedFilterIncludingFilterDefinition:(BOOL)includeDefinition
+{
+ NSMutableArray *rootItems = [NSMutableArray arrayWithCapacity:[model count]];
+ for(NSDictionary *item in model) {
+ [rootItems addObject:[self _serializeSubtree:item includingDefinition:includeDefinition]];
+ }
+ //the root serialized filter can either be an AND of multiple root items or a single root item
+ if([rootItems count] == 1) {
+ return [rootItems objectAtIndex:0];
+ }
+ else {
+ return @{
+ SerFilterClass: SerFilterClassGroup,
+ SerFilterGroupIsConjunction: @YES,
+ SerFilterGroupChildren: rootItems,
+ };
+ }
+}
+
+- (NSDictionary *)_serializeSubtree:(NSDictionary *)item includingDefinition:(BOOL)includeDefinition
+{
+ NSRuleEditorRowType rowType = (NSRuleEditorRowType)[[item objectForKey:@"rowType"] unsignedIntegerValue];
+ // check if we have an AND or OR compound row
+ if(rowType == NSRuleEditorRowTypeCompound) {
+ // process all children
+ NSArray *subrows = [item objectForKey:@"subrows"];
+ NSMutableArray *children = [[NSMutableArray alloc] initWithCapacity:[subrows count]];
+ for(NSDictionary *subitem in subrows) {
+ [children addObject:[self _serializeSubtree:subitem includingDefinition:includeDefinition]];
+ }
+ StringNode *node = [[item objectForKey:@"criteria"] objectAtIndex:0];
+ BOOL isConjunction = [@"AND" isEqualToString:[node value]];
+ NSDictionary *out = @{
+ SerFilterClass: SerFilterClassGroup,
+ SerFilterGroupIsConjunction: @(isConjunction),
+ SerFilterGroupChildren: children,
+ };
+ [children release];
+ return out;
+ }
+ else {
+ NSArray *criteria = [item objectForKey:@"criteria"];
+ NSArray *displayValues = [item objectForKey:@"displayValues"];
+ ColumnNode *col = [criteria objectAtIndex:0];
+ OpNode *op = [criteria objectAtIndex:1];
+ NSMutableArray *filterValues = [[NSMutableArray alloc] initWithCapacity:2];
+ for (NSUInteger i = 2; i < [criteria count]; ++i) { // the first two must always be column and operator
+ if([(RuleNode *)[criteria objectAtIndex:i] type] != RuleNodeTypeArgument) continue;
+ // if we found an argument, the displayValue will be an NSTextField we can ask for the value
+ NSString *value = [(NSTextField *)[displayValues objectAtIndex:i] stringValue];
+ [filterValues addObject:value];
+ }
+ NSDictionary *out = @{
+ SerFilterClass: SerFilterClassExpression,
+ SerFilterExprColumn: [col name],
+ SerFilterExprType: [[op settings] objectForKey:@"filterType"],
+ SerFilterExprComparison: [[op filter] objectForKey:@"MenuLabel"],
+ SerFilterExprValues: filterValues,
+ };
+ if(includeDefinition) {
+ out = [NSMutableDictionary dictionaryWithDictionary:out];
+ [(NSMutableDictionary *)out setObject:[op filter] forKey:SerFilterExprDefinition];
+ }
+ [filterValues release];
+ return out;
+ }
+}
+
+void _addIfNotNil(NSMutableArray *array, id toAdd)
+{
+ if(toAdd != nil) [array addObject:toAdd];
+}
+
+- (void)restoreSerializedFilters:(NSDictionary *)serialized
+{
+ if(!serialized) return;
+
+ // we have to exchange the whole model object or NSRuleEditor will get confused
+ NSMutableArray *newModel = [[NSMutableArray alloc] init];
+
+ @autoreleasepool {
+ // if the root object is an AND group directly restore its contents, otherwise restore the object
+ if(SerIsGroup(serialized) && [[serialized objectForKey:SerFilterGroupIsConjunction] boolValue]) {
+ for(NSDictionary *child in [serialized objectForKey:SerFilterGroupChildren]) {
+ _addIfNotNil(newModel, [self _restoreSerializedFilter:child]);
+ }
+ }
+ else {
+ _addIfNotNil(newModel, [self _restoreSerializedFilter:serialized]);
+ }
+ }
+
+ [self setModel:newModel];
+ [newModel release];
+}
+
+- (NSMutableDictionary *)_restoreSerializedFilter:(NSDictionary *)serialized
+{
+ NSMutableDictionary *obj = [[NSMutableDictionary alloc] initWithCapacity:4];
+
+ if(SerIsGroup(serialized)) {
+ [obj setObject:@(NSRuleEditorRowTypeCompound) forKey:@"rowType"];
+
+ StringNode *sn = [[StringNode alloc] init];
+ [sn setValue:([[serialized objectForKey:SerFilterGroupIsConjunction] boolValue] ? @"AND" : @"OR")];
+ // those have to be mutable arrays for the rule editor to work
+ NSMutableArray *criteria = [NSMutableArray arrayWithObject:sn];
+ [obj setObject:criteria forKey:@"criteria"];
+
+ id displayValue = [self ruleEditor:filterRuleEditor displayValueForCriterion:sn inRow:-1];
+ NSMutableArray *displayValues = [NSMutableArray arrayWithObject:displayValue];
+ [obj setObject:displayValues forKey:@"displayValues"];
+ [sn release];
+
+ NSArray *children = [serialized objectForKey:SerFilterGroupChildren];
+ NSMutableArray *subrows = [[NSMutableArray alloc] initWithCapacity:[children count]];
+ for(NSDictionary *child in children) {
+ _addIfNotNil(subrows, [self _restoreSerializedFilter:child]);
+ }
+ [obj setObject:subrows forKey:@"subrows"];
+ [subrows release];
+ }
+ else {
+ [obj setObject:@(NSRuleEditorRowTypeSimple) forKey:@"rowType"];
+ //simple rows can't have child rows
+ [obj setObject:[NSMutableArray array] forKey:@"subrows"];
+
+ NSMutableArray *criteria = [NSMutableArray arrayWithCapacity:5];
+
+ //first look up the column, bail if it doesn't exist anymore or types changed
+ NSString *columnName = [serialized objectForKey:SerFilterExprColumn];
+ ColumnNode *col = [self _columnForName:columnName];
+ if(!col) {
+ SPLog(@"cannot deserialize unknown column: %@", columnName);
+ goto fail;
+ }
+ [criteria addObject:col];
+
+ //next try to find the given operator
+ NSString *operatorName = [serialized objectForKey:SerFilterExprComparison];
+ OpNode *op = [self _operatorNamed:operatorName forColumn:col];
+ if(!op) {
+ SPLog(@"cannot deserialize unknown operator: %@",operatorName);
+ goto fail;
+ }
+ [criteria addObject:op];
+
+ // we still have to check if the current column type is the same as when we serialized because an operator
+ // with the same name can still act differently for different types
+ NSString *curFilterType = [[op settings] objectForKey:@"filterType"];
+ NSString *serFilterType = [serialized objectForKey:SerFilterExprType]; // this is optional
+ if(serFilterType && ![curFilterType isEqualToString:serFilterType]) {
+ SPLog(@"mismatch in filter types for operator %@: current=%@, serialized=%@",op,curFilterType,serFilterType);
+ goto fail;
+ }
+
+ //now we have to create the argument node(s)
+ NSInteger numOfArgs = [[[op filter] objectForKey:@"NumberOfArguments"] integerValue];
+ //fail if the current op requires more arguments than we have stored values for
+ NSArray *values = [serialized objectForKey:SerFilterExprValues];
+ if(numOfArgs > [values count]) {
+ SPLog(@"filter operator %@ requires %ld arguments, but only have %ld stored values!",op,numOfArgs,[values count]);
+ goto fail;
+ }
+
+ // otherwise add them
+ for (NSUInteger i = 0; i < numOfArgs; ++i) {
+ // insert connector node between args?
+ if(i > 0) {
+ ConnectorNode *node = [[ConnectorNode alloc] init];
+ [node setFilter:[op filter]];
+ [node setLabelIndex:(i-1)]; // label 0 follows argument 0
+ [criteria addObject:node];
+ [node release];
+ }
+ ArgNode *arg = [[ArgNode alloc] init];
+ [arg setArgIndex:i];
+ [arg setFilter:[op filter]];
+ [arg setInitialValue:[values objectAtIndex:i]];
+ [criteria addObject:arg];
+ [arg release];
+ }
+
+ [obj setObject:criteria forKey:@"criteria"];
+
+ //the last thing that remains is creating the displayValues for all criteria
+ NSMutableArray *displayValues = [NSMutableArray arrayWithCapacity:[criteria count]];
+ for(id criterion in criteria) {
+ id dispValue = [self ruleEditor:filterRuleEditor displayValueForCriterion:criterion inRow:-1];
+ if(!dispValue) {
+ SPLog(@"got nil displayValue for criterion %@ on deserialization!",criterion);
+ goto fail;
+ }
+ [displayValues addObject:dispValue];
+ }
+ [obj setObject:displayValues forKey:@"displayValues"];
+ }
+
+ return [obj autorelease];
+
+fail:
+ [obj release];
+ return nil;
+}
+
++ (NSDictionary *)makeSerializedFilterForColumn:(NSString *)colName operator:(NSString *)opName values:(NSArray *)values
+{
+ return @{
+ SerFilterClass: SerFilterClassExpression,
+ SerFilterExprColumn: colName,
+ SerFilterExprComparison: opName,
+ SerFilterExprValues: values,
+ };
+}
+
+- (ColumnNode *)_columnForName:(NSString *)name
+{
+ if([name length]) {
+ for (ColumnNode *col in columns) {
+ if ([name isEqualToString:[col name]]) return col;
+ }
+ }
+ return nil;
+}
+
+- (OpNode *)_operatorNamed:(NSString *)title forColumn:(ColumnNode *)col
+{
+ if([title length]) {
+ // check if we have the operator cache, otherwise build it
+ if(![col operatorCache]) {
+ NSArray *ops = [self _compareTypesForColumn:col];
+ [col setOperatorCache:ops];
+ }
+ // try to find it in the operator cache
+ for(OpNode *node in [col operatorCache]) {
+ if([[[node filter] objectForKey:@"MenuLabel"] isEqualToString:title]) return node;
+ }
+ }
+ return nil;
+}
+
+BOOL SerIsGroup(NSDictionary *dict)
+{
+ return [SerFilterClassGroup isEqual:[dict objectForKey:SerFilterClass]];
+}
+
+/**
+ * This method looks at the given serialized filter in a recursive manner and
+ * when it encounters
+ * - a group node with only a single child or
+ * - a child that is a group node of the same kind as the parent one
+ * it will pull the child(ren) up
+ *
+ * So for example:
+ * AND(expr1) => expr1
+ * AND(expr1,AND(expr2,expr3)) => AND(expr1,expr2,expr3)
+ *
+ * The input dict is not modified, the returned dict will be equal to the input
+ * dict or have parts of it removed or replaced with new dicts.
+ */
++ (NSDictionary *)_flattenSerializedFilter:(NSDictionary *)in
+{
+ // return non-group-nodes as is
+ if(!SerIsGroup(in)) return in;
+
+ NSNumber *inIsConjunction = [in objectForKey:SerFilterGroupIsConjunction];
+
+ // first give all children the chance to flatten (depth first)
+ NSArray *children = [in objectForKey:SerFilterGroupChildren];
+ NSMutableArray *flatChildren = [NSMutableArray arrayWithCapacity:[children count]];
+ NSUInteger changed = 0;
+ for(NSDictionary *child in children) {
+ NSDictionary *flattened = [self _flattenSerializedFilter:child];
+ //take a closer look at the (possibly changed) child - is it a group node of the same kind as us?
+ if(SerIsGroup(flattened) && [inIsConjunction isEqual:[flattened objectForKey:SerFilterGroupIsConjunction]]) {
+ [flatChildren addObjectsFromArray:[flattened objectForKey:SerFilterGroupChildren]];
+ changed++;
+ }
+ else if(flattened != child) {
+ changed++;
+ }
+ [flatChildren addObject:flattened];
+ }
+ // if there is only a single child, return it (flattening)
+ if([flatChildren count] == 1) return [flatChildren objectAtIndex:0];
+ // if none of the children changed return the original input
+ if(!changed) return in;
+ // last variant: some of our children changed, but we remain
+ return @{
+ SerFilterClass: SerFilterClassGroup,
+ SerFilterGroupIsConjunction: inIsConjunction,
+ SerFilterGroupChildren: flatChildren
+ };
+}
+
++ (void)_writeFilterTree:(NSDictionary *)in toString:(NSMutableString *)out wrapInParenthesis:(BOOL)wrap binary:(BOOL)isBINARY error:(NSError **)err
+{
+ NSError *myErr = nil;
+
+ if(wrap) [out appendString:@"("];
+
+ if(SerIsGroup(in)) {
+ BOOL isConjunction = [[in objectForKey:SerFilterGroupIsConjunction] boolValue];
+ NSString *connector = isConjunction ? @"AND" : @"OR";
+ BOOL first = YES;
+ NSArray *children = [in objectForKey:SerFilterGroupChildren];
+ for(NSDictionary *child in children) {
+ if(!first) [out appendFormat:@" %@ ",connector];
+ else first = NO;
+ // if the child is a group node but of a different kind we want to wrap it in order to prevent operator precedence confusion
+ // expression children will always be wrapped for clarity, except if there is only a single one and we are already wrapped
+ BOOL wrapChild = YES;
+ if(SerIsGroup(child)) {
+ BOOL childIsConjunction = [[child objectForKey:SerFilterGroupIsConjunction] boolValue];
+ if(isConjunction == childIsConjunction) wrapChild = NO;
+ }
+ else {
+ if(wrap && [children count] == 1) wrapChild = NO;
+ }
+ [self _writeFilterTree:child toString:out wrapInParenthesis:wrapChild binary:isBINARY error:&myErr];
+ if(myErr) {
+ if(err) *err = myErr;
+ return;
+ }
+ }
+ }
+ else {
+ // finally - build a SQL filter expression
+ NSDictionary *filter = [in objectForKey:SerFilterExprDefinition];
+ if(!filter) {
+ if(err) *err = [NSError errorWithDomain:SPErrorDomain code:0 userInfo:@{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Fatal error while retrieving content filter. No filter definition found.", @"filter to sql conversion : internal error : 0"),
+ }];
+ return;
+ }
+
+ if(![filter objectForKey:@"NumberOfArguments"]) {
+ if(err) *err = [NSError errorWithDomain:SPErrorDomain code:1 userInfo:@{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Error while retrieving filter clause. No “NumberOfArguments” key found.", @"filter to sql conversion : internal error : invalid filter definition (1)"),
+ }];
+ return;
+ }
+
+ if(![filter objectForKey:@"Clause"] || ![(NSString *)[filter objectForKey:@"Clause"] length]) {
+ if(err) *err = [NSError errorWithDomain:SPErrorDomain code:2 userInfo:@{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Content Filter clause is empty.", @"filter to sql conversion : internal error : invalid filter definition (2)"),
+ }];
+ return;
+ }
+
+ NSArray *values = [in objectForKey:SerFilterExprValues];
+
+ SPTableFilterParser *parser = [[SPTableFilterParser alloc] initWithFilterClause:[filter objectForKey:@"Clause"]
+ numberOfArguments:[[filter objectForKey:@"NumberOfArguments"] integerValue]];
+ [parser setArgument:[values objectOrNilAtIndex:0]];
+ [parser setFirstBetweenArgument:[values objectOrNilAtIndex:0]];
+ [parser setSecondBetweenArgument:[values objectOrNilAtIndex:1]];
+ [parser setSuppressLeadingTablePlaceholder:[[filter objectForKey:@"SuppressLeadingFieldPlaceholder"] boolValue]];
+ [parser setCaseSensitive:isBINARY];
+ [parser setCurrentField:[in objectForKey:SerFilterExprColumn]];
+
+ NSString *sql = [parser filterString];
+ // SPTableFilterParser will return nil if it doesn't like the arguments and NSMutableString doesn't like nil
+ if(!sql) {
+ if(err) *err = [NSError errorWithDomain:SPErrorDomain code:3 userInfo:@{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"No valid SQL expression could be generated. Make sure that you have filled in all required fields.", @"filter to sql conversion : internal error : SPTableFilterParser failed"),
+ }];
+ [parser release];
+ return;
+ }
+ [out appendString:sql];
+
+ [parser release];
+ }
+
+ if(wrap) [out appendString:@")"];
+}
+
+@end
+
+#pragma mark -
+
+@implementation RuleNode
+
+@synthesize type = type;
+
+- (NSUInteger)hash {
+ return type;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) return YES;
+ if (other && [[other class] isEqual:[self class]] && [(RuleNode *)other type] == type) return YES;
+
+ return NO;
+}
+
+@end
+
+@implementation ColumnNode
+
+@synthesize name = name;
+@synthesize typegrouping = typegrouping;
+@synthesize operatorCache = operatorCache;
+
+- (instancetype)init
+{
+ if((self = [super init])) {
+ type = RuleNodeTypeColumn;
+ }
+ return self;
+}
+
+- (NSString *)description
+{
+ return [NSString stringWithFormat:@"ColumnNode<%@@%p>",[self name],self];
+}
+
+- (NSUInteger)hash {
+ return ([name hash] ^ [typegrouping hash] ^ [super hash]);
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) return YES;
+ if (other && [[other class] isEqual:[self class]] && [name isEqualToString:[other name]] && [typegrouping isEqualToString:[other typegrouping]]) return YES;
+
+ return NO;
+}
+
+@end
+
+
+@implementation StringNode
+
+@synthesize value = value;
+
+- (instancetype)init
+{
+ if((self = [super init])) {
+ type = RuleNodeTypeString;
+ }
+ return self;
+}
+
+- (NSUInteger)hash {
+ return ([value hash] ^ [super hash]);
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) return YES;
+ if (other && [[other class] isEqual:[self class]] && [value isEqualToString:[(StringNode *)other value]]) return YES;
+
+ return NO;
+}
+
+@end
+
+@implementation OpNode
+
+@synthesize parentColumn = parentColumn;
+@synthesize settings = settings;
+@synthesize filter = filter;
+
+- (instancetype)init
+{
+ if((self = [super init])) {
+ type = RuleNodeTypeOperator;
+ }
+ return self;
+}
+
+- (void)dealloc
+{
+ [self setFilter:nil];
+ [self setSettings:nil];
+ [super dealloc];
+}
+
+- (NSUInteger)hash {
+ return (([parentColumn hash] << 16) ^ [settings hash] ^ [super hash]);
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) return YES;
+ if (other && [[other class] isEqual:[self class]] && [settings isEqualToDictionary:[(OpNode *)other settings]] && [parentColumn isEqual:[other parentColumn]]) return YES;
+
+ return NO;
+}
+
+@end
+
+@implementation ArgNode
+
+@synthesize filter = filter;
+@synthesize argIndex = argIndex;
+@synthesize initialValue = initialValue;
+
+- (instancetype)init
+{
+ if((self = [super init])) {
+ type = RuleNodeTypeArgument;
+ }
+ return self;
+}
+
+- (void)dealloc
+{
+ [self setInitialValue:nil];
+ [self setFilter:nil];
+ [super dealloc];
+}
+
+- (NSUInteger)hash {
+ // initialValue does not count towards hash because two Args are not different if only the initialValue differs
+ return ((argIndex << 16) ^ [filter hash] ^ [super hash]);
+}
+
+- (BOOL)isEqual:(id)other {
+ // initialValue does not count towards isEqual: because two Args are not different if only the initialValue differs
+ if (other == self) return YES;
+ if (other && [[other class] isEqual:[self class]] && [filter isEqualToDictionary:[(ArgNode *)other filter]] && argIndex == [(ArgNode *)other argIndex]) return YES;
+
+ return NO;
+}
+
+@end
+
+@implementation ConnectorNode
+
+@synthesize filter = filter;
+@synthesize labelIndex = labelIndex;
+
+- (instancetype)init
+{
+ if((self = [super init])) {
+ type = RuleNodeTypeConnector;
+ }
+ return self;
+}
+
+- (void)dealloc
+{
+ [self setFilter:nil];
+ [super dealloc];
+}
+
+- (NSUInteger)hash {
+ return ((labelIndex << 16) ^ [filter hash] ^ [super hash]);
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) return YES;
+ if (other && [[other class] isEqual:[self class]] && [filter isEqualToDictionary:[(ConnectorNode *)other filter]] && labelIndex == [(ConnectorNode *)other labelIndex]) return YES;
+
+ return NO;
+}
+
+@end