aboutsummaryrefslogtreecommitdiff
path: root/umlgen/src/main/java/moe
diff options
context:
space:
mode:
Diffstat (limited to 'umlgen/src/main/java/moe')
-rw-r--r--umlgen/src/main/java/moe/yuuta/umlgen/AssocAnnotationParams.kt6
-rw-r--r--umlgen/src/main/java/moe/yuuta/umlgen/AssociateRelation.kt33
-rw-r--r--umlgen/src/main/java/moe/yuuta/umlgen/ClassItem.kt21
-rw-r--r--umlgen/src/main/java/moe/yuuta/umlgen/ExtendRelation.kt7
-rw-r--r--umlgen/src/main/java/moe/yuuta/umlgen/Main.kt144
-rw-r--r--umlgen/src/main/java/moe/yuuta/umlgen/Parser.kt56
-rw-r--r--umlgen/src/main/java/moe/yuuta/umlgen/TargetRegistry.kt38
-rw-r--r--umlgen/src/main/java/moe/yuuta/umlgen/Visitor.kt37
8 files changed, 342 insertions, 0 deletions
diff --git a/umlgen/src/main/java/moe/yuuta/umlgen/AssocAnnotationParams.kt b/umlgen/src/main/java/moe/yuuta/umlgen/AssocAnnotationParams.kt
new file mode 100644
index 0000000..9bc2702
--- /dev/null
+++ b/umlgen/src/main/java/moe/yuuta/umlgen/AssocAnnotationParams.kt
@@ -0,0 +1,6 @@
+package moe.yuuta.umlgen
+
+/**
+ * Represent the parsed params for @Assoc annotation.
+ */
+data class AssocAnnotationParams(val lower: Int, val upper: Int, val partOf: Boolean)
diff --git a/umlgen/src/main/java/moe/yuuta/umlgen/AssociateRelation.kt b/umlgen/src/main/java/moe/yuuta/umlgen/AssociateRelation.kt
new file mode 100644
index 0000000..31b5fd4
--- /dev/null
+++ b/umlgen/src/main/java/moe/yuuta/umlgen/AssociateRelation.kt
@@ -0,0 +1,33 @@
+package moe.yuuta.umlgen
+
+/**
+ * Indicate an associate relationship, from one class to another, with lower / upper bonds and whether it is a partOf
+ * relation.
+ */
+@JvmRecord
+data class AssociateRelation(val from: String,
+ val to: String,
+ val lower: Int,
+ val upper: Int,
+ val partOf: Boolean) {
+ /**
+ * Destruct the params.
+ */
+ constructor(from: String, to: String, params: AssocAnnotationParams):
+ this(from, to, params.lower, params.upper, params.partOf)
+
+ /**
+ * Return the label to be put at arrow head.
+ * For lower == upper == 1, just returns "" because it's the default association.
+ * For upper == infinity (-1), return lower..*
+ * Otherwise, return lower..upper.
+ */
+ fun head(): String =
+ if (lower == upper && upper == 1) {
+ ""
+ } else if (upper == -1) {
+ "${lower}..*"
+ } else {
+ "${lower}..${upper}"
+ }
+}
diff --git a/umlgen/src/main/java/moe/yuuta/umlgen/ClassItem.kt b/umlgen/src/main/java/moe/yuuta/umlgen/ClassItem.kt
new file mode 100644
index 0000000..245f0e0
--- /dev/null
+++ b/umlgen/src/main/java/moe/yuuta/umlgen/ClassItem.kt
@@ -0,0 +1,21 @@
+package moe.yuuta.umlgen
+
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+import com.github.javaparser.ast.body.EnumDeclaration
+
+/**
+ * Represent a class with abstract / interface tag, or no tag.
+ */
+@JvmRecord
+data class ClassItem(val name: String, val tag: String?) {
+ /**
+ * Automatically determine if the class is abstract, interface, or else, and set the tag.
+ */
+ constructor(cls: ClassOrInterfaceDeclaration): this(cls.nameAsString,
+ if (cls.isAbstract) "abstract" else if (cls.isInterface) "interface" else null)
+
+ /**
+ * For enum, use null as tag.
+ */
+ constructor(cls: EnumDeclaration): this(cls.nameAsString, null)
+}
diff --git a/umlgen/src/main/java/moe/yuuta/umlgen/ExtendRelation.kt b/umlgen/src/main/java/moe/yuuta/umlgen/ExtendRelation.kt
new file mode 100644
index 0000000..86dec54
--- /dev/null
+++ b/umlgen/src/main/java/moe/yuuta/umlgen/ExtendRelation.kt
@@ -0,0 +1,7 @@
+package moe.yuuta.umlgen
+
+/**
+ * Represents an extend / implemention relationship from one class to another.
+ */
+@JvmRecord
+data class ExtendRelation(val from: String, val to: String, val impl: Boolean)
diff --git a/umlgen/src/main/java/moe/yuuta/umlgen/Main.kt b/umlgen/src/main/java/moe/yuuta/umlgen/Main.kt
new file mode 100644
index 0000000..f7482a7
--- /dev/null
+++ b/umlgen/src/main/java/moe/yuuta/umlgen/Main.kt
@@ -0,0 +1,144 @@
+package moe.yuuta.umlgen
+
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+import com.github.javaparser.ast.body.EnumDeclaration
+import com.github.javaparser.ast.body.FieldDeclaration
+import com.github.javaparser.ast.type.Type
+import freemarker.template.Configuration
+import freemarker.template.TemplateException
+import java.io.IOException
+import java.io.OutputStreamWriter
+import java.nio.file.Files
+import java.nio.file.Paths
+
+object Main {
+ @Throws(IOException::class, TemplateException::class)
+ @JvmStatic
+ fun main(args: Array<String>) {
+ if (args.size < 2) {
+ System.err.println("Usage: umlgen /path/to/sources/ <all|type|assoc>")
+ System.exit(64)
+ return
+ }
+ if (args[1] != "all" && args[1] != "type" && args[1] != "assoc") {
+ System.err.println("Invalid selection: ${args[1]}")
+ System.exit(64)
+ return
+ }
+ val parser = Parser()
+ val visitor = Visitor(parser)
+ val path = Paths.get(args[0])
+ Files.walkFileTree(path, visitor)
+ val cfg = Configuration()
+ cfg.setClassForTemplateLoading(Main::class.java, "/")
+ val input: MutableMap<String, Any> = HashMap()
+ input["classes"] = getClasses().map { ClassItem(it) } + getEnums().map { ClassItem(it) }
+ input["classes"]
+ if (args[1] == "all" || args[1] == "type") {
+ input["extends"] = getExtends()
+ }
+ if (args[1] == "all" || args[1] == "assoc") {
+ val singles = getAssociations()
+ .filter { (_, f) -> f.getCommonType().isClassOrInterfaceType && !isMap(f) && !isList(f) }
+ .filter { (_, f) -> isSelfType(f.commonType) }
+ .map { (cls, f) ->
+ val params = parseAssocAnnotation(f, AssocAnnotationParams(1, 1, false))
+ if (params.upper != 1 && params.upper != -1) {
+ System.err.printf("Expr '%s' contains invalid upperBond settings. Resetting to 1.\n", f)
+ }
+ AssociateRelation(cls.nameAsString, f.getCommonType().asClassOrInterfaceType().nameAsString, params)
+ }
+ val arr = getAssociations()
+ .filter { (_, f) -> f.getCommonType().isArrayType && f.getElementType().isClassOrInterfaceType }
+ .filter { (_, f) -> isSelfType(f.elementType) }
+ .map { (cls, f) ->
+ val params = parseAssocAnnotation(f, AssocAnnotationParams(0, -1, false))
+ AssociateRelation(cls.nameAsString, f.getElementType().asClassOrInterfaceType().nameAsString, params)
+ }
+ val lst = getAssociations()
+ .filter { (_, f) -> isList(f) }
+ .map { (cls, f) -> Triple(cls, f, f.commonType.asClassOrInterfaceType().typeArguments.get()[0]) }
+ .filter { (_, _, f) -> isSelfType(f) }
+ .map { (cls, f0, f) ->
+ val params = parseAssocAnnotation(f0, AssocAnnotationParams(0, -1, false))
+ AssociateRelation(cls.nameAsString, f.asClassOrInterfaceType().nameAsString, params)
+ }
+
+ input["assocs"] =
+ (singles + arr + lst).groupBy { Pair(it.from, it.to) }.values.map { it.maxBy { it.upper } }
+ }
+ val template = cfg.getTemplate("graph.ftl")
+ template.process(input, OutputStreamWriter(System.out))
+ }
+
+ private fun filterThrowable(c: ClassOrInterfaceDeclaration): Boolean {
+ // Naive detection ... should use recursion.
+ return if (c.implementedTypes.isEmpty() && c.extendedTypes.isEmpty()) {
+ true
+ } else !c.nameAsString.endsWith("Exception")
+ }
+
+ private fun getClasses(): List<ClassOrInterfaceDeclaration> =
+ TargetRegistry.getInstance().getNodes()
+ .filter { (_, node) -> node.isClassOrInterfaceDeclaration }
+ .filter { (pkg, _) -> pkg != null && !pkg.startsWith("ui.tui") }
+ .map { (_, node) -> node.asClassOrInterfaceDeclaration() }
+ .filter { filterThrowable(it) }
+
+ private fun getEnums(): List<EnumDeclaration> =
+ TargetRegistry.getInstance().getNodes()
+ .filter { (_, node) -> node.isEnumDeclaration }
+ .map { (_, node) -> node.asEnumDeclaration() }
+
+ private fun parseAssocAnnotation(field: FieldDeclaration,
+ def: AssocAnnotationParams): AssocAnnotationParams {
+ var params = def
+ if (field.getAnnotationByName("Assoc").isPresent) {
+ val expr = field.getAnnotationByName("Assoc").get()
+ val annotationArgs = expr.asNormalAnnotationExpr().pairs
+ .associateBy({ it.nameAsString }, { it.value })
+ if (annotationArgs.containsKey("lowerBond")) {
+ params = AssocAnnotationParams(annotationArgs["lowerBond"]!!.asIntegerLiteralExpr().asNumber().toInt(),
+ params.upper,
+ params.partOf)
+ }
+ if (annotationArgs.containsKey("upperBond")) {
+ params = AssocAnnotationParams(params.lower,
+ annotationArgs["upperBond"]!!.asIntegerLiteralExpr().asNumber().toInt(),
+ params.partOf)
+ }
+ if (annotationArgs.containsKey("partOf")) {
+ params = AssocAnnotationParams(params.lower, params.upper,
+ annotationArgs["partOf"]!!.isBooleanLiteralExpr)
+ }
+ }
+ return params
+ }
+
+ private fun getAssociations(): List<Pair<ClassOrInterfaceDeclaration, FieldDeclaration>> =
+ getClasses().flatMap { cls ->
+ cls.fields.map { Pair(cls, it) }
+ }.distinct()
+
+ private fun getExtends(): List<ExtendRelation> =
+ getClasses().flatMap {
+ it.extendedTypes.map { superType -> ExtendRelation(it.nameAsString, superType.nameAsString, false) } +
+ it.implementedTypes.map { superType -> ExtendRelation(it.nameAsString, superType.nameAsString, true) }
+ }.filter { n: ExtendRelation -> TargetRegistry.getInstance().hasClass(n.to) }.distinct()
+
+ private fun isMap(f: FieldDeclaration): Boolean =
+ f.getCommonType().isClassOrInterfaceType &&
+ f.getCommonType().asClassOrInterfaceType().nameAsString.endsWith("Map") &&
+ f.getCommonType().asClassOrInterfaceType().typeArguments.isPresent &&
+ f.getCommonType().asClassOrInterfaceType().typeArguments.get().size == 2
+
+ private fun isList(f: FieldDeclaration): Boolean =
+ f.getCommonType().isClassOrInterfaceType &&
+ (f.getCommonType().asClassOrInterfaceType().nameAsString.endsWith("List") ||
+ f.getCommonType().asClassOrInterfaceType().nameAsString.equals("Collection")) &&
+ f.getCommonType().asClassOrInterfaceType().typeArguments.isPresent &&
+ f.getCommonType().asClassOrInterfaceType().typeArguments.get().size == 1
+
+ private fun isSelfType(type: Type): Boolean =
+ type.isClassOrInterfaceType && TargetRegistry.getInstance().hasClass(type.asClassOrInterfaceType().nameAsString)
+}
diff --git a/umlgen/src/main/java/moe/yuuta/umlgen/Parser.kt b/umlgen/src/main/java/moe/yuuta/umlgen/Parser.kt
new file mode 100644
index 0000000..9c4fae5
--- /dev/null
+++ b/umlgen/src/main/java/moe/yuuta/umlgen/Parser.kt
@@ -0,0 +1,56 @@
+package moe.yuuta.umlgen
+
+import com.github.javaparser.JavaParser
+import com.github.javaparser.Problem
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.body.TypeDeclaration
+import moe.yuuta.umlgen.TargetRegistry.Companion.getInstance
+import java.nio.file.Path
+import java.util.function.BiFunction
+import java.util.function.Consumer
+
+/**
+ * Accepts path and buffer. Parse the Java source code. Append all parsed nodes into the global registry.
+ */
+class Parser : BiFunction<Path, String, Void?> {
+ private val parser: JavaParser
+
+ /**
+ * Init with a default [JavaParser].
+ */
+ init {
+ parser = JavaParser()
+ }
+
+ /**
+ * Parse the buf. Prompt for error if fails. Add all parsed nodes into the global registry.
+ * @param path Path of the buf.
+ * @param buf A UTF-8 buffer that contains the Java source.
+ * @return null
+ */
+ override fun apply(path: Path, buf: String): Void? {
+ val res = parser.parse(buf)
+ if (!res.isSuccessful || res.result.isEmpty) {
+ System.err.println("Cannot parse $path")
+ res.problems.forEach(Consumer { x: Problem? -> System.err.println(x) })
+ return null
+ }
+ val r = res.result.get()
+ val pkg = parsePackage(r)
+ r.types.forEach(Consumer { t: TypeDeclaration<*>? -> getInstance().register(pkg, t!!) })
+ return null
+ }
+
+ /**
+ * Read the package declaration from the CompilationUnit, optional.
+ * @param r non-null
+ * @return The package name or null.
+ */
+ private fun parsePackage(r: CompilationUnit): String? {
+ return if (r.packageDeclaration.isPresent) {
+ r.packageDeclaration.get().nameAsString
+ } else {
+ null
+ }
+ }
+}
diff --git a/umlgen/src/main/java/moe/yuuta/umlgen/TargetRegistry.kt b/umlgen/src/main/java/moe/yuuta/umlgen/TargetRegistry.kt
new file mode 100644
index 0000000..7c193bd
--- /dev/null
+++ b/umlgen/src/main/java/moe/yuuta/umlgen/TargetRegistry.kt
@@ -0,0 +1,38 @@
+package moe.yuuta.umlgen
+
+import com.github.javaparser.ast.body.TypeDeclaration
+import java.util.*
+
+/**
+ * A global registry for all parsed nodes.
+ */
+class TargetRegistry {
+ private val cls: MutableSet<String> = HashSet()
+ private val nodes: MutableList<Pair<String?, TypeDeclaration<*>>> = ArrayList()
+
+ fun register(pkg: String?, node: TypeDeclaration<*>) {
+ nodes.add(Pair(pkg, node))
+ if (node.isClassOrInterfaceDeclaration || node.isEnumDeclaration) {
+ cls.add(node.nameAsString)
+ }
+ }
+
+ fun getNodes(): List<Pair<String?, TypeDeclaration<*>>> {
+ return nodes
+ }
+
+ fun hasClass(cls: String): Boolean {
+ return this.cls.contains(cls)
+ }
+
+ companion object {
+ private var sInstance: TargetRegistry? = null
+ @JvmStatic
+ fun getInstance(): TargetRegistry {
+ if (sInstance == null) {
+ sInstance = TargetRegistry()
+ }
+ return sInstance!!
+ }
+ }
+}
diff --git a/umlgen/src/main/java/moe/yuuta/umlgen/Visitor.kt b/umlgen/src/main/java/moe/yuuta/umlgen/Visitor.kt
new file mode 100644
index 0000000..ea99b99
--- /dev/null
+++ b/umlgen/src/main/java/moe/yuuta/umlgen/Visitor.kt
@@ -0,0 +1,37 @@
+package moe.yuuta.umlgen
+
+import java.io.IOException
+import java.nio.charset.StandardCharsets
+import java.nio.file.*
+import java.nio.file.attribute.BasicFileAttributes
+import java.util.function.BiFunction
+
+class Visitor(private val consumer: BiFunction<Path, String, Void?>) : FileVisitor<Path> {
+ @Throws(IOException::class)
+ override fun preVisitDirectory(path: Path, basicFileAttributes: BasicFileAttributes): FileVisitResult {
+ return FileVisitResult.CONTINUE
+ }
+
+ @Throws(IOException::class)
+ override fun visitFileFailed(path: Path, e: IOException?): FileVisitResult {
+ System.err.printf("Open %s: %s\n", path.toFile(), e?.message)
+ return FileVisitResult.CONTINUE
+ }
+
+ @Throws(IOException::class)
+ override fun postVisitDirectory(path: Path, e: IOException?): FileVisitResult {
+ return FileVisitResult.CONTINUE
+ }
+
+ @Throws(IOException::class)
+ override fun visitFile(path: Path, basicFileAttributes: BasicFileAttributes?): FileVisitResult {
+ if (path.fileName.endsWith(".java")) {
+ return FileVisitResult.CONTINUE
+ }
+ val fd = Files.newInputStream(path, StandardOpenOption.READ)
+ val buf = String(fd.readAllBytes(), StandardCharsets.UTF_8)
+ fd.close()
+ consumer.apply(path, buf)
+ return FileVisitResult.CONTINUE
+ }
+}