Extending The Tolog Engine

In tolog, the majority of the predicates you use come directly from associations in the topic map itself. Sometimes, however it is useful to be able to use other predicates. For example, the basic tolog language defines the predicates instance-of and direct-instance-of to determine the class membership of topics. The extension mechanism of the tolog engine enables other predicates to be developed and installed at runtime. To understand how extensions work, it is important to understand the way that tolog queries are evaluated.

A predicate in a tolog query is expressed as follows:

predicate-name( parameter-1-player:parameter-1-role,
                parameter-2-player:parameter-2-role, ...)
The role part of each parameter is optional. A parameter can be either a reference to a topic (typically by its XTM element ID); another object that has been created by the actions of a preceding predicate; or a variable:
my-predicate( topicA: role1, $A:$B )

When a predicate is evaluated, variables may be either bound or unbound. A bound variable is one to which a value has been assigned by the evaluation of some preceeding predicate in the query. An unbound variable is one which has yet to have a value assigned to it. A predicate matches when the parameters to it satisfy the predicate. If there are some unbound parameters on the input, then the predicate must determine which topics in the topic map can be found which satisfy the predicate - these are then used to create a list of potential bindings for the variable which will be passed on for further evaluation in any following predicates.

When you create a predicate extension, you are creating a functional object which can:

A predicate extension can be created either by implementing the interface org.tm4j.tologx.parser.Predicate or by extending the class org.tm4j.tologx.predicates.PredicateBase. These two options are described in more detail below. In general we advise that you implement your predicates by extending PredicateBase as this class provides basic utilities that can make implementation much easier.

If you intend to implement your own predicates, you are encouraged to take a look at the source for implementing the default tolog preicates which can all be found in the package org.tm4j.tologx.predicates. Most are good examples of using the org.tm4j.tologx.predicates.PredicateBase as the base for your extension predicates.

Warning

When looking at extensions for the first time, don't look at the class org.tm4j.tologx.predicates.DynamicAssociationPredicate. This is a much more complex example and will probably only confuse matter! The org.tm4j.tologx.predicates.InstanceOfPredicate is a much more easy to understand example.

Custom Predicates Implementing org.tm4j.tologx.parser.Predicate

This interface defines three methods which are described below:

setParameters/getParameters

These methods set and return the parameters for the predicate. These are the parameters contained in the query string so some items in the List may be org.tm4j.tologx.parser.Variable instances.

Where a parameter to a predicate has a role component associated with it, rather than get a Variable or a single Object value in the List, you will get an instance of the org.tm4j.tologx.parser.PlayerRoleVarPair class. This class provides methods to access the player and role parts of the predicate separately. See the Javadoc of this class for more detail.

initialise(TopicMap)

This method is invoked when the predicate is first added to the query to be executed. The TopicMap parameter specifies the topic map to be queried against.

matches(List, TologContext)

This method is invoked during the evaluation of the query. The List parameter is a list of the values for the parameters of the predicate. An unbound variable is represented by a Variable instance, bound variables or static parameter values defined in the query are represented by their value. The TologContext parameter is used by the query engine for tracking the state of the query evaluation and you should not need to access this value.

The return value for this method is a org.tm4j.tologx.utils.VariableSet instance which you must create an initialise for yourself. The VariableSet is used to carry the matches for any unbound variables in the match.

Custom Predicates Extending org.tm4j.tologx.predicates.PredicateBase

The PredicateBase class provides a more convenient base for creating your own predicates on. The class provides for automated parameter checks and has useful utility functions for intialising and populating the VariableSet to be returned by the predicate's processing.

matches(List params)

This is an abstract method which must be implemented by a predicate implementation. This is the main processing method invoked when processing the query. The parameter params is a list of the predicate parameters in parameter order. Where a parameter is still an unbound variable, it is represented in the list using a Variable instance.

The return value for this method is a VariableSet that specifies all of the mappings for the unbound variables in params that satisfy the predicate.

getPredicateName()

This abstract method returns the name for the predicate. This name string will be used in exception detail strings raised by the default parameter validation code.

validateParameters()

This method is invoked from the setParameters() method to check that the parameters match the expectations of the predicate in both number and parameter type. The default implementation of this method uses the methods getParamInfo(), getMinParamCount() and getMaxParamCount to perform the validation. You may, however, override this method to specify your own validation process (e.g. for predicates that accept a variable number of parameters).

