From ba31941d9b8c509df30e21b52ec768015e968863 Mon Sep 17 00:00:00 2001 From: Jonas_Jones Date: Tue, 4 Mar 2025 14:31:23 +0100 Subject: [PATCH] added PlantUML generator script Python script to generate Class-/Package-Diagram in PlantUML (.puml) --- puml_generator.py | 367 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 puml_generator.py diff --git a/puml_generator.py b/puml_generator.py new file mode 100644 index 0000000..aff97a6 --- /dev/null +++ b/puml_generator.py @@ -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 \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)