diff --git a/src/main/kotlin/hu/webhejj/perspektive/ClassDiagram.kt b/src/main/kotlin/hu/webhejj/perspektive/ClassDiagram.kt index f1a863a..e52b53c 100644 --- a/src/main/kotlin/hu/webhejj/perspektive/ClassDiagram.kt +++ b/src/main/kotlin/hu/webhejj/perspektive/ClassDiagram.kt @@ -90,12 +90,14 @@ class ClassDiagram( } private fun scanMembers(kClass: KClass<*>): List { - return kClass.declaredMemberProperties.filter { scanConfig.isAllowed(kClass, it) }.map { scanKProperty(it, isStatic = false) } + + val umlMembers = kClass.declaredMemberProperties.filter { scanConfig.isAllowed(kClass, it) }.map { scanKProperty(it, isStatic = false) } + kClass.staticProperties.filter { scanConfig.isAllowed(kClass, it) }.map { scanKProperty(it, isStatic = true) } + kClass.declaredMemberFunctions.filter { scanConfig.isAllowed(kClass, it) }.map { mapper.toMethodMember(it, isStatic = false) } + kClass.staticFunctions.filter { scanConfig.isAllowed(kClass, it) }.map { mapper.toMethodMember(it, isStatic = true) } + kClass.enumValues().map { mapper.toEnumMember(it) } // try { kClass.declaredMemberFunctions.umlMethods(kClass, isStatic = false) } catch (e: Throwable) { listOf() } + + + return collapseJavaBeanProperties(umlMembers) } private fun scanKProperty(kProperty: KProperty<*>, isStatic: Boolean): UmlMember { @@ -105,6 +107,30 @@ class ClassDiagram( return mapper.toPropertyMember(kProperty, isStatic = isStatic) } + private fun collapseJavaBeanProperties(members: List): List { + val mutableMembers = members.toMutableList() + members + .filter { it.kind == UmlMember.Kind.PROPERTY } + .forEach { umlMember -> + val isGetterName = "is${umlMember.name.capitalize()}" + val getterName = "get${umlMember.name.capitalize()}" + val setterName = "set${umlMember.name.capitalize()}" + members + .find { it.kind == UmlMember.Kind.METHOD && it.name in listOf(isGetterName, getterName) } + ?.also { + mutableMembers.remove(it) + umlMember.stereotypes.add("get") + } + members + .find { it.kind == UmlMember.Kind.METHOD && it.name == setterName } + ?.also { + mutableMembers.remove(it) + umlMember.stereotypes.add("set") + } + } + return mutableMembers.toList() + } + fun renderWithPlantUml(file: File, renderingOptions: PlantUmlOptions = PlantUmlOptions()) { PlantUmlWriter().also { it.write(file, this, renderingOptions) diff --git a/src/main/kotlin/hu/webhejj/perspektive/plantuml/PlantUmlWriter.kt b/src/main/kotlin/hu/webhejj/perspektive/plantuml/PlantUmlWriter.kt index 5da9964..5c60f54 100644 --- a/src/main/kotlin/hu/webhejj/perspektive/plantuml/PlantUmlWriter.kt +++ b/src/main/kotlin/hu/webhejj/perspektive/plantuml/PlantUmlWriter.kt @@ -99,7 +99,7 @@ class PlantUmlWriter { umlClass.typeParameters.joinToString(separator = ",", prefix = "<", postfix = ">") { it.name } } - val stereotypes = if (umlClass.stereotypes.isEmpty()) "" else umlClass.stereotypes.joinToString(prefix = "<< ", postfix = " >>") + val stereotypes = stereotypesString(umlClass.stereotypes) output.println("$abstract$kind ${umlClass.name.qualified}$generics $spot $stereotypes {") } @@ -109,6 +109,7 @@ class PlantUmlWriter { classDiagram: ClassDiagram, output: PrintWriter, ) { + val stereotypes = stereotypesString(umlClass.stereotypes) umlClass.members .filter { it.kind == UmlMember.Kind.PROPERTY } .filter { prop -> classDiagram.umlClasses.none { it.name == prop.type } } @@ -117,10 +118,11 @@ class PlantUmlWriter { val static = if (prop.isStatic) "{static} " else "" output.print(" $abstract$static${prop.visibility.plantumlPrefix}${prop.name}: ${prop.type.simple}${genericsString(prop.typeProjections)}") if (prop.cardinality == UmlCardinality.OPTIONAL) { - output.println("?") + output.print("?") } else { - output.println() + output.print("") } + output.println(stereotypesString(prop.stereotypes)) } } @@ -138,8 +140,9 @@ class PlantUmlWriter { .forEach { val abstract = if (it.isAbstract) "{abstract} " else "" val static = if (it.isStatic) "{static} " else "" + val returnType = if(it.type.simple == "Unit") "" else ": ${it.type.simple}" val generics = genericsString(it.typeProjections) - output.println(" $abstract$static${it.visibility.plantumlPrefix}${it.name}(${it.parameters.joinToString()}): ${it.type.simple}$generics") + output.println(" $abstract$static${it.visibility.plantumlPrefix}${it.name}(${it.parameters.joinToString()})${returnType}$generics") } } @@ -211,3 +214,6 @@ private val UmlVisibility?.plantumlPrefix: String UmlVisibility.INTERNAL -> "~" null -> "" } + +private fun stereotypesString(stereotypes: List) = + if (stereotypes.isEmpty()) "" else stereotypes.joinToString(prefix = "<< ", postfix = " >>") diff --git a/src/main/kotlin/hu/webhejj/perspektive/uml/KotlinReflectionToUmlMapper.kt b/src/main/kotlin/hu/webhejj/perspektive/uml/KotlinReflectionToUmlMapper.kt index 8753589..e97391e 100644 --- a/src/main/kotlin/hu/webhejj/perspektive/uml/KotlinReflectionToUmlMapper.kt +++ b/src/main/kotlin/hu/webhejj/perspektive/uml/KotlinReflectionToUmlMapper.kt @@ -68,6 +68,7 @@ class KotlinReflectionToUmlMapper { isAbstract = kProperty.isAbstract, isStatic = isStatic, cardinality = cardinality, + stereotypes = mutableListOf(), ) } @@ -92,10 +93,11 @@ class KotlinReflectionToUmlMapper { type = kFunction.returnType.umlName, typeProjections = kFunction.returnType.arguments.map { it.uml }, // dropping first method parameter (`this` reference) - parameters = kFunction.parameters.drop(1).map { it.name ?: "" }, + parameters = kFunction.parameters.drop(1).map { it.type.umlName.simple }, isAbstract = kFunction.isAbstract, isStatic = isStatic, cardinality = UmlCardinality.SCALAR, // TODO + stereotypes = mutableListOf(), ) } @@ -110,6 +112,7 @@ class KotlinReflectionToUmlMapper { isAbstract = false, isStatic = false, cardinality = UmlCardinality.SCALAR, + stereotypes = mutableListOf(), ) } } diff --git a/src/main/kotlin/hu/webhejj/perspektive/uml/UmlModel.kt b/src/main/kotlin/hu/webhejj/perspektive/uml/UmlModel.kt index b8f337c..2dba839 100644 --- a/src/main/kotlin/hu/webhejj/perspektive/uml/UmlModel.kt +++ b/src/main/kotlin/hu/webhejj/perspektive/uml/UmlModel.kt @@ -46,6 +46,7 @@ data class UmlMember( val isAbstract: Boolean, val isStatic: Boolean, val cardinality: UmlCardinality, + val stereotypes: MutableList, ) { enum class Kind { PROPERTY, diff --git a/src/test/java/hu/webhejj/perspektive/testmodel/JavaBeanModel.java b/src/test/java/hu/webhejj/perspektive/testmodel/JavaBeanModel.java new file mode 100644 index 0000000..6df2e82 --- /dev/null +++ b/src/test/java/hu/webhejj/perspektive/testmodel/JavaBeanModel.java @@ -0,0 +1,42 @@ +package hu.webhejj.perspektive.testmodel; + +import java.util.Objects; + +public class JavaBeanModel { + + private String field; + private Boolean bool; + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public Boolean isBool() { + return bool; + } + + public void setBool(Boolean bool) { + this.bool = bool; + } + + public String method(String arg) { + return arg; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + JavaBeanModel that = (JavaBeanModel) o; + return Objects.equals(field, that.field) && Objects.equals(bool, that.bool); + } + + @Override + public int hashCode() { + return Objects.hash(field, bool); + } +} diff --git a/src/test/java/hu/webhejj/perspektive/testmodel/TestJavaModel.java b/src/test/java/hu/webhejj/perspektive/testmodel/JavaStaticModel.java similarity index 86% rename from src/test/java/hu/webhejj/perspektive/testmodel/TestJavaModel.java rename to src/test/java/hu/webhejj/perspektive/testmodel/JavaStaticModel.java index 6a931a5..174e7bc 100644 --- a/src/test/java/hu/webhejj/perspektive/testmodel/TestJavaModel.java +++ b/src/test/java/hu/webhejj/perspektive/testmodel/JavaStaticModel.java @@ -1,6 +1,6 @@ package hu.webhejj.perspektive.testmodel; -public class TestJavaModel { +public class JavaStaticModel { public static String staticField = "staticField"; diff --git a/src/test/kotlin/hu/webhejj/perspektive/TestModelTest.kt b/src/test/kotlin/hu/webhejj/perspektive/TestModelTest.kt index 4f144c5..f8b688c 100644 --- a/src/test/kotlin/hu/webhejj/perspektive/TestModelTest.kt +++ b/src/test/kotlin/hu/webhejj/perspektive/TestModelTest.kt @@ -1,59 +1,74 @@ package hu.webhejj.perspektive import hu.webhejj.perspektive.testmodel.AbstractClass +import hu.webhejj.perspektive.testmodel.ComplexDataClass import hu.webhejj.perspektive.testmodel.GenericDataClass import hu.webhejj.perspektive.testmodel.GenericDataClass2 import hu.webhejj.perspektive.testmodel.ListContainer import hu.webhejj.perspektive.testmodel.MapContainer import hu.webhejj.perspektive.testmodel.SubClass -import hu.webhejj.perspektive.testmodel.TestJavaModel +import hu.webhejj.perspektive.testmodel.JavaBeanModel +import hu.webhejj.perspektive.testmodel.JavaStaticModel import org.junit.jupiter.api.Test import java.io.File class TestModelTest { - - private val targetDir = File("build/ktuml/") + private val targetDir = File("build/perspektive/") @Test - fun testModel() { + fun `data class`() { val classDiagram = ClassDiagram() - classDiagram.scanKClass(SubClass::class) - classDiagram.scanKClass(GenericDataClass2::class) - classDiagram.renderWithPlantUml(File(targetDir, "testModel.plantuml")) + classDiagram.scanKClass(ComplexDataClass::class) + classDiagram.renderWithPlantUml(File(targetDir, "data-class.plantuml")) } @Test - fun testTypeParameters() { + fun `generic data class`() { val classDiagram = ClassDiagram() classDiagram.scanKClass(GenericDataClass::class) - classDiagram.renderWithPlantUml(File(targetDir, "testTypeParameters.plantuml")) + classDiagram.renderWithPlantUml(File(targetDir, "generic-data-class.plantuml")) } @Test - fun testListFields() { + fun subclass() { + val classDiagram = ClassDiagram() + classDiagram.scanKClass(SubClass::class) + classDiagram.scanKClass(GenericDataClass2::class) + classDiagram.renderWithPlantUml(File(targetDir, "subclass.plantuml")) + } + + @Test + fun `list container`() { val classDiagram = ClassDiagram() classDiagram.scanKClass(ListContainer::class) - classDiagram.renderWithPlantUml(File(targetDir, "testListFields.plantuml")) + classDiagram.renderWithPlantUml(File(targetDir, "list-container.plantuml")) } @Test - fun testMapFields() { + fun `map container`() { val classDiagram = ClassDiagram() classDiagram.scanKClass(MapContainer::class) - classDiagram.renderWithPlantUml(File(targetDir, "testMapFields.plantuml")) + classDiagram.renderWithPlantUml(File(targetDir, "map-container.plantuml")) } @Test - fun testAbstractClass() { + fun `abstract class`() { val classDiagram = ClassDiagram() classDiagram.scanKClass(AbstractClass::class) - classDiagram.renderWithPlantUml(File(targetDir, "testAbstractClass.plantuml")) + classDiagram.renderWithPlantUml(File(targetDir, "abstract-class.plantuml")) + } + + @Test + fun `java static`() { + val classDiagram = ClassDiagram() + classDiagram.scanKClass(JavaStaticModel::class) + classDiagram.renderWithPlantUml(File(targetDir, "java-static.plantuml")) } @Test - fun testStatic() { + fun `java bean`() { val classDiagram = ClassDiagram() - classDiagram.scanKClass(TestJavaModel::class) - classDiagram.renderWithPlantUml(File(targetDir, "testStatic.plantuml")) + classDiagram.scanKClass(JavaBeanModel::class) + classDiagram.renderWithPlantUml(File(targetDir, "java-bean.plantuml")) } } diff --git a/src/test/kotlin/hu/webhejj/perspektive/testmodel/TestModel.kt b/src/test/kotlin/hu/webhejj/perspektive/testmodel/TestModel.kt index ec662ae..37d0eb9 100644 --- a/src/test/kotlin/hu/webhejj/perspektive/testmodel/TestModel.kt +++ b/src/test/kotlin/hu/webhejj/perspektive/testmodel/TestModel.kt @@ -42,6 +42,19 @@ data class SimpleDataClass( val field: String, ) + +data class ComplexDataClass( + val readonly: String, + var writable: String, + val nullable: String?, + val withDefault: String = "default", +) { + fun method(): String { + return "function" + } +} + + data class GenericDataClass( val key: K, val value: V,