added PlantUML generator script
Python script to generate Class-/Package-Diagram in PlantUML (.puml)
This commit is contained in:
parent
d88f3ddad0
commit
ba31941d9b
1 changed files with 367 additions and 0 deletions
367
puml_generator.py
Normal file
367
puml_generator.py
Normal file
|
@ -0,0 +1,367 @@
|
|||
__author__ = "7987847, Werner"
|
||||
__email__ = "s5260822@stud.uni-frankfurt.de"
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
def get_java_files(root_dir:str) -> list:
|
||||
"""
|
||||
Get all Java files in the root_dir and its subdirectories.
|
||||
|
||||
:param root_dir: The root directory to search for Java files.
|
||||
:return: A list of relative file paths to the Java files.
|
||||
"""
|
||||
java_files = []
|
||||
for dirpath, dirnames, filenames in os.walk(root_dir):
|
||||
for filename in filenames:
|
||||
if filename.endswith('.java'):
|
||||
# Create relative file path from the root_dir
|
||||
relative_path = os.path.relpath(os.path.join(dirpath, filename), root_dir)
|
||||
java_files.append(relative_path)
|
||||
print(java_files)
|
||||
return java_files
|
||||
|
||||
|
||||
def get_package_slug(java_file:str) -> str:
|
||||
"""
|
||||
Get the base package slug from a Java file.
|
||||
|
||||
:param java_file: The relative file path to the Java file.
|
||||
:return: The base package slug.
|
||||
"""
|
||||
# open the file and read the first line
|
||||
with open(java_file, "r") as file:
|
||||
line = file.readline()
|
||||
# extract the package name from the first line
|
||||
if line.startswith("package "):
|
||||
return line.split(" ")[1].replace(";", "").strip()
|
||||
return ""
|
||||
|
||||
|
||||
def is_line_field_declaration(line:str) -> bool:
|
||||
"""
|
||||
Check if a line is a field declaration.
|
||||
|
||||
:param line: The line to check.
|
||||
:return: True if the line is a field declaration, False otherwise.
|
||||
"""
|
||||
if "public" not in line and "private" not in line and "protected" not in line:
|
||||
return
|
||||
field_pattern = re.compile(r'^\s*(public|private|protected|\s)*\s*(static|final|\s)*\s*(\w[\w\d]*)\s+(\w[\w\d]*)\s*(=\s*[^;]*)?;')
|
||||
match = field_pattern.match(line.strip())
|
||||
return match is not None
|
||||
|
||||
|
||||
def get_field_visibility(line):
|
||||
"""
|
||||
Checks if the line is a field declaration and returns its visibility (+, -, or #).
|
||||
"""
|
||||
field_pattern = re.compile(r'^\s*(public|private|protected|\s)*\s*(static|final|\s)*\s*(\w[\w\d]*)\s+(\w[\w\d]*)\s*(=\s*[^;]*)?;')
|
||||
match = field_pattern.match(line.strip())
|
||||
if match:
|
||||
print(match.groups())
|
||||
# Check for the access modifier in the line and return the corresponding symbol
|
||||
split_line = line.split(" ")
|
||||
if "public" in split_line:
|
||||
return "+"
|
||||
elif "private" in split_line:
|
||||
return "-"
|
||||
elif "protected" in split_line:
|
||||
return "#"
|
||||
else:
|
||||
# If there's no access modifier, assume package-private (default) -> return '-'
|
||||
return "-"
|
||||
return None # Return None if it's not a field declaration
|
||||
|
||||
|
||||
def is_line_method_declaration(line:str) -> bool:
|
||||
"""
|
||||
Check if a line is a method declaration.
|
||||
|
||||
:param line: The line to check.
|
||||
:return: True if the line is a method declaration, False otherwise.
|
||||
"""
|
||||
method_pattern = re.compile(r'^\s*(public|private|protected|\s)*\s*(static|final|\s)*\s*(\w[\w\d]*)\s+(\w[\w\d]*)\s*\(.*\)\s*{')
|
||||
print(line.strip())
|
||||
match = method_pattern.match(line.strip())
|
||||
print(match)
|
||||
return match is not None
|
||||
|
||||
|
||||
def get_method_visibility(line:str) -> str:
|
||||
"""
|
||||
Checks if the line is a method declaration and returns its visibility (+, -, or #).
|
||||
"""
|
||||
method_pattern = re.compile(r'^\s*(public|private|protected|\s)*\s*(static|final|\s)*\s*(\w[\w\d]*)\s+(\w[\w\d]*)\s*\(.*\)\s*{')
|
||||
match = method_pattern.match(line.strip())
|
||||
if match:
|
||||
# Check for the access modifier in the line and return the corresponding symbol
|
||||
access_modifier = line.strip().split(" ")[0]
|
||||
if "public" in access_modifier:
|
||||
return "+"
|
||||
elif "private" in access_modifier:
|
||||
return "-"
|
||||
elif "protected" in access_modifier:
|
||||
return "#"
|
||||
else:
|
||||
# If there's no access modifier, assume package-private (default) -> return '-'
|
||||
return "-"
|
||||
return None # Return None if it's not a method declaration
|
||||
|
||||
|
||||
class JavaClass:
|
||||
def __init__(self,
|
||||
class_name,
|
||||
class_package,
|
||||
puml_content,
|
||||
uml_relations,
|
||||
class_fields,
|
||||
class_methods):
|
||||
self.class_name = class_name
|
||||
self.class_package = class_package
|
||||
self.puml_content = puml_content
|
||||
self.uml_relations = uml_relations
|
||||
self.class_fields = class_fields
|
||||
self.class_methods = class_methods
|
||||
|
||||
def set_puml_content(self, puml_content):
|
||||
self.puml_content = puml_content
|
||||
|
||||
def get_class_name(self):
|
||||
return self.class_name
|
||||
|
||||
def get_class_package(self):
|
||||
return self.class_package
|
||||
|
||||
def get_puml_content(self):
|
||||
return self.puml_content
|
||||
|
||||
def get_uml_relations(self):
|
||||
return self.uml_relations
|
||||
|
||||
def get_class_fields(self):
|
||||
return self.class_fields
|
||||
|
||||
def get_class_methods(self):
|
||||
return self.class_methods
|
||||
|
||||
|
||||
def gen_puml_code_from_class(java_class:JavaClass, no_pkgs:bool=False) -> str:
|
||||
"""
|
||||
Generate PlantUML code from a JavaClass object.
|
||||
|
||||
:param java_class: The JavaClass object.
|
||||
:param no_pkgs: Do not generate package visualisation in the PlantUML diagram.
|
||||
:return: The PlantUML code as a string.
|
||||
"""
|
||||
puml_code = ""
|
||||
class_name = java_class.get_class_name()
|
||||
class_package = java_class.get_class_package()
|
||||
class_fields = java_class.get_class_fields()
|
||||
class_methods = java_class.get_class_methods()
|
||||
uml_relations = java_class.get_uml_relations()
|
||||
|
||||
if not no_pkgs:
|
||||
puml_code += f"package {class_package + " {"}\n"
|
||||
|
||||
puml_code += f"class \"{class_name + "\" as " + class_package + "." + class_name + " {"}\n"
|
||||
|
||||
for field, visibility in class_fields.items():
|
||||
puml_code += f" {visibility} {field}\n"
|
||||
|
||||
for method, visibility in class_methods.items():
|
||||
puml_code += f" {visibility} {method}()\n"
|
||||
|
||||
puml_code += "}\n"
|
||||
|
||||
if not no_pkgs:
|
||||
puml_code += "}\n"
|
||||
|
||||
for related_class, relation in uml_relations.items():
|
||||
if not related_class == "*":
|
||||
puml_code += f"{class_package + "." + class_name} {relation}-- {related_class}\n"
|
||||
|
||||
java_class.set_puml_content(puml_code)
|
||||
|
||||
return java_class
|
||||
|
||||
|
||||
def get_field_name_from_line(line:str) -> str:
|
||||
"""
|
||||
Get the field name from a field declaration line.
|
||||
|
||||
:param line: The field declaration line.
|
||||
:return: The field name.
|
||||
"""
|
||||
return line.strip().split("=")[0].strip().split(" ")[-1].replace(";", "")
|
||||
|
||||
|
||||
def get_method_name_from_line(line:str) -> str:
|
||||
"""
|
||||
Get the method name from a method declaration line.
|
||||
|
||||
:param line: The method declaration line.
|
||||
:return: The method name.
|
||||
"""
|
||||
return line.strip().split("(")[0].split(" ")[-1]
|
||||
|
||||
|
||||
def class_to_puml(filename:str) -> JavaClass:
|
||||
imported_classes = {}
|
||||
puml_content = []
|
||||
uml_relations = {}
|
||||
class_fields = {}
|
||||
class_methods = {}
|
||||
class_name = filename.split("/")[-1].replace(".java", "")
|
||||
class_package = get_package_slug(filename)
|
||||
|
||||
reached_class = False
|
||||
class_getter = False
|
||||
class_setter = False
|
||||
|
||||
next_line_getter = False
|
||||
next_line_setter = False
|
||||
|
||||
# open file
|
||||
with open(filename, "r") as javafile:
|
||||
for javaline in javafile:
|
||||
javaline = javaline.strip()
|
||||
if next_line_getter:
|
||||
if is_line_field_declaration(javaline):
|
||||
field_name = get_field_name_from_line(javaline)
|
||||
print("FIELD NAME:", field_name)
|
||||
class_methods[f"get{field_name.capitalize()}"] = get_field_visibility(javaline)
|
||||
next_line_getter = False
|
||||
elif is_line_method_declaration(javaline):
|
||||
method_name = javaline.split(" ")[-1].replace("()", "")
|
||||
class_methods[method_name] = get_method_visibility(javaline)
|
||||
next_line_getter = False
|
||||
if next_line_setter:
|
||||
if is_line_field_declaration(javaline):
|
||||
field_name = get_field_name_from_line(javaline)
|
||||
class_methods[f"set{field_name.capitalize()}"] = get_field_visibility(javaline)
|
||||
next_line_setter = False
|
||||
elif is_line_method_declaration(javaline):
|
||||
method_name = get_method_name_from_line(javaline)
|
||||
class_methods[method_name] = get_method_visibility(javaline)
|
||||
next_line_setter = False
|
||||
elif javaline.startswith("import"):
|
||||
importline_package_slug = javaline.strip().split(" ")[1].replace(";", "")
|
||||
imported_classes[importline_package_slug.split(".")[-1]] = importline_package_slug
|
||||
|
||||
print(javaline)
|
||||
if javaline.startswith("@"):
|
||||
if "@Getter" in javaline and "@Setter" in javaline:
|
||||
if not reached_class:
|
||||
class_getter = True
|
||||
class_setter = True
|
||||
else:
|
||||
next_line_getter = True
|
||||
next_line_setter = True
|
||||
elif "@Getter" in javaline:
|
||||
if not reached_class:
|
||||
class_getter = True
|
||||
else:
|
||||
next_line_getter = True
|
||||
elif "@Setter" in javaline:
|
||||
if not reached_class:
|
||||
class_setter = True
|
||||
else:
|
||||
next_line_setter = True
|
||||
elif " class " in javaline:
|
||||
reached_class = True
|
||||
elif is_line_field_declaration(javaline):
|
||||
print(javaline)
|
||||
visibility = get_field_visibility(javaline)
|
||||
field_name = get_field_name_from_line(javaline)
|
||||
class_fields[field_name] = visibility
|
||||
|
||||
if class_getter:
|
||||
class_methods[f"get{field_name.capitalize()}"] = visibility
|
||||
if class_setter:
|
||||
class_methods[f"set{field_name.capitalize()}"] = visibility
|
||||
|
||||
elif is_line_method_declaration(javaline):
|
||||
visibility = get_method_visibility(javaline)
|
||||
method_name = get_method_name_from_line(javaline)
|
||||
class_methods[method_name] = visibility
|
||||
|
||||
for class_package_slug in imported_classes:
|
||||
if not class_package_slug in uml_relations:
|
||||
uml_relations[class_package_slug] = "<"
|
||||
|
||||
java_class = JavaClass(
|
||||
class_name,
|
||||
class_package,
|
||||
puml_content,
|
||||
uml_relations,
|
||||
class_fields,
|
||||
class_methods)
|
||||
|
||||
return gen_puml_code_from_class(java_class)
|
||||
|
||||
|
||||
def generate_puml(root_dir:str=".", no_pkgs:bool=False) -> None:
|
||||
"""
|
||||
Generate a PlantUML diagram from the Java files in the root_dir.
|
||||
|
||||
:param root_dir: The root directory to search for Java files.
|
||||
:param no_pkgs: Do not generate package visualisation in the PlantUML diagram
|
||||
"""
|
||||
java_files = get_java_files(root_dir)
|
||||
if len(java_files) == 0:
|
||||
print("Error: No Java Files Found in this or any subsequent directory!")
|
||||
|
||||
base_package_slug = ""
|
||||
|
||||
if any("Main.java" in s for s in java_files):
|
||||
base_package_slug = get_package_slug(
|
||||
next((s for s in java_files if "Main.java" in s), None))
|
||||
else:
|
||||
base_package_slug = get_package_slug(java_files[0])
|
||||
|
||||
puml_code = ""
|
||||
|
||||
for class_file in java_files:
|
||||
puml_code += class_to_puml(class_file).get_puml_content()
|
||||
|
||||
# write the PlantUML code to a file
|
||||
with open(f"{root_dir}/generated_class_diagram.puml", "w") as puml_file:
|
||||
puml_file.write(f"@startuml \n title {base_package_slug}\n")
|
||||
puml_file.write(puml_code)
|
||||
puml_file.write("@enduml\n")
|
||||
|
||||
|
||||
|
||||
def get_arguments(argv: list) -> tuple:
|
||||
"""
|
||||
Get the root directory and optional flags from the command line arguments.
|
||||
|
||||
:param argv: The command line arguments.
|
||||
:return: A tuple containing the root directory and the no_pkgs flag.
|
||||
"""
|
||||
root_dir = "."
|
||||
if "--dir" in argv:
|
||||
root_dir = argv[argv.index("--dir") + 1]
|
||||
no_pkgs = "--no-pkgs" in argv
|
||||
return root_dir, no_pkgs
|
||||
|
||||
|
||||
def print_help() -> None:
|
||||
"""
|
||||
Print a help message.
|
||||
"""
|
||||
print("Usage: python puml_generator.py")
|
||||
print("Generates a PlantUML diagram from the Java files in the root_dir.")
|
||||
print("Optional flags:")
|
||||
print("--dir <root_dir> \t\t Specify the root directory for the Java files.")
|
||||
print("--help \t\t\t\t Print this help message.")
|
||||
print("--no-pkgs \t\t\t Do not generate package visualisation in the PlantUML diagram.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "-h" in sys.argv or "--help" in sys.argv:
|
||||
print_help()
|
||||
else:
|
||||
root_dir, no_pkgs = get_arguments(sys.argv)
|
||||
generate_puml(root_dir, no_pkgs)
|
Loading…
Add table
Add a link
Reference in a new issue