Construction de l’instance

Nous allons détailler ici les différentes solutions offertes pour obtenir une instance fonctionnelle du moteur de règles.

Les API Java

Il est possible d’utiliser les API publiques exposées par MRules pour construire ou modifier une instance.

3 étapes principales sont alors nécessaires :

  • Instanciation de la grappe d’Objets.
  • Instanciation du contexte de compilation et compilation.
  • Instanciation du contexte d’exécution et exécution.

Une instance existante et déjà compilée peut tout à fait être modifiée, auquel cas les modifications faites ne seront prises en compte qu’après une nouvelle compilation.

Le code très simple ci-dessous montre l’exemple d’une instance imprimant en console « Good Morning! » jusqu’à 12, « Good Afternoon! » jusqu’à 20h puis « Good night! ».

package com.massa.mrules.demos.site;

import java.util.ArrayList;
import java.util.List;

import com.massa.mrules.accessor.MPropertyAccessor;
import com.massa.mrules.accessor.MValueAccessor;
import com.massa.mrules.action.MActionPrint;
import com.massa.mrules.condition.ICondition;
import com.massa.mrules.condition.MEvaluationCondition;
import com.massa.mrules.context.compile.MCompilationContext;
import com.massa.mrules.context.execute.MExecutionContext;
import com.massa.mrules.exception.MExecutionException;
import com.massa.mrules.exception.MRuleValidationException;
import com.massa.mrules.executable.IExecutable;
import com.massa.mrules.executable.MRule;
import com.massa.mrules.operator.evaluation.LT;
import com.massa.mrules.set.MRuleExecutionSet;

public class SimpleManualInstance {

    public static void main(final String[] args) throws MRuleValidationException, MExecutionException {
        SimpleManualInstance.buildAndExecute();
    }

    public static void buildAndExecute() throws MRuleValidationException, MExecutionException {
        //Main sequence of executables iterated by main Execution Set.
        final List mainExecutables = new ArrayList();

        //First Rule: if before 12, print Good Morning!
        ICondition condition = new MEvaluationCondition(
                new LT(), // Less Than Operator
                new MPropertyAccessor("#TIME"), // Property Accessor for Current Time Global Variable
                new MValueAccessor("12:00:00")); // Constant for 12
        List thens = new ArrayList();
        thens.add(new MActionPrint(new MValueAccessor("Good Morning!n"))); // Action to perform if true
        IExecutable e = new MRule(condition, thens); //Build rule instance. No else needed.
        mainExecutables.add(e);

        //Second Rule: if before 20, print Good Afternoon!
        condition = new MEvaluationCondition(
                new LT(), // Less Than Operator
                new MPropertyAccessor("#TIME"), // Property Accessor for Current Time Global Variable
                new MValueAccessor("20:00:00")); // Constant for 20
        thens = new ArrayList();
        thens.add(new MActionPrint(new MValueAccessor("Good Afternoon!n"))); // Action to perform if true
        e = new MRule(condition, thens); //Build rule instance. No else needed.
        mainExecutables.add(e);

        //Default action if nothing else is chosen before: Good Night!
        mainExecutables.add(new MActionPrint(new MValueAccessor("Good Night!n")));

        //Defining execution set, with stopAtFirstValidatedCondition to true
        //So that we exit as soon as something is printed.
        final MRuleExecutionSet set = new MRuleExecutionSet(mainExecutables, true);

        //Contexts. Compilation context needs a non null base, so provide a simple Object instance.
        final MCompilationContext<object> comp = new MCompilationContext<object>(set, new Object());
        final MExecutionContext<void> exec = new MExecutionContext<void>(set);

        //Compilation and execution.
        comp.compile();
        exec.execute();
    }
}

Cependant, on voit que la construction par ce biais reste très verbeux. Il comporte de plus le principal désavantage de ne pas pouvoir être modifié sans l’intervention d’une équipe de développement.

L’utilisation des API peut s’avérer toutefois utile pour la création d’une IHM dédiée à la modification d’une instance existante issue d’une Factory.

Les Factory

