From 1696367e8ca7c094b7c1216ecfa9e29fd2b78a45 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mikl=C3=B3s=20T=C3=B3th?= <tothmiklostibor@gmail.com>
Date: Sat, 27 Feb 2021 23:44:27 +0100
Subject: [PATCH] class diagrams are fun

---
 .gitlab-ci.yml    |   9 ---
 .gitmodules       |   3 -
 Dockerfile        |   3 +-
 Makefile          |   9 ++-
 plab/classdiag.go | 197 ++++++++++++++++++++++++++++++++++++++++++++++
 plab/javadoc.go   |  76 +++++++++++++-----
 plab/main.go      |   2 +-
 7 files changed, 262 insertions(+), 37 deletions(-)
 delete mode 100644 .gitmodules
 create mode 100644 plab/classdiag.go

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1e0605a..0df48d5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -18,15 +18,6 @@ go:
     paths:
       - plab/out/plab
 
-#java:
-#  image: openjdk:11
-#  stage: build
-#  script:
-#    - cd robinbird
-#    - ./gradlew build
-#  artifacts:
-#    paths:
-#      - robinbird/build/distributions/robinbird.tar
 
 Build:
   stage: docker
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index 434f625..0000000
--- a/.gitmodules
+++ /dev/null
@@ -1,3 +0,0 @@
-[submodule "robinbird"]
-	path = robinbird
-	url = https://github.com/SeokhyunKim/robinbird.git
diff --git a/Dockerfile b/Dockerfile
index 9390111..e517f80 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,8 @@
 FROM texlive/texlive
-RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get -y upgrade && apt-get -y install openjdk-11-jdk-headless hunspell hunspell-hu hunspell-en-gb hunspell-en-us python3-pip && pip3 install javalang
+RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get -y upgrade && apt-get -y install inkscape plantuml openjdk-11-jdk-headless hunspell hunspell-hu hunspell-en-gb hunspell-en-us python3-pip && pip3 install javalang
 COPY tikz-uml.sty /usr/local/texlive/2020/texmf-dist/tex/latex/tikz-uml/tikz-uml.sty
 COPY jsonDoclet.jar /root/jsonDoclet.jar
 COPY ./plab/out/plab /usr/bin/plab
-#ADD robinbird/build/distributions/robinbird.tar /
 RUN mktexlsr
 COPY gen_seq_diag.py /
 RUN chmod +x /gen_seq_diag.py
\ No newline at end of file
diff --git a/Makefile b/Makefile
index f8f2279..ed4db04 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,7 @@
-all: docker
+all: podman
 
