home *** CD-ROM | disk | FTP | other *** search
- //
- // MiscMergeCommand.m -- abstract class to build merge commands from
- // Written by Don Yacktman Copyright (c) 1995 by Don Yacktman.
- // Version 1.0. All rights reserved.
- // This notice may not be removed from this source code.
- //
- // This object is included in the MiscKit by permission from the author
- // and its use is governed by the MiscKit license, found in the file
- // "LICENSE.rtf" in the MiscKit distribution. Please refer to that file
- // for a list of all applicable permissions and restrictions.
- //
-
-
- #import <misckit/MiscMergeCommand.h>
- #import <misckit/misckit.h>
-
- @class MiscMergeEngine;
-
- @interface MiscMergeCommand(private)
- - _getQuotedArgumentStringFrom:(MiscString *)aString toEnd:(BOOL)endFlag;
- @end
-
- @implementation MiscMergeCommand
- /*" The MiscMergeCommand class implements a merge command. Since the
- MiscKit merge engine can dynamically add new commands, it is possible
- to create custom subclasses of MiscMergeCommand to implement new
- functionality in the engine. The merge engine already implements
- most of the commands a user would want, but certain applications may
- wish to override those commands, replace them, or add new commands.
-
- To create a subclass, two methods, -#{parseFromString:} and
- -#{executeForMerge:} need to be implemented. The first is expected to
- break up a text string into whatever arguments a particular MiscMergeCommand
- subclass needs in order to function. The second performs, during a merge,
- whatever special task the command is designed to do.
-
- The other methods in this object may be used by subclasses to aid in
- parsing the command string. They can grab key words, conditional operators,
- an arguments (single word or quoted string). A special kind of argument,
- promptable, is also supported. A promptable argument is expected to have
- its actual value determined at run time.
-
- When implementing commands, the full API of the MiscMergeEngine object is
- available. This allows the programmer to store information in the engine,
- manipulate the symbol tables used for resolving fields, and alter the
- output being created by the merge.
-
- The MiscKit source code is a good place to look for examples of how
- to implement various MiscMergeCommand subclasses.
- "*/
-
- - init
- /*" Causes an error, since the -#{initFrom:} method %{must} be called
- to initialize MiscMergeCommands. Frees self and returns nil.
- "*/
- {
- [super init];
- [self error:"Must use -initFrom: with MiscMergeCommand classes!\n"];
- [self free];
- return nil;
- }
-
- - initFrom:aString
- /*" Initializes a new instance of MiscMergeCommand, based upon the text
- of %{aString}. Actual parsing is performed by the -#{parseFromString:}
- method. If parsing fails and returns nil, then this method will free
- the receiving instance and return nil. Returns self otherwise.
- "*/
- {
- [super init];
- if (![self parseFromString:aString]) { // fatal parse error
- [self free];
- return nil;
- }
- return self;
- }
-
- - parseFromString:(MiscString *)aString
- /*" This method is called while parsing a merge template. The text of
- the full merge command is contained in %{aString}. This method should
- break %{aString} up into keywords, conditionals, and arguments as
- needed and store the results in instance variables for later use during
- merges. Note that returning self tells the template parsing machinery
- that all is well. Return nil if there is an error or the command cannot
- be properly initialized. (But do %{not} call -#{free} on self if nil
- is returned!)
- "*/
- {
- // subclass responsibility
- return nil;
- }
-
- - executeForMerge:(MiscMergeEngine *)aMerger
- /*" This method is called by the merge engine while it is
- performing a merge. The command is expected to perform it's
- specified function when this call is received. Returns self. The
- return value is currently ignored by the caller, which is usually,
- but does not have to be, %{aMerger}.
- "*/
- {
- // subclass responsibility
- return nil;
- }
-
- - (BOOL)eatKeyWord:(MiscString *)aKeyWord from:(MiscString *)aString
- isOptional:(BOOL)flag
- /*" Attempts to remove the contents of %{aKeyWord} from %{aString}.
- If %{flag} is YES, then no complaint will be made if %{aKeyWord} is
- missing. YES or NO is returned to tell the caller if the required
- key word was found or not, no matter what the value of %{flag} was.
- Note that it %{aKeyWord} is optional, then it will only be found
- if %{aString} has no whitespace at the beginning. This is a design
- decision, since the merger has a few pseudo-commands, such as copy,
- which could give a less desireable behavior if the whitespace were
- trimmed from the start of %{aString}. Since user-implemented
- MiscMergeCommand subclasses should pass YES for %{flag} this
- subtle design decision should not have a major impact.
- "*/
- {
- // If the keyword is optional, it may be implied, such as the "copy"
- // pseudo-command. If that is the case, then we don't want to alter
- // the string at all. So a copy command with the keyword in place
- // must not have any leading whitespace, since we won't attempt to
- // trim it. This can leave unwanted keywords in the parse stream
- // if you aren't careful... This is a problem with the design and
- // not a bug. This implementation will disturb things less than if
- // we always did a trim, so I prefer it.
- if (!flag) [aString trimLeadWhiteSpaces];
- if (![aString compareTo:aKeyWord n:[aKeyWord length]
- caseSensitive:NO]) {
- [aString removeFrom:0 length:[aKeyWord length]];
- return YES;
- }
- if (!flag) {
- [self error_keyword:aKeyWord];
- }
- return NO;
- }
-
- - _getQuotedArgumentStringFrom:(MiscString *)aString toEnd:(BOOL)endFlag
- {
- id ret = nil; int count = 0;
-
- [aString trimLeadWhiteSpaces];
- if ([aString charAt:0] == '"') {
- [aString removeFrom:0 length:1];
- }
-
- if (endFlag) { // eat to the end, regardless.
- [aString trimTailWhiteSpaces];
- if ([aString charAt:([aString length]-1)] == '"') {
- ret = [aString midFrom:0 length:([aString length]-1)];
- } else {
- [self error_closequote];
- ret = [aString copy];
- }
- [aString setStringValue:""];
- return ret;
- }
-
- while (!ret) {
- int right = [aString spotOf:'"'
- occurrenceNum:count caseSensitive:NO] - 1;
- if (right < -1) {
- ret = [aString copy];
- [self error_closequote];
- }
- if (right == -1) {
- ret = [MiscString new];
- [self error_closequote];
- }
- if ([aString charAt:(right - 1)] == '\\') {
- count += 1;
- } else {
- ret = [aString midFrom:0 to:right];
- }
- }
- [aString replace:[ret stringValue] with:""];
- return ret;
- }
-
- - getArgumentStringFrom:(MiscString *)aString toEnd:(BOOL)endFlag
- /*" Attempts to parse an argument from %{aString}. If %{endFlag}
- is set, then whatever is found to the end of %{aString} will be
- assumend to be the required argument. Otherwise, if an argument
- contains whitespace, it should be surrounded by quotation marks “"”.
- The parsed argument will be removed from %{aString}.
- "*/
- {
- [aString trimLeadWhiteSpaces];
- if ([aString charAt:0] == '"') {
- return [self _getQuotedArgumentStringFrom:aString toEnd:endFlag];
- } else {
- if (!endFlag) {
- id ret;
- int brk = [aString spotOfChars:" \t\n<>!=" occurrenceNum:0];
- if (brk <= 0) { // goes to end anyway
- id ret = [aString copy];
- [aString setStringValue:""];
- return ret;
- }
- ret = [aString midFrom:0 to:brk];
- [aString replace:[ret stringValue] with:""];
- return ret;
- } else {
- id ret = [aString copy];
- [aString setStringValue:""];
- return ret;
- }
- }
- return nil;
- }
-
- - getPromptFrom:(MiscString *)aString toEnd:(BOOL)endFlag
- /*" Attempts to parse a promptable argument from %{aString}. If the
- argument begins with a "?" then the argument is a "prompt". Removes
- the parsed argument from %{aString} and returns it. Returns nil if
- the wrong kind of argument was found. It is expected that a promptable
- argument's value will be determined at run time by asking the user for
- the value that should be stored for it.
- "*/
- {
- [aString trimLeadWhiteSpaces];
- if ([aString charAt:0] == '?') {
- [aString removeFrom:0 length:1];
- return [self getArgumentStringFrom:aString toEnd:endFlag];
- } else {
- [self error_noprompt];
- return nil;
- }
- return nil;
- }
-
- - getPromptableArgumentStringFrom:(MiscString *)aString
- wasPrompt:(BOOL *)prompt toEnd:(BOOL)endFlag
- /*" Attempts to parse an argument, which could be promptable, from %{aString}.
- If the argument begins with a "?" then the argument is a "prompt". Otherwise,
- a regular argument is parsed. Removes the parsed argument from %{aString}
- and returns it. %{prompt} is set to YES or NO depending upon what was parsed.
- "*/
- {
- [aString trimLeadWhiteSpaces];
- if ([aString charAt:0] == '?') {
- *prompt = NO;
- return [self getArgumentStringFrom:aString toEnd:endFlag];
- } else {
- *prompt = YES;
- return [self getPromptFrom:aString toEnd:endFlag];
- }
- return nil;
- }
-
- - (MISC_Merge_Cond_Op)getConditionalFrom:(MiscString *)aString
- /*" Attempts to parse a conditional from %{aString}. Currently recognized
- conditionals are: <>, ><, !=, <=, =<, >=, =>, <, >, ==, =. Returns the
- type of conditional found or MISC_MCO_NONE if an unrecognized conditional
- is found. Removes the parsed conditional from %{aString}.
- "*/
- {
- MISC_Merge_Cond_Op operator = MISC_MCO_NONE;
- id chars = [MiscString newWithString:"=!<> \t\n"];
- id condString = [MiscString new];
- [aString trimLeadWhiteSpaces];
- while ([chars spotOf:[aString charAt:0]] >= 0) {
- [condString addChar:[aString charAt:0]];
- [aString removeFrom:0 length:1];
- }
- [condString replaceEveryOccurrenceOfChars:" \t\n" with:""];
- if ([condString length] > 2) {
- [self error_conditional:condString];
- return MISC_MCO_NONE;
- }
- if ([condString length] < 1) return MISC_MCO_NONE;
- if (![condString cmp:"<>" n:2]) {
- operator = MISC_MCO_NOTEQUAL;
- } else if (![condString cmp:"><" n:2]) {
- operator = MISC_MCO_NOTEQUAL;
- } else if (![condString cmp:"!=" n:2]) {
- operator = MISC_MCO_NOTEQUAL;
- } else if (![condString cmp:"<=" n:2]) {
- operator = MISC_MCO_LESSTHANOREQUAL;
- } else if (![condString cmp:">=" n:2]) {
- operator = MISC_MCO_GREATERTHANOREQUAL;
- } else if (![condString cmp:"=>" n:2]) {
- operator = MISC_MCO_GREATERTHANOREQUAL;
- } else if (![condString cmp:"=<" n:2]) {
- operator = MISC_MCO_LESSTHANOREQUAL;
- } else if (![condString cmp:">" n:1]) {
- operator = MISC_MCO_GREATERTHAN;
- } else if (![condString cmp:"<" n:1]) {
- operator = MISC_MCO_LESSTHAN;
- } else if (![condString cmp:"==" n:2]) {
- operator = MISC_MCO_EQUAL;
- } else if (![condString cmp:"=" n:1]) {
- operator = MISC_MCO_EQUAL;
- } else {
- [self error_conditional:condString];
- return MISC_MCO_NONE;
- }
- return operator;
- }
-
- - (void)error_conditional:(MiscString *)theCond
- /*" This method is called if, while parsing, it is discovered that
- a conditional is unrecognized. Prints the name of the merge
- command class, the text “Unrecognized conditional:”, and the -#{stringValue}
- of %{theCond} to the console.
- "*/
- {
- fprintf(stderr, "%s: Unrecognized conditional: \"%s\".\n",
- [[self class] name], [theCond stringValue]);
- }
-
- - (void)error_keyword:(MiscString *)aKeyWord
- /*" This method is called if, while parsing, it is discovered that
- a required key word is missing. Prints the name of the merge
- command class, the text “Missing key word:”, and the -#{stringValue}
- of %{aKeyWord} to the console.
- "*/
- {
- fprintf(stderr, "%s: Missing key word: \"%s\".\n",
- [[self class] name], [aKeyWord stringValue]);
- }
-
- - (void)error_noprompt
- /*" This method is called if, while parsing, it is discovered that
- the required prompt is missing. (Referring to arguments that are
- promptable.) Prints the name of the merge
- command class and the text “Missing prompt.” to the console.
- "*/
- {
- fprintf(stderr, "%s: Missing prompt.\n", [[self class] name]);
- }
-
- - (void)error_closequote
- /*" This method is called if, while parsing, it is discovered that
- quotations are not matched up properly. Prints the name of the merge
- command class and the text “Closing quote missing or spurious extra
- argument added.” to the console.
- "*/
- {
- fprintf(stderr,
- "%s: Closing quote missing or spurious extra argument added.\n",
- [[self class] name]);
- }
-
- // Note: if aCommand doesn't implement the MiscMergeCondCallback protocol,
- // you'll get a NO back automatically.
- + (BOOL)evaluateConditionWith:(MiscMergeEngine *)anEngine
- for:aCommand
- /*" Evaluates the condition in %{aCommand} in the context of the merge
- in progress in %{anEngine}. Returns YES if the condition evaluated true
- and NO if not. If %{aCommand} doesn't implement the MiscMergeCondCallback
- protocol, you'll get a NO back automatically. This method should be used
- by MiscMergeCommand subclasses that need to evaluate conditionals as part
- of their task, such as the "if" command.
- "*/
- {
- id v1, firstOperand, secondOperand;
- int comparison;
-
- // return NO if the callback isn't implemented...
- if (![aCommand conformsTo:@protocol(MiscMergeCondCallback)]) return NO;
-
- // we cache this return value to save us 4 calls...
- v1 = [aCommand value1];
-
- // special evaluators for first argument
- if (![v1 casecmp:"--NONE--"]) return NO;
- if (![v1 casecmp:"--NO--"]) return NO;
- if (![v1 casecmp:"--ALL--"]) return YES;
- if (![v1 casecmp:"--YES--"]) return YES;
-
- // turn the operands into their field values; literals will
- // come through unchanged...
- firstOperand = [anEngine getField:v1];
- secondOperand = [anEngine getField:[aCommand value2]];
- // string compare is default, so do it first
- comparison = [firstOperand compareTo:secondOperand];
- // if both start with a digit, then we'll override with a numerical comp.
- if ((NXIsDigit([firstOperand charAt:0]) ||
- ([firstOperand charAt:0] == '-')) &&
- (NXIsDigit([secondOperand charAt:0]) ||
- ([secondOperand charAt:0] == '-'))) {
- float firstOp = [firstOperand floatValue];
- float secondOp = [secondOperand floatValue];
- if (firstOp < secondOp) comparison = -1;
- else if (firstOp > secondOp) comparison = 1;
- else comparison = 0;
- }
- // now that we have comparison results, turn them into a YES/NO
- // depending upon the chosen operator.
- switch ([aCommand operator]) {
- case MISC_MCO_NONE : { // true if field is non-empty
- if ([firstOperand length] > 0) return YES;
- break;
- }
- case MISC_MCO_EQUAL : {
- if (!comparison) return YES;
- break;
- }
- case MISC_MCO_NOTEQUAL : {
- if (comparison) return YES;
- break;
- }
- case MISC_MCO_LESSTHANOREQUAL : {
- if (!comparison || (comparison < 0)) return YES;
- break;
- }
- case MISC_MCO_GREATERTHANOREQUAL : {
- if (!comparison || (comparison > 0)) return YES;
- break;
- }
- case MISC_MCO_LESSTHAN : {
- if (comparison < 0) return YES;
- break;
- }
- case MISC_MCO_GREATERTHAN : {
- if (comparison > 0) return YES;
- break;
- }
- }
- return NO;
- }
-
- @end
-