getParamInfo()

This abstract method returns a List of PredicateBase.ParameterInfo instances which describe the parameters that the predicate takes. Each ParameterInfo instance defines the values allowed for one predicate. A ParameterInfo instance specifies two things about the parameter it describes:

  1. If the parameter can be specified with a variable. Parameters which do not allow a variable to be used are "input-only" parameters where the value MUST be specified in the query string.
  2. An array of the allowed classes for the parameter. This can be used to, for example restrict a predicate to only allow a String value for a parameter. If a value of some type other than the allowed types is provided, an exception will be raised when the query is processed. However, in most cases, it is preferable for a parameter to allow any type of object and then to simply fail to match on values it does not understand rather than cause an entire query to fail at runtime.

The ParameterInfo class provides constructors that allow you to specifiy either just whether a variable is allowed (in which case any type of parameter is also allowed); or to specify both whether a variable is allowed and the Class or Classes that are allowed for the parameter.

The number of ParameterInfo in the returned list is also treated as the default parameter count for the predicate. Unless one of the getMinParameters() or getMaxParameters() methods are overriden, the default validation code will require that the number of parameters passed to this predicate must be exactly the same as the number of ParameterInfo instances in this returned list.

getMinParamCount()/ getMaxParamCount()

These methods are invoked by the default parameter validation method and return an integer indicating the minimum and maximum number of parameters allowed by the predicate. If not overridden, these methods return the number of ParameterInfo instances returned by the getParamInfo() method.

getTopicMap()

Returns the topic map that this predicate is to process against.

initialiseResultsSet(VariableSet)

This method sets up the VariableSet passed in so that it can return values for all of the potentially unbound variables in the predicate. This method should be invoked with a VariableSet before using that VariableSet in any call to addResultsRow().

addResultsRow(VariableSet vs, Object[] matches)

This method adds a new row of variable mappings to the VariableSet vs, from the matches listed in the Object array matches. This method assumes that matches contains one object for each parameter of the predicate, in parameter order. The code scans matches and for each position where the original query specified an unbound variable, the matched object is retrieved from matches and added to the row to be put into vs.

Extension Predicate Example

The following code shows how the DirectInstanceOf predicate is implemented by extending PredicateBase.

/**
 * Implementation of the tolog built-in direct-instance-of
 * predicate.
 * <p>direct-instance-of($INST, $CLS) matches
 * where <code>$INST</code> is a Topic that has 
 * <code>$CLS</code> as one of its typing Topics.</p>
 */
public class DirectInstanceOfPredicate extends PredicateBase {

  /**
   * The reference name for this predicate in a query
   */
  public static final String PREDICATE_NAME = "direct-instance-of";
	
  /**
   * The parameters that can be processed by this predicate.
   * For this predicate, there MUST be two parameters and
   * a Variable may be used for either one.
   * NOTE: Although the predicate only matches on Topics,
   * we do not enforce that here, instead any parameter 
   * value that is not a Topic instance will simply not
   * return any matches.
   */
  private static final ParameterInfo[] PARAMETER_INFO = new ParameterInfo[] {
    new ParameterInfo(true),
    new ParameterInfo(true)
  };
	
  /**
   * This predicate makes use of the standard TopicTypesIndex
   */
  private TopicTypesIndex m_tti;
	
  /**
   * Initialisation method. This method opens the TopicTypesIndex
   * that the predicate will use for certain matches.
   * NOTE: This method calls super.initialise() which stores the
   * TopicMap reference for later use.
   * @param tm the TopicMap that this predicate will query over.
   */
  public void initialise(TopicMap tm) throws TologParserException {
    super.initialise(tm);
    try {
      m_tti = tm.getIndexManager().getTopicTypesIndex();
      if (!m_tti.isOpen()) m_tti.open();	
    } catch (Exception ex) {
      throw new TologParserException("Unable to initialise " + 
         PREDICATE_NAME + " predicate.", ex);
    }
  }

  /**
   * Returns the parameter information for this predicate.
   */
  public ParameterInfo[] getParamInfo() {
    return PARAMETER_INFO;
  }

