__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)