/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jena.reasoner.rulesys.impl;
import java.util.*;
import org.apache.jena.graph.* ;
import org.apache.jena.reasoner.* ;
import org.apache.jena.reasoner.rulesys.* ;
import org.apache.jena.util.OneToManyMap ;
import org.apache.jena.util.PrintUtil ;
import org.apache.jena.util.iterator.WrappedIterator ;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A RETE version of the the forward rule system engine. It neeeds to reference
* an enclosing ForwardInfGraphI which holds the raw data and deductions.
*/
public class RETEEngine implements FRuleEngineI {
/** The parent InfGraph which is employing this engine instance */
protected ForwardRuleInfGraphI infGraph;
/** Set of rules being used */
protected List<Rule> rules;
/** Map from predicate node to clause processor, Node_ANY is used for wildcard predicates */
protected OneToManyMap<Node, RETENode> clauseIndex;
/** Queue of newly added triples waiting to be processed */
protected List<Triple> addsPending = new ArrayList<>();
/** A HashSet of pending triples for faster lookup */
protected HashSet<Triple> addsHash = new HashSet<>();
/** Queue of newly deleted triples waiting to be processed */
protected List<Triple> deletesPending = new ArrayList<>();
/** The conflict set of rules waiting to fire */
protected RETEConflictSet conflictSet;
/** Map from predicates used in the rules to object value they are used with, can be Node.ANY */
// protected Map<Node, Node> predicatePatterns;
protected OneToManyMap<Node, Node> predicatePatterns;
/** Flag, if true then there is a wildcard predicate in the rule set so that selective insert is not useful */
protected boolean wildcardRule;
/** Set to true to flag that derivations should be logged */
protected boolean recordDerivations;
/** performance stats - number of rules fired */
long nRulesFired = 0;
/** True if we have processed the axioms in the rule set */
boolean processedAxioms = false;
/** True if all the rules are monotonic, so we short circuit the conflict set processing */
boolean isMonotonic = true;
protected static Logger logger = LoggerFactory.getLogger(FRuleEngine.class);
// =======================================================================
// Constructors
/**
* Constructor.
* @param parent the F or FB infGraph that it using this engine, the parent graph
* holds the deductions graph and source data.
* @param rules the rule set to be processed
*/
public RETEEngine(ForwardRuleInfGraphI parent, List<Rule> rules) {
infGraph = parent;
this.rules = rules;
// Check if this is a monotonic rule set
isMonotonic = true;
for ( Rule r : rules )
{
if ( !r.isMonotonic() )
{
isMonotonic = false;
break;
}
}
}
/**
* Constructor. Build an empty engine to which rules must be added
* using setRuleStore().
* @param parent the F or FB infGraph that it using this engine, the parent graph
* holds the deductions graph and source data.
*/
public RETEEngine(ForwardRuleInfGraphI parent) {
infGraph = parent;
}
// =======================================================================
// Control methods
/**
* Process all available data. This should be called once a deductions graph
* has be prepared and loaded with any precomputed deductions. It will process
* the rule axioms and all relevant existing exiting data entries.
* @param ignoreBrules set to true if rules written in backward notation should be ignored
* @param inserts the set of triples to be processed, normally this is the
* raw data graph but may include additional deductions made by preprocessing hooks
*/
@Override
public void init(boolean ignoreBrules, Finder inserts) {
compile(rules, ignoreBrules);
findAndProcessAxioms();
fastInit(inserts);
}
/**
* Process all available data. This version expects that all the axioms
* have already be preprocessed and the clause index already exists.
* @param inserts the set of triples to be processed, normally this is the
* raw data graph but may include additional deductions made by preprocessing hooks
*/
@Override
public void fastInit(Finder inserts) {
conflictSet = new RETEConflictSet(new RETERuleContext(infGraph, this), isMonotonic);
// Below is used during testing to ensure that all ruleset work (if less efficiently) if marked as non-monotonic
// conflictSet = new RETEConflictSet(new RETERuleContext(infGraph, this), false);
findAndProcessActions();
if (infGraph.getRawGraph() != null) {
// Insert the data
if (wildcardRule) {
for (Iterator<Triple> i = inserts.find(new TriplePattern(null, null, null)); i.hasNext(); ) {
addTriple( i.next(), false );
}
} else {
for (Map.Entry<Node, Node> ent : predicatePatterns.entrySet()) {
// System.out.println("FastInit: " + ent.getKey() + " = " + ent.getValue());
for (Iterator<Triple> i = inserts.find(new TriplePattern(null, ent.getKey(), ent.getValue())); i.hasNext(); ) {
Triple t = i.next();
addTriple(t, false);
}
}
}
}
// Run the engine
runAll();
}
/**
* Add one triple to the data graph, run any rules triggered by
* the new data item, recursively adding any generated triples.
*/
@Override
public synchronized void add(Triple t) {
addTriple(t, false);
runAll();
}
/**
* Remove one triple to the data graph.
* @return true if the effects could be correctly propagated or
* false if not (in which case the entire engine should be restarted).
*/
@Override
public synchronized boolean delete(Triple t) {
deleteTriple(t, false);
runAll();
return true;
}
/**
* Return the number of rules fired since this rule engine instance
* was created and initialized
*/
@Override
public long getNRulesFired() {
return nRulesFired;
}
/**
* Return true if the internal engine state means that tracing is worthwhile.
* It will return false during the axiom bootstrap phase.
*/
@Override
public boolean shouldTrace() {
return true;
// return processedAxioms;
}
/**
* Set to true to enable derivation caching
*/
@Override
public void setDerivationLogging(boolean recordDerivations) {
this.recordDerivations = recordDerivations;
}
/**
* Access the precomputed internal rule form. Used when precomputing the
* internal axiom closures.
*/
@Override
public Object getRuleStore() {
return new RuleStore(clauseIndex, predicatePatterns, wildcardRule, isMonotonic);
}
/**
* Set the internal rule from from a precomputed state.
*/
@Override
public void setRuleStore(Object ruleStore) {
RuleStore rs = (RuleStore)ruleStore;
predicatePatterns = rs.predicatePatterns;
wildcardRule = rs.wildcardRule;
isMonotonic = rs.isMonotonic;
// Clone the RETE network to this engine
RETERuleContext context = new RETERuleContext(infGraph, this);
Map<RETENode, RETENode> netCopy = new HashMap<>();
clauseIndex = new OneToManyMap<>();
for ( Map.Entry<Node, RETENode> entry : rs.clauseIndex.entrySet() )
{
clauseIndex.put( entry.getKey(), entry.getValue().clone( netCopy, context ) );
}
}
/**
* Add a rule firing request to the conflict set.
*/
public void requestRuleFiring(Rule rule, BindingEnvironment env, boolean isAdd) {
conflictSet.add(rule, env, isAdd);
}
// =======================================================================
// Compiler support
/**
* Compile a list of rules into the internal rule store representation.
* @param rules the list of Rule objects
* @param ignoreBrules set to true if rules written in backward notation should be ignored
*/
public void compile(List<Rule> rules, boolean ignoreBrules) {
clauseIndex = new OneToManyMap<>();
predicatePatterns = new OneToManyMap<>();
wildcardRule = false;
for ( Rule rule : rules )
{
if ( ignoreBrules && rule.isBackward() )
{
continue;
}
int numVars = rule.getNumVars();
boolean[] seenVar = new boolean[numVars];
RETESourceNode prior = null;
for ( int i = 0; i < rule.bodyLength(); i++ )
{
Object clause = rule.getBodyElement( i );
if ( clause instanceof TriplePattern )
{
// Create the filter node for this pattern
ArrayList<Node> clauseVars = new ArrayList<>( numVars );
RETEClauseFilter clauseNode =
RETEClauseFilter.compile( (TriplePattern) clause, numVars, clauseVars );
Node predicate = ( (TriplePattern) clause ).getPredicate();
Node object = ( (TriplePattern) clause ).getObject();
if ( predicate.isVariable() )
{
clauseIndex.put( Node.ANY, clauseNode );
wildcardRule = true;
}
else
{
clauseIndex.put( predicate, clauseNode );
if ( !wildcardRule )
{
if ( object.isVariable() )
{
object = Node.ANY;
}
if ( Functor.isFunctor( object ) )
{
object = Node.ANY;
}
recordPredicatePattern( predicate, object );
}
}
// Create list of variables which should be cross matched between the earlier clauses and this one
ArrayList<Byte> matchIndices = new ArrayList<>( numVars );
for ( Iterator<Node> iv = clauseVars.iterator(); iv.hasNext(); )
{
int varIndex = ( (Node_RuleVariable) iv.next() ).getIndex();
if ( seenVar[varIndex] )
{
matchIndices.add( new Byte( (byte) varIndex ) );
}
seenVar[varIndex] = true;
}
// Build the join node
if ( prior == null )
{
// First clause, no joins yet
prior = clauseNode;
}
else
{
RETEQueue leftQ = new RETEQueue( matchIndices );
RETEQueue rightQ = new RETEQueue( matchIndices );
leftQ.setSibling( rightQ );
rightQ.setSibling( leftQ );
clauseNode.setContinuation( rightQ );
prior.setContinuation( leftQ );
prior = leftQ;
}
}
}
// Finished compiling a rule - add terminal
if ( prior != null )
{
RETETerminal term = createTerminal( rule );
prior.setContinuation( term );
}
}
if (wildcardRule) predicatePatterns = null;
}
/**
* Helper. Record a new predicate/value pattern. Ensure there is either
* one wildcard or a number of specific patterns
*/
private void recordPredicatePattern(Node predicate, Node value) {
if (predicatePatterns.contains(predicate, Node.ANY)) {
// already fully covered
} else if (value.equals(Node.ANY)) {
predicatePatterns.remove(predicate);
predicatePatterns.put(predicate, value);
} else {
// TODO possibly introduce a threshold here for the number of patterns allowed before generalizing
predicatePatterns.put(predicate, value);
}
return;
}
/**
* Create a terminal node for the RETE network. Normally this is RETETerminal
* but some people have experimented with alternative delete handling by
* creating RETETerminal subclasses so this hook simplifies use of that
* approach.
*/
protected RETETerminal createTerminal(Rule rule) {
return new RETETerminal(rule, this, infGraph);
}
// =======================================================================
// Internal implementation methods
/**
* Add a new triple to the network.
* @param triple the new triple
* @param deduction true if the triple has been generated by the rules and so should be
* added to the deductions graph.
*/
public synchronized void addTriple(Triple triple, boolean deduction) {
if (infGraph.shouldTrace()) {
logger.debug("Add triple: " + PrintUtil.print(triple));
}
if (deletesPending.size() > 0) deletesPending.remove(triple);
if (!addsHash.contains(triple)) { // Experimental, not sure why it wasn't done before
addsPending.add(triple);
addsHash.add(triple);
}
if (deduction) {
infGraph.addDeduction(triple);
}
}
/**
* Remove a new triple from the network.
* @param triple the new triple
* @param deduction true if the remove has been generated by the rules
*/
public synchronized void deleteTriple(Triple triple, boolean deduction) {
addsPending.remove(triple);
addsHash.remove(triple);
deletesPending.add(triple);
if (deduction) {
infGraph.getCurrentDeductionsGraph().delete(triple);
Graph raw = infGraph.getRawGraph();
// deduction retractions should not remove asserted facts, so commented out next line
// raw.delete(triple);
if (raw.contains(triple)) {
// Built in a graph which can't delete this triple
// so block further processing of this delete to avoid loops
deletesPending.remove(triple);
}
}
}
/**
* Increment the rule firing count, called by the terminal nodes in the
* network.
*/
protected void incRuleCount() {
nRulesFired++;
}
/**
* Find the next pending add triple.
* @return the triple or null if there are none left.
*/
protected synchronized Triple nextAddTriple() {
int size = addsPending.size();
if (size > 0) {
Triple t=addsPending.remove(size - 1);
addsHash.remove(t);
return t;
}
return null;
}
/**
* Find the next pending add triple.
* @return the triple or null if there are none left.
*/
protected synchronized Triple nextDeleteTriple() {
int size = deletesPending.size();
if (size > 0) {
return deletesPending.remove(size - 1);
}
return null;
}
/**
* Process the queue of pending insert/deletes until the queues are empty.
* Public to simplify unit tests - not normally called directly.
*/
public void runAll() {
while(true) {
boolean isAdd = false;
Triple next = nextDeleteTriple();
if (next == null) {
next = nextAddTriple();
isAdd = true;
}
if (next == null) {
// Nothing more to inject, if this is a non-mon rule set now process one rule from the conflict set
if (conflictSet.isEmpty()) return; // Finished
conflictSet.fireOne();
} else {
inject(next, isAdd);
}
}
}
/**
* Inject a single triple into the RETE network
*/
private void inject(Triple t, boolean isAdd) {
if (infGraph.shouldTrace()) {
logger.debug((isAdd ? "Inserting" : "Deleting") + " triple: " + PrintUtil.print(t));
}
Iterator<RETENode> i1 = clauseIndex.getAll(t.getPredicate());
Iterator<RETENode> i2 = clauseIndex.getAll(Node.ANY);
Iterator<RETENode> i = WrappedIterator.create(i1).andThen( i2 );
while (i.hasNext()) {
RETEClauseFilter cf = (RETEClauseFilter) i.next();
// firedRules guard in here?
cf.fire(t, isAdd);
}
}
/**
* This fires a triple into the current RETE network.
* This format of call is used in the unit testing but needs to be public
* because the tester is in another package.
*/
public void testTripleInsert(Triple t) {
Iterator<RETENode> i1 = clauseIndex.getAll(t.getPredicate());
Iterator<RETENode> i2 = clauseIndex.getAll(Node.ANY);
Iterator<RETENode> i = WrappedIterator.create( i1 ).andThen( i2 );
while (i.hasNext()) {
RETEClauseFilter cf = (RETEClauseFilter) i.next();
cf.fire(t, true);
}
}
/**
* Scan the rules for any axioms and insert those
*/
protected void findAndProcessAxioms() {
for ( Rule r : rules )
{
if ( r.isAxiom() )
{
// An axiom
RETERuleContext context = new RETERuleContext( infGraph, this );
context.setEnv( new BindingVector( new Node[r.getNumVars()] ) );
context.setRule( r );
if ( context.shouldFire( true ) )
{
RETEConflictSet.execute( context, true );
}
/* // Old version, left during debug and final checks
for (int j = 0; j < r.headLength(); j++) {
Object head = r.getHeadElement(j);
if (head instanceof TriplePattern) {
TriplePattern h = (TriplePattern) head;
Triple t = new Triple(h.getSubject(), h.getPredicate(), h.getObject());
addTriple(t, true);
}
}
*/
}
}
processedAxioms = true;
}
/**
* Scan the rules for any run actions and run those
*/
protected void findAndProcessActions() {
RETERuleContext tempContext = new RETERuleContext(infGraph, this);
for ( Rule r : rules )
{
if ( r.bodyLength() == 0 )
{
for ( int j = 0; j < r.headLength(); j++ )
{
Object head = r.getHeadElement( j );
if ( head instanceof Functor )
{
Functor f = (Functor) head;
Builtin imp = f.getImplementor();
if ( imp != null )
{
tempContext.setRule( r );
tempContext.setEnv( new BindingVector( r.getNumVars() ) );
imp.headAction( f.getArgs(), f.getArgLength(), tempContext );
}
else
{
throw new ReasonerException(
"Invoking undefined Functor " + f.getName() + " in " + r.toShortString() );
}
}
}
}
}
}
//=======================================================================
// Inner classes
/**
* Structure used in the clause index to indicate a particular
* clause in a rule. This is used purely as an internal data
* structure so we just use direct field access.
*/
protected static class ClausePointer {
/** The rule containing this clause */
protected Rule rule;
/** The index of the clause in the rule body */
protected int index;
/** constructor */
ClausePointer(Rule rule, int index) {
this.rule = rule;
this.index = index;
}
/** Get the clause pointed to */
TriplePattern getClause() {
return (TriplePattern)rule.getBodyElement(index);
}
}
/**
* Structure used to wrap up processed rule indexes.
*/
public static class RuleStore {
/** Map from predicate node to rule + clause, Node_ANY is used for wildcard predicates */
protected OneToManyMap<Node, RETENode> clauseIndex;
/** Map from predicates used in the rules to object value they are used with, can be Node.ANY */
protected OneToManyMap<Node, Node> predicatePatterns;
/** Flag, if true then there is a wildcard predicate in the rule set so that selective insert is not useful */
protected boolean wildcardRule;
/** True if all the rules are monotonic, so we short circuit the conflict set processing */
protected boolean isMonotonic = true;
/** Constructor */
RuleStore(OneToManyMap<Node, RETENode> clauseIndex, OneToManyMap<Node, Node> predicatesPatterns, boolean wildcardRule, boolean isMonotonic) {
this.clauseIndex = clauseIndex;
this.predicatePatterns = predicatesPatterns;
this.wildcardRule = wildcardRule;
this.isMonotonic = isMonotonic;
}
}
}