|
|
1.1 ! root 1: /* QBE.m: ! 2: * You may freely copy, distribute, and reuse the code in this example. ! 3: * NeXT disclaims any warranty of any kind, expressed or implied, as to its ! 4: * fitness for any particular use. ! 5: * ! 6: * Written by Craig Federighi ! 7: * ! 8: * ! 9: * Example of a Query By Example object: Connects to an EOController for an ! 10: * easy contruction of qualifiers from data entered into fields of an ! 11: * existing user interface. ! 12: */ ! 13: ! 14: #import "QBE.h" ! 15: #import "DictionaryDataSource.h" ! 16: ! 17: #import <eoaccess/eoaccess.h> ! 18: #import <foundation/NSString.h> ! 19: #import <foundation/NSArray.h> ! 20: ! 21: #import <foundation/NSCharacterSet.h> ! 22: #import <foundation/NSScanner.h> ! 23: ! 24: ! 25: ! 26: /* A private category to strip white spaces */ ! 27: ! 28: @interface NSString(_QBEStripWhite) ! 29: - (NSString *)stringStrippingLeadingCharacterSet:(NSCharacterSet *)skipset; ! 30: @end ! 31: ! 32: @implementation NSString(_QBEStripWhite) ! 33: ! 34: /* Returns autoreleased string produced by generating new string ! 35: * with the leading junk skipped. ! 36: */ ! 37: - (NSString *)stringStrippingLeadingCharacterSet:(NSCharacterSet *)skipset ! 38: { ! 39: NSRange range; ! 40: ! 41: range = [self rangeOfCharacterFromSet: [skipset invertedSet]]; ! 42: return [self substringFromIndex: range.location]; ! 43: } ! 44: ! 45: @end ! 46: ! 47: /* A category on EOAssociation to turn on and off editability of ! 48: * underlying UI objects ! 49: */ ! 50: @interface EOAssociation(_QBEEditable) ! 51: - (BOOL)isEditable; ! 52: - (void)setEditable:(BOOL)yn; ! 53: @end ! 54: ! 55: @implementation EOAssociation(_QBEEditable) ! 56: - (BOOL)isEditable { return NO; } ! 57: - (void)setEditable:(BOOL)yn {} ! 58: @end ! 59: ! 60: @implementation EOControlAssociation(_QBEEditable) ! 61: - (BOOL)isEditable { ! 62: Cell * cell; ! 63: if ([[self destination] isKindOfClass:[ActionCell class]]) ! 64: cell = [self destination]; ! 65: else ! 66: cell = [[self destination] cell]; ! 67: ! 68: return [cell isEditable]; ! 69: } ! 70: ! 71: - (void)setEditable:(BOOL)yn { ! 72: Cell * cell; ! 73: if ([[self destination] isKindOfClass:[ActionCell class]]) ! 74: cell = [self destination]; ! 75: else ! 76: cell = [[self destination] cell]; ! 77: ! 78: [cell setEditable:yn]; ! 79: } ! 80: @end ! 81: ! 82: @implementation EOColumnAssociation(_QBEEditable) ! 83: - (BOOL)isEditable {return [tableView isEditable]; } ! 84: - (void)setEditable:(BOOL)yn ! 85: { ! 86: [tableView setEditable:yn]; ! 87: } ! 88: @end ! 89: ! 90: ! 91: @implementation EOActionCellAssociation(_QBEEditable) ! 92: - (BOOL)isEditable { ! 93: return [(Cell *)[self destination] isEditable]; ! 94: } ! 95: ! 96: - (void)setEditable:(BOOL)yn { ! 97: [(Cell *)[self destination] setEditable:yn]; ! 98: } ! 99: @end ! 100: ! 101: ! 102: /* Need to tell what sort of associations can participate in QBE ! 103: * editing. Master-detail associations, for example, should be ! 104: * deactivated. ! 105: */ ! 106: @interface EOAssociation(_isQBEEditor) ! 107: - (BOOL)isQBEEditor; // Should remain active in QBE editing ! 108: @end ! 109: ! 110: @implementation EOAssociation(_isQBEEditor) ! 111: - (BOOL)isQBEEditor {return YES;} ! 112: @end ! 113: ! 114: @implementation EOQualifiedAssociation(_isQBEEditor) ! 115: - (BOOL)isQBEEditor {return NO;} ! 116: @end ! 117: ! 118: // Make sure an Adaptor is ready to convert values ! 119: // (the SybaseAdaptor can't do this until after it's connected) ! 120: @interface EOAdaptorChannel (canConvert) ! 121: - (BOOL)canConvertValues; ! 122: @end ! 123: ! 124: @implementation EOAdaptorChannel (canConvert) ! 125: - (BOOL)canConvertValues ! 126: { ! 127: EOAdaptorChannel *adaptorChannel = nil; ! 128: EOAdaptor *adaptor; ! 129: BOOL retValue = YES; ! 130: ! 131: if (![self isOpen]) { ! 132: adaptor = [[adaptorChannel adaptorContext] adaptor]; ! 133: if (![adaptor hasValidConnectionDictionary]){ ! 134: if (![adaptor runLoginPanelAndValidateConnectionDictionary]) ! 135: retValue = NO; ! 136: } ! 137: ! 138: if (retValue == YES && ![self openChannel]) ! 139: retValue = NO; ! 140: } ! 141: return retValue; ! 142: } ! 143: @end ! 144: ! 145: ! 146: ! 147: /* Actual implementation of QBE object */ ! 148: ! 149: @implementation QBE ! 150: ! 151: - init ! 152: { ! 153: return self; ! 154: } ! 155: ! 156: static NSString *operators[] = { ! 157: @"=", ! 158: @">", ! 159: @"<", ! 160: @"<=", ! 161: @">=", ! 162: NULL ! 163: }; ! 164: ! 165: ! 166: /* Build the qualifier based on the user input */ ! 167: ! 168: - qualifierForKey:(NSString *)key value:value entity:(EOEntity *)entity ! 169: { ! 170: EOAdaptorChannel *adaptorChannel = [[realsource databaseChannel] adaptorChannel]; ! 171: EOAdaptor *adaptor = [[adaptorChannel adaptorContext] adaptor]; ! 172: NSString **ops; ! 173: NSString *op = nil, *fmt; ! 174: id target = value; ! 175: EOQualifier *qualifier; ! 176: EOAttribute *attr; ! 177: BOOL isStringAttribute; ! 178: ! 179: // Be sure that we can connect to the database before constructing a ! 180: // qualifier. Without a connection, the adaptor can't ! 181: // formatValue:forAttribute: ! 182: if (![adaptorChannel canConvertValues]) ! 183: return nil; ! 184: ! 185: [adaptorChannel setDebugEnabled:YES]; ! 186: ! 187: // Check type of attribute so we know whether to quote target ! 188: attr = [entity attributeNamed:key]; ! 189: isStringAttribute = (strcmp([attr valueClassName], "NSString") == 0); ! 190: ! 191: // Look for an operator. If we see one, rip it off and use it ! 192: if ([value isKindOfClass:[NSString class]]) { ! 193: for(ops=operators; *ops; ops++) { ! 194: if ( [(NSString *)value hasPrefix: *ops] ) { ! 195: op = *ops; ! 196: ! 197: // Need to strip whitespace! ! 198: target = [[(NSString *)value substringFromIndex: [op length]] ! 199: stringStrippingLeadingCharacterSet: ! 200: [NSCharacterSet whitespaceCharacterSet]]; ! 201: break; ! 202: } ! 203: } ! 204: } ! 205: ! 206: // compute format string ! 207: if (isStringAttribute && !op) { ! 208: // Wildcard search ! 209: op = @"LIKE"; ! 210: target = [NSString stringWithFormat:@"%%%@%%", target]; ! 211: } else if (!op) { ! 212: // Default for non string attributes ! 213: op = @"="; ! 214: } ! 215: ! 216: fmt = @"%A %@ %@"; ! 217: qualifier = [[EOQualifier alloc] initWithEntity:entity ! 218: qualifierFormat:fmt, key, op, ! 219: [adaptor formatValue:target forAttribute:attr]]; ! 220: return [qualifier autorelease]; ! 221: } ! 222: ! 223: ! 224: - (EOQualifier *)makeQualifierForEo:eo entity:(EOEntity *)entity ! 225: conjoin:(BOOL)isConjoin ! 226: // Construct a qualifier for an EO ! 227: { ! 228: NSDictionary *valueDict; ! 229: NSArray *attributes; ! 230: NSMutableArray *attrnames; ! 231: int count, i; ! 232: EOQualifier *qualifier, *root = nil; ! 233: ! 234: // get a dictionary of values for the eo ! 235: attributes = [entity attributes]; ! 236: ! 237: count = [attributes count]; ! 238: attrnames = [NSMutableArray arrayWithCapacity:count]; ! 239: ! 240: // We should really only ask for the classAttributes ! 241: for(i=0; i<count; i++) ! 242: [attrnames addObject: ! 243: [(EOAttribute *)[attributes objectAtIndex:i] name]]; ! 244: ! 245: valueDict = [eo valuesForKeys:attrnames]; ! 246: ! 247: for(i=0; i<count; i++) { ! 248: NSString *key = [attrnames objectAtIndex:i]; ! 249: id value = [valueDict objectForKey:key]; ! 250: if( value ) { ! 251: qualifier = [self qualifierForKey:key value:value entity:entity]; ! 252: if (qualifier) { ! 253: if (root) { ! 254: if (isConjoin) ! 255: [root conjoinWithQualifier:qualifier]; ! 256: else ! 257: [root disjoinWithQualifier:qualifier]; ! 258: } else ! 259: root = qualifier; ! 260: } ! 261: } ! 262: } ! 263: ! 264: return root; ! 265: } ! 266: ! 267: - makeQualifier:(BOOL)isConjoin ! 268: { ! 269: EOEntity *entity; ! 270: NSArray *objects; ! 271: EOQualifier *root = nil; ! 272: int count, i; ! 273: ! 274: // force flush of changes to objects ! 275: [controller saveToObjects]; ! 276: objects = [controller allObjects]; ! 277: entity = [realsource entity]; ! 278: ! 279: for(count=[objects count],i=0; i < count; i++) { ! 280: id eo = [objects objectAtIndex:i]; ! 281: EOQualifier *q = [self makeQualifierForEo:eo entity:entity ! 282: conjoin:isConjoin]; ! 283: if (root && q) ! 284: [root disjoinWithQualifier:q]; ! 285: else ! 286: root = q; ! 287: } ! 288: ! 289: return root; ! 290: } ! 291: ! 292: - enterQueryMode:sender ! 293: // tweak the controller into QBE state: ! 294: // - no records, buffer edits, save to objects ! 295: { ! 296: DictionaryDataSource *dictsource; ! 297: NSArray *associations; ! 298: int i, count; ! 299: EOEntity *entity; ! 300: ! 301: if (realsource) ! 302: return [self addQuery:nil]; ! 303: ! 304: // flush everything we have now ! 305: [controller saveToDataSource]; ! 306: ! 307: // swap in tempory datasource ! 308: realsource = [(EODatabaseDataSource *)[controller dataSource] retain]; ! 309: dictsource = [[[DictionaryDataSource alloc] init] autorelease]; ! 310: [controller setDataSource: dictsource]; ! 311: ! 312: // We don't want any detail controllers to be updated to match the ! 313: // QBE entry (since it's meaningless and probably not complete) ! 314: ! 315: // Prepare array to add associations to ! 316: removedAssociations = [[NSMutableArray alloc] init]; ! 317: ! 318: // Remove all Qualified Associations ! 319: associations = [controller associations]; ! 320: for(count=[associations count], i=0; i < count; i++) { ! 321: EOAssociation *assoc = [associations objectAtIndex: i]; ! 322: if (![assoc isQBEEditor]) { ! 323: [removedAssociations addObject: assoc]; ! 324: [controller removeAssociation:assoc]; ! 325: } ! 326: } ! 327: ! 328: // Remove the next controller (for a fetch) ! 329: nextController = [controller nextController]; ! 330: [controller setNextController: nil]; ! 331: ! 332: // unset the delegate ! 333: controllersDelegate = [controller delegate]; ! 334: [controller setDelegate:nil]; ! 335: ! 336: // Should force all "qualifyable" associations (to database attributes) ! 337: // to editable and all others uneditable. ! 338: ! 339: // Make all remaining associations editable ! 340: // Remember those that weren't so we can restore them. ! 341: entity = [realsource entity]; ! 342: wereUneditableAssoc = [[NSMutableArray alloc] init]; ! 343: wereEditableAssoc = [[NSMutableArray alloc] init]; ! 344: associations = [controller associations]; ! 345: ! 346: // Note which are uneditable ! 347: for(count=[associations count], i=0; i < count; i++) { ! 348: EOAssociation *assoc = [associations objectAtIndex: i]; ! 349: BOOL isQualifyable = ([entity attributeNamed:[assoc key]] != nil); ! 350: if ( isQualifyable && ![assoc isEditable] ) { ! 351: [wereUneditableAssoc addObject:assoc]; ! 352: } else if (!isQualifyable && [assoc isEditable]) { ! 353: [wereUneditableAssoc addObject:assoc]; ! 354: } ! 355: } ! 356: // make them temporily editable ! 357: for(count=[wereUneditableAssoc count], i=0; i < count; i++) { ! 358: EOAssociation *assoc = [wereUneditableAssoc objectAtIndex: i]; ! 359: [assoc setEditable:YES]; ! 360: } ! 361: // make them temporily uneditable ! 362: for(count=[wereEditableAssoc count], i=0; i < count; i++) { ! 363: EOAssociation *assoc = [wereEditableAssoc objectAtIndex: i]; ! 364: [assoc setEditable:NO]; ! 365: } ! 366: ! 367: return [self addQuery:nil]; ! 368: } ! 369: ! 370: - addQuery:sender ! 371: { ! 372: int count; ! 373: if (!realsource) return nil; ! 374: ! 375: count = [[controller allObjects] count]; ! 376: ! 377: [controller insertObjectAtIndex:count]; ! 378: [controller setSelectionIndexes: ! 379: [NSArray arrayWithObject:[NSNumber numberWithInt:count]]]; ! 380: ! 381: return self; ! 382: } ! 383: ! 384: - exitQueryMode:sender ! 385: // restore old datasource and controller settings ! 386: { ! 387: int count, i; ! 388: ! 389: // restore datasource ! 390: [controller setDataSource: realsource]; ! 391: realsource = nil; // mark that we're out of query mode ! 392: ! 393: // restore removed qualified associations ! 394: for(count=[removedAssociations count], i=0; i<count; i++) { ! 395: EOAssociation *assoc = [removedAssociations objectAtIndex:i]; ! 396: [controller addAssociation:assoc]; ! 397: } ! 398: [removedAssociations release]; ! 399: ! 400: // Reset uneditable associations to back to uneditable again ! 401: for(count=[wereUneditableAssoc count], i=0; i<count; i++) { ! 402: EOAssociation *assoc = [wereUneditableAssoc objectAtIndex:i]; ! 403: [assoc setEditable:NO]; ! 404: } ! 405: [wereUneditableAssoc release]; ! 406: ! 407: // Reset editable associations to back to editable again ! 408: for(count=[wereEditableAssoc count], i=0; i<count; i++) { ! 409: EOAssociation *assoc = [wereEditableAssoc objectAtIndex:i]; ! 410: [assoc setEditable:YES]; ! 411: } ! 412: [wereEditableAssoc release]; ! 413: ! 414: ! 415: // restore next controller ! 416: [controller setNextController:nextController]; ! 417: ! 418: // restore the delegate ! 419: [controller setDelegate:controllersDelegate]; ! 420: ! 421: [controller fetch]; ! 422: ! 423: return self; ! 424: } ! 425: ! 426: - actionOutsideQueryMode ! 427: { ! 428: // We're not in query mode and they pushed a query button. ! 429: // Reset the qualifier and do a basic fetch ! 430: EODatabaseDataSource *source = (EODatabaseDataSource *)[controller dataSource]; ! 431: [source setAuxiliaryQualifier:nil]; ! 432: [controller fetch]; ! 433: return self; ! 434: } ! 435: ! 436: - applyQualifier:sender ! 437: // set qualifier to AND query for current attr settings, ! 438: // exit query mode and fetch ! 439: { ! 440: if (!realsource) ! 441: return [self actionOutsideQueryMode]; ! 442: ! 443: [realsource setAuxiliaryQualifier:[self makeQualifier:YES]]; // conjoin ! 444: return [self exitQueryMode:self]; ! 445: } ! 446: ! 447: - applyDisjointQualifier:sender ! 448: // set qualifier to AND query for current attr settings, ! 449: // exit query mode and fetch ! 450: { ! 451: if (!realsource) ! 452: return [self actionOutsideQueryMode]; ! 453: ! 454: [realsource setAuxiliaryQualifier:[self makeQualifier:NO]]; // Disjoin ! 455: return [self exitQueryMode:self]; ! 456: } ! 457: ! 458: - toggleQueryFetch:sender ! 459: { ! 460: return (realsource) ? [self applyQualifier:sender] ! 461: : [self enterQueryMode:sender]; ! 462: } ! 463: ! 464: @end ! 465:
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.