-docker:
-	docker build -t projlab/projlab .
+go_plab:
+	bash -c "cd plab; make podman"
+
+podman: go_plab
+	podman build -t projlab/projlab .
diff --git a/plab/classdiag.go b/plab/classdiag.go
new file mode 100644
index 0000000..19720c7
--- /dev/null
+++ b/plab/classdiag.go
@@ -0,0 +1,197 @@
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"regexp"
+	"strings"
+)
+
+var Classdiag = Subcommand{
+	Name: "classdiag",
+	Command: func(args []string) {
+		if len(args) > 1 {
+			fmt.Println("usage: classdiag [source location]")
+			return
+		}
+
+		if len(args) != 0 {
+			e := genJavadoc(args[0])
+			if e != nil {
+				panic(e)
+			}
+		}
+
+		classes, e := readJavadoc()
+		if e != nil {
+			panic(e)
+		}
+
+		classes = linkInheritance(classes)
+		classes = resolveDocsFromInheritance(classes)
+
+		makePlantUML(classes)
+
+		fakePipeline()
+	},
+	Help: "create and parse javadoc from code",
+}
+
+func makePlantUML(classes map[string]*Class) {
+	genPlantUMLStr(classes)
+}
+
+var arrowRegex = regexp.MustCompile(`(<|<\|)?[^0-9](--|\.\.)(>|\|>)?`)
+
+const classFile = "/tmp/projlabclasses.txt"
+
+func genPlantUMLStr(classes map[string]*Class) {
+	var output bytes.Buffer
+	output.WriteString(fmt.Sprintln("@startuml"))
+	output.WriteString(fmt.Sprintln("skinparam classAttributeIconSize 0"))
+	output.WriteString(fmt.Sprintln("set namespaceSeparator none"))
+
+	for _, c := range classes {
+		after := ""
+		if c.Superclass.realClass != nil {
+			output.WriteString(fmt.Sprintf("%s --|> %s\n", c.Name, c.Superclass.realClass.Name))
+		}
+
+		for _, i := range c.Interfaces {
+			output.WriteString(fmt.Sprintf("%s ..|> %s\n", c.Name, i.Name))
+		}
+
+		t := "class"
+		if strings.Contains(c.Modifiers, "interface") {
+			t = "interface"
+		}
+		if strings.Contains(c.Modifiers, "abstract") {
+			t = "abstract " + t
+		}
+		output.WriteString(fmt.Sprintf("%s %s {\n", t, c.Name))
+		for _, f := range c.Fields {
+			uml := false
+			for _, a := range f.Annotations {
+				if a.TypeName == "Docs" {
+					for _, e := range a.Elements {
+						switch e.QualifiedName {
+						case "projlab.Docs.uml":
+							e.Value = strings.ReplaceAll(e.Value, "\\\"", "🍆")
+							e.Value = strings.ReplaceAll(e.Value, "\"", "")
+							e.Value = strings.ReplaceAll(e.Value, "🍆", "\"")
+							uml = true
+							if e.Value != "" {
+								after += fmt.Sprintf("\n%s %s %s", f.Type.Name, e.Value, c.Name)
+							}
+						}
+					}
+				}
+			}
+			if !uml {
+				output.WriteString(fmt.Sprintf("%s%s: %s\n", f.GetVisibility(), f.Name, f.Type.Name))
+			}
+		}
+		for _, m := range c.Methods {
+			params := ""
+			for _, p := range m.Parameters {
+				params += fmt.Sprintf("%s: %s, ", p.Name, p.Type.Name)
+			}
+			params = strings.TrimSuffix(params, ", ")
+			mod := ""
+			if strings.Contains(m.Modifiers, "abstract") {
+				mod += " {abstract} "
+			}
+			if strings.Contains(m.Modifiers, "static") {
+				mod += " {static} "
+			}
+			output.WriteString(fmt.Sprintf("%s%s%s(%s): %s\n", m.GetVisibility(), mod, m.Name, params, m.ReturnType.Name))
+		}
+		output.WriteString(fmt.Sprintln("}"))
+		output.WriteString(fmt.Sprintln(after))
+
+		for _, a := range c.Annotations {
+			if a.TypeName == "Docs" {
+				for _, e := range a.Elements {
+					switch e.QualifiedName {
+					case "projlab.Docs.uml":
+						e.Value = strings.ReplaceAll(e.Value, "\\\"", "🍆")
+						e.Value = strings.ReplaceAll(e.Value, "\"", "")
+						e.Value = strings.ReplaceAll(e.Value, "🍆", "\"")
+						output.WriteString(fmt.Sprintln(e.Value))
+					}
+				}
+			}
+		}
+
+		output.WriteString(fmt.Sprintln())
+	}
+
+	almostDone := output.String()
+	lines := strings.Split(almostDone, "\n")
+	skip := make(map[string][]string)
+	for _, l := range lines {
+		if arrowRegex.MatchString(l) {
+			words := strings.Split(l, " ")
+			one := words[0]
+			other := words[len(words)-1]
+			skip[one] = append(skip[one], other)
+			skip[other] = append(skip[other], one)
+		}
+	}
+
+	for _, c := range classes {
+		out, e := json.Marshal(*c)
+		if e != nil {
+			panic(e)
+		}
+		str := string(out)
+		for _, c2 := range classes {
+			if c2.Name == c.Name {
+				continue
+			}
+			if strings.Contains(str, c2.Name) {
+				skips := skip[c2.Name]
+				shouldSkip := false
+				for _, s := range skips {
+					if c.Name == s {
+						shouldSkip = true
+						break
+					}
+				}
+				if !shouldSkip {
+					output.WriteString(fmt.Sprintf("%s ..> %s\n", c.Name, c2.Name))
+				}
+			}
+		}
+	}
+
+	output.WriteString(fmt.Sprintln("@enduml"))
+	e := ioutil.WriteFile(classFile, output.Bytes(), os.ModePerm)
+	if e != nil {
+		panic(e)
+	}
+
+}
+
+func fakePipeline() {
+	cmd := exec.Command("plantuml", "-tsvg", classFile)
+	b, e := cmd.CombinedOutput()
+	if e != nil {
+		fmt.Println(string(b))
+		panic(e)
+	}
+
+	outfile := strings.ReplaceAll(classFile, ".txt", ".eps")
+	cmd = exec.Command("inkscape", "-o", outfile, strings.ReplaceAll(classFile, ".txt", ".svg"))
+	b, e = cmd.CombinedOutput()
+	if e != nil {
+		fmt.Println(string(b))
+		panic(e)
+	}
+
+	fmt.Println(outfile)
+}
diff --git a/plab/javadoc.go b/plab/javadoc.go
index f495e31..1c02f09 100644
--- a/plab/javadoc.go
+++ b/plab/javadoc.go
@@ -6,6 +6,7 @@ import (
 	"io/ioutil"
 	"os"
 	"os/exec"
+	"os/user"
 	"path/filepath"
 	"sort"
 	"strings"
@@ -20,6 +21,22 @@ type InternalType struct {
 	QualifiedName string
 	DocString     string
 	Annotations   []Annot
+	Modifiers     string
+}
+
+func (i *InternalType) GetVisibility() string {
+	visibilities := map[string]string{
+		"public":    "+",
+		"private":   "-",
+		"protected": "#",
+		"package":   "~",
+	}
+	for k, v := range visibilities {
+		if strings.Contains(i.Modifiers, k) {
+			return v
+		}
+	}
+	return visibilities["package"]
 }
 
 func (i *InternalType) GetAnnot() []Annot {
@@ -44,13 +61,11 @@ type Method struct {
 	Parameters []*Params
 	ReturnType InternalType
 	Exceptions []struct{} // TODO
-	Modifiers  string
 }
 
 type Field struct {
 	InternalType
-	Modifiers string
-	Type      InternalType
+	Type InternalType
 }
 
 type Class struct {
@@ -64,9 +79,9 @@ type Class struct {
 		InternalType
 		realClass *Class
 	}
-	Methods   []*Method
-	Modifiers string
-	Fields    []*Field
+	Methods []*Method
+
+	Fields []*Field
 }
 
 func (c *Class) getMethod(name string) *Method {
@@ -118,9 +133,14 @@ func genJavadoc(src string) error {
 		return e
 	}
 
+	doclet := "/root/jsonDoclet.jar"
+	if usr, err := user.Current(); err == nil {
+		doclet = usr.HomeDir + "/jsonDoclet.jar"
+	}
+
 	cmd := exec.Command("javadoc",
 		"-doclet", "com.raidandfade.JsonDoclet.Main",
-		"-docletpath", "/root/jsonDoclet.jar",
+		"-docletpath", doclet,
 		"-sourcepath", p,
 		"-private",
 		"-subpackages", "projlab")
@@ -149,6 +169,24 @@ func readJson(fname string) (*Class, error) {
 		return nil, e
 	}
 
+	if c.Name == "Docs" {
+		return nil, nil
+	}
+
+	for _, a := range c.Annotations {
+		if a.TypeName == "Docs" {
+			for _, e := range a.Elements {
+				switch e.QualifiedName {
+				case "projlab.Docs.skip":
+					e.Value = strings.ReplaceAll(e.Value, "\"", "")
+					if e.Value == "true" {
+						return nil, nil
+					}
+				}
+			}
+		}
+	}
+
 	replaceFieldType := func(field interface{}, newType string) {
 		field.(*Field).Type.Name = newType
 	}
@@ -167,7 +205,9 @@ func readJson(fname string) (*Class, error) {
 						switch e.QualifiedName {
 						case "projlab.Docs.type":
 							e.Value = strings.ReplaceAll(e.Value, "\"", "")
-							replace(arr[i], e.Value)
+							if e.Value != "" {
+								replace(arr[i], e.Value)
+							}
 						}
 					}
 				}
@@ -175,15 +215,15 @@ func readJson(fname string) (*Class, error) {
 		}
 	}
 
-	tmp := make([]annotated, 0, len(c.Fields))
-	for i := range tmp {
-		tmp[i] = &c.Fields[i].InternalType
+	tmp := make([]annotated, len(c.Fields))
+	for i := range c.Fields {
+		tmp[i] = c.Fields[i]
 	}
 	do(tmp, replaceFieldType)
 
-	tmp = make([]annotated, 0, len(c.Methods))
-	for i := range tmp {
-		tmp[i] = &c.Methods[i].InternalType
+	tmp = make([]annotated, len(c.Methods))
+	for i := range c.Methods {
+		tmp[i] = c.Methods[i]
 	}
 	do(tmp, replaceMethodType)
 
@@ -213,7 +253,9 @@ func readJavadoc() (map[string]*Class, error) {
 		if e != nil {
 			return nil, e
 		}
-		m[c.QualifiedName] = c
+		if c != nil {
+			m[c.QualifiedName] = c
+		}
 	}
 
 	return m, nil
@@ -321,10 +363,6 @@ func printLatex(c map[string]*Class) {
 }
 
 func printClassDoc(c *Class) {
-	if c.QualifiedName == "projlab.Docs" {
-		return
-	}
-
 	fmt.Printf("\\subsubsection{\\texttt{%s \\textcolor{blue}{%s}}}\n", c.Modifiers, c.Name)
 	fmt.Println("\\begin{itemize}")
 
diff --git a/plab/main.go b/plab/main.go
index f80af2c..23ef0b8 100644
--- a/plab/main.go
+++ b/plab/main.go
@@ -3,7 +3,7 @@ package main
 import "os"
 
 func main() {
-	mainCmds := CmdFrom(os.Args[0], "go projlab tool", Timetable, Javadoc)
+	mainCmds := CmdFrom(os.Args[0], "go projlab tool", Timetable, Javadoc, Classdiag)
 	if len(os.Args) < 2 {
 		os.Args = append(os.Args, "help")
 	}
-- 
GitLab