Une Factory d’instance de moteur de règles est une classe Java devant impérativement implémenter l’interface IRuleExecutionSetFactory. Cette interface définit les paramètres de compilation et des méthodes de lecture / écriture.

En effet, le contrat global d’une implémentation de Fabrique est de gérer un format de configuration aussi bien en lecture (construction de l’instance du moteur de règles à partir d’une configuration) qu’en écriture (sauvegarde de la configuration à partir de l’instance de moteur de règles).

La fabrique va également effectuer la phase de compilation si les informations nécessaires sont disponibles :

  • Un contexte de compilation est directement fourni à la fabrique, avec les information nécessaire (données d’entrée / sortie).
  • Le Rule Set peut fournir une fabrique de contexte, qui va permettre d’instancier automatiquement les éléments nécessaires.

La version actuelle de MRules fournit trois implémentations :

Grammaire XML légère

La classe de fabrique est SimpleXMLFactory. Elle gère une grammaire simple ne prenant pas en compte tous les Addons ni toutes les possibilités de conditionnements.

Le principe de cette syntaxe est le suivant :

  • Définir un certain nombre de blocs condition / action (« rule »). Pour chacun d’eux :
  • Tester le contenu d’une ou plusieurs propriétés du bean d’entrée (tous les opérateurs sont gérés).
  • Positionner une ou plusieurs propriétés du bean de sortie.

Exemple :

<?xml version="1.0" encoding="UTF-8"?>

<!-- 
This example shows possibilities offered by the simple XML grammar.
 -->
<rules name="RuleSetName" stopAtFirstValidatedCondition="true">
    <rule name="rule 1">
        <if>
            <!-- Logical operator is AND -->
            <!-- Property types are detected automatically and values are cast to be compared -->
            <!-- Default evaluation operator is EQ, but might be modified using the dedicated attribute -->
            <inStringProperty>In String Value</inStringProperty>
            <inIntProperty2>1</inIntProperty2>
            <inDateProperty operator="GTE">2016-01-01</inDateProperty>
        </if>
        <then>
            <!-- Nested beans are handled -->
            <outProperty1.subDoubleProperty>1.234</outProperty1.subDoubleProperty>
            <outProperty1.subBooleanProperty>true</outProperty1.subBooleanProperty>
            <outProperty2.subStringProperty>Out String Value</outProperty2.subStringProperty>
        </then>        
    </rule>
    <rule name="rule 2">
        <if>
            <!-- In this particular case, the IN operator is implicitly used. -->
            <inStringProperty>String Value 1</inStringProperty>
            <inStringProperty>String Value 2</inStringProperty>
            <inStringProperty>String Value 3</inStringProperty>
            <inStringProperty>String Value 4</inStringProperty>
        </if>
        <then>
            <outProperty2.subStringProperty>Out String Value</outProperty2.subStringProperty>
        </then>        
    </rule>
    <rule name="rule 2 bis">
        <if>
            <!-- Other way to build the previous condition. -->
            <inStringProperty operator="IN" separator=";">String Value 1;String Value 2;String Value 3;String Value 4</inStringProperty>
        </if>
        <then>
            <outProperty2.subStringProperty>Out String Value</outProperty2.subStringProperty>
        </then>        
    </rule>
    <rule name="rule 3 bis">
        <if>
            <!-- List "inListProperty" must contain at least all 4 values. -->
            <inListProperty operator="COLINCLUDES" separator=";">String 1;String 2;String 3;String 4</inListProperty>
        </if>
        <then>
            <outProperty2.subStringProperty>Out String Value</outProperty2.subStringProperty>
        </then>        
    </rule>
    <rule name="default rule">
        <!-- No if node, so always true -->
        <then>
            <outProperty2.subStringProperty>Default out String</outProperty2.subStringProperty>
            <outProperty1.subBooleanProperty>false</outProperty1.subBooleanProperty>
        </then>        
    </rule>
</rules>

Cette grammaire peut cependant être amplement suffisante dans de nombreux cas d’utilisation.

Grammaire XML complète

