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; + } +}