  /**
   * Returns the reference name for the predicate.
   */
  public String getPredicateName() {
    return PREDICATE_NAME;
  }

  /**
   * Invoked to process a set of parameters for the predicate.
   */
  public VariableSet matches(List params) throws TologProcessingException {
    // Get the parameters - at this point we cannot make 
    // any assumption about the parameter types
    Object instParam = params.get(0);
    Object clsParam = params.get(1);
		
    // Create a new VariableSet to retrieve the results
    VariableSet ret = new VariableSet();
		
    // Use the PredicateBase.initialiseResultsSet()
    // method to set up this results set properly.
    // This enables us to use PredicateBase.adResultsRow()
    // to append results as we process.
    initialiseResultsSet(ret);
		
    // Select the method which will process our parameters
    if ((instParam instanceof Variable) && (clsParam instanceof Variable)) {
      // Both parameters are variables. We need to do an
      // "open" match of all topic types and their instances.
      openMatch(ret);
    } else if ((instParam instanceof Topic) && (clsParam instanceof Variable)) {
      // The class parameter is a variable but the instance parameter is 
      // a Topic. So we just need to find the typing topics of the 
      // instance.
      getClasses((Topic)instParam, ret);
    } else if ((instParam instanceof Variable) && (clsParam instanceof Topic)) {
      // Find all the topics that are typed by the topic
      // in clsParam
      getInstances((Topic)clsParam, ret);
    } else if ((instParam instanceof Topic) && (clsParam instanceof Topic)){
      // Just check that clsParam is a typing topic of instParam
      checkInstance((Topic)instParam, (Topic)clsParam, ret);
    }
    // Note that if either instParam or clsParam is neither
    // a Topic nor a Variable, then the results set will
    // remain empty.
    return ret;
  }

  /**
   * Checks if the Topic <code>inst</code> has the Topic
   * <code>cls</code> amongst its types. If so, then
   * the pair <code>inst,cls</code> are added to the
   * VariableSet <code>ret</code>.
   * @param inst the instance topic to check
   * @param cls  the typing topic to look for
   * @param ret  the VariableSet to take the results row if a match is found
   */
  private void checkInstance(Topic inst, Topic cls, VariableSet ret) throws TologProcessingException {
    Iterator it = inst.getTypes().iterator();
    while (it.hasNext()) {
      Topic type = (Topic)it.next();
      if (cls.equals(type)) {
        addResultsRow(ret, new Object[] {inst, cls});
      }
    }
  }

  /**
   * Adds a row to the results set <code>ret</code>
   * for every instance of the Topic <code>cls</code>.
   * One results row containing the pair <code>inst</code>,
   * <code>cls</code> is added to the VariableSet for
   * each Topic, <code>inst</code>, that has the Topic 
   * <code>cls</code> amongst its typing topics.
   * @param cls the typing topic to retrieve the instances of
   * @param ret the VariableSet to receive results rows
   */
  private void getInstances(Topic cls, VariableSet ret) throws TologProcessingException {
    Iterator it = m_tti.getTopicsOfType(cls).iterator();
    while (it.hasNext()) {
      addResultsRow(ret, new Object[] {it.next(), cls});
    }
  }

  /**
   * Gets all typing topics of the Topic <code>inst</code> and
   * for each one adds a pair <code>inst</code>, <code>cls</code>
   * to the VariableSet <code>ret</code>.
   * @param inst the instance Topic to be processed.
   * @param ret the VariableSet to receive results rows.
   */
  private void getClasses(Topic inst, VariableSet ret) throws TologProcessingException {
    Iterator it = inst.getTypes().iterator();
    while (it.hasNext()) {
      addResultsRow(ret, new Object[] {inst, it.next() });
    }
  }

  /**
   * Populates the VariableSet <code>ret</code> with one row
   * for each type-instance relationship between two Topics
   * in the queried TopicMap.
   * @param ret the VariableSet to receive the results rows.
   */
  private void openMatch(VariableSet ret) throws TologProcessingException {
    Iterator types = m_tti.getTopicTypes().iterator();
    while (types.hasNext()) {
      Topic type = (Topic)types.next();
      Iterator insts = m_tti.getTopicsOfType(type).iterator();
      while (insts.hasNext()) {
        addResultsRow(ret, new Object[] {insts.next(), type});
      }
    }
  }
}