
de.tsl2.nano.specification.rules.RuleDecisionTable Maven / Gradle / Ivy
package de.tsl2.nano.specification.rules;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import org.simpleframework.xml.core.Commit;
import de.tsl2.nano.core.ENV;
import de.tsl2.nano.core.ITransformer;
import de.tsl2.nano.core.util.CollectionUtil;
import de.tsl2.nano.core.util.FileUtil;
import de.tsl2.nano.core.util.StringUtil;
import de.tsl2.nano.core.util.Util;
import de.tsl2.nano.specification.ParType;
import de.tsl2.nano.specification.Pool;
import de.tsl2.nano.tree.STree;
import de.tsl2.nano.tree.Tree;
/**
* Reads a CSV-file containing a decision table and creates rule conditions. enables creating (business) rules on
* decision tables done by product managers in tools like Excel - to be transformed/interpreted by this class into a
* machine readable rule.
*
* Each line starts with a key/name followed by one or more values. If the line starts with an empty cell, the key from
* last line will be used to append the values of the new line.
* #KEYWORD Description:
*
*
* - name (otional) : rule name. if null the file name will be used
* - description (optional) : rule description
* - parameter...(optional) : additional (used in condition expressions) parameters with default value
* - matrix : decision table
*
*
* Decision Table Description:
* The first line is the header, starting with keyword {@link #KEY_MATRIX} followed by condition names.
* The following lines start with the name of a rule parameter, followed by all possible conditions.
*
* Conditions:
*
*
* All conditions have the form: [OPERATOR]{VALUE}
* operators are: =, !=, <, <=, >, >=
* if no operator is given, = will be used
* a value must be a Comparable and can referenz another parameter
*
*
* The first line that starts with an empty cell will end the matrix (decision table). the last line of the matrix will
* be the exptected result line.
*
* Example:
*
*
* Name;ABR-7493;;;;;;;
* Beschreibung;"Rezeptfehler ""Betragshorror"" bei §302: Rezept kann ohne Änderung der Daten nicht abgerechnet werden.";;;;;;;
* ;"Rezeptfehler ""Betragshorror"" bei §300, Rechnungstyp Arzneimittel oder Hilfsmittel: Rezept kann ohne Änderung der Daten nicht abgerechnet werden.";;;;;;;
* ;"Rezeptfehler ""Betragshorror"" bei §300, Rechnungstyp Pflegehilfsmittel: Rezept kann ohne Änderung der Daten abgerechnet werden.";;;;;;;
* ;"*Rezeptfehler ""Betragshorror"" bei §300 Pflegehilfsmittel Irrläufer: Rezept kann ohne Änderung der Daten abgerechnet werden.";;;;;;;
* ;;;;;;;;
* MATRIX;R1;R2;R3;R4;R5;R6;R7;R8
* DIFF;0;0;0;0;>0;>0;>0;>0
* PARAGRAPH;300;300;302;302;300;300;302;302
* ISTHILFSMITTEL;Ja;Nein;Ja;Nein;Ja;Nein;Ja;Nein
* ERGBEBNIS;OK;OK;OK;OK;WARNUNG;FEHLER;FEHLER;FEHLER
*
*
* @author Thomas Schneider / 2015
*/
@SuppressWarnings("rawtypes")
public class RuleDecisionTable extends AbstractRule {
/** serialVersionUID */
private static final long serialVersionUID = 982758388836072241L;
/** contains the name, description and result vector - and additional variables */
transient Map properties;
/** the content of the decision table */
transient Map>> par;
/**
* caclulated table on all conditions compared to the given arguments. will be hold in memory for performance issues
*/
transient byte[][] matchtable;
public static final char PREFIX = '&';
public RuleDecisionTable() {
}
public RuleDecisionTable(String name, String csvExpression, LinkedHashMap parameter) {
super(name, saveCSV(name, csvExpression), parameter);
}
RuleDecisionTable(Map properties, Map>> par) {
super();
this.name = (String) properties.get("name");
setOperation((String) properties.get("operation"));
this.properties = properties;
this.par = par;
checkConsistence(properties, par);
}
private static String saveCSV(String name, String csvExpression) {
String path = ENV.get(Pool.class).getDirectory(RuleDecisionTable.class) + name;
FileUtil.save(path, csvExpression);
return path;
}
/**
* check for overlapping conditions
*
* UNDER CONSTRUCTION
*
* @param properties
* @param par
*/
private void checkConsistence(Map properties, Map>> par) {
// Map args = new HashMap();
// for (String name : par.keySet()) {
// for (Condition c : par.get(name)) {
// //TODO: check all combinations...
// args.put(name, c.operand2);
// byte[][] mt = createMatchTable(args);
// //check for more than one result
// evalResult(mt, false);
// }
// }
}
@Override
public String prefix() {
return String.valueOf(PREFIX);
}
public T run(Map context, Object... extArgs) {
//first, we create a matching table with 0 or 1
T result = evalResult(createMatchTable(context));
if (result == null && ENV.isModeStrict()) {
throw new IllegalArgumentException("no value of given context matches any rule in decision table!\n\tcontext: " + context + "\n\tdecisiontable:\n\t" + par);
}
return result;
}
@SuppressWarnings("unchecked")
byte[][] createMatchTable(Map args) {
byte[][] mt = createEmptyMatchTable();
int k = 0;
List> conditions;
Comparable value;
for (String name : par.keySet()) {
conditions = par.get(name);
value = (Comparable) args.get(name);
for (int i = 0; i < conditions.size(); i++) {
mt[k][i] = (byte) (conditions.get(i).isTrue(value) ? 1 : 0);
}
k++;
}
return mt;
}
/**
* the first vector that is filled with 1 will be used as matching index for the result vector.
*
* @param mt matching table
* @return object from result collection
*/
private T evalResult(byte[][] mt) {
return evalResult(mt, true);
}
private T evalResult(byte[][] mt, boolean stopOnFirst) {
boolean matched = false;
for (int i = 0; i < mt[0].length; i++) {
if (matches(mt, i)) {
if (stopOnFirst)
return getResultVector().get(i);
else {
if (matched)
throw new IllegalStateException(this + " is inconsistent at parameter index" + i);
}
matched = true;
}
}
return null;
}
private boolean matches(byte[][] ba, int i) {
for (int k = 0; k < ba.length; k++) {
if (ba[k][i] != 1)
return false;
}
return true;
}
@SuppressWarnings("unchecked")
private List getResultVector() {
Object[] resultVector = (Object[]) properties.get(DecisionTableInterpreter.KEY_RESULT);
return (List) (resultVector != null ? (List) Arrays.asList(resultVector) : new LinkedList<>());
}
private byte[][] createEmptyMatchTable() {
if (matchtable == null)
matchtable = new byte[par.keySet().size()][getResultVector().size()];
for (int i = 0; i < matchtable.length; i++) {
for (int j = 0; j < matchtable[i].length; j++) {
matchtable[i][j] = 0;
}
}
return matchtable;
}
/**
* transforms the current decision table to a tree. each odd level holds the parameter names, followed by its
* conditions (the values of the table) in the next (even) level. So, the tree nodes are strings or conditions.
*
* @return transformed decision table
*/
//UNTESTED!
@SuppressWarnings("unchecked")
public STree toTree() {
Set keys = par.keySet();
STree tree = null, parent = null;
for (String k : keys) {
/*
* create new tree node(s) and append all conditions to the new node(s).
* the child map holds a set of keys, so identical keys will be removed.
*/
if (parent == null)
tree = new STree(k, parent, par.get(k).toArray());
else {
Collection children = parent.values();
for (Tree child : children) {
child.put(k.hashCode(), new STree(k, child, par.get(k)));
}
}
parent = tree;
}
return (STree) tree.getRoot();
}
//UNTESTED!
@SuppressWarnings("unchecked")
public static RuleDecisionTable fromTree(STree tree) {
final Map> par = new HashMap>();
HashMap properties = new HashMap();
tree.transformTree(new ITransformer, STree>() {
@Override
public STree transform(STree t) {
if (t.getNode() instanceof String) {
Collection current = par.get(t.getNode());
par.put(
(String) t.getNode(),
(List) (current == null ? t.getChildren() : current
.addAll((Collection extends Condition>) t.getChildren())));
} else {
//do nothing
}
return t;
}
});
return new RuleDecisionTable(properties, par);
}
public static RuleDecisionTable fromCSV(String fileName) {
return new DecisionTableInterpreter().scan(fileName, "\t");
}
@Override
public String getOperation() {
//don't load through super method
return operation;
}
@SuppressWarnings("unchecked")
@Override
@Commit
protected void initDeserializing() {
RuleDecisionTable fromCSV = fromCSV(getOperation());
par = fromCSV.par;
properties = fromCSV.properties;
matchtable = fromCSV.matchtable;
super.initDeserializing();
}
@Override
public String toString() {
return Util.toString(this.getClass(), "name=" + properties.get("name"));
}
}
class DecisionTableInterpreter {
/** keyword to start the decision table */
static final String KEY_MATRIX = "matrix";
/** keyword for the end of the parameter matrix. the property map will hold the expected results on this key. */
static final String KEY_RESULT = "result";
RuleDecisionTable> scan(String csv, String delimiter) {
Map properties = new HashMap();
Map>> par = new LinkedHashMap>>();
Scanner sc = null;
try {
sc = new Scanner(FileUtil.userDirFile(csv));
sc.useDelimiter(delimiter);
String key, lastKey = null, values[] = null;
boolean withinMatrix = false;
String line;
while (sc.hasNextLine()) {
line = sc.nextLine();
key = StringUtil.substring(line, null, delimiter).toLowerCase();
values = StringUtil.substring(line, delimiter, null).split(delimiter);
//e.g. on a description there are more than one line...
if (Util.isEmpty(key)) {
if (lastKey == null)
continue;
key = withinMatrix ? KEY_RESULT : lastKey;
withinMatrix = false;
values = CollectionUtil.concat((String[]) properties.get(key), values);
} else if (!withinMatrix) {
withinMatrix = KEY_MATRIX.equals(key);
}
if (withinMatrix && !KEY_MATRIX.equals(key))
par.put(key, interpret(values));
else
properties.put(key, values);
lastKey = key;
}
//no KEY_RESULT found --> use the last line as result line.
if (!properties.containsKey(KEY_RESULT)) {
par.remove(lastKey);
properties.put(KEY_RESULT, values);
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} finally {
if (sc != null)
sc.close();
}
if (properties.get("name") == null) {
String name = StringUtil.substring(csv, "/", ".", true);
properties.put("name", name);
properties.put("operation", csv);
}
return createRule(properties, par);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private RuleDecisionTable createRule(Map properties, Map>> par) {
return new RuleDecisionTable(properties, par);
}
/**
* extracts parameter referenced in conditions
*
* @param conditions values of a csv-line
* @return
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private List> interpret(String[] conditions) {
final String OPEXP = "[^\\w\\s.,;]{1,2}";
ArrayList> conds = new ArrayList>();
String op, c;
for (int i = 0; i < conditions.length; i++) {
op = StringUtil.extract(conditions[i], OPEXP);
if (Util.isEmpty(op))
op = "=";
c = StringUtil.substring(conditions[i], op, null);
conds.add(new Condition(op, c));
}
return conds;
}
}
class Condition> {
String op;
T operand2;
Condition(String op, T operand) {
super();
this.op = op;
this.operand2 = operand;
}
public boolean isTrue(Comparable comparable) {
int c = comparable != null ? comparable.compareTo(operand2) : -1;
return op.equals("=") ? c == 0 : op.equals("<") ? c < 0 : op.equals("<=") ? c <= 0 : op.equals(">") ? c > 0
: op.equals(">=") ? c >= 0 : op.equals("!=") ? c != 0 : false;
}
@Override
public String toString() {
return Util.toString(getClass(), op, operand2);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy