Rule.java
/*
** Module : Rule.java
** Abstract : Performs arbitrary actions based upon a conditional expression
**
** Copyright (c) 2005-2022, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20050221 @20102 Created initial version. Implementation of a
** conditional rule with zero or more actions.
** 002 ECF 20050318 @20422 Added support for an "else" condition. Now
** actions can be designated to run either upon
** the rule's condition evaluating to true or to
** false.
** 003 ECF 20050318 @20439 Store raw text of condition currently being
** evaluated in symbol resolver. This allows
** workers to access this information for
** reporting purposes.
** 004 ECF 20050429 @20961 Allow actions to be either logical or
** arithmetic expressions. This eliminates the
** need to wrap a user function which returns a
** numeric result inside a bogus logical
** expression.
** 005 ECF 20050525 @21291 Changes to support the new expression engine
** implementation. This code was significantly
** simplified by using the universal Expression
** object. Also added debug reporting.
** 006 GES 20050602 @21381 Changes to implement the RuleListElement
** interface.
** 007 ECF 20050603 @21396 Added rule type to debug rule report.
** 008 GES 20050603 @21397 Added support for nested rules and converted
** actions to be a simple case of a Rule object.
** 009 ECF 20050604 @21415 Create expressions using AstSymbolResolver's
** createExpression method. Changed constructor
** to remove AstSymbolResolver parameter, which
** is no longer necessary.
** 010 GES 20050623 @21545 Added WHILE looping support and left the
** current IF/ELSE construct intact.
** 011 ECF 20050726 @21831 Added break and continue support for while
** loops. These constructs are implemented as
** specialized actions which are added as any
** other actions to a while rule. In something
** of an abuse of exception handling, these
** actions throw BreakException and
** ContinueException, respectively, when
** applied. The exceptions are caught within the
** innermost while loop and the flow of control
** is altered accordingly.
** 012 ECF 20060421 @25643 Added debug feature. apply() method sets rule
** as the active rule in the pattern engine.
** Refactored rule report.
** 013 ECF 20080730 @39266 Added cleanup() method. Provides a hook for
** termination processing. Made break and
** continue processing slightly more efficient.
** 014 GES 20090518 @42388 Import change.
** 015 ECF 20150715 Replace StringBuffer with StringBuilder.
** 016 HC 20160907 Implemented a simple tracing support for rules processing.
** 017 ECF 20171228 Improve performance by reducing context-local lookups.
** 018 CA 20221010 Avoid using iterators on ArrayList - instead, use a 'for' loop and get the
** element by index, as iterators are more expensive in terms of memory and
** performance. Refs #6813
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.pattern;
import java.util.*;
import com.goldencode.ast.*;
import com.goldencode.expr.*;
/**
* A single rule which defines a list of zero of more actions and/or nested
* rules to be performed if this rule's condition is met. Consists of a
* conditional expression and an optional list of action expressions and/or
* nested rules.
* <p>
* As the pattern recognition engine processes an AST, it applies rules
* in a {@link RuleSet} to various nodes in the tree. A rule's condition
* expression is executed to determine whether that rule's actions need to
* be executed. A symbol resolver uses the data stored in an AST object to
* supply the information needed by the condition expression during
* expression. This data is exposed to user expressions via libraries
* registered by {@link PatternWorker} implementations.
* <p>
* An expression used for the rule's condition may contain any combination
* of variables and user functions, so long as the final operation of the
* expression resolves to a boolean result. It is this boolean result which
* determines whether any of the rule's actions/nested rules will be executed.
* Actions and nested rules can be set to register on either a
* <code>true</code> or <code>false</code> result from the condition
* expression.
* <p>
* Action expressions will typically contain a user function to act as a
* callback or hook to some processing, since variables and simple operators
* cannot be made to perform any task beyond simple evaluation to a boolean
* or numeric result. The value returned by an action is always ignored.
* <p>
* Rules are used in multiple ways. By embedding an action as a function in
* a rule's condition expression, one can perform some desired functionality
* unconditionally. This is in fact how a {@link RuleSet}'s initialization
* and termination rules are typically used, though they are not limited to
* this use.
* <p>
* By nesting rules within rules, one can build up an arbitrary set of
* control flow to match specific application requirements.
* <p>
* Using the <code>loop</code> instance member, this rule can be converted
* into a <code>while</code> loop instead of the default <code>if/else</code>
* control structure. Please see {@link #apply} for more details.
*
* @see RuleSet
* @see AstSymbolResolver
*/
public final class Rule
implements RuleListElement
{
/** Logical expression which forms the condition of this rule */
private Expression condition = null;
/** List of actions/rules to trigger if rule's condition is met */
private ArrayList<RuleListElement> trueActions = null;
/** List of actions/rules to trigger if rule's condition is not met */
private ArrayList<RuleListElement> falseActions = null;
/** Type of rule from {@link RuleContainer} constants */
private int type = RuleContainer.RULE_NONE;
/** If <code>true</code> this rule acts as a WHILE instead as an IF. */
private boolean loop = false;
/** The file this rule is defined in, used for tracing. */
private String file;
/** The file line this rule is defined at, used for tracing. */
private int line;
/**
* Constructor which accepts a resolver and an expression which represents
* the condition of this rule.
*
* @param infix
* A logical (boolean) expression, in infix notation, which
* defines the condition which will trigger this rule's action(s)
* to be executed.
* @param type
* Type of rule from {@link RuleContainer} constants.
*
* @see #apply
*/
public Rule(String infix, int type)
{
this(infix, type, null, 0);
}
/**
* Constructor which accepts a resolver and an expression which represents
* the condition of this rule.
*
* @param infix
* A logical (boolean) expression, in infix notation, which
* defines the condition which will trigger this rule's action(s)
* to be executed.
* @param type
* Type of rule from {@link RuleContainer} constants.
* @param file
* The file this rule is defined in, used for tracing.
* @param line
* The file line this rule is defined at, used for tracing.
* @see #apply
*/
public Rule(String infix, int type, String file, int line)
{
condition = AstSymbolResolver.createExpression(infix);
this.type = type;
this.file = file;
this.line = line;
}
/**
* Sets the state variable for whether this rule acts as a WHILE instead
* of as an IF.
*
* @param loop
* If <code>true</code>, this is a WHILE loop.
*/
public void setLoop(boolean loop)
{
this.loop = loop;
}
/**
* Gets the state variable for whether this rule acts as a WHILE instead
* of as an IF.
*
* @return If <code>true</code>, this is a WHILE loop.
*/
public boolean getLoop()
{
return loop;
}
/**
* Add an action to be executed in the event this rule's condition
* evaluates to <code>trigger</code>. This action is added to the end of
* the list of such actions to be performed, and it will be executed in the
* order of that list. The list is lazily created.
*
* @param infix
* An expression, in infix notation, which defines an action to
* execute if the specified condition evaluates to the value
* specified by <code>trigger</code>. This typically consists
* of one or more user functions.
* @param trigger
* The boolean result of evaluating this rule's condition which
* should trigger this action. That is, if <code>true</code>, the
* action will be triggered only if the condition evaluates to
* <code>true</code>. If <code>false</code>, the action will be
* triggered only if the condition evaluates to
* <code>false</code>.
* @param file
* The file the action is defined in, used for tracing.
* @param line
* The file line the action is defined at, used for tracing.
*
* @see #apply
*/
public void addAction(String infix, boolean trigger, String file, int line)
{
Rule action = new Rule(infix, type, file, line);
// Store the expression in the correct trigger list.
if (trigger)
{
if (trueActions == null)
{
trueActions = new ArrayList<RuleListElement>(1);
}
trueActions.add(action);
}
else
{
if (falseActions == null)
{
falseActions = new ArrayList<RuleListElement>(1);
}
falseActions.add(action);
}
}
/**
* Add a nested <code>Rule</code> to be executed in the event this rule's
* condition evaluates to <code>trigger</code>. This rule is added to the
* end of the list of such actions/rules to be performed, and it will be
* executed in the order of that list. The list is lazily created.
*
* @param rule
* The rule to execute if the specified condition evaluates to
* the value specified by <code>trigger</code>.
* @param trigger
* The boolean result of evaluating this rule's condition which
* should trigger this action. That is, if <code>true</code>, the
* rule will be triggered only if the condition evaluates to
* <code>true</code>. If <code>false</code>, the rule will be
* triggered only if the condition evaluates to
* <code>false</code>.
*
* @see #apply
*/
public void addRule(RuleListElement rule, boolean trigger)
{
// Store the expression in the correct trigger list.
if (trigger)
{
if (trueActions == null)
{
trueActions = new ArrayList<RuleListElement>(1);
}
trueActions.add(rule);
}
else
{
if (falseActions == null)
{
falseActions = new ArrayList<RuleListElement>(1);
}
falseActions.add(rule);
}
}
/**
* Add a break action to be executed in the event this rule's condition
* evaluates to <code>trigger</code>. This action is added to the end of
* the list of such actions to be performed, and it will be executed in the
* order of that list. The list is lazily created.
* <p>
* Upon execution of the break action, a {@link Rule.BreakException} is
* thrown.
*
* @param trigger
* The boolean result of evaluating this rule's condition which
* should trigger this action. That is, if <code>true</code>, the
* action will be triggered only if the condition evaluates to
* <code>true</code>. If <code>false</code>, the action will be
* triggered only if the condition evaluates to
* <code>false</code>.
*
* @see #apply
*/
public void addBreak(boolean trigger)
{
ExceptionAction action = new ExceptionAction()
{
public void apply(AstSymbolResolver resolver, int type)
{
throw BreakException.SINGLETON;
}
};
addRule(action, trigger);
}
/**
* Add a continue action to be executed in the event this rule's condition
* evaluates to <code>trigger</code>. This action is added to the end of
* the list of such actions to be performed, and it will be executed in the
* order of that list. The list is lazily created.
* <p>
* Upon execution of the break action, a {@link Rule.ContinueException} is
* thrown.
*
* @param trigger
* The boolean result of evaluating this rule's condition which
* should trigger this action. That is, if <code>true</code>, the
* action will be triggered only if the condition evaluates to
* <code>true</code>. If <code>false</code>, the action will be
* triggered only if the condition evaluates to
* <code>false</code>.
*
* @see #apply
*/
public void addContinue(boolean trigger)
{
ExceptionAction action = new ExceptionAction()
{
public void apply(AstSymbolResolver resolver, int type)
{
throw ContinueException.SINGLETON;
}
};
addRule(action, trigger);
}
/**
* Apply this rule (if and only if this rule's type matches the specified
* type) by evaluating its condition expression and executing
* the appropriate list of actions, based upon the result of evaluating
* the rule's condition. That is, if the condition evaluates to
* <code>true</code>, all "on true" actions defined for this rule are
* executed, in the order in which they were added, regardless of the
* success of any action previously executed in the list. If the condition
* evaluates to <code>false</code>, all "on false" actions defined for
* this rule are executed, in the order in which they were added,
* regardless of the success of any action previously executed in the
* list.
* <p>
* If the loop instance member is <code>true</code>, the core processing
* will be executed multiple times as long as the return value of the
* condition expression is not <code>null</code> and the return type
* is a <code>Boolean</code>. If the loop member is <code>false</code>,
* the core processing is essentially an IF/ELSE construct. It is
* important to note that any <code>falseActions</code> WILL NOT EVER be
* executed if <code>loop</code> is <code>true</code> since actions
* are only processed in the WHILE loop when the condition is
* <code>true</code>.
* <p>
* If trace level debugging is enabled, a rule report is emitted to
* <code>stdout</code> for debug purposes. This includes the current
* source and copy AST set in the symbol resolver, as well as details
* about the condition expression and each action which is executed in
* the course of applying this rule.
*
* @param resolver
* AST symbol resolver.
* @param type
* Type of rule for which to apply: init, walk, or post, using
* the <code>RULE_*</code> constants.
*/
public void apply(AstSymbolResolver resolver, int type)
{
// Immediately return if we do not represent a rule of the requested type
if (this.type != type)
{
return;
}
resolver.setCurrentCondition(condition.getInfix());
resolver.getPatternEngine().setActiveRule(this);
boolean trace = (RuleContainer.getDebugLevel() >= DebugLevels.MSG_TRACE);
if (trace)
{
System.out.println(createReportHeader());
if (trueActions != null || falseActions != null)
{
System.out.println("Actions:");
if (trueActions != null)
{
System.out.println(" On True:");
Iterator<RuleListElement> iter = trueActions.iterator();
while (iter.hasNext())
{
RuleListElement next = iter.next();
if (next instanceof Rule)
{
Rule rule = (Rule) next;
System.out.println(" " + rule.condition.getInfix());
}
}
}
if (falseActions != null)
{
System.out.println(" On False:");
Iterator<RuleListElement> iter = falseActions.iterator();
while (iter.hasNext())
{
RuleListElement next = iter.next();
if (next instanceof Rule)
{
Rule rule = (Rule) next;
System.out.println(" " + rule.condition.getInfix());
}
}
}
}
}
try
{
if (RulesTracing.isOn)
{
RulesTracing.pushLocation(file, line);
}
Object result = condition.execute();
// are we a WHILE or an IF
if (loop)
{
Boolean control = (Boolean) result;
// execute multiple times (null == false and the return type
// MUST be a boolean)
while (control != null && control.booleanValue())
{
try
{
coreProcessing(resolver, result);
}
catch (BreakException exc)
{
if (trace)
{
System.out.println("-- BREAK loop [" + condition.getInfix() + "] --");
}
break;
}
catch (ContinueException exc)
{
if (trace)
{
System.out.println("-- CONTINUE loop: [" + condition.getInfix() + "] --");
}
control = (Boolean) condition.execute();
continue;
}
control = (Boolean) condition.execute();
}
}
else
{
// only execute once
coreProcessing(resolver, result);
}
}
finally
{
if (RulesTracing.isOn)
{
RulesTracing.popLocation();
}
}
resolver.setCurrentCondition(null);
if (trace)
{
System.out.println(createReportFooter());
}
}
/**
* Indicates whether this object either represents a rule of the specified
* type.
*
* @param type
* Type of rule for which to search: init, walk, or post, using
* the <code>RULE_*</code> constants.
*
* @return <code>true</code> if the object is a rule of the specified
* type, else <code>false</code>.
*/
public boolean hasType(int type)
{
return (this.type == type);
}
/**
* Reports that this instance is NOT rule container.
*
* @return Always <code>false</code>.
*/
public boolean isContainer()
{
return false;
}
/**
* Termination hook to allow resources to be cleaned up when this object is
* about to go out of service.
* <p>
* Cascades to all <code>RuleListElement</code>s managed by this rule.
*/
public void cleanup()
{
condition = null;
if (trueActions != null)
{
int size = trueActions.size();
RuleListElement elem;
for (int i = 0; i < size; i++)
{
elem = trueActions.get(i);
elem.cleanup();
}
trueActions = null;
}
if (falseActions != null)
{
int size = falseActions.size();
RuleListElement elem;
for (int i = 0; i < size; i++)
{
elem = falseActions.get(i);
elem.cleanup();
}
falseActions = null;
}
}
/**
* Get the rule type; one of the {@link RuleContainer} constants.
*
* @return Rule type.
*/
int getType()
{
return type;
}
/**
* Create a simple report which describes the current state of this rule.
* Includes rule type, condition expression, whether it is a loop, and the
* source and copy AST to which the rule currently is being applied.
*
* @return Text report as described above.
*/
String createReport()
{
StringBuilder buf = new StringBuilder();
buf.append(createReportHeader());
buf.append(createReportFooter());
return buf.toString();
}
/**
* Create a simple report header which describes the current state of this
* rule. Includes rule type, condition expression, whether it is a loop,
* and the source and copy AST to which the rule currently is being
* applied.
*
* @return Text report header as described above.
*/
private String createReportHeader()
{
AstSymbolResolver resolver = AstSymbolResolver.getResolver();
Aast srcAst = resolver.getSourceAst();
Aast copyAst = resolver.getCopyAst();
String sep = System.getProperty("line.separator");
StringBuilder buf = new StringBuilder();
buf.append("-----------------------");
buf.append(sep);
buf.append(" RULE REPORT ");
buf.append(sep);
buf.append("-----------------------");
buf.append(sep);
buf.append("Rule Type : " + getRuleTypeName());
buf.append(sep);
buf.append("Source AST: " + composeAstString(srcAst));
buf.append(sep);
buf.append("Copy AST : " + composeAstString(copyAst));
buf.append(sep);
buf.append("Condition : " + condition.getInfix());
buf.append(sep);
buf.append("Loop : " + loop);
buf.append(sep);
return buf.toString();
}
/**
* Create a standard footer which is appended to a report describing the
* current state of this rule.
*
* @return Static report footer.
*/
private String createReportFooter()
{
String sep = System.getProperty("line.separator");
StringBuilder buf = new StringBuilder();
buf.append("--- END RULE REPORT ---");
buf.append(sep);
buf.append(sep);
return buf.toString();
}
/**
* Centralizes the core processing of this rule so that multiple types
* of control flow construct can be implemented externally but the common
* processing can be shared.
*
* @param resolver
* AST symbol resolver.
* @param result
* The return value from the condition expression of this rule.
*/
private void coreProcessing(AstSymbolResolver resolver, Object result)
{
if (result == null || result instanceof Boolean)
{
boolean rc = (result == null) ? false : ((Boolean) result).booleanValue();
executeActions(resolver, rc ? trueActions : falseActions);
}
else if (trueActions != null || falseActions != null)
{
throw new IllegalArgumentException(
"Condition must evaluate to true/false to trigger action(s): " +
condition.getInfix());
}
}
/**
* Execute all actions/rules in the specified list, in order, discarding
* results of the action expressions themselves.
*
* @param resolver
* AST symbol resolver.
* @param actions
* The list of actions/rules to be executed, either
* {@link #trueActions} (if the rule's condition evaluated to
* <code>true</code>), or {@link #falseActions} (if the rule's
* condition evaluated to <code>false</code>).
*/
private void executeActions(AstSymbolResolver resolver, ArrayList<RuleListElement> actions)
{
if (actions == null)
{
return;
}
int size = actions.size();
RuleListElement action;
for (int i = 0; i < size; i++)
{
action = actions.get(i);
action.apply(resolver, type);
}
}
/**
* Compose and return a string which represents the contents of an AST.
* This includes the AST's text, its token path from its root, its line
* and column number and its ID.
* <p>
* This text is used in the rule report generated for debug purposes.
*
* @param ast
* AST on which the string is based.
*
* @return Text describing the AST.
*/
private String composeAstString(Aast ast)
{
if (ast == null)
{
return null;
}
StringBuilder buf = new StringBuilder();
buf.append("[ ")
.append(ast.getText())
.append(" ] ")
.append(ast.getPath())
.append(" @")
.append(ast.getLine())
.append(":")
.append(ast.getColumn())
.append(" {")
.append(ast.getId())
.append("}");
return buf.toString();
}
/**
* Debug helper method to get the descriptive name of this rule's type.
*
* @return Rule type name.
*/
private String getRuleTypeName()
{
switch (type)
{
case RuleContainer.RULE_WALK:
return "WALK";
case RuleContainer.RULE_INIT:
return "INIT";
case RuleContainer.RULE_POST:
return "POST";
case RuleContainer.RULE_DESCENT:
return "DESCENT";
case RuleContainer.RULE_NEXT_CHILD:
return "NEXT_CHILD";
case RuleContainer.RULE_ASCENT:
return "ASCENT";
case RuleContainer.RULE_NONE:
return "NONE";
}
return "UNKNOWN";
}
/**
* Abstract base class for specialized action implementations which throw
* an exception when applied. Provides a common base class adapter for
* anonymous class implementations when adding break and continue actions
* which terminate and continue while loops respectively. This is simply
* a convenience adapter to implement some of the methods required by the
* {@link RuleListElement} interface.
*/
private abstract class ExceptionAction
implements RuleListElement
{
/**
* Returns whether the enclosing rule's type matches the specified
* type.
*
* @return <code>true</code> if the enclosing rule's type ==
* <code>type</code>, else <code>false</code>.
*/
public boolean hasType(int type)
{
return (Rule.this.type == type);
}
/**
* Returns false.
*
* @return <code>false</code>.
*/
public boolean isContainer()
{
return false;
}
/**
* Termination hook to allow resources to be cleaned up when this object
* is about to go out of service.
*/
public void cleanup()
{
}
}
/**
* Specialized exception type thrown by a break action to terminate a
* loop within a while rule.
*/
private static class BreakException
extends RuntimeException
{
/** Exception that is re-used for every break rule */
private static final BreakException SINGLETON = new BreakException();
/**
* Default constructor.
*/
BreakException()
{
super();
}
}
/**
* Specialized exception type thrown by a continue action to continue a
* loop within a while rule.
*/
private static class ContinueException
extends RuntimeException
{
/** Exception that is re-used for every continue rule */
private static final ContinueException SINGLETON = new ContinueException();
/**
* Default constructor.
*/
ContinueException()
{
super();
}
}
}