RuleContainer.java
/*
** Module : RuleContainer.java
** Abstract : Abstract class to manage a list of rules
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ---------------------------------Description-------------------------------------
** 001 ECF 20050228 @20141 Created initial version. Manages a list of
** rules.
** 002 ECF 20050310 @20265 Moved expression library support from
** RuleSet subclass. This permits other
** subclasses to take advantage of this support
** also.
** 003 ECF 20050310 @20277 Removed language references. This feature set
** was obsoleted by a change to the expression
** parser which converts language token names
** to token types before expression compilation.
** 004 ECF 20050314 @20330 Added user variable support. A pool of
** variables is maintained within the scope of
** this rule container. The user may read/write
** these variables from logical expressions in
** rules contained directly or indirectly within
** this container.
** 005 ECF 20050315 @20409 Added constants for descent and ascent rule
** types.
** 006 GES 20050428 @20917 Added constant for next-child rule type.
** 007 ECF 20050429 @20962 Changes related to namespace support for user
** functions and variables. Moved pattern worker
** alias registration and management from the
** pattern engine. Only namespace aliases are
** managed by this class. The pattern workers
** themselves are managed by AstSymbolResolver.
** References to them are stored here, mapped by
** namespace alias.
** 008 ECF 20050526 @21294 Changes to support new expression engine
** implementation. Pattern worker and variable
** registration was changed significantly. Also
** added a static debugLevel construct for debug
** reporting.
** 009 GES 20050601 @21357 Converted the expression library from storing
** an infix string to storing a NamedExpression
** instance.
** 010 GES 20050602 @21382 Changes to implement the RuleListElement
** interface, to migrate the list of rules to
** a list of rules/rule containers, removal
** of the custom iterator processing and
** replacement with a recursive RuleListElement
** processing that allows containers to be
** arbitrarily nested. Some processing was
** moved here from PatternEngine to make the
** nesting possible.
** 011 GES 20050602 @21388 Converted NamedExpression to NamedFunction.
** 012 ECF 20050604 @21417 Create expressions using AstSymbolResolver's
** createExpression method. Use new Rule
** constructor.
** 013 ECF 20050607 @21432 Fixed stack overflow in getFunctionContainer
** method. Added the 'omit' parameter to prevent
** infinite loop caused by circular includes or
** by parent and child rule containers including
** the same entity. See also H014 (@21433).
** 014 ECF 20050607 @21433 The fix in H013 (@21432) will only prevent
** stack overflow due to circular includes with
** one degree of separation. This should cover
** the majority of (if not all) cases. If we
** find includes which are circular with greater
** degrees of separation, a more complex
** mechanism will need to be implemented.
** 015 GES 20060128 @24132 Exposed debug level and the related constants
** as public.
** 016 GES 20070514 @33495 Moved debug level constants into a separate
** interface.
** 017 SIY 20070612 @34022 Prevent direct/indirect recursion during
** calls to getFunctionContainer().
** 018 ECF 20080730 @39267 Reduced memory footprint. Lowered default
** sizes of all collections. Implemented cleanup
** method to release resources.
** 019 GES 20130203 Moved to using an Initializer class to allow a
** simple instance to be assigned to a new variable
** in addition to the previous expression approach.
** 020 ECF 20140403 Implemented generics.
** 021 HC 20160907 Implemented a simple tracing support for rules processing.
** 022 ECF 20171228 Improve performance by reducing context-local lookups.
** 023 ECF 20190620 EmptyIterator API change.
** 024 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
** TJD 20220504 Upgrade do Java 11 minor changes
** 025 CA 20230531 Moved the scope state from the resolver to the actual scope; this avoids using
** a WeakHashMap to determine the scope's state associated with a certain resolver,
** as long as a single resolver instance uses that scope (common for conversion).
** 026 CA 20230712 Performance improvement - cache the function's 'winner' container.
*/
/*
** 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.expr.*;
import com.goldencode.util.*;
/**
* Abstract base class for concrete implementations of rule containers.
* This class provides shared implementation logic for managing and
* applying a list of rules, for managing a library of named expressions,
* and for registering pattern workers to their associated namespaces.
* A rule container defines an expression engine scope for the purpose of
* resolving pattern worker libraries and user variables at expression
* parsing and compilation time. An instance may have an enclosing scope
* as well.
* <p>
* This implementation stores all rule types in a single list. A specialized
* iterator is used to visit only the types of rules required for a particular
* iteration (see {@link #rules}). This iterator is not accessed directly by
* client code. Instead, a concrete subclass will call the {@link #apply}
* method to iterate over the target set of rules and apply them in turn.
* <p>
* Named expressions support associates a library of aliased expression
* strings with a list of rules, such that any individual rule can access a
* pre-defined expression by its alias.
* <p>
* A pool of readable and writable user variables is registered with the
* expression engine, for use in rules contained directly or indirectly
* within this container. User variables are accessed directly by variable
* name in expressions. Duplicate variable names are not permitted.
* <p>
* The level of message output generated by the pattern engine as a whole
* is maintained by this class. It is accessible via static methods.
*
* @see #getDebugLevel
* @see #setDebugLevel
*/
abstract class RuleContainer
extends Scope
implements RuleListElement,
DebugLevels
{
/** Constant indicating no rule type */
protected static final int RULE_NONE = 0;
/** Constant indicating init-rule type */
protected static final int RULE_INIT = 1;
/** Constant indicating walk-rule type */
protected static final int RULE_WALK = 2;
/** Constant indicating post-rule type */
protected static final int RULE_POST = 3;
/** Constant indicating descent-rule type */
protected static final int RULE_DESCENT = 4;
/** Constant indicating next-child-rule type */
protected static final int RULE_NEXT_CHILD = 5;
/** Constant indicating ascent-rule type */
protected static final int RULE_ASCENT = 6;
/** Current debug level */
private static int debugLevel = MSG_NONE;
/** Pattern workers indexed by namespace alias */
private Map<String, PatternWorker> namespaces = null;
/** Parent container which provides enclosing scope */
private RuleContainer parent = null;
/** List of all rules of all types */
private ArrayList<RuleListElement> rules = null;
/** Rule containers included for their expression libraries */
private Set<RuleContainer> includes = null;
/** Library of named functions associated with this ruleset */
private Map<String, NamedFunction> functionLibrary = null;
/** Flag to prevent direct or indirect recursion during search. */
private boolean processingGetFunctionContainer = false;
/** Cache of the resolved container for a function name. */
private Map<String, RuleContainer> winners = new HashMap<>();
/**
* Constructor which assigns this container's enclosing scope.
*
* @param parent
* Rule container which provides enclosing scope to this rule
* container. May be <code>null</code>.
*/
protected RuleContainer(RuleContainer parent)
{
this.parent = parent;
}
/**
* Return the current debug level for the process.
*
* @return Debug level.
*/
public static int getDebugLevel()
{
return debugLevel;
}
/**
* Set the current debug level for the process.
*
* @param debugLevel
* Debug message output level using <code>MSG_*</code>
* constants.
*/
public static void setDebugLevel(int debugLevel)
{
RuleContainer.debugLevel = debugLevel;
}
/**
* Return the object which defines the enclosing scope to this rule
* container's scope.
*
* @return This container's parent scope. May be <code>null</code>.
*/
public Scope getEnclosingScope()
{
return parent;
}
/**
* Register a new user variable with this container. If the variable's
* name is already in use within this rule container's scope, it cannot
* be re-used and this method will fail. If both <code>type</code> and
* <code>expression</code> are provided, <code>type</code> must match the
* return type of <code>expression</code>.
*
* @param name
* Name by which this variable will be referenced in expressions.
* @param type
* Data type of the variable. May be <code>null</code> if and
* only if <code>expression</code> is not <code>null</code>
* <b>and</b> <code>expression</code>'s return type is not
* <code>null</code>.
* @param expression
* Expression which will be used to initialize the variable each
* time it is {@link com.goldencode.expr.Variable#reset reset}
* by the pattern engine. May be <code>null</code> if and only
* if <code>type</code> is not <code>null</code>.
*
* @throws SymbolException
* if the variable's name is already in use in this container's
* scope.
* @throws ExpressionException
* if <code>type</code> is a class which is not assignable from
* <code>expression</code>'s return type.
* @throws IllegalArgumentException
* if <code>type</code> is not provided and it cannot be
* determined from <code>expression</code>.
*
* @see com.goldencode.expr.SymbolResolver#resetVariables
*/
public void registerVariable(String name, Class<?> type, String expression)
{
ExpressionInitializer ei = null;
if (expression != null)
{
Expression expr = AstSymbolResolver.createExpression(expression);
ei = new ExpressionInitializer(expr);
}
AstSymbolResolver resolver = AstSymbolResolver.getResolver();
resolver.registerVariable(this, name, type, ei, false);
}
/**
* Register a new user variable with this container. If the variable's
* name is already in use within this rule container's scope, it cannot
* be re-used and this method will fail. If both <code>type</code> and
* <code>value</code> are provided, <code>type</code> must match the
* return type of <code>expression</code>.
*
* @param name
* Name by which this variable will be referenced in expressions.
* @param type
* Data type of the variable. May be <code>null</code> if and
* only if <code>value</code> is not <code>null</code>.
* @param value
* The specific instance to which the variable will be
* will be initialized. Unless assigned explicitly from a rule
* set, this instance will not be set to <code>null</code>
* using {@link com.goldencode.expr.Variable#reset reset} in the
* pattern engine on subsequent runs. May be <code>null</code> if
* and only if <code>type</code> is not <code>null</code>.
*
* @throws SymbolException
* if the variable's name is already in use in this container's
* scope.
* @throws IllegalArgumentException
* if <code>type</code> is not provided and it cannot be
* determined from <code>expression</code>.
*
* @see com.goldencode.expr.SymbolResolver#resetVariables
*/
public void registerVariableValue(String name, Class<?> type, Object value)
{
SimpleInitializer si = (value == null) ? null : new SimpleInitializer(value);
AstSymbolResolver resolver = AstSymbolResolver.getResolver();
resolver.registerVariable(this, name, type, si, false);
}
/**
* Store a pre-defined function under the specified name. The name acts
* as an alias for the function, which can be embedded in the
* <code>evalLib</code> or <code>execLib</code> user function to reference
* the named function in rules.
* <p>
* Note that if any named function already exists in the current library
* for this container with the alias <code>name</code> at the time this
* method is invoked, it will be replaced by <code>func</code>.
*
* @param name
* Alias under which to store the function.
* @param func
* The associated named function.
*/
public void putFunction(String name, NamedFunction func)
{
if (functionLibrary == null)
{
functionLibrary = new HashMap<>(4);
}
functionLibrary.put(name, func);
}
/**
* Register a pattern worker implementation with this rule container,
* such that its methods will be called by the engine at the appropriate
* times. Calls the pattern worker's {@link PatternWorker#initialize}
* method.
* <p>
* If an instance of the same pattern worker class is already registered,
* <code>worker</code> is discarded, and the previously registered
* instance is associated with <code>name</code> instead.
*
* @param name
* Optional namespace which can be used as a qualifier in
* expressions to indicate that this pattern worker should be
* used to resolve a constant to a literal. Should be
* non-<code>null</code> for any pattern worker implementation
* which implements {@link PatternWorker#resolveConstant}
* directly; may be <code>null</code> otherwise, however, only
* one <code>null</code> name is allowed per rule container.
* @param worker
* The pattern worker object to register.
*
* @return <code>true</code> if the worker was successfully registered;
* <code>false</code> if a worker of the same type was already
* registered. In either case, <code>namespace</code> is mapped
* to the registered worker upon return from this method.
*
* @throws IllegalArgumentException
* if <code>name</code> is already in use or if it is an invalid
* namespace identifier.
*/
public boolean registerWorker(String name, PatternWorker worker)
{
// Map worker to its namespace, if provided.
if (name != null)
{
name = name.trim();
// Make sure namespace is not all whitespace.
if (name.length() == 0)
{
throw new IllegalArgumentException(
"'" + name + "' is not a valid pattern worker namespace");
}
// Make sure this namespace is available.
if (namespaces != null && namespaces.containsKey(name))
{
throw new IllegalArgumentException(
"Pattern worker namespace '" + name + "' cannot be used more than once");
}
}
// If a worker of this type was already registered, that instance
// will be returned by the following call. Otherwise, our instance
// will be registered and returned to us.
AstSymbolResolver resolver = AstSymbolResolver.getResolver();
PatternWorker registered = resolver.registerWorker(worker);
// Map the registered worker to the given namespace.
if (name != null)
{
if (namespaces == null)
{
namespaces = new HashMap<>(4);
}
namespaces.put(name, registered);
}
// Register the worker library (if any) defined for this pattern
// worker, within the scope of this rule container.
Object library = registered.getLibrary();
if (library != null)
{
resolver.registerLibrary(this, name, library);
}
return (registered == worker);
}
/**
* Add a new, empty ruleset to the end of the current pipeline. Rulesets
* are processed in the order in which they are added to the pipeline.
* The ruleset is returned in order that the caller may configure it.
*
* @return Newly created ruleset.
*/
public RuleSet addRuleSet()
{
if (rules == null)
{
rules = new ArrayList<>(1);
}
RuleSet ruleSet = new RuleSet(this);
rules.add(ruleSet);
return ruleSet;
}
/**
* Add a rule of the specified type to the container for later use.
*
* @param expr
* Logical expression which will be compiled into the condition
* expression of the rule.
* @param type
* Type of rule to add: init, walk, post, etc., using the
* <code>RULE_*</code> constants.
* @param file
* The file the rule is defined in, used for tracing.
* @param line
* The file line the rule is defined at, used for tracing.
*
* @throws IllegalArgumentException
* if the rule type is unrecognized.
*/
public Rule addRule(String expr, int type, String file, int line)
{
switch (type)
{
case RULE_WALK:
case RULE_INIT:
case RULE_POST:
case RULE_DESCENT:
case RULE_NEXT_CHILD:
case RULE_ASCENT:
break;
default:
throw new IllegalArgumentException("Invalid rule type: " + type);
}
if (rules == null)
{
rules = new ArrayList<>(1);
}
Rule rule = new Rule(expr, type, file, line);
rules.add(rule);
return rule;
}
/**
* Return an iterator to the list of <code>RuleListElements</code>, each
* element of which can be either a <code>Rule</code> or a
* <code>RuleContainer</code> instance.
*/
public final ArrayList<RuleListElement> ruleList()
{
return rules;
}
/**
* Termination hook to allow resources to be cleaned up when this object is
* about to go out of service.
* <p>
* Cascades to all contained <code>RuleListElements</code>.
*/
public void cleanup()
{
if (rules != null)
{
int size = rules.size();
RuleListElement elem;
for (int i = 0; i < size; i++)
{
elem = rules.get(i);
elem.cleanup();
}
}
if (functionLibrary != null)
{
Iterator<NamedFunction> iter2 = functionLibrary.values().iterator();
while (iter2.hasNext())
{
NamedFunction func = iter2.next();
func.cleanup();
}
}
rules = null;
functionLibrary = null;
includes = null;
namespaces = null;
parent = null;
}
/**
* Apply every rule of the specified type, in the order they were added
* to this container.
*
* @param resolver
* AST symbol resolver.
* @param type
* Type of rule to apply: init, walk, or post, using the
* <code>RULE_*</code> constants.
*/
public void apply(AstSymbolResolver resolver, int type)
{
// save the current scope and reset the scope in the resolver to ourself
RuleContainer old = resolver.getRuleScope();
resolver.setRuleScope(this);
if (rules != null)
{
int size = rules.size();
RuleListElement next;
for (int i = 0; i < size; i++)
{
next = rules.get(i);
next.apply(resolver, type);
}
}
// reset the resolvers scope to the original
resolver.setRuleScope(old);
}
/**
* Reports that this instance is a rule container.
*
* @return Always <code>true</code>.
*/
public boolean isContainer()
{
return true;
}
/**
* Indicates whether this object's internal list of rules contains any
* rules 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 list of rules contains any rule of
* the specified type, else <code>false</code>.
*/
public boolean hasType(int type)
{
if (rules == null)
{
return false;
}
int size = rules.size();
RuleListElement next;
for (int i = 0; i < size; i++)
{
next = rules.get(i);
if (next.hasType(type))
{
return true;
}
}
return false;
}
/**
* Instantiate and register a pattern worker implementation, given its
* class name. The target class' default constructor is used. This results
* in the registered worker's {@link PatternWorker#initialize} method
* being invoked immediately after construction.
* <p>
* If an instance of the specified class is already registered, this
* request is ignored. There can be only one.
*
* @param name
* Optional namespace which can be used as a qualifier in
* expressions to indicate that this pattern worker should be
* used to resolve a constant to a literal. Should be
* non-<code>null</code> for any pattern worker implementation
* which implements {@link PatternWorker#resolveConstant}
* directly; may be <code>null</code> otherwise.
* @param className
* Fully qualified name of the pattern worker's class.
*
* @throws ClassNotFoundException
* if a class named <code>className</code> is not found in the
* JVM's classpath.
* @throws InstantiationException
* if an error occurs while instantiating the class (e.g., the
* default constructor throws an exception).
* @throws IllegalAccessException
* if the target class or its default constructor is not
* accessible.
* @throws ClassCastException
* if the target class does not implement the {@link
* PatternWorker} interface.
*/
boolean registerWorker(String name, String className)
throws ReflectiveOperationException
{
AstSymbolResolver resolver = AstSymbolResolver.getResolver();
PatternWorker worker = resolver.getRegisteredWorker(className);
if (worker == null)
{
worker = (PatternWorker) Class.forName(className).getDeclaredConstructor().newInstance();
}
return registerWorker(name, worker);
}
/**
* Retrieve the pattern worker, if any, registered for the given
* namespace.
*
* @param name
* Namespace identifier with which the requested pattern worker
* is associated. May not be <code>null</code>.
*
* @return The requested pattern worker instance, or <code>null</code>
* if no match was found.
*/
PatternWorker getWorker(String name)
{
if (name == null || namespaces == null)
{
return null;
}
return (PatternWorker) namespaces.get(name);
}
/**
* Add a rule container to this container's set of includes.
*
* @param included
* Rule container to include.
*/
void include(RuleContainer included)
{
if (includes == null)
{
includes = new LinkedHashSet<>(4);
}
includes.add(included);
}
/**
* Get a named function, given the alias which is used to uniquely
* identify that function.
*
* @param name
* Alias which identifies the function.
*
* @return The function aliased by <code>name</code>, or
* <code>null</code> if no function is found.
*/
NamedFunction getFunction(String name)
{
if (functionLibrary == null)
{
return null;
}
return (NamedFunction) functionLibrary.get(name);
}
/**
* Get the container instance which contains the function named
* <code>name</code> in its function library. This may be this container
* or one that this container includes, or <code>null</code>.
*
* @param name
* Name of function to look up.
* @param omit
* RuleContainer instance to omit from the list of includes to
* check. This prevents circular references between parents and
* included containers, or between included containers themselves.
*
* @return This container or one of its included containers, or
* <code>null</code> if neither this container nor any it
* includes can resolve the given name to an function.
*/
RuleContainer getFunctionContainer(String name, RuleContainer omit)
{
if (processingGetFunctionContainer)
{
return null;
}
try
{
processingGetFunctionContainer = true;
// Is this function library part of this current container?
if (functionLibrary != null && functionLibrary.containsKey(name))
{
return this;
}
RuleContainer winner = winners.get(name);
if (winner != null)
{
return winner;
}
// Perhaps it resides in one of our includes?
if (includes != null)
{
Iterator<RuleContainer> iter = includes.iterator();
while (winner == null && iter.hasNext())
{
RuleContainer next = iter.next();
if (next != omit)
{
winner = next.getFunctionContainer(name, this);
}
}
}
// Check our parent, recursively until the top of the tree is
// reached or the named function is found. Omit ourselves from our
// parent's check of its includes.
if (winner == null && parent != null)
{
winner = parent.getFunctionContainer(name, this);
}
winners.put(name, winner);
return winner;
}
finally
{
processingGetFunctionContainer = false;
}
}
}