diff --git a/src/main/java/org/clafer/cli/Main.java b/src/main/java/org/clafer/cli/Main.java
index e7489c64..90f72721 100644
--- a/src/main/java/org/clafer/cli/Main.java
+++ b/src/main/java/org/clafer/cli/Main.java
@@ -26,11 +26,12 @@ public static void main(String[] args) throws Exception {
accepts( "n", "Specify the maximum number of instances." ).withRequiredArg().ofType( Integer.class );
accepts( "noprint", "Don't print the instances to the console or a file");
accepts( "output", "Output instances to the given file." ).withRequiredArg().ofType( File.class ).describedAs( "text file" );
+ accepts( "plantuml", "Print the clafer model as PlantUML" );
accepts( "prettify", "Use simple and pretty output format (not formal)." );
- accepts( "sysml", "Print the instances as SysMLv2" );
accepts( "repl", "Run in REPL (interactive) mode." );
accepts( "scope", "Override the default global scope value." ).withRequiredArg().ofType( Integer.class );
accepts( "search", "PreferSmallerInstances/PreferLargerInstances/Random" ).withRequiredArg().ofType( ClaferSearchStrategy.class );
+ accepts( "sysml", "Print the instances as SysMLv2" );
accepts( "time", "Time how long it takes to find all instances (and print if it is turned on");
accepts( "v", "Run in validation mode; checks all assertions." );
accepts( "version", "Display the tool version" );
diff --git a/src/main/java/org/clafer/cli/Normal.java b/src/main/java/org/clafer/cli/Normal.java
index 50778f46..4b528bb1 100644
--- a/src/main/java/org/clafer/cli/Normal.java
+++ b/src/main/java/org/clafer/cli/Normal.java
@@ -13,6 +13,9 @@
import org.clafer.objective.Objective;
import org.clafer.scope.Scope;
import org.clafer.ast.AstModel;
+import org.plantuml.ast.PlantumlProgram;
+import org.plantuml.compiler.AstPlantumlCompiler;
+import org.plantuml.pprinter.PlantumlPrinter;
import org.sysml.ast.SysmlProperty;
import org.sysml.ast.SysmlPropertyDef;
import org.sysml.compiler.AstSysmlCompiler;
@@ -22,12 +25,16 @@
public class Normal {
// Running the model itself(instantiating or optimizing)
public static void runNormal(JavascriptFile javascriptFile, OptionSet options, PrintStream outStream) throws Exception {
+ //do this first to cut irrelevant optimizing message
+ boolean plantuml = options.has("plantuml");
Objective[] objectives = javascriptFile.getObjectives();
- if (objectives.length == 0)
- System.out.println("Instantiating...");
- else
- System.out.println("Optimizing...");
+ if (!plantuml){
+ if (objectives.length == 0)
+ System.out.println("Instantiating...");
+ else
+ System.out.println("Optimizing...");
+ }
// handle scopes
Scope scope = Utils.resolveScopes(javascriptFile, options);
@@ -48,6 +55,13 @@ public static void runNormal(JavascriptFile javascriptFile, OptionSet options,
boolean printOff = options.has("noprint");
boolean dataTackingOn = options.has("dataFile");
boolean timeOn = options.has("time");
+
+ // check for conflicting options
+ if (plantuml && sysml) {
+ System.err.println("Bad CLI config: both plantuml and sysml are selected");
+ return;
+ }
+
File dataFile;
PrintStream dataStream = null;
if (dataTackingOn) {
@@ -55,6 +69,18 @@ public static void runNormal(JavascriptFile javascriptFile, OptionSet options,
dataStream = new PrintStream(dataFile);
}
+ if (plantuml) {
+ AstModel top = javascriptFile.getModel();
+ AstPlantumlCompiler compiler = new AstPlantumlCompiler();
+ PlantumlProgram prog = compiler.compile(top);
+ PlantumlPrinter pprinter = new PlantumlPrinter(outStream);
+ pprinter.visit(prog, "");
+ if (dataStream != null){
+ dataStream.close();
+ }
+ return;
+ }
+
double elapsedTime;
long startTime = System.nanoTime();
@@ -117,5 +143,10 @@ public static void runNormal(JavascriptFile javascriptFile, OptionSet options,
System.out.println("Generated " + (n == -1 ? "all " : "") + index + " optimal instance(s) within the scope\n");
}
}
+
+ // make sure to close this resource
+ if (dataStream != null){
+ dataStream.close();
+ }
}
}
diff --git a/src/main/java/org/plantuml/ast/PlantumlConnection.java b/src/main/java/org/plantuml/ast/PlantumlConnection.java
new file mode 100644
index 00000000..c1fed20d
--- /dev/null
+++ b/src/main/java/org/plantuml/ast/PlantumlConnection.java
@@ -0,0 +1,61 @@
+package org.plantuml.ast;
+
+import java.io.IOException;
+
+public class PlantumlConnection implements PlantumlExpr {
+ private final String fromObj;
+ private final String toObj;
+ private final char fromConn;
+ private final char toConn;
+
+ private final char lineChar;
+
+ private final String label;
+
+ public PlantumlConnection(String fromObj, String toObj, char fromConn, char toConn, String label){
+ this.fromObj = fromObj;
+ this.toObj = toObj;
+ this.fromConn = fromConn;
+ this.toConn = toConn;
+ this.label = label;
+ this.lineChar = '-';
+ }
+
+ public PlantumlConnection(String fromObj, String toObj, char fromConn, char toConn, String label, char lineChar){
+ this.fromObj = fromObj;
+ this.toObj = toObj;
+ this.fromConn = fromConn;
+ this.toConn = toConn;
+ this.label = label;
+ this.lineChar = lineChar;
+ }
+
+ public String getFromObj(){
+ return fromObj;
+ }
+
+ public String getToObj(){
+ return toObj;
+ }
+
+ public char getToConn(){
+ return toConn;
+ }
+
+ public char getFromConn(){
+ return fromConn;
+ }
+
+ public String getLabel(){
+ return label;
+ }
+
+ public char getLineChar(){
+ return lineChar;
+ }
+
+ @Override
+ public B accept(PlantumlExprVisitor visitor, A a) throws IOException {
+ return visitor.visit(this, a);
+ }
+}
diff --git a/src/main/java/org/plantuml/ast/PlantumlExpr.java b/src/main/java/org/plantuml/ast/PlantumlExpr.java
new file mode 100644
index 00000000..c32e48da
--- /dev/null
+++ b/src/main/java/org/plantuml/ast/PlantumlExpr.java
@@ -0,0 +1,16 @@
+package org.plantuml.ast;
+
+import java.io.IOException;
+
+public interface PlantumlExpr {
+ /**
+ * Dynamic dispatch on the visitor.
+ *
+ * @param the parameter type
+ * @param the return type
+ * @param visitor the visitor
+ * @param a the parameter
+ * @return the return value
+ */
+ B accept(PlantumlExprVisitor visitor, A a) throws IOException;
+}
diff --git a/src/main/java/org/plantuml/ast/PlantumlExprVisitor.java b/src/main/java/org/plantuml/ast/PlantumlExprVisitor.java
new file mode 100644
index 00000000..115ef7f1
--- /dev/null
+++ b/src/main/java/org/plantuml/ast/PlantumlExprVisitor.java
@@ -0,0 +1,21 @@
+package org.plantuml.ast;
+
+import java.io.IOException;
+
+/**
+ * AST Visitor
+ *
+ * We make AST visitors capable of throwing IOExecptions as it's convenient for pretty printers
+ * However, we could likely get rid of this throw some type of interface conversion.
+ */
+public interface PlantumlExprVisitor {
+ B visit(PlantumlProgram plantumlProgram, A a) throws IOException;
+
+ B visit(PlantumlObject plantumlObject, A a) throws IOException;
+
+ B visit(PlantumlPropertyGroup plantumlPropertyGroup, A a) throws IOException;
+
+ B visit(PlantumlProperty plantumlProperty, A a) throws IOException;
+
+ B visit(PlantumlConnection plantumlConnection, A a) throws IOException;
+}
diff --git a/src/main/java/org/plantuml/ast/PlantumlId.java b/src/main/java/org/plantuml/ast/PlantumlId.java
new file mode 100644
index 00000000..f1be4665
--- /dev/null
+++ b/src/main/java/org/plantuml/ast/PlantumlId.java
@@ -0,0 +1,10 @@
+package org.plantuml.ast;
+
+public interface PlantumlId {
+ /**
+ * PlantUML
+ *
+ * @return the name of the identifier
+ */
+ String getName();
+}
diff --git a/src/main/java/org/plantuml/ast/PlantumlObject.java b/src/main/java/org/plantuml/ast/PlantumlObject.java
new file mode 100644
index 00000000..24bebce8
--- /dev/null
+++ b/src/main/java/org/plantuml/ast/PlantumlObject.java
@@ -0,0 +1,27 @@
+package org.plantuml.ast;
+
+import java.io.IOException;
+
+public class PlantumlObject implements PlantumlExpr, PlantumlId {
+ private final String name;
+ private final PlantumlPropertyGroup[] propertyGroups;
+
+ public PlantumlObject(String name, PlantumlPropertyGroup[] propertyGroups) {
+ this.name = name;
+ this.propertyGroups = propertyGroups;
+ }
+
+ public PlantumlPropertyGroup[] getPropertyGroups() {
+ return propertyGroups;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public B accept(PlantumlExprVisitor visitor, A a) throws IOException {
+ return visitor.visit(this, a);
+ }
+}
diff --git a/src/main/java/org/plantuml/ast/PlantumlProgram.java b/src/main/java/org/plantuml/ast/PlantumlProgram.java
new file mode 100644
index 00000000..0cfad97d
--- /dev/null
+++ b/src/main/java/org/plantuml/ast/PlantumlProgram.java
@@ -0,0 +1,41 @@
+package org.plantuml.ast;
+
+import org.sysml.ast.SysmlExpr;
+import org.sysml.ast.SysmlExprVisitor;
+
+import java.io.IOException;
+
+/**
+ * Main PlantUML Program Element
+ *
+ * This is likely quite wrong. For our use case, we consider a PlantUML program as a collection
+ * of objects and connections.
+ */
+public class PlantumlProgram implements PlantumlExpr {
+ private PlantumlObject[] objects;
+ private PlantumlConnection[] connections;
+
+ public PlantumlProgram() {
+ this.objects = new PlantumlObject[0];
+ this.connections = new PlantumlConnection[0];
+ }
+
+ public PlantumlProgram(PlantumlObject[] objects, PlantumlConnection[] connections) {
+ this.objects = objects;
+ this.connections = connections;
+
+ }
+
+ public PlantumlObject[] getObjects(){
+ return objects;
+ }
+
+ public PlantumlConnection[] getConnections(){
+ return connections;
+ }
+
+ @Override
+ public B accept(PlantumlExprVisitor visitor, A a) throws IOException {
+ return visitor.visit(this, a);
+ }
+}
diff --git a/src/main/java/org/plantuml/ast/PlantumlProperty.java b/src/main/java/org/plantuml/ast/PlantumlProperty.java
new file mode 100644
index 00000000..ba85b23d
--- /dev/null
+++ b/src/main/java/org/plantuml/ast/PlantumlProperty.java
@@ -0,0 +1,20 @@
+package org.plantuml.ast;
+
+import java.io.IOException;
+
+public class PlantumlProperty implements PlantumlExpr {
+ private final String prop;
+
+ public PlantumlProperty(String prop) {
+ this.prop = prop;
+ }
+
+ public String getProp(){
+ return prop;
+ }
+
+ @Override
+ public B accept(PlantumlExprVisitor visitor, A a) throws IOException {
+ return visitor.visit(this, a);
+ }
+}
diff --git a/src/main/java/org/plantuml/ast/PlantumlPropertyGroup.java b/src/main/java/org/plantuml/ast/PlantumlPropertyGroup.java
new file mode 100644
index 00000000..87b8fe47
--- /dev/null
+++ b/src/main/java/org/plantuml/ast/PlantumlPropertyGroup.java
@@ -0,0 +1,28 @@
+package org.plantuml.ast;
+
+import java.io.IOException;
+
+public class PlantumlPropertyGroup implements PlantumlId, PlantumlExpr {
+
+ private final String name;
+ private PlantumlProperty[] properties;
+
+ public PlantumlPropertyGroup(String name, PlantumlProperty[] properties) {
+ this.properties = properties;
+ this.name = name;
+ }
+
+ public PlantumlProperty[] getProperties() {
+ return this.properties;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public B accept(PlantumlExprVisitor visitor, A a) throws IOException {
+ return visitor.visit(this, a);
+ }
+}
diff --git a/src/main/java/org/plantuml/compiler/AstPlantumlCompiler.java b/src/main/java/org/plantuml/compiler/AstPlantumlCompiler.java
new file mode 100644
index 00000000..359dde4a
--- /dev/null
+++ b/src/main/java/org/plantuml/compiler/AstPlantumlCompiler.java
@@ -0,0 +1,264 @@
+package org.plantuml.compiler;
+
+import org.clafer.ast.*;
+import org.plantuml.ast.*;
+import org.sysml.compiler.SysmlCompilerUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.WeakHashMap;
+
+/**
+ * Clafer AST to PlantUML
+ *
+ * Note that this compilation doesn't require instances, so we don't need to run the solver
+ * to compile.
+ *
+ * TODO: this should be refactored to cut down the code re-use.
+ */
+public class AstPlantumlCompiler {
+ /**
+ * collect all concrete clafers
+ * @param concreteClafers concreteClafers held in a claferModel
+ * @return ArrayList of all nested clafers (abstract included)
+ */
+ private ArrayList getConcreteObjects(List concreteClafers) {
+ ArrayList objs = new ArrayList();
+
+ for (AstConcreteClafer ast: concreteClafers) {
+ if (ast.getRef() != null) {
+ continue;
+ }
+ ArrayList pgs = new ArrayList();
+
+ ArrayList constrs = new ArrayList();
+ for (AstConstraint constr: ast.getConstraints()) {
+ constrs.add(new PlantumlProperty(constr.toString()));
+ }
+
+ if (constrs.size() > 0){
+ pgs.add(new PlantumlPropertyGroup("Constraints", constrs.toArray(new PlantumlProperty[0])));
+ }
+
+ // create an object and add it
+ PlantumlObject obj = new PlantumlObject(
+ SysmlCompilerUtils.getPropertyId(ast.getName()),
+ pgs.toArray(new PlantumlPropertyGroup[0])
+ );
+
+ if (!obj.getName().startsWith("#")) {
+ objs.add(obj);
+ }
+
+ // add all of its children
+ // TODO: check for collisions?
+ //objs.addAll(getAbstractObjects(ast.getAbstractChildren()));
+ objs.addAll(getConcreteObjects(ast.getChildren()));
+ }
+ return objs;
+ }
+
+ /**
+ * collect all abstract clafers (give them an abstract attribute)
+ * @param abstractClafers abstractClafers held in a claferModel
+ * @return ArrayList of all nested clafers (concrete included)
+ */
+ private ArrayList getAbstractObjects(List abstractClafers) {
+ ArrayList objs = new ArrayList();
+
+ for (AstAbstractClafer ast: abstractClafers) {
+ if (ast.getRef() != null) {
+ continue;
+ }
+ ArrayList pgs = new ArrayList();
+
+ ArrayList constrs = new ArrayList();
+ for (AstConstraint constr: ast.getConstraints()) {
+ constrs.add(new PlantumlProperty(constr.toString()));
+ }
+
+ ArrayList refs = new ArrayList();
+ for (AstConcreteClafer clafer: ast.getChildren()){
+ AstRef ref = clafer.getRef();
+ if (ref != null) {
+ refs.add(new PlantumlProperty(ref.toString()));
+ }
+ }
+
+ if (refs.size() > 0){
+ pgs.add(new PlantumlPropertyGroup("Attributes", refs.toArray(new PlantumlProperty[0])));
+ }
+
+ if (constrs.size() > 0){
+ pgs.add(new PlantumlPropertyGroup("Constraints", constrs.toArray(new PlantumlProperty[0])));
+ }
+
+ // create an object and add it
+ PlantumlObject obj = new PlantumlObject(
+ SysmlCompilerUtils.getPropertyId(ast.getName()),
+ pgs.toArray(new PlantumlPropertyGroup[0])
+ );
+
+ if (!obj.getName().startsWith("#")){
+ objs.add(obj);
+ }
+
+ // add all of its children
+ // TODO: check for collisions?
+ objs.addAll(getAbstractObjects(ast.getAbstractChildren()));
+ objs.addAll(getConcreteObjects(ast.getChildren()));
+ }
+ return objs;
+ }
+
+ /**
+ * top-level object collector
+ * @param model the root clafer model
+ * @return ArrayList of all clafers (abstract and concrete) suitable for PlantUML objects
+ */
+ private ArrayList getObjects(AstModel model) {
+ ArrayList objs = getAbstractObjects(model.getAbstracts());
+ objs.addAll(getConcreteObjects(model.getChildren()));
+ return objs;
+ }
+
+ private ArrayList getConcreteConnections(List concreteClafers) {
+ ArrayList connections = new ArrayList();
+
+ for (AstConcreteClafer ast: concreteClafers) {
+ if (ast.getRef() != null) {
+ continue;
+ }
+ String fromObj = SysmlCompilerUtils.getPropertyId(ast.getParent().getName());
+ String toObj = SysmlCompilerUtils.getPropertyId(ast.getName());
+ Card card = ast.getCard();
+ String label = "";
+ char toConn = '*';
+ char fromConn = '-';
+ if (ast.getParent().hasGroupCard()){
+ if (ast.getParent().getGroupCard().toString().equals("1")){
+ fromConn = '+';
+ } else if (ast.getParent().getGroupCard().toString().equals("1..*")) {
+ fromConn = '*';
+ }
+ }
+ if (card.toString().equals("0..1")){
+ toConn = 'o';
+ } else if (card.toString().equals("1")) {
+ toConn = '*';
+ } else {
+ if (card.toString().startsWith("0")) {
+ toConn = 'o';
+ }
+ label = card.toString();
+ }
+ if (!(fromObj.startsWith("#") || toObj.startsWith("#"))) {
+ connections.add(
+ new PlantumlConnection(
+ fromObj,
+ toObj,
+ fromConn,
+ toConn,
+ label
+ )
+ );
+ }
+
+ AstClafer superClafer = ast.getSuperClafer();
+ if (superClafer != null) {
+ String scName = SysmlCompilerUtils.getPropertyId(superClafer.getName());
+ if (!scName.startsWith("#")) {
+ fromObj = toObj;
+ toObj = scName;
+ connections.add(
+ new PlantumlConnection(
+ fromObj,
+ toObj,
+ '.',
+ '>',
+ "",
+ '.'
+ )
+ );
+ }
+ }
+
+ connections.addAll(getConcreteConnections(ast.getChildren()));
+ }
+
+ return connections;
+ }
+
+ private ArrayList getAbstractConnections(List abstractClafers) {
+ ArrayList connections = new ArrayList();
+
+ for (AstAbstractClafer ast: abstractClafers) {
+ if (ast.getRef() != null) {
+ continue;
+ }
+ String fromObj = SysmlCompilerUtils.getPropertyId(ast.getParent().getName());
+ String toObj = SysmlCompilerUtils.getPropertyId(ast.getName());
+ String label = "";
+ char toConn = '*';
+ char fromConn = '-';
+ if (ast.getParent().hasGroupCard()){
+ if (ast.getParent().getGroupCard().toString().equals("1")){
+ fromConn = '+';
+ } else if (ast.getParent().getGroupCard().toString().equals("1..*")) {
+ fromConn = '*';
+ }
+ }
+ if (!(fromObj.startsWith("#") || toObj.startsWith("#"))) {
+ connections.add(
+ new PlantumlConnection(
+ fromObj,
+ toObj,
+ fromConn,
+ toConn,
+ label
+ )
+ );
+ }
+
+ AstClafer superClafer = ast.getSuperClafer();
+ if (superClafer != null) {
+ String scName = SysmlCompilerUtils.getPropertyId(superClafer.getName());
+ if (!scName.startsWith("#")) {
+ fromObj = toObj;
+ toObj = scName;
+ connections.add(
+ new PlantumlConnection(
+ fromObj,
+ toObj,
+ '.',
+ '>',
+ "",
+ '.'
+ )
+ );
+ }
+ }
+
+ connections.addAll(getAbstractConnections(ast.getAbstractChildren()));
+ connections.addAll(getConcreteConnections(ast.getChildren()));
+ }
+
+ return connections;
+ }
+
+ private ArrayList getConnections(AstModel model) {
+ ArrayList connections = getAbstractConnections(model.getAbstracts());
+ connections.addAll(getConcreteConnections(model.getChildren()));
+ return connections;
+ }
+
+ public PlantumlProgram compile(AstModel model) {
+ ArrayList objs = getObjects(model);
+ ArrayList conns = getConnections(model);
+
+ return new PlantumlProgram(
+ objs.toArray(new PlantumlObject[0]), conns.toArray(new PlantumlConnection[0])
+ );
+ }
+}
diff --git a/src/main/java/org/plantuml/pprinter/PlantumlPrinter.java b/src/main/java/org/plantuml/pprinter/PlantumlPrinter.java
new file mode 100644
index 00000000..be64cfc5
--- /dev/null
+++ b/src/main/java/org/plantuml/pprinter/PlantumlPrinter.java
@@ -0,0 +1,81 @@
+package org.plantuml.pprinter;
+
+import org.plantuml.ast.*;
+
+import java.io.IOException;
+
+/**
+ * PlantUML -> Text
+ *
+ * Visits the PlantUML AST and generates text output to an Appendable stream
+ */
+public class PlantumlPrinter implements PlantumlExprVisitor {
+ private final String indentBase;
+ private final Appendable out;
+
+ public PlantumlPrinter(Appendable out) {
+ this.out = out;
+ this.indentBase = " ";
+ }
+
+ // implement the visitor
+ @Override
+ public Void visit(PlantumlProgram ast, String indent) throws IOException {
+ this.out.append(indent).append("@startuml").append("\n");
+ for (PlantumlObject obj: ast.getObjects()){
+ obj.accept(this, indent + indentBase);
+ }
+ this.out.append('\n');
+ for (PlantumlConnection conn: ast.getConnections()){
+ conn.accept(this, indent + indentBase);
+ }
+ this.out.append(indent).append("@enduml").append("\n");
+ return null;
+ }
+
+ @Override
+ public Void visit(PlantumlObject plantumlObject, String s) throws IOException {
+ this.out.append(s).append("object ").append(plantumlObject.getName());
+ if (plantumlObject.getPropertyGroups().length > 0) {
+ this.out.append(" {\n");
+ for (PlantumlPropertyGroup grp: plantumlObject.getPropertyGroups()){
+ grp.accept(this, s + indentBase);
+ }
+ this.out.append(s).append("}\n");
+ } else {
+ this.out.append("\n");
+ }
+ return null;
+ }
+
+ @Override
+ public Void visit(PlantumlPropertyGroup plantumlPropertyGroup, String s) throws IOException {
+ this.out.append(s).append(".. ").append(plantumlPropertyGroup.getName()).append(" ..").append("\n");
+ for (PlantumlProperty prop: plantumlPropertyGroup.getProperties()){
+ prop.accept(this, s);
+ }
+ return null;
+ }
+
+ @Override
+ public Void visit(PlantumlProperty plantumlProperty, String s) throws IOException {
+ this.out.append(s).append("* ").append(plantumlProperty.getProp()).append('\n');
+ return null;
+ }
+
+ @Override
+ public Void visit(PlantumlConnection plantumlConnection, String s) throws IOException {
+ this.out.append(s)
+ .append(plantumlConnection.getFromObj())
+ .append(" ").append(plantumlConnection.getFromConn()).append(plantumlConnection.getLineChar()).append(plantumlConnection.getToConn())
+ .append(" ")
+ .append(plantumlConnection.getToObj());
+ if (plantumlConnection.getLabel().length() > 0){
+ this.out.append(" ")
+ .append(": ")
+ .append(plantumlConnection.getLabel());
+ }
+ this.out.append('\n');
+ return null;
+ }
+}