We will see here how to develop your own Addon, through a simple but realistic example. The case is: an e-commerce website wants, under some conditions, to generate a discount code for a client. This code can be a fixed amount or a percentage reduction.
To generate the code during the rule engine execution, the best solution is to develop a specific Addon, which is a fast and simple operation as all the tools are provided to ease the implementation.
In this example, the Addon will be a reading Accessor (IReadAccessor), meaning a component allowing to retrieve data.
It will be configured with 4 parameters:
- The client ID to which the code is attached
- A type (percentage or amount)
- A value
- An optional validity period
Let’s start by creating the implementation class. To achieve this, it’s possible to directly implement the interface or to extend the abstract class AbstractReadAccessor, providing some code components allowing to simplify the final code.
The code below shows the class skeleton, with configuration fields, associated getters / setters and the simpliest methods code.
package com.massa.mrules.demos.site; // imports /** * Simple example of a Read Accessor implementation. * Extends {@link AbstractReadAccessor} which provides a useful base. */ @PersistantProperties(properties={ //Declare here all properties that must or might be provided by configuration. @PersistantProperty(property="clientId"), @PersistantProperty(property="type"), @PersistantProperty(property="value"), @PersistantProperty(property="validity") }) public class DiscountGenerator extends AbstractReadAccessor { /** All addons are Serializable. */ private static final long serialVersionUID = 1L; /** Unique ID is defined as a Constant. */ public static final String DISCOUNT_GENERATOR_ID = "DISCOUNTGEN"; /** Mandatory: client ID. */ private IReadAccessor clientId; /** Mandatory: PERCENT or AMOUNT. */ private String type; /** Mandatory: Discount value. */ private Double value; /** Optional: validity (in days). */ private Integer validity; /** * TODO: Compilation */ @Override public void compileValue(final ICompilationContext ctxt, final Class<?> expectedType) throws MRuleValidationException { } /** * TODO: Compilation */ @Override public <G, C extends Collection> void compileCollection(final ICompilationContext ctxt, final Class expectedType, final Class expectedGenericType) throws MRuleValidationException { throw new MRuleValidationException(new MessageInfo(MRulesMessages.MRE_VALUE_ONLY_ACCESSOR), this); } /** * TODO: Execution */ public Object get(final IExecutionContext ctxt) throws MAccessorException { } /** * TODO: Cost */ public CostInfo getEstimatedReadCost(final ICompilationContext ctxt) throws MRuleValidationException { } /** * As a discount code is always a String, directly returning this type. */ public Class<?> getType(final IContext ctxt) throws MAccessorException { return String.class; } /** * No Collection is handled here. */ public Class<?> getGenericCollectionType(final IContext ctxt) throws MAccessorException { return null; } /** * Returns the unique identifier which is defined also as a Constant. */ public String getImplementationId() { return DiscountGenerator.DISCOUNT_GENERATOR_ID; } /** * Builds a human readable representation using all input values. */ public void toString(final OuterWithLevel outer) throws IOException { outer.write(this.getImplementationId()).write('(').write(this.type).write(", ").write(this.value).write(", ").write(this.validity).write(')'); } /** * No collection handled, no iteration will be necessary. */ public boolean isShouldIterate() { return false; } /** * Returns a clone of this instance. Only the clientId sub value should be cloned in our case. */ @Override public DiscountGenerator clone() { final DiscountGenerator ret = (DiscountGenerator) super.clone(); ret.clientId = MAddonsUtils.cloneAddon(this.clientId); return ret; } /** * No caching should be used here, as a new discount code should be generated at each call. */ public boolean isCacheUsed() { return false; } ////////////////////////////////////////// ////////// Getter / Setter part ////////// ////////////////////////////////////////// public IReadAccessor getClientId() { return this.clientId; } public void setClientId(final IReadAccessor clientId) { this.clientId = clientId; } public String getType() { return this.type; } public void setType(final String type) { this.type = type; } public Double getValue() { return this.value; } public void setValue(final Double value) { this.value = value; } public Integer getValidity() { return this.validity; } public void setValidity(final Integer validity) { this.validity = validity; } }
This first step completed, it’s now time to write the 4 last methods code (commented as TODO):
- Two compilation methods. The one handling Collections is not relevant in this case, it will throw an Exception. The compilation for unitary Objects will validate the configuration data.
- The execution method. It will call a Helper allowing to generate the code, potentially reuseable by other application modules.
- The method evaluating execution cost, internally used to opitmize coll ordering.
/** * Here, the compilation will check the input validity and compile the client ID sub accessor. * Note the last instructions, which allows to improve memory impact by deduplicating identical compiled objects. */ @Override public void compileValue(final ICompilationContext ctxt, final Class<?> expectedType) throws MRuleValidationException { if (this.clientId == null) { throw new MRuleValidationException(new MessageInfo(MRulesMessages.MRE_NULL_PARAMETER, "clientId"), this); } if (this.type == null) { throw new MRuleValidationException(new MessageInfo(MRulesMessages.MRE_NULL_PARAMETER, "type"), this); } if (this.value == null) { throw new MRuleValidationException(new MessageInfo(MRulesMessages.MRE_NULL_PARAMETER, "value"), this); } if (!"PERCENT".equals(this.type) && !"AMOUNT".equals(this.type)) { throw new MRuleValidationException(new MessageInfo(DiscountGeneratorMessages.INVALID_TYPE, this.type), this); } if ("PERCENT".equals(this.type) && (this.value > 100 || this.value < 0)) { throw new MRuleValidationException(new MessageInfo(DiscountGeneratorMessages.INVALID_PERCENT, this.value), this); } if ("AMOUNT".equals(this.type) && this.value < 0) { throw new MRuleValidationException(new MessageInfo(DiscountGeneratorMessages.INVALID_AMOUNT, this.value), this); } if (this.validity != null && this.validity <= 0) { throw new MRuleValidationException(new MessageInfo(DiscountGeneratorMessages.INVALID_VALIDITY, this.validity), this); } this.clientId.compileRead(ctxt); this.clientId = MAddonsUtils.deduplicate(ctxt, this.clientId); } /** * It's unuseful here to handle returning Collections. So this compilation access point can be ignored, and simply * handled with a dedicated error message indicating that this accessor only returns single values. */ @Override public <G, C extends Collection> void compileCollection(final ICompilationContext ctxt, final Class expectedType, final Class expectedGenericType) throws MRuleValidationException { throw new MRuleValidationException(new MessageInfo(MRulesMessages.MRE_VALUE_ONLY_ACCESSOR), this); } /** * The main code of your Addon is here. It will generate the discount code (using for example an external Helper) and return * the result to the caller. */ public Object get(final IExecutionContext ctxt) throws MAccessorException { final String cId = (String) this.clientId.get(ctxt); if ("AMOUNT".equals(this.type)) { return DiscountGeneratorHelper.generateAmountDiscount(cId, this.value, this.validity); } else { return DiscountGeneratorHelper.generatePercentDiscount(cId, this.value, this.validity); } } /** * CostInfo is just an indication to reorder actions in optimization phase. Here, we consider * that the fact of generating the code costs "one operation", and we add it to the cost of retrieving * the client ID. */ public CostInfo getEstimatedReadCost(final ICompilationContext ctxt) throws MRuleValidationException { final CostInfo ret = new CostInfo(1); if (this.clientId != null) { ret.addEstimatedCost(this.clientId.getEstimatedReadCost(ctxt).getEstimatedCost()); } return ret; }
It’s worth mentionning the presence of error message dedicated to the Addon in the compilation method. These messages must be declared in a Java class and implement the MessageCode interface.
package com.massa.mrules.demos.site; //imports /** * All message codes for the discount generator addon. */ public final class DiscountGeneratorMessages { public static final MessageCode INVALID_TYPE = new MessageCodeFromField(); public static final MessageCode INVALID_PERCENT = new MessageCodeFromField(); public static final MessageCode INVALID_AMOUNT = new MessageCodeFromField(); public static final MessageCode INVALID_VALIDITY = new MessageCodeFromField(); //Initialization of messages. Note the "discountgen" prefix for message keys. static { MessageCode.initMessages(DiscountGeneratorMessages.class, ResourceBundle.getBundle("com/massa/mrules/demos/site/DiscountGeneratorMessages"), "discountgen"); } private DiscountGeneratorMessages() { } }
Then also the internationalization file, named here DiscountGeneratorMessages.properties:
discountgen.INVALID_TYPE=Type must have one of these two values: 'AMOUNT' or 'PERCENT'. Found '{0}'. discountgen.INVALID_PERCENT=Percentage must be between 0 and 100. Found '{0}'. discountgen.INVALID_AMOUNT=Amount must be superior to 0. Found '{0}'. discountgen.INVALID_VALIDITY=Validity must be superior to 0. Found '{0}'.
Also, the test Helper for the example called in the “get” method:
package com.massa.mrules.demos.site; /** * A class generating an arbitrary code, just for the example. */ public final class DiscountGeneratorHelper { private static int IDX = 1; public static String generatePercentDiscount(final String clientId, final double percent, final Integer validity) { return "P" + DiscountGeneratorHelper.IDX++ + "DISC-" + clientId + (validity == null ? "" : "-" + validity); } public static String generateAmountDiscount(final String clientId, final double amount, final Integer validity) { return "A" + DiscountGeneratorHelper.IDX++ + "DISC-" + clientId + (validity == null ? "" : "-" + validity); } }
Finally, don’t forget to declare this Addon so that it will be loaded at launch time. To achieve this, it’s necessary to create a file named “mrules-addons.xml”, directly accessible in the CLASSPATH and declaring the Addon. The priority attribute allows to order these files loading, to be able to override declarations (especially concerning data conversion formats).
<?xml version="1.0" encoding="UTF-8" standalone='no' ?> <!DOCTYPE mrulesaddons SYSTEM "mrules-addons.dtd"> <mrulesconfig priority="1"> <addon class="com.massa.mrules.demos.site.DiscountGenerator" /> </mrulesconfig>
A simple main launcher allows to test this example:
package com.massa.mrules.demos.site; // imports public class DiscountGeneratorMain { public static void main(final String[] args) throws Exception { final Map<String, String> input = new HashMap<String, String>(); input.put("customerId", "CUST-ID"); final MCompilationContext<Map<String, String>> compctxt = new MCompilationContext<Map<String, String>>(); compctxt.setInput(input); final RichXMLFactory factory = new RichXMLFactory(compctxt); final IMruleExecutionSet eng = factory.read(ClassLoader.getSystemResourceAsStream("DiscountGenerator.xml")); final MExecutionContext<Map<String, String>> exectxt = new MExecutionContext<Map<String, String>>(eng); exectxt.setInput(input); exectxt.execute(); } }
As a conclusion, we have seen here the steps necessary for creating a specific Addon, in order to adapt the MRules library to the business needs of the application it integrates. Using this Addon system, simple to set up, all possibilities are opened to developers for easily and quickly integrate all kings of behaviors.
The full source code of the example can be downloaded here.