|
|
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.