La classe de fabrique est RichXMLFactory. Elle permet l’utilisation de tous les Addons, y compris d’éventuels Addons personnalisés. Ces derniers doivent déclarer les propriétés de configuration à sauvegarder par l’intermédiaire des annotations PersisantProperties et PersistantProperty.

Le noeud racine déclare l’implémentation de IMRuleExecutionSet à utiliser et ses paramètres de configuration (Persistant Property).
Chaque déclaration d’Addon doit préciser l’attribut « implem », donnant l’ID unique de l’implémentation d’Addon à utiliser.
Chaque attribut de noeud représente un paramètre de configuration de l’Addon déclaré. Notez la syntaxe « PROPERTY:#TIME » : elle permet de s’éviter la création d’un noeud enfant pour déclarer un Addon, en permettant de donner en argument la valeur de la propriété par défaut. Tous les Addons ne déclarent pas une propriété par défaut : elles sont décrites sur la page de propriétés des Addons. On peut également enchainer les Addons, exemple :
« NULLIFEMPTY:PROPERTY:myProperty.mySubProperty ».
Chaque noeud représente un également paramètre de configuration de l’Addon déclaré dans le noeud parent, mais ne pouvant pas être représenté sous forme de chaîne de caractères.

L’exemple suivant reproduit la démonstation utilisant les API Java :

<?xml version="1.0" encoding="UTF-8"?>
<!-- Complete XML syntax example. 
All addons nodes declare the "implem" attribute, containing the unique ID of the Addon to invoke.
- Other attributes are Persistant Properties. Note the specific syntax "PROPERTY:#TIME" :
  it saves from declaring a subnode for an Addon, by allowing to provide as an argument the
  default Addon property. Not all Addons declare a Default Property: they are described in the Addons properties page.
- Sub Nodes are Persistant Properties which can not be represented as an Attribute.
-->
 
<!-- Root node declares the IMRuleExecutionSet implementation to use and its parameters. -->
<rules implem="RULEEXECUTIONSET" name="Hello" stopAtFirstValidatedCondition="true">
    <!-- Each "executable" nodes declare one step to execute. -->

    <!-- First Rule: if before 12, prints Good Morning! -->
    <executable implem="RULE">
        <condition implem="EVAL" source="PROPERTY:#TIME" operator="LT" reference="VALUE:12:00:00"/>
        <then implem="PRINT" source="VALUE:Good Morning!&#10;" /> 
    </executable>

    <!-- Second Rule: if before 20, prints Good Afternoon! -->
    <executable implem="RULE">
        <condition implem="EVAL" source="PROPERTY:#TIME" operator="LT" reference="VALUE:20:00:00"/>
        <then implem="PRINT" source="VALUE:Good Afternoon!&#10;" /> 
    </executable>
    
    <!-- Default: Prints Good Night! -->
    <executable implem="PRINT" source="VALUE:Good Night!&#10;" /> 
</rules>

Le lanceur Java suivant peut alors être utilisé :

package com.massa.mrules.demos.site;

import java.io.InputStream;

import com.massa.mrules.context.compile.CompilationLevel;
import com.massa.mrules.context.compile.MCompilationContext;
import com.massa.mrules.context.execute.MExecutionContext;
import com.massa.mrules.exception.MConfigurationException;
import com.massa.mrules.exception.MExecutionException;
import com.massa.mrules.exception.MRuleValidationException;
import com.massa.mrules.factory.xml.RichXMLFactory;
import com.massa.mrules.set.IMruleExecutionSet;

public class SimpleRichXmlInstance {

    public static void main(final String[] args) throws MRuleValidationException, MExecutionException, MConfigurationException {
        SimpleRichXmlInstance.buildAndExecute();
    }

    public static void buildAndExecute() throws MRuleValidationException, MExecutionException, MConfigurationException {
        //Compilation context.
        final MCompilationContext<Object> comp = new MCompilationContext<Object>(CompilationLevel.STANDARD, new Object());
        
        //Build instance with factory
        final RichXMLFactory factory = new RichXMLFactory(comp);
        final InputStream xml = ClassLoader.getSystemResourceAsStream("RichExample.xml");
        final IMruleExecutionSet set = factory.read(xml);

        //Execution context.
        final MExecutionContext<Void> exec = new MExecutionContext<Void>(set);
        exec.execute();
    }
}

A noter que la syntaxe présentée ci dessus peut être beaucoup plus compacte au niveau de la représentation XML, en permettant de valuer directement des propriétés des sous-objets dans les noeuds parents. Elle est parfaitement fonctionnelle en lecture mais n’est pas encore gérée en écriture :

<?xml version="1.0" encoding="UTF-8"?>
<!-- Root node declares the IRuleExecutionSet implementation to use and its parameters. -->
<rules implem="RULEEXECUTIONSET" name="Hello" stopAtFirstValidatedCondition="true">
    <!-- Each "executable" nodes declare one step to execute. -->

    <!-- First Rule: if before 12, prints Good Morning! -->
    <executable implem="RULE" then="PRINT:VALUE:Good Morning!&#10;" condition="EVAL" condition.source="PROPERTY:#TIME" condition.operator="LT" condition.reference="VALUE:08:00:00" />

    <!-- Second Rule: if before 12, prints Good Afternoon! -->
    <executable implem="RULE" then="PRINT:VALUE:Good Afternoon!&#10;" condition="EVAL" condition.source="PROPERTY:#TIME" condition.operator="LT" condition.reference="VALUE:20:00:00" />
    
    <!-- Default: Prints Good Night! -->
    <executable implem="PRINT" source="VALUE:Good Night!&#10;" /> 
</rules>

On peut également noter que dans cet exemple, la fabrique prend en charge la compilation, mais qu’un contexte doit être fourni par le lanceur. En spécifiant une fabrique de contexte dans la configuration du moteur de règles, toutes les données nécessaires à la compilation seront créées automatiquement. Cette fonctionnalité est intéressante si l’on désire centraliser la construction des instances via le Builder (cf. Accéder aux instances).

Une légère modification de la configuration XML est nécessaire :

<?xml version="1.0" encoding="UTF-8"?>
<!-- Complete XML syntax example. 
All addons nodes declare the "implem" attribute, containing the unique ID of the Addon to invoke.
- Other attributes are Persistant Properties. Note the specific syntax "PROPERTY:#TIME" :
  it saves from declaring a subnode for an Addon, by allowing to provide as an argument the
  default Addon property. Not all Addons declare a Default Property: they are described in the Addons properties page.
- Sub Nodes are Persistant Properties which can not be represented as an Attribute.
-->
 
<!-- Root node declares the IMRuleExecutionSet implementation to use and its parameters. -->
<rules implem="RULEEXECUTIONSET" name="Hello" stopAtFirstValidatedCondition="true">
    <!-- This first line allows the compilation phase to be performed automatically by the factory.
    No input are needed, so the generic Object type might be used.-->
    <contextFactory implem="CONTEXT" classIn="Object"/>

    <!-- Each "executable" nodes declare one step to execute. -->

    <!-- First Rule: if before 12, prints Good Morning! -->
    <executable implem="RULE">
        <condition implem="EVAL" source="PROPERTY:#TIME" operator="LT" reference="VALUE:12:00:00"/>
        <then implem="PRINT" source="VALUE:Good Morning!&#10;" /> 
    </executable>

    <!-- Second Rule: if before 12, prints Good Afternoon! -->
    <executable implem="RULE">
        <condition implem="EVAL" source="PROPERTY:#TIME" operator="LT" reference="VALUE:20:00:00"/>
        <then implem="PRINT" source="VALUE:Good Afternoon!&#10;" /> 
    </executable>
    
    <!-- Default: Prints Good Night! -->
    <executable implem="PRINT" source="VALUE:Good Night!&#10;" /> 
</rules>

Le lanceur Java simplifié suivant peut alors être utilisé :

package com.massa.mrules.demos.site;

import java.io.InputStream;

import com.massa.mrules.context.compile.CompilationLevel;
import com.massa.mrules.context.compile.MCompilationContext;
import com.massa.mrules.context.execute.MExecutionContext;
import com.massa.mrules.exception.MConfigurationException;
import com.massa.mrules.exception.MExecutionException;
import com.massa.mrules.exception.MRuleValidationException;
import com.massa.mrules.factory.xml.RichXMLFactory;
import com.massa.mrules.set.IMruleExecutionSet;

public class SimpleRichXmlInstance {

    public static void main(final String[] args) throws MRuleValidationException, MExecutionException, MConfigurationException {
        SimpleRichXmlInstance.buildAndExecute();
    }

    public static void buildAndExecute() throws MRuleValidationException, MExecutionException, MConfigurationException {
        //Build instance with factory
        final RichXMLFactory factory = new RichXMLFactory();
        final InputStream xml = ClassLoader.getSystemResourceAsStream("RichExample.xml");
        final IMruleExecutionSet set = factory.read(xml);

        //Execution context.
        final MExecutionContext<Void> exec = new MExecutionContext<Void>(set);
        exec.execute();
    }
}

La grammaire complète permet également de sauver les propriétés génériques du rule set. Ces propriétés sont accessible programmatiquement via les méthodes « setProperty » et « getProperty ». La syntaxe est très simple :

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generic properties example. -->
 
<!-- Root node. -->
<rules implem="RULEEXECUTIONSET" name="Ruleset with properties">
    <properties>
        <property_name_1>value 1</property_name_1>
        <property_name_2>value 2</property_name_2>
    </properties>
    <!-- ... -->
</rules>

Grammaire DSL

Pour pouvoir utiliser cette grammaire et donc la fabrique associée, il vous faut la licence pour le moteur de grammaire (« mrules-dsl »). Il vous faut également importer la dépendance vers l’extension « mrules-ext-dsl-bre ».

La classe de fabrique est NativeLanguageFactory. Elle permet de faciliter la fabrication du ruleset à partir de la grammaire « Langage Naturel » basée sur notre moteur de grammaire.

Nous vous invitons à consulter la page de détails de l’extension pour plus de détail sur l’utilisation de la grammaire et pour la description précise de la syntaxe et des outils d’édition.

L’exemple suivant reproduit la démonstation utilisant cette grammaire :

// Begining rule set
ruleset

  // Declares global rule set configuration (name, context factory, ...)
  name is "Ruleset with properties"
  context factory is standalone
  stop at first validated condition is true

  // Declares rules
  
  // First Rule: if before 12, prints Good Morning!
  if #TIME is lesser than "12:00:00" then
    print "Good Morning!\n"
  end of if
  
  // Second Rule: if before 12, prints Good Afternoon!
  // Note the sign "<" which is equivalent to "is lesser than"
  if #TIME < "20:00:00" then
    print "Good Afternoon!\n"
  end of if
  
  //Default: Prints Good Night!
  print "Good Night!\n"

// Ending rule set
end of ruleset

Le lanceur Java simplifié suivant peut alors être utilisé :

package com.massa.mrules.demos.site;

import java.io.InputStreamReader;

import com.massa.mrules.context.execute.MExecutionContext;
import com.massa.mrules.exception.MConfigurationException;
import com.massa.mrules.exception.MExecutionException;
import com.massa.mrules.extensions.dsl.factory.NativeLanguageFactory;
import com.massa.mrules.set.IMruleExecutionSet;

public class SimpleDslInstance {

    public static void main(final String[] args) throws MConfigurationException, MExecutionException {
        SimpleDslInstance.buildAndExecute("DslExample.mrl");
    }

    public static void buildAndExecute(final String mrlFile) throws MConfigurationException, MExecutionException {
        //Read
        final NativeLanguageFactory factory = new NativeLanguageFactory();
        final IMruleExecutionSet set = factory.read(new InputStreamReader(ClassLoader.getSystemResourceAsStream(mrlFile)));

        //Execute
        final MExecutionContext exec = new MExecutionContext(set);
        exec.execute();
    }
}

Sérialisation

Une autre implémentation permet de sauvegarder une instance en utilisant la sérialisation binaire Java. Le flux peut optionnellement être encodé en Base 64.