diff --git a/drools-retediagram/.gitignore b/drools-retediagram/.gitignore new file mode 100644 index 00000000000..ec1ce76316b --- /dev/null +++ b/drools-retediagram/.gitignore @@ -0,0 +1,19 @@ +/target +/local + +# Eclipse, Netbeans and IntelliJ files +/.* +!.gitignore +/nbproject +/*.ipr +/*.iws +/*.iml + +# Repository wide ignore mac DS_Store files +.DS_Store + +# generated files +dependency-reduced-pom.xml + +# these modules don't exist anymore on master +/drools-clips/ diff --git a/drools-retediagram/README.md b/drools-retediagram/README.md new file mode 100644 index 00000000000..9874a202d42 --- /dev/null +++ b/drools-retediagram/README.md @@ -0,0 +1,126 @@ +# drools-retediagram + +An experiment to plot Rete diagram, similar to `ReteDumper`, using Dot language format. + +## Usage + +To plot the diagram files for a Knowledge Base of a given `KieSession` using the defaults: + +```java +ReteDiagram.newInstance().diagramRete(kieSession); +``` + +It is possible to change some settings and formatting, as in the following example: + +```java +ReteDiagram.newInstance() + .configLayout(Layout.PARTITION) // use Partition layout, instead of default Vertical layout + .configFilenameScheme(new File("./target"), true) // set output directory manually instead of OS default for temporary directory + .configOpenFile(true, false) // automatically open SVG file with OS default application + .diagramRete(kieSession); +``` + +Please notice to leverage the OS capability to automatically open file with the default application, it is required that the JVM property `java.awt.headless` but be `false`, which can be set manually with: + +```java +System.setProperty("java.awt.headless", "false"); +``` + +## Example: simple KB plotted with default Vertical layout + +Given the following rules in the Knowledge Base: + +``` +rule "For color" +no-loop +when + Measurement( id == "color", $colorVal : val ) +then + controlSet.add($colorVal); +end + +rule "Likes cheddar" +when + Cheese( $cheddar : name == "cheddar" ) + $person : Person( favouriteCheese == $cheddar ) +then + System.out.println( $person.getName() + " likes cheddar" ); +end + +rule "Don't like cheddar" +when + Cheese( $cheddar : name == "cheddar" ) + $person : Person( favouriteCheese != $cheddar ) +then + System.out.println( $person.getName() + " does not like cheddar" ); +end + +rule "Color count" +when + accumulate( $m: Measurement( id == "color" ); $c: count($m) ) +then + System.out.println( $c ); +end + +rule "Not a Color" +when + not ( Measurement( id == "color" ) and String() ) +then + System.out.println( "no color yet." ); +end +``` + +The diagram plotted with the defaults settings and Vertical layout: + +```java +ReteDiagram.newInstance().diagramRete(kieSession); +``` + +is rendered as: + +![example1](example1.png) + +the diagram displays the default entry point, Object Type nodes, Alpha nodes, Left Input Adapter nodes, join Beta nodes, etc. down to Rule Terminal nodes. + +## Example: plotting Partition layout + +Given the following rules in the Knowledge Base: + +``` +rule R0 when + $i : Integer( intValue == 0 ) + String( toString == $i.toString ) +then + list.add($i); +end +rule R1 when + $i : Integer( intValue == 1 ) + String( toString == $i.toString ) +then + list.add($i); +end +rule R2 when + $i : Integer( intValue == 2 ) + String( toString == $i.toString ) +then + list.add($i); +end +rule R3 when + $i : Integer( intValue == 2 ) + String( length == $i ) +then + list.add($i); +end +``` + +The diagram plotted with the Partition layout: + +```java +ReteDiagram.newInstance().configLayout(Layout.PARTITION).diagramRete(ksession); +``` + +is rendered as: + +![example2](example2.png) + +The diagram displays nodes in their respective Partitions. diff --git a/drools-retediagram/example1.png b/drools-retediagram/example1.png new file mode 100644 index 00000000000..e5df317fb28 Binary files /dev/null and b/drools-retediagram/example1.png differ diff --git a/drools-retediagram/example2.png b/drools-retediagram/example2.png new file mode 100644 index 00000000000..6b298b1d23d Binary files /dev/null and b/drools-retediagram/example2.png differ diff --git a/drools-retediagram/pom.xml b/drools-retediagram/pom.xml new file mode 100644 index 00000000000..e74b15743b7 --- /dev/null +++ b/drools-retediagram/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + org.drools + drools + 7.53.0-SNAPSHOT + + drools-retediagram + jar + + drools-retediagram + Internal utility (EXPERIMENTAL) to plot Rete diagram, similar to `ReteDumper`, using Dot language format + http://drools.org + + + org.drools.retediagram + + + + + guru.nidi + graphviz-java + + + org.drools + drools-compiler + + + org.drools + drools-mvel + test + + + org.drools + drools-core + test-jar + test + + + org.drools + drools-compiler + test-jar + test + + + org.drools + drools-legacy-test-util + test-jar + test + + + + junit + junit + test + + + org.slf4j + jul-to-slf4j + test + + + ch.qos.logback + logback-classic + test + + + diff --git a/drools-retediagram/src/main/java/org/drools/retediagram/ReteDiagram.java b/drools-retediagram/src/main/java/org/drools/retediagram/ReteDiagram.java new file mode 100644 index 00000000000..a8ebe891e35 --- /dev/null +++ b/drools-retediagram/src/main/java/org/drools/retediagram/ReteDiagram.java @@ -0,0 +1,532 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed 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.drools.retediagram; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.PrintStream; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.drools.core.base.ClassObjectType; +import org.drools.core.common.BaseNode; +import org.drools.core.impl.InternalKnowledgeBase; +import org.drools.core.reteoo.AccumulateNode; +import org.drools.core.reteoo.AlphaNode; +import org.drools.core.reteoo.BetaNode; +import org.drools.core.reteoo.EntryPointNode; +import org.drools.core.reteoo.JoinNode; +import org.drools.core.reteoo.LeftInputAdapterNode; +import org.drools.core.reteoo.LeftTupleSource; +import org.drools.core.reteoo.NotNode; +import org.drools.core.reteoo.ObjectSource; +import org.drools.core.reteoo.ObjectTypeNode; +import org.drools.core.reteoo.Rete; +import org.drools.core.reteoo.RightInputAdapterNode; +import org.drools.core.reteoo.RuleTerminalNode; +import org.drools.core.reteoo.Sink; +import org.drools.core.spi.BetaNodeFieldConstraint; +import org.drools.core.spi.ObjectType; +import org.kie.api.KieBase; +import org.kie.api.runtime.KieRuntime; +import org.kie.api.runtime.KieSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import guru.nidi.graphviz.engine.Format; +import guru.nidi.graphviz.engine.Graphviz; +import guru.nidi.graphviz.model.MutableGraph; +import guru.nidi.graphviz.parse.Parser; + +public class ReteDiagram { + + private static final Logger LOG = LoggerFactory.getLogger(ReteDiagram.class); + + public enum Layout { + PARTITION, VLEVEL + } + + private Layout layout; + private File outputPath; + private boolean prefixTimestamp; + private boolean outputSVG; + private boolean outputPNG; + private boolean openSVG; + private boolean openPNG; + private boolean printDebugVerticalCluster = false; + + private ReteDiagram() { } + + /** + * With default settings. + */ + public static ReteDiagram newInstance() { + File outpath = new File("."); + try { + outpath = Files.createTempDirectory("retediagram").toFile(); + } catch (Exception e) { + // do nothing. + } + return new ReteDiagram() + .configLayout(Layout.VLEVEL) + .configFilenameScheme(outpath, true) + .configGraphvizRender(true, true) + .configOpenFile(false, false) + ; + } + + /** + * Changes diagram Layout + */ + public ReteDiagram configLayout(Layout layout) { + this.layout = layout; + return this; + } + + public ReteDiagram configPrintDebugVerticalCluster(boolean printDebugVerticalCluster) { + this.printDebugVerticalCluster = printDebugVerticalCluster; + return this; + } + + public ReteDiagram configFilenameScheme(File outputPath, boolean prefixTimestamp) { + this.outputPath = outputPath; + this.prefixTimestamp = prefixTimestamp; + return this; + } + + public ReteDiagram configGraphvizRender(boolean outputSVG, boolean outputPNG) { + this.outputSVG = outputSVG; + this.outputPNG = outputPNG; + return this; + } + + public ReteDiagram configOpenFile(boolean openSVG, boolean openPNG) { + this.openSVG = openSVG; + this.openPNG = openPNG; + return this; + } + + public void diagramRete(KieBase kbase) { + diagramRete((InternalKnowledgeBase) kbase); + } + + public void diagramRete(KieRuntime session) { + diagramRete((InternalKnowledgeBase)session.getKieBase()); + } + + public void diagramRete(KieSession session) { + diagramRete((InternalKnowledgeBase)session.getKieBase()); + } + + public void diagramRete(InternalKnowledgeBase kBase) { + diagramRete(kBase.getRete()); + } + + public void diagramRete(Rete rete) { + String timestampPrefix = (new SimpleDateFormat("yyyyMMddHHmmssSSS")).format(new Date()); + String fileNameNoExtension = (prefixTimestamp?timestampPrefix+".":"") + rete.getKnowledgeBase().getId(); + String gvFileName = fileNameNoExtension + ".gv"; + String svgFileName = fileNameNoExtension + ".svg"; + String pngFileName = fileNameNoExtension + ".png"; + File gvFile = new File(outputPath, gvFileName); + File svgFile = new File(outputPath, svgFileName); + File pngFile = new File(outputPath, pngFileName); + try (PrintStream out = new PrintStream(new FileOutputStream(gvFile));) { + out.println("digraph g {\n" + + "graph [fontname = \"Overpass\" fontsize=11];\n" + + " node [fontname = \"Overpass\" fontsize=11];\n" + + " edge [fontname = \"Overpass\" fontsize=11];"); + HashMap, Set> levelMap = new HashMap<>(); + HashMap, List> nodeMap = new HashMap<>(); + List> vertexes = new ArrayList<>(); + Set visitedNodesIDs = new HashSet<>(); + for (EntryPointNode entryPointNode : rete.getEntryPointNodes().values()) { + visitNodes( entryPointNode, "", visitedNodesIDs, nodeMap, vertexes, levelMap, out); + } + + out.println(""); + printNodeMap(nodeMap, out); + + out.println(""); + printVertexes(vertexes, out); + + out.println(""); + printLevelMap(levelMap, out, vertexes); + + out.println(""); + if (layout == Layout.PARTITION) { + printPartitionMap(nodeMap, out, vertexes); + } + + out.println("}"); + } catch (Exception e) { + LOG.error("Error building diagram", e); + } + LOG.info("Written gvFile: {}", gvFile); + + if (outputSVG) { + try { + MutableGraph g = new Parser().read(gvFile); + Graphviz.fromGraph(g).render(Format.SVG).toFile(svgFile); + LOG.info("Written svgFile: {}", svgFile); + } catch (Exception e) { + LOG.error("Error building SVG file", e); + } + } + if (outputPNG) { + try { + MutableGraph g = new Parser().read(gvFile); + Graphviz.fromGraph(g).render(Format.PNG).toFile(pngFile); + LOG.info("Written pngFile: {}", pngFile); + } catch (Exception e) { + LOG.error("Error building PNG file", e); + } + } + + if (outputSVG && openSVG) { + try { + java.awt.Desktop.getDesktop().open(svgFile); + } catch (Exception e) { + LOG.error("Error opening SVG file", e); + } + } + if (outputPNG && openPNG) { + try { + java.awt.Desktop.getDesktop().open(pngFile); + } catch (Exception e) { + LOG.error("Error opening PNG file", e); + } + } + } + + private static void printVertexes(List> vertexes, PrintStream out ) { + for ( Vertex v : vertexes ) { + out.println(printNodeId(v.from) + " -> " + printNodeId(v.to) + " ;"); + } + } + + private static void printNodeMap(HashMap, List> nodeMap, PrintStream out) { + printNodeMapNodes(nodeMap.get(EntryPointNode.class), out); + printNodeMapNodes(nodeMap.get(ObjectTypeNode.class), out); + printNodeMapNodes(nodeMap.getOrDefault(AlphaNode.class, Collections.emptyList()), out); + // LIAs + List l3 = nodeMap.entrySet().stream() + .filter(kv->LeftInputAdapterNode.class.isAssignableFrom( kv.getKey() )) + .flatMap(kv->kv.getValue().stream()).collect(toList()); + printNodeMapNodes(l3, out); + printNodeMapNodes(nodeMap.getOrDefault(RightInputAdapterNode.class, Collections.emptyList()), out); + // Level 4: BN + List l4 = nodeMap.entrySet().stream() + .filter(kv->BetaNode.class.isAssignableFrom( kv.getKey() )) + .flatMap(kv->kv.getValue().stream()).collect(toList()); + printNodeMapNodes(l4, out); + printNodeMapNodes(nodeMap.get(RuleTerminalNode.class), out); + } + + public static void printNodeMapNodes(List nodes, PrintStream out) { + for (BaseNode node : nodes) { + out.println(printNodeId(node) + " " + printNodeAttributes(node) + " ;"); + } + } + + public static class Vertex { + public final F from; + public final T to; + public Vertex(F from, T to) { + this.from = from; + this.to = to; + } + public static Vertex of(F from, T to) { + return new Vertex(from, to); + } + } + + private static void printPartitionMap(HashMap, List> nodeMap, PrintStream out, List> vertexes) { + Map> byPartition = nodeMap.entrySet().stream() + .flatMap(kv->kv.getValue().stream()) + .collect(groupingBy(n->n.getPartitionId() == null ? 0 : n.getPartitionId().getId())); + + for (Entry> kv : byPartition.entrySet()) { + printClusterMapCluster("P"+kv.getKey(), new HashSet<>(kv.getValue()), out); + } + } + + private void printLevelMap(HashMap, Set> levelMap, PrintStream out, List> vertexes) { + + // Level 1: OTN + Set l1 = levelMap.entrySet().stream() + .filter(kv->ObjectTypeNode.class.isAssignableFrom( kv.getKey() )) + .flatMap(kv->kv.getValue().stream()).collect(toSet()); + printLevelMapLevel("l1", l1, out); + + // Level 2: AN + Set l2 = levelMap.entrySet().stream() + .filter(kv->AlphaNode.class.isAssignableFrom( kv.getKey() )) + .flatMap(kv->kv.getValue().stream()).collect(toSet()); + printLevelMapLevel("l2", l2, out); + + // Level 3: LIA + Set l3 = levelMap.entrySet().stream() + .filter(kv->LeftInputAdapterNode.class.isAssignableFrom( kv.getKey() )) + .flatMap(kv->kv.getValue().stream()).collect(toSet()); + printLevelMapLevel("l3", l3, out); + + // RIA + Set lria = levelMap.entrySet().stream() + .filter(kv->RightInputAdapterNode.class.isAssignableFrom( kv.getKey() )) + .flatMap(kv->kv.getValue().stream()).collect(toSet()); + printLevelMapLevel("lria", lria, out); + + // RIA beta sources + Set lriaSources = new HashSet<>(); + Set> onlyBetas = vertexes.stream().filter(v->v.from instanceof BetaNode).collect(toSet()); + for (BaseNode ria : lria) { + Set t = onlyBetas.stream() + .filter(v->v.to.equals(ria)) + .map(v->v.from) + .collect(toSet()); + lriaSources.addAll(t); + } + for (BaseNode lriaSource : lriaSources) { + lriaSources.addAll( recurseIncomingVertex(lriaSource, onlyBetas) ); + } + printLevelMapLevel("lriaSources", lriaSources, out); + + // subnetwork Betas + Set lsubbeta = levelMap.entrySet().stream() + .filter(kv->BetaNode.class.isAssignableFrom( kv.getKey() )) + .flatMap(kv->kv.getValue().stream()) + .filter(b-> ((BetaNode) b).getObjectType() == null ) + .collect(toSet()); + printLevelMapLevel("lsubbeta", lsubbeta, out); + + // Level 4: BN + Set l4 = levelMap.entrySet().stream() + .filter(kv->BetaNode.class.isAssignableFrom( kv.getKey() )) + .flatMap(kv->kv.getValue().stream()) + .filter(b-> !lriaSources.contains(b) ) + .filter(b-> !lsubbeta.contains(b) ) + .collect(toSet()); + printLevelMapLevel("l4", l4, out); + + // Level 5: RTN + Set l5 = levelMap.entrySet().stream() + .filter(kv->RuleTerminalNode.class.isAssignableFrom( kv.getKey() )) + .flatMap(kv->kv.getValue().stream()).collect(toSet()); + printLevelMapLevel("l5", l5, out); + + out.println( + ((this.printDebugVerticalCluster) ? "" : " edge[style=invis];\n") + + " l1->l2->l3->lriaSources->lria->lsubbeta->l4->l5;"); + } + + private static Set recurseIncomingVertex(BaseNode to, Set> vertexes) { + Set acc = new HashSet<>(); + for (Vertex v : vertexes) { + if (v.to.equals(to)) { + acc.add( v.from ); + acc.addAll( recurseIncomingVertex(v.from, vertexes) ); + } + } + return acc; + } + + private static void printClusterMapCluster(String levelId, Set value, PrintStream out) { + StringBuilder nodeIds = new StringBuilder(); + for (BaseNode n : value) { + nodeIds.append(printNodeId(n)+"; "); + } + String level = String.format(" subgraph cluster_%1$s{style=dotted; labelloc=b; label=\"%1$s\"; %2$s}", + levelId, + nodeIds.toString()); + out.println(level); + } + + private void printLevelMapLevel(String levelId, Set value, PrintStream out) { + StringBuilder nodeIds = new StringBuilder(); + for (BaseNode n : value) { + nodeIds.append(printNodeId(n)+"; "); + } + if (layout == Layout.PARTITION) { + String level = String.format(" subgraph %1$s{%1$s[" + ((this.printDebugVerticalCluster) ? "shape=point, xlabel=\"%1$s\"" : "shape=none, label=\"\"") + "]; %2$s}", + levelId, + nodeIds.toString()); + out.println(level); + } else { + String level = String.format(" {rank=same; %1$s[" + ((this.printDebugVerticalCluster) ? "shape=point, xlabel=\"%1$s\"" : "shape=none, label=\"\"") + "]; %2$s}", + levelId, + nodeIds.toString()); + out.println(level); + } + } + + private static void visitNodes(BaseNode node, String ident, Set visitedNodesIDs, HashMap, List> nodeMap, List> vertexes, Map, Set> levelMap, PrintStream out) { + if (!visitedNodesIDs.add( node.getId() )) { + return; + } + addToNodeMap(node, nodeMap); + addToLevel(node, levelMap); + Sink[] sinks = getSinks( node ); + if (sinks != null) { + for (Sink sink : sinks) { + vertexes.add(Vertex.of(node, (BaseNode)sink)); + if (sink instanceof BaseNode) { + visitNodes((BaseNode)sink, ident + " ", visitedNodesIDs, nodeMap, vertexes, levelMap, out); + } + } + } + } + + private static void addToNodeMap(BaseNode node, HashMap, List> nodeMap) { + nodeMap.computeIfAbsent(node.getClass(), k -> new ArrayList<>()).add(node); + } + + private static void addToLevel(BaseNode node, Map, Set> levelMap) { + levelMap.computeIfAbsent(node.getClass(), k -> new HashSet<>()).add(node); + } + + private static String printNodeId(BaseNode node) { + if (node instanceof EntryPointNode ) { + return "EP"+node.getId(); + } else if (node instanceof ObjectTypeNode ) { + return "OTN"+node.getId(); + } else if (node instanceof AlphaNode ) { + return "AN"+node.getId(); + } else if (node instanceof LeftInputAdapterNode ) { + return "LIA"+node.getId(); + } else if (node instanceof RightInputAdapterNode ) { + return "RIA"+node.getId(); + } else if (node instanceof BetaNode ) { + return "BN"+node.getId(); + } else if (node instanceof RuleTerminalNode ) { + return "RTN"+node.getId(); + } + return "UNK"+node.getId(); + } + + private static String printNodeAttributes(BaseNode node) { + if (node instanceof EntryPointNode ) { + EntryPointNode n = (EntryPointNode) node; + return String.format("[shape=circle width=0.15 fillcolor=black style=filled label=\"\" xlabel=\"%1$s\"]", + n.getEntryPoint().getEntryPointId()); + } else if (node instanceof ObjectTypeNode ) { + ObjectTypeNode n = (ObjectTypeNode) node; + return String.format("[shape=rect style=rounded label=\"%1$s\"]", + strObjectType(n.getObjectType()) ); + } else if (node instanceof AlphaNode ) { + AlphaNode n = (AlphaNode) node; + return String.format("[label=\"%1$s\"]", + escapeDot(n.getConstraint().toString())); + } else if (node instanceof LeftInputAdapterNode ) { + return "[shape=house orientation=-90]"; + } else if (node instanceof RightInputAdapterNode ) { + return "[shape=house orientation=90]"; + } else if (node instanceof JoinNode ) { + BetaNode n = (BetaNode) node; + BetaNodeFieldConstraint[] constraints = n.getConstraints(); + String label = "\u22C8"; + if (constraints.length > 0) { + label = strObjectType(n.getObjectType(), false); + label = label + "( "+ Arrays.stream(constraints).map(Object::toString).collect(joining(", ")) + " )"; + } + return String.format("[shape=box label=\"%1$s\" href=\"http://drools.org\"]", + escapeDot(label)); + } else if (node instanceof NotNode ) { + NotNode n = (NotNode) node; + String label = "\u22C8"; + if (n.getObjectType() != null) { + label = strObjectType(n.getObjectType(), false); + label = label + "("; + if ( n.getConstraints().length>0 ) { + label = label + " "+ Arrays.stream(n.getConstraints()).map(Object::toString).collect(joining(", ")) + " "; + } + label = label + ")"; + } + return String.format("[shape=box label=\"not( %1$s )\"]", label ); + } else if (node instanceof AccumulateNode ) { + AccumulateNode n = (AccumulateNode) node; + return String.format("[shape=box label=<%1$s
%2$s
%3$s>]", + n, Arrays.asList(n.getAccumulate().getAccumulators()), Arrays.asList(n.getConstraints()) ); + } else if (node instanceof RuleTerminalNode ) { + RuleTerminalNode n = (RuleTerminalNode) node; + return String.format("[shape=doublecircle width=0.2 fillcolor=black style=filled label=\"\" xlabel=\"%1$s\" href=\"http://drools.org\"]", + n.getRule().getName()); + } + return String.format("[shape=box style=dotted label=\"%1$s\"]", node.toString()); + } + + private static String strObjectType(ObjectType ot) { + return strObjectType(ot, true); + } + + private static String strObjectType(ObjectType ot, boolean prependAbbrPackage) { + if (ot instanceof ClassObjectType) { + return abbrvClassForObjectType((ClassObjectType) ot, prependAbbrPackage); + } + return "??"+ ((ot==null)?"null":ot.toString()); + } + + private static String abbrvClassForObjectType(ClassObjectType cot, boolean prependAbbrPackage) { + Class classType = cot.getClassType(); + StringBuilder result = new StringBuilder(); + if (prependAbbrPackage) { + String[] packageToken = classType.getPackage().getName().split("\\."); + for (String pt : packageToken) { + result.append(pt.charAt(0) + "."); + } + } + result.append(classType.getSimpleName()); + return result.toString(); + } + + private static String escapeDot(String string) { + String escapeQuote = string.replace("\"", "\\\""); + return escapeQuote; + } + + public static Sink[] getSinks( BaseNode node ) { + Sink[] sinks = null; + if (node instanceof EntryPointNode ) { + EntryPointNode source = (EntryPointNode) node; + Collection otns = source.getObjectTypeNodes().values(); + sinks = otns.toArray(new Sink[otns.size()]); + } else if (node instanceof ObjectSource ) { + ObjectSource source = (ObjectSource) node; + sinks = source.getObjectSinkPropagator().getSinks(); + } else if (node instanceof LeftTupleSource ) { + LeftTupleSource source = (LeftTupleSource) node; + sinks = source.getSinkPropagator().getSinks(); + } + return sinks; + } +} diff --git a/drools-retediagram/src/test/java/org/drools/retediagram/RuleTest.java b/drools-retediagram/src/test/java/org/drools/retediagram/RuleTest.java new file mode 100644 index 00000000000..579592633cc --- /dev/null +++ b/drools-retediagram/src/test/java/org/drools/retediagram/RuleTest.java @@ -0,0 +1,605 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed 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.drools.retediagram; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; + +import org.drools.core.reteoo.ReteDumper; +import org.drools.mvel.CommonTestMethodBase; +import org.drools.retediagram.ReteDiagram.Layout; +import org.drools.retediagram.model.Measurement; +import org.junit.BeforeClass; +import org.junit.Test; +import org.kie.api.KieBase; +import org.kie.api.event.rule.DebugAgendaEventListener; +import org.kie.api.io.ResourceType; +import org.kie.api.runtime.KieSession; +import org.kie.internal.builder.KnowledgeBuilderConfiguration; +import org.kie.internal.builder.KnowledgeBuilderFactory; +import org.kie.internal.utils.KieHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.bridge.SLF4JBridgeHandler; + +public class RuleTest extends CommonTestMethodBase { + static final Logger LOG = LoggerFactory.getLogger(RuleTest.class); + + @BeforeClass + public static void init() { // route dependencies using java util Logging to slf4j + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + java.util.logging.Logger.getLogger("").setLevel(Level.FINEST); + } + + @Test + public void test() { + KieBase kieBase = new KieHelper() + .addFromClassPath("/rules.drl") + .build(); + + LOG.info("Creating kieSession"); + KieSession session = kieBase.newKieSession(); + + LOG.info("Populating globals"); + Set check = new HashSet(); + session.setGlobal("controlSet", check); + + LOG.info("Now running data"); + + Measurement mRed= new Measurement("color", "red"); + session.insert(mRed); + session.fireAllRules(); + + Measurement mGreen= new Measurement("color", "green"); + session.insert(mGreen); + session.fireAllRules(); + + Measurement mBlue= new Measurement("color", "blue"); + session.insert(mBlue); + session.fireAllRules(); + + LOG.info("Final checks"); + + assertEquals("Size of object in Working Memory is 3", 3, session.getObjects().size()); + assertTrue("contains red", check.contains("red")); + assertTrue("contains green", check.contains("green")); + assertTrue("contains blue", check.contains("blue")); + + ReteDumper.dumpRete(session); + System.out.println("---"); + ReteDiagram.newInstance() + .configLayout(Layout.VLEVEL) + // needs: System.setProperty("java.awt.headless", "false"); for: .configOpenFile(true, true) + .diagramRete(session.getKieBase()); + } + + @Test + public void testVeryBasic() { + String drl = + "import org.drools.retediagram.model.*;\n" + + "rule R1\n" + + "when\n" + + " $p : Person( age > 18 )\n" + + "then\n" + + " System.out.println(\"Person can drive \"+$p);\n"+ + "end" + ; + KieSession kieSession = new KieHelper().addContent( drl, ResourceType.DRL ) + .build().newKieSession(); + + ReteDiagram.newInstance().diagramRete(kieSession); + } + + @Test + public void testManyAccumulatesWithSubnetworks() { + String drl = "package org.drools.compiler.tests; \n" + + "" + + "declare FunctionResult\n" + + " father : Applied\n" + + "end\n" + + "\n" + + "declare Field\n" + + " applied : Applied\n" + + "end\n" + + "\n" + + "declare Applied\n" + + "end\n" + + "\n" + + "\n" + + "rule \"Seed\"\n" + + "when\n" + + "then\n" + + " Applied app = new Applied();\n" + + " Field fld = new Field();\n" + + "\n" + + " insert( app );\n" + + " insert( fld );\n" + + "end\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "rule \"complexSubNetworks\"\n" + + "when\n" + + " $fld : Field( $app : applied )\n" + + " $a : Applied( this == $app )\n" + + " accumulate (\n" + + " $res : FunctionResult( father == $a ),\n" + + " $args : collectList( $res )\n" + + " )\n" + + " accumulate (\n" + + " $res : FunctionResult( father == $a ),\n" + + " $deps : collectList( $res )\n" + + " )\n" + + " accumulate (\n" + + " $x : String()\n" + + " and\n" + + " not String( this == $x ),\n" + + " $exprFieldList : collectList( $x )\n" + + " )\n" + + "then\n" + + "end\n" + + "\n"; + + KnowledgeBuilderConfiguration kbConf = KnowledgeBuilderFactory.newKnowledgeBuilderConfiguration(); + KieBase kbase = loadKnowledgeBaseFromString(kbConf, drl); + + KieSession ksession = kbase.newKieSession(); + + int num = ksession.fireAllRules(); + // only one rule should fire, but the partial propagation of the asserted facts should not cause a runtime NPE + assertEquals( 1, num ); + ReteDiagram.newInstance().configLayout(Layout.PARTITION).diagramRete(ksession); + } + + + @Test + public void testLinkRiaNodesWithSubSubNetworks() { + String drl = "package org.drools.compiler.tests; \n" + + "" + + "import java.util.*; \n" + + "" + + "global List list; \n" + + "" + + "declare MyNode\n" + + "end\n" + + "" + + "rule Init\n" + + "when\n" + + "then\n" + + " insert( new MyNode() );\n" + + " insert( new MyNode() );\n" + + "end\n" + + "" + + "" + + "rule \"Init tree nodes\"\n" + + "salience -10\n" + + "when\n" + + " accumulate (\n" + + " MyNode(),\n" + + " $x : count( 1 )\n" + + " )\n" + + " accumulate (\n" + + " $n : MyNode()\n" + + " and\n" + + " accumulate (\n" + + " $val : Double( ) from Arrays.asList( 1.0, 2.0, 3.0 ),\n" + + " $rc : count( $val );\n" + + " $rc == 3 \n" + + " ),\n" + + " $y : count( $n )\n" + + " )\n" + + "then\n" + + " list.add( $x ); \n" + + " list.add( $y ); \n" + + " System.out.println( $x ); \n" + + " System.out.println( $y ); \n" + + "end\n"; + + KnowledgeBuilderConfiguration kbConf = KnowledgeBuilderFactory.newKnowledgeBuilderConfiguration(); + KieBase kbase = loadKnowledgeBaseFromString(kbConf, drl); + + KieSession ksession = kbase.newKieSession(); + List list = new ArrayList(); + ksession.setGlobal( "list", list ); + + ksession.fireAllRules(); + + assertEquals( 2, list.size() ); + assertEquals( 2, list.get( 0 ).intValue() ); + assertEquals( 2, list.get( 1 ).intValue() ); + + ReteDiagram.newInstance().configLayout(Layout.PARTITION).diagramRete((KieSession)ksession); + } + + @Test + public void testMArio() { + String drl = + "rule R1y ruleflow-group \"Y\" when\n" + + " Integer() \n" + + " Number() from accumulate ( Integer( ) and $s : String( ) ; count($s) )\n" + + "then\n" + + " System.out.println(\"R1\");" + + "end\n" + + "\n" + + "rule R1x ruleflow-group \"X\" when\n" + + " Integer() \n" + + " Number() from accumulate ( Integer( ) and $s : String( ) ; count($s) )\n" + + "then\n" + + " System.out.println(\"R1\");" + + "end\n" + + "" + + "rule R2 ruleflow-group \"X\" when\n" + + " $i : Integer()\n" + + "then\n" + + " System.out.println(\"R2\");" + + " update($i);" + + "end\n"; + + KieSession kieSession = new KieHelper().addContent( drl, ResourceType.DRL ) + .build().newKieSession(); + + ReteDiagram.newInstance().diagramRete(kieSession); + } + + @Test + public void testMario20161021() { + String drl = "import java.util.Set;\n" + + "declare Notification end\n" + + "declare Fall end\n" + + "declare NetworkElement end\n" + + "rule R1 when\n" + + " $notification : Notification()\n" + + " $epcs : Set() from collect( Fall() )\n" + + " then\n" + + "end\n" + + "\n" + + "rule R2 when\n" + + " $notification : Notification()\n" + + " not Fall()\n" + + " $epcs : Set() from collect(Fall())\n" + + "\n" + + " then\n" + + "end\n" + + "\n" + + "rule R3 when\n" + + " $ne : NetworkElement()\n" + + " then\n" + + "end"; + + KieSession kieSession = new KieHelper().addContent( drl, ResourceType.DRL ) + .build().newKieSession(); + ReteDumper.dumpRete(kieSession); + ReteDiagram.newInstance().diagramRete(kieSession); + } + + @Test + public void testTzimani20161021() { + String drl = "import java.util.Set;\ndeclare Person end\ndeclare FactWithCheese end\ndeclare Cheese end\n"+ + "rule \"R1\"\n" + + " when\n" + + " $person : Person()\n" + + " $aFacts : Set() from collect( FactWithCheese() )\n" + + " then\n" + + "end\n" + + "\n" + + "rule \"R2\"\n" + + " when\n" + + " $person : Person()\n" + + " not FactWithCheese()\n" + + " $aFacts : Set() from collect( FactWithCheese() )\n" + + "\n" + + " then\n" + + "end\n" + + "\n" + + "rule \"R3\"\n" + + " when\n" + + " $cheese : Cheese()\n" + + " then\n" + + "end"; + + KieSession kieSession = new KieHelper().addContent( drl, ResourceType.DRL ) + .build().newKieSession(); + ReteDumper.dumpRete(kieSession); + ReteDiagram.newInstance().diagramRete(kieSession); + } + + @Test + public void testDROOLS1360() { + String drl = + "import " + AtomicInteger.class.getCanonicalName() + ";\n" + + "global java.util.List list;\n" + + // "rule R1y when\n" + + // " AtomicInteger() \n" + + // " Number() from accumulate ( AtomicInteger( ) and $s : String( ) ; count($s) )" + + // " eval(false)\n" + + // "then\n" + + // "end\n" + + "\n" + + "rule R2 when\n" + + " $i : AtomicInteger( get() < 3 )\n" + + "then\n" + + " $i.incrementAndGet();" + + " insert(\"test\" + $i.get());" + + " update($i);" + + "end\n" + + "\n" + + "rule R1x when\n" + + " AtomicInteger() \n" + + " $c : Number() from accumulate ( AtomicInteger( ) and $s : String( ) ; count($s) )\n" + + " eval(true)\n" + + "then\n" + + " list.add($c);" + + "end\n" + ; + + KieSession kieSession = new KieHelper().addContent( drl, ResourceType.DRL ) + .build().newKieSession(); + ReteDumper.dumpRete(kieSession); + ReteDiagram.newInstance().diagramRete(kieSession); + } + + @Test + public void testDROOLS_1326_simple() { + String drl = "package "+this.getClass().getPackage().getName()+";\n" + + "import "+MyPojo.class.getCanonicalName()+"\n" + + "global java.util.Set controlSet;\n" + + "rule R1\n" + + "when\n" + + " $my: MyPojo(\n" + + " vBoolean == true,\n" + + " $s : vString, vString != null,\n" + + " $l : vLong\n" + + " )\n" + + " not MyPojo(\n" + + " vBoolean == true,\n" + + " vString == $s,\n" + + " vLong > $l\n" + + " )\n" + + "then\n" + + " System.out.println($my);\n" + + " System.out.println($l);\n" + + " controlSet.add($l);\n" + + "end"; + System.out.println(drl); + + KieSession session = new KieHelper().addContent(drl, ResourceType.DRL).build().newKieSession(); + ReteDumper.dumpRete(session); + ReteDiagram.newInstance().diagramRete(session); + } + + @Test + public void testDROOLS_1326_simple_again() { + String drl = "package "+this.getClass().getPackage().getName()+";\n" + + "import "+MyPojo.class.getCanonicalName()+"\n" + + "global java.util.Set controlSet;\n" + + "rule R1\n" + + "when\n" + + " $my: MyPojo(\n" + + " vBoolean == true,\n" + + " $s : vString, vString != null,\n" + + " $l : vLong\n" + + " )\n" + + " not MyPojo(\n" + + " vBoolean == true,\n" + + " vString.equals($s),\n" + + " vLong > $l\n" + + " )\n" + + "then\n" + + " System.out.println(\"->> firing with \"+ kcontext.getKieRuntime().getFactHandle($my) );\n" + + " System.out.println(\"->> firing with \"+$l);\n" + + " controlSet.add($l);\n" + + "end"; + System.out.println(drl); + + KieSession session = new KieHelper().addContent(drl, ResourceType.DRL).build().newKieSession(); + ReteDumper.dumpRete(session); + ReteDiagram.newInstance().diagramRete(session); + } + + public static class MyPojo { + private boolean vBoolean; + private String vString; + private long vLong; + + public MyPojo(boolean vBoolean, String vString, long vLong) { + super(); + this.vBoolean = vBoolean; + this.vString = vString; + this.vLong = vLong; + } + + public boolean isvBoolean() { + return vBoolean; + } + + public boolean getvBoolean() { + return vBoolean; + } + + + public String getvString() { + return vString; + } + + public long getvLong() { + return vLong; + } + + public void setvBoolean(boolean vBoolean) { + this.vBoolean = vBoolean; + } + + public void setvString(String vString) { + this.vString = vString; + } + + public void setvLong(long vLong) { + this.vLong = vLong; + } + } + + public static class LongHolder { + + private final Long value; + + public LongHolder(Long value) { + this.value = value; + } + + public Long getValue() { + return value; + } + + } + + public static class WeirdObject { + private Integer status; + private WeirdNested nested; + + public WeirdObject(Integer status, WeirdNested nested) { + this.status = status; + this.nested = nested; + } + + public Integer getStatus() { + return status; + } + + public Long added() { + return nested.getId(); + } + + public WeirdNested getNested() { + return nested; + } + + } + public static class WeirdNested { + private Long id; + + public WeirdNested(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + } + + @Test + public void testCheck() { + String drl = "package i.p;\n" + + "import " + LongHolder.class.getCanonicalName() + "\n" + + "import " + WeirdObject.class.getCanonicalName() + "\n" + + "import " + TestObjectEnum.class.getCanonicalName() + "\n" + + "rule fileArule1 when\n" + + " $t : LongHolder()\n" + + " WeirdObject(added == $t.value, status == TestObjectEnum.ONE.getValue() )\n" + + "then\n" + + "end\n" + + "rule fileArule2 when\n" + + " $t : LongHolder()\n" + + " WeirdObject(nested.id == $t.value, status == 0 )\n" + + "then\n" + + "end\n" + ; + + String drl2 = "package c.t.p;\n" + + "import " + WeirdObject.class.getCanonicalName() + "\n" + + "import " + TestObjectEnum.class.getCanonicalName() + "\n" + + "rule fileBrule1 when\n" + + " WeirdObject(status == 1)\n" + + "then\n" + + "end\n" + ; + + KieSession kieSession = new KieHelper() + .addContent(drl2, ResourceType.DRL) + .addContent(drl, ResourceType.DRL) + .build().newKieSession(); + + ReteDiagram.newInstance().diagramRete(kieSession); + kieSession.addEventListener(new DebugAgendaEventListener()); + + kieSession.insert(new LongHolder(12345L)); + kieSession.insert(new WeirdObject(1, new WeirdNested(12345L))); + + kieSession.fireAllRules(); + } + + public static class TestObjectFunction { + public static int return1() { + return 1; + } + } + + public static enum TestObjectEnum { + ZERO(0), + ONE(1); + private int value; + TestObjectEnum(int value) { + this.value = value; + } + public int getValue() { + return this.value; + } + } + + @Test + public void test20180111() { + String drl = "global java.util.List list;\n" + + "rule R0 when\n" + + " $i : Integer( intValue == 0 )\n" + + " String( toString == $i.toString )\n" + + "then\n" + + " list.add($i);\n" + + "end\n" + + "rule R1 when\n" + + " $i : Integer( intValue == 1 )\n" + + " String( toString == $i.toString )\n" + + "then\n" + + " list.add($i);\n" + + "end\n" + + "rule R2 when\n" + + " $i : Integer( intValue == 2 )\n" + + " String( toString == $i.toString )\n" + + "then\n" + + " list.add($i);\n" + + "end\n" + + "rule R3 when\n" + + " $i : Integer( intValue == 2 )\n" + + " String( length == $i )\n" + + "then\n" + + " list.add($i);\n" + + "end"; + KnowledgeBuilderConfiguration kbConf = KnowledgeBuilderFactory.newKnowledgeBuilderConfiguration(); + KieBase kbase = loadKnowledgeBaseFromString(kbConf, drl); + + KieSession ksession = kbase.newKieSession(); + ReteDiagram.newInstance().configLayout(Layout.PARTITION).diagramRete(ksession); + } +} \ No newline at end of file diff --git a/drools-retediagram/src/test/java/org/drools/retediagram/model/Cheese.java b/drools-retediagram/src/test/java/org/drools/retediagram/model/Cheese.java new file mode 100644 index 00000000000..397029e862d --- /dev/null +++ b/drools-retediagram/src/test/java/org/drools/retediagram/model/Cheese.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed 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.drools.retediagram.model; + +public class Cheese { + + private final String name; + + public Cheese(String name) { + super(); + this.name = name; + } + + public String getName() { + return name; + } + +} diff --git a/drools-retediagram/src/test/java/org/drools/retediagram/model/Measurement.java b/drools-retediagram/src/test/java/org/drools/retediagram/model/Measurement.java new file mode 100644 index 00000000000..31f96d4fb49 --- /dev/null +++ b/drools-retediagram/src/test/java/org/drools/retediagram/model/Measurement.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed 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.drools.retediagram.model; + +public class Measurement { + private String id; + private String val; + + public Measurement(String id, String val) { + super(); + this.id = id; + this.val = val; + } + + public String getId() { + return id; + } + + public String getVal() { + return val; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Measurement ["); + if (id != null) + builder.append("id=").append(id).append(", "); + if (val != null) + builder.append("val=").append(val); + builder.append("]"); + return builder.toString(); + } + + +} diff --git a/drools-retediagram/src/test/java/org/drools/retediagram/model/Person.java b/drools-retediagram/src/test/java/org/drools/retediagram/model/Person.java new file mode 100644 index 00000000000..6ecbfb1977b --- /dev/null +++ b/drools-retediagram/src/test/java/org/drools/retediagram/model/Person.java @@ -0,0 +1,45 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed 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.drools.retediagram.model; + +public class Person { + private String name; + private long age; + private Cheese favouriteCheese; + + public Person(String name, Cheese favouriteCheese) { + this.name = name; + this.favouriteCheese = favouriteCheese; + } + + public Person(String name, long age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public Cheese getFavouriteCheese() { + return favouriteCheese; + } + + public long getAge() { + return age; + } +} diff --git a/drools-retediagram/src/test/resources/logback.xml b/drools-retediagram/src/test/resources/logback.xml new file mode 100644 index 00000000000..b8d959f55c7 --- /dev/null +++ b/drools-retediagram/src/test/resources/logback.xml @@ -0,0 +1,33 @@ + + + + + + + + %date{HH:mm:ss.SSS} [%thread] %-5level %class{36}.%method:%line - %msg%n + + + + + + + + + + + diff --git a/drools-retediagram/src/test/resources/rules.drl b/drools-retediagram/src/test/resources/rules.drl new file mode 100644 index 00000000000..136d286ce58 --- /dev/null +++ b/drools-retediagram/src/test/resources/rules.drl @@ -0,0 +1,41 @@ +package org.drools.retediagram.model; + +global java.util.Set controlSet; + +rule "For color" +no-loop +when + Measurement( id == "color", $colorVal : val ) +then + controlSet.add($colorVal); +end + +rule "Likes cheddar" +when + Cheese( $cheddar : name == "cheddar" ) + $person : Person( favouriteCheese == $cheddar ) +then + System.out.println( $person.getName() + " likes cheddar" ); +end + +rule "Don't like cheddar" +when + Cheese( $cheddar : name == "cheddar" ) + $person : Person( favouriteCheese != $cheddar ) +then + System.out.println( $person.getName() + " does not like cheddar" ); +end + +rule "Color count" +when + accumulate( $m: Measurement( id == "color" ); $c: count($m) ) +then + System.out.println( $c ); +end + +rule "Not a Color" +when + not ( Measurement( id == "color" ) and String() ) +then + System.out.println( "no color yet." ); +end \ No newline at end of file diff --git a/pom.xml b/pom.xml index 4d745d28933..d0a0d52d821 100644 --- a/pom.xml +++ b/pom.xml @@ -86,6 +86,7 @@ drools-engine drools-engine-classic + drools